@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.
- package/dist/__test-helpers__/peer-mutual-auth.d.ts +42 -0
- package/dist/__test-helpers__/scratch-db.d.ts +16 -0
- package/dist/agent/host-roster.d.ts +100 -37
- package/dist/agent/registry.d.ts +1 -8
- package/dist/app-on-error.d.ts +1 -1
- package/dist/app.d.ts +4 -2
- package/dist/artifacts/artifact-store.d.ts +0 -2
- package/dist/audit/access-log.d.ts +1 -1
- package/dist/cache/source-cache.d.ts +1 -1
- package/dist/cache/user-cache.d.ts +0 -28
- package/dist/cli/api-client.d.ts +27 -1
- package/dist/cli/commands/access-log.d.ts +1 -1
- package/dist/cli/commands/cold-store-impl.d.ts +7 -84
- package/dist/cli/commands/db.d.ts +1 -0
- package/dist/cli/commands/host.d.ts +3 -0
- package/dist/cli/commands/orchestrator-service/status.d.ts +2 -0
- package/dist/cli/commands/org.d.ts +12 -0
- package/dist/cli/commands/scaler.d.ts +14 -0
- package/dist/cli/commands/secret.d.ts +1 -45
- package/dist/cli/commands/service-health.d.ts +21 -13
- package/dist/cli/commands/shared/cli-action.d.ts +17 -0
- package/dist/cli/commands/shared/db.d.ts +5 -5
- package/dist/cli/commands/shared/org-id.d.ts +5 -0
- package/dist/cli/commands/shared/service-verbs.d.ts +19 -0
- package/dist/cli/commands/shared/table.d.ts +3 -0
- package/dist/cli/commands/shared/unbound-context-warning.d.ts +2 -22
- package/dist/cli/commands/source-manifest.d.ts +3 -3
- package/dist/cli/commands/source.d.ts +23 -0
- package/dist/cli/commands/trust-policy.d.ts +7 -31
- package/dist/cli/join.d.ts +2 -2
- package/dist/cli/kici-admin.d.ts +6 -1
- package/dist/cli/local-github-ingress-url.d.ts +6 -0
- package/dist/cli/root-option-routing.d.ts +46 -0
- package/dist/cli/webhook-url-notes.d.ts +15 -0
- package/dist/cli.js +10667 -8461
- package/dist/cluster/coordinator-credential.d.ts +21 -0
- package/dist/cluster/coordinator.d.ts +2 -2
- package/dist/cluster/index.d.ts +4 -4
- package/dist/cluster/join-client.d.ts +23 -21
- package/dist/cluster/join-handler.d.ts +24 -16
- package/dist/cluster/join-protocol-v2.d.ts +84 -0
- package/dist/cluster/join-token.d.ts +78 -56
- package/dist/cluster/peer-auth-coordinator.d.ts +16 -2
- package/dist/cluster/peer-client.d.ts +99 -5
- package/dist/cluster/peer-crypto.d.ts +60 -3
- package/dist/cluster/peer-discovery.d.ts +67 -0
- package/dist/cluster/peer-forget.d.ts +4 -9
- package/dist/cluster/peer-handler.d.ts +28 -6
- package/dist/cluster/peer-registry.d.ts +15 -1
- package/dist/cluster/peer-request-waiters.d.ts +26 -0
- package/dist/cluster/scaler-orphans-peer.d.ts +4 -9
- package/dist/cluster/scaler-reload-fanout.d.ts +29 -0
- package/dist/cluster/scaler-reload-peer.d.ts +16 -0
- package/dist/cluster/worker-cluster-settings.d.ts +1 -0
- package/dist/cold-store/load-access-log-range.d.ts +3 -3
- package/dist/cold-store/tables/access-log.d.ts +4 -12
- package/dist/cold-store/tables/event-log.d.ts +8 -19
- package/dist/cold-store/tables/execution-jobs.d.ts +7 -24
- package/dist/cold-store/tables/execution-runs.d.ts +6 -20
- package/dist/cold-store/tables/execution-steps.d.ts +8 -26
- package/dist/cold-store/tables/secret-audit-log.d.ts +4 -11
- package/dist/config/env-overlay.d.ts +3 -8
- package/dist/config/reload.d.ts +9 -2
- package/dist/config/schema.d.ts +16 -2
- package/dist/config/types.d.ts +6 -2
- package/dist/config.d.ts +22 -17
- package/dist/contexts/held-runs.d.ts +3 -6
- package/dist/dashboard/cursor.d.ts +12 -0
- package/dist/dashboard/handler.d.ts +5 -12
- package/dist/db/deferred-index-build.d.ts +42 -0
- package/dist/db/deferred-indexes.d.ts +3 -37
- package/dist/db/migrations/100_held_runs_hold_type_vocabulary.d.ts +3 -5
- package/dist/db/migrations/157_host_roster_identity_source.d.ts +21 -0
- package/dist/db/migrations/158_normalize_legacy_values.d.ts +4 -0
- package/dist/db/repos/org-ids-repo.d.ts +87 -0
- package/dist/db/schema-snapshot.generated.d.ts +3 -0
- package/dist/db/types.d.ts +21 -24
- package/dist/fault-injection-types.d.ts +0 -6
- package/dist/git/dispatch-git-auth.d.ts +6 -12
- package/dist/github-app-name-refresher/github-app-name-refresher.d.ts +25 -13
- package/dist/helpers/json.d.ts +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +357 -216
- package/dist/metrics/prometheus.d.ts +0 -5
- package/dist/oidc/id-token-claims.d.ts +1 -1
- package/dist/oidc/resolve-signer.d.ts +2 -4
- package/dist/orchestrator-core.d.ts +10 -3
- package/dist/pipeline/global-eval-round.d.ts +2 -26
- package/dist/pipeline/install-secrets-resolver.d.ts +0 -3
- package/dist/pipeline/manual-schedule.d.ts +1 -7
- package/dist/provenance/dispatch-context.d.ts +5 -7
- package/dist/provenance/pending-attestations-repo.d.ts +1 -1
- package/dist/provenance/trust-root.d.ts +4 -5
- package/dist/provenance/verify-deferred-capture.d.ts +3 -3
- package/dist/provider-registry.d.ts +13 -40
- package/dist/providers/github/manifest.d.ts +58 -7
- package/dist/providers/universal-git/ssh-auth.d.ts +0 -5
- package/dist/reporting/check-run-reporter.d.ts +8 -21
- package/dist/reporting/check-run-summary.d.ts +2 -2
- package/dist/reporting/execution-tracker.d.ts +1 -1
- package/dist/reporting/log-chunk-sink.d.ts +1 -1
- package/dist/reporting/run-summary-fields.d.ts +25 -0
- package/dist/reporting/s3-log-storage.d.ts +2 -2
- package/dist/routes/admin-access-log.d.ts +2 -9
- package/dist/routes/admin-backends.d.ts +2 -9
- package/dist/routes/admin-cluster-name.d.ts +2 -8
- package/dist/routes/admin-cluster-settings.d.ts +2 -8
- package/dist/routes/admin-config.d.ts +0 -3
- package/dist/routes/admin-contexts.d.ts +3 -11
- package/dist/routes/admin-db.d.ts +2 -9
- package/dist/routes/admin-env.d.ts +33 -0
- package/dist/routes/admin-event-dlq.d.ts +2 -9
- package/dist/routes/admin-event-inspect.d.ts +2 -9
- package/dist/routes/admin-event-log.d.ts +2 -9
- package/dist/routes/admin-events.d.ts +3 -13
- package/dist/routes/admin-held-runs.d.ts +2 -8
- package/dist/routes/admin-list-limit.d.ts +3 -0
- package/dist/routes/admin-maintenance.d.ts +2 -9
- package/dist/routes/admin-orchestrator-drain.d.ts +2 -9
- package/dist/routes/admin-org-settings.d.ts +2 -8
- package/dist/routes/admin-orgs.d.ts +27 -0
- package/dist/routes/admin-peer-forget.d.ts +2 -9
- package/dist/routes/admin-queue-execution.d.ts +3 -10
- package/dist/routes/admin-registrations.d.ts +3 -14
- package/dist/routes/admin-runs.d.ts +3 -14
- package/dist/routes/admin-scaler-orphans.d.ts +2 -9
- package/dist/routes/admin-scaler-reload.d.ts +41 -0
- package/dist/routes/admin-scheduled-jobs.d.ts +2 -9
- package/dist/routes/admin-signing-keys.d.ts +2 -9
- package/dist/routes/admin-sources.d.ts +28 -32
- package/dist/routes/admin-trust-policy.d.ts +2 -8
- package/dist/routes/admin.d.ts +29 -30
- package/dist/routes/cluster-join.d.ts +13 -0
- package/dist/routes/github-webhook.d.ts +13 -11
- package/dist/scaler/bare-metal-backend.d.ts +42 -1
- package/dist/scaler/config.d.ts +12 -84
- package/dist/scaler/file-reload.d.ts +48 -0
- package/dist/scaler/manager.d.ts +47 -33
- package/dist/scaler/scaler-state-store.d.ts +5 -0
- package/dist/scaler/types.d.ts +8 -9
- package/dist/secrets/job-secret-gate.d.ts +2 -5
- package/dist/secrets/secret-resolver.d.ts +2 -4
- package/dist/security/trust-policy-gate.d.ts +0 -4
- package/dist/server.d.ts +6 -17
- package/dist/server.js +10933 -10836
- package/dist/sources/source-manager.d.ts +0 -24
- package/dist/sources/webhook-url-resolvers.d.ts +60 -0
- package/dist/standalone.js +10457 -10060
- package/dist/worker/reroute-dispatch.d.ts +1 -0
- package/dist/worker/reroute-spawn-control.d.ts +1 -3
- package/dist/worker-core.d.ts +9 -2
- package/dist/ws/agent-handler.d.ts +3 -3
- package/dist/ws/dashboard-access.d.ts +57 -0
- package/dist/ws/dashboard-backends-handler.d.ts +1 -6
- package/dist/ws/dashboard-context-handler.d.ts +10 -15
- package/dist/ws/dashboard-fleet-handler.d.ts +4 -3
- package/dist/ws/dashboard-fleet-write-handler.d.ts +6 -5
- package/dist/ws/dashboard-global-workflows-handler.d.ts +1 -6
- package/dist/ws/dashboard-registrations-handler.d.ts +1 -7
- package/dist/ws/platform-client.d.ts +94 -327
- package/installer-image-digests.json +3 -3
- package/package.json +4 -4
- package/sbom.spdx.json +53 -53
- package/dist/cache/legacy-prefixes.d.ts +0 -19
- package/dist/cli/commands/agent-service/logs.d.ts +0 -14
- package/dist/cli/commands/agent-service/restart.d.ts +0 -12
- package/dist/cli/commands/agent-service/start.d.ts +0 -12
- package/dist/cli/commands/agent-service/stop.d.ts +0 -12
- package/dist/cli/commands/agent-service/uninstall.d.ts +0 -13
- package/dist/cli/commands/cache.d.ts +0 -51
- package/dist/cli/commands/orchestrator-service/logs.d.ts +0 -14
- package/dist/cli/commands/orchestrator-service/restart.d.ts +0 -12
- package/dist/cli/commands/orchestrator-service/start.d.ts +0 -12
- package/dist/cli/commands/orchestrator-service/stop.d.ts +0 -12
- package/dist/cli/commands/orchestrator-service/uninstall.d.ts +0 -13
- package/dist/git/inherited-secret.d.ts +0 -29
package/dist/index.js
CHANGED
|
@@ -6,12 +6,13 @@ import { createLogger, createS3Client, sha256, toErrorMessage } from "@kici-dev/
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import { mkdtempSync, promises, writeFileSync } from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
|
-
import { createHash, createHmac,
|
|
9
|
+
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
10
10
|
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
11
11
|
import { Kysely, PostgresDialect, sql } from "kysely";
|
|
12
12
|
import pg from "pg";
|
|
13
|
+
import { DEFAULT_APPROVAL_EXPIRY_SECONDS, ForkPolicy, HoldScope, HoldType, OrchRole, TriggerSource } from "@kici-dev/engine";
|
|
13
14
|
import { z } from "zod";
|
|
14
|
-
import {
|
|
15
|
+
import { ORCHESTRATOR_DEFAULT_PORT } from "@kici-dev/shared/env";
|
|
15
16
|
import { platform, tmpdir } from "node:os";
|
|
16
17
|
import { createInterface } from "node:readline";
|
|
17
18
|
import { Octokit } from "@octokit/rest";
|
|
@@ -702,7 +703,7 @@ function createCacheStorage(config) {
|
|
|
702
703
|
* defect this layout closes — so serving it would serve the bug. For a v6 lock
|
|
703
704
|
* the fallback is unreachable anyway, since the schema bump moves every hash.
|
|
704
705
|
* Old objects are orphaned. The cache TTL is enforced lazily on access, so
|
|
705
|
-
* nothing here removes them
|
|
706
|
+
* nothing here removes them.
|
|
706
707
|
*/
|
|
707
708
|
const logger$4 = createLogger({ prefix: "source-cache" });
|
|
708
709
|
/**
|
|
@@ -1105,8 +1106,7 @@ const DEFAULT_USER_CACHE_TTL_MS = 6048e5;
|
|
|
1105
1106
|
const TAR_SUFFIX = ".tar.gz";
|
|
1106
1107
|
/**
|
|
1107
1108
|
* Stem prefix of an in-flight upload, `.tmp-<uuid>.tar.gz`: the presigned PUT
|
|
1108
|
-
* target a save lands on before commit copies it to its final key.
|
|
1109
|
-
* the retired-layout classifier so both agree on what an upload looks like.
|
|
1109
|
+
* target a save lands on before commit copies it to its final key.
|
|
1110
1110
|
*/
|
|
1111
1111
|
const TEMP_UPLOAD_STEM = ".tmp-";
|
|
1112
1112
|
/** Whether a listed object is an in-flight upload rather than a cache entry. */
|
|
@@ -1122,7 +1122,7 @@ const KEY_DISCRIMINATOR_TAIL = new RegExp(`^(.*)-([0-9a-f]{32})$`);
|
|
|
1122
1122
|
* is what keeps two other kinds of object out of a restore: an in-flight
|
|
1123
1123
|
* `.tmp-<uuid>.tar.gz` upload, which is a partial tarball until commit copies
|
|
1124
1124
|
* it to its final key, and an object under the retired un-discriminated layout,
|
|
1125
|
-
* which nothing writes
|
|
1125
|
+
* which nothing writes.
|
|
1126
1126
|
*/
|
|
1127
1127
|
function isCommittedEntry(key) {
|
|
1128
1128
|
return key.endsWith(TAR_SUFFIX) && KEY_DISCRIMINATOR_TAIL.test(key.slice(0, -7));
|
|
@@ -1147,9 +1147,6 @@ function stripKeyDiscriminator(stem) {
|
|
|
1147
1147
|
* signature on a pre-signed PUT/GET. Repo identifiers like `.` (the internal
|
|
1148
1148
|
* provider's repo id) hit exactly this case, so the all-dots guard keeps the
|
|
1149
1149
|
* object key canonical and the namespace boundary intact.
|
|
1150
|
-
*
|
|
1151
|
-
* Exported so an operator tool that addresses an org's prefix from a raw org id
|
|
1152
|
-
* (`kici-admin cache purge-legacy --org`) lands on the segment the writer used.
|
|
1153
1150
|
*/
|
|
1154
1151
|
function sanitizeSegment(s) {
|
|
1155
1152
|
const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -1199,10 +1196,8 @@ function finalKey(prefix, key) {
|
|
|
1199
1196
|
}
|
|
1200
1197
|
/**
|
|
1201
1198
|
* The object key a committed save under `key` lands on for this ref. This is
|
|
1202
|
-
* the writer's own path
|
|
1203
|
-
* through it
|
|
1204
|
-
* legacy-layout classifier tests) takes it from here rather than restating
|
|
1205
|
-
* the format.
|
|
1199
|
+
* the writer's own path: `beginSave` and `commitSave` address the entry
|
|
1200
|
+
* through it.
|
|
1206
1201
|
*/
|
|
1207
1202
|
function userCacheEntryKey(ref, key) {
|
|
1208
1203
|
return finalKey(writePrefix(ref), key);
|
|
@@ -1451,7 +1446,7 @@ var PeerCredentialStore = class {
|
|
|
1451
1446
|
}
|
|
1452
1447
|
/** The most recently revoked credential row for `instanceId`, or null. */
|
|
1453
1448
|
async findLatestRevokedByInstanceId(instanceId) {
|
|
1454
|
-
const row = await this.db.selectFrom("peer_credentials").selectAll().where("instance_id", "=", instanceId).where("revoked_at", "is not", null).orderBy("revoked_at", "desc").executeTakeFirst();
|
|
1449
|
+
const row = await this.db.selectFrom("peer_credentials").selectAll().where("instance_id", "=", instanceId).where("revoked_at", "is not", null).orderBy("revoked_at", "desc").orderBy("created_at", "desc").executeTakeFirst();
|
|
1455
1450
|
return row ? mapRow(row) : null;
|
|
1456
1451
|
}
|
|
1457
1452
|
/**
|
|
@@ -1626,12 +1621,27 @@ createLogger({ prefix: "coordinator-credential" });
|
|
|
1626
1621
|
* A coordinator without a join token also wires a `selfIssue` callback. When a
|
|
1627
1622
|
* decision finds no credential and no token, the callback issues the
|
|
1628
1623
|
* coordinator its own credential, once per storm, under the same mutex.
|
|
1624
|
+
*
|
|
1625
|
+
* A coordinator also wires `corroborateRejection`. Any dialled endpoint can
|
|
1626
|
+
* send a rejection, so before it deletes the file the coordinator asks its own
|
|
1627
|
+
* cluster database: a credential the database holds as valid, or a database it
|
|
1628
|
+
* cannot read, keeps the file.
|
|
1629
1629
|
*/
|
|
1630
1630
|
const logger = createLogger({ prefix: "peer-auth-coordinator" });
|
|
1631
1631
|
/** How long a waiting peer-client awaits an in-flight sibling token-join. */
|
|
1632
1632
|
const DEFAULT_JOIN_WAIT_TIMEOUT_MS = 1e4;
|
|
1633
1633
|
/** Max read→await→re-read cycles before a waiter gives up and joins/aborts. */
|
|
1634
1634
|
const MAX_DECIDE_ITERATIONS = 3;
|
|
1635
|
+
/** What `reportRejection` did with the credential file. */
|
|
1636
|
+
let RejectionAction = /* @__PURE__ */ function(RejectionAction) {
|
|
1637
|
+
/** A sibling refreshed the file: retry with the fresh credential. */
|
|
1638
|
+
RejectionAction["RetryCredential"] = "retry-credential";
|
|
1639
|
+
/** The file was deleted: the next decision token-joins or self-issues. */
|
|
1640
|
+
RejectionAction["Rejoin"] = "rejoin";
|
|
1641
|
+
/** This cluster's database holds the credential as valid (or is unreadable): the file stays. */
|
|
1642
|
+
RejectionAction["KeepCredential"] = "keep-credential";
|
|
1643
|
+
return RejectionAction;
|
|
1644
|
+
}({});
|
|
1635
1645
|
function deferred() {
|
|
1636
1646
|
let resolve;
|
|
1637
1647
|
return {
|
|
@@ -1647,6 +1657,7 @@ var PeerAuthCoordinator = class {
|
|
|
1647
1657
|
joinToken;
|
|
1648
1658
|
joinWaitTimeoutMs;
|
|
1649
1659
|
selfIssue;
|
|
1660
|
+
corroborateRejection;
|
|
1650
1661
|
/** The issuance in progress; concurrent sibling clients share it. */
|
|
1651
1662
|
selfIssueInFlight = null;
|
|
1652
1663
|
/** An operator revoked this coordinator's credential: never issue again in this process. */
|
|
@@ -1661,6 +1672,7 @@ var PeerAuthCoordinator = class {
|
|
|
1661
1672
|
this.joinToken = opts.joinToken;
|
|
1662
1673
|
this.joinWaitTimeoutMs = opts.joinWaitTimeoutMs ?? DEFAULT_JOIN_WAIT_TIMEOUT_MS;
|
|
1663
1674
|
this.selfIssue = opts.selfIssue;
|
|
1675
|
+
this.corroborateRejection = opts.corroborateRejection;
|
|
1664
1676
|
}
|
|
1665
1677
|
/** Run `fn` exclusively against the credential file. */
|
|
1666
1678
|
async withLock(fn) {
|
|
@@ -1771,6 +1783,17 @@ var PeerAuthCoordinator = class {
|
|
|
1771
1783
|
});
|
|
1772
1784
|
return "retry-credential";
|
|
1773
1785
|
}
|
|
1786
|
+
if (cred && this.corroborateRejection) {
|
|
1787
|
+
const verdict = await this.corroborateRejection(cred.credential);
|
|
1788
|
+
if (verdict !== "not-held") {
|
|
1789
|
+
logger.warn("A peer rejected a credential this cluster holds as valid; keeping the credential file", {
|
|
1790
|
+
instanceId: this.instanceId,
|
|
1791
|
+
reason,
|
|
1792
|
+
verdict
|
|
1793
|
+
});
|
|
1794
|
+
return "keep-credential";
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1774
1797
|
try {
|
|
1775
1798
|
await unlink(this.credentialFile);
|
|
1776
1799
|
logger.warn("Deleted stale credential file after server rejection", {
|
|
@@ -1790,24 +1813,49 @@ var PeerAuthCoordinator = class {
|
|
|
1790
1813
|
//#endregion
|
|
1791
1814
|
//#region src/cluster/join-token.ts
|
|
1792
1815
|
/**
|
|
1793
|
-
* Join
|
|
1816
|
+
* Join tokens: creation, lookup and claim.
|
|
1794
1817
|
*
|
|
1795
1818
|
* Token format: kici_join_v1.<base64url(routing_json)>.<random_256bit_hex>
|
|
1796
|
-
* - Routing JSON: { orgId, routingKey, expiry }
|
|
1797
|
-
*
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
1800
|
-
* - encryption_key = HKDF-SHA256(secret, salt="kici-join-encrypt", info="v1", length=32)
|
|
1801
|
-
* - validation_hash = SHA-256(secret) (stored in DB for lookup)
|
|
1819
|
+
* - Routing JSON: { orgId, routingKey, expiry, role }. A lookup key only: the
|
|
1820
|
+
* row the token was stored as decides the role and routing a claim returns.
|
|
1821
|
+
* - Secret: 32 random bytes as hex. The database stores token_hash =
|
|
1822
|
+
* SHA-256(secret), never the secret.
|
|
1802
1823
|
*
|
|
1803
|
-
*
|
|
1804
|
-
*
|
|
1824
|
+
* A row is found by its hash (claimByHash, readByHash), or by its routing fields
|
|
1825
|
+
* plus a proof the caller checks against each candidate hash
|
|
1826
|
+
* (resolveLiveTokenByRouting).
|
|
1805
1827
|
*/
|
|
1806
1828
|
const joinTokenLogger = createLogger({ prefix: "join-token" });
|
|
1807
1829
|
const TOKEN_PREFIX = "kici_join_v1";
|
|
1808
1830
|
const DEFAULT_EXPIRY_MS = 36e5;
|
|
1809
1831
|
const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used. Create a new token with: kici-admin peer create-token";
|
|
1810
1832
|
const TOKEN_EXPIRED_MESSAGE = "Join token has expired. Create a new token with: kici-admin peer create-token";
|
|
1833
|
+
const INVALID_JOIN_TOKEN_MESSAGE = "Invalid join token";
|
|
1834
|
+
const ResolvedJoinTokenStatus = z.enum([
|
|
1835
|
+
"live",
|
|
1836
|
+
"expired",
|
|
1837
|
+
"unknown"
|
|
1838
|
+
]);
|
|
1839
|
+
const joinRoutingClaimSchema = z.object({
|
|
1840
|
+
orgId: z.string().min(1),
|
|
1841
|
+
routingKey: z.string().min(1),
|
|
1842
|
+
expiry: z.number().finite()
|
|
1843
|
+
}).passthrough();
|
|
1844
|
+
/** Read a `routing_info` value (object, or a JSON string) and the `role` column. */
|
|
1845
|
+
function routingFromRow(routingInfo, role) {
|
|
1846
|
+
const raw = typeof routingInfo === "string" ? JSON.parse(routingInfo) : routingInfo;
|
|
1847
|
+
const parsed = joinRoutingClaimSchema.parse(raw);
|
|
1848
|
+
const rowRole = OrchRole.parse(role);
|
|
1849
|
+
return {
|
|
1850
|
+
role: rowRole,
|
|
1851
|
+
routing: {
|
|
1852
|
+
orgId: parsed.orgId,
|
|
1853
|
+
routingKey: parsed.routingKey,
|
|
1854
|
+
expiry: parsed.expiry,
|
|
1855
|
+
role: rowRole
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1811
1859
|
var JoinTokenManager = class {
|
|
1812
1860
|
deps;
|
|
1813
1861
|
constructor(deps) {
|
|
@@ -1819,8 +1867,8 @@ var JoinTokenManager = class {
|
|
|
1819
1867
|
*/
|
|
1820
1868
|
async createToken(opts) {
|
|
1821
1869
|
const expiryMs = opts.expiryMs ?? DEFAULT_EXPIRY_MS;
|
|
1822
|
-
const expiry = Date.now() + expiryMs;
|
|
1823
|
-
const role = opts.role ??
|
|
1870
|
+
const expiry = Date.now() + Math.floor(expiryMs);
|
|
1871
|
+
const role = opts.role ?? OrchRole.enum.coordinator;
|
|
1824
1872
|
const routing = {
|
|
1825
1873
|
orgId: opts.orgId,
|
|
1826
1874
|
routingKey: opts.routingKey,
|
|
@@ -1831,10 +1879,9 @@ var JoinTokenManager = class {
|
|
|
1831
1879
|
const routingB64 = Buffer.from(JSON.stringify(routing)).toString("base64url");
|
|
1832
1880
|
const secretHex = secret.toString("hex");
|
|
1833
1881
|
const token = `${TOKEN_PREFIX}.${routingB64}.${secretHex}`;
|
|
1834
|
-
const { validationHash } = deriveKeys(secret);
|
|
1835
1882
|
await this.deps.db.insertInto("join_tokens").values({
|
|
1836
1883
|
id: randomUUID(),
|
|
1837
|
-
token_hash:
|
|
1884
|
+
token_hash: tokenHashOf(secretHex),
|
|
1838
1885
|
routing_info: JSON.stringify(routing),
|
|
1839
1886
|
role,
|
|
1840
1887
|
created_by: opts.createdBy,
|
|
@@ -1847,66 +1894,92 @@ var JoinTokenManager = class {
|
|
|
1847
1894
|
return token;
|
|
1848
1895
|
}
|
|
1849
1896
|
/**
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
* Single UPDATE with `WHERE token_hash = ? AND consumed_at IS NULL AND
|
|
1853
|
-
* expires_at > NOW()` — only one caller can win the claim across a
|
|
1854
|
-
* shared-DB multi-coordinator mesh. The winner gets `{ routing, keys }`;
|
|
1855
|
-
* every other concurrent caller gets `TOKEN_ALREADY_USED_MESSAGE` and is
|
|
1856
|
-
* expected to fall into the idempotent recovery branch in
|
|
1857
|
-
* `peer-handler.ts` (which serialises credential issuance via the
|
|
1858
|
-
* `peer_credentials_active_uniq` partial unique index).
|
|
1897
|
+
* Peer token mode: claim the row the presented token's secret hashes to. Role and
|
|
1898
|
+
* routing come from the row (`claimByHash`), never from the token's routing part.
|
|
1859
1899
|
*
|
|
1860
|
-
*
|
|
1861
|
-
*
|
|
1862
|
-
*
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
1866
|
-
* Self-healing reuse: a join token is re-consumable by the same joining
|
|
1867
|
-
* peer (`peerInstanceId`) until its `expires_at`. A peer that lost its
|
|
1868
|
-
* credential (transient outage / deleted credential file) re-presents the
|
|
1869
|
-
* still-valid join token already in its env; the coordinator accepts the
|
|
1870
|
-
* reuse and issues a fresh credential — no operator action, no cluster
|
|
1871
|
-
* redeploy. Reuse is bounded by BOTH `expires_at` AND the consuming
|
|
1872
|
-
* instanceId, so it never widens a leaked token's usefulness beyond the
|
|
1873
|
-
* instance that first consumed it.
|
|
1900
|
+
* Self-healing reuse: a join token is re-consumable by the same joining peer
|
|
1901
|
+
* (`peerInstanceId`) until its `expires_at`. A peer that lost its credential
|
|
1902
|
+
* re-presents the still-valid join token already in its env, and the coordinator
|
|
1903
|
+
* issues a fresh credential without an operator action. Reuse is bounded by both
|
|
1904
|
+
* `expires_at` and the consuming instanceId, so it never widens a leaked token's
|
|
1905
|
+
* usefulness beyond the instance that first consumed it.
|
|
1874
1906
|
*/
|
|
1875
1907
|
async validateAndConsumeToken(token, consumedBy, peerInstanceId) {
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1908
|
+
return this.claimByHash(tokenHashOf(parseToken(token).secretHex), consumedBy, peerInstanceId);
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Find the row whose proof the caller accepts, among rows whose routing matches
|
|
1912
|
+
* the claimed routing. Consumption state is not checked here; `claimByHash` owns it.
|
|
1913
|
+
* At most JOIN_TOKEN_ROUTING_CANDIDATE_CAP rows, newest first, are offered.
|
|
1914
|
+
*/
|
|
1915
|
+
async resolveLiveTokenByRouting(claimed, accepts) {
|
|
1916
|
+
const rows = await this.deps.db.selectFrom("join_tokens").select(["token_hash", "expires_at"]).where(sql`routing_info->>'orgId'`, "=", claimed.orgId).where(sql`routing_info->>'routingKey'`, "=", claimed.routingKey).where(sql`routing_info->>'expiry'`, "=", String(claimed.expiry)).orderBy("created_at", "desc").limit(16).execute();
|
|
1917
|
+
for (const row of rows) {
|
|
1918
|
+
if (!accepts(row.token_hash)) continue;
|
|
1919
|
+
return new Date(row.expires_at).getTime() > Date.now() ? {
|
|
1920
|
+
status: ResolvedJoinTokenStatus.enum.live,
|
|
1921
|
+
tokenHash: row.token_hash
|
|
1922
|
+
} : { status: ResolvedJoinTokenStatus.enum.expired };
|
|
1923
|
+
}
|
|
1924
|
+
return { status: ResolvedJoinTokenStatus.enum.unknown };
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Atomically consume the row with this hash, or re-validate it for the instance
|
|
1928
|
+
* that consumed it (self-healing reuse, bounded by expires_at). Routing and role
|
|
1929
|
+
* come from the row.
|
|
1930
|
+
*
|
|
1931
|
+
* One UPDATE ... WHERE consumed_at IS NULL wins the claim across a shared-DB
|
|
1932
|
+
* multi-coordinator mesh; every other concurrent caller gets
|
|
1933
|
+
* TOKEN_ALREADY_USED_MESSAGE, the message peer-handler's idempotent retry keys on.
|
|
1934
|
+
* On a 0-row claim, a follow-up SELECT tells not-found, expired, already-used and
|
|
1935
|
+
* reusable-by-the-same-instance apart.
|
|
1936
|
+
*/
|
|
1937
|
+
async claimByHash(tokenHash, consumedBy, consumerInstanceId) {
|
|
1938
|
+
const claimed = await this.deps.db.updateTable("join_tokens").set({
|
|
1879
1939
|
consumed_at: /* @__PURE__ */ new Date(),
|
|
1880
1940
|
consumed_by: consumedBy,
|
|
1881
|
-
consumed_by_instance:
|
|
1882
|
-
}).where("token_hash", "=",
|
|
1883
|
-
if (
|
|
1941
|
+
consumed_by_instance: consumerInstanceId
|
|
1942
|
+
}).where("token_hash", "=", tokenHash).where("consumed_at", "is", null).where("expires_at", ">", /* @__PURE__ */ new Date()).returning(["routing_info", "role"]).executeTakeFirst();
|
|
1943
|
+
if (claimed) {
|
|
1884
1944
|
joinTokenLogger.info("Consumed join token", {
|
|
1885
1945
|
consumedBy,
|
|
1886
|
-
|
|
1946
|
+
consumerInstanceId,
|
|
1947
|
+
tokenFingerprint: tokenFingerprint(tokenHash)
|
|
1887
1948
|
});
|
|
1888
1949
|
return {
|
|
1889
|
-
|
|
1890
|
-
|
|
1950
|
+
tokenHash,
|
|
1951
|
+
...routingFromRow(claimed.routing_info, claimed.role)
|
|
1891
1952
|
};
|
|
1892
1953
|
}
|
|
1893
|
-
const row = await this.deps.db.selectFrom("join_tokens").
|
|
1894
|
-
|
|
1954
|
+
const row = await this.deps.db.selectFrom("join_tokens").select([
|
|
1955
|
+
"routing_info",
|
|
1956
|
+
"role",
|
|
1957
|
+
"expires_at",
|
|
1958
|
+
"consumed_at",
|
|
1959
|
+
"consumed_by_instance"
|
|
1960
|
+
]).where("token_hash", "=", tokenHash).executeTakeFirst();
|
|
1961
|
+
if (!row) throw new Error(INVALID_JOIN_TOKEN_MESSAGE);
|
|
1895
1962
|
if (new Date(row.expires_at).getTime() <= Date.now()) throw new Error(TOKEN_EXPIRED_MESSAGE);
|
|
1896
1963
|
if (row.consumed_at) {
|
|
1897
|
-
if (row.consumed_by_instance ===
|
|
1964
|
+
if (row.consumed_by_instance === consumerInstanceId) {
|
|
1898
1965
|
joinTokenLogger.info("Re-validated join token for returning instance", {
|
|
1899
1966
|
consumedBy,
|
|
1900
|
-
|
|
1967
|
+
consumerInstanceId,
|
|
1968
|
+
tokenFingerprint: tokenFingerprint(tokenHash)
|
|
1901
1969
|
});
|
|
1902
1970
|
return {
|
|
1903
|
-
|
|
1904
|
-
|
|
1971
|
+
tokenHash,
|
|
1972
|
+
...routingFromRow(row.routing_info, row.role)
|
|
1905
1973
|
};
|
|
1906
1974
|
}
|
|
1907
1975
|
throw new Error(TOKEN_ALREADY_USED_MESSAGE);
|
|
1908
1976
|
}
|
|
1909
|
-
throw new Error(
|
|
1977
|
+
throw new Error(INVALID_JOIN_TOKEN_MESSAGE);
|
|
1978
|
+
}
|
|
1979
|
+
/** Role and routing of the row with this hash, or null. */
|
|
1980
|
+
async readByHash(tokenHash) {
|
|
1981
|
+
const row = await this.deps.db.selectFrom("join_tokens").select(["routing_info", "role"]).where("token_hash", "=", tokenHash).executeTakeFirst();
|
|
1982
|
+
return row ? routingFromRow(row.routing_info, row.role) : null;
|
|
1910
1983
|
}
|
|
1911
1984
|
};
|
|
1912
1985
|
/**
|
|
@@ -1939,21 +2012,17 @@ function parseToken(token) {
|
|
|
1939
2012
|
if (!routing.orgId || !routing.routingKey || !routing.expiry) throw new Error("Invalid join token routing data");
|
|
1940
2013
|
return {
|
|
1941
2014
|
routing,
|
|
2015
|
+
routingB64: parts[1],
|
|
1942
2016
|
secretHex: parts[2]
|
|
1943
2017
|
};
|
|
1944
2018
|
}
|
|
1945
|
-
/**
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
*/
|
|
1950
|
-
function
|
|
1951
|
-
|
|
1952
|
-
const derived = hkdfSync("sha256", secret, Buffer.from("kici-join-encrypt"), Buffer.from("v1"), 32);
|
|
1953
|
-
return {
|
|
1954
|
-
encryptionKey: Buffer.from(derived),
|
|
1955
|
-
validationHash
|
|
1956
|
-
};
|
|
2019
|
+
/** Hex SHA-256 of a token secret: the value `join_tokens.token_hash` stores. */
|
|
2020
|
+
function tokenHashOf(secretHex) {
|
|
2021
|
+
return sha256(Buffer.from(secretHex, "hex"));
|
|
2022
|
+
}
|
|
2023
|
+
/** A log-safe tag for a token hash. Never log the hash itself: it is the join key root. */
|
|
2024
|
+
function tokenFingerprint(tokenHash) {
|
|
2025
|
+
return sha256(`kici-join-log:${tokenHash}`).slice(0, 12);
|
|
1957
2026
|
}
|
|
1958
2027
|
/**
|
|
1959
2028
|
* Reason a run was held in the security queue. Persisted verbatim in
|
|
@@ -2250,18 +2319,15 @@ var HeldRunStore = class {
|
|
|
2250
2319
|
* incidental: `expireOverdue` is not scope-filtered, so a released-but-not-yet-
|
|
2251
2320
|
* resumed row would otherwise be expired out from under its resume.
|
|
2252
2321
|
*
|
|
2253
|
-
* The filter
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
* filter on `hold_scope`: a job-scoped timer hold used to be excluded here,
|
|
2257
|
-
* which left it with no release path at all — created, never released,
|
|
2258
|
-
* eventually expired, its job never dispatched.
|
|
2322
|
+
* The filter deliberately does NOT filter on `hold_scope`: a job-scoped timer
|
|
2323
|
+
* hold used to be excluded here, which left it with no release path at all —
|
|
2324
|
+
* created, never released, eventually expired, its job never dispatched.
|
|
2259
2325
|
*/
|
|
2260
2326
|
async releaseDueWaitHolds() {
|
|
2261
2327
|
return (await this.db.updateTable("held_runs").set({
|
|
2262
2328
|
status: "released",
|
|
2263
2329
|
resolved_at: sql`now()`
|
|
2264
|
-
}).where("status", "=", "pending").where("hold_type", "
|
|
2330
|
+
}).where("status", "=", "pending").where("hold_type", "=", HoldType.enum.timer).where("expires_at", "<", sql`now()`).returningAll().execute()).map((row) => ({
|
|
2265
2331
|
holdId: row.id,
|
|
2266
2332
|
runId: row.run_id,
|
|
2267
2333
|
jobId: row.job_id,
|
|
@@ -2279,7 +2345,7 @@ var HeldRunStore = class {
|
|
|
2279
2345
|
* `listQueuedHoldsForContext` fetches the rows it actually releases.
|
|
2280
2346
|
*/
|
|
2281
2347
|
async listAllQueuedHolds() {
|
|
2282
|
-
return await this.db.selectFrom("held_runs").innerJoin("execution_runs", "execution_runs.run_id", "held_runs.run_id").select(["held_runs.org_id as orgId", "execution_runs.context as concurrencyGroup"]).where("held_runs.status", "=", "pending").where("held_runs.hold_type", "
|
|
2348
|
+
return await this.db.selectFrom("held_runs").innerJoin("execution_runs", "execution_runs.run_id", "held_runs.run_id").select(["held_runs.org_id as orgId", "execution_runs.context as concurrencyGroup"]).where("held_runs.status", "=", "pending").where("held_runs.hold_type", "=", HoldType.enum.concurrency).execute();
|
|
2283
2349
|
}
|
|
2284
2350
|
/**
|
|
2285
2351
|
* List the pending queued (concurrency) holds for one context's concurrency
|
|
@@ -2292,7 +2358,7 @@ var HeldRunStore = class {
|
|
|
2292
2358
|
* starve whoever waited longest.
|
|
2293
2359
|
*/
|
|
2294
2360
|
async listQueuedHoldsForContext(orgId, concurrencyGroup) {
|
|
2295
|
-
return this.db.selectFrom("held_runs").innerJoin("execution_runs", "execution_runs.run_id", "held_runs.run_id").selectAll("held_runs").where("held_runs.org_id", "=", orgId).where("held_runs.status", "=", "pending").where("held_runs.hold_type", "
|
|
2361
|
+
return this.db.selectFrom("held_runs").innerJoin("execution_runs", "execution_runs.run_id", "held_runs.run_id").selectAll("held_runs").where("held_runs.org_id", "=", orgId).where("held_runs.status", "=", "pending").where("held_runs.hold_type", "=", HoldType.enum.concurrency).where("execution_runs.context", "=", concurrencyGroup).where("execution_runs.customer_id", "=", orgId).orderBy("held_runs.created_at", "asc").execute();
|
|
2296
2362
|
}
|
|
2297
2363
|
/**
|
|
2298
2364
|
* Expire overdue pending runs. Called by the stale detector.
|
|
@@ -2332,12 +2398,10 @@ function createHeldRunStoreFromUrl(databaseUrl, opts) {
|
|
|
2332
2398
|
const DEFAULT_FORK_POLICY = ForkPolicy.enum.ignore;
|
|
2333
2399
|
Object.freeze({
|
|
2334
2400
|
forkPolicy: DEFAULT_FORK_POLICY,
|
|
2335
|
-
approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
|
|
2336
2401
|
approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
|
|
2337
2402
|
});
|
|
2338
2403
|
Object.freeze({
|
|
2339
2404
|
forkPolicy: ForkPolicy.enum.hold,
|
|
2340
|
-
approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
|
|
2341
2405
|
approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
|
|
2342
2406
|
});
|
|
2343
2407
|
z.enum([
|
|
@@ -2367,34 +2431,9 @@ const TrustPolicySource = z.enum(["platform", "local"]);
|
|
|
2367
2431
|
*/
|
|
2368
2432
|
const DEFAULT_TRUST_POLICY = {
|
|
2369
2433
|
forkPolicy: DEFAULT_FORK_POLICY,
|
|
2370
|
-
approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
|
|
2371
2434
|
approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
|
|
2372
2435
|
};
|
|
2373
2436
|
/**
|
|
2374
|
-
* Resolve a local patch's approval-expiry window against what is already stored.
|
|
2375
|
-
*
|
|
2376
|
-
* One value is being set, spelled two ways, so the pair is merged as a unit and
|
|
2377
|
-
* always re-derived from the winning seconds value:
|
|
2378
|
-
*
|
|
2379
|
-
* - a patch naming `approvalExpirySeconds` sets the window, and the hours field
|
|
2380
|
-
* is recomputed from it — a stale hours value left behind would be the coarse
|
|
2381
|
-
* view of a window that is no longer stored;
|
|
2382
|
-
* - a patch naming only `approvalExpiryHours` sets the window too, and the
|
|
2383
|
-
* seconds field is recomputed from it — keeping the old seconds value would
|
|
2384
|
-
* let it out-rank the operator's explicit hours and silently discard the
|
|
2385
|
-
* write;
|
|
2386
|
-
* - a patch naming both takes the seconds value, because it is the more
|
|
2387
|
-
* specific of the two (`approvalExpirySecondsOf`);
|
|
2388
|
-
* - a patch naming neither leaves the stored window untouched.
|
|
2389
|
-
*/
|
|
2390
|
-
function mergedExpiry(patch, existing) {
|
|
2391
|
-
const seconds = (patch.approvalExpirySeconds != null || patch.approvalExpiryHours != null ? approvalExpirySecondsOf(patch) : null) ?? existing?.approvalExpirySeconds ?? DEFAULT_TRUST_POLICY.approvalExpirySeconds ?? DEFAULT_APPROVAL_EXPIRY_SECONDS;
|
|
2392
|
-
return {
|
|
2393
|
-
approvalExpiryHours: approvalExpiryHoursOf(seconds),
|
|
2394
|
-
approvalExpirySeconds: seconds
|
|
2395
|
-
};
|
|
2396
|
-
}
|
|
2397
|
-
/**
|
|
2398
2437
|
* Namespace for the per-org advisory lock taken by `upsertLocal`, so the key
|
|
2399
2438
|
* cannot collide with another feature's advisory lock in the same database.
|
|
2400
2439
|
*/
|
|
@@ -2410,11 +2449,7 @@ var TrustPolicyStore = class {
|
|
|
2410
2449
|
if (!row) return null;
|
|
2411
2450
|
return {
|
|
2412
2451
|
forkPolicy: row.fork_policy,
|
|
2413
|
-
|
|
2414
|
-
approvalExpirySeconds: approvalExpirySecondsOf({
|
|
2415
|
-
approvalExpiryHours: Number(row.approval_expiry_hours),
|
|
2416
|
-
approvalExpirySeconds: row.approval_expiry_seconds == null ? null : Number(row.approval_expiry_seconds)
|
|
2417
|
-
}),
|
|
2452
|
+
approvalExpirySeconds: Number(row.approval_expiry_seconds),
|
|
2418
2453
|
source: row.source,
|
|
2419
2454
|
updatedAt: row.updated_at
|
|
2420
2455
|
};
|
|
@@ -2454,7 +2489,7 @@ var TrustPolicyStore = class {
|
|
|
2454
2489
|
const existing = await this.get(orgId, trx);
|
|
2455
2490
|
const merged = {
|
|
2456
2491
|
forkPolicy: patch.forkPolicy ?? existing?.forkPolicy ?? DEFAULT_TRUST_POLICY.forkPolicy,
|
|
2457
|
-
|
|
2492
|
+
approvalExpirySeconds: patch.approvalExpirySeconds ?? existing?.approvalExpirySeconds ?? DEFAULT_TRUST_POLICY.approvalExpirySeconds
|
|
2458
2493
|
};
|
|
2459
2494
|
await this.write(orgId, merged, TrustPolicySource.enum.local, trx);
|
|
2460
2495
|
await onWrite?.(trx, merged);
|
|
@@ -2462,11 +2497,9 @@ var TrustPolicyStore = class {
|
|
|
2462
2497
|
});
|
|
2463
2498
|
}
|
|
2464
2499
|
async write(orgId, policy, source, executor = this.db) {
|
|
2465
|
-
const seconds = approvalExpirySecondsOf(policy);
|
|
2466
2500
|
const columns = {
|
|
2467
2501
|
fork_policy: policy.forkPolicy,
|
|
2468
|
-
|
|
2469
|
-
approval_expiry_seconds: seconds,
|
|
2502
|
+
approval_expiry_seconds: policy.approvalExpirySeconds,
|
|
2470
2503
|
source,
|
|
2471
2504
|
updated_at: sql`now()`
|
|
2472
2505
|
};
|
|
@@ -2555,6 +2588,8 @@ function firstCauseMessage(err) {
|
|
|
2555
2588
|
function isTransportFailure(err) {
|
|
2556
2589
|
return err instanceof TypeError && err.message === "fetch failed";
|
|
2557
2590
|
}
|
|
2591
|
+
/** The admin API address kici-admin dials when neither --url nor KICI_ADMIN_URL is set. */
|
|
2592
|
+
const DEFAULT_ADMIN_URL = `http://localhost:${ORCHESTRATOR_DEFAULT_PORT}`;
|
|
2558
2593
|
async function fetchAdminApi(url, init, baseUrl) {
|
|
2559
2594
|
try {
|
|
2560
2595
|
return await fetch(url, init);
|
|
@@ -2565,9 +2600,20 @@ async function fetchAdminApi(url, init, baseUrl) {
|
|
|
2565
2600
|
}
|
|
2566
2601
|
const detail = firstCauseMessage(err);
|
|
2567
2602
|
const cause = detail ? ` (${detail})` : "";
|
|
2568
|
-
|
|
2603
|
+
const defaultHint = baseUrl === DEFAULT_ADMIN_URL ? ` ${DEFAULT_ADMIN_URL} is kici-admin's built-in default, the orchestrator's default port on this host; an orchestrator on another host, or one whose KICI_PORT sets another port, needs KICI_ADMIN_URL or --url.` : "";
|
|
2604
|
+
throw new Error(`cannot reach the orchestrator admin API at ${baseUrl}${cause}. Set KICI_ADMIN_URL to the orchestrator's HTTP address, or pass --base-url where the subcommand accepts it. Note KICI_ORCHESTRATOR_URL is NOT read by this CLI; if other subcommands appear to work, they are using the --database-url / KICI_DATABASE_URL direct-DB path rather than HTTP.${defaultHint}`, { cause: err });
|
|
2569
2605
|
}
|
|
2570
2606
|
}
|
|
2607
|
+
/** Build a `?a=1&b=2` query string, skipping undefined, null and empty-string values. */
|
|
2608
|
+
function toQuery(params) {
|
|
2609
|
+
const usp = new URLSearchParams();
|
|
2610
|
+
for (const [k, v] of Object.entries(params)) {
|
|
2611
|
+
if (v === void 0 || v === null || v === "") continue;
|
|
2612
|
+
usp.set(k, String(v));
|
|
2613
|
+
}
|
|
2614
|
+
const qs = usp.toString();
|
|
2615
|
+
return qs ? `?${qs}` : "";
|
|
2616
|
+
}
|
|
2571
2617
|
var AdminApiClient = class {
|
|
2572
2618
|
baseUrl;
|
|
2573
2619
|
token;
|
|
@@ -2767,17 +2813,17 @@ var AdminApiClient = class {
|
|
|
2767
2813
|
return this.request("POST", `/api/v1/admin/generic-sources/${encodeURIComponent(id)}/disable`);
|
|
2768
2814
|
}
|
|
2769
2815
|
async queryAudit(opts) {
|
|
2770
|
-
const
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
return (await this.request("GET", `/api/v1/admin/audit${qs
|
|
2816
|
+
const qs = toQuery({
|
|
2817
|
+
contextName: opts?.contextName,
|
|
2818
|
+
routingKey: opts?.routingKey,
|
|
2819
|
+
action: opts?.action,
|
|
2820
|
+
from: opts?.from,
|
|
2821
|
+
to: opts?.to,
|
|
2822
|
+
limit: opts?.limit,
|
|
2823
|
+
offset: opts?.offset,
|
|
2824
|
+
includeArchived: opts?.includeArchived || void 0
|
|
2825
|
+
});
|
|
2826
|
+
return (await this.request("GET", `/api/v1/admin/audit${qs}`)).entries;
|
|
2781
2827
|
}
|
|
2782
2828
|
async createToken(data) {
|
|
2783
2829
|
return this.request("POST", "/api/v1/admin/tokens", data);
|
|
@@ -2868,7 +2914,23 @@ var AdminApiClient = class {
|
|
|
2868
2914
|
* other failure throws.
|
|
2869
2915
|
*/
|
|
2870
2916
|
async forgetPeer(body) {
|
|
2871
|
-
const res = await
|
|
2917
|
+
const { res, json, text } = await this.postForBody("/api/v1/admin/peers/forget", body);
|
|
2918
|
+
if (res.ok) return json;
|
|
2919
|
+
if (res.status === 409 && json.acknowledgementRequired === true) return {
|
|
2920
|
+
instanceId: body.instanceId,
|
|
2921
|
+
results: json.results ?? [],
|
|
2922
|
+
acknowledgementRequired: true,
|
|
2923
|
+
...json.error ? { error: json.error } : {}
|
|
2924
|
+
};
|
|
2925
|
+
throw new Error(`HTTP ${res.status}: ${json.error ?? text}`);
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* POST `body` and parse the answer whatever its status, for a route that
|
|
2929
|
+
* reports per-instance results on a non-2xx status too. An unparseable body
|
|
2930
|
+
* reads as `{}`.
|
|
2931
|
+
*/
|
|
2932
|
+
async postForBody(path, body) {
|
|
2933
|
+
const res = await fetchAdminApi(`${this.baseUrl}${path}`, {
|
|
2872
2934
|
method: "POST",
|
|
2873
2935
|
headers: {
|
|
2874
2936
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -2883,14 +2945,25 @@ var AdminApiClient = class {
|
|
|
2883
2945
|
} catch {
|
|
2884
2946
|
parsed = void 0;
|
|
2885
2947
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2948
|
+
return {
|
|
2949
|
+
res,
|
|
2950
|
+
json: parsed ?? {},
|
|
2951
|
+
text
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Reload the scaler config on this orchestrator and, unless `single`, on every
|
|
2956
|
+
* peer it is connected to. A response where an instance refused its file or
|
|
2957
|
+
* was not reached (422) is returned like a success, so the caller can show
|
|
2958
|
+
* every instance; a 404 means the orchestrator predates the route.
|
|
2959
|
+
*/
|
|
2960
|
+
async scalerReload(body) {
|
|
2961
|
+
const { res, json, text } = await this.postForBody("/api/v1/admin/scaler/reload", body);
|
|
2962
|
+
if ((res.ok || res.status === 422) && Array.isArray(json.results)) return {
|
|
2963
|
+
scope: json.scope ?? (body.single ? "single" : "cluster"),
|
|
2964
|
+
results: json.results
|
|
2893
2965
|
};
|
|
2966
|
+
if (res.status === 404) throw new Error("this orchestrator predates kici-admin scaler reload; send SIGHUP to it or upgrade it");
|
|
2894
2967
|
throw new Error(`HTTP ${res.status}: ${json.error ?? text}`);
|
|
2895
2968
|
}
|
|
2896
2969
|
/** List the live Firecracker VMs on this coordinator, or on the `target` peer. */
|
|
@@ -2942,15 +3015,15 @@ var AdminApiClient = class {
|
|
|
2942
3015
|
* are returned.
|
|
2943
3016
|
*/
|
|
2944
3017
|
async listRuns(opts) {
|
|
2945
|
-
const
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
return this.request("GET", `/api/v1/admin/runs${qs
|
|
3018
|
+
const qs = toQuery({
|
|
3019
|
+
status: opts?.status,
|
|
3020
|
+
workflowName: opts?.workflowName,
|
|
3021
|
+
repo: opts?.repo,
|
|
3022
|
+
since: opts?.since,
|
|
3023
|
+
limit: opts?.limit || void 0,
|
|
3024
|
+
offset: opts?.offset || void 0
|
|
3025
|
+
});
|
|
3026
|
+
return this.request("GET", `/api/v1/admin/runs${qs}`);
|
|
2954
3027
|
}
|
|
2955
3028
|
/**
|
|
2956
3029
|
* Count matching runs without returning the row list. Wraps the list
|
|
@@ -3003,27 +3076,27 @@ var AdminApiClient = class {
|
|
|
3003
3076
|
* secret_audit_log table.
|
|
3004
3077
|
*/
|
|
3005
3078
|
async getRunSecretOutputs(runId, opts) {
|
|
3006
|
-
const
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
return this.request("GET", `/api/v1/admin/runs/${encodeURIComponent(runId)}/secret-outputs${qs
|
|
3079
|
+
const qs = toQuery({
|
|
3080
|
+
outputKey: opts?.outputKey,
|
|
3081
|
+
reveal: opts?.reveal || void 0
|
|
3082
|
+
});
|
|
3083
|
+
return this.request("GET", `/api/v1/admin/runs/${encodeURIComponent(runId)}/secret-outputs${qs}`);
|
|
3011
3084
|
}
|
|
3012
3085
|
async listEventLog(opts) {
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
return this.request("GET", `/api/v1/admin/event-log${qs
|
|
3086
|
+
const qs = toQuery({
|
|
3087
|
+
orgId: opts?.orgId,
|
|
3088
|
+
routingKey: opts?.routingKey,
|
|
3089
|
+
event: opts?.event,
|
|
3090
|
+
action: opts?.action,
|
|
3091
|
+
status: opts?.status,
|
|
3092
|
+
from: opts?.from,
|
|
3093
|
+
to: opts?.to,
|
|
3094
|
+
deliveryId: opts?.deliveryId,
|
|
3095
|
+
limit: opts?.limit,
|
|
3096
|
+
offset: opts?.offset,
|
|
3097
|
+
includeArchived: opts?.includeArchived || void 0
|
|
3098
|
+
});
|
|
3099
|
+
return this.request("GET", `/api/v1/admin/event-log${qs}`);
|
|
3027
3100
|
}
|
|
3028
3101
|
async getEventLog(deliveryId, opts) {
|
|
3029
3102
|
const params = new URLSearchParams({ orgId: opts.orgId });
|
|
@@ -3033,44 +3106,42 @@ var AdminApiClient = class {
|
|
|
3033
3106
|
}
|
|
3034
3107
|
/** List internal events, newest first (`GET /api/v1/admin/events`). */
|
|
3035
3108
|
async listEvents(opts) {
|
|
3036
|
-
const
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
return this.request("GET", `/api/v1/admin/events${qs
|
|
3109
|
+
const qs = toQuery({
|
|
3110
|
+
name: opts?.name,
|
|
3111
|
+
outcome: opts?.outcome,
|
|
3112
|
+
since: opts?.since,
|
|
3113
|
+
before: opts?.before,
|
|
3114
|
+
limit: opts?.limit
|
|
3115
|
+
});
|
|
3116
|
+
return this.request("GET", `/api/v1/admin/events${qs}`);
|
|
3044
3117
|
}
|
|
3045
3118
|
/** One internal event with its redacted payload and dispatched runs. */
|
|
3046
3119
|
async getEvent(id) {
|
|
3047
3120
|
return this.request("GET", `/api/v1/admin/events/${encodeURIComponent(id)}`);
|
|
3048
3121
|
}
|
|
3049
3122
|
async listAccessLog(opts) {
|
|
3050
|
-
const
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
return this.request("GET", `/api/v1/admin/access-log${qs
|
|
3123
|
+
const qs = toQuery({
|
|
3124
|
+
orgId: opts?.orgId,
|
|
3125
|
+
actorType: opts?.actorType,
|
|
3126
|
+
actorId: opts?.actorId,
|
|
3127
|
+
action: opts?.action,
|
|
3128
|
+
source: opts?.source,
|
|
3129
|
+
outcome: opts?.outcome,
|
|
3130
|
+
targetType: opts?.targetType,
|
|
3131
|
+
targetId: opts?.targetId,
|
|
3132
|
+
from: opts?.from,
|
|
3133
|
+
to: opts?.to,
|
|
3134
|
+
q: opts?.q,
|
|
3135
|
+
agentLabel: opts?.agentLabel,
|
|
3136
|
+
agentOnly: opts?.agentOnly || void 0,
|
|
3137
|
+
limit: opts?.limit,
|
|
3138
|
+
cursor: opts?.cursor
|
|
3139
|
+
});
|
|
3140
|
+
return this.request("GET", `/api/v1/admin/access-log${qs}`);
|
|
3068
3141
|
}
|
|
3069
3142
|
async getAccessLogEntry(id, opts) {
|
|
3070
|
-
const
|
|
3071
|
-
|
|
3072
|
-
const qs = params.toString();
|
|
3073
|
-
return this.request("GET", `/api/v1/admin/access-log/${encodeURIComponent(id)}${qs ? `?${qs}` : ""}`);
|
|
3143
|
+
const qs = toQuery({ orgId: opts?.orgId });
|
|
3144
|
+
return this.request("GET", `/api/v1/admin/access-log/${encodeURIComponent(id)}${qs}`);
|
|
3074
3145
|
}
|
|
3075
3146
|
async diagnose() {
|
|
3076
3147
|
return this.request("GET", "/admin/diagnose");
|
|
@@ -3154,6 +3225,28 @@ function validateWebhookUrl(value) {
|
|
|
3154
3225
|
if (url.protocol !== "https:") throw new Error(`--webhook-url must be an https:// URL (got ${url.protocol}//…)`);
|
|
3155
3226
|
return value;
|
|
3156
3227
|
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Every permission and event a KiCI GitHub App needs. The manifest requests
|
|
3230
|
+
* exactly these, and `kici-admin source refresh` reports an existing App that
|
|
3231
|
+
* lacks any of them.
|
|
3232
|
+
*/
|
|
3233
|
+
const GITHUB_APP_REQUIREMENTS = {
|
|
3234
|
+
permissions: {
|
|
3235
|
+
contents: "read",
|
|
3236
|
+
metadata: "read",
|
|
3237
|
+
pull_requests: "read",
|
|
3238
|
+
checks: "write",
|
|
3239
|
+
members: "read",
|
|
3240
|
+
issues: "read"
|
|
3241
|
+
},
|
|
3242
|
+
events: [
|
|
3243
|
+
"push",
|
|
3244
|
+
"pull_request",
|
|
3245
|
+
"check_run",
|
|
3246
|
+
"check_suite",
|
|
3247
|
+
"issue_comment"
|
|
3248
|
+
]
|
|
3249
|
+
};
|
|
3157
3250
|
function buildGithubAppManifest(input) {
|
|
3158
3251
|
return {
|
|
3159
3252
|
name: input.name,
|
|
@@ -3165,19 +3258,8 @@ function buildGithubAppManifest(input) {
|
|
|
3165
3258
|
redirect_url: input.redirectUrl,
|
|
3166
3259
|
...input.setupUrl ? { setup_url: input.setupUrl } : {},
|
|
3167
3260
|
public: false,
|
|
3168
|
-
default_permissions: {
|
|
3169
|
-
|
|
3170
|
-
metadata: "read",
|
|
3171
|
-
pull_requests: "read",
|
|
3172
|
-
checks: "write",
|
|
3173
|
-
members: "read"
|
|
3174
|
-
},
|
|
3175
|
-
default_events: [
|
|
3176
|
-
"push",
|
|
3177
|
-
"pull_request",
|
|
3178
|
-
"check_run",
|
|
3179
|
-
"check_suite"
|
|
3180
|
-
]
|
|
3261
|
+
default_permissions: { ...GITHUB_APP_REQUIREMENTS.permissions },
|
|
3262
|
+
default_events: [...GITHUB_APP_REQUIREMENTS.events]
|
|
3181
3263
|
};
|
|
3182
3264
|
}
|
|
3183
3265
|
/**
|
|
@@ -3356,6 +3438,65 @@ function openBrowserBestEffort(url) {
|
|
|
3356
3438
|
exec(cmd, () => {});
|
|
3357
3439
|
}
|
|
3358
3440
|
//#endregion
|
|
3441
|
+
//#region src/sources/webhook-url-resolvers.ts
|
|
3442
|
+
/**
|
|
3443
|
+
* Every webhook-URL decision the orchestrator makes for a GitHub-App source,
|
|
3444
|
+
* by operating mode. Pure functions: the entry points (`server.ts`,
|
|
3445
|
+
* `standalone.ts`) pass in the mode, the public base, the org and the Platform
|
|
3446
|
+
* values, so each rule is unit-tested here once.
|
|
3447
|
+
*/
|
|
3448
|
+
/** Why a webhook URL could not be resolved. The CLI maps each to a reason and a hint. */
|
|
3449
|
+
const WebhookUrlNote = z.enum([
|
|
3450
|
+
"unsupported-provider",
|
|
3451
|
+
"no-public-url",
|
|
3452
|
+
"platform-no-public-url",
|
|
3453
|
+
"platform-unavailable",
|
|
3454
|
+
"platform-url-unknown",
|
|
3455
|
+
"org-not-identified",
|
|
3456
|
+
"resolver-unavailable",
|
|
3457
|
+
"resolve-failed"
|
|
3458
|
+
]);
|
|
3459
|
+
//#endregion
|
|
3460
|
+
//#region src/cli/webhook-url-notes.ts
|
|
3461
|
+
/**
|
|
3462
|
+
* The text the CLI prints when the orchestrator returns no webhook URL. Shared
|
|
3463
|
+
* by `kici-admin source add` and the manifest setup flow, so each
|
|
3464
|
+
* `WebhookUrlNote` reads the same in both.
|
|
3465
|
+
*/
|
|
3466
|
+
const DASHBOARD_HINT = "Find the webhook URL in the dashboard under Sources.";
|
|
3467
|
+
/**
|
|
3468
|
+
* Short reason shown in `(unavailable — <reason>)` when no webhook URL could be
|
|
3469
|
+
* resolved. Takes a plain string too, because an orchestrator of another
|
|
3470
|
+
* version can send a note this CLI does not know.
|
|
3471
|
+
*/
|
|
3472
|
+
function webhookNoteReason(note) {
|
|
3473
|
+
switch (note) {
|
|
3474
|
+
case WebhookUrlNote.enum["no-public-url"]: return "KICI_WEBHOOK_PUBLIC_URL is not set on the orchestrator";
|
|
3475
|
+
case WebhookUrlNote.enum["platform-no-public-url"]: return "the Platform has no public webhook URL configured";
|
|
3476
|
+
case WebhookUrlNote.enum["platform-unavailable"]: return "could not reach the Platform";
|
|
3477
|
+
case WebhookUrlNote.enum["platform-url-unknown"]: return "the Platform did not send its GitHub webhook URL";
|
|
3478
|
+
case WebhookUrlNote.enum["org-not-identified"]: return "the orchestrator does not know its Platform org yet";
|
|
3479
|
+
case WebhookUrlNote.enum["unsupported-provider"]: return "this provider has no webhook URL to print";
|
|
3480
|
+
case WebhookUrlNote.enum["resolver-unavailable"]: return "this orchestrator cannot resolve webhook URLs";
|
|
3481
|
+
case WebhookUrlNote.enum["resolve-failed"]: return "resolving the webhook URL failed";
|
|
3482
|
+
default: return "webhook URL could not be determined";
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
/** Actionable next-step hint paired with {@link webhookNoteReason}. */
|
|
3486
|
+
function webhookNoteHint(note) {
|
|
3487
|
+
switch (note) {
|
|
3488
|
+
case WebhookUrlNote.enum["no-public-url"]: return "Set KICI_WEBHOOK_PUBLIC_URL to the orchestrator's public base, then re-run; paste the printed URL into your GitHub App or repo webhook.";
|
|
3489
|
+
case WebhookUrlNote.enum["platform-unavailable"]: return "Retry once the orchestrator reconnects, or find the URL in the dashboard under Sources.";
|
|
3490
|
+
case WebhookUrlNote.enum["platform-url-unknown"]: return "Retry when the orchestrator is connected to the Platform, or pass --webhook-url with the URL the App must deliver to.";
|
|
3491
|
+
case WebhookUrlNote.enum["org-not-identified"]: return "Retry when the orchestrator is connected to the Platform, or pass --webhook-url.";
|
|
3492
|
+
case WebhookUrlNote.enum["unsupported-provider"]: return "Configure the webhook from the provider's own settings.";
|
|
3493
|
+
case WebhookUrlNote.enum["resolve-failed"]: return "Retry; if it fails again, find the webhook URL in the dashboard under Sources.";
|
|
3494
|
+
case WebhookUrlNote.enum["platform-no-public-url"]:
|
|
3495
|
+
case WebhookUrlNote.enum["resolver-unavailable"]:
|
|
3496
|
+
default: return DASHBOARD_HINT;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
//#endregion
|
|
3359
3500
|
//#region src/cli/commands/source-manifest.ts
|
|
3360
3501
|
/**
|
|
3361
3502
|
* `kici-admin source add github --manifest` — the one-click GitHub App setup
|
|
@@ -3406,7 +3547,7 @@ const realManifestSetupDeps = {
|
|
|
3406
3547
|
/** Resolve the org-scoped webhook URL via the pre-flight route; abort if null. */
|
|
3407
3548
|
async function resolveWebhookUrl(client) {
|
|
3408
3549
|
const pre = await client.get("/api/v1/admin/sources/github-webhook-url");
|
|
3409
|
-
if (!pre.webhookUrl) throw new Error(`Cannot resolve the
|
|
3550
|
+
if (!pre.webhookUrl) throw new Error(`Cannot resolve the webhook URL for the new GitHub App: ${webhookNoteReason(pre.webhookNote)}. ` + webhookNoteHint(pre.webhookNote));
|
|
3410
3551
|
return pre.webhookUrl;
|
|
3411
3552
|
}
|
|
3412
3553
|
/**
|
|
@@ -3501,6 +3642,6 @@ async function runGithubManifestSetup(opts, client, deps = realManifestSetupDeps
|
|
|
3501
3642
|
}
|
|
3502
3643
|
}
|
|
3503
3644
|
//#endregion
|
|
3504
|
-
export { AdminApiClient, DEFAULT_TRUST_POLICY, DEFAULT_USER_CACHE_QUOTA_BYTES, DEFAULT_USER_CACHE_TTL_MS, DepCache, HeldRunStore, JoinTokenManager, PeerAuthCoordinator, PeerCredentialStore, S3CacheStorage, SecurityHoldReason, SourceCache, TrustPolicySource, TrustPolicyStore, UserCache, createCacheStorage, createHeldRunStoreFromUrl, createJoinTokenManagerFromUrl, createPeerCredentialStoreFromUrl, createTrustPolicyStoreFromUrl, runGithubManifestSetup };
|
|
3645
|
+
export { AdminApiClient, DEFAULT_TRUST_POLICY, DEFAULT_USER_CACHE_QUOTA_BYTES, DEFAULT_USER_CACHE_TTL_MS, DepCache, HeldRunStore, JoinTokenManager, PeerAuthCoordinator, PeerCredentialStore, RejectionAction, S3CacheStorage, SecurityHoldReason, SourceCache, TrustPolicySource, TrustPolicyStore, UserCache, createCacheStorage, createHeldRunStoreFromUrl, createJoinTokenManagerFromUrl, createPeerCredentialStoreFromUrl, createTrustPolicyStoreFromUrl, runGithubManifestSetup };
|
|
3505
3646
|
|
|
3506
3647
|
//# sourceMappingURL=index.js.map
|