@kici-dev/orchestrator 0.1.20 → 0.1.22

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 (71) hide show
  1. package/dist/agent/dispatcher.d.ts +44 -0
  2. package/dist/agent/host-roster-reaper.d.ts +2 -1
  3. package/dist/agent/host-roster.d.ts +54 -1
  4. package/dist/agent/registry.d.ts +22 -0
  5. package/dist/app.d.ts +5 -0
  6. package/dist/approvals/step-approval-bridge.d.ts +5 -0
  7. package/dist/cli/commands/source-manifest.d.ts +42 -0
  8. package/dist/cli/loopback-callback.d.ts +24 -0
  9. package/dist/cli/open-browser.d.ts +12 -0
  10. package/dist/cli/service/compose.d.ts +13 -0
  11. package/dist/cli/service/deploy-env.d.ts +31 -0
  12. package/dist/cli.js +1204 -256
  13. package/dist/cluster/coordinator.d.ts +5 -3
  14. package/dist/cluster/index.d.ts +2 -0
  15. package/dist/cluster/join-token.d.ts +27 -5
  16. package/dist/cluster/peer-auth-coordinator.d.ts +36 -0
  17. package/dist/cluster/peer-client.d.ts +34 -14
  18. package/dist/cluster/peer-credentials.d.ts +14 -2
  19. package/dist/cluster/peer-handler.d.ts +2 -2
  20. package/dist/cluster/rerouted-job-guard.d.ts +39 -0
  21. package/dist/config.d.ts +6 -0
  22. package/dist/dashboard/needs-edges.d.ts +4 -3
  23. package/dist/db/migrations/043_rerouted_to_peer.d.ts +4 -0
  24. package/dist/db/migrations/044_check_mode.d.ts +4 -0
  25. package/dist/db/migrations/045_host_properties.d.ts +16 -0
  26. package/dist/db/migrations/046_join_token_consumed_by_instance.d.ts +17 -0
  27. package/dist/db/migrations/047_needs_run_on.d.ts +4 -0
  28. package/dist/db/migrations/048_host_reboot_pending.d.ts +19 -0
  29. package/dist/db/migrations/049_held_runs_payload.d.ts +14 -0
  30. package/dist/db/migrations/050_sources_slug.d.ts +17 -0
  31. package/dist/db/types.d.ts +60 -5
  32. package/dist/deployment/deployment-identity.d.ts +9 -0
  33. package/dist/entry-helpers.d.ts +7 -0
  34. package/dist/environments/held-runs.d.ts +7 -1
  35. package/dist/github-app-name-refresher/github-app-name-refresher.d.ts +77 -0
  36. package/dist/index.d.ts +5 -1
  37. package/dist/index.js +1187 -33
  38. package/dist/lockfile-validate.d.ts +24 -0
  39. package/dist/orchestrator-core.d.ts +13 -1
  40. package/dist/pipeline/decorating-secret-resolver.d.ts +32 -0
  41. package/dist/pipeline/dispatch-matched-workflow.d.ts +127 -3
  42. package/dist/pipeline/install-secrets-resolver.d.ts +2 -2
  43. package/dist/pipeline/needs-scheduler.d.ts +22 -13
  44. package/dist/pipeline/processor.d.ts +2 -2
  45. package/dist/pipeline/test-pipeline.d.ts +37 -58
  46. package/dist/providers/github/manifest-form.d.ts +15 -0
  47. package/dist/providers/github/manifest.d.ts +103 -0
  48. package/dist/reporting/execution-tracker.d.ts +10 -1
  49. package/dist/routes/admin-sources.d.ts +18 -0
  50. package/dist/routes/admin.d.ts +8 -0
  51. package/dist/secrets/pg-secret-store.d.ts +9 -0
  52. package/dist/secrets/secret-resolver.d.ts +16 -1
  53. package/dist/server.js +4373 -2322
  54. package/dist/sources/source-store.d.ts +4 -0
  55. package/dist/sources/source-validator.d.ts +2 -0
  56. package/dist/stale-detector/reboot-deadline-sweep.d.ts +29 -0
  57. package/dist/stale-detector/stale-run-detector.d.ts +8 -0
  58. package/dist/standalone.js +3582 -1932
  59. package/dist/worker/in-memory-job-queue.d.ts +17 -0
  60. package/dist/worker/peer-outbox.d.ts +36 -0
  61. package/dist/worker/worker-outbox-relay.d.ts +13 -0
  62. package/dist/ws/agent-handler.d.ts +11 -6
  63. package/dist/ws/dashboard-fleet-handler.d.ts +33 -0
  64. package/dist/ws/dashboard-fleet-write-handler.d.ts +60 -0
  65. package/dist/ws/fleet-runs-on-all.d.ts +16 -0
  66. package/dist/ws/inventory-api.d.ts +17 -0
  67. package/dist/ws/platform-client.d.ts +13 -1
  68. package/dist/ws/test-relay-handlers.d.ts +4 -2
  69. package/installer-image-digests.json +3 -3
  70. package/package.json +4 -4
  71. package/sbom.spdx.json +77 -128
package/dist/index.js CHANGED
@@ -3,12 +3,19 @@ import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCom
3
3
  import { Upload } from "@aws-sdk/lib-storage";
4
4
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
5
5
  import { createLogger, createS3Client, sha256 } from "@kici-dev/shared";
6
- import { promises } from "node:fs";
6
+ import * as fs from "node:fs";
7
+ import { mkdtempSync, promises, writeFileSync } from "node:fs";
7
8
  import { dirname, join } from "node:path";
8
- import { createHmac, randomBytes, randomUUID } from "node:crypto";
9
- import "node:fs/promises";
9
+ import { createHmac, hkdfSync, randomBytes, randomUUID } from "node:crypto";
10
+ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
10
11
  import { Kysely, PostgresDialect, sql } from "kysely";
11
12
  import pg from "pg";
13
+ import { platform, tmpdir } from "node:os";
14
+ import { createInterface } from "node:readline";
15
+ import { Octokit } from "@octokit/rest";
16
+ import { createAppAuth } from "@octokit/auth-app";
17
+ import { createServer } from "node:http";
18
+ import { exec } from "node:child_process";
12
19
  import.meta.url;
13
20
  //#endregion
14
21
  //#region src/storage/s3.ts
@@ -558,7 +565,7 @@ function createCacheStorage(config) {
558
565
  * downloads the source tarball, extracts it, and imports the workflow entry
559
566
  * via the shared oxc-transform ESM loader hook.
560
567
  */
561
- const logger$2 = createLogger({ prefix: "source-cache" });
568
+ const logger$3 = createLogger({ prefix: "source-cache" });
562
569
  /** Cache key format: source/{contentHash}.tar.gz */
563
570
  function sourceKey(contentHash) {
564
571
  return `source/${contentHash}.tar.gz`;
@@ -571,7 +578,7 @@ var SourceCache = class {
571
578
  async has(contentHash) {
572
579
  const key = sourceKey(contentHash);
573
580
  const exists = await this.storage.has(key);
574
- logger$2.debug(`has(${contentHash}): ${exists}`);
581
+ logger$3.debug(`has(${contentHash}): ${exists}`);
575
582
  return exists;
576
583
  }
577
584
  async get(contentHash) {
@@ -579,8 +586,8 @@ var SourceCache = class {
579
586
  const data = await this.storage.get(key);
580
587
  if (data) {
581
588
  await this.storage.touch(key);
582
- logger$2.debug(`get(${contentHash}): hit (${data.length} bytes)`);
583
- } else logger$2.debug(`get(${contentHash}): miss`);
589
+ logger$3.debug(`get(${contentHash}): hit (${data.length} bytes)`);
590
+ } else logger$3.debug(`get(${contentHash}): miss`);
584
591
  return data;
585
592
  }
586
593
  async getUrl(contentHash) {
@@ -588,8 +595,8 @@ var SourceCache = class {
588
595
  const url = await this.storage.getUrl(key);
589
596
  if (url) {
590
597
  await this.storage.touch(key);
591
- logger$2.debug(`getUrl(${contentHash}): hit`);
592
- } else logger$2.debug(`getUrl(${contentHash}): miss`);
598
+ logger$3.debug(`getUrl(${contentHash}): hit`);
599
+ } else logger$3.debug(`getUrl(${contentHash}): miss`);
593
600
  return url;
594
601
  }
595
602
  async getUploadUrl(contentHash) {
@@ -600,12 +607,12 @@ var SourceCache = class {
600
607
  const key = sourceKey(contentHash);
601
608
  await this.storage.put(key, tarball);
602
609
  const size = typeof tarball === "string" ? Buffer.byteLength(tarball) : tarball.length;
603
- logger$2.info(`store(${contentHash}): stored (${size} bytes)`);
610
+ logger$3.info(`store(${contentHash}): stored (${size} bytes)`);
604
611
  }
605
612
  async remove(contentHash) {
606
613
  const key = sourceKey(contentHash);
607
614
  const removed = await this.storage.delete(key);
608
- logger$2.info(`remove(${contentHash}): ${removed ? "removed" : "not found"}`);
615
+ logger$3.info(`remove(${contentHash}): ${removed ? "removed" : "not found"}`);
609
616
  return removed;
610
617
  }
611
618
  };
@@ -620,7 +627,7 @@ var SourceCache = class {
620
627
  *
621
628
  * Cache key format: deps/{platform}-{arch}/{lockfileHash}.tar.gz
622
629
  */
623
- const logger$1 = createLogger({ prefix: "dep-cache" });
630
+ const logger$2 = createLogger({ prefix: "dep-cache" });
624
631
  /** Default max tarball size: 500MB */
625
632
  const DEFAULT_MAX_TARBALL_BYTES = 524288e3;
626
633
  /** Build cache key for dependency tarball: deps/{platform}-{arch}/{lockfileHash}.tar.gz */
@@ -638,7 +645,7 @@ var DepCache = class {
638
645
  async has(lockfileHash, platform, arch) {
639
646
  const key = depKey(lockfileHash, platform, arch);
640
647
  const exists = await this.storage.has(key);
641
- logger$1.debug(`has(${lockfileHash}): ${exists}`, {
648
+ logger$2.debug(`has(${lockfileHash}): ${exists}`, {
642
649
  platform,
643
650
  arch
644
651
  });
@@ -653,11 +660,11 @@ var DepCache = class {
653
660
  const url = await this.storage.getUrl(key);
654
661
  if (url) {
655
662
  await this.storage.touch(key);
656
- logger$1.debug(`getUrl(${lockfileHash}): hit`, {
663
+ logger$2.debug(`getUrl(${lockfileHash}): hit`, {
657
664
  platform,
658
665
  arch
659
666
  });
660
- } else logger$1.debug(`getUrl(${lockfileHash}): miss`, {
667
+ } else logger$2.debug(`getUrl(${lockfileHash}): miss`, {
661
668
  platform,
662
669
  arch
663
670
  });
@@ -672,7 +679,7 @@ var DepCache = class {
672
679
  const key = depKey(lockfileHash, platform, arch);
673
680
  const url = await this.storage.getUrl(key);
674
681
  if (!url) {
675
- logger$1.debug(`getUrlAndHash(${lockfileHash}): miss`, {
682
+ logger$2.debug(`getUrlAndHash(${lockfileHash}): miss`, {
676
683
  platform,
677
684
  arch
678
685
  });
@@ -681,7 +688,7 @@ var DepCache = class {
681
688
  await this.storage.touch(key);
682
689
  const hashKey = `deps/${platform}-${arch}/${lockfileHash}.hash`;
683
690
  const hash = (await this.storage.get(hashKey))?.toString("utf-8") || void 0;
684
- logger$1.debug(`getUrlAndHash(${lockfileHash}): hit`, {
691
+ logger$2.debug(`getUrlAndHash(${lockfileHash}): hit`, {
685
692
  platform,
686
693
  arch,
687
694
  hasHash: !!hash
@@ -706,7 +713,7 @@ var DepCache = class {
706
713
  if (tarballData.length > this.maxTarballBytes) throw new Error(`Dep tarball exceeds max size: ${tarballData.length} bytes > ${this.maxTarballBytes} bytes limit`);
707
714
  const key = depKey(lockfileHash, platform, arch);
708
715
  await this.storage.put(key, tarballData);
709
- logger$1.info(`store: ${tarballData.length} bytes`, {
716
+ logger$2.info(`store: ${tarballData.length} bytes`, {
710
717
  lockfileHash,
711
718
  platform,
712
719
  arch
@@ -723,7 +730,7 @@ var DepCache = class {
723
730
  async remove(lockfileHash, platform, arch) {
724
731
  const key = depKey(lockfileHash, platform, arch);
725
732
  const removed = await this.storage.delete(key);
726
- logger$1.info(`remove(${lockfileHash}): ${removed ? "removed" : "not found"}`, {
733
+ logger$2.info(`remove(${lockfileHash}): ${removed ? "removed" : "not found"}`, {
727
734
  platform,
728
735
  arch
729
736
  });
@@ -754,7 +761,7 @@ var DepCache = class {
754
761
  * hash and size accounting outside the tarball's own (presigned, metadata-less)
755
762
  * upload.
756
763
  */
757
- const logger = createLogger({ prefix: "user-cache" });
764
+ const logger$1 = createLogger({ prefix: "user-cache" });
758
765
  /**
759
766
  * Cluster-wide default quota: 5 GiB. Serves as the fallback when an org has no
760
767
  * per-org override in `org_settings.user_cache_quota_bytes`. The cluster-wide
@@ -798,7 +805,7 @@ var UserCache = class {
798
805
  try {
799
806
  limits = await this.orgLimitsReader(org);
800
807
  } catch (err) {
801
- logger.warn("user-cache org-limits lookup failed — using cluster defaults", {
808
+ logger$1.warn("user-cache org-limits lookup failed — using cluster defaults", {
802
809
  org,
803
810
  error: err instanceof Error ? err.message : String(err)
804
811
  });
@@ -899,7 +906,7 @@ var UserCache = class {
899
906
  const prefix = this.writePrefix(ref);
900
907
  const final = this.finalKey(prefix, ref.key);
901
908
  if (await this.storage.has(final)) {
902
- logger.info("user-cache save skipped (immutable key exists)", { key: ref.key });
909
+ logger$1.info("user-cache save skipped (immutable key exists)", { key: ref.key });
903
910
  return { skip: true };
904
911
  }
905
912
  const tempKey = `${prefix}.tmp-${randomUUID()}${TAR_SUFFIX}`;
@@ -921,7 +928,7 @@ var UserCache = class {
921
928
  await this.storage.initMeta(final);
922
929
  await this.storage.put(`${final}.hash`, ref.tarHash);
923
930
  await this.storage.put(`${final}.size`, String(ref.sizeBytes));
924
- logger.info("user-cache entry committed", {
931
+ logger$1.info("user-cache entry committed", {
925
932
  key: ref.key,
926
933
  sizeBytes: ref.sizeBytes
927
934
  });
@@ -964,7 +971,7 @@ var UserCache = class {
964
971
  await this.storage.delete(`${key}.hash`);
965
972
  await this.storage.delete(`${key}.size`);
966
973
  total -= size;
967
- logger.info("user-cache eviction (org over quota)", {
974
+ logger$1.info("user-cache eviction (org over quota)", {
968
975
  org: ref.org,
969
976
  key,
970
977
  freedBytes: size
@@ -996,14 +1003,24 @@ var PeerCredentialStore = class {
996
1003
  this.db = db;
997
1004
  }
998
1005
  /**
999
- * Save a new peer credential.
1006
+ * Save a new peer credential, revoking any prior active credential for the
1007
+ * same instanceId (the `peer_credentials_active_uniq` partial unique index
1008
+ * permits exactly one active credential per instanceId).
1009
+ *
1010
+ * Returns `{ revokedCount }` — how many previously-active credentials this
1011
+ * save revoked. Because a coordinator's peer-clients share one
1012
+ * identity-scoped credential, a `revokedCount > 0` here means every sibling
1013
+ * peer-client of `instanceId` that was authenticating with the old
1014
+ * credential is now invalidated and will be rejected on its next proof. The
1015
+ * caller logs this so a revoke that cascades sibling rejections is visible in
1016
+ * the orchestrator logs.
1000
1017
  */
1001
1018
  async save(opts) {
1002
1019
  const expiryDays = opts.expiryDays ?? DEFAULT_EXPIRY_DAYS;
1003
1020
  const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1e3);
1004
1021
  const runOnce = async () => {
1005
- await this.db.transaction().execute(async (trx) => {
1006
- await trx.updateTable("peer_credentials").set({ revoked_at: /* @__PURE__ */ new Date() }).where("instance_id", "=", opts.instanceId).where("revoked_at", "is", null).execute();
1022
+ return this.db.transaction().execute(async (trx) => {
1023
+ const revokeResult = await trx.updateTable("peer_credentials").set({ revoked_at: /* @__PURE__ */ new Date() }).where("instance_id", "=", opts.instanceId).where("revoked_at", "is", null).executeTakeFirst();
1007
1024
  await trx.insertInto("peer_credentials").values({
1008
1025
  instance_id: opts.instanceId,
1009
1026
  credential_hash: opts.credentialHash,
@@ -1012,15 +1029,13 @@ var PeerCredentialStore = class {
1012
1029
  source_token_hash: opts.sourceTokenHash ?? null,
1013
1030
  expires_at: expiresAt
1014
1031
  }).execute();
1032
+ return Number(revokeResult?.numUpdatedRows ?? 0n);
1015
1033
  });
1016
1034
  };
1017
1035
  try {
1018
- await runOnce();
1036
+ return { revokedCount: await runOnce() };
1019
1037
  } catch (err) {
1020
- if (err instanceof Error && "code" in err && err.code === "23505") {
1021
- await runOnce();
1022
- return;
1023
- }
1038
+ if (err instanceof Error && "code" in err && err.code === "23505") return { revokedCount: await runOnce() };
1024
1039
  throw err;
1025
1040
  }
1026
1041
  }
@@ -1087,6 +1102,38 @@ function mapRow(row) {
1087
1102
  };
1088
1103
  }
1089
1104
  /**
1105
+ * Write credential data to a JSON file with restrictive permissions.
1106
+ *
1107
+ * Creates the parent directory (e.g., ~/.kici/) with 0700 if it doesn't exist.
1108
+ * The credential file is written with 0600 permissions (owner read/write only).
1109
+ *
1110
+ * @param filePath - Absolute path to the credential file
1111
+ * @param data - Credential data to persist
1112
+ */
1113
+ async function writeCredentialFile(filePath, data) {
1114
+ await mkdir(dirname(filePath), {
1115
+ recursive: true,
1116
+ mode: 448
1117
+ });
1118
+ await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
1119
+ await chmod(filePath, 384);
1120
+ }
1121
+ /**
1122
+ * Read credential data from a JSON file.
1123
+ *
1124
+ * @param filePath - Absolute path to the credential file
1125
+ * @returns Parsed credential data, or null if the file doesn't exist
1126
+ */
1127
+ async function readCredentialFile(filePath) {
1128
+ try {
1129
+ const content = await readFile(filePath, "utf-8");
1130
+ return JSON.parse(content);
1131
+ } catch (err) {
1132
+ if (err.code === "ENOENT") return null;
1133
+ throw err;
1134
+ }
1135
+ }
1136
+ /**
1090
1137
  * Construct a PeerCredentialStore bound to a Postgres URL, returning the
1091
1138
  * store plus a disposer that closes the underlying pool. Lets tests
1092
1139
  * exercise the real Kysely-backed store without importing pg/kysely.
@@ -1108,6 +1155,1113 @@ async function createPeerCredentialStoreFromUrl(databaseUrl, opts) {
1108
1155
  };
1109
1156
  }
1110
1157
  //#endregion
1111
- export { DEFAULT_USER_CACHE_QUOTA_BYTES, DEFAULT_USER_CACHE_TTL_MS, DepCache, PeerCredentialStore, S3CacheStorage, SourceCache, UserCache, createCacheStorage, createPeerCredentialStoreFromUrl };
1158
+ //#region src/cluster/peer-auth-coordinator.ts
1159
+ /**
1160
+ * Per-orchestrator coordinator that owns the shared peer credential file.
1161
+ *
1162
+ * A single orchestrator runs N peer-clients (one per cluster peer) that all
1163
+ * share one identity-scoped credential file. Left uncoordinated, a reconnect
1164
+ * storm makes each sibling independently token-join (each join revokes the
1165
+ * prior credential, invalidating the others) and delete the shared file on any
1166
+ * rejection — a credential revocation cascade. This coordinator serializes all
1167
+ * file access through one in-process mutex so only one peer-client token-joins
1168
+ * per storm, and it never deletes a credential file a sibling has refreshed.
1169
+ */
1170
+ const logger = createLogger({ prefix: "peer-auth-coordinator" });
1171
+ /** How long a waiting peer-client awaits an in-flight sibling token-join. */
1172
+ const DEFAULT_JOIN_WAIT_TIMEOUT_MS = 1e4;
1173
+ /** Max read→await→re-read cycles before a waiter gives up and joins/aborts. */
1174
+ const MAX_DECIDE_ITERATIONS = 3;
1175
+ function deferred() {
1176
+ let resolve;
1177
+ return {
1178
+ promise: new Promise((res) => {
1179
+ resolve = res;
1180
+ }),
1181
+ resolve
1182
+ };
1183
+ }
1184
+ var PeerAuthCoordinator = class {
1185
+ credentialFile;
1186
+ instanceId;
1187
+ joinToken;
1188
+ joinWaitTimeoutMs;
1189
+ /** Promise-chain mutex tail; every file op awaits the prior one. */
1190
+ lock = Promise.resolve();
1191
+ /** Set while one peer-client is mid token-join; siblings await it. */
1192
+ inFlightJoin = null;
1193
+ constructor(opts) {
1194
+ this.credentialFile = opts.credentialFile;
1195
+ this.instanceId = opts.instanceId;
1196
+ this.joinToken = opts.joinToken;
1197
+ this.joinWaitTimeoutMs = opts.joinWaitTimeoutMs ?? DEFAULT_JOIN_WAIT_TIMEOUT_MS;
1198
+ }
1199
+ /** Run `fn` exclusively against the credential file. */
1200
+ async withLock(fn) {
1201
+ const run = this.lock.then(fn, fn);
1202
+ this.lock = run.then(() => void 0, () => void 0);
1203
+ return run;
1204
+ }
1205
+ async readValidCredential() {
1206
+ const cred = await readCredentialFile(this.credentialFile);
1207
+ return cred && cred.instanceId === this.instanceId ? cred : null;
1208
+ }
1209
+ async decideAuth() {
1210
+ for (let i = 0; i < MAX_DECIDE_ITERATIONS; i++) {
1211
+ const decision = await this.withLock(async () => {
1212
+ const cred = await this.readValidCredential();
1213
+ if (cred) return {
1214
+ mode: "credential",
1215
+ credential: cred
1216
+ };
1217
+ if (this.inFlightJoin) return "await-join";
1218
+ if (this.joinToken) {
1219
+ const join = deferred();
1220
+ this.inFlightJoin = join;
1221
+ return {
1222
+ mode: "token-join",
1223
+ token: this.joinToken,
1224
+ complete: this.makeComplete(join)
1225
+ };
1226
+ }
1227
+ return { mode: "no-auth" };
1228
+ });
1229
+ if (decision !== "await-join") return decision;
1230
+ await this.awaitInFlightJoin();
1231
+ }
1232
+ if (this.joinToken) {
1233
+ const join = deferred();
1234
+ this.inFlightJoin = join;
1235
+ return {
1236
+ mode: "token-join",
1237
+ token: this.joinToken,
1238
+ complete: this.makeComplete(join)
1239
+ };
1240
+ }
1241
+ return { mode: "no-auth" };
1242
+ }
1243
+ makeComplete(join) {
1244
+ return (issued) => {
1245
+ this.withLock(async () => {
1246
+ if (issued) await writeCredentialFile(this.credentialFile, issued);
1247
+ if (this.inFlightJoin === join) this.inFlightJoin = null;
1248
+ join.resolve(issued);
1249
+ });
1250
+ };
1251
+ }
1252
+ async awaitInFlightJoin() {
1253
+ const join = this.inFlightJoin;
1254
+ if (!join) return;
1255
+ let timer;
1256
+ const timeout = new Promise((res) => {
1257
+ timer = setTimeout(() => {
1258
+ if (this.inFlightJoin === join) this.inFlightJoin = null;
1259
+ logger.warn("In-flight peer token-join timed out; waiter will retry", { instanceId: this.instanceId });
1260
+ res();
1261
+ }, this.joinWaitTimeoutMs);
1262
+ });
1263
+ await Promise.race([join.promise.then(() => void 0), timeout]);
1264
+ if (timer) clearTimeout(timer);
1265
+ }
1266
+ async reportRejection(provedCredential, reason) {
1267
+ return this.withLock(async () => {
1268
+ const cred = await this.readValidCredential();
1269
+ if (cred && cred.credential !== provedCredential) {
1270
+ logger.info("Credential refreshed by sibling; retrying credential auth", {
1271
+ instanceId: this.instanceId,
1272
+ reason
1273
+ });
1274
+ return "retry-credential";
1275
+ }
1276
+ try {
1277
+ await unlink(this.credentialFile);
1278
+ logger.warn("Deleted stale credential file after server rejection", {
1279
+ instanceId: this.instanceId,
1280
+ reason
1281
+ });
1282
+ } catch (err) {
1283
+ if (err.code !== "ENOENT") logger.warn("Failed to delete stale credential file", {
1284
+ instanceId: this.instanceId,
1285
+ path: this.credentialFile
1286
+ });
1287
+ }
1288
+ return "rejoin";
1289
+ });
1290
+ }
1291
+ };
1292
+ //#endregion
1293
+ //#region src/cluster/join-token.ts
1294
+ /**
1295
+ * Join token manager for zero-knowledge cluster bootstrap.
1296
+ *
1297
+ * Token format: kici_join_v1.<base64url(routing_json)>.<random_256bit_hex>
1298
+ * - Routing JSON: { orgId, routingKey, expiry } (cleartext for Platform relay routing)
1299
+ * - Secret: 32 random bytes as hex (used for key derivation)
1300
+ *
1301
+ * Key derivation:
1302
+ * - encryption_key = HKDF-SHA256(secret, salt="kici-join-encrypt", info="v1", length=32)
1303
+ * - validation_hash = SHA-256(secret) (stored in DB for lookup)
1304
+ *
1305
+ * Config bundle encryption: AES-256-GCM with random 12-byte IV.
1306
+ * Wire format: <12-byte IV><16-byte auth tag><ciphertext>
1307
+ */
1308
+ const joinTokenLogger = createLogger({ prefix: "join-token" });
1309
+ const TOKEN_PREFIX = "kici_join_v1";
1310
+ const DEFAULT_EXPIRY_MS = 36e5;
1311
+ const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used. Generate a new token with: kici admin create-join-token";
1312
+ var JoinTokenManager = class {
1313
+ deps;
1314
+ constructor(deps) {
1315
+ this.deps = deps;
1316
+ }
1317
+ /**
1318
+ * Create a new join token.
1319
+ * Returns the full token string (only available at creation time).
1320
+ */
1321
+ async createToken(opts) {
1322
+ const expiryMs = opts.expiryMs ?? DEFAULT_EXPIRY_MS;
1323
+ const expiry = Date.now() + expiryMs;
1324
+ const role = opts.role ?? "coordinator";
1325
+ const routing = {
1326
+ orgId: opts.orgId,
1327
+ routingKey: opts.routingKey,
1328
+ expiry,
1329
+ role
1330
+ };
1331
+ const secret = randomBytes(32);
1332
+ const token = `${TOKEN_PREFIX}.${Buffer.from(JSON.stringify(routing)).toString("base64url")}.${secret.toString("hex")}`;
1333
+ const { validationHash } = deriveKeys(secret);
1334
+ await this.deps.db.insertInto("join_tokens").values({
1335
+ id: randomUUID(),
1336
+ token_hash: validationHash,
1337
+ routing_info: JSON.stringify(routing),
1338
+ role,
1339
+ created_by: opts.createdBy,
1340
+ expires_at: new Date(expiry)
1341
+ }).execute();
1342
+ joinTokenLogger.info("Created join token", {
1343
+ orgId: opts.orgId,
1344
+ routingKey: opts.routingKey
1345
+ });
1346
+ return token;
1347
+ }
1348
+ /**
1349
+ * Atomically validate and consume a token in one DB round-trip.
1350
+ *
1351
+ * Single UPDATE with `WHERE token_hash = ? AND consumed_at IS NULL AND
1352
+ * expires_at > NOW()` — only one caller can win the claim across a
1353
+ * shared-DB multi-coordinator mesh. The winner gets `{ routing, keys }`;
1354
+ * every other concurrent caller gets `TOKEN_ALREADY_USED_MESSAGE` and is
1355
+ * expected to fall into the idempotent recovery branch in
1356
+ * `peer-handler.ts` (which serialises credential issuance via the
1357
+ * `peer_credentials_active_uniq` partial unique index).
1358
+ *
1359
+ * On a 0-row claim, a follow-up SELECT disambiguates not-found / expired
1360
+ * / already-used / reusable-by-same-instance so callers can branch on the
1361
+ * specific reason — the recovery path in peer-handler.ts keys on the
1362
+ * "already used" string specifically. The follow-up only fires on the
1363
+ * unhappy path.
1364
+ *
1365
+ * Self-healing reuse: a join token is re-consumable by the same joining
1366
+ * peer (`peerInstanceId`) until its `expires_at`. A peer that lost its
1367
+ * credential (transient outage / deleted credential file) re-presents the
1368
+ * still-valid join token already in its env; the coordinator accepts the
1369
+ * reuse and issues a fresh credential — no operator action, no cluster
1370
+ * redeploy. Reuse is bounded by BOTH `expires_at` AND the consuming
1371
+ * instanceId, so it never widens a leaked token's usefulness beyond the
1372
+ * instance that first consumed it.
1373
+ */
1374
+ async validateAndConsumeToken(token, consumedBy, peerInstanceId) {
1375
+ const parsed = parseToken(token);
1376
+ const keys = deriveKeys(Buffer.from(parsed.secretHex, "hex"));
1377
+ const updateResult = await this.deps.db.updateTable("join_tokens").set({
1378
+ consumed_at: /* @__PURE__ */ new Date(),
1379
+ consumed_by: consumedBy,
1380
+ consumed_by_instance: peerInstanceId
1381
+ }).where("token_hash", "=", keys.validationHash).where("consumed_at", "is", null).where("expires_at", ">", /* @__PURE__ */ new Date()).executeTakeFirst();
1382
+ if (Number(updateResult?.numUpdatedRows ?? 0n) > 0) {
1383
+ joinTokenLogger.info("Consumed join token", {
1384
+ consumedBy,
1385
+ peerInstanceId
1386
+ });
1387
+ return {
1388
+ routing: parsed.routing,
1389
+ keys
1390
+ };
1391
+ }
1392
+ const row = await this.deps.db.selectFrom("join_tokens").selectAll().where("token_hash", "=", keys.validationHash).executeTakeFirst();
1393
+ if (!row) throw new Error("Invalid join token");
1394
+ if (new Date(row.expires_at).getTime() <= Date.now()) throw new Error("Join token has expired. Generate a new token with: kici admin create-join-token");
1395
+ if (row.consumed_at) {
1396
+ if (row.consumed_by_instance === peerInstanceId) {
1397
+ joinTokenLogger.info("Re-validated join token for returning instance", {
1398
+ consumedBy,
1399
+ peerInstanceId
1400
+ });
1401
+ return {
1402
+ routing: parsed.routing,
1403
+ keys
1404
+ };
1405
+ }
1406
+ throw new Error(TOKEN_ALREADY_USED_MESSAGE);
1407
+ }
1408
+ throw new Error("Invalid join token");
1409
+ }
1410
+ };
1411
+ /**
1412
+ * Build a JoinTokenManager backed by its own connection pool to the given
1413
+ * orchestrator database URL. Mirrors `createPeerCredentialStoreFromUrl`;
1414
+ * consumed by E2E tests that need to exercise token validation/reuse against
1415
+ * the real cluster DB.
1416
+ */
1417
+ function createJoinTokenManagerFromUrl(databaseUrl, opts) {
1418
+ const db = new Kysely({ dialect: new PostgresDialect({ pool: new pg.Pool({
1419
+ connectionString: databaseUrl,
1420
+ max: opts?.maxConnections ?? 3
1421
+ }) }) });
1422
+ return {
1423
+ manager: new JoinTokenManager({ db }),
1424
+ dispose: async () => {
1425
+ await db.destroy();
1426
+ }
1427
+ };
1428
+ }
1429
+ /**
1430
+ * Parse a join token string into routing info and secret hex.
1431
+ */
1432
+ function parseToken(token) {
1433
+ const parts = token.split(".");
1434
+ if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX) throw new Error(`Invalid join token format. Expected: ${TOKEN_PREFIX}.<routing>.<secret>`);
1435
+ const routingJson = Buffer.from(parts[1], "base64url").toString("utf-8");
1436
+ const routing = JSON.parse(routingJson);
1437
+ if (!routing.orgId || !routing.routingKey || !routing.expiry) throw new Error("Invalid join token routing data");
1438
+ return {
1439
+ routing,
1440
+ secretHex: parts[2]
1441
+ };
1442
+ }
1443
+ /**
1444
+ * Derive encryption key and validation hash from a token secret.
1445
+ * - encryptionKey: HKDF-SHA256 with salt="kici-join-encrypt", info="v1"
1446
+ * - validationHash: SHA-256 of secret (stored in DB for lookup)
1447
+ */
1448
+ function deriveKeys(secret) {
1449
+ const validationHash = sha256(secret);
1450
+ const derived = hkdfSync("sha256", secret, Buffer.from("kici-join-encrypt"), Buffer.from("v1"), 32);
1451
+ return {
1452
+ encryptionKey: Buffer.from(derived),
1453
+ validationHash
1454
+ };
1455
+ }
1456
+ //#endregion
1457
+ //#region src/cli/api-client.ts
1458
+ /**
1459
+ * HTTP client wrapper for the kici-admin CLI.
1460
+ *
1461
+ * Communicates with the orchestrator admin API via Bearer token authentication.
1462
+ * All methods are thin wrappers around fetch() that handle JSON serialization,
1463
+ * error formatting, and URL construction.
1464
+ */
1465
+ var AdminApiClient = class {
1466
+ baseUrl;
1467
+ token;
1468
+ constructor(baseUrl, token) {
1469
+ this.baseUrl = baseUrl;
1470
+ this.token = token;
1471
+ }
1472
+ /**
1473
+ * Make an authenticated HTTP request to the admin API.
1474
+ */
1475
+ async request(method, path, body) {
1476
+ const url = `${this.baseUrl}${path}`;
1477
+ const headers = {
1478
+ Authorization: `Bearer ${this.token}`,
1479
+ "Content-Type": "application/json"
1480
+ };
1481
+ const res = await fetch(url, {
1482
+ method,
1483
+ headers,
1484
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1485
+ });
1486
+ if (!res.ok) {
1487
+ const text = await res.text();
1488
+ let errorBody;
1489
+ try {
1490
+ errorBody = JSON.parse(text).error ?? text;
1491
+ } catch {
1492
+ errorBody = text;
1493
+ }
1494
+ throw new Error(`HTTP ${res.status}: ${errorBody}`);
1495
+ }
1496
+ if (res.status === 204) return;
1497
+ return await res.json();
1498
+ }
1499
+ /**
1500
+ * Public GET request returning parsed JSON.
1501
+ */
1502
+ async get(path) {
1503
+ return this.request("GET", path);
1504
+ }
1505
+ /**
1506
+ * Public POST request returning parsed JSON.
1507
+ */
1508
+ async post(path, body) {
1509
+ return this.request("POST", path, body);
1510
+ }
1511
+ /**
1512
+ * Public PATCH request returning parsed JSON.
1513
+ */
1514
+ async patch(path, body) {
1515
+ return this.request("PATCH", path, body);
1516
+ }
1517
+ /**
1518
+ * Public PUT request returning parsed JSON.
1519
+ */
1520
+ async put(path, body) {
1521
+ return this.request("PUT", path, body);
1522
+ }
1523
+ /**
1524
+ * Public DELETE request returning parsed JSON.
1525
+ */
1526
+ async delete(path) {
1527
+ return this.request("DELETE", path);
1528
+ }
1529
+ /**
1530
+ * Public GET request returning raw response text.
1531
+ */
1532
+ async getText(path) {
1533
+ const url = `${this.baseUrl}${path}`;
1534
+ const res = await fetch(url, {
1535
+ method: "GET",
1536
+ headers: { Authorization: `Bearer ${this.token}` }
1537
+ });
1538
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
1539
+ return res.text();
1540
+ }
1541
+ /** Enumerate the cluster topology for `debug-bundle --fleet --list` / `--pick`. */
1542
+ async getFleetTopology() {
1543
+ return this.get("/admin/fleet-topology");
1544
+ }
1545
+ /**
1546
+ * Drive the fleet fan-out and write the assembled ZIP to `outPath`. The
1547
+ * response is an octet-stream, so it is read as bytes rather than parsed JSON.
1548
+ */
1549
+ async downloadFleetBundle(body, outPath) {
1550
+ const res = await fetch(`${this.baseUrl}/admin/fleet-bundle`, {
1551
+ method: "POST",
1552
+ headers: {
1553
+ Authorization: `Bearer ${this.token}`,
1554
+ "Content-Type": "application/json"
1555
+ },
1556
+ body: JSON.stringify(body)
1557
+ });
1558
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
1559
+ const buf = Buffer.from(await res.arrayBuffer());
1560
+ fs.writeFileSync(outPath, buf);
1561
+ }
1562
+ async listScopes(orgId) {
1563
+ return this.request("GET", `/api/v1/admin/secrets/scopes?orgId=${encodeURIComponent(orgId)}`);
1564
+ }
1565
+ async listKeys(orgId, scope) {
1566
+ const params = new URLSearchParams({
1567
+ orgId,
1568
+ scope
1569
+ });
1570
+ return this.request("GET", `/api/v1/admin/secrets/keys?${params}`);
1571
+ }
1572
+ async setSecret(orgId, scope, key, value) {
1573
+ return this.request("PUT", `/api/v1/admin/secrets/${encodeURIComponent(orgId)}/${encodeURIComponent(scope)}/${encodeURIComponent(key)}`, { value });
1574
+ }
1575
+ async deleteSecret(orgId, scope, key) {
1576
+ return this.request("DELETE", `/api/v1/admin/secrets/${encodeURIComponent(orgId)}/${encodeURIComponent(scope)}/${encodeURIComponent(key)}`);
1577
+ }
1578
+ async listVariables(orgId, environment) {
1579
+ const params = new URLSearchParams({ orgId });
1580
+ return this.request("GET", `/api/v1/admin/environments/${encodeURIComponent(environment)}/variables?${params}`);
1581
+ }
1582
+ async setVariable(orgId, environment, key, value, locked) {
1583
+ const params = new URLSearchParams({ orgId });
1584
+ return this.request("PUT", `/api/v1/admin/environments/${encodeURIComponent(environment)}/variables/${encodeURIComponent(key)}?${params}`, {
1585
+ value,
1586
+ locked
1587
+ });
1588
+ }
1589
+ async deleteVariable(orgId, environment, key) {
1590
+ const params = new URLSearchParams({ orgId });
1591
+ return this.request("DELETE", `/api/v1/admin/environments/${encodeURIComponent(environment)}/variables/${encodeURIComponent(key)}?${params}`);
1592
+ }
1593
+ async rotateKey() {
1594
+ return this.request("POST", "/api/v1/admin/rotate-key");
1595
+ }
1596
+ async createGenericSource(data) {
1597
+ return this.request("POST", "/api/v1/admin/generic-sources", data);
1598
+ }
1599
+ async listGenericSources(orgId, includeDeleted) {
1600
+ const params = new URLSearchParams({ orgId });
1601
+ if (includeDeleted) params.set("includeDeleted", "true");
1602
+ return this.request("GET", `/api/v1/admin/generic-sources?${params}`);
1603
+ }
1604
+ async getGenericSource(id) {
1605
+ return this.request("GET", `/api/v1/admin/generic-sources/${encodeURIComponent(id)}`);
1606
+ }
1607
+ async updateGenericSource(id, data) {
1608
+ return this.request("PATCH", `/api/v1/admin/generic-sources/${encodeURIComponent(id)}`, data);
1609
+ }
1610
+ async deleteGenericSource(id, hard) {
1611
+ const qs = hard ? "?hard=true" : "";
1612
+ return this.request("DELETE", `/api/v1/admin/generic-sources/${encodeURIComponent(id)}${qs}`);
1613
+ }
1614
+ async enableGenericSource(id) {
1615
+ return this.request("POST", `/api/v1/admin/generic-sources/${encodeURIComponent(id)}/enable`);
1616
+ }
1617
+ async disableGenericSource(id) {
1618
+ return this.request("POST", `/api/v1/admin/generic-sources/${encodeURIComponent(id)}/disable`);
1619
+ }
1620
+ async queryAudit(opts) {
1621
+ const params = new URLSearchParams();
1622
+ if (opts?.contextName) params.set("contextName", opts.contextName);
1623
+ if (opts?.routingKey) params.set("routingKey", opts.routingKey);
1624
+ if (opts?.action) params.set("action", opts.action);
1625
+ if (opts?.from) params.set("from", opts.from);
1626
+ if (opts?.to) params.set("to", opts.to);
1627
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
1628
+ if (opts?.offset !== void 0) params.set("offset", String(opts.offset));
1629
+ if (opts?.includeArchived) params.set("includeArchived", "true");
1630
+ const qs = params.toString();
1631
+ return (await this.request("GET", `/api/v1/admin/audit${qs ? `?${qs}` : ""}`)).entries;
1632
+ }
1633
+ async createToken(data) {
1634
+ return this.request("POST", "/api/v1/admin/tokens", data);
1635
+ }
1636
+ async listTokens() {
1637
+ return (await this.request("GET", "/api/v1/admin/tokens")).tokens;
1638
+ }
1639
+ async revokeToken(id) {
1640
+ return this.request("DELETE", `/api/v1/admin/tokens/${encodeURIComponent(id)}`);
1641
+ }
1642
+ async createAgentToken(opts) {
1643
+ return this.request("POST", "/api/v1/agent-tokens", opts);
1644
+ }
1645
+ async listAgentTokens(opts) {
1646
+ const qs = opts?.type ? `?type=${encodeURIComponent(opts.type)}` : "";
1647
+ return this.request("GET", `/api/v1/agent-tokens${qs}`);
1648
+ }
1649
+ /**
1650
+ * Revoke an agent token.
1651
+ *
1652
+ * The orchestrator both flips `agent_tokens.revoked_at` AND
1653
+ * synchronously closes every in-flight agent WS authenticated by
1654
+ * this token. The returned `kicked` count is the number
1655
+ * of WS connections that were closed on the wire — surface it to
1656
+ * the operator so they know the revocation actually propagated.
1657
+ */
1658
+ async revokeAgentToken(id) {
1659
+ return this.request("DELETE", `/api/v1/agent-tokens/${encodeURIComponent(id)}`);
1660
+ }
1661
+ async configSeed(config, description) {
1662
+ return this.request("POST", "/admin/config/seed", {
1663
+ config,
1664
+ description
1665
+ });
1666
+ }
1667
+ async configGet(path) {
1668
+ const qs = path ? `?path=${encodeURIComponent(path)}` : "";
1669
+ return this.request("GET", `/admin/config${qs}`);
1670
+ }
1671
+ async configSet(path, value, description) {
1672
+ return this.request("PUT", "/admin/config", {
1673
+ path,
1674
+ value,
1675
+ description
1676
+ });
1677
+ }
1678
+ async configDelete(path, description) {
1679
+ return this.request("DELETE", "/admin/config", {
1680
+ path,
1681
+ description
1682
+ });
1683
+ }
1684
+ async configExport() {
1685
+ return this.request("GET", "/admin/config/export");
1686
+ }
1687
+ async configValidate(config, type) {
1688
+ return this.request("POST", "/admin/config/validate", {
1689
+ config,
1690
+ type
1691
+ });
1692
+ }
1693
+ async configDiff() {
1694
+ return this.request("GET", "/admin/config/diff");
1695
+ }
1696
+ async configHistory(limit) {
1697
+ const qs = limit !== void 0 ? `?limit=${limit}` : "";
1698
+ return this.request("GET", `/admin/config/history${qs}`);
1699
+ }
1700
+ async configRollback(version) {
1701
+ return this.request("POST", "/admin/config/rollback", { version });
1702
+ }
1703
+ async configReload(opts) {
1704
+ return this.request("POST", "/admin/config/reload", opts);
1705
+ }
1706
+ async createApiKey(opts) {
1707
+ return this.request("POST", "/api/v1/api-keys", opts);
1708
+ }
1709
+ async addRoutingKeyPermission(keyId, pattern) {
1710
+ return this.request("POST", `/api/v1/api-keys/${encodeURIComponent(keyId)}/routing-permissions`, { pattern });
1711
+ }
1712
+ async addBackend(params) {
1713
+ return this.request("POST", "/api/v1/admin/backends", params);
1714
+ }
1715
+ async removeBackend(name) {
1716
+ return this.request("DELETE", `/api/v1/admin/backends/${encodeURIComponent(name)}`);
1717
+ }
1718
+ async listBackends() {
1719
+ return this.request("GET", "/api/v1/admin/backends");
1720
+ }
1721
+ async getBackend(name) {
1722
+ return this.request("GET", `/api/v1/admin/backends/${encodeURIComponent(name)}`);
1723
+ }
1724
+ async testBackend(params) {
1725
+ return this.request("POST", "/api/v1/admin/backends/test", params);
1726
+ }
1727
+ async testNamedBackend(name) {
1728
+ return this.request("POST", `/api/v1/admin/backends/${encodeURIComponent(name)}/test`);
1729
+ }
1730
+ async syncBackend(name) {
1731
+ return this.request("POST", `/api/v1/admin/backends/${encodeURIComponent(name)}/sync`);
1732
+ }
1733
+ async syncAllBackends() {
1734
+ return this.request("POST", "/api/v1/admin/backends/sync");
1735
+ }
1736
+ /**
1737
+ * List execution runs. `status` accepts either a single status or a
1738
+ * comma-separated list (e.g. `success,failed`). `since` is an ISO-8601
1739
+ * timestamp; only runs with `created_at` strictly later than this value
1740
+ * are returned.
1741
+ */
1742
+ async listRuns(opts) {
1743
+ const params = new URLSearchParams();
1744
+ if (opts?.status) params.set("status", opts.status);
1745
+ if (opts?.workflowName) params.set("workflowName", opts.workflowName);
1746
+ if (opts?.repo) params.set("repo", opts.repo);
1747
+ if (opts?.since) params.set("since", opts.since);
1748
+ if (opts?.limit) params.set("limit", String(opts.limit));
1749
+ if (opts?.offset) params.set("offset", String(opts.offset));
1750
+ const qs = params.toString();
1751
+ return this.request("GET", `/api/v1/admin/runs${qs ? `?${qs}` : ""}`);
1752
+ }
1753
+ /**
1754
+ * Count matching runs without returning the row list. Wraps the list
1755
+ * endpoint with `?count=true`, which skips the row query server-side.
1756
+ */
1757
+ async countRuns(opts) {
1758
+ const params = new URLSearchParams();
1759
+ params.set("count", "true");
1760
+ if (opts?.status) params.set("status", opts.status);
1761
+ if (opts?.workflowName) params.set("workflowName", opts.workflowName);
1762
+ if (opts?.repo) params.set("repo", opts.repo);
1763
+ if (opts?.since) params.set("since", opts.since);
1764
+ return this.request("GET", `/api/v1/admin/runs?${params.toString()}`);
1765
+ }
1766
+ /**
1767
+ * Fetch the run header (no jobs, no steps). Use `getRunJobs` for the
1768
+ * jobs sub-resource.
1769
+ */
1770
+ async getRun(runId) {
1771
+ return this.request("GET", `/api/v1/admin/runs/${encodeURIComponent(runId)}`);
1772
+ }
1773
+ /**
1774
+ * Fetch the jobs list for a run. When `includeSteps: true`, each job
1775
+ * entry embeds a `steps[]` array.
1776
+ */
1777
+ async getRunJobs(runId, opts) {
1778
+ const qs = opts?.includeSteps ? "?includeSteps=true" : "";
1779
+ return this.request("GET", `/api/v1/admin/runs/${encodeURIComponent(runId)}/jobs${qs}`);
1780
+ }
1781
+ /**
1782
+ * Fetch the scrub status of the run's ephemeral key. Never returns the
1783
+ * key material itself — only `{ exists, createdAt }`.
1784
+ */
1785
+ async getRunEphemeralKey(runId) {
1786
+ return this.request("GET", `/api/v1/admin/runs/${encodeURIComponent(runId)}/ephemeral-key`);
1787
+ }
1788
+ /**
1789
+ * List secret outputs for a run. Values are masked unless `reveal: true`
1790
+ * is passed AND the calling token has the `secret.reveal` permission.
1791
+ * Every reveal call writes a `secret-outputs.reveal` row to the
1792
+ * secret_audit_log table.
1793
+ */
1794
+ async getRunSecretOutputs(runId, opts) {
1795
+ const params = new URLSearchParams();
1796
+ if (opts?.outputKey) params.set("outputKey", opts.outputKey);
1797
+ if (opts?.reveal) params.set("reveal", "true");
1798
+ const qs = params.toString();
1799
+ return this.request("GET", `/api/v1/admin/runs/${encodeURIComponent(runId)}/secret-outputs${qs ? `?${qs}` : ""}`);
1800
+ }
1801
+ async listEventLog(opts) {
1802
+ const params = new URLSearchParams();
1803
+ if (opts?.orgId) params.set("orgId", opts.orgId);
1804
+ if (opts?.routingKey) params.set("routingKey", opts.routingKey);
1805
+ if (opts?.event) params.set("event", opts.event);
1806
+ if (opts?.action) params.set("action", opts.action);
1807
+ if (opts?.status) params.set("status", opts.status);
1808
+ if (opts?.from) params.set("from", opts.from);
1809
+ if (opts?.to) params.set("to", opts.to);
1810
+ if (opts?.deliveryId) params.set("deliveryId", opts.deliveryId);
1811
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
1812
+ if (opts?.offset !== void 0) params.set("offset", String(opts.offset));
1813
+ if (opts?.includeArchived) params.set("includeArchived", "true");
1814
+ const qs = params.toString();
1815
+ return this.request("GET", `/api/v1/admin/event-log${qs ? `?${qs}` : ""}`);
1816
+ }
1817
+ async getEventLog(deliveryId, opts) {
1818
+ const params = new URLSearchParams({ orgId: opts.orgId });
1819
+ if (opts.includePayload) params.set("includePayload", "true");
1820
+ if (opts.routingKey) params.set("routingKey", opts.routingKey);
1821
+ return this.request("GET", `/api/v1/admin/event-log/${encodeURIComponent(deliveryId)}?${params}`);
1822
+ }
1823
+ async listAccessLog(opts) {
1824
+ const params = new URLSearchParams();
1825
+ if (opts?.orgId) params.set("orgId", opts.orgId);
1826
+ if (opts?.actorType) params.set("actorType", opts.actorType);
1827
+ if (opts?.actorId) params.set("actorId", opts.actorId);
1828
+ if (opts?.action) params.set("action", opts.action);
1829
+ if (opts?.source) params.set("source", opts.source);
1830
+ if (opts?.outcome) params.set("outcome", opts.outcome);
1831
+ if (opts?.targetType) params.set("targetType", opts.targetType);
1832
+ if (opts?.targetId) params.set("targetId", opts.targetId);
1833
+ if (opts?.from) params.set("from", opts.from);
1834
+ if (opts?.to) params.set("to", opts.to);
1835
+ if (opts?.q) params.set("q", opts.q);
1836
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
1837
+ if (opts?.cursor) params.set("cursor", opts.cursor);
1838
+ const qs = params.toString();
1839
+ return this.request("GET", `/api/v1/admin/access-log${qs ? `?${qs}` : ""}`);
1840
+ }
1841
+ async getAccessLogEntry(id, opts) {
1842
+ const params = new URLSearchParams();
1843
+ if (opts?.orgId) params.set("orgId", opts.orgId);
1844
+ const qs = params.toString();
1845
+ return this.request("GET", `/api/v1/admin/access-log/${encodeURIComponent(id)}${qs ? `?${qs}` : ""}`);
1846
+ }
1847
+ async diagnose() {
1848
+ return this.request("GET", "/admin/diagnose");
1849
+ }
1850
+ };
1851
+ //#endregion
1852
+ //#region src/providers/github/auth.ts
1853
+ /**
1854
+ * GitHub clone token provider and authentication utilities.
1855
+ *
1856
+ * Implements the CloneTokenProvider interface from @kici-dev/engine for GitHub.
1857
+ * Uses GitHub App installation tokens for repository access.
1858
+ */
1859
+ /**
1860
+ * Create an Octokit instance authenticated as a GitHub App installation.
1861
+ *
1862
+ * Uses @octokit/auth-app to handle:
1863
+ * - JWT generation for app-level authentication
1864
+ * - Installation token creation and automatic refresh
1865
+ *
1866
+ * Each call creates a new Octokit instance scoped to the specific installation.
1867
+ * The auth-app strategy handles token caching internally per Octokit instance.
1868
+ *
1869
+ * @param config - GitHub App credentials (appId + privateKey)
1870
+ * @param installationId - GitHub App installation ID for the target account/org
1871
+ * @returns Octokit instance with auto-refreshing installation tokens
1872
+ */
1873
+ function createInstallationOctokit(config, installationId) {
1874
+ return new Octokit({
1875
+ authStrategy: createAppAuth,
1876
+ auth: {
1877
+ appId: config.appId,
1878
+ privateKey: config.privateKey,
1879
+ installationId
1880
+ }
1881
+ });
1882
+ }
1883
+ //#endregion
1884
+ //#region src/providers/github/manifest.ts
1885
+ /**
1886
+ * GitHub App Manifest flow helpers. The manifest encodes KiCI's exact
1887
+ * permissions + events + webhook config so the operator never picks them by
1888
+ * hand — GitHub creates a correctly-configured App from this object in one
1889
+ * click. See docs.github.com "Registering a GitHub App from a manifest".
1890
+ *
1891
+ * The webhook secret is NOT part of the manifest: GitHub generates it during
1892
+ * registration and returns it on the conversion response, so both GitHub and
1893
+ * the Platform end up sharing the same secret with zero operator effort.
1894
+ */
1895
+ /**
1896
+ * Validate a self-hosted webhook URL supplied via `source add github
1897
+ * --webhook-url`. Must be a well-formed absolute `https://` URL. Returns the URL
1898
+ * verbatim on success; throws a clear error otherwise. The validated URL is
1899
+ * baked into `manifest.hook_attributes.url` as-is — KiCI adds no ingress and
1900
+ * does not receive events at it; the operator owns delivery.
1901
+ */
1902
+ function validateWebhookUrl(value) {
1903
+ let url;
1904
+ try {
1905
+ url = new URL(value);
1906
+ } catch {
1907
+ throw new Error(`--webhook-url must be a valid absolute URL: ${value}`);
1908
+ }
1909
+ if (url.protocol !== "https:") throw new Error(`--webhook-url must be an https:// URL (got ${url.protocol}//…)`);
1910
+ return value;
1911
+ }
1912
+ function buildGithubAppManifest(input) {
1913
+ return {
1914
+ name: input.name,
1915
+ url: "https://kici.dev",
1916
+ hook_attributes: {
1917
+ url: input.webhookUrl,
1918
+ active: true
1919
+ },
1920
+ redirect_url: input.redirectUrl,
1921
+ ...input.setupUrl ? { setup_url: input.setupUrl } : {},
1922
+ public: false,
1923
+ default_permissions: {
1924
+ contents: "read",
1925
+ metadata: "read",
1926
+ pull_requests: "read",
1927
+ checks: "write",
1928
+ members: "read"
1929
+ },
1930
+ default_events: [
1931
+ "push",
1932
+ "pull_request",
1933
+ "check_run",
1934
+ "check_suite"
1935
+ ]
1936
+ };
1937
+ }
1938
+ /**
1939
+ * Exchange the short-lived manifest `code` for the App's id, private key, and
1940
+ * webhook secret. Runs server-to-server directly against GitHub — the private
1941
+ * key never transits the Platform.
1942
+ */
1943
+ async function convertManifestCode(code, deps = {}) {
1944
+ const { data } = await (deps.octokit ?? new Octokit()).request("POST /app-manifests/{code}/conversions", { code });
1945
+ const d = data;
1946
+ if (!d.webhook_secret) throw new Error("GitHub returned no webhook secret for the new App — cannot verify inbound events. Re-run the setup, or configure a webhook secret manually with source update.");
1947
+ return {
1948
+ appId: String(d.id),
1949
+ slug: d.slug,
1950
+ name: d.name,
1951
+ privateKey: d.pem,
1952
+ webhookSecret: d.webhook_secret,
1953
+ clientId: d.client_id,
1954
+ clientSecret: d.client_secret,
1955
+ htmlUrl: d.html_url
1956
+ };
1957
+ }
1958
+ function appOctokitFor(creds) {
1959
+ return new Octokit({
1960
+ authStrategy: createAppAuth,
1961
+ auth: {
1962
+ appId: creds.appId,
1963
+ privateKey: creds.privateKey
1964
+ }
1965
+ });
1966
+ }
1967
+ /**
1968
+ * Poll GitHub (as the App, via a JWT) until at least one installation exists,
1969
+ * returning the first installation's id + account login. Throws on timeout.
1970
+ */
1971
+ async function waitForInstallation(creds, opts) {
1972
+ const now = opts.now ?? Date.now;
1973
+ const octokit = opts.appOctokit ?? appOctokitFor(creds);
1974
+ const deadline = now() + opts.timeoutMs;
1975
+ for (;;) {
1976
+ const { data } = await octokit.request("GET /app/installations", { per_page: 1 });
1977
+ const installs = data;
1978
+ if (Array.isArray(installs) && installs.length > 0) return {
1979
+ installationId: installs[0].id,
1980
+ accountLogin: installs[0].account?.login ?? ""
1981
+ };
1982
+ if (now() >= deadline) throw new Error("Timed out waiting for the GitHub App to be installed");
1983
+ await new Promise((r) => setTimeout(r, opts.pollMs));
1984
+ }
1985
+ }
1986
+ /**
1987
+ * Mint an installation token from the captured private key and confirm the App
1988
+ * can reach repos — proves the key works end-to-end (the same path the agent's
1989
+ * clone uses at runtime).
1990
+ */
1991
+ async function verifyRepoAccess(creds, installationId, deps = {}) {
1992
+ const { data } = await (deps.octokit ?? createInstallationOctokit(creds, installationId)).request("GET /installation/repositories", { per_page: 1 });
1993
+ return { repoCount: data.total_count };
1994
+ }
1995
+ //#endregion
1996
+ //#region src/providers/github/manifest-form.ts
1997
+ /**
1998
+ * GitHub's create-from-manifest flow is an HTML form POST (the `manifest`
1999
+ * JSON cannot ride in a query string). These helpers render the auto-submitting
2000
+ * form and the headless code-display page. Pure string builders — no IO — so
2001
+ * both the CLI loopback server and the static marketing-site page can reuse the
2002
+ * same shapes.
2003
+ */
2004
+ function escapeHtml(s) {
2005
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2006
+ }
2007
+ function manifestCreateUrl(githubOrg) {
2008
+ return githubOrg ? `https://github.com/organizations/${githubOrg}/settings/apps/new` : "https://github.com/settings/apps/new";
2009
+ }
2010
+ function renderManifestFormHtml(opts) {
2011
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Create your KiCI GitHub App</title></head>
2012
+ <body>
2013
+ <p>Redirecting you to GitHub to create your KiCI GitHub App…</p>
2014
+ <form id="f" method="post" action="${escapeHtml(`${opts.createUrl}?state=${encodeURIComponent(opts.state)}`)}">
2015
+ <input type="hidden" name="manifest" value="${escapeHtml(opts.manifestJson)}">
2016
+ <noscript><button type="submit">Continue to GitHub</button></noscript>
2017
+ </form>
2018
+ <script>document.getElementById('f').submit();<\/script>
2019
+ </body></html>`;
2020
+ }
2021
+ function renderCodeDisplayHtml(code) {
2022
+ return `<!doctype html><html><head><meta charset="utf-8"><title>KiCI — copy your setup code</title></head>
2023
+ <body>
2024
+ <p>App created. Copy this code back into the <code>kici-admin</code> prompt:</p>
2025
+ <pre id="c" style="font-size:1.2rem;padding:12px;border:1px solid #ccc">${escapeHtml(code)}</pre>
2026
+ <button onclick="navigator.clipboard.writeText(document.getElementById('c').textContent)">Copy</button>
2027
+ </body></html>`;
2028
+ }
2029
+ //#endregion
2030
+ //#region src/cli/loopback-callback.ts
2031
+ function startManifestLoopback(opts) {
2032
+ return new Promise((resolveServer) => {
2033
+ let outcome;
2034
+ const waiters = [];
2035
+ const resolveCode = (value) => {
2036
+ if (outcome) return;
2037
+ outcome = {
2038
+ ok: true,
2039
+ value
2040
+ };
2041
+ for (const w of waiters) w.resolve(value);
2042
+ };
2043
+ const rejectCode = (error) => {
2044
+ if (outcome) return;
2045
+ outcome = {
2046
+ ok: false,
2047
+ error
2048
+ };
2049
+ for (const w of waiters) w.reject(error);
2050
+ };
2051
+ const server = createServer((req, res) => {
2052
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
2053
+ if (url.pathname === "/cb") {
2054
+ const code = url.searchParams.get("code") ?? "";
2055
+ const state = url.searchParams.get("state") ?? "";
2056
+ res.writeHead(200, { "content-type": "text/html" });
2057
+ res.end(renderCodeDisplayHtml(code));
2058
+ if (state !== opts.state) rejectCode(/* @__PURE__ */ new Error("OAuth state mismatch — aborting"));
2059
+ else resolveCode({
2060
+ code,
2061
+ state
2062
+ });
2063
+ return;
2064
+ }
2065
+ res.writeHead(200, { "content-type": "text/html" });
2066
+ res.end(renderManifestFormHtml({
2067
+ createUrl: opts.createUrl,
2068
+ state: opts.state,
2069
+ manifestJson: opts.manifestJson
2070
+ }));
2071
+ });
2072
+ server.on("clientError", () => {});
2073
+ server.listen(0, "127.0.0.1", () => {
2074
+ const base = `http://127.0.0.1:${server.address().port}`;
2075
+ resolveServer({
2076
+ redirectUrl: `${base}/cb`,
2077
+ formUrl: `${base}/`,
2078
+ waitForCode: (timeoutMs) => new Promise((resolve, reject) => {
2079
+ if (outcome) {
2080
+ outcome.ok ? resolve(outcome.value) : reject(outcome.error);
2081
+ return;
2082
+ }
2083
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Timed out waiting for GitHub callback")), timeoutMs);
2084
+ waiters.push({
2085
+ resolve: (v) => {
2086
+ clearTimeout(timer);
2087
+ resolve(v);
2088
+ },
2089
+ reject: (e) => {
2090
+ clearTimeout(timer);
2091
+ reject(e);
2092
+ }
2093
+ });
2094
+ }),
2095
+ close: () => server.close()
2096
+ });
2097
+ });
2098
+ });
2099
+ }
2100
+ //#endregion
2101
+ //#region src/cli/open-browser.ts
2102
+ /**
2103
+ * Open a URL in the operator's default browser, best-effort. Used by the GitHub
2104
+ * App manifest setup flow to launch the create / install pages. Failure is
2105
+ * non-fatal — the caller always prints the URL too, so a headless host (or a
2106
+ * blocked launcher) just falls back to copy-paste.
2107
+ *
2108
+ * `KICI_BROWSER_CMD` overrides the launcher: `none` suppresses it entirely
2109
+ * (E2E / headless capture), any other value is run with `{url}` substituted —
2110
+ * mirroring the `kici login` convention.
2111
+ */
2112
+ function openBrowserBestEffort(url) {
2113
+ const override = process.env.KICI_BROWSER_CMD;
2114
+ if (override === "none") return;
2115
+ if (override) {
2116
+ exec(override.replace("{url}", url), () => {});
2117
+ return;
2118
+ }
2119
+ exec(platform() === "darwin" ? `open ${JSON.stringify(url)}` : platform() === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`, () => {});
2120
+ }
2121
+ //#endregion
2122
+ //#region src/cli/commands/source-manifest.ts
2123
+ /**
2124
+ * `kici-admin source add github --manifest` — the one-click GitHub App setup
2125
+ * flow. Drives GitHub's App Manifest flow end-to-end: builds a pre-configured
2126
+ * manifest (KiCI's exact permissions/events/webhook URL), hands it to GitHub via
2127
+ * an auto-submitting form, catches the returned short-lived setup code over a
2128
+ * localhost loopback (or copy-paste in --no-browser mode), exchanges it for the
2129
+ * App's id + private key + webhook secret, then reuses the existing source
2130
+ * storage + Platform-registration path.
2131
+ *
2132
+ * The private-key-bearing conversion happens entirely here on the orchestrator
2133
+ * host — it never transits the Platform (sovereignty invariant).
2134
+ */
2135
+ /** Static marketing-site page the CLI points at in headless paste-code mode. */
2136
+ const STATIC_CALLBACK_URL = "https://kici.dev/gh-manifest-callback";
2137
+ /** How long to wait for the operator to click "Create" on GitHub. */
2138
+ const CREATE_TIMEOUT_MS = 5 * 6e4;
2139
+ /** How long to wait for the operator to install the App on at least one repo. */
2140
+ const INSTALL_TIMEOUT_MS = 5 * 6e4;
2141
+ const INSTALL_POLL_MS = 3e3;
2142
+ function defaultReadLine(prompt) {
2143
+ const rl = createInterface({
2144
+ input: process.stdin,
2145
+ output: process.stdout
2146
+ });
2147
+ return new Promise((resolve) => {
2148
+ rl.question(prompt, (answer) => {
2149
+ rl.close();
2150
+ resolve(answer.trim());
2151
+ });
2152
+ });
2153
+ }
2154
+ function defaultWriteRecoveryFile(appId, pem) {
2155
+ const file = join(mkdtempSync(join(tmpdir(), "kici-gh-app-")), `app-${appId}.private-key.pem`);
2156
+ writeFileSync(file, pem, { mode: 384 });
2157
+ return file;
2158
+ }
2159
+ const realManifestSetupDeps = {
2160
+ startLoopback: startManifestLoopback,
2161
+ openBrowser: openBrowserBestEffort,
2162
+ readLine: defaultReadLine,
2163
+ convert: (code) => convertManifestCode(code),
2164
+ waitForInstallation,
2165
+ verifyRepoAccess,
2166
+ writeRecoveryFile: defaultWriteRecoveryFile
2167
+ };
2168
+ /** Resolve the org-scoped webhook URL via the pre-flight route; abort if null. */
2169
+ async function resolveWebhookUrl(client) {
2170
+ const pre = await client.get("/api/v1/admin/sources/github-webhook-url");
2171
+ if (!pre.webhookUrl) throw new Error(`Cannot resolve the GitHub webhook URL${pre.webhookNote ? ` (${pre.webhookNote})` : ""}. The manifest flow needs it to configure the App. Ensure the orchestrator is connected to the Platform and has identified its org, then retry.`);
2172
+ return pre.webhookUrl;
2173
+ }
2174
+ /**
2175
+ * Catch the short-lived setup code: over the loopback (browser mode) or by
2176
+ * printing the static-site URL and reading the pasted code (headless mode).
2177
+ */
2178
+ async function captureCode(opts, deps, manifestJson, state, redirectUrl, createUrl, loopback) {
2179
+ if (opts.noBrowser) {
2180
+ const url = `${STATIC_CALLBACK_URL}#m=${Buffer.from(manifestJson, "utf-8").toString("base64url")}&state=${encodeURIComponent(state)}&createUrl=${encodeURIComponent(createUrl)}`;
2181
+ console.log("Open this URL in a browser to create your App:");
2182
+ console.log(` ${url}`);
2183
+ console.log("After clicking \"Create\", copy the setup code shown on the page.");
2184
+ const code = await deps.readLine("Paste the setup code here: ");
2185
+ if (!code) throw new Error("No setup code was provided");
2186
+ return code;
2187
+ }
2188
+ console.log("→ Opening GitHub to create your App…");
2189
+ await deps.openBrowser(loopback.formUrl);
2190
+ console.log(` (if your browser did not open, visit ${loopback.formUrl} )`);
2191
+ const { code } = await loopback.waitForCode(CREATE_TIMEOUT_MS);
2192
+ return code;
2193
+ }
2194
+ /** Store credentials + register; on failure, save the PEM so the App is not orphaned. */
2195
+ async function storeAndRegister(opts, client, deps, creds) {
2196
+ try {
2197
+ return await client.post("/api/v1/admin/sources", {
2198
+ provider: "github",
2199
+ name: creds.name,
2200
+ slug: creds.slug,
2201
+ appId: creds.appId,
2202
+ privateKey: creds.privateKey,
2203
+ webhookSecret: creds.webhookSecret
2204
+ });
2205
+ } catch (err) {
2206
+ const file = deps.writeRecoveryFile(creds.appId, creds.privateKey);
2207
+ console.error("");
2208
+ console.error("⚠ The GitHub App was created but storing it on the orchestrator failed.");
2209
+ console.error(` App id: ${creds.appId}`);
2210
+ console.error(` Private key saved to: ${file}`);
2211
+ console.error(" Recover with:");
2212
+ console.error(` kici-admin source add github --name "${opts.name}" --app-id ${creds.appId} --private-key @${file} --webhook-secret ${creds.webhookSecret}`);
2213
+ throw err;
2214
+ }
2215
+ }
2216
+ async function runGithubManifestSetup(opts, client, deps = realManifestSetupDeps) {
2217
+ const webhookUrl = opts.webhookUrl ? validateWebhookUrl(opts.webhookUrl) : await resolveWebhookUrl(client);
2218
+ const state = randomBytes(16).toString("hex");
2219
+ const createUrl = manifestCreateUrl(opts.githubOrg);
2220
+ const loopback = await deps.startLoopback({
2221
+ state,
2222
+ manifestJson: "{}",
2223
+ createUrl
2224
+ });
2225
+ try {
2226
+ const manifest = buildGithubAppManifest({
2227
+ name: opts.name,
2228
+ webhookUrl,
2229
+ redirectUrl: opts.noBrowser ? STATIC_CALLBACK_URL : loopback.redirectUrl
2230
+ });
2231
+ const manifestJson = JSON.stringify(manifest);
2232
+ loopback.close();
2233
+ const lb = await deps.startLoopback({
2234
+ state,
2235
+ manifestJson,
2236
+ createUrl
2237
+ });
2238
+ try {
2239
+ const code = await captureCode(opts, deps, manifestJson, state, lb.redirectUrl, createUrl, lb);
2240
+ const creds = await deps.convert(code);
2241
+ console.log(`→ ✓ App created (id ${creds.appId}), credentials captured`);
2242
+ const stored = await storeAndRegister(opts, client, deps, creds);
2243
+ console.log(`→ ✓ Stored on orchestrator (encrypted), registered as ${stored.routingKey}`);
2244
+ const installUrl = `https://github.com/apps/${creds.slug}/installations/new`;
2245
+ console.log(`→ Install the App on your repos: ${installUrl}`);
2246
+ if (!opts.noBrowser) await deps.openBrowser(installUrl);
2247
+ const { installationId, accountLogin } = await deps.waitForInstallation(creds, {
2248
+ timeoutMs: INSTALL_TIMEOUT_MS,
2249
+ pollMs: INSTALL_POLL_MS
2250
+ });
2251
+ console.log(`→ ✓ Installation detected (account ${accountLogin})`);
2252
+ const { repoCount } = await deps.verifyRepoAccess(creds, installationId);
2253
+ console.log(`→ ✓ Credentials verified (${repoCount} repositories reachable)`);
2254
+ console.log("");
2255
+ console.log(`GitHub App "${opts.name}" is live.`);
2256
+ console.log(` Webhook: ${webhookUrl}`);
2257
+ } finally {
2258
+ lb.close();
2259
+ }
2260
+ } finally {
2261
+ loopback.close();
2262
+ }
2263
+ }
2264
+ //#endregion
2265
+ export { AdminApiClient, DEFAULT_USER_CACHE_QUOTA_BYTES, DEFAULT_USER_CACHE_TTL_MS, DepCache, JoinTokenManager, PeerAuthCoordinator, PeerCredentialStore, S3CacheStorage, SourceCache, UserCache, createCacheStorage, createJoinTokenManagerFromUrl, createPeerCredentialStoreFromUrl, runGithubManifestSetup };
1112
2266
 
1113
2267
  //# sourceMappingURL=index.js.map