Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ca8c1fb60 | |||
| 92b0ec179f | |||
| 06f447459e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,7 +6,7 @@ deno.lock
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist_serve/
|
||||
# ts_bundled/ is committed (embedded frontend bundle)
|
||||
|
||||
# Development
|
||||
.nogit/
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-24 - 2.4.0 - feat(opsserver)
|
||||
serve embedded frontend bundle from committed ts_bundled instead of using external dist_serve directory
|
||||
|
||||
- Switch server to use bundledContent from committed ts_bundled bundle (base64ts) instead of pointing at a serveDir
|
||||
- Update bundler config to emit ./ts_bundled/bundle.ts with outputMode 'base64ts' and includeFiles mapping
|
||||
- Remove dist_serve from .gitignore and commit ts_bundled (embedded frontend bundle)
|
||||
- Bump devDependency @git.zone/tsbundle to ^2.8.4 and deno dependency @api.global/typedserver to ^8.3.1
|
||||
|
||||
## 2026-02-24 - 2.3.0 - feat(storage)
|
||||
add StorageManager and cache subsystem; integrate storage into ConnectionManager and GitopsApp, migrate legacy connections, and add tests
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/gitops",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"exports": "./mod.ts",
|
||||
"nodeModulesDir": "auto",
|
||||
"tasks": {
|
||||
@@ -13,13 +13,14 @@
|
||||
"@std/encoding": "jsr:@std/encoding@^1.0.10",
|
||||
"@api.global/typedrequest-interfaces": "npm:@api.global/typedrequest-interfaces@^3.0.19",
|
||||
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6",
|
||||
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.0",
|
||||
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1",
|
||||
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
|
||||
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1",
|
||||
"@apiclient.xyz/gitea": "npm:@apiclient.xyz/gitea@^1.0.3",
|
||||
"@apiclient.xyz/gitlab": "npm:@apiclient.xyz/gitlab@^2.0.3",
|
||||
"@push.rocks/smartmongo": "npm:@push.rocks/smartmongo@^5.1.0",
|
||||
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.0.15"
|
||||
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.0.15",
|
||||
"@push.rocks/smartsecret": "npm:@push.rocks/smartsecret@^1.0.1"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
|
||||
29544
dist_serve/bundle.js
Normal file
29544
dist_serve/bundle.js
Normal file
File diff suppressed because one or more lines are too long
33
dist_serve/index.html
Normal file
33
dist_serve/index.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="user-scalable=0, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"
|
||||
/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>GitOps</title>
|
||||
<link rel="preconnect" href="https://assetbroker.lossless.one/" crossorigin>
|
||||
<link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
|
||||
<style>
|
||||
html {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
body {
|
||||
position: relative;
|
||||
background: #000;
|
||||
margin: 0px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<p style="color: #fff; text-align: center; margin-top: 100px;">
|
||||
JavaScript is required to run the GitOps dashboard.
|
||||
</p>
|
||||
</noscript>
|
||||
</body>
|
||||
<script defer type="module" src="/bundle.js"></script>
|
||||
</html>
|
||||
@@ -3,11 +3,11 @@
|
||||
"bundles": [
|
||||
{
|
||||
"from": "./ts_web/index.ts",
|
||||
"to": "./dist_serve/bundle.js",
|
||||
"outputMode": "bundle",
|
||||
"to": "./ts_bundled/bundle.ts",
|
||||
"outputMode": "base64ts",
|
||||
"bundler": "esbuild",
|
||||
"production": true,
|
||||
"includeFiles": ["./html/index.html"]
|
||||
"includeFiles": [{"from": "./html/index.html", "to": "index.html"}]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -15,7 +15,8 @@
|
||||
"bundles": [
|
||||
{
|
||||
"from": "./ts_web/index.ts",
|
||||
"to": "./dist_serve/bundle.js",
|
||||
"to": "./ts_bundled/bundle.ts",
|
||||
"outputMode": "base64ts",
|
||||
"watchPatterns": ["./ts_web/**/*"],
|
||||
"triggerReload": true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/gitops",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"description": "GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs",
|
||||
"main": "mod.ts",
|
||||
"type": "module",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@design.estate/dees-element": "^2.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
"@git.zone/tsbundle": "^2.8.4",
|
||||
"@git.zone/tswatch": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BaseProvider, GiteaProvider, GitLabProvider } from '../ts/providers/ind
|
||||
import { ConnectionManager } from '../ts/classes/connectionmanager.ts';
|
||||
import { GitopsApp } from '../ts/classes/gitopsapp.ts';
|
||||
import { StorageManager } from '../ts/storage/index.ts';
|
||||
import * as smartsecret from '@push.rocks/smartsecret';
|
||||
|
||||
Deno.test('GiteaProvider instantiates correctly', () => {
|
||||
const provider = new GiteaProvider('test-id', 'https://gitea.example.com', 'test-token');
|
||||
@@ -20,7 +21,8 @@ Deno.test('GitLabProvider instantiates correctly', () => {
|
||||
|
||||
Deno.test('ConnectionManager instantiates correctly', () => {
|
||||
const storage = new StorageManager({ backend: 'memory' });
|
||||
const manager = new ConnectionManager(storage);
|
||||
const secret = new smartsecret.SmartSecret({ service: 'gitops-test' });
|
||||
const manager = new ConnectionManager(storage, secret);
|
||||
assertExists(manager);
|
||||
});
|
||||
|
||||
@@ -28,6 +30,7 @@ Deno.test('GitopsApp instantiates correctly', () => {
|
||||
const app = new GitopsApp();
|
||||
assertExists(app);
|
||||
assertExists(app.storageManager);
|
||||
assertExists(app.smartSecret);
|
||||
assertExists(app.connectionManager);
|
||||
assertExists(app.opsServer);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { assertEquals, assertExists } from 'https://deno.land/std@0.208.0/assert/mod.ts';
|
||||
import { StorageManager } from '../ts/storage/index.ts';
|
||||
import * as smartsecret from '@push.rocks/smartsecret';
|
||||
|
||||
Deno.test('StorageManager memory: set and get', async () => {
|
||||
const sm = new StorageManager({ backend: 'memory' });
|
||||
@@ -114,7 +115,8 @@ Deno.test('StorageManager filesystem: list keys', async () => {
|
||||
Deno.test('ConnectionManager with StorageManager: create and load', async () => {
|
||||
const { ConnectionManager } = await import('../ts/classes/connectionmanager.ts');
|
||||
const sm = new StorageManager({ backend: 'memory' });
|
||||
const cm = new ConnectionManager(sm);
|
||||
const secret = new smartsecret.SmartSecret({ service: 'gitops-test' });
|
||||
const cm = new ConnectionManager(sm, secret);
|
||||
await cm.init();
|
||||
|
||||
// Create a connection
|
||||
@@ -129,7 +131,7 @@ Deno.test('ConnectionManager with StorageManager: create and load', async () =>
|
||||
assertEquals(stored.id, conn.id);
|
||||
|
||||
// Create a new ConnectionManager and verify it loads the connection
|
||||
const cm2 = new ConnectionManager(sm);
|
||||
const cm2 = new ConnectionManager(sm, secret);
|
||||
await cm2.init();
|
||||
const conns = cm2.getConnections();
|
||||
assertEquals(conns.length, 1);
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/gitops',
|
||||
version: '2.3.0',
|
||||
version: '2.4.0',
|
||||
description: 'GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs'
|
||||
}
|
||||
|
||||
@@ -6,17 +6,21 @@ import type { StorageManager } from '../storage/index.ts';
|
||||
|
||||
const LEGACY_CONNECTIONS_FILE = './.nogit/connections.json';
|
||||
const CONNECTIONS_PREFIX = '/connections/';
|
||||
const KEYCHAIN_PREFIX = 'keychain:';
|
||||
|
||||
/**
|
||||
* Manages provider connections — persists each connection as an
|
||||
* individual JSON file via StorageManager.
|
||||
* individual JSON file via StorageManager. Tokens are stored in
|
||||
* the OS keychain (or encrypted file fallback) via SmartSecret.
|
||||
*/
|
||||
export class ConnectionManager {
|
||||
private connections: interfaces.data.IProviderConnection[] = [];
|
||||
private storageManager: StorageManager;
|
||||
private smartSecret: plugins.smartsecret.SmartSecret;
|
||||
|
||||
constructor(storageManager: StorageManager) {
|
||||
constructor(storageManager: StorageManager, smartSecret: plugins.smartsecret.SmartSecret) {
|
||||
this.storageManager = storageManager;
|
||||
this.smartSecret = smartSecret;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
@@ -51,6 +55,18 @@ export class ConnectionManager {
|
||||
for (const key of keys) {
|
||||
const conn = await this.storageManager.getJSON<interfaces.data.IProviderConnection>(key);
|
||||
if (conn) {
|
||||
if (conn.token.startsWith(KEYCHAIN_PREFIX)) {
|
||||
// Token is in keychain — retrieve it
|
||||
const realToken = await this.smartSecret.getSecret(conn.id);
|
||||
if (realToken) {
|
||||
conn.token = realToken;
|
||||
} else {
|
||||
logger.warn(`Could not retrieve token for connection ${conn.id} from keychain`);
|
||||
}
|
||||
} else if (conn.token && conn.token !== '***') {
|
||||
// Plaintext token found — auto-migrate to keychain
|
||||
await this.migrateTokenToKeychain(conn);
|
||||
}
|
||||
this.connections.push(conn);
|
||||
}
|
||||
}
|
||||
@@ -61,11 +77,33 @@ export class ConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a plaintext token to keychain storage.
|
||||
*/
|
||||
private async migrateTokenToKeychain(
|
||||
conn: interfaces.data.IProviderConnection,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.smartSecret.setSecret(conn.id, conn.token);
|
||||
// Save sentinel to JSON file
|
||||
const jsonConn = { ...conn, token: `${KEYCHAIN_PREFIX}${conn.id}` };
|
||||
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, jsonConn);
|
||||
logger.info(`Migrated token for connection "${conn.name}" to keychain`);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to migrate token for ${conn.id} to keychain: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async persistConnection(conn: interfaces.data.IProviderConnection): Promise<void> {
|
||||
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, conn);
|
||||
// Store real token in keychain
|
||||
await this.smartSecret.setSecret(conn.id, conn.token);
|
||||
// Save JSON with sentinel value
|
||||
const jsonConn = { ...conn, token: `${KEYCHAIN_PREFIX}${conn.id}` };
|
||||
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, jsonConn);
|
||||
}
|
||||
|
||||
private async removeConnection(id: string): Promise<void> {
|
||||
await this.smartSecret.deleteSecret(id);
|
||||
await this.storageManager.delete(`${CONNECTIONS_PREFIX}${id}.json`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import { ConnectionManager } from './connectionmanager.ts';
|
||||
import { OpsServer } from '../opsserver/index.ts';
|
||||
@@ -10,6 +11,7 @@ import { resolvePaths } from '../paths.ts';
|
||||
*/
|
||||
export class GitopsApp {
|
||||
public storageManager: StorageManager;
|
||||
public smartSecret: plugins.smartsecret.SmartSecret;
|
||||
public connectionManager: ConnectionManager;
|
||||
public opsServer: OpsServer;
|
||||
public cacheDb: CacheDb;
|
||||
@@ -21,7 +23,8 @@ export class GitopsApp {
|
||||
backend: 'filesystem',
|
||||
fsPath: paths.defaultStoragePath,
|
||||
});
|
||||
this.connectionManager = new ConnectionManager(this.storageManager);
|
||||
this.smartSecret = new plugins.smartsecret.SmartSecret({ service: 'gitops' });
|
||||
this.connectionManager = new ConnectionManager(this.storageManager, this.smartSecret);
|
||||
|
||||
this.cacheDb = CacheDb.getInstance({
|
||||
storagePath: paths.defaultTsmDbPath,
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import type { GitopsApp } from '../classes/gitopsapp.ts';
|
||||
import * as handlers from './handlers/index.ts';
|
||||
import { files as bundledFiles } from '../../ts_bundled/bundle.ts';
|
||||
|
||||
export class OpsServer {
|
||||
public gitopsAppRef: GitopsApp;
|
||||
@@ -22,11 +23,10 @@ export class OpsServer {
|
||||
}
|
||||
|
||||
public async start(port = 3000) {
|
||||
const absoluteServeDir = plugins.path.resolve('./dist_serve');
|
||||
this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
|
||||
domain: 'localhost',
|
||||
feedMetadata: undefined,
|
||||
serveDir: absoluteServeDir,
|
||||
bundledContent: bundledFiles,
|
||||
});
|
||||
|
||||
// Chain typedrouters
|
||||
|
||||
@@ -28,3 +28,7 @@ export { giteaClient, gitlabClient };
|
||||
import * as smartmongo from '@push.rocks/smartmongo';
|
||||
import * as smartdata from '@push.rocks/smartdata';
|
||||
export { smartmongo, smartdata };
|
||||
|
||||
// Secrets
|
||||
import * as smartsecret from '@push.rocks/smartsecret';
|
||||
export { smartsecret };
|
||||
|
||||
11
ts_bundled/bundle.ts
Normal file
11
ts_bundled/bundle.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/gitops',
|
||||
version: '2.3.0',
|
||||
version: '2.4.0',
|
||||
description: 'GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user