@kici-dev/orchestrator 0.14.1 → 0.15.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/dist/__test-helpers__/peer-mutual-auth.d.ts +42 -0
  2. package/dist/agent/host-roster.d.ts +100 -37
  3. package/dist/app.d.ts +4 -2
  4. package/dist/cli/commands/host.d.ts +3 -0
  5. package/dist/cli/commands/source-manifest.d.ts +3 -3
  6. package/dist/cli/commands/source.d.ts +23 -0
  7. package/dist/cli/join.d.ts +2 -2
  8. package/dist/cli/kici-admin.d.ts +5 -0
  9. package/dist/cli/local-github-ingress-url.d.ts +6 -0
  10. package/dist/cli/root-option-routing.d.ts +46 -0
  11. package/dist/cli/webhook-url-notes.d.ts +15 -0
  12. package/dist/cli.js +1429 -885
  13. package/dist/cluster/coordinator-credential.d.ts +21 -0
  14. package/dist/cluster/index.d.ts +4 -4
  15. package/dist/cluster/join-client.d.ts +23 -21
  16. package/dist/cluster/join-handler.d.ts +24 -16
  17. package/dist/cluster/join-protocol-v2.d.ts +84 -0
  18. package/dist/cluster/join-token.d.ts +78 -56
  19. package/dist/cluster/peer-auth-coordinator.d.ts +16 -2
  20. package/dist/cluster/peer-client.d.ts +72 -4
  21. package/dist/cluster/peer-crypto.d.ts +60 -3
  22. package/dist/cluster/peer-discovery.d.ts +67 -0
  23. package/dist/cluster/peer-handler.d.ts +17 -5
  24. package/dist/cluster/peer-registry.d.ts +15 -1
  25. package/dist/config/schema.d.ts +14 -0
  26. package/dist/config/types.d.ts +4 -0
  27. package/dist/config.d.ts +9 -0
  28. package/dist/db/migrations/157_host_roster_identity_source.d.ts +21 -0
  29. package/dist/db/types.d.ts +16 -2
  30. package/dist/github-app-name-refresher/github-app-name-refresher.d.ts +25 -13
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +225 -81
  33. package/dist/providers/github/manifest.d.ts +58 -7
  34. package/dist/routes/admin-sources.d.ts +26 -23
  35. package/dist/routes/admin.d.ts +14 -17
  36. package/dist/routes/cluster-join.d.ts +13 -0
  37. package/dist/routes/github-webhook.d.ts +13 -11
  38. package/dist/scaler/bare-metal-backend.d.ts +42 -1
  39. package/dist/scaler/types.d.ts +7 -5
  40. package/dist/server.js +9040 -7631
  41. package/dist/sources/source-manager.d.ts +0 -24
  42. package/dist/sources/webhook-url-resolvers.d.ts +60 -0
  43. package/dist/standalone.js +9944 -8818
  44. package/dist/ws/dashboard-fleet-handler.d.ts +4 -3
  45. package/dist/ws/dashboard-fleet-write-handler.d.ts +7 -1
  46. package/dist/ws/platform-client.d.ts +18 -6
  47. package/installer-image-digests.json +3 -3
  48. package/package.json +4 -4
  49. package/sbom.spdx.json +53 -53
@@ -1,9 +1,18 @@
1
1
  /**
2
- * ECDH key exchange and session encryption for peer-to-peer channels.
2
+ * ECDH key exchange, mutual-v2 authentication and session encryption for
3
+ * peer-to-peer channels.
3
4
  *
4
5
  * Uses X25519 for key agreement and AES-256-GCM for message encryption.
5
- * Session keys are derived via HKDF-SHA256 from the ECDH shared secret
6
- * and a caller-provided nonce (to ensure per-session uniqueness).
6
+ * The handshake key (K_hs) is derived via HKDF-SHA256 from the ECDH shared
7
+ * secret and the server nonce; it protects the authentication exchange only.
8
+ *
9
+ * mutual-v2: both sides hash the handshake transcript (TH: both ephemeral keys,
10
+ * the nonce and the advertised schemes). The client proves the shared key (PSK:
11
+ * sha256 of the peer credential, or the join token hash) with an HMAC over TH,
12
+ * and the server answers with its own HMAC over TH and the client proof. Every
13
+ * frame after acceptance uses the application key (K_app), derived from K_hs
14
+ * and the PSK, so a side that skipped the proof check cannot read or write them.
15
+ * Every field is length-prefixed, so field boundaries are unambiguous.
7
16
  *
8
17
  * Wire format for encrypted messages (base64 encoded):
9
18
  * IV (12 bytes) || AuthTag (16 bytes) || Ciphertext
@@ -54,4 +63,52 @@ export declare function encryptMessage(plaintext: string, sessionKey: Buffer): s
54
63
  * @throws If the session key is wrong or the ciphertext has been tampered with
55
64
  */
56
65
  export declare function decryptMessage(encrypted: string, sessionKey: Buffer): string;
66
+ /** Length of the server nonce in `peer.hello`. */
67
+ export declare const HANDSHAKE_NONCE_BYTES = 32;
68
+ /** Length of a mutual-v2 proof (HMAC-SHA256). */
69
+ export declare const PEER_PROOF_BYTES = 32;
70
+ /** Encode each field as a 4-byte big-endian length followed by its bytes. */
71
+ export declare function lengthPrefixed(...fields: Array<Buffer | string>): Buffer;
72
+ export interface PeerTranscriptInput {
73
+ serverEphemeralPublicKey: Buffer;
74
+ serverNonce: Buffer;
75
+ /** The schemes exactly as the server advertised them, unknown ones included. */
76
+ authSchemes: readonly string[];
77
+ clientEphemeralPublicKey: Buffer;
78
+ }
79
+ /** TH: binds both ephemeral keys, the nonce and the advertised schemes. */
80
+ export declare function peerTranscriptHash(input: PeerTranscriptInput): Buffer;
81
+ export interface ClientProofInput {
82
+ psk: Buffer;
83
+ transcriptHash: Buffer;
84
+ mode: string;
85
+ clientInstanceId: string;
86
+ role: string;
87
+ protocolVersion: number;
88
+ /** The join token's routing segment in token mode; empty in credential mode. */
89
+ tokenRouting: string;
90
+ }
91
+ export declare function computeClientProof(input: ClientProofInput): Buffer;
92
+ export interface ServerProofInput {
93
+ psk: Buffer;
94
+ transcriptHash: Buffer;
95
+ clientProof: Buffer;
96
+ serverInstanceId: string;
97
+ grantedRole: string;
98
+ /** The credential the server issues in this response, or null when it issues none. */
99
+ sessionCredential: string | null;
100
+ }
101
+ export declare function computeServerProof(input: ServerProofInput): Buffer;
102
+ /** A proof as sent on the wire (64 hex characters), or null when malformed. */
103
+ export declare function decodeProof(hex: string): Buffer | null;
104
+ /** Constant-time comparison; a length mismatch is a mismatch, never a throw. */
105
+ export declare function proofMatches(expected: Buffer, presented: Buffer): boolean;
106
+ /** K_app: protects every frame after acceptance, and needs the PSK as well as K_hs. */
107
+ export declare function deriveAppKey(input: {
108
+ handshakeKey: Buffer;
109
+ psk: Buffer;
110
+ transcriptHash: Buffer;
111
+ clientProof: Buffer;
112
+ serverProof: Buffer;
113
+ }): Buffer;
57
114
  //# sourceMappingURL=peer-crypto.d.ts.map
@@ -0,0 +1,67 @@
1
+ import { OrchRole } from '@kici-dev/engine';
2
+ import { PeerDiscoveryMode } from '../config/schema.js';
3
+ /** Discovered clients plus pending candidates, far above any real coordinator count. */
4
+ export declare const MAX_DISCOVERED_PEER_CLIENTS = 64;
5
+ /** A discovered client not connected for this long is dropped; a returning peer is announced again. */
6
+ export declare const DISCOVERED_PEER_IDLE_PRUNE_MS: number;
7
+ /** What discovery needs from a peer client. */
8
+ export interface DiscoveryPeerClient {
9
+ readonly state: string;
10
+ connect(): void;
11
+ disconnect(): void;
12
+ }
13
+ /** Hooks a discovered client calls back. */
14
+ export interface DiscoveredClientHooks {
15
+ /** Mutual authentication succeeded with the announced instance id. */
16
+ onAuthenticated: () => void;
17
+ /** Mutual authentication failed; the client closed and will not reconnect. */
18
+ onMutualAuthFailed: () => void;
19
+ }
20
+ /** One peer the Platform announced. */
21
+ export interface PeerAnnouncement {
22
+ instanceId?: string;
23
+ address?: string | null;
24
+ orchRole?: OrchRole;
25
+ }
26
+ export interface PeerDiscoveryDeps {
27
+ mode: PeerDiscoveryMode;
28
+ selfInstanceId: string;
29
+ /** Every outbound peer client. Static clients sit under their URL until they authenticate. */
30
+ peerClients: Map<string, DiscoveryPeerClient>;
31
+ createClient: (address: string, expectedInstanceId: string, hooks: DiscoveredClientHooks) => DiscoveryPeerClient;
32
+ /** Minimum time between dials of an address whose last discovered dial failed mutual authentication. */
33
+ redialAfterFailureMs: number;
34
+ maxClients?: number;
35
+ idlePruneMs?: number;
36
+ now?: () => number;
37
+ }
38
+ export declare class PeerDiscovery {
39
+ private readonly deps;
40
+ private readonly discovered;
41
+ private readonly candidates;
42
+ /** Address -> when its last discovered dial failed mutual authentication. */
43
+ private readonly failedAt;
44
+ private readonly maxClients;
45
+ private readonly idlePruneMs;
46
+ private readonly now;
47
+ private timer;
48
+ constructor(deps: PeerDiscoveryDeps);
49
+ announce(peer: PeerAnnouncement): void;
50
+ /** A static client authenticated as this peer: discovery lets go of it. */
51
+ releaseToStatic(instanceId: string): void;
52
+ /** Drop discovered clients and candidates that stayed unconnected for the idle window. */
53
+ pruneIdle(): void;
54
+ start(): void;
55
+ stop(): void;
56
+ private size;
57
+ private dialDiscovered;
58
+ private dialCandidate;
59
+ /**
60
+ * The candidate authenticated. It runs before the candidate registers the
61
+ * peer, so the replaced client disconnects first.
62
+ */
63
+ private promote;
64
+ private drop;
65
+ private dropCandidate;
66
+ }
67
+ //# sourceMappingURL=peer-discovery.d.ts.map
@@ -1,13 +1,18 @@
1
1
  /**
2
2
  * WebSocket handler for incoming peer connections from other orchestrators.
3
3
  *
4
- * Accepts WS upgrade, performs ECDH key exchange for encrypted channel,
5
- * validates authentication via join token (first connect) or HMAC credential
6
- * proof (reconnection), registers the peer in PeerRegistry, and routes
7
- * messages bidirectionally. Sends periodic heartbeats to the connecting peer.
4
+ * Accepts WS upgrade, performs ECDH key exchange, and authenticates the peer
5
+ * with mutual-v2: `peer.hello` advertises the scheme, the peer proves its
6
+ * credential (or, on first join, its join token) with an HMAC over the
7
+ * handshake transcript, and this server answers with its own proof and
8
+ * switches to the application key. A join token never arrives here: the peer
9
+ * sends the token's routing segment, and the role and routing key come from
10
+ * the join_tokens row. A request in the scheme earlier releases used is refused
11
+ * with PEER_MUTUAL_AUTH_REQUIRED_REASON. Registers the peer in PeerRegistry and
12
+ * routes messages bidirectionally. Sends periodic heartbeats to the peer.
8
13
  */
9
14
  import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type JobProgressAck, type PeerScalerEvent, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerScalerOrphansRequest, type PeerForgetRequest, type PeerForgetResponse, type PeerScalerOrphansResponse, type PeerClusterSettingsRequest, type WorkerClusterSettings, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
10
- import type { PeerRegistry } from './peer-registry.js';
15
+ import { type PeerRegistry } from './peer-registry.js';
11
16
  import { type PEER_FORGET_TIMEOUT, type PeerForgetRequestHandler } from './peer-forget.js';
12
17
  import { type SCALER_ORPHANS_TIMEOUT, type ScalerOrphansRequestHandler } from './scaler-orphans-peer.js';
13
18
  import type { PeerCredentialStore } from './peer-credentials.js';
@@ -120,6 +125,13 @@ export interface PeerHandlerDeps {
120
125
  */
121
126
  onPeerForgetRequest?: PeerForgetRequestHandler;
122
127
  }
128
+ /** The schemes this server lists in peer.hello, in order. */
129
+ export declare const ADVERTISED_PEER_AUTH_SCHEMES: readonly string[];
130
+ /**
131
+ * A peer.auth.request from a release before mutual authentication: it carries
132
+ * a proof or a token, or names no scheme.
133
+ */
134
+ export declare function isLegacyPeerAuthRequest(raw: unknown): boolean;
123
135
  /**
124
136
  * Create a handler function for incoming peer WebSocket connections.
125
137
  *
@@ -5,7 +5,17 @@
5
5
  * enabling routing decisions without real-time queries. Follows the
6
6
  * same label-matching semantics as agent/registry.ts for consistency.
7
7
  */
8
- import type { PeerHeartbeat, PeerCapabilities, ScalerCapacitySummary } from '@kici-dev/engine';
8
+ import type { PeerAuthScheme, PeerHeartbeat, PeerCapabilities, ScalerCapacitySummary } from '@kici-dev/engine';
9
+ /** Which side of a peer link authenticated: the peer dialled us, or we dialled it. */
10
+ export declare enum PeerLinkDirection {
11
+ Inbound = "inbound",
12
+ Outbound = "outbound"
13
+ }
14
+ /** The scheme each direction of a peer link authenticated with; null while that direction is down. */
15
+ export interface PeerAuthSchemes {
16
+ inbound: PeerAuthScheme | null;
17
+ outbound: PeerAuthScheme | null;
18
+ }
9
19
  export interface PeerAgentInfo {
10
20
  agentId: string;
11
21
  labels: string[];
@@ -52,6 +62,8 @@ export interface PeerInfo {
52
62
  clusterSettingsVersion: number;
53
63
  /** Cluster role of the peer. */
54
64
  role: 'coordinator' | 'worker';
65
+ /** The authentication scheme of each link direction. */
66
+ authScheme: PeerAuthSchemes;
55
67
  hostname?: string;
56
68
  osRelease?: string;
57
69
  totalMemoryMb?: number;
@@ -119,6 +131,8 @@ export declare class PeerRegistry {
119
131
  routingKeys: string[];
120
132
  role?: 'coordinator' | 'worker';
121
133
  }): void;
134
+ /** Record the scheme one direction of the link authenticated with, or null when it closed. */
135
+ setAuthScheme(instanceId: string, direction: PeerLinkDirection, scheme: PeerAuthScheme | null): void;
122
136
  /**
123
137
  * Remove a peer entirely from the registry.
124
138
  */
@@ -10,6 +10,12 @@
10
10
  * and PgSecretStore, not through config schemas.
11
11
  */
12
12
  import { z } from 'zod';
13
+ /** Whether a coordinator dials the peers the Platform announces. */
14
+ export declare const PeerDiscoveryMode: z.ZodEnum<{
15
+ platform: "platform";
16
+ static: "static";
17
+ }>;
18
+ export type PeerDiscoveryMode = z.infer<typeof PeerDiscoveryMode>;
13
19
  /**
14
20
  * Schema for the local YAML configuration file.
15
21
  * All fields optional except database.url.
@@ -105,6 +111,10 @@ export declare const sharedConfigSchema: z.ZodObject<{
105
111
  }>>;
106
112
  coordinatorUrl: z.ZodOptional<z.ZodString>;
107
113
  peerStaleTimeoutMs: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
114
+ peerDiscovery: z.ZodOptional<z.ZodEnum<{
115
+ platform: "platform";
116
+ static: "static";
117
+ }>>;
108
118
  }, z.core.$strip>>;
109
119
  webhookPayloadDir: z.ZodOptional<z.ZodString>;
110
120
  cacheTtlDays: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
@@ -193,6 +203,10 @@ export declare const appConfigSchema: z.ZodObject<{
193
203
  }>>;
194
204
  coordinatorUrl: z.ZodOptional<z.ZodString>;
195
205
  peerStaleTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
206
+ peerDiscovery: z.ZodDefault<z.ZodEnum<{
207
+ platform: "platform";
208
+ static: "static";
209
+ }>>;
196
210
  }, z.core.$strip>>;
197
211
  webhookPayloadDir: z.ZodOptional<z.ZodString>;
198
212
  cacheTtlDays: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -7,6 +7,7 @@
7
7
  * - AppConfig: merged result type used throughout the codebase
8
8
  */
9
9
  import type { OrchestratorMode } from '@kici-dev/engine';
10
+ import type { PeerDiscoveryMode } from './schema.js';
10
11
  /**
11
12
  * Per-orchestrator settings loaded from YAML file.
12
13
  * These are instance-specific and never shared across orchestrators.
@@ -85,6 +86,7 @@ export interface SharedConfig {
85
86
  raftHeartbeatMs?: number;
86
87
  peerHeartbeatIntervalMs?: number;
87
88
  peerMaxReconnectDelayMs?: number;
89
+ peerDiscovery?: PeerDiscoveryMode;
88
90
  };
89
91
  webhookPayloadDir?: string;
90
92
  cacheTtlDays?: number;
@@ -189,6 +191,8 @@ export interface AppConfig {
189
191
  coordinatorUrl?: string;
190
192
  /** Stale peer timeout in ms. */
191
193
  peerStaleTimeoutMs: number;
194
+ /** Whether this coordinator dials Platform-announced peers. */
195
+ peerDiscovery: PeerDiscoveryMode;
192
196
  };
193
197
  /** Webhook payload storage directory */
194
198
  webhookPayloadDir?: string;
package/dist/config.d.ts CHANGED
@@ -245,6 +245,10 @@ declare const configSchema: z.ZodObject<{
245
245
  peerStaleTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
246
246
  electionGracePeriodMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
247
247
  singleNode: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>, z.ZodTransform<boolean, string | boolean>>;
248
+ peerDiscovery: z.ZodDefault<z.ZodEnum<{
249
+ platform: "platform";
250
+ static: "static";
251
+ }>>;
248
252
  trustedProxies: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string[], string>>;
249
253
  }, z.core.$strip>>;
250
254
  }, z.core.$strip>;
@@ -459,6 +463,10 @@ export declare const packagingConfigSchema: z.ZodObject<{
459
463
  peerStaleTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
460
464
  electionGracePeriodMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
461
465
  singleNode: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>, z.ZodTransform<boolean, string | boolean>>;
466
+ peerDiscovery: z.ZodDefault<z.ZodEnum<{
467
+ platform: "platform";
468
+ static: "static";
469
+ }>>;
462
470
  trustedProxies: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string[], string>>;
463
471
  }, z.core.$strip>>;
464
472
  }, z.core.$strip>;
@@ -673,6 +681,7 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
673
681
  peerStaleTimeoutMs: number;
674
682
  electionGracePeriodMs: number;
675
683
  singleNode: boolean;
684
+ peerDiscovery: "platform" | "static";
676
685
  trustedProxies: string[];
677
686
  };
678
687
  }>;
@@ -0,0 +1,21 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `host_roster.identity_source text NOT NULL DEFAULT 'operator'`, limited
4
+ * to `agent`, `operator` and `platform`.
5
+ *
6
+ * It records who last set a host's identity (labels, hostname, properties):
7
+ * the host's own agent registration, a local `kici-admin host declare`, or a
8
+ * Platform-relayed dashboard declare. A `platform` row is an unconfirmed
9
+ * placeholder that fan-out, inventory and the unreachable alarm skip.
10
+ *
11
+ * Backfill: a row whose `platform` column is set has registered at least once
12
+ * (only an agent registration writes `platform`), so it becomes `agent`. Every
13
+ * other row stays `operator`: a dashboard-created row from before this column
14
+ * cannot be told apart from a `kici-admin` declare, and treating it as
15
+ * `platform` would drop every declared, never-connected host out of fan-out.
16
+ *
17
+ * Idempotent: a re-run on a database that already has the column is a no-op.
18
+ */
19
+ export declare function up(db: Kysely<unknown>): Promise<void>;
20
+ export declare function down(db: Kysely<unknown>): Promise<void>;
21
+ //# sourceMappingURL=157_host_roster_identity_source.d.ts.map
@@ -13,6 +13,17 @@ export declare enum JobKind {
13
13
  Gate = "gate",
14
14
  Proxy = "proxy"
15
15
  }
16
+ /**
17
+ * Who last set a roster host's identity (labels, hostname, properties).
18
+ * `platform` marks an unconfirmed placeholder created from the dashboard: it
19
+ * is never a fan-out, inventory, agent-pin or unreachable-alarm target until
20
+ * its agent registers (`agent`) or an operator declares it (`operator`).
21
+ */
22
+ export declare enum HostIdentitySource {
23
+ agent = "agent",
24
+ operator = "operator",
25
+ platform = "platform"
26
+ }
16
27
  /**
17
28
  * PostgreSQL-only database types.
18
29
  * Column names use snake_case matching the actual database column names.
@@ -187,8 +198,9 @@ export type ClusterMeta = Selectable<ClusterMetaTable>;
187
198
  export type NewClusterMeta = Insertable<ClusterMetaTable>;
188
199
  /**
189
200
  * Join tokens table
190
- * Stores hashed join tokens for zero-knowledge cluster bootstrap.
191
- * Tokens are one-time use (consumed_at set after validation).
201
+ * Stores hashed join tokens. `kici-admin join` and peer token mode find a row by
202
+ * its hash; a token is consumed on first use and re-usable by the instance that
203
+ * consumed it until it expires.
192
204
  */
193
205
  export interface JoinTokenTable {
194
206
  /** UUID primary key */
@@ -2839,6 +2851,8 @@ export interface HostRosterTable {
2839
2851
  * presigned URL). NULL / false ⇒ the conservative `ssh-push` fallback.
2840
2852
  */
2841
2853
  s3_reachable: ColumnType<boolean | null, boolean | null | undefined, boolean | null>;
2854
+ /** Who last set the host's identity. See {@link HostIdentitySource}. */
2855
+ identity_source: ColumnType<HostIdentitySource, HostIdentitySource | undefined, HostIdentitySource>;
2842
2856
  created_at: Generated<Date>;
2843
2857
  updated_at: ColumnType<Date, Date | string | undefined, Date | string>;
2844
2858
  }
@@ -10,11 +10,17 @@
10
10
  * the Platform `webhook_sources` row and the dashboard Sources tab pick up the
11
11
  * new name/slug without any extra plumbing here.
12
12
  *
13
+ * The same pass checks each App against the events and permissions KiCI needs
14
+ * (`GITHUB_APP_REQUIREMENTS`) and the permissions each installation has
15
+ * accepted, and logs one warning per App with gaps. A gap never blocks the name
16
+ * and slug sync.
17
+ *
13
18
  * Lifecycle mirrors `StaleRunDetector`: `start()` runs an immediate refresh
14
19
  * (so a rename made while the orchestrator was down propagates on next boot)
15
20
  * then a `setInterval`; `stop()` clears it. Per-source errors are logged and
16
21
  * never abort the loop.
17
22
  */
23
+ import { type GithubAppIdentity, type GithubAppInstallationGrant, type GithubAppRequirementGaps } from '../providers/github/manifest.js';
18
24
  /** A source row as returned by listSources() — the slice this module needs. */
19
25
  export type SourceIdentityRow = {
20
26
  routing_key: string;
@@ -39,12 +45,14 @@ export interface RefreshableSourceStore {
39
45
  export type FetchGithubAppIdentity = (creds: {
40
46
  appId: string;
41
47
  privateKey: string;
42
- }) => Promise<{
43
- name: string;
44
- slug: string;
45
- }>;
46
- /** Outcome of a single source refresh. */
47
- export interface RefreshResult {
48
+ }) => Promise<GithubAppIdentity>;
49
+ /** List a GitHub App's installations. Matches `listGithubAppInstallations`. */
50
+ export type FetchGithubAppInstallations = (creds: {
51
+ appId: string;
52
+ privateKey: string;
53
+ }) => Promise<GithubAppInstallationGrant[]>;
54
+ /** Outcome of a single source refresh: the name and slug sync plus the requirement gaps. */
55
+ export interface RefreshResult extends GithubAppRequirementGaps {
48
56
  routingKey: string;
49
57
  changed: boolean;
50
58
  oldName: string;
@@ -54,27 +62,31 @@ export interface RefreshResult {
54
62
  }
55
63
  /**
56
64
  * Refresh one already-resolved GitHub source's identity from GitHub and persist
57
- * it when the name or slug drifted. The caller supplies the row it already holds,
58
- * so this never lists the sources table. Throws for a non-GitHub row or missing
59
- * credentials.
65
+ * it when the name or slug drifted, then compare the App and its installations
66
+ * with the required events and permissions. The caller supplies the row it
67
+ * already holds, so this never lists the sources table. Throws for a non-GitHub
68
+ * row or missing credentials. A failed installations listing throws after the
69
+ * name and slug sync has been written, so the sync never waits on the gap check.
60
70
  */
61
- export declare function refreshResolvedGithubSource(sourceStore: Pick<RefreshableSourceStore, 'getSourceWithSecrets' | 'updateSource'>, row: SourceIdentityRow, fetchIdentity: FetchGithubAppIdentity): Promise<RefreshResult>;
71
+ export declare function refreshResolvedGithubSource(sourceStore: Pick<RefreshableSourceStore, 'getSourceWithSecrets' | 'updateSource'>, row: SourceIdentityRow, fetchIdentity: FetchGithubAppIdentity, fetchInstallations: FetchGithubAppInstallations): Promise<RefreshResult>;
62
72
  /**
63
73
  * Re-fetch one GitHub source's identity from GitHub and persist it when the
64
- * name or slug drifted. Resolves the row by routing key (one sources read) then
65
- * delegates. Shared by `kici-admin source refresh`. Throws for a missing or
74
+ * name or slug drifted, and report requirement gaps. Resolves the row by routing
75
+ * key (one sources read) then delegates. Shared by `kici-admin source refresh`. Throws for a missing or
66
76
  * non-GitHub routing key.
67
77
  */
68
- export declare function refreshGithubSourceIdentity(sourceStore: RefreshableSourceStore, routingKey: string, fetchIdentity: FetchGithubAppIdentity): Promise<RefreshResult>;
78
+ export declare function refreshGithubSourceIdentity(sourceStore: RefreshableSourceStore, routingKey: string, fetchIdentity: FetchGithubAppIdentity, fetchInstallations: FetchGithubAppInstallations): Promise<RefreshResult>;
69
79
  export interface GithubAppNameRefresherDeps {
70
80
  sourceStore: RefreshableSourceStore;
71
81
  fetchIdentity: FetchGithubAppIdentity;
82
+ fetchInstallations: FetchGithubAppInstallations;
72
83
  /** Refresh cadence in ms. Default cluster value: 24h (`config.githubAppNameRefreshIntervalMs`). */
73
84
  scanIntervalMs: number;
74
85
  }
75
86
  export declare class GithubAppNameRefresher {
76
87
  private readonly sourceStore;
77
88
  private readonly fetchIdentity;
89
+ private readonly fetchInstallations;
78
90
  private readonly scanIntervalMs;
79
91
  private interval;
80
92
  constructor(deps: GithubAppNameRefresherDeps);
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { SourceCache } from './cache/source-cache.js';
5
5
  export { DepCache } from './cache/dep-cache.js';
6
6
  export { UserCache, DEFAULT_USER_CACHE_QUOTA_BYTES, DEFAULT_USER_CACHE_TTL_MS, type UserCacheRef, type UserCacheRestoreResult, type UserCacheBeginSaveResult, } from './cache/user-cache.js';
7
7
  export { PeerCredentialStore, createPeerCredentialStoreFromUrl, type PeerCredential, type CredentialFileData, } from './cluster/peer-credentials.js';
8
- export { PeerAuthCoordinator, type AuthDecision, type RejectionAction, } from './cluster/peer-auth-coordinator.js';
8
+ export { PeerAuthCoordinator, RejectionAction, type AuthDecision, } from './cluster/peer-auth-coordinator.js';
9
9
  export { JoinTokenManager, createJoinTokenManagerFromUrl } from './cluster/join-token.js';
10
10
  export { HeldRunStore, createHeldRunStoreFromUrl, SecurityHoldReason, } from './contexts/held-runs.js';
11
11
  export { TrustPolicyStore, createTrustPolicyStoreFromUrl, TrustPolicySource, DEFAULT_TRUST_POLICY, type StoredTrustPolicy, } from './security/trust-policy-store.js';