@kici-dev/orchestrator 0.14.2 → 0.16.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 (176) hide show
  1. package/dist/__test-helpers__/peer-mutual-auth.d.ts +42 -0
  2. package/dist/__test-helpers__/scratch-db.d.ts +16 -0
  3. package/dist/agent/host-roster.d.ts +100 -37
  4. package/dist/agent/registry.d.ts +1 -8
  5. package/dist/app-on-error.d.ts +1 -1
  6. package/dist/app.d.ts +4 -2
  7. package/dist/artifacts/artifact-store.d.ts +0 -2
  8. package/dist/audit/access-log.d.ts +1 -1
  9. package/dist/cache/source-cache.d.ts +1 -1
  10. package/dist/cache/user-cache.d.ts +0 -28
  11. package/dist/cli/api-client.d.ts +27 -1
  12. package/dist/cli/commands/access-log.d.ts +1 -1
  13. package/dist/cli/commands/cold-store-impl.d.ts +7 -84
  14. package/dist/cli/commands/db.d.ts +1 -0
  15. package/dist/cli/commands/host.d.ts +3 -0
  16. package/dist/cli/commands/orchestrator-service/status.d.ts +2 -0
  17. package/dist/cli/commands/org.d.ts +12 -0
  18. package/dist/cli/commands/scaler.d.ts +14 -0
  19. package/dist/cli/commands/secret.d.ts +1 -45
  20. package/dist/cli/commands/service-health.d.ts +21 -13
  21. package/dist/cli/commands/shared/cli-action.d.ts +17 -0
  22. package/dist/cli/commands/shared/db.d.ts +5 -5
  23. package/dist/cli/commands/shared/org-id.d.ts +5 -0
  24. package/dist/cli/commands/shared/service-verbs.d.ts +19 -0
  25. package/dist/cli/commands/shared/table.d.ts +3 -0
  26. package/dist/cli/commands/shared/unbound-context-warning.d.ts +2 -22
  27. package/dist/cli/commands/source-manifest.d.ts +3 -3
  28. package/dist/cli/commands/source.d.ts +23 -0
  29. package/dist/cli/commands/trust-policy.d.ts +7 -31
  30. package/dist/cli/join.d.ts +2 -2
  31. package/dist/cli/kici-admin.d.ts +6 -1
  32. package/dist/cli/local-github-ingress-url.d.ts +6 -0
  33. package/dist/cli/root-option-routing.d.ts +46 -0
  34. package/dist/cli/webhook-url-notes.d.ts +15 -0
  35. package/dist/cli.js +10667 -8461
  36. package/dist/cluster/coordinator-credential.d.ts +21 -0
  37. package/dist/cluster/coordinator.d.ts +2 -2
  38. package/dist/cluster/index.d.ts +4 -4
  39. package/dist/cluster/join-client.d.ts +23 -21
  40. package/dist/cluster/join-handler.d.ts +24 -16
  41. package/dist/cluster/join-protocol-v2.d.ts +84 -0
  42. package/dist/cluster/join-token.d.ts +78 -56
  43. package/dist/cluster/peer-auth-coordinator.d.ts +16 -2
  44. package/dist/cluster/peer-client.d.ts +99 -5
  45. package/dist/cluster/peer-crypto.d.ts +60 -3
  46. package/dist/cluster/peer-discovery.d.ts +67 -0
  47. package/dist/cluster/peer-forget.d.ts +4 -9
  48. package/dist/cluster/peer-handler.d.ts +28 -6
  49. package/dist/cluster/peer-registry.d.ts +15 -1
  50. package/dist/cluster/peer-request-waiters.d.ts +26 -0
  51. package/dist/cluster/scaler-orphans-peer.d.ts +4 -9
  52. package/dist/cluster/scaler-reload-fanout.d.ts +29 -0
  53. package/dist/cluster/scaler-reload-peer.d.ts +16 -0
  54. package/dist/cluster/worker-cluster-settings.d.ts +1 -0
  55. package/dist/cold-store/load-access-log-range.d.ts +3 -3
  56. package/dist/cold-store/tables/access-log.d.ts +4 -12
  57. package/dist/cold-store/tables/event-log.d.ts +8 -19
  58. package/dist/cold-store/tables/execution-jobs.d.ts +7 -24
  59. package/dist/cold-store/tables/execution-runs.d.ts +6 -20
  60. package/dist/cold-store/tables/execution-steps.d.ts +8 -26
  61. package/dist/cold-store/tables/secret-audit-log.d.ts +4 -11
  62. package/dist/config/env-overlay.d.ts +3 -8
  63. package/dist/config/reload.d.ts +9 -2
  64. package/dist/config/schema.d.ts +16 -2
  65. package/dist/config/types.d.ts +6 -2
  66. package/dist/config.d.ts +22 -17
  67. package/dist/contexts/held-runs.d.ts +3 -6
  68. package/dist/dashboard/cursor.d.ts +12 -0
  69. package/dist/dashboard/handler.d.ts +5 -12
  70. package/dist/db/deferred-index-build.d.ts +42 -0
  71. package/dist/db/deferred-indexes.d.ts +3 -37
  72. package/dist/db/migrations/100_held_runs_hold_type_vocabulary.d.ts +3 -5
  73. package/dist/db/migrations/157_host_roster_identity_source.d.ts +21 -0
  74. package/dist/db/migrations/158_normalize_legacy_values.d.ts +4 -0
  75. package/dist/db/repos/org-ids-repo.d.ts +87 -0
  76. package/dist/db/schema-snapshot.generated.d.ts +3 -0
  77. package/dist/db/types.d.ts +21 -24
  78. package/dist/fault-injection-types.d.ts +0 -6
  79. package/dist/git/dispatch-git-auth.d.ts +6 -12
  80. package/dist/github-app-name-refresher/github-app-name-refresher.d.ts +25 -13
  81. package/dist/helpers/json.d.ts +3 -0
  82. package/dist/index.d.ts +1 -1
  83. package/dist/index.js +357 -216
  84. package/dist/metrics/prometheus.d.ts +0 -5
  85. package/dist/oidc/id-token-claims.d.ts +1 -1
  86. package/dist/oidc/resolve-signer.d.ts +2 -4
  87. package/dist/orchestrator-core.d.ts +10 -3
  88. package/dist/pipeline/global-eval-round.d.ts +2 -26
  89. package/dist/pipeline/install-secrets-resolver.d.ts +0 -3
  90. package/dist/pipeline/manual-schedule.d.ts +1 -7
  91. package/dist/provenance/dispatch-context.d.ts +5 -7
  92. package/dist/provenance/pending-attestations-repo.d.ts +1 -1
  93. package/dist/provenance/trust-root.d.ts +4 -5
  94. package/dist/provenance/verify-deferred-capture.d.ts +3 -3
  95. package/dist/provider-registry.d.ts +13 -40
  96. package/dist/providers/github/manifest.d.ts +58 -7
  97. package/dist/providers/universal-git/ssh-auth.d.ts +0 -5
  98. package/dist/reporting/check-run-reporter.d.ts +8 -21
  99. package/dist/reporting/check-run-summary.d.ts +2 -2
  100. package/dist/reporting/execution-tracker.d.ts +1 -1
  101. package/dist/reporting/log-chunk-sink.d.ts +1 -1
  102. package/dist/reporting/run-summary-fields.d.ts +25 -0
  103. package/dist/reporting/s3-log-storage.d.ts +2 -2
  104. package/dist/routes/admin-access-log.d.ts +2 -9
  105. package/dist/routes/admin-backends.d.ts +2 -9
  106. package/dist/routes/admin-cluster-name.d.ts +2 -8
  107. package/dist/routes/admin-cluster-settings.d.ts +2 -8
  108. package/dist/routes/admin-config.d.ts +0 -3
  109. package/dist/routes/admin-contexts.d.ts +3 -11
  110. package/dist/routes/admin-db.d.ts +2 -9
  111. package/dist/routes/admin-env.d.ts +33 -0
  112. package/dist/routes/admin-event-dlq.d.ts +2 -9
  113. package/dist/routes/admin-event-inspect.d.ts +2 -9
  114. package/dist/routes/admin-event-log.d.ts +2 -9
  115. package/dist/routes/admin-events.d.ts +3 -13
  116. package/dist/routes/admin-held-runs.d.ts +2 -8
  117. package/dist/routes/admin-list-limit.d.ts +3 -0
  118. package/dist/routes/admin-maintenance.d.ts +2 -9
  119. package/dist/routes/admin-orchestrator-drain.d.ts +2 -9
  120. package/dist/routes/admin-org-settings.d.ts +2 -8
  121. package/dist/routes/admin-orgs.d.ts +27 -0
  122. package/dist/routes/admin-peer-forget.d.ts +2 -9
  123. package/dist/routes/admin-queue-execution.d.ts +3 -10
  124. package/dist/routes/admin-registrations.d.ts +3 -14
  125. package/dist/routes/admin-runs.d.ts +3 -14
  126. package/dist/routes/admin-scaler-orphans.d.ts +2 -9
  127. package/dist/routes/admin-scaler-reload.d.ts +41 -0
  128. package/dist/routes/admin-scheduled-jobs.d.ts +2 -9
  129. package/dist/routes/admin-signing-keys.d.ts +2 -9
  130. package/dist/routes/admin-sources.d.ts +28 -32
  131. package/dist/routes/admin-trust-policy.d.ts +2 -8
  132. package/dist/routes/admin.d.ts +29 -30
  133. package/dist/routes/cluster-join.d.ts +13 -0
  134. package/dist/routes/github-webhook.d.ts +13 -11
  135. package/dist/scaler/bare-metal-backend.d.ts +42 -1
  136. package/dist/scaler/config.d.ts +12 -84
  137. package/dist/scaler/file-reload.d.ts +48 -0
  138. package/dist/scaler/manager.d.ts +47 -33
  139. package/dist/scaler/scaler-state-store.d.ts +5 -0
  140. package/dist/scaler/types.d.ts +8 -9
  141. package/dist/secrets/job-secret-gate.d.ts +2 -5
  142. package/dist/secrets/secret-resolver.d.ts +2 -4
  143. package/dist/security/trust-policy-gate.d.ts +0 -4
  144. package/dist/server.d.ts +6 -17
  145. package/dist/server.js +10933 -10836
  146. package/dist/sources/source-manager.d.ts +0 -24
  147. package/dist/sources/webhook-url-resolvers.d.ts +60 -0
  148. package/dist/standalone.js +10457 -10060
  149. package/dist/worker/reroute-dispatch.d.ts +1 -0
  150. package/dist/worker/reroute-spawn-control.d.ts +1 -3
  151. package/dist/worker-core.d.ts +9 -2
  152. package/dist/ws/agent-handler.d.ts +3 -3
  153. package/dist/ws/dashboard-access.d.ts +57 -0
  154. package/dist/ws/dashboard-backends-handler.d.ts +1 -6
  155. package/dist/ws/dashboard-context-handler.d.ts +10 -15
  156. package/dist/ws/dashboard-fleet-handler.d.ts +4 -3
  157. package/dist/ws/dashboard-fleet-write-handler.d.ts +6 -5
  158. package/dist/ws/dashboard-global-workflows-handler.d.ts +1 -6
  159. package/dist/ws/dashboard-registrations-handler.d.ts +1 -7
  160. package/dist/ws/platform-client.d.ts +94 -327
  161. package/installer-image-digests.json +3 -3
  162. package/package.json +4 -4
  163. package/sbom.spdx.json +53 -53
  164. package/dist/cache/legacy-prefixes.d.ts +0 -19
  165. package/dist/cli/commands/agent-service/logs.d.ts +0 -14
  166. package/dist/cli/commands/agent-service/restart.d.ts +0 -12
  167. package/dist/cli/commands/agent-service/start.d.ts +0 -12
  168. package/dist/cli/commands/agent-service/stop.d.ts +0 -12
  169. package/dist/cli/commands/agent-service/uninstall.d.ts +0 -13
  170. package/dist/cli/commands/cache.d.ts +0 -51
  171. package/dist/cli/commands/orchestrator-service/logs.d.ts +0 -14
  172. package/dist/cli/commands/orchestrator-service/restart.d.ts +0 -12
  173. package/dist/cli/commands/orchestrator-service/start.d.ts +0 -12
  174. package/dist/cli/commands/orchestrator-service/stop.d.ts +0 -12
  175. package/dist/cli/commands/orchestrator-service/uninstall.d.ts +0 -13
  176. package/dist/git/inherited-secret.d.ts +0 -29
@@ -17,6 +17,27 @@ export declare enum CredentialIssueReason {
17
17
  FileMissing = "credential-file-missing",
18
18
  FileMismatch = "credential-file-mismatch"
19
19
  }
20
+ /** What a coordinator's own cluster database says about a credential a peer rejected. */
21
+ export declare enum RejectionCorroboration {
22
+ /** An unrevoked, unexpired row holds this credential: the peer's rejection is not this cluster's view. */
23
+ HeldValid = "held-valid",
24
+ /** No live row holds it: the rejection is right, and the file goes. */
25
+ NotHeld = "not-held",
26
+ /** The database could not be read: keep the file and retry on the next attempt. */
27
+ Unreadable = "unreadable"
28
+ }
29
+ /**
30
+ * The `corroborateRejection` callback a coordinator's `PeerAuthCoordinator`
31
+ * runs before it deletes its credential file after a peer rejected it. Any
32
+ * dialled endpoint can send a rejection, so the coordinator's own database
33
+ * decides: an expired or revoked row does not hold the credential, and the file
34
+ * then goes so the self-issue path reconciles.
35
+ */
36
+ export declare function coordinatorRejectionCorroborator(opts: {
37
+ db: Kysely<Database>;
38
+ instanceId: string;
39
+ logger?: Logger;
40
+ }): (credential: string) => Promise<RejectionCorroboration>;
20
41
  export declare const COORDINATOR_CREDENTIAL_REVOKED_MESSAGE = "Peer credential for this coordinator was revoked; not issuing a new one";
21
42
  export type CoordinatorCredentialStore = Pick<PeerCredentialStore, 'save' | 'findUnrevokedByInstanceId' | 'findLatestRevokedByInstanceId' | 'retireSelfIssued'>;
22
43
  export interface EnsureCoordinatorCredentialOptions {
@@ -212,8 +212,8 @@ export declare class RunCoordinator {
212
212
  * timer: whichever fires first removes the tracking entry.
213
213
  * - `final: false` — the worker is retrying. Re-arm the window for one more
214
214
  * attempt plus the backoff; treat the `maxAttempts`-th such relay as final.
215
- * - no verdict — an older worker that retries without a bound. The window
216
- * armed at the reroute keeps running unchanged.
215
+ * - no verdict — a repeated report of one failed spawn. The window armed at
216
+ * the reroute keeps running unchanged.
217
217
  *
218
218
  * Two guards make the re-dispatch safe. First, source provenance: the failure
219
219
  * must come from `fromPeerId === tracked.peerId` (the authenticated connection
@@ -6,11 +6,11 @@
6
6
  */
7
7
  export { PeerRegistry } from './peer-registry.js';
8
8
  export type { PeerInfo, PeerAgentInfo, PeerRegistryOptions } from './peer-registry.js';
9
- export { PeerClient } from './peer-client.js';
9
+ export { PeerClient, PeerDialOrigin } from './peer-client.js';
10
10
  export type { PeerClientOptions } from './peer-client.js';
11
- export { PeerAuthCoordinator } from './peer-auth-coordinator.js';
12
- export { coordinatorSelfIssuer } from './coordinator-credential.js';
13
- export type { AuthDecision, RejectionAction } from './peer-auth-coordinator.js';
11
+ export { PeerAuthCoordinator, RejectionAction } from './peer-auth-coordinator.js';
12
+ export { coordinatorRejectionCorroborator, coordinatorSelfIssuer, RejectionCorroboration, } from './coordinator-credential.js';
13
+ export type { AuthDecision } from './peer-auth-coordinator.js';
14
14
  export { createPeerHandler } from './peer-handler.js';
15
15
  export type { PeerHandlerDeps } from './peer-handler.js';
16
16
  export { PeerCredentialStore, readCredentialFile, writeCredentialFile, } from './peer-credentials.js';
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Join client for zero-knowledge cluster bootstrap.
2
+ * Join client: the joiner side of `kici-admin join` (join protocol v2).
3
3
  *
4
- * Provides the joiner-side logic for `kici-admin join`:
5
- * 1. Connect to Platform relay (WS) or direct peer (HTTP POST)
6
- * 2. Send join.request with the join token
7
- * 3. Receive join.response with encrypted config bundle
8
- * 4. Decrypt bundle using token-derived AES-256-GCM key
9
- * 5. Write the decrypted config as an env file the orchestrator boots from
4
+ * 1. Build a join.request from the token: its routing part, a one-time X25519
5
+ * public key, a nonce, and a proof that this host holds the token secret.
6
+ * The secret itself stays on this host.
7
+ * 2. Send it through the Platform relay (WS) or directly to a peer (HTTP POST).
8
+ * 3. Check the existing orchestrator's proof in the join.response, then open the
9
+ * configuration bundle sealed to the one-time key.
10
+ * 4. Write the bundle as the env file the orchestrator boots from.
10
11
  *
11
- * The token carries routing info (cleartext for Platform relay) and a secret
12
- * (used for HKDF key derivation). Only the joiner and the token creator
13
- * can derive the encryption key -- the Platform relay sees only ciphertext.
12
+ * A relay sees the routing part, the public keys, the nonces, the proofs and the
13
+ * ciphertext; none of them lets it read or change the bundle.
14
14
  */
15
15
  import { type JoinRequest, type JoinResponse } from '@kici-dev/engine';
16
16
  import type { ConfigBundle } from './join-handler.js';
@@ -27,10 +27,12 @@ interface JoinClientOptions {
27
27
  }
28
28
  /** Default path the join writes its env file to. */
29
29
  export declare const DEFAULT_JOIN_ENV_FILE = "./kici-orchestrator.env";
30
- /**
31
- * Decrypt a base64-encoded encrypted config bundle using a derived encryption key.
32
- */
33
- export declare function decryptAndParseBundle(encryptedB64: string, encryptionKey: Buffer): ConfigBundle;
30
+ /** How long a join waits for the join.response, through either transport. */
31
+ export declare const JOIN_TIMEOUT_MS = 30000;
32
+ /** POST target for `--peer`, keeping any base path of the peer URL. */
33
+ export declare function joinEndpointUrl(peerUrl: string): string;
34
+ /** One-line operator message for a refused join. */
35
+ export declare function describeJoinRefusal(response: JoinResponse): string;
34
36
  /**
35
37
  * Startup environment variable each `ConfigBundle.storage` field is delivered
36
38
  * through.
@@ -67,19 +69,19 @@ export declare class JoinClient {
67
69
  private readonly options;
68
70
  constructor(options: JoinClientOptions);
69
71
  /**
70
- * Execute the join flow:
71
- * 1. Send join.request with token to Platform relay or direct peer
72
- * 2. Receive join.response with encrypted config bundle
73
- * 3. Decrypt bundle using token-derived key
74
- * 4. Write the env file the orchestrator boots from
72
+ * Execute the join flow: send the v2 join.request, check the existing
73
+ * orchestrator's proof, open the sealed bundle, and write the env file. Nothing
74
+ * is written when the response fails its proof.
75
75
  */
76
76
  join(): Promise<void>;
77
77
  /**
78
- * Join via Platform relay: connect WS, authenticate, send join.request, receive join.response.
78
+ * Join via Platform relay: connect WS, authenticate with the API key, send the
79
+ * join.request, receive the join.response.
79
80
  */
80
81
  joinViaPlatform(request: JoinRequest): Promise<JoinResponse>;
81
82
  /**
82
- * Join via direct peer: POST to peer's join endpoint.
83
+ * Join via direct peer: POST the join.request to the peer's join endpoint.
84
+ * There is no fallback to an earlier join protocol.
83
85
  */
84
86
  joinViaPeer(request: JoinRequest): Promise<JoinResponse>;
85
87
  }
@@ -1,17 +1,14 @@
1
1
  /**
2
- * Join handler for processing join requests from new orchestrators.
2
+ * Join handler: answers join protocol v2 requests from new orchestrators, whether they
3
+ * arrive through the Platform relay or on POST /api/v1/cluster/join.
3
4
  *
4
- * Supports both Platform relay and direct peer transport. When a new orchestrator
5
- * sends a join.request with a kici_join_v1 token, this handler:
6
- * 1. Validates the token (exists in DB, not expired, not consumed)
7
- * 2. Builds a config bundle (DB URL, S3 config, secrets key, cluster ID)
8
- * 3. Encrypts the bundle with the token-derived AES-256-GCM key
9
- * 4. Consumes the token (one-time use)
10
- * 5. Returns the encrypted bundle to the joiner
11
- *
12
- * The Platform relay sees only routing metadata and ciphertext -- zero-knowledge.
5
+ * It finds the join token row by the request's routing fields, verifies the joiner's
6
+ * proof against each candidate row, claims the row by hash, and seals the configuration
7
+ * bundle (database URL, object storage, secrets key, cluster ID) to the joiner's
8
+ * one-time key. A version-1 frame (one that carries the token) is refused before any
9
+ * lookup.
13
10
  */
14
- import type { JoinRequest, JoinResponse } from '@kici-dev/engine';
11
+ import { type JoinResponse } from '@kici-dev/engine';
15
12
  import type { Kysely } from 'kysely';
16
13
  import type { ClusterIdentity } from './cluster-identity.js';
17
14
  import type { SharedConfigStore } from '../config/shared-store.js';
@@ -20,6 +17,11 @@ interface JoinHandlerDeps {
20
17
  sharedConfigStore: SharedConfigStore;
21
18
  clusterIdentity: ClusterIdentity;
22
19
  databaseUrl: string;
20
+ /**
21
+ * This orchestrator's own secrets key (`KICI_SECRET_KEY` or its key file). The
22
+ * bundle carries it when the shared configuration holds no `secrets.key`.
23
+ */
24
+ secretKey?: string;
23
25
  }
24
26
  /**
25
27
  * Config bundle distributed to new orchestrators via join token.
@@ -40,18 +42,24 @@ export interface ConfigBundle {
40
42
  secretKey?: string;
41
43
  clusterId: string;
42
44
  }
45
+ /** The answer to a failure the joiner cannot act on; the detail goes to the log. */
46
+ export declare const JOIN_INTERNAL_ERROR_MESSAGE = "Internal error";
43
47
  export declare class JoinHandler {
44
48
  private readonly deps;
45
49
  private readonly tokenManager;
46
50
  constructor(deps: JoinHandlerDeps);
47
51
  /**
48
- * Handle a join request from a new orchestrator.
49
- * Validates token, builds config bundle, encrypts with token-derived key, consumes token.
50
- * Echoes messageId from request for Platform relay correlation.
52
+ * Answer one join.request frame. Every frame gets exactly one answer, a refused
53
+ * version-1 or malformed one included. Echoes the request's messageId for Platform
54
+ * relay correlation. A failure the joiner cannot act on (a database outage) carries
55
+ * no errorCode and a generic message.
51
56
  */
52
- handleJoinRequest(request: JoinRequest): Promise<JoinResponse>;
57
+ handleJoinRequest(raw: unknown): Promise<JoinResponse>;
58
+ private answer;
53
59
  /**
54
- * Build the config bundle from SharedConfig + local config.
60
+ * Build the config bundle from SharedConfig + local config. The secrets key is
61
+ * the shared configuration's `secrets.key`, else this orchestrator's own key, so
62
+ * a joined orchestrator decrypts what the cluster encrypted.
55
63
  */
56
64
  buildConfigBundle(): Promise<ConfigBundle>;
57
65
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Join protocol v2: the key schedule, transcripts, proofs and sealed bundle.
3
+ *
4
+ * The joiner proves it holds the join token, and the existing orchestrator proves the
5
+ * same back, without either side sending the token secret. Both key from
6
+ * H = SHA-256(secret), the value `join_tokens.token_hash` stores. The bundle is sealed
7
+ * under a key that mixes an ephemeral X25519 agreement with a token-derived binding
8
+ * key, so a relay that records every frame can neither open nor forge it.
9
+ *
10
+ * Key schedule (HKDF-SHA256, salt "kici-join-v2"):
11
+ * - requestKey = HKDF(H, info "joiner-proof"), responseKey = HKDF(H, info "server-proof"),
12
+ * bindingKey = HKDF(H, info "bundle-binding")
13
+ * - T1 = SHA-256("kici-join-v2/request" || lp(routing) || lp(E_j) || lp(n_j))
14
+ * - T2 = SHA-256(T1 || lp(E_s) || lp(n_s))
15
+ * - joinerProof = HMAC(requestKey, T1), serverProof = HMAC(responseKey, T2)
16
+ * - bundleKey = HKDF(ikm X25519(e_s, E_j), salt bindingKey, info "kici-join-v2/bundle" || T2)
17
+ * - encryptedBundle = AES-256-GCM(bundleKey, aad T2), packed IV || tag || ciphertext
18
+ *
19
+ * lp(x) is a 4-byte big-endian length, then x.
20
+ */
21
+ import { type KeyObject } from 'node:crypto';
22
+ import { type EcdhKeyPair } from './peer-crypto.js';
23
+ export declare const JOIN_PROTOCOL_V2: 2;
24
+ export declare const SERVER_PROOF_MISMATCH_MESSAGE = "The join response did not come from an orchestrator that holds this token";
25
+ export interface JoinKeys {
26
+ requestKey: Buffer;
27
+ responseKey: Buffer;
28
+ bindingKey: Buffer;
29
+ }
30
+ /** Derive the proof and binding keys from H = SHA-256(secret), as 32 raw bytes. */
31
+ export declare function deriveJoinKeys(tokenHash: Buffer): JoinKeys;
32
+ export declare function requestTranscript(routingB64: string, joinerPublicKey: Buffer, joinerNonce: Buffer): Buffer;
33
+ export declare function responseTranscript(requestT: Buffer, serverPublicKey: Buffer, serverNonce: Buffer): Buffer;
34
+ export declare const computeJoinerProof: (keys: JoinKeys, t1: Buffer) => string;
35
+ export declare const verifyJoinerProof: (keys: JoinKeys, t1: Buffer, presented: string) => boolean;
36
+ export declare const computeServerProof: (keys: JoinKeys, t2: Buffer) => string;
37
+ export declare const verifyServerProof: (keys: JoinKeys, t2: Buffer, presented: string) => boolean;
38
+ /** Load a DER SPKI public key, refusing anything but X25519. */
39
+ export declare function loadX25519PublicKey(der: Buffer): KeyObject;
40
+ /** A low-order peer key yields an all-zero agreement, which carries no secret. */
41
+ export declare function assertNonZeroSharedSecret(shared: Buffer): void;
42
+ export declare function agreeSharedSecret(privateKey: KeyObject, peerPublicKeyDer: Buffer): Buffer;
43
+ export declare function deriveBundleKey(shared: Buffer, bindingKey: Buffer, responseT: Buffer): Buffer;
44
+ /** AES-256-GCM with a random 12-byte IV; packed IV || tag || ciphertext. */
45
+ export declare function sealBundle(plaintext: string, key: Buffer, aad: Buffer): Buffer;
46
+ export declare function openBundle(sealed: Buffer, key: Buffer, aad: Buffer): string;
47
+ export interface JoinRequestV2Fields {
48
+ joinProtocol: typeof JOIN_PROTOCOL_V2;
49
+ routing: string;
50
+ joinerPublicKey: string;
51
+ joinerNonce: string;
52
+ joinerProof: string;
53
+ }
54
+ /** What the joiner keeps between its request and the response. Never leaves the host. */
55
+ export interface JoinerState {
56
+ keys: JoinKeys;
57
+ requestT: Buffer;
58
+ ephemeral: EcdhKeyPair;
59
+ }
60
+ /** Joiner: build the request fields from the token's routing part and H. */
61
+ export declare function createJoinRequest(input: {
62
+ routingB64: string;
63
+ tokenHash: Buffer;
64
+ }): {
65
+ fields: JoinRequestV2Fields;
66
+ state: JoinerState;
67
+ };
68
+ export interface SealedJoinResponse {
69
+ joinProtocol: typeof JOIN_PROTOCOL_V2;
70
+ serverPublicKey: string;
71
+ serverNonce: string;
72
+ serverProof: string;
73
+ encryptedBundle: string;
74
+ }
75
+ /** Existing orchestrator: seal the bundle to the joiner's one-time key, with a fresh key of its own. */
76
+ export declare function sealJoinResponse(input: {
77
+ keys: JoinKeys;
78
+ requestT: Buffer;
79
+ joinerPublicKey: Buffer;
80
+ bundle: object;
81
+ }): SealedJoinResponse;
82
+ /** Joiner: check the server proof first, then open the bundle. */
83
+ export declare function openJoinResponse(state: JoinerState, response: SealedJoinResponse): unknown;
84
+ //# sourceMappingURL=join-protocol-v2.d.ts.map
@@ -1,18 +1,19 @@
1
1
  /**
2
- * Join token manager for zero-knowledge cluster bootstrap.
2
+ * Join tokens: creation, lookup and claim.
3
3
  *
4
4
  * Token format: kici_join_v1.<base64url(routing_json)>.<random_256bit_hex>
5
- * - Routing JSON: { orgId, routingKey, expiry } (cleartext for Platform relay routing)
6
- * - Secret: 32 random bytes as hex (used for key derivation)
5
+ * - Routing JSON: { orgId, routingKey, expiry, role }. A lookup key only: the
6
+ * row the token was stored as decides the role and routing a claim returns.
7
+ * - Secret: 32 random bytes as hex. The database stores token_hash =
8
+ * SHA-256(secret), never the secret.
7
9
  *
8
- * Key derivation:
9
- * - encryption_key = HKDF-SHA256(secret, salt="kici-join-encrypt", info="v1", length=32)
10
- * - validation_hash = SHA-256(secret) (stored in DB for lookup)
11
- *
12
- * Config bundle encryption: AES-256-GCM with random 12-byte IV.
13
- * Wire format: <12-byte IV><16-byte auth tag><ciphertext>
10
+ * A row is found by its hash (claimByHash, readByHash), or by its routing fields
11
+ * plus a proof the caller checks against each candidate hash
12
+ * (resolveLiveTokenByRouting).
14
13
  */
14
+ import { OrchRole } from '@kici-dev/engine';
15
15
  import { Kysely } from 'kysely';
16
+ import { z } from 'zod';
16
17
  /**
17
18
  * Silence the module-level logger (e.g. when a CLI is emitting JSON on stdout
18
19
  * and a stray log line would break the contract).
@@ -20,16 +21,41 @@ import { Kysely } from 'kysely';
20
21
  export declare function silenceJoinTokenLogger(): void;
21
22
  export declare const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used. Create a new token with: kici-admin peer create-token";
22
23
  export declare const TOKEN_EXPIRED_MESSAGE = "Join token has expired. Create a new token with: kici-admin peer create-token";
23
- interface TokenRouting {
24
+ export declare const INVALID_JOIN_TOKEN_MESSAGE = "Invalid join token";
25
+ /** Rows sharing one routing triple that a lookup will try; bounds HMAC work per request. */
26
+ export declare const JOIN_TOKEN_ROUTING_CANDIDATE_CAP = 16;
27
+ export type JoinTokenRole = OrchRole;
28
+ export interface TokenRouting {
24
29
  orgId: string;
25
30
  routingKey: string;
26
31
  expiry: number;
27
- role: 'coordinator' | 'worker';
32
+ role: JoinTokenRole;
28
33
  }
29
- interface DerivedKeys {
30
- encryptionKey: Buffer;
31
- validationHash: string;
34
+ /** The routing fields a presented token claims. A lookup key only: the row decides. */
35
+ export type JoinRoutingClaim = Pick<TokenRouting, 'orgId' | 'routingKey' | 'expiry'>;
36
+ /** A consumed (or same-instance re-validated) join token, as the database records it. */
37
+ export interface ClaimedJoinToken {
38
+ /** Hex SHA-256 of the token secret (`join_tokens.token_hash`). */
39
+ tokenHash: string;
40
+ /** From `routing_info`, never from the presented token. */
41
+ routing: TokenRouting;
42
+ /** From the `role` column. */
43
+ role: JoinTokenRole;
32
44
  }
45
+ export declare const ResolvedJoinTokenStatus: z.ZodEnum<{
46
+ expired: "expired";
47
+ live: "live";
48
+ unknown: "unknown";
49
+ }>;
50
+ export type ResolvedJoinTokenStatus = z.infer<typeof ResolvedJoinTokenStatus>;
51
+ export type ResolvedJoinToken = {
52
+ status: typeof ResolvedJoinTokenStatus.enum.live;
53
+ tokenHash: string;
54
+ } | {
55
+ status: typeof ResolvedJoinTokenStatus.enum.expired;
56
+ } | {
57
+ status: typeof ResolvedJoinTokenStatus.enum.unknown;
58
+ };
33
59
  interface JoinTokenManagerDeps {
34
60
  db: Kysely<any>;
35
61
  }
@@ -44,39 +70,44 @@ export declare class JoinTokenManager {
44
70
  orgId: string;
45
71
  routingKey: string;
46
72
  createdBy: string;
47
- role?: 'coordinator' | 'worker';
73
+ role?: JoinTokenRole;
48
74
  expiryMs?: number;
49
75
  }): Promise<string>;
50
76
  /**
51
- * Atomically validate and consume a token in one DB round-trip.
52
- *
53
- * Single UPDATE with `WHERE token_hash = ? AND consumed_at IS NULL AND
54
- * expires_at > NOW()` — only one caller can win the claim across a
55
- * shared-DB multi-coordinator mesh. The winner gets `{ routing, keys }`;
56
- * every other concurrent caller gets `TOKEN_ALREADY_USED_MESSAGE` and is
57
- * expected to fall into the idempotent recovery branch in
58
- * `peer-handler.ts` (which serialises credential issuance via the
59
- * `peer_credentials_active_uniq` partial unique index).
77
+ * Peer token mode: claim the row the presented token's secret hashes to. Role and
78
+ * routing come from the row (`claimByHash`), never from the token's routing part.
60
79
  *
61
- * On a 0-row claim, a follow-up SELECT disambiguates not-found / expired
62
- * / already-used / reusable-by-same-instance so callers can branch on the
63
- * specific reason — the recovery path in peer-handler.ts keys on the
64
- * "already used" string specifically. The follow-up only fires on the
65
- * unhappy path.
80
+ * Self-healing reuse: a join token is re-consumable by the same joining peer
81
+ * (`peerInstanceId`) until its `expires_at`. A peer that lost its credential
82
+ * re-presents the still-valid join token already in its env, and the coordinator
83
+ * issues a fresh credential without an operator action. Reuse is bounded by both
84
+ * `expires_at` and the consuming instanceId, so it never widens a leaked token's
85
+ * usefulness beyond the instance that first consumed it.
86
+ */
87
+ validateAndConsumeToken(token: string, consumedBy: string, peerInstanceId: string): Promise<ClaimedJoinToken>;
88
+ /**
89
+ * Find the row whose proof the caller accepts, among rows whose routing matches
90
+ * the claimed routing. Consumption state is not checked here; `claimByHash` owns it.
91
+ * At most JOIN_TOKEN_ROUTING_CANDIDATE_CAP rows, newest first, are offered.
92
+ */
93
+ resolveLiveTokenByRouting(claimed: JoinRoutingClaim, accepts: (tokenHash: string) => boolean): Promise<ResolvedJoinToken>;
94
+ /**
95
+ * Atomically consume the row with this hash, or re-validate it for the instance
96
+ * that consumed it (self-healing reuse, bounded by expires_at). Routing and role
97
+ * come from the row.
66
98
  *
67
- * Self-healing reuse: a join token is re-consumable by the same joining
68
- * peer (`peerInstanceId`) until its `expires_at`. A peer that lost its
69
- * credential (transient outage / deleted credential file) re-presents the
70
- * still-valid join token already in its env; the coordinator accepts the
71
- * reuse and issues a fresh credential — no operator action, no cluster
72
- * redeploy. Reuse is bounded by BOTH `expires_at` AND the consuming
73
- * instanceId, so it never widens a leaked token's usefulness beyond the
74
- * instance that first consumed it.
99
+ * One UPDATE ... WHERE consumed_at IS NULL wins the claim across a shared-DB
100
+ * multi-coordinator mesh; every other concurrent caller gets
101
+ * TOKEN_ALREADY_USED_MESSAGE, the message peer-handler's idempotent retry keys on.
102
+ * On a 0-row claim, a follow-up SELECT tells not-found, expired, already-used and
103
+ * reusable-by-the-same-instance apart.
75
104
  */
76
- validateAndConsumeToken(token: string, consumedBy: string, peerInstanceId: string): Promise<{
105
+ claimByHash(tokenHash: string, consumedBy: string, consumerInstanceId: string): Promise<ClaimedJoinToken>;
106
+ /** Role and routing of the row with this hash, or null. */
107
+ readByHash(tokenHash: string): Promise<{
77
108
  routing: TokenRouting;
78
- keys: DerivedKeys;
79
- }>;
109
+ role: JoinTokenRole;
110
+ } | null>;
80
111
  }
81
112
  /**
82
113
  * Build a JoinTokenManager backed by its own connection pool to the given
@@ -104,23 +135,14 @@ export declare function isTokenAlreadyUsedError(err: unknown): boolean;
104
135
  */
105
136
  export declare function parseToken(token: string): {
106
137
  routing: TokenRouting;
138
+ routingB64: string;
107
139
  secretHex: string;
108
140
  };
109
- /**
110
- * Derive encryption key and validation hash from a token secret.
111
- * - encryptionKey: HKDF-SHA256 with salt="kici-join-encrypt", info="v1"
112
- * - validationHash: SHA-256 of secret (stored in DB for lookup)
113
- */
114
- export declare function deriveKeys(secret: Buffer): DerivedKeys;
115
- /**
116
- * Encrypt a config bundle with the derived encryption key.
117
- * Format: <12-byte IV><16-byte auth tag><ciphertext>
118
- */
119
- export declare function encryptBundle(plaintext: object, encryptionKey: Buffer): Buffer;
120
- /**
121
- * Decrypt a config bundle with the derived encryption key.
122
- * Expects format: <12-byte IV><16-byte auth tag><ciphertext>
123
- */
124
- export declare function decryptBundle(data: Buffer, encryptionKey: Buffer): object;
141
+ /** Hex SHA-256 of a token secret: the value `join_tokens.token_hash` stores. */
142
+ export declare function tokenHashOf(secretHex: string): string;
143
+ /** A log-safe tag for a token hash. Never log the hash itself: it is the join key root. */
144
+ export declare function tokenFingerprint(tokenHash: string): string;
145
+ /** Decode the base64url routing part of a token into its lookup fields. */
146
+ export declare function decodeJoinRouting(routingB64: string): JoinRoutingClaim;
125
147
  export {};
126
148
  //# sourceMappingURL=join-token.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { type CredentialFileData } from './peer-credentials.js';
2
- import { CoordinatorCredentialOutcome } from './coordinator-credential.js';
2
+ import { CoordinatorCredentialOutcome, RejectionCorroboration } from './coordinator-credential.js';
3
3
  export type AuthDecision = {
4
4
  mode: 'credential';
5
5
  credential: CredentialFileData;
@@ -10,13 +10,22 @@ export type AuthDecision = {
10
10
  } | {
11
11
  mode: 'no-auth';
12
12
  };
13
- export type RejectionAction = 'retry-credential' | 'rejoin';
13
+ /** What `reportRejection` did with the credential file. */
14
+ export declare enum RejectionAction {
15
+ /** A sibling refreshed the file: retry with the fresh credential. */
16
+ RetryCredential = "retry-credential",
17
+ /** The file was deleted: the next decision token-joins or self-issues. */
18
+ Rejoin = "rejoin",
19
+ /** This cluster's database holds the credential as valid (or is unreadable): the file stays. */
20
+ KeepCredential = "keep-credential"
21
+ }
14
22
  export declare class PeerAuthCoordinator {
15
23
  private readonly credentialFile;
16
24
  private readonly instanceId;
17
25
  private readonly joinToken?;
18
26
  private readonly joinWaitTimeoutMs;
19
27
  private readonly selfIssue?;
28
+ private readonly corroborateRejection?;
20
29
  /** The issuance in progress; concurrent sibling clients share it. */
21
30
  private selfIssueInFlight;
22
31
  /** An operator revoked this coordinator's credential: never issue again in this process. */
@@ -35,6 +44,11 @@ export declare class PeerAuthCoordinator {
35
44
  * credential and no join token. Only a coordinator wires it.
36
45
  */
37
46
  selfIssue?: () => Promise<CoordinatorCredentialOutcome>;
47
+ /**
48
+ * Asks this coordinator's own cluster database whether it holds a rejected
49
+ * credential as valid. Only a coordinator with a database wires it.
50
+ */
51
+ corroborateRejection?: (credential: string) => Promise<RejectionCorroboration>;
38
52
  });
39
53
  /** Run `fn` exclusively against the credential file. */
40
54
  private withLock;