@wpelevator/tug 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +24 -22
  2. package/dist/agent-credential.d.ts +39 -0
  3. package/dist/agent-credential.js +135 -0
  4. package/dist/agent.d.ts +15 -9
  5. package/dist/agent.js +126 -104
  6. package/dist/backup-check.d.ts +11 -0
  7. package/dist/backup-check.js +10 -0
  8. package/dist/backup-config.d.ts +5 -0
  9. package/dist/backup-config.js +23 -3
  10. package/dist/backup-destination.d.ts +1 -1
  11. package/dist/backup-destination.js +1 -1
  12. package/dist/backup-restore.js +28 -8
  13. package/dist/backup-scheduler.d.ts +7 -1
  14. package/dist/backup-scheduler.js +21 -6
  15. package/dist/cli-build.js +70 -4
  16. package/dist/cli-config.d.ts +9 -5
  17. package/dist/cli-config.js +89 -34
  18. package/dist/cli-install.js +1 -1
  19. package/dist/cli-live-transport.d.ts +3 -2
  20. package/dist/cli-live-transport.js +9 -5
  21. package/dist/cli.js +276 -142
  22. package/dist/compose.d.ts +3 -1
  23. package/dist/compose.js +14 -2
  24. package/dist/control-credential.d.ts +12 -0
  25. package/dist/control-credential.js +46 -0
  26. package/dist/control.d.ts +18 -1
  27. package/dist/control.js +586 -202
  28. package/dist/credentials.d.ts +111 -0
  29. package/dist/{tokens.js → credentials.js} +70 -54
  30. package/dist/db.d.ts +20 -5
  31. package/dist/db.js +3 -3
  32. package/dist/diagnostics.d.ts +44 -0
  33. package/dist/diagnostics.js +65 -0
  34. package/dist/ingress.js +4 -3
  35. package/dist/main.js +35 -0
  36. package/dist/normalize.d.ts +6 -0
  37. package/dist/normalize.js +16 -2
  38. package/dist/pki.d.ts +119 -0
  39. package/dist/pki.js +339 -0
  40. package/dist/protocol.d.ts +53 -22
  41. package/dist/status-report.d.ts +11 -0
  42. package/dist/status-report.js +147 -0
  43. package/dist/templates/agent.env +1 -3
  44. package/dist/templates/control-compose.yml +4 -3
  45. package/dist/templates/control-install.sh +8 -2
  46. package/dist/tls-server.d.ts +11 -0
  47. package/dist/tls-server.js +23 -0
  48. package/package.json +10 -8
  49. package/dist/tokens.d.ts +0 -91
package/dist/compose.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare function bindPorts(ports: PortEntry[], ip: string, loopbackPort?:
23
23
  * A service with no placement nodes runs on every node in `nodes`.
24
24
  */
25
25
  export declare function loopbackIngressServices(state: DesiredState, node: string, ingressNodes: readonly string[], nodes: readonly string[]): string[];
26
- /** Where `composeForNode` binds published ports that name no host IP. */
26
+ /** Where the agent's container definitions bind published ports that name no host IP. */
27
27
  export interface PortBind {
28
28
  address: string;
29
29
  loopback?: ReadonlySet<string>;
@@ -34,6 +34,8 @@ export declare function addressBoundServices(compose: ComposeFile, names: Iterab
34
34
  export declare function isRolling(svc: ComposeService | undefined): boolean;
35
35
  /** Service keys the agent can create a container from; anything else is rejected with its name. */
36
36
  export declare const SUPPORTED_SERVICE_KEYS: readonly string[];
37
+ /** Lower-cased ingress hostnames a Compose definition claims, by `service` name. */
38
+ export declare function ingressHostsOf(compose: ComposeFile): Map<string, string>;
37
39
  export declare function validateCompose(compose: ComposeFile | undefined, nodes?: string[]): asserts compose is ComposeFile;
38
40
  export interface BuildDesiredStateOptions {
39
41
  project: string;
package/dist/compose.js CHANGED
@@ -279,6 +279,16 @@ function validateTopLevel(compose, errors) {
279
279
  errors.push(`volumes.${key}: ${field} is not supported`);
280
280
  }
281
281
  }
282
+ /** Lower-cased ingress hostnames a Compose definition claims, by `service` name. */
283
+ export function ingressHostsOf(compose) {
284
+ const hosts = new Map();
285
+ for (const [name, svc] of Object.entries(compose.services || {})) {
286
+ const host = svc['x-tug']?.ingress?.host;
287
+ if (typeof host === 'string')
288
+ hosts.set(host.toLowerCase(), name);
289
+ }
290
+ return hosts;
291
+ }
282
292
  export function validateCompose(compose, nodes = []) {
283
293
  if (!compose?.services || typeof compose.services !== 'object')
284
294
  throw new Error('compose file must define services');
@@ -290,7 +300,9 @@ export function validateCompose(compose, nodes = []) {
290
300
  const placement = lp.placement || {};
291
301
  const selected = placement.nodes || [];
292
302
  const hasLabels = Array.isArray(placement.labels) && placement.labels.length > 0;
293
- const isGlobal = lp.global === true || placement.global === true;
303
+ const isGlobal = lp.global === true;
304
+ if ('global' in placement)
305
+ errors.push(`${name}: x-tug.placement.global is not supported; use x-tug.global`);
294
306
  if (nodes.length > 1 && selected.length === 0 && !hasLabels && !isGlobal)
295
307
  errors.push(`${name}: multi-node deployments require x-tug.placement.nodes or placement.labels`);
296
308
  if (lp.replicas &&
@@ -313,7 +325,7 @@ export function buildDesiredState({ project, version, compose, revision, default
313
325
  for (const [name, src] of Object.entries(compose.services || {})) {
314
326
  const lp = src['x-tug'] || {};
315
327
  let placement = lp.placement || (defaultNode ? { nodes: [defaultNode] } : {});
316
- const global = lp.global === true || placement.global === true;
328
+ const global = lp.global === true;
317
329
  const labels = placement.labels || [];
318
330
  if (nodes && (!placement.nodes || !placement.nodes.length) && (labels.length || global)) {
319
331
  const matched = nodes
@@ -0,0 +1,12 @@
1
+ export interface IssueArgs {
2
+ name: string;
3
+ role: string;
4
+ replace: boolean;
5
+ }
6
+ export declare function parseIssueArgs(argv: string[]): IssueArgs;
7
+ /**
8
+ * Host-side issuance (`docker exec ... control issue-credential`), independent of the API and of any running listener. It
9
+ * is create-only unless `replace` is set, which replaces the named credential's key; a running control closes the old
10
+ * key's sessions within a second. Returns the credential once; control keeps no private-key copy.
11
+ */
12
+ export declare function issueHostCredential(args: IssueArgs): Promise<string>;
@@ -0,0 +1,46 @@
1
+ import path from 'node:path';
2
+ import { openDb } from "./db.js";
3
+ import { Pki, encodeCredential } from "./pki.js";
4
+ import { ROLES, createCredential, isRole, parseCredentialRequest } from "./credentials.js";
5
+ export function parseIssueArgs(argv) {
6
+ const args = { name: 'root', role: 'admin', replace: false };
7
+ for (let i = 0; i < argv.length; i++) {
8
+ if (argv[i] === '--name')
9
+ args.name = argv[++i];
10
+ else if (argv[i] === '--role')
11
+ args.role = argv[++i];
12
+ else if (argv[i] === '--replace')
13
+ args.replace = true;
14
+ else
15
+ throw new Error(`unknown argument ${argv[i]}`);
16
+ }
17
+ if (!isRole(args.role))
18
+ throw new Error(`--role must be one of ${ROLES.join(', ')}`);
19
+ return args;
20
+ }
21
+ /**
22
+ * Host-side issuance (`docker exec ... control issue-credential`), independent of the API and of any running listener. It
23
+ * is create-only unless `replace` is set, which replaces the named credential's key; a running control closes the old
24
+ * key's sessions within a second. Returns the credential once; control keeps no private-key copy.
25
+ */
26
+ export async function issueHostCredential(args) {
27
+ const dataDir = process.env.TUG_DATA_DIR || './data';
28
+ const publicUrl = process.env.TUG_PUBLIC_URL;
29
+ if (!publicUrl)
30
+ throw new Error('TUG_PUBLIC_URL is required');
31
+ const db = await openDb(path.join(dataDir, 'tug.db'));
32
+ try {
33
+ const fresh = !db.get('SELECT 1 FROM operator_credentials') && !db.get('SELECT 1 FROM nodes');
34
+ const pki = await Pki.load(path.join(dataDir, 'pki'), fresh);
35
+ const request = parseCredentialRequest({ name: args.name, role: args.role });
36
+ const created = await createCredential(db, pki, new URL(publicUrl).origin, request, 'host', {
37
+ replace: args.replace,
38
+ });
39
+ if (args.replace)
40
+ db.run('INSERT INTO events(id,level,type,message,metadata_json,created_at,actor) VALUES(?,?,?,?,?,?,?)', `evt_${Date.now().toString(36)}`, 'info', 'credential.recover', `credential ${args.name} replaced on the control host`, '{}', new Date().toISOString(), 'host');
41
+ return encodeCredential(created.credential);
42
+ }
43
+ finally {
44
+ db.close();
45
+ }
46
+ }
package/dist/control.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import express from 'express';
3
3
  import { resolveImage } from './registry.ts';
4
+ import type { Request } from 'express';
4
5
  import { saveImageArchive, inspectImage, validateImageArchive } from './artifacts.ts';
5
6
  import https from 'node:https';
6
7
  import http from 'node:http';
7
8
  import { openDb } from './db.ts';
9
+ import type { Db } from './db.ts';
8
10
  import type { NodeRow } from './db.ts';
9
11
  import { buildImage, run as runCommand } from './build.ts';
10
12
  declare module 'express-serve-static-core' {
@@ -25,5 +27,20 @@ export declare function createApp(options?: {
25
27
  openDatabase?: typeof openDb;
26
28
  /** Minimum time between event prunes triggered by agent reports; default one hour. */ eventPruneIntervalMs?: number;
27
29
  /** Override the live open/agent startup timeout in milliseconds. */ liveStartTimeoutMs?: number;
30
+ /** Credential-free HTTPS origin clients use to reach control; defaults to `TUG_PUBLIC_URL`. */ publicUrl?: string;
31
+ /**
32
+ * Replaces certificate-based identity resolution. Only for in-process tests that skip TLS; production never sets it.
33
+ * It returns the principal URI and key hash a connection would have presented.
34
+ */
35
+ identity?: (req: Request, db: Db) => {
36
+ uri: string;
37
+ keyHash: string;
38
+ } | null;
28
39
  }): Promise<import("express-serve-static-core").Express>;
29
- export declare function listen(app: express.Express, port?: number): Promise<http.Server | https.Server>;
40
+ /** Attaches the agent and operator WebSocket upgrade handling to `server`; `listen` uses it on the mTLS listener. */
41
+ export declare function attachAgentStream(app: express.Express, server: http.Server | https.Server): void;
42
+ /**
43
+ * Starts the single mTLS listener. Chain validation rejects missing or foreign client certificates during the handshake;
44
+ * the secureConnection gate then checks current database state, and every request checks it again.
45
+ */
46
+ export declare function listen(app: express.Express, port?: number): Promise<https.Server>;