@retinue/agentkit 0.2.0 → 0.3.1
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/README.md +1 -1
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +78 -1
- package/dist/agents/agent.js +118 -5
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +20 -0
- package/dist/agents/engine.js +177 -6
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/content-parts.d.ts +18 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +44 -2
- package/dist/core/events.js +31 -2
- package/dist/core/validation.js +19 -0
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/knowledge.d.ts +8 -0
- package/dist/entries/knowledge.js +8 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +3 -1
- package/dist/graphql/resolvers.js +14 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +24 -0
- package/dist/knowledge/index.js +36 -1
- package/dist/knowledge/retrieval.d.ts +26 -1
- package/dist/knowledge/retrieval.js +77 -1
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +89 -0
- package/dist/models/streaming.js +179 -2
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +46 -0
- package/dist/server/bin.d.ts +72 -0
- package/dist/server/bin.js +304 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli-worker.js +6 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +10 -5
- package/dist/server/config.d.ts +11 -1
- package/dist/server/config.js +18 -0
- package/dist/server/doctor.d.ts +85 -0
- package/dist/server/doctor.js +280 -0
- package/dist/server/pool.d.ts +47 -0
- package/dist/server/pool.js +73 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +1 -0
- package/dist/telemetry/spans.js +5 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +27 -1
- package/dist/toolkit/index.js +24 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/tools/credentials.d.ts +265 -6
- package/dist/tools/credentials.js +318 -3
- package/dist/tools/define.d.ts +14 -0
- package/dist/tools/define.js +3 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/index.d.ts +8 -0
- package/dist/tools/library/index.js +7 -0
- package/dist/tools/registry.d.ts +33 -0
- package/dist/tools/registry.js +72 -2
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +29 -2
package/dist/runtime/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { PlatformError } from "../core/errors.js";
|
|
5
5
|
import type { AgentId, ConversationId, PrincipalId, RunId, TenantId } from "../core/ids.js";
|
|
6
|
-
export declare const RUN_STATUSES: readonly ["queued", "running", "waiting-for-question", "waiting-for-approval", "retry-pending", "completed", "failed", "cancelled"];
|
|
6
|
+
export declare const RUN_STATUSES: readonly ["queued", "running", "waiting-for-question", "waiting-for-approval", "waiting-for-connection", "retry-pending", "completed", "failed", "cancelled"];
|
|
7
7
|
export type RunStatus = (typeof RUN_STATUSES)[number];
|
|
8
8
|
/**
|
|
9
9
|
* The state machine from the specification. A transition absent from this map is a
|
package/dist/runtime/index.js
CHANGED
|
@@ -7,6 +7,14 @@ export const RUN_STATUSES = [
|
|
|
7
7
|
"running",
|
|
8
8
|
"waiting-for-question",
|
|
9
9
|
"waiting-for-approval",
|
|
10
|
+
/**
|
|
11
|
+
* The run needs a connection the tenant does not have, and a person must complete a consent — task #264.
|
|
12
|
+
*
|
|
13
|
+
* A third pause beside question and approval, and deliberately the *same shape*: the runtime already stops a
|
|
14
|
+
* run and resumes it twice over, and a bespoke polling loop or a failed run somebody restarts by hand would
|
|
15
|
+
* be a second mechanism for the thing the durable runtime exists to do.
|
|
16
|
+
*/
|
|
17
|
+
"waiting-for-connection",
|
|
10
18
|
"retry-pending",
|
|
11
19
|
"completed",
|
|
12
20
|
"failed",
|
|
@@ -21,6 +29,7 @@ export const RUN_TRANSITIONS = {
|
|
|
21
29
|
running: [
|
|
22
30
|
"waiting-for-question",
|
|
23
31
|
"waiting-for-approval",
|
|
32
|
+
"waiting-for-connection",
|
|
24
33
|
"retry-pending",
|
|
25
34
|
"completed",
|
|
26
35
|
"failed",
|
|
@@ -28,6 +37,9 @@ export const RUN_TRANSITIONS = {
|
|
|
28
37
|
],
|
|
29
38
|
"waiting-for-question": ["queued", "cancelled"],
|
|
30
39
|
"waiting-for-approval": ["queued", "cancelled"],
|
|
40
|
+
// Resumes to `queued` exactly as the other two do: consent completing is an answer, and the run rejoins the
|
|
41
|
+
// queue rather than continuing in whatever process happened to receive the callback.
|
|
42
|
+
"waiting-for-connection": ["queued", "cancelled"],
|
|
31
43
|
"retry-pending": ["queued", "cancelled"],
|
|
32
44
|
completed: [],
|
|
33
45
|
failed: [],
|
package/dist/runtime/worker.d.ts
CHANGED
|
@@ -101,16 +101,51 @@ export type DurableWorkerDeps = {
|
|
|
101
101
|
* lost message costs latency rather than a stuck flow. A hook whose delivery was load-bearing would need a
|
|
102
102
|
* queue, not a callback.
|
|
103
103
|
*/
|
|
104
|
+
/**
|
|
105
|
+
* The tenant's concurrent-run limit, or undefined for unlimited — REQ-058 (#246), task #265.
|
|
106
|
+
*
|
|
107
|
+
* A function rather than a value, for the reason `RateLimitGuardDeps.policyFor` gives: limits are per tenant
|
|
108
|
+
* and change without a redeploy, and a value captured at construction would be the limits of whoever booted
|
|
109
|
+
* the process.
|
|
110
|
+
*
|
|
111
|
+
* **Absent or non-positive means unlimited**, which is the branch that matters on the day this ships. A
|
|
112
|
+
* deployment upgrading into the feature with nothing configured must keep working; the alternative is an
|
|
113
|
+
* outage caused by adding a safety feature, which is how safety features get removed.
|
|
114
|
+
*/
|
|
115
|
+
readonly concurrencyFor?: (tenantId: Run["tenantId"]) => Promise<number | undefined> | number | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* How long a deferred run waits before its next attempt. Default one second.
|
|
118
|
+
*
|
|
119
|
+
* Not zero, and not the lease length. Zero spins a worker against the cap, burning capacity on the check
|
|
120
|
+
* itself; a full lease is longer than most runs, so a freed slot would sit idle. A second is short enough
|
|
121
|
+
* that a slot is picked up promptly and long enough that the retry is not the load.
|
|
122
|
+
*/
|
|
123
|
+
readonly deferralBackoffMs?: number;
|
|
104
124
|
readonly onRunSettled?: (input: {
|
|
105
125
|
readonly context: ExecutionContext;
|
|
106
126
|
readonly run: Run;
|
|
107
127
|
readonly outcome: "completed" | "failed" | "cancelled";
|
|
108
128
|
}) => Promise<void> | void;
|
|
109
129
|
};
|
|
110
|
-
|
|
130
|
+
/**
|
|
131
|
+
* `deferred` is distinct from `skipped`, and conflating them would strand work — #265.
|
|
132
|
+
*
|
|
133
|
+
* `skipped` means *somebody else has this, or it is already finished* — there is nothing to do and the job is
|
|
134
|
+
* done with. `deferred` means *this tenant is at its concurrency limit right now*: the run is still queued, it
|
|
135
|
+
* still needs to run, and a caller that acknowledged it the way it acknowledges a `skipped` job would drop it
|
|
136
|
+
* for ever. A `deferred` result must be re-enqueued, and `ProcessResult.retryAfterMs` says when.
|
|
137
|
+
*/
|
|
138
|
+
export type ProcessOutcome = "completed" | "failed" | "cancelled" | "skipped" | "lost" | "paused" | "deferred";
|
|
111
139
|
export type ProcessResult = {
|
|
112
140
|
readonly run: Run | null;
|
|
113
141
|
readonly outcome: ProcessOutcome;
|
|
142
|
+
/**
|
|
143
|
+
* How long to wait before trying this run again. Set only for `deferred`.
|
|
144
|
+
*
|
|
145
|
+
* Without it a deferred run is re-enqueued immediately and spins against the cap, burning a worker slot on
|
|
146
|
+
* the check itself — which would make a concurrency limit cost more capacity than it saves.
|
|
147
|
+
*/
|
|
148
|
+
readonly retryAfterMs?: number;
|
|
114
149
|
};
|
|
115
150
|
export declare const createDurableWorker: (deps: DurableWorkerDeps) => {
|
|
116
151
|
/** Claim (under an optional lock) and drive a run. Idempotent: a claimed/terminal run is skipped. */
|
package/dist/runtime/worker.js
CHANGED
|
@@ -37,6 +37,7 @@ export const createDurableWorker = (deps) => {
|
|
|
37
37
|
const clock = deps.clock ?? (() => new Date(now()).toISOString());
|
|
38
38
|
const leaseMs = deps.leaseMs ?? 30_000;
|
|
39
39
|
const keepaliveEveryMs = deps.keepaliveEveryMs ?? Math.max(1, Math.floor(leaseMs / 3));
|
|
40
|
+
const deferralBackoffMs = Math.max(1, deps.deferralBackoffMs ?? 1_000);
|
|
40
41
|
const channelFor = deps.channelFor ?? ((r) => `conversation:${r.conversationId}`);
|
|
41
42
|
const { runs, checkpoints, publisher, engine, workerId } = deps;
|
|
42
43
|
/** Drive one already-claimed run to a terminal state. */
|
|
@@ -114,6 +115,7 @@ export const createDurableWorker = (deps) => {
|
|
|
114
115
|
inputTokens: event.inputTokens,
|
|
115
116
|
outputTokens: event.outputTokens,
|
|
116
117
|
cachedInputTokens: event.cachedInputTokens ?? 0,
|
|
118
|
+
...(event.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: event.cacheWriteTokens }),
|
|
117
119
|
...(event.reasoningTokens === undefined ? {} : { reasoningTokens: event.reasoningTokens }),
|
|
118
120
|
...(event.imageCount === undefined ? {} : { imageCount: event.imageCount }),
|
|
119
121
|
...(event.audioSeconds === undefined ? {} : { audioSeconds: event.audioSeconds }),
|
|
@@ -322,7 +324,13 @@ export const createDurableWorker = (deps) => {
|
|
|
322
324
|
pause = "waiting-for-question";
|
|
323
325
|
else if (body.type === "approval.requested")
|
|
324
326
|
pause = "waiting-for-approval";
|
|
325
|
-
|
|
327
|
+
// The third pause — #264. Same shape as the other two: an event the engine emits, a status the worker
|
|
328
|
+
// parks in, and a resume back to `queued`.
|
|
329
|
+
else if (body.type === "connection.requested")
|
|
330
|
+
pause = "waiting-for-connection";
|
|
331
|
+
else if (body.type === "question.answered" ||
|
|
332
|
+
body.type === "approval.decided" ||
|
|
333
|
+
body.type === "connection.completed")
|
|
326
334
|
pause = null;
|
|
327
335
|
await heartbeat(); // throttled; runs on every event so a tool-heavy run keeps its lease alive
|
|
328
336
|
if (cancelRequested)
|
|
@@ -382,9 +390,34 @@ export const createDurableWorker = (deps) => {
|
|
|
382
390
|
if (!lock)
|
|
383
391
|
return { run: await runs.findById({ tenantId, id: runId }), outcome: "skipped" };
|
|
384
392
|
try {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
393
|
+
const maxConcurrent = await deps.concurrencyFor?.(tenantId);
|
|
394
|
+
const now = clock();
|
|
395
|
+
const claimed = await runs.claim({
|
|
396
|
+
tenantId,
|
|
397
|
+
id: runId,
|
|
398
|
+
workerId,
|
|
399
|
+
leaseMs,
|
|
400
|
+
now,
|
|
401
|
+
...(maxConcurrent === undefined ? {} : { maxConcurrent }),
|
|
402
|
+
});
|
|
403
|
+
if (!claimed) {
|
|
404
|
+
const run = await runs.findById({ tenantId, id: runId });
|
|
405
|
+
/**
|
|
406
|
+
* Why the claim failed, diagnosed *after* the fact — #265.
|
|
407
|
+
*
|
|
408
|
+
* The decision was already made atomically inside `claim`; this only chooses which outcome to report,
|
|
409
|
+
* so a slightly stale count costs a wrong label rather than a wrong admission. The run being still
|
|
410
|
+
* `queued` is the discriminator: a run somebody else is driving is `running`, and a finished one is
|
|
411
|
+
* terminal, so `queued` after a refused claim means the cap is what stopped it.
|
|
412
|
+
*/
|
|
413
|
+
if (maxConcurrent !== undefined && maxConcurrent > 0 && run?.status === "queued") {
|
|
414
|
+
const live = await runs.countLive({ tenantId, now });
|
|
415
|
+
if (live >= maxConcurrent) {
|
|
416
|
+
return { run, outcome: "deferred", retryAfterMs: deferralBackoffMs };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { run, outcome: "skipped" };
|
|
420
|
+
}
|
|
388
421
|
return await drive(claimed);
|
|
389
422
|
}
|
|
390
423
|
finally {
|
|
@@ -190,6 +190,52 @@ export const SECURITY_CHECKS = [
|
|
|
190
190
|
* forgotten case.
|
|
191
191
|
*/
|
|
192
192
|
export const CREDENTIAL_FIELD_EXEMPTIONS = [
|
|
193
|
+
{
|
|
194
|
+
file: "connections/oauth/client.ts",
|
|
195
|
+
reason: "`ResolvedOAuthClient.clientSecret` and `registerTenantOAuthApp`'s parameter are a *tenant's own* OAuth " +
|
|
196
|
+
"client secret, which is exactly as much a secret as the deployment's above and is treated more " +
|
|
197
|
+
"carefully, not less: it is sealed by the same `SecretCipher` as every access token before it reaches " +
|
|
198
|
+
"the database, and the stored row holds ciphertext — asserted by the same raw-row test that covers " +
|
|
199
|
+
"connections. It exists in memory only between being opened for one flow and being handed to the token " +
|
|
200
|
+
"request, is never written to a table in plaintext, never placed in a message part, a result envelope or " +
|
|
201
|
+
"an audit row, and never put in an authorization URL. The readable half of a registration — client id, " +
|
|
202
|
+
"redirect URIs, scopes — is deliberately *not* sealed, so a settings screen renders without a key.",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
file: "connections/oauth/index.ts",
|
|
206
|
+
reason: "`OAuthProviderConfig.clientSecret` is the *deployment's own* OAuth client secret, supplied by the host " +
|
|
207
|
+
"at wiring time — the same shape and the same limits as the model provider's key above. It goes into one " +
|
|
208
|
+
"token-request body over the back channel and is never put in an authorization URL, because a secret in " +
|
|
209
|
+
"a URL is in the browser history, the referrer header and every proxy log between. `TokenResponse` " +
|
|
210
|
+
"carries `accessToken`/`refreshToken` for the moments between the exchange and sealing them: the " +
|
|
211
|
+
"connection service passes them straight to `SecretCipher.seal`, and what reaches the database is " +
|
|
212
|
+
"ciphertext. Neither is written to a table, placed in a message part or a result envelope, and the " +
|
|
213
|
+
"provider's own error body is deliberately not echoed because it routinely quotes the request.",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
file: "tools/credentials.ts",
|
|
217
|
+
reason: "The `Credential` union is the *resolved* value — the one place a third-party secret legitimately exists, " +
|
|
218
|
+
"for the duration of one call. `password` and the other secret-bearing fields are named honestly rather " +
|
|
219
|
+
"than hidden behind a generic `value`, because a `basic` credential genuinely has two parts and calling " +
|
|
220
|
+
"them `a` and `b` would make every call site guess. This module is what makes this the *only* such place: " +
|
|
221
|
+
"it is why a tool takes a `credentialRef` instead of a secret, and why nothing is stored on the tool. " +
|
|
222
|
+
"The exemption comes with compensating controls rather than on its own — #260 defines the secret fields " +
|
|
223
|
+
"**non-enumerably** and overrides `toJSON`, `toString` and `util.inspect`, so a spread, a " +
|
|
224
|
+
"`JSON.stringify` or a `console.log` of a credential yields `[credential redacted]` and never the secret. " +
|
|
225
|
+
"It is still never written to a table, never placed in a message part, a result envelope or an audit " +
|
|
226
|
+
"row, and `CredentialAudit` receives the scheme and the ref, never the value.",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
file: "adapters/audio/openai.ts",
|
|
230
|
+
reason: "`OpenAiAudioConfig.apiKey` is the *deployment's own* provider key, supplied by the host at wiring time — " +
|
|
231
|
+
"the same shape, the same limits and the same reasoning as the embeddings adapter and the model provider " +
|
|
232
|
+
"factory. It reaches one `authorization` header on an operator-configured endpoint and nowhere else: it " +
|
|
233
|
+
"is never written to a table, never placed in a message part, a result envelope or an audit row, and the " +
|
|
234
|
+
"provider's error body is truncated rather than echoed whole. A `credentialRef` would be the wrong shape " +
|
|
235
|
+
"here for the reason it is wrong for the model provider — this is one deployment's key for its own " +
|
|
236
|
+
"provider account, not a per-tenant third-party grant, and routing it through the credential resolver " +
|
|
237
|
+
"would imply a per-tenant secret that does not exist.",
|
|
238
|
+
},
|
|
193
239
|
{
|
|
194
240
|
file: "models/provider-factory.ts",
|
|
195
241
|
reason: "`ProviderCredentials.apiKey` is the model provider's own key, supplied by the host at wiring time and " +
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `retinue` — the executable, task #252.
|
|
4
|
+
*
|
|
5
|
+
* `cli.ts` and `cli-worker.ts` have been here since #110 and the package had **no `bin`**, so a consumer had to
|
|
6
|
+
* write an entrypoint before anything ran. Applying the schema was the sharpest case:
|
|
7
|
+
* `adapters/postgres/migrations.ts` is a module, so provisioning a database meant writing a program first.
|
|
8
|
+
*
|
|
9
|
+
* ## What this does and does not invent
|
|
10
|
+
*
|
|
11
|
+
* It does not invent a deployment's wiring. `serve` and `worker` load `RETINUE_APP_MODULE` exactly as they did,
|
|
12
|
+
* and refuse to start without it — a permissive default would serve an open API to anyone who forgot to set it,
|
|
13
|
+
* which is the rule `cli.ts` established and this keeps.
|
|
14
|
+
*
|
|
15
|
+
* `migrate` and `doctor` deliberately need **no** app module: a database is provisioned before an application
|
|
16
|
+
* exists, and a diagnostic that cannot run until everything else is configured is a diagnostic nobody can use.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The advisory-lock key comes from `schema.ts` — #252's AC-2, and #266's AC-4.
|
|
20
|
+
*
|
|
21
|
+
* It used to be defined here. That made "the CLI and `auto` mode use the same key" a property of two constants
|
|
22
|
+
* happening to be equal, and a copy that drifted would produce two locks, no serialisation, and the original
|
|
23
|
+
* crash returning with the fix apparently in place. Two constants that must be equal are one constant.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Create the configured schema if it is not there, before anything tries to use it.
|
|
27
|
+
*
|
|
28
|
+
* `migrate` is the command that owns provisioning, so the namespace it was told to provision into is
|
|
29
|
+
* its job too — and the reason is worse than a missing-schema error. **Postgres does not error.**
|
|
30
|
+
* `SET search_path TO retinue, public` succeeds when `retinue` does not exist: a missing entry is
|
|
31
|
+
* skipped, not rejected. `CREATE TABLE conversations` then lands in the first schema that *does*
|
|
32
|
+
* exist, which is `public` — so all 34 platform migrations would silently be created alongside the
|
|
33
|
+
* product's tables, reporting success the whole way. Verified against a real Postgres 17: the SET
|
|
34
|
+
* returns, the CREATE returns, and `information_schema` says `public`.
|
|
35
|
+
*
|
|
36
|
+
* That is the failure this function exists to prevent, and it is why it runs before the pool rather
|
|
37
|
+
* than relying on a connection error that never comes.
|
|
38
|
+
*
|
|
39
|
+
* On its own connection with the **default** search path, which is the part that is easy to get wrong.
|
|
40
|
+
* A connection configured for `retinue` cannot be the one that creates `retinue` — `openPostgres`
|
|
41
|
+
* destroys it during setup, before a statement of ours runs.
|
|
42
|
+
*
|
|
43
|
+
* `IF NOT EXISTS` and nothing else: no owner, no grants, no drop. Provisioning a namespace is additive;
|
|
44
|
+
* deciding who may use it is a deployment's decision and not a migration's.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Which `migrate` invocations must change nothing.
|
|
48
|
+
*
|
|
49
|
+
* Its own function because it is the link between a flag and a side effect, and that link is invisible
|
|
50
|
+
* to a test of either end: sabotaging it to `false` — so `--dry-run` provisions a schema — broke
|
|
51
|
+
* nothing, while every assertion about `ensureSchema` itself stayed green. The list is also the same
|
|
52
|
+
* one the read-only branch below uses, so a third flag added to one and not the other cannot silently
|
|
53
|
+
* become a writing dry run.
|
|
54
|
+
*/
|
|
55
|
+
export declare const READ_ONLY_FLAGS: readonly ["--status", "--dry-run"];
|
|
56
|
+
export declare const isReadOnly: (flags: ReadonlySet<string>) => boolean;
|
|
57
|
+
export declare const ensureSchema: (config: {
|
|
58
|
+
readonly databaseUrl: string;
|
|
59
|
+
readonly databaseSchema?: string;
|
|
60
|
+
}, { create, connect, }: {
|
|
61
|
+
readonly create: boolean;
|
|
62
|
+
readonly connect?: (settings: {
|
|
63
|
+
readonly databaseUrl: string;
|
|
64
|
+
}) => Promise<{
|
|
65
|
+
readonly sql: {
|
|
66
|
+
query<Row>(text: string, params?: readonly unknown[]): Promise<Row[]>;
|
|
67
|
+
};
|
|
68
|
+
readonly end: () => Promise<void>;
|
|
69
|
+
}>;
|
|
70
|
+
}) => Promise<boolean>;
|
|
71
|
+
export declare const main: (argv: readonly string[], env?: NodeJS.ProcessEnv) => Promise<number>;
|
|
72
|
+
//# sourceMappingURL=bin.d.ts.map
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `retinue` — the executable, task #252.
|
|
4
|
+
*
|
|
5
|
+
* `cli.ts` and `cli-worker.ts` have been here since #110 and the package had **no `bin`**, so a consumer had to
|
|
6
|
+
* write an entrypoint before anything ran. Applying the schema was the sharpest case:
|
|
7
|
+
* `adapters/postgres/migrations.ts` is a module, so provisioning a database meant writing a program first.
|
|
8
|
+
*
|
|
9
|
+
* ## What this does and does not invent
|
|
10
|
+
*
|
|
11
|
+
* It does not invent a deployment's wiring. `serve` and `worker` load `RETINUE_APP_MODULE` exactly as they did,
|
|
12
|
+
* and refuse to start without it — a permissive default would serve an open API to anyone who forgot to set it,
|
|
13
|
+
* which is the rule `cli.ts` established and this keeps.
|
|
14
|
+
*
|
|
15
|
+
* `migrate` and `doctor` deliberately need **no** app module: a database is provisioned before an application
|
|
16
|
+
* exists, and a diagnostic that cannot run until everything else is configured is a diagnostic nobody can use.
|
|
17
|
+
*/
|
|
18
|
+
import { runApiHost } from "./cli.js";
|
|
19
|
+
import { runWorker } from "./cli-worker.js";
|
|
20
|
+
import { loadConfig } from "./config.js";
|
|
21
|
+
import { report, runChecks } from "./doctor.js";
|
|
22
|
+
const USAGE = `retinue <command>
|
|
23
|
+
|
|
24
|
+
migrate Apply pending migrations to RETINUE_DATABASE_URL.
|
|
25
|
+
--status Report applied and pending migrations; change nothing.
|
|
26
|
+
--dry-run Print the statements that would run; change nothing.
|
|
27
|
+
serve Start the API host. Needs RETINUE_APP_MODULE.
|
|
28
|
+
worker Start a run worker. Needs RETINUE_APP_MODULE.
|
|
29
|
+
doctor Check configuration, database, schema and Redis. Reports every failure.
|
|
30
|
+
|
|
31
|
+
Configuration comes from the environment; see .env.example.`;
|
|
32
|
+
/**
|
|
33
|
+
* Opened lazily and per command, so `doctor` and `migrate` never load a driver they do not use.
|
|
34
|
+
*
|
|
35
|
+
* The pool itself comes from `pool.ts`, which is also what the API host and the worker use — the schema
|
|
36
|
+
* a deployment configures has to be the same one migrations run against, and that is only true while
|
|
37
|
+
* there is one place that decides it.
|
|
38
|
+
*
|
|
39
|
+
* A connect timeout, because the default is *none*: `doctor` against a refused port sat silently instead
|
|
40
|
+
* of reporting the failure it exists to report. Short, since every command here either connects
|
|
41
|
+
* immediately or is misconfigured.
|
|
42
|
+
*/
|
|
43
|
+
const postgres = async (config) => {
|
|
44
|
+
const { openPostgres } = await import("./pool.js");
|
|
45
|
+
return openPostgres({ ...config, connectionTimeoutMillis: 5_000 });
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The advisory-lock key comes from `schema.ts` — #252's AC-2, and #266's AC-4.
|
|
49
|
+
*
|
|
50
|
+
* It used to be defined here. That made "the CLI and `auto` mode use the same key" a property of two constants
|
|
51
|
+
* happening to be equal, and a copy that drifted would produce two locks, no serialisation, and the original
|
|
52
|
+
* crash returning with the fix apparently in place. Two constants that must be equal are one constant.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* Create the configured schema if it is not there, before anything tries to use it.
|
|
56
|
+
*
|
|
57
|
+
* `migrate` is the command that owns provisioning, so the namespace it was told to provision into is
|
|
58
|
+
* its job too — and the reason is worse than a missing-schema error. **Postgres does not error.**
|
|
59
|
+
* `SET search_path TO retinue, public` succeeds when `retinue` does not exist: a missing entry is
|
|
60
|
+
* skipped, not rejected. `CREATE TABLE conversations` then lands in the first schema that *does*
|
|
61
|
+
* exist, which is `public` — so all 34 platform migrations would silently be created alongside the
|
|
62
|
+
* product's tables, reporting success the whole way. Verified against a real Postgres 17: the SET
|
|
63
|
+
* returns, the CREATE returns, and `information_schema` says `public`.
|
|
64
|
+
*
|
|
65
|
+
* That is the failure this function exists to prevent, and it is why it runs before the pool rather
|
|
66
|
+
* than relying on a connection error that never comes.
|
|
67
|
+
*
|
|
68
|
+
* On its own connection with the **default** search path, which is the part that is easy to get wrong.
|
|
69
|
+
* A connection configured for `retinue` cannot be the one that creates `retinue` — `openPostgres`
|
|
70
|
+
* destroys it during setup, before a statement of ours runs.
|
|
71
|
+
*
|
|
72
|
+
* `IF NOT EXISTS` and nothing else: no owner, no grants, no drop. Provisioning a namespace is additive;
|
|
73
|
+
* deciding who may use it is a deployment's decision and not a migration's.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* Which `migrate` invocations must change nothing.
|
|
77
|
+
*
|
|
78
|
+
* Its own function because it is the link between a flag and a side effect, and that link is invisible
|
|
79
|
+
* to a test of either end: sabotaging it to `false` — so `--dry-run` provisions a schema — broke
|
|
80
|
+
* nothing, while every assertion about `ensureSchema` itself stayed green. The list is also the same
|
|
81
|
+
* one the read-only branch below uses, so a third flag added to one and not the other cannot silently
|
|
82
|
+
* become a writing dry run.
|
|
83
|
+
*/
|
|
84
|
+
export const READ_ONLY_FLAGS = ["--status", "--dry-run"];
|
|
85
|
+
export const isReadOnly = (flags) => READ_ONLY_FLAGS.some((flag) => flags.has(flag));
|
|
86
|
+
export const ensureSchema = async (config, { create,
|
|
87
|
+
// Injectable so a test can run this against a real Postgres — PGlite — without a live server. The
|
|
88
|
+
// default is the same `postgres` every command here uses.
|
|
89
|
+
connect = (settings) => postgres(settings), }) => {
|
|
90
|
+
if (config.databaseSchema === undefined)
|
|
91
|
+
return true;
|
|
92
|
+
const { sql, end } = await connect({ databaseUrl: config.databaseUrl });
|
|
93
|
+
try {
|
|
94
|
+
if (create) {
|
|
95
|
+
// Validated as an unquoted identifier by `loadConfig` — the only reason this concatenation is safe,
|
|
96
|
+
// and the reason that check refuses anything Postgres would need quoted.
|
|
97
|
+
await sql.query(`CREATE SCHEMA IF NOT EXISTS ${config.databaseSchema}`);
|
|
98
|
+
console.log(`schema: ${config.databaseSchema} ready`);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The read-only paths report instead of creating, and that is not fussiness.
|
|
103
|
+
*
|
|
104
|
+
* `--dry-run` and `--status` are documented a few lines below as side-effect free — the note says
|
|
105
|
+
* `public` has 0 tables after a dry run against a fresh database — and a reader deciding whether to
|
|
106
|
+
* trust a dry run is exactly the reader who must not find it provisioned a schema. Creating one is
|
|
107
|
+
* a small side effect and a large broken promise.
|
|
108
|
+
*/
|
|
109
|
+
const rows = await sql.query(`select exists (select 1 from pg_namespace where nspname = $1) as exists`, [config.databaseSchema]);
|
|
110
|
+
if (rows[0]?.exists === true)
|
|
111
|
+
return true;
|
|
112
|
+
console.error(`schema: ${config.databaseSchema} does not exist. Postgres will not complain — it skips a missing ` +
|
|
113
|
+
`entry in search_path — so tables would be created in the next schema on the path instead, ` +
|
|
114
|
+
`silently. Run \`retinue migrate\`, which creates it, or unset RETINUE_DATABASE_SCHEMA to use the ` +
|
|
115
|
+
`connection's own schema deliberately.`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await end();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const migrate = async (flags, env = process.env) => {
|
|
123
|
+
const config = loadConfig(env);
|
|
124
|
+
// Before the pool, because a missing schema does not fail: `SET search_path` skips an entry that does
|
|
125
|
+
// not exist and every CREATE lands in `public` instead, silently. The read-only flags report rather
|
|
126
|
+
// than create — see `ensureSchema`.
|
|
127
|
+
if (!(await ensureSchema(config, { create: !isReadOnly(flags) })))
|
|
128
|
+
return 1;
|
|
129
|
+
const { sql, open, end } = await postgres(config);
|
|
130
|
+
try {
|
|
131
|
+
const { createSchemaManager, MIGRATION_LOCK } = await import("../entries/adapters-postgres.js");
|
|
132
|
+
if (isReadOnly(flags)) {
|
|
133
|
+
// Read-only paths take no lock. `plan()` and `currentVersion()` are documented as side-effect free — only
|
|
134
|
+
// `apply()` creates the ledger table — which is what makes a dry run honest rather than a dry run that
|
|
135
|
+
// provisions one table. Verified: after `--dry-run` against a fresh database, `public` has 0 tables.
|
|
136
|
+
const manager = createSchemaManager(sql);
|
|
137
|
+
const pending = await manager.plan();
|
|
138
|
+
const current = await manager.currentVersion();
|
|
139
|
+
const target = manager.targetVersion();
|
|
140
|
+
if (flags.has("--status")) {
|
|
141
|
+
console.log(`schema: ${current} of ${target} migrations applied`);
|
|
142
|
+
for (const change of pending)
|
|
143
|
+
console.log(` pending ${change.id} (${change.statements.length} statement(s))`);
|
|
144
|
+
if (pending.length === 0)
|
|
145
|
+
console.log(" nothing pending");
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
console.log(`schema: ${current} of ${target} applied; ${pending.length} pending`);
|
|
149
|
+
for (const change of pending) {
|
|
150
|
+
console.log(`\n-- ${change.id}`);
|
|
151
|
+
for (const statement of change.statements)
|
|
152
|
+
console.log(`${statement};`);
|
|
153
|
+
}
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Applied under a session advisory lock, on **one** connection — AC-2.
|
|
158
|
+
*
|
|
159
|
+
* Measured before this existed: two concurrent `retinue migrate` runs against one database left the ledger
|
|
160
|
+
* correct (30 rows, 30 distinct) and **crashed one process** with
|
|
161
|
+
* `duplicate key value violates unique constraint "pg_type_typname_nsp_index"` — Postgres's own type
|
|
162
|
+
* catalogue, racing on DDL. The data was safe and the operator experience was not: running migrate from two
|
|
163
|
+
* pods, which is the ordinary Kubernetes init-container pattern, gives one crash loop and an error that
|
|
164
|
+
* names nothing an operator can act on.
|
|
165
|
+
*
|
|
166
|
+
* `createPoolOpener` rather than `createPgExecutor`, because `pool.query` picks a different connection per
|
|
167
|
+
* call — so a `pg_advisory_lock` taken through it would be held by a connection we might never get back, and
|
|
168
|
+
* the unlock would land elsewhere. One checked-out client makes the pair correct, and the lock is released
|
|
169
|
+
* by the session ending if the process dies mid-migration.
|
|
170
|
+
*/
|
|
171
|
+
return await open(async (locked) => {
|
|
172
|
+
await locked.query("select pg_advisory_lock($1)", [MIGRATION_LOCK]);
|
|
173
|
+
try {
|
|
174
|
+
const manager = createSchemaManager(locked);
|
|
175
|
+
const pending = await manager.plan();
|
|
176
|
+
const target = manager.targetVersion();
|
|
177
|
+
if (pending.length === 0) {
|
|
178
|
+
console.log(`schema already at ${target}; nothing to apply`);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
await manager.apply();
|
|
182
|
+
console.log(`applied ${pending.length} migration(s); schema now at ${await manager.currentVersion()}`);
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await locked.query("select pg_advisory_unlock($1)", [MIGRATION_LOCK]);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await end();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const doctor = async (env = process.env) => {
|
|
195
|
+
const results = await runChecks({
|
|
196
|
+
env,
|
|
197
|
+
connectPostgres: async (settings) => {
|
|
198
|
+
const { sql, end } = await postgres(settings);
|
|
199
|
+
return { query: (text, params) => sql.query(text, params), end };
|
|
200
|
+
},
|
|
201
|
+
connectRedis: async (url) => {
|
|
202
|
+
const { Redis } = await import("ioredis");
|
|
203
|
+
/**
|
|
204
|
+
* Fail fast, on every axis ioredis has one.
|
|
205
|
+
*
|
|
206
|
+
* `maxRetriesPerRequest` alone was not enough: it bounds *command* retries, and a refused **connection**
|
|
207
|
+
* is retried for ever by the default `retryStrategy`. So `doctor` against a closed port hung rather than
|
|
208
|
+
* reporting it. `retryStrategy: () => null` stops reconnecting, `enableOfflineQueue: false` makes a
|
|
209
|
+
* command fail immediately instead of queueing for a connection that will never come, and `connectTimeout`
|
|
210
|
+
* bounds the first attempt.
|
|
211
|
+
*/
|
|
212
|
+
const redis = new Redis(url, {
|
|
213
|
+
maxRetriesPerRequest: 1,
|
|
214
|
+
connectTimeout: 3_000,
|
|
215
|
+
// No reconnection: a refused connection must fail, not be retried behind a spinner.
|
|
216
|
+
retryStrategy: () => null,
|
|
217
|
+
/**
|
|
218
|
+
* The offline queue stays **on**, and that is a correction.
|
|
219
|
+
*
|
|
220
|
+
* Turning it off made the check fail against a *working* Redis: `lazyConnect: false` starts connecting
|
|
221
|
+
* but `ping()` is called before the socket is ready, and with no offline queue the command is rejected
|
|
222
|
+
* immediately with "Stream isn't writeable". So the queue is what lets the first command wait for the
|
|
223
|
+
* connection; `retryStrategy` and `connectTimeout` are what stop it waiting for ever, and `withTimeout`
|
|
224
|
+
* is the backstop.
|
|
225
|
+
*/
|
|
226
|
+
lazyConnect: false,
|
|
227
|
+
});
|
|
228
|
+
// Otherwise an unreachable Redis emits an unhandled 'error' and takes the process down before the report.
|
|
229
|
+
redis.on("error", () => undefined);
|
|
230
|
+
return {
|
|
231
|
+
ping: () => redis.ping(),
|
|
232
|
+
/**
|
|
233
|
+
* `disconnect()`, not `quit()`, and wrapped so it is always a promise.
|
|
234
|
+
*
|
|
235
|
+
* `quit()` sends a QUIT command, which needs a working connection — so on the failure path it hangs or
|
|
236
|
+
* rejects, which is the path where closing matters most. `disconnect()` drops the socket unilaterally.
|
|
237
|
+
* It returns `void`, and returning that raw produced `Cannot read properties of undefined (reading
|
|
238
|
+
* 'catch')` in the caller's cleanup.
|
|
239
|
+
*/
|
|
240
|
+
quit: async () => {
|
|
241
|
+
redis.disconnect();
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
schemaVersions: async (sql) => {
|
|
246
|
+
const { createSchemaManager, MIGRATION_LOCK } = await import("../entries/adapters-postgres.js");
|
|
247
|
+
const manager = createSchemaManager(sql);
|
|
248
|
+
return { current: await manager.currentVersion(), target: manager.targetVersion() };
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
return report(results);
|
|
252
|
+
};
|
|
253
|
+
export const main = async (argv, env = process.env) => {
|
|
254
|
+
const [command, ...rest] = argv;
|
|
255
|
+
const flags = new Set(rest.filter((a) => a.startsWith("--")));
|
|
256
|
+
switch (command) {
|
|
257
|
+
case "migrate":
|
|
258
|
+
return migrate(flags, env);
|
|
259
|
+
case "doctor":
|
|
260
|
+
return doctor(env);
|
|
261
|
+
case "serve":
|
|
262
|
+
await runApiHost(env);
|
|
263
|
+
// Resolves once listening; the process stays alive on the server's own handles.
|
|
264
|
+
return 0;
|
|
265
|
+
case "worker":
|
|
266
|
+
await runWorker(env);
|
|
267
|
+
return 0;
|
|
268
|
+
case undefined:
|
|
269
|
+
case "--help":
|
|
270
|
+
case "-h":
|
|
271
|
+
case "help":
|
|
272
|
+
console.log(USAGE);
|
|
273
|
+
return command === undefined ? 1 : 0;
|
|
274
|
+
default:
|
|
275
|
+
console.error(`unknown command "${command}"\n\n${USAGE}`);
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
/** Commands that finish and must return the prompt. `serve` and `worker` deliberately do not. */
|
|
280
|
+
const ONE_SHOT = new Set(["migrate", "doctor", "help", "--help", "-h"]);
|
|
281
|
+
// Only when invoked as the binary, so importing this module for a test starts nothing.
|
|
282
|
+
if (process.argv[1] !== undefined && /(^|\/)(retinue|bin\.js)$/.test(process.argv[1])) {
|
|
283
|
+
const command = process.argv[2];
|
|
284
|
+
main(process.argv.slice(2))
|
|
285
|
+
.then((code) => {
|
|
286
|
+
/**
|
|
287
|
+
* A one-shot command **exits**, rather than setting `exitCode` and hoping the loop drains.
|
|
288
|
+
*
|
|
289
|
+
* `doctor` reported correctly and then hung: a driver that has been asked to stop reconnecting can still
|
|
290
|
+
* hold a socket handle, and one lingering handle keeps Node alive for ever. Closing every client is the
|
|
291
|
+
* fix and this is the guarantee — a diagnostic that never returns the prompt is a diagnostic nobody runs
|
|
292
|
+
* twice. `serve` and `worker` are excluded because their whole job is to stay up.
|
|
293
|
+
*/
|
|
294
|
+
if (command === undefined || ONE_SHOT.has(command))
|
|
295
|
+
process.exit(code);
|
|
296
|
+
if (code !== 0)
|
|
297
|
+
process.exitCode = code;
|
|
298
|
+
})
|
|
299
|
+
.catch((error) => {
|
|
300
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/server/boot.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* substance: configuration is validated *before* anything connects, so a misconfigured deployment
|
|
6
6
|
* fails with a message about the variable rather than a connection error that names nothing useful.
|
|
7
7
|
*/
|
|
8
|
-
import { type SchemaMode, type SqlExecutor } from "../entries/adapters-postgres.js";
|
|
8
|
+
import { type ConnectionOpener, type SchemaMode, type SqlExecutor } from "../entries/adapters-postgres.js";
|
|
9
|
+
import { type TransactionRunner } from "../adapters/postgres/transaction.js";
|
|
9
10
|
import { type RetinueConfig, type Env } from "./config.js";
|
|
10
11
|
export type StartupLog = {
|
|
11
12
|
readonly event: string;
|
|
@@ -13,16 +14,33 @@ export type StartupLog = {
|
|
|
13
14
|
};
|
|
14
15
|
export type BootOptions = {
|
|
15
16
|
readonly env: Env;
|
|
16
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Built after configuration validates, so a bad variable never opens a connection.
|
|
19
|
+
*
|
|
20
|
+
* `open` is optional and is what a `TransactionRunner` needs — the one primitive a pool-backed `SqlExecutor`
|
|
21
|
+
* cannot express, because `pool.query` picks a different connection per call. A caller that supplies it gets a
|
|
22
|
+
* runner in the result; one that does not gets `undefined`, and anything needing a transaction refuses by name.
|
|
23
|
+
*/
|
|
17
24
|
readonly connect: (config: RetinueConfig) => Promise<{
|
|
18
25
|
readonly sql: SqlExecutor;
|
|
26
|
+
readonly open?: ConnectionOpener;
|
|
19
27
|
}>;
|
|
20
28
|
readonly log?: (entry: StartupLog) => void;
|
|
21
29
|
readonly version?: string;
|
|
22
30
|
};
|
|
23
31
|
export type BootResult = {
|
|
24
32
|
readonly config: RetinueConfig;
|
|
33
|
+
/**
|
|
34
|
+
* The executor an application should build its stores over.
|
|
35
|
+
*
|
|
36
|
+
* **Scoped when a runner exists**, so a store built on it joins the ambient transaction without knowing
|
|
37
|
+
* transactions exist. Handing back the unscoped executor alongside a runner is the trap
|
|
38
|
+
* `transaction.ts` warns about: "a store built over a *non*-scoped executor silently escapes the
|
|
39
|
+
* transaction rather than failing".
|
|
40
|
+
*/
|
|
25
41
|
readonly sql: SqlExecutor;
|
|
42
|
+
/** Present when `connect` supplied an `open`. `undefined` for a process that needs no transactions. */
|
|
43
|
+
readonly runner?: TransactionRunner;
|
|
26
44
|
readonly schema: {
|
|
27
45
|
readonly mode: SchemaMode;
|
|
28
46
|
readonly applied: readonly string[];
|