@kici-dev/orchestrator 0.11.0 → 0.13.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__/mock-db-query.d.ts +4 -5
- package/dist/agent/agent-version.d.ts +5 -5
- package/dist/agent/dispatcher.d.ts +48 -0
- package/dist/agent/registry.d.ts +38 -9
- package/dist/agent-packaging/node-runtime-members.d.ts +14 -0
- package/dist/app.d.ts +9 -3
- package/dist/approvals/apply-decision.d.ts +1 -1
- package/dist/audit/access-log.d.ts +2 -3
- package/dist/cache/cache-upload-settle.d.ts +75 -0
- package/dist/cache/pending-inits.d.ts +9 -0
- package/dist/cancel/cancel-run.d.ts +22 -4
- package/dist/cancel/peer-job-cancel.d.ts +50 -0
- package/dist/cancel/sweep-stuck-cancelling.d.ts +3 -1
- package/dist/cli/commands/access-log.d.ts +3 -3
- package/dist/cli/commands/agent-service/agent-env.d.ts +20 -0
- package/dist/cli/commands/agent-service/status.d.ts +3 -3
- package/dist/cli/commands/attestations.d.ts +1 -1
- package/dist/cli/commands/context.d.ts +2 -2
- package/dist/cli/commands/event-dlq.d.ts +5 -6
- package/dist/cli/commands/event-log.d.ts +2 -3
- package/dist/cli/commands/execution.d.ts +1 -3
- package/dist/cli/commands/local-hook.d.ts +3 -2
- package/dist/cli/commands/local-trigger.d.ts +7 -0
- package/dist/cli/commands/orchestrator-service/status.d.ts +10 -3
- package/dist/cli/commands/queue.d.ts +1 -2
- package/dist/cli/commands/runs.d.ts +2 -3
- package/dist/cli/commands/service-health.d.ts +68 -0
- package/dist/cli/commands/shared/env-file-mode.d.ts +3 -2
- package/dist/cli/commands/shared/upgrade-safety.d.ts +11 -5
- package/dist/cli/commands/shared/versioned-upgrade.d.ts +119 -7
- package/dist/cli/commands/workflow.d.ts +3 -4
- package/dist/cli/open-browser.d.ts +1 -1
- package/dist/cli/service/entrypoint.d.ts +18 -0
- package/dist/cli/service/image-digests.d.ts +10 -0
- package/dist/cli/service/index.d.ts +4 -2
- package/dist/cli/service/instance/manifest.d.ts +39 -0
- package/dist/cli/service/launchd.d.ts +45 -10
- package/dist/cli/service/shutdown-grace.d.ts +35 -0
- package/dist/cli/service/types.d.ts +9 -0
- package/dist/cli/service/windows-acl.d.ts +40 -0
- package/dist/cli/service/windows-env-file.d.ts +45 -0
- package/dist/cli/service/windows.d.ts +13 -0
- package/dist/cli.js +2601 -1232
- package/dist/cluster/cluster-identity.d.ts +4 -4
- package/dist/cluster/coordinator-credential.d.ts +45 -0
- package/dist/cluster/coordinator.d.ts +18 -2
- package/dist/cluster/index.d.ts +1 -0
- package/dist/cluster/instance-heartbeat.d.ts +6 -1
- package/dist/cluster/join-token.d.ts +4 -2
- package/dist/cluster/leader-gated-scheduler.d.ts +7 -1
- package/dist/cluster/peer-auth-coordinator.d.ts +13 -0
- package/dist/cluster/peer-client.d.ts +4 -0
- package/dist/cluster/peer-credentials.d.ts +32 -1
- package/dist/cluster/raft-state.d.ts +6 -0
- package/dist/cluster/raft.d.ts +16 -2
- package/dist/cold-store/load-access-log-range.d.ts +3 -3
- package/dist/cold-store/load-event-log-range.d.ts +1 -1
- package/dist/cold-store/load-secret-audit-log-range.d.ts +2 -2
- package/dist/cold-store/orchestrator-cold-store.d.ts +9 -11
- package/dist/cold-store/tables/access-log.d.ts +2 -2
- package/dist/cold-store/tables/event-log.d.ts +4 -10
- package/dist/cold-store/tables/execution-jobs.d.ts +1 -1
- package/dist/cold-store/tables/execution-runs.d.ts +4 -4
- package/dist/cold-store/tables/execution-steps.d.ts +1 -1
- package/dist/cold-store/tables/secret-audit-log.d.ts +3 -3
- package/dist/config.d.ts +6 -1
- package/dist/contexts/held-runs.d.ts +2 -2
- package/dist/dashboard/attestation-filters.d.ts +1 -1
- package/dist/dashboard/handler.d.ts +8 -8
- package/dist/db/deferred-indexes.d.ts +2 -2
- package/dist/db/migrations/005_cold_store_chunk_counter.d.ts +2 -4
- package/dist/db/migrations/006_runs_jobs_steps_archived_at.d.ts +4 -4
- package/dist/db/migrations/007_audit_logs_archived_at.d.ts +6 -9
- package/dist/db/migrations/008_event_log_archived_at.d.ts +4 -6
- package/dist/db/migrations/010_cold_store_chunks.d.ts +2 -2
- package/dist/db/migrations/012_peer_credentials_active_uniq.d.ts +2 -4
- package/dist/db/migrations/016_org_settings_allow_http_npm.d.ts +2 -2
- package/dist/db/migrations/045_host_properties.d.ts +1 -1
- package/dist/db/migrations/046_join_token_consumed_by_instance.d.ts +1 -1
- package/dist/db/migrations/048_host_reboot_pending.d.ts +1 -1
- package/dist/db/migrations/049_held_runs_payload.d.ts +1 -1
- package/dist/db/migrations/050_sources_slug.d.ts +1 -1
- package/dist/db/migrations/051_binding_host_pattern.d.ts +1 -1
- package/dist/db/migrations/052_host_reach_metadata.d.ts +1 -1
- package/dist/db/migrations/053_agent_token_single_use.d.ts +1 -1
- package/dist/db/migrations/054_local_working_tree.d.ts +1 -1
- package/dist/db/migrations/055_agent_token_mandatory_labels.d.ts +1 -1
- package/dist/db/migrations/056_execution_jobs_environments.d.ts +1 -1
- package/dist/db/migrations/064_execution_jobs_skipped_environments.d.ts +1 -1
- package/dist/db/migrations/152_org_settings_cache_upload_settle_timeout.d.ts +15 -0
- package/dist/db/migrations/153_execution_runs_cancel_force.d.ts +16 -0
- package/dist/db/types.d.ts +27 -13
- package/dist/events/types.d.ts +1 -1
- package/dist/fault-injection-types.d.ts +2 -2
- package/dist/firecracker/host-network.d.ts +21 -3
- package/dist/firecracker/net-interfaces.d.ts +1 -1
- package/dist/helpers/run-detached.d.ts +12 -0
- package/dist/index.js +242 -67
- package/dist/oidc/db-signer.d.ts +1 -1
- package/dist/pipeline/changed-files.d.ts +51 -0
- package/dist/pipeline/cross-source-tally.d.ts +21 -0
- package/dist/pipeline/degraded-reason.d.ts +8 -0
- package/dist/pipeline/dispatch-matched-workflow.d.ts +10 -0
- package/dist/pipeline/global-eval-round.d.ts +5 -4
- package/dist/pipeline/processor.d.ts +11 -3
- package/dist/pipeline/rerun-inputs.d.ts +16 -7
- package/dist/provenance/retrier-mint.d.ts +1 -1
- package/dist/provenance/trust-root.d.ts +3 -3
- package/dist/providers/github/changed-files.d.ts +14 -4
- package/dist/providers/universal-git/changed-files.d.ts +6 -0
- package/dist/providers/universal-git/clone-token.d.ts +9 -13
- package/dist/providers/universal-git/config.d.ts +4 -0
- package/dist/providers/universal-git/index.d.ts +1 -2
- package/dist/providers/universal-git/normalizer.d.ts +2 -3
- package/dist/providers/universal-git/ssh-auth.d.ts +4 -4
- package/dist/queue/cleanup.d.ts +2 -2
- package/dist/queue/job-queue.d.ts +6 -6
- package/dist/queue/scheduled-job.d.ts +5 -7
- package/dist/registration/extractor.d.ts +1 -1
- package/dist/registration/registration-index.d.ts +1 -1
- package/dist/reporting/execution-tracker.d.ts +3 -3
- package/dist/routes/admin-access-log.d.ts +1 -1
- package/dist/routes/admin-auth.d.ts +2 -2
- package/dist/routes/admin-event-log.d.ts +2 -2
- package/dist/routes/admin-runs.d.ts +3 -3
- package/dist/routes/admin-scheduled-jobs.d.ts +2 -3
- package/dist/routes/health.d.ts +9 -0
- package/dist/scaler/container-backend.d.ts +3 -1
- package/dist/scaler/container-runtime.d.ts +9 -0
- package/dist/scaler/firecracker-backend.d.ts +15 -10
- package/dist/scaler/firecracker-extra-hosts.d.ts +20 -0
- package/dist/scaler/machine-ledger.d.ts +3 -4
- package/dist/scaler/manager.d.ts +26 -2
- package/dist/scaler/scaler-state-store.d.ts +1 -1
- package/dist/scaler/types.d.ts +4 -1
- package/dist/secrets/audit-logger.d.ts +2 -2
- package/dist/security/reduced-privilege-note.d.ts +2 -3
- package/dist/security/trust-policy-store.d.ts +3 -3
- package/dist/server.d.ts +2 -1
- package/dist/server.js +4955 -3206
- package/dist/stale-detector/stale-run-detector.d.ts +1 -1
- package/dist/stale-detector/workflow-deadline-detector.d.ts +7 -1
- package/dist/standalone.d.ts +1 -1
- package/dist/standalone.js +12944 -11243
- package/dist/storage/filesystem.d.ts +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/s3.d.ts +36 -0
- package/dist/storage/sign-url.d.ts +2 -1
- package/dist/storage/types.d.ts +2 -2
- package/dist/ws/agent-handler.d.ts +58 -6
- package/dist/ws/agent-heartbeat.d.ts +4 -5
- package/dist/ws/dashboard-context-handler.d.ts +1 -1
- package/dist/ws/platform-client.d.ts +44 -32
- package/dist/ws/server-options.d.ts +1 -1
- package/dist/ws/test-relay-handlers.d.ts +1 -1
- package/installer-image-digests.json +3 -3
- package/package.json +5 -5
- package/sbom.spdx.json +53 -53
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import "node:module";
|
|
|
2
2
|
import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
3
3
|
import { Upload } from "@aws-sdk/lib-storage";
|
|
4
4
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
5
|
-
import { createLogger, createS3Client, sha256 } from "@kici-dev/shared";
|
|
5
|
+
import { createLogger, createS3Client, sha256, toErrorMessage } from "@kici-dev/shared";
|
|
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";
|
|
@@ -33,6 +33,7 @@ import { exec } from "node:child_process";
|
|
|
33
33
|
* TTL is enforced lazily: expired items are deleted on access.
|
|
34
34
|
* Pre-signed URLs are generated for getUrl() with 15-minute expiry.
|
|
35
35
|
*/
|
|
36
|
+
const logger$5 = createLogger({ prefix: "s3-cache" });
|
|
36
37
|
/** Pre-signed download URL expiry in seconds (15 minutes) */
|
|
37
38
|
const PRESIGNED_URL_EXPIRY_SECONDS = 900;
|
|
38
39
|
/** Pre-signed upload URL expiry in seconds (30 minutes -- uploads take longer) */
|
|
@@ -46,6 +47,11 @@ var S3CacheStorage = class {
|
|
|
46
47
|
bucket;
|
|
47
48
|
prefix;
|
|
48
49
|
ttlMs;
|
|
50
|
+
/**
|
|
51
|
+
* False once the backend answered a conditional self-copy with
|
|
52
|
+
* `501 NotImplemented`. From then on `updateMeta` copies unconditionally.
|
|
53
|
+
*/
|
|
54
|
+
conditionalCopySupported = true;
|
|
49
55
|
constructor(options) {
|
|
50
56
|
this.bucket = options.bucket;
|
|
51
57
|
this.prefix = options.prefix;
|
|
@@ -77,6 +83,13 @@ var S3CacheStorage = class {
|
|
|
77
83
|
* Returns null if the object doesn't exist (NoSuchKey/NotFound).
|
|
78
84
|
*/
|
|
79
85
|
async readMeta(key) {
|
|
86
|
+
return (await this.readHead(key))?.meta ?? null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Read an object's TTL metadata together with the ETag of the version it
|
|
90
|
+
* belongs to. Returns null if the object doesn't exist (NoSuchKey/NotFound).
|
|
91
|
+
*/
|
|
92
|
+
async readHead(key) {
|
|
80
93
|
try {
|
|
81
94
|
const head = await this.client.send(new HeadObjectCommand({
|
|
82
95
|
Bucket: this.bucket,
|
|
@@ -86,8 +99,11 @@ var S3CacheStorage = class {
|
|
|
86
99
|
const lastAccessedAt = head.Metadata?.["last-accessed-at"] ?? "";
|
|
87
100
|
if (!createdAt || !lastAccessedAt) return null;
|
|
88
101
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
meta: {
|
|
103
|
+
createdAt,
|
|
104
|
+
lastAccessedAt
|
|
105
|
+
},
|
|
106
|
+
...head.ETag && { etag: head.ETag }
|
|
91
107
|
};
|
|
92
108
|
} catch (err) {
|
|
93
109
|
if (this.isNotFoundError(err)) return null;
|
|
@@ -97,8 +113,41 @@ var S3CacheStorage = class {
|
|
|
97
113
|
/**
|
|
98
114
|
* Update metadata on an S3 object by copying it to itself.
|
|
99
115
|
* This is the standard S3 pattern for updating metadata without re-uploading data.
|
|
116
|
+
*
|
|
117
|
+
* The copy is conditional on `etag`, the version whose metadata the caller
|
|
118
|
+
* read. A self-copy is a whole-object write: without the condition, one that
|
|
119
|
+
* races a `put()` of the same key can land after it and write the older body
|
|
120
|
+
* back. Cache pointers are rewritten in place while builds publish them, so a
|
|
121
|
+
* touch-on-read racing a publish reverted the pointer to a tarball that no
|
|
122
|
+
* longer exists. `CopySourceIfMatch` pins the bytes copied to that version and
|
|
123
|
+
* `IfMatch` refuses the write unless the key still holds it.
|
|
124
|
+
*
|
|
125
|
+
* Returns false when the object was replaced since the read (`412`), or is
|
|
126
|
+
* being replaced right now (`409 ConditionalRequestConflict`): the newer
|
|
127
|
+
* write stamps metadata of its own, so there is nothing left to refresh.
|
|
128
|
+
*
|
|
129
|
+
* An S3-compatible backend that does not implement conditional copies
|
|
130
|
+
* answers `501 NotImplemented`. The copy then runs again without the
|
|
131
|
+
* condition, and later copies skip it, as on a backend that ignores the
|
|
132
|
+
* headers.
|
|
100
133
|
*/
|
|
101
|
-
async updateMeta(key, meta) {
|
|
134
|
+
async updateMeta(key, meta, etag) {
|
|
135
|
+
const condition = this.conditionalCopySupported ? etag : void 0;
|
|
136
|
+
try {
|
|
137
|
+
await this.copyOntoItself(key, meta, condition);
|
|
138
|
+
return true;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (!condition) throw err;
|
|
141
|
+
if (this.isPreconditionFailedError(err) || this.isConditionalConflictError(err)) return false;
|
|
142
|
+
if (!this.isNotImplementedError(err)) throw err;
|
|
143
|
+
this.conditionalCopySupported = false;
|
|
144
|
+
logger$5.warn("The object store does not implement conditional copies; refreshing cache metadata unconditionally", { bucket: this.bucket });
|
|
145
|
+
await this.copyOntoItself(key, meta, void 0);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Copy an object onto itself with new TTL metadata, conditional on `etag` when given. */
|
|
150
|
+
async copyOntoItself(key, meta, etag) {
|
|
102
151
|
const objectKey = this.objectKey(key);
|
|
103
152
|
await this.client.send(new CopyObjectCommand({
|
|
104
153
|
Bucket: this.bucket,
|
|
@@ -108,9 +157,30 @@ var S3CacheStorage = class {
|
|
|
108
157
|
Metadata: {
|
|
109
158
|
"created-at": meta.createdAt,
|
|
110
159
|
"last-accessed-at": meta.lastAccessedAt
|
|
160
|
+
},
|
|
161
|
+
...etag && {
|
|
162
|
+
CopySourceIfMatch: etag,
|
|
163
|
+
IfMatch: etag
|
|
111
164
|
}
|
|
112
165
|
}));
|
|
113
166
|
}
|
|
167
|
+
/** Check if an error is a failed conditional request (PreconditionFailed, 412). */
|
|
168
|
+
isPreconditionFailedError(err) {
|
|
169
|
+
return this.hasErrorShape(err, "PreconditionFailed", 412);
|
|
170
|
+
}
|
|
171
|
+
/** Check if an error is a conditional write that raced another write (ConditionalRequestConflict, 409). */
|
|
172
|
+
isConditionalConflictError(err) {
|
|
173
|
+
return this.hasErrorShape(err, "ConditionalRequestConflict", 409);
|
|
174
|
+
}
|
|
175
|
+
/** Check if an error is a request the backend does not implement (NotImplemented, 501). */
|
|
176
|
+
isNotImplementedError(err) {
|
|
177
|
+
return this.hasErrorShape(err, "NotImplemented", 501);
|
|
178
|
+
}
|
|
179
|
+
hasErrorShape(err, name, status) {
|
|
180
|
+
if (typeof err !== "object" || err === null) return false;
|
|
181
|
+
if (err.name === name) return true;
|
|
182
|
+
return err.$metadata?.httpStatusCode === status;
|
|
183
|
+
}
|
|
114
184
|
/** Check if an error is a "not found" error (NoSuchKey, NotFound, 404). */
|
|
115
185
|
isNotFoundError(err) {
|
|
116
186
|
if (typeof err !== "object" || err === null) return false;
|
|
@@ -135,8 +205,9 @@ var S3CacheStorage = class {
|
|
|
135
205
|
}).done();
|
|
136
206
|
}
|
|
137
207
|
async get(key, ttlMsOverride) {
|
|
138
|
-
const
|
|
139
|
-
if (!
|
|
208
|
+
const head = await this.readHead(key);
|
|
209
|
+
if (!head) return null;
|
|
210
|
+
const { meta, etag } = head;
|
|
140
211
|
if (this.isExpired(meta, ttlMsOverride)) {
|
|
141
212
|
await this.deleteObject(key);
|
|
142
213
|
return null;
|
|
@@ -155,7 +226,7 @@ var S3CacheStorage = class {
|
|
|
155
226
|
}
|
|
156
227
|
try {
|
|
157
228
|
meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
158
|
-
await this.updateMeta(key, meta);
|
|
229
|
+
await this.updateMeta(key, meta, etag);
|
|
159
230
|
} catch {}
|
|
160
231
|
return data;
|
|
161
232
|
}
|
|
@@ -174,10 +245,10 @@ var S3CacheStorage = class {
|
|
|
174
245
|
return existed;
|
|
175
246
|
}
|
|
176
247
|
async touch(key) {
|
|
177
|
-
const
|
|
178
|
-
if (!
|
|
179
|
-
meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
180
|
-
await this.updateMeta(key, meta);
|
|
248
|
+
const head = await this.readHead(key);
|
|
249
|
+
if (!head) return;
|
|
250
|
+
head.meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
251
|
+
await this.updateMeta(key, head.meta, head.etag);
|
|
181
252
|
}
|
|
182
253
|
async getUrl(key, ttlMsOverride) {
|
|
183
254
|
const meta = await this.readMeta(key);
|
|
@@ -299,7 +370,8 @@ var S3CacheStorage = class {
|
|
|
299
370
|
* Signature input: `${method}:${key}:${expiresUnixMs}` with HMAC-SHA256 keyed
|
|
300
371
|
* by a per-orchestrator-process secret (random 32-byte hex at boot, persists
|
|
301
372
|
* for the lifetime of the process). Tokens become invalid on orchestrator
|
|
302
|
-
* restart — fine for the filesystem backend's
|
|
373
|
+
* restart — fine for the filesystem backend's single-host / test-sandbox use
|
|
374
|
+
* case.
|
|
303
375
|
*/
|
|
304
376
|
/** Default URL lifetime: 1 hour (matches S3 backend's PUT-URL lifetime). */
|
|
305
377
|
const DEFAULT_SIGN_URL_TTL_MS = 36e5;
|
|
@@ -328,7 +400,7 @@ function signToken(secret, method, key, ttlMs = DEFAULT_SIGN_URL_TTL_MS) {
|
|
|
328
400
|
* are replaced by HMAC-signed `http://<orchUrl>/api/v1/cache/blob/...` URLs
|
|
329
401
|
* — see `sign-url.ts` for the token mechanics and `app.ts` for the route.
|
|
330
402
|
*
|
|
331
|
-
* Intended for single-host deployments and
|
|
403
|
+
* Intended for single-host deployments and test sandboxes where standing up
|
|
332
404
|
* an S3-compatible service is overkill. Production should still use S3.
|
|
333
405
|
*
|
|
334
406
|
* Metadata is co-located as a sibling JSON file (`<key>.meta.json`) so the
|
|
@@ -632,7 +704,7 @@ function createCacheStorage(config) {
|
|
|
632
704
|
* Old objects are orphaned. The cache TTL is enforced lazily on access, so
|
|
633
705
|
* nothing here removes them; `kici-admin cache purge-legacy` does.
|
|
634
706
|
*/
|
|
635
|
-
const logger$
|
|
707
|
+
const logger$4 = createLogger({ prefix: "source-cache" });
|
|
636
708
|
/**
|
|
637
709
|
* Cache key for a source tarball, addressed by the tarball's OWN content hash:
|
|
638
710
|
* `source/v2/{orgId}/{sourceTarDigest}.tar.gz`.
|
|
@@ -675,25 +747,25 @@ var SourceCache = class SourceCache {
|
|
|
675
747
|
async has(orgId, contentHash) {
|
|
676
748
|
const digest = await this.resolvePointer(orgId, contentHash);
|
|
677
749
|
if (!digest) {
|
|
678
|
-
logger$
|
|
750
|
+
logger$4.debug(`has(${contentHash}): false (no pointer)`, { orgId });
|
|
679
751
|
return false;
|
|
680
752
|
}
|
|
681
753
|
const exists = await this.storage.has(sourceTarballKey(orgId, digest));
|
|
682
|
-
logger$
|
|
754
|
+
logger$4.debug(`has(${contentHash}): ${exists}`, { orgId });
|
|
683
755
|
return exists;
|
|
684
756
|
}
|
|
685
757
|
async get(orgId, contentHash) {
|
|
686
758
|
const digest = await this.resolvePointer(orgId, contentHash);
|
|
687
759
|
if (!digest) {
|
|
688
|
-
logger$
|
|
760
|
+
logger$4.debug(`get(${contentHash}): miss (no pointer)`, { orgId });
|
|
689
761
|
return null;
|
|
690
762
|
}
|
|
691
763
|
const key = sourceTarballKey(orgId, digest);
|
|
692
764
|
const data = await this.storage.get(key);
|
|
693
765
|
if (data) {
|
|
694
766
|
await this.storage.touch(key);
|
|
695
|
-
logger$
|
|
696
|
-
} else logger$
|
|
767
|
+
logger$4.debug(`get(${contentHash}): hit (${data.length} bytes)`, { orgId });
|
|
768
|
+
} else logger$4.debug(`get(${contentHash}): miss (dangling pointer)`, { orgId });
|
|
697
769
|
return data;
|
|
698
770
|
}
|
|
699
771
|
/**
|
|
@@ -709,18 +781,18 @@ var SourceCache = class SourceCache {
|
|
|
709
781
|
const pointerKey = sourcePointerKey(orgId, contentHash);
|
|
710
782
|
const digest = (await this.storage.get(pointerKey))?.toString("utf-8").trim() || null;
|
|
711
783
|
if (!digest) {
|
|
712
|
-
logger$
|
|
784
|
+
logger$4.debug(`getUrlAndDigest(${contentHash}): miss (no pointer)`, { orgId });
|
|
713
785
|
return null;
|
|
714
786
|
}
|
|
715
787
|
const key = sourceTarballKey(orgId, digest);
|
|
716
788
|
const url = await this.storage.getUrl(key);
|
|
717
789
|
if (!url) {
|
|
718
|
-
logger$
|
|
790
|
+
logger$4.debug(`getUrlAndDigest(${contentHash}): miss (dangling pointer)`, { orgId });
|
|
719
791
|
return null;
|
|
720
792
|
}
|
|
721
793
|
await this.storage.touch(key);
|
|
722
794
|
await this.storage.touch(pointerKey);
|
|
723
|
-
logger$
|
|
795
|
+
logger$4.debug(`getUrlAndDigest(${contentHash}): hit`, { orgId });
|
|
724
796
|
return {
|
|
725
797
|
url,
|
|
726
798
|
digest
|
|
@@ -746,7 +818,7 @@ var SourceCache = class SourceCache {
|
|
|
746
818
|
*/
|
|
747
819
|
async publishPointer(orgId, contentHash, sourceTarDigest) {
|
|
748
820
|
await this.storage.put(sourcePointerKey(orgId, contentHash), sourceTarDigest);
|
|
749
|
-
logger$
|
|
821
|
+
logger$4.info(`publishPointer(${contentHash}) -> ${sourceTarDigest.slice(0, 12)}`, { orgId });
|
|
750
822
|
}
|
|
751
823
|
/** Store a tarball and the pointer that names it. */
|
|
752
824
|
async store(orgId, contentHash, tarball) {
|
|
@@ -754,7 +826,7 @@ var SourceCache = class SourceCache {
|
|
|
754
826
|
const digest = SourceCache.computeDigest(data);
|
|
755
827
|
await this.storage.put(sourceTarballKey(orgId, digest), data);
|
|
756
828
|
await this.storage.put(sourcePointerKey(orgId, contentHash), digest);
|
|
757
|
-
logger$
|
|
829
|
+
logger$4.info(`store(${contentHash}): stored (${data.length} bytes)`, {
|
|
758
830
|
orgId,
|
|
759
831
|
digest: digest.slice(0, 12)
|
|
760
832
|
});
|
|
@@ -768,7 +840,7 @@ var SourceCache = class SourceCache {
|
|
|
768
840
|
const digest = await this.resolvePointer(orgId, contentHash);
|
|
769
841
|
await this.storage.delete(sourcePointerKey(orgId, contentHash));
|
|
770
842
|
const removed = digest ? await this.storage.delete(sourceTarballKey(orgId, digest)) : false;
|
|
771
|
-
logger$
|
|
843
|
+
logger$4.info(`remove(${contentHash}): ${removed ? "removed" : "not found"}`, { orgId });
|
|
772
844
|
return removed;
|
|
773
845
|
}
|
|
774
846
|
};
|
|
@@ -785,7 +857,7 @@ var SourceCache = class SourceCache {
|
|
|
785
857
|
* Keys: `deps/{platform}-{arch}/{depsHash}.tar.gz` (immutable) and
|
|
786
858
|
* `deps/{platform}-{arch}/{lockfileHash}.hash` (the pointer).
|
|
787
859
|
*/
|
|
788
|
-
const logger$
|
|
860
|
+
const logger$3 = createLogger({ prefix: "dep-cache" });
|
|
789
861
|
/** Default max tarball size: 500MB */
|
|
790
862
|
const DEFAULT_MAX_TARBALL_BYTES = 524288e3;
|
|
791
863
|
/** Default dependency-cache entry TTL: 30 days. */
|
|
@@ -866,14 +938,14 @@ var DepCache = class DepCache {
|
|
|
866
938
|
const ttlMsOverride = await this.resolveTtlMsOverride();
|
|
867
939
|
const depsHash = await this.resolvePointer(lockfileHash, platform, arch, ttlMsOverride, siblingsDigest);
|
|
868
940
|
if (!depsHash) {
|
|
869
|
-
logger$
|
|
941
|
+
logger$3.debug(`has(${lockfileHash}): false (no pointer)`, {
|
|
870
942
|
platform,
|
|
871
943
|
arch
|
|
872
944
|
});
|
|
873
945
|
return false;
|
|
874
946
|
}
|
|
875
947
|
const exists = await this.storage.has(depTarballKey(depsHash, platform, arch), ttlMsOverride);
|
|
876
|
-
logger$
|
|
948
|
+
logger$3.debug(`has(${lockfileHash}): ${exists}`, {
|
|
877
949
|
platform,
|
|
878
950
|
arch
|
|
879
951
|
});
|
|
@@ -897,7 +969,7 @@ var DepCache = class DepCache {
|
|
|
897
969
|
const pointerKey = depPointerKey(lockfileHash, platform, arch, siblingsDigest);
|
|
898
970
|
const hash = (await this.storage.get(pointerKey, ttlMsOverride))?.toString("utf-8").trim() || void 0;
|
|
899
971
|
if (!hash) {
|
|
900
|
-
logger$
|
|
972
|
+
logger$3.debug(`getUrlAndHash(${lockfileHash}): miss (no pointer)`, {
|
|
901
973
|
platform,
|
|
902
974
|
arch
|
|
903
975
|
});
|
|
@@ -906,7 +978,7 @@ var DepCache = class DepCache {
|
|
|
906
978
|
const key = depTarballKey(hash, platform, arch);
|
|
907
979
|
const url = await this.storage.getUrl(key, ttlMsOverride);
|
|
908
980
|
if (!url) {
|
|
909
|
-
logger$
|
|
981
|
+
logger$3.debug(`getUrlAndHash(${lockfileHash}): miss (dangling pointer)`, {
|
|
910
982
|
platform,
|
|
911
983
|
arch
|
|
912
984
|
});
|
|
@@ -914,7 +986,7 @@ var DepCache = class DepCache {
|
|
|
914
986
|
}
|
|
915
987
|
await this.storage.touch(key);
|
|
916
988
|
await this.storage.touch(pointerKey);
|
|
917
|
-
logger$
|
|
989
|
+
logger$3.debug(`getUrlAndHash(${lockfileHash}): hit`, {
|
|
918
990
|
platform,
|
|
919
991
|
arch,
|
|
920
992
|
hasHash: true
|
|
@@ -938,7 +1010,7 @@ var DepCache = class DepCache {
|
|
|
938
1010
|
*/
|
|
939
1011
|
async publishPointer(lockfileHash, platform, arch, depsHash, siblingsDigest) {
|
|
940
1012
|
await this.storage.put(depPointerKey(lockfileHash, platform, arch, siblingsDigest), depsHash);
|
|
941
|
-
logger$
|
|
1013
|
+
logger$3.info(`publishPointer(${lockfileHash}) -> ${depsHash.slice(0, 12)}`, {
|
|
942
1014
|
platform,
|
|
943
1015
|
arch
|
|
944
1016
|
});
|
|
@@ -953,7 +1025,7 @@ var DepCache = class DepCache {
|
|
|
953
1025
|
const depsHash = DepCache.computeHash(tarballData);
|
|
954
1026
|
await this.storage.put(depTarballKey(depsHash, platform, arch), tarballData);
|
|
955
1027
|
await this.storage.put(depPointerKey(lockfileHash, platform, arch), depsHash);
|
|
956
|
-
logger$
|
|
1028
|
+
logger$3.info(`store: ${tarballData.length} bytes`, {
|
|
957
1029
|
lockfileHash,
|
|
958
1030
|
platform,
|
|
959
1031
|
arch,
|
|
@@ -972,7 +1044,7 @@ var DepCache = class DepCache {
|
|
|
972
1044
|
const depsHash = await this.resolvePointer(lockfileHash, platform, arch, await this.resolveTtlMsOverride());
|
|
973
1045
|
await this.storage.delete(depPointerKey(lockfileHash, platform, arch));
|
|
974
1046
|
const removed = depsHash ? await this.storage.delete(depTarballKey(depsHash, platform, arch)) : false;
|
|
975
|
-
logger$
|
|
1047
|
+
logger$3.info(`remove(${lockfileHash}): ${removed ? "removed" : "not found"}`, {
|
|
976
1048
|
platform,
|
|
977
1049
|
arch
|
|
978
1050
|
});
|
|
@@ -1016,7 +1088,7 @@ function keyDiscriminator(exact) {
|
|
|
1016
1088
|
* hash and size accounting outside the tarball's own (presigned, metadata-less)
|
|
1017
1089
|
* upload.
|
|
1018
1090
|
*/
|
|
1019
|
-
const logger$
|
|
1091
|
+
const logger$2 = createLogger({ prefix: "user-cache" });
|
|
1020
1092
|
/**
|
|
1021
1093
|
* Cluster-wide default quota: 5 GiB. Serves as the fallback when an org has no
|
|
1022
1094
|
* per-org override in `org_settings.user_cache_quota_bytes`. The cluster-wide
|
|
@@ -1164,7 +1236,7 @@ var UserCache = class {
|
|
|
1164
1236
|
try {
|
|
1165
1237
|
limits = await this.orgLimitsReader(org);
|
|
1166
1238
|
} catch (err) {
|
|
1167
|
-
logger$
|
|
1239
|
+
logger$2.warn("user-cache org-limits lookup failed — using cluster defaults", {
|
|
1168
1240
|
org,
|
|
1169
1241
|
error: err instanceof Error ? err.message : String(err)
|
|
1170
1242
|
});
|
|
@@ -1221,7 +1293,7 @@ var UserCache = class {
|
|
|
1221
1293
|
async beginSave(ref) {
|
|
1222
1294
|
const final = userCacheEntryKey(ref, ref.key);
|
|
1223
1295
|
if (await this.storage.has(final)) {
|
|
1224
|
-
logger$
|
|
1296
|
+
logger$2.info("user-cache save skipped (immutable key exists)", { key: ref.key });
|
|
1225
1297
|
return { skip: true };
|
|
1226
1298
|
}
|
|
1227
1299
|
const tempKey = `${writePrefix(ref)}${TEMP_UPLOAD_STEM}${randomUUID()}${TAR_SUFFIX}`;
|
|
@@ -1242,7 +1314,7 @@ var UserCache = class {
|
|
|
1242
1314
|
await this.storage.initMeta(final);
|
|
1243
1315
|
await this.storage.put(`${final}.hash`, ref.tarHash);
|
|
1244
1316
|
await this.storage.put(`${final}.size`, String(ref.sizeBytes));
|
|
1245
|
-
logger$
|
|
1317
|
+
logger$2.info("user-cache entry committed", {
|
|
1246
1318
|
key: ref.key,
|
|
1247
1319
|
sizeBytes: ref.sizeBytes
|
|
1248
1320
|
});
|
|
@@ -1292,7 +1364,7 @@ var UserCache = class {
|
|
|
1292
1364
|
await this.storage.delete(`${key}.hash`);
|
|
1293
1365
|
await this.storage.delete(`${key}.size`);
|
|
1294
1366
|
total -= size;
|
|
1295
|
-
logger$
|
|
1367
|
+
logger$2.info("user-cache eviction (org over quota)", {
|
|
1296
1368
|
org: ref.org,
|
|
1297
1369
|
key,
|
|
1298
1370
|
freedBytes: size
|
|
@@ -1300,18 +1372,12 @@ var UserCache = class {
|
|
|
1300
1372
|
}
|
|
1301
1373
|
}
|
|
1302
1374
|
};
|
|
1303
|
-
//#endregion
|
|
1304
|
-
//#region src/cluster/peer-credentials.ts
|
|
1305
1375
|
/**
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
* 1. DB CRUD (PeerCredentialStore) — coordinator stores credentials for all peers
|
|
1310
|
-
* 2. File I/O (readCredentialFile/writeCredentialFile) — workers persist their
|
|
1311
|
-
* credential locally for reconnection after restart
|
|
1312
|
-
*
|
|
1313
|
-
* Credential files are written with 0600 permissions to protect the secret.
|
|
1376
|
+
* `metadata` key set when a coordinator retires the self-issued credential its
|
|
1377
|
+
* previous run left behind. An operator revoke never sets it, which is how a
|
|
1378
|
+
* retirement is told apart from a revoke.
|
|
1314
1379
|
*/
|
|
1380
|
+
const RETIRED_BY_INSTANCE_KEY = "retiredByInstance";
|
|
1315
1381
|
/** Default credential expiry: 90 days. */
|
|
1316
1382
|
const DEFAULT_EXPIRY_DAYS = 90;
|
|
1317
1383
|
/**
|
|
@@ -1348,7 +1414,8 @@ var PeerCredentialStore = class {
|
|
|
1348
1414
|
role: opts.role,
|
|
1349
1415
|
routing_keys: sql`${sql.val(opts.routingKeys)}`,
|
|
1350
1416
|
source_token_hash: opts.sourceTokenHash ?? null,
|
|
1351
|
-
expires_at: expiresAt
|
|
1417
|
+
expires_at: expiresAt,
|
|
1418
|
+
...opts.metadata && { metadata: sql`${JSON.stringify(opts.metadata)}::jsonb` }
|
|
1352
1419
|
}).execute();
|
|
1353
1420
|
return Number(revokeResult?.numUpdatedRows ?? 0n);
|
|
1354
1421
|
});
|
|
@@ -1375,6 +1442,33 @@ var PeerCredentialStore = class {
|
|
|
1375
1442
|
return row ? mapRow(row) : null;
|
|
1376
1443
|
}
|
|
1377
1444
|
/**
|
|
1445
|
+
* The credential row for `instanceId` that nobody revoked, expired or not.
|
|
1446
|
+
* `peer_credentials_active_uniq` allows at most one.
|
|
1447
|
+
*/
|
|
1448
|
+
async findUnrevokedByInstanceId(instanceId) {
|
|
1449
|
+
const row = await this.db.selectFrom("peer_credentials").selectAll().where("instance_id", "=", instanceId).where("revoked_at", "is", null).executeTakeFirst();
|
|
1450
|
+
return row ? mapRow(row) : null;
|
|
1451
|
+
}
|
|
1452
|
+
/** The most recently revoked credential row for `instanceId`, or null. */
|
|
1453
|
+
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();
|
|
1455
|
+
return row ? mapRow(row) : null;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Revoke `instanceId`'s unrevoked, self-issued row whose hash is
|
|
1459
|
+
* `credentialHash`, recording who retired it. Every condition sits in the
|
|
1460
|
+
* WHERE clause, so a row that changed meanwhile is left alone.
|
|
1461
|
+
* Returns whether a row was retired.
|
|
1462
|
+
*/
|
|
1463
|
+
async retireSelfIssued(opts) {
|
|
1464
|
+
const marker = JSON.stringify({ [RETIRED_BY_INSTANCE_KEY]: opts.retiredByInstance });
|
|
1465
|
+
const result = await this.db.updateTable("peer_credentials").set({
|
|
1466
|
+
revoked_at: /* @__PURE__ */ new Date(),
|
|
1467
|
+
metadata: sql`COALESCE(metadata, '{}'::jsonb) || ${marker}::jsonb`
|
|
1468
|
+
}).where("instance_id", "=", opts.instanceId).where("credential_hash", "=", opts.credentialHash).where("revoked_at", "is", null).where(sql`metadata->>'issuance'`, "=", "self").executeTakeFirst();
|
|
1469
|
+
return Number(result?.numUpdatedRows ?? 0n) > 0;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1378
1472
|
* Update the last_seen_at timestamp for a credential.
|
|
1379
1473
|
* Optionally records which coordinator validated it.
|
|
1380
1474
|
*/
|
|
@@ -1419,7 +1513,8 @@ function mapRow(row) {
|
|
|
1419
1513
|
lastSeenAt: row.last_seen_at ? new Date(row.last_seen_at) : null,
|
|
1420
1514
|
lastValidatedBy: row.last_validated_by ?? null,
|
|
1421
1515
|
expiresAt: new Date(row.expires_at),
|
|
1422
|
-
revokedAt: row.revoked_at ? new Date(row.revoked_at) : null
|
|
1516
|
+
revokedAt: row.revoked_at ? new Date(row.revoked_at) : null,
|
|
1517
|
+
metadata: row.metadata ?? {}
|
|
1423
1518
|
};
|
|
1424
1519
|
}
|
|
1425
1520
|
/**
|
|
@@ -1461,7 +1556,7 @@ async function readCredentialFile(filePath) {
|
|
|
1461
1556
|
* store plus a disposer that closes the underlying pool. Lets tests
|
|
1462
1557
|
* exercise the real Kysely-backed store without importing pg/kysely.
|
|
1463
1558
|
*
|
|
1464
|
-
* Intended for
|
|
1559
|
+
* Intended for tests of the store's own CRUD methods — production
|
|
1465
1560
|
* code constructs the store with an already-allocated Kysely instance
|
|
1466
1561
|
* shared with the orchestrator.
|
|
1467
1562
|
*/
|
|
@@ -1479,6 +1574,43 @@ async function createPeerCredentialStoreFromUrl(databaseUrl, opts) {
|
|
|
1479
1574
|
};
|
|
1480
1575
|
}
|
|
1481
1576
|
//#endregion
|
|
1577
|
+
//#region src/helpers/run-detached.ts
|
|
1578
|
+
/**
|
|
1579
|
+
* Running work that nothing awaits.
|
|
1580
|
+
*
|
|
1581
|
+
* A promise that nobody awaits and nobody catches becomes an unhandled
|
|
1582
|
+
* rejection when it fails, and the orchestrator's shutdown hook treats an
|
|
1583
|
+
* unhandled rejection as fatal: one failed query in a timer tick or a message
|
|
1584
|
+
* handler would stop the whole process. `runDetached` runs such work and logs
|
|
1585
|
+
* its failure instead. ESLint's `no-floating-promises` (with `ignoreVoid:
|
|
1586
|
+
* false`) refuses a bare or `void`-ed promise anywhere in this package, so
|
|
1587
|
+
* detached work runs through here or carries its own `.catch`.
|
|
1588
|
+
*/
|
|
1589
|
+
/**
|
|
1590
|
+
* Run `work` without waiting for it. A throw, or a rejection of the promise it
|
|
1591
|
+
* returns, is logged at error level as `<task> failed` with `context` and the
|
|
1592
|
+
* error message, and goes no further. `context` is read when the failure is
|
|
1593
|
+
* logged, so the work may fill it in while it runs.
|
|
1594
|
+
*/
|
|
1595
|
+
function runDetached(logger, task, work, context = {}) {
|
|
1596
|
+
const fail = (err) => {
|
|
1597
|
+
logger.error(`${task} failed`, {
|
|
1598
|
+
...context,
|
|
1599
|
+
error: toErrorMessage(err)
|
|
1600
|
+
});
|
|
1601
|
+
};
|
|
1602
|
+
let outcome;
|
|
1603
|
+
try {
|
|
1604
|
+
outcome = work();
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
fail(err);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
Promise.resolve(outcome).catch(fail);
|
|
1610
|
+
}
|
|
1611
|
+
createLogger({ prefix: "instance-heartbeat" });
|
|
1612
|
+
createLogger({ prefix: "coordinator-credential" });
|
|
1613
|
+
//#endregion
|
|
1482
1614
|
//#region src/cluster/peer-auth-coordinator.ts
|
|
1483
1615
|
/**
|
|
1484
1616
|
* Per-orchestrator coordinator that owns the shared peer credential file.
|
|
@@ -1490,6 +1622,10 @@ async function createPeerCredentialStoreFromUrl(databaseUrl, opts) {
|
|
|
1490
1622
|
* rejection — a credential revocation cascade. This coordinator serializes all
|
|
1491
1623
|
* file access through one in-process mutex so only one peer-client token-joins
|
|
1492
1624
|
* per storm, and it never deletes a credential file a sibling has refreshed.
|
|
1625
|
+
*
|
|
1626
|
+
* A coordinator without a join token also wires a `selfIssue` callback. When a
|
|
1627
|
+
* decision finds no credential and no token, the callback issues the
|
|
1628
|
+
* coordinator its own credential, once per storm, under the same mutex.
|
|
1493
1629
|
*/
|
|
1494
1630
|
const logger = createLogger({ prefix: "peer-auth-coordinator" });
|
|
1495
1631
|
/** How long a waiting peer-client awaits an in-flight sibling token-join. */
|
|
@@ -1510,6 +1646,11 @@ var PeerAuthCoordinator = class {
|
|
|
1510
1646
|
instanceId;
|
|
1511
1647
|
joinToken;
|
|
1512
1648
|
joinWaitTimeoutMs;
|
|
1649
|
+
selfIssue;
|
|
1650
|
+
/** The issuance in progress; concurrent sibling clients share it. */
|
|
1651
|
+
selfIssueInFlight = null;
|
|
1652
|
+
/** An operator revoked this coordinator's credential: never issue again in this process. */
|
|
1653
|
+
selfIssueRevoked = false;
|
|
1513
1654
|
/** Promise-chain mutex tail; every file op awaits the prior one. */
|
|
1514
1655
|
lock = Promise.resolve();
|
|
1515
1656
|
/** Set while one peer-client is mid token-join; siblings await it. */
|
|
@@ -1519,6 +1660,7 @@ var PeerAuthCoordinator = class {
|
|
|
1519
1660
|
this.instanceId = opts.instanceId;
|
|
1520
1661
|
this.joinToken = opts.joinToken;
|
|
1521
1662
|
this.joinWaitTimeoutMs = opts.joinWaitTimeoutMs ?? DEFAULT_JOIN_WAIT_TIMEOUT_MS;
|
|
1663
|
+
this.selfIssue = opts.selfIssue;
|
|
1522
1664
|
}
|
|
1523
1665
|
/** Run `fn` exclusively against the credential file. */
|
|
1524
1666
|
async withLock(fn) {
|
|
@@ -1527,10 +1669,21 @@ var PeerAuthCoordinator = class {
|
|
|
1527
1669
|
return run;
|
|
1528
1670
|
}
|
|
1529
1671
|
async readValidCredential() {
|
|
1530
|
-
|
|
1672
|
+
let cred;
|
|
1673
|
+
try {
|
|
1674
|
+
cred = await readCredentialFile(this.credentialFile);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
logger.warn("Ignoring unreadable peer credential file", {
|
|
1677
|
+
instanceId: this.instanceId,
|
|
1678
|
+
path: this.credentialFile,
|
|
1679
|
+
error: toErrorMessage(err)
|
|
1680
|
+
});
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1531
1683
|
return cred && cred.instanceId === this.instanceId ? cred : null;
|
|
1532
1684
|
}
|
|
1533
1685
|
async decideAuth() {
|
|
1686
|
+
let selfIssueTried = false;
|
|
1534
1687
|
for (let i = 0; i < MAX_DECIDE_ITERATIONS; i++) {
|
|
1535
1688
|
const decision = await this.withLock(async () => {
|
|
1536
1689
|
const cred = await this.readValidCredential();
|
|
@@ -1548,10 +1701,19 @@ var PeerAuthCoordinator = class {
|
|
|
1548
1701
|
complete: this.makeComplete(join)
|
|
1549
1702
|
};
|
|
1550
1703
|
}
|
|
1704
|
+
if (this.selfIssue && !selfIssueTried && !this.selfIssueRevoked) return "self-issue";
|
|
1551
1705
|
return { mode: "no-auth" };
|
|
1552
1706
|
});
|
|
1553
|
-
if (decision
|
|
1554
|
-
|
|
1707
|
+
if (decision === "await-join") {
|
|
1708
|
+
await this.awaitInFlightJoin();
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
if (decision === "self-issue") {
|
|
1712
|
+
selfIssueTried = true;
|
|
1713
|
+
await this.runSelfIssue();
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
return decision;
|
|
1555
1717
|
}
|
|
1556
1718
|
if (this.joinToken) {
|
|
1557
1719
|
const join = deferred();
|
|
@@ -1564,13 +1726,25 @@ var PeerAuthCoordinator = class {
|
|
|
1564
1726
|
}
|
|
1565
1727
|
return { mode: "no-auth" };
|
|
1566
1728
|
}
|
|
1729
|
+
/** Run the issuer under the mutex, once for every concurrent caller. */
|
|
1730
|
+
runSelfIssue() {
|
|
1731
|
+
const selfIssue = this.selfIssue;
|
|
1732
|
+
if (!selfIssue) return Promise.resolve("failed");
|
|
1733
|
+
this.selfIssueInFlight ??= this.withLock(selfIssue).then((outcome) => {
|
|
1734
|
+
if (outcome === "revoked") this.selfIssueRevoked = true;
|
|
1735
|
+
return outcome;
|
|
1736
|
+
}).finally(() => {
|
|
1737
|
+
this.selfIssueInFlight = null;
|
|
1738
|
+
});
|
|
1739
|
+
return this.selfIssueInFlight;
|
|
1740
|
+
}
|
|
1567
1741
|
makeComplete(join) {
|
|
1568
1742
|
return (issued) => {
|
|
1569
|
-
this.withLock(async () => {
|
|
1743
|
+
runDetached(logger, "Peer token-join completion", () => this.withLock(async () => {
|
|
1570
1744
|
if (issued) await writeCredentialFile(this.credentialFile, issued);
|
|
1571
1745
|
if (this.inFlightJoin === join) this.inFlightJoin = null;
|
|
1572
1746
|
join.resolve(issued);
|
|
1573
|
-
});
|
|
1747
|
+
}), { instanceId: this.instanceId });
|
|
1574
1748
|
};
|
|
1575
1749
|
}
|
|
1576
1750
|
async awaitInFlightJoin() {
|
|
@@ -1632,7 +1806,8 @@ var PeerAuthCoordinator = class {
|
|
|
1632
1806
|
const joinTokenLogger = createLogger({ prefix: "join-token" });
|
|
1633
1807
|
const TOKEN_PREFIX = "kici_join_v1";
|
|
1634
1808
|
const DEFAULT_EXPIRY_MS = 36e5;
|
|
1635
|
-
const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used.
|
|
1809
|
+
const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used. Create a new token with: kici-admin peer create-token";
|
|
1810
|
+
const TOKEN_EXPIRED_MESSAGE = "Join token has expired. Create a new token with: kici-admin peer create-token";
|
|
1636
1811
|
var JoinTokenManager = class {
|
|
1637
1812
|
deps;
|
|
1638
1813
|
constructor(deps) {
|
|
@@ -1717,7 +1892,7 @@ var JoinTokenManager = class {
|
|
|
1717
1892
|
}
|
|
1718
1893
|
const row = await this.deps.db.selectFrom("join_tokens").selectAll().where("token_hash", "=", keys.validationHash).executeTakeFirst();
|
|
1719
1894
|
if (!row) throw new Error("Invalid join token");
|
|
1720
|
-
if (new Date(row.expires_at).getTime() <= Date.now()) throw new Error(
|
|
1895
|
+
if (new Date(row.expires_at).getTime() <= Date.now()) throw new Error(TOKEN_EXPIRED_MESSAGE);
|
|
1721
1896
|
if (row.consumed_at) {
|
|
1722
1897
|
if (row.consumed_by_instance === peerInstanceId) {
|
|
1723
1898
|
joinTokenLogger.info("Re-validated join token for returning instance", {
|
|
@@ -1737,8 +1912,8 @@ var JoinTokenManager = class {
|
|
|
1737
1912
|
/**
|
|
1738
1913
|
* Build a JoinTokenManager backed by its own connection pool to the given
|
|
1739
1914
|
* orchestrator database URL. Mirrors `createPeerCredentialStoreFromUrl`;
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1915
|
+
* used by tests that need to exercise token validation/reuse against a real
|
|
1916
|
+
* cluster DB.
|
|
1742
1917
|
*/
|
|
1743
1918
|
function createJoinTokenManagerFromUrl(databaseUrl, opts) {
|
|
1744
1919
|
const pool = new pg.Pool({
|
|
@@ -2137,8 +2312,8 @@ var HeldRunStore = class {
|
|
|
2137
2312
|
/**
|
|
2138
2313
|
* Build a HeldRunStore backed by its own connection pool to the given
|
|
2139
2314
|
* orchestrator database URL. Mirrors `createPeerCredentialStoreFromUrl` /
|
|
2140
|
-
* `createJoinTokenManagerFromUrl`;
|
|
2141
|
-
*
|
|
2315
|
+
* `createJoinTokenManagerFromUrl`; used by tests that exercise the PR-scoped
|
|
2316
|
+
* hold selection against a real orchestrator DB.
|
|
2142
2317
|
*/
|
|
2143
2318
|
function createHeldRunStoreFromUrl(databaseUrl, opts) {
|
|
2144
2319
|
const pool = new pg.Pool({
|
|
@@ -2303,9 +2478,9 @@ var TrustPolicyStore = class {
|
|
|
2303
2478
|
};
|
|
2304
2479
|
/**
|
|
2305
2480
|
* Build a TrustPolicyStore backed by its own connection pool to the given
|
|
2306
|
-
* orchestrator database URL. Mirrors `createHeldRunStoreFromUrl`;
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2481
|
+
* orchestrator database URL. Mirrors `createHeldRunStoreFromUrl`; exported for
|
|
2482
|
+
* tests that assert the pushed policy is persisted in a real orchestrator DB
|
|
2483
|
+
* rather than held in process memory.
|
|
2309
2484
|
*/
|
|
2310
2485
|
function createTrustPolicyStoreFromUrl(databaseUrl, opts) {
|
|
2311
2486
|
const pool = new pg.Pool({
|
|
@@ -3107,7 +3282,7 @@ function startManifestLoopback(opts) {
|
|
|
3107
3282
|
* blocked launcher) just falls back to copy-paste.
|
|
3108
3283
|
*
|
|
3109
3284
|
* `KICI_BROWSER_CMD` overrides the launcher: `none` suppresses it entirely
|
|
3110
|
-
* (
|
|
3285
|
+
* (automated tests / headless capture), any other value is run with `{url}` substituted —
|
|
3111
3286
|
* mirroring the `kici login` convention.
|
|
3112
3287
|
*/
|
|
3113
3288
|
function openBrowserBestEffort(url) {
|