@sema-agent/server 1.314.0 → 1.316.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/approval-hmac.d.ts +17 -0
- package/dist/approval-hmac.js +27 -0
- package/dist/config-center/apply-effective.d.ts +22 -0
- package/dist/config-center/apply-effective.js +283 -0
- package/dist/config-center/http-client.d.ts +23 -0
- package/dist/config-center/http-client.js +109 -0
- package/dist/config-center/restart-signal.d.ts +12 -0
- package/dist/config-center/restart-signal.js +70 -0
- package/dist/config-center/skills-mcp.d.ts +13 -0
- package/dist/config-center/skills-mcp.js +113 -0
- package/dist/config-center/types.d.ts +143 -0
- package/dist/config-center/types.js +2 -0
- package/dist/fleet/fleet-bus.js +92 -83
- package/dist/hooks/hook-runner.js +18 -9
- package/dist/http/idempotency.d.ts +1 -0
- package/dist/http/idempotency.js +3 -0
- package/dist/http/principal-gate.d.ts +12 -0
- package/dist/http/principal-gate.js +52 -0
- package/dist/http/route-ctx.d.ts +67 -0
- package/dist/http/route-ctx.js +2 -0
- package/dist/http/routes/attachments.d.ts +4 -0
- package/dist/http/routes/attachments.js +106 -0
- package/dist/http/routes/capabilities.d.ts +4 -0
- package/dist/http/routes/capabilities.js +111 -0
- package/dist/http/routes/images.d.ts +20 -0
- package/dist/http/routes/images.js +572 -0
- package/dist/http/routes/memory-policy.d.ts +4 -0
- package/dist/http/routes/memory-policy.js +105 -0
- package/dist/http/routes/observability.d.ts +4 -0
- package/dist/http/routes/observability.js +63 -0
- package/dist/http/routes/session-sync.d.ts +36 -0
- package/dist/http/routes/session-sync.js +513 -0
- package/dist/http/routes/sessions-list.d.ts +4 -0
- package/dist/http/routes/sessions-list.js +117 -0
- package/dist/http/routes/sessions.d.ts +17 -0
- package/dist/http/routes/sessions.js +746 -0
- package/dist/http/send.d.ts +6 -0
- package/dist/http/send.js +19 -0
- package/dist/http/server.d.ts +101 -76
- package/dist/http/server.js +129 -2410
- package/dist/http/sse-log.d.ts +25 -0
- package/dist/http/sse-log.js +59 -0
- package/dist/http/workspace-content.d.ts +3 -0
- package/dist/http/workspace-content.js +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.js +169 -165
- package/dist/main.js +462 -451
- package/dist/plugins/approval-store-sql.d.ts +52 -0
- package/dist/plugins/approval-store-sql.js +119 -0
- package/dist/plugins/breaker-state-sql.d.ts +29 -0
- package/dist/plugins/breaker-state-sql.js +104 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
- package/dist/plugins/checkpoint-store-sql.js +224 -0
- package/dist/plugins/file-snapshot-store-sql.d.ts +38 -0
- package/dist/plugins/file-snapshot-store-sql.js +210 -0
- package/dist/plugins/image-bake-store-sql.d.ts +53 -0
- package/dist/plugins/image-bake-store-sql.js +463 -0
- package/dist/plugins/image-index-sql.d.ts +33 -0
- package/dist/plugins/image-index-sql.js +328 -0
- package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
- package/dist/plugins/k8s-bg-scripts.js +129 -0
- package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
- package/dist/plugins/k8s-exec-protocol.js +87 -0
- package/dist/plugins/outcome-ledger-sql.d.ts +70 -0
- package/dist/plugins/outcome-ledger-sql.js +158 -0
- package/dist/plugins/pg-approval-store.d.ts +1 -27
- package/dist/plugins/pg-approval-store.js +1 -83
- package/dist/plugins/pg-breaker-state.d.ts +1 -19
- package/dist/plugins/pg-breaker-state.js +1 -88
- package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
- package/dist/plugins/pg-checkpoint-store.js +1 -189
- package/dist/plugins/pg-cost-quota.js +3 -143
- package/dist/plugins/pg-file-snapshot-store.d.ts +1 -27
- package/dist/plugins/pg-file-snapshot-store.js +1 -190
- package/dist/plugins/pg-image-bake.d.ts +1 -40
- package/dist/plugins/pg-image-bake.js +1 -420
- package/dist/plugins/pg-image-index.d.ts +1 -21
- package/dist/plugins/pg-image-index.js +1 -217
- package/dist/plugins/pg-outcome-ledger.d.ts +2 -22
- package/dist/plugins/pg-outcome-ledger.js +1 -104
- package/dist/plugins/pg-rate-limiter.d.ts +2 -32
- package/dist/plugins/pg-rate-limiter.js +4 -147
- package/dist/plugins/pg-resume-anchor-store.d.ts +1 -13
- package/dist/plugins/pg-resume-anchor-store.js +1 -22
- package/dist/plugins/pg-run-store.d.ts +1 -75
- package/dist/plugins/pg-run-store.js +1 -287
- package/dist/plugins/pg-session-policy-store.d.ts +1 -11
- package/dist/plugins/pg-session-policy-store.js +1 -90
- package/dist/plugins/pg-tool-result-store.d.ts +1 -15
- package/dist/plugins/pg-tool-result-store.js +1 -52
- package/dist/plugins/pg-workflow-journal-store.d.ts +1 -20
- package/dist/plugins/pg-workflow-journal-store.js +1 -34
- package/dist/plugins/pg-workflow-run-store.d.ts +1 -55
- package/dist/plugins/pg-workflow-run-store.js +1 -220
- package/dist/plugins/remote-env-k8s.d.ts +5 -38
- package/dist/plugins/remote-env-k8s.js +7 -213
- package/dist/plugins/resume-anchor-store-sql.d.ts +23 -0
- package/dist/plugins/resume-anchor-store-sql.js +38 -0
- package/dist/plugins/run-store-sql.d.ts +89 -0
- package/dist/plugins/run-store-sql.js +397 -0
- package/dist/plugins/session-policy-store-sql.d.ts +22 -0
- package/dist/plugins/session-policy-store-sql.js +109 -0
- package/dist/plugins/sql-driver.d.ts +25 -0
- package/dist/plugins/sql-driver.js +59 -0
- package/dist/plugins/tidb-approval-store.d.ts +1 -39
- package/dist/plugins/tidb-approval-store.js +1 -71
- package/dist/plugins/tidb-breaker-state.d.ts +1 -17
- package/dist/plugins/tidb-breaker-state.js +1 -74
- package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
- package/dist/plugins/tidb-checkpoint-store.js +1 -191
- package/dist/plugins/tidb-file-snapshot-store.d.ts +1 -27
- package/dist/plugins/tidb-file-snapshot-store.js +1 -192
- package/dist/plugins/tidb-image-bake.d.ts +1 -38
- package/dist/plugins/tidb-image-bake.js +1 -348
- package/dist/plugins/tidb-image-index.d.ts +1 -19
- package/dist/plugins/tidb-image-index.js +1 -149
- package/dist/plugins/tidb-outcome-ledger.d.ts +1 -57
- package/dist/plugins/tidb-outcome-ledger.js +1 -97
- package/dist/plugins/tidb-resume-anchor-store.d.ts +1 -13
- package/dist/plugins/tidb-resume-anchor-store.js +1 -22
- package/dist/plugins/tidb-run-store.d.ts +1 -75
- package/dist/plugins/tidb-run-store.js +1 -306
- package/dist/plugins/tidb-session-policy-store.d.ts +1 -11
- package/dist/plugins/tidb-session-policy-store.js +1 -86
- package/dist/plugins/tidb-tool-result-store.d.ts +1 -13
- package/dist/plugins/tidb-tool-result-store.js +1 -38
- package/dist/plugins/tidb-workflow-journal-store.d.ts +1 -20
- package/dist/plugins/tidb-workflow-journal-store.js +1 -34
- package/dist/plugins/tidb-workflow-run-store.d.ts +1 -61
- package/dist/plugins/tidb-workflow-run-store.js +1 -231
- package/dist/plugins/tool-result-store-sql.d.ts +25 -0
- package/dist/plugins/tool-result-store-sql.js +69 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +30 -0
- package/dist/plugins/workflow-journal-store-sql.js +53 -0
- package/dist/plugins/workflow-run-store-sql.d.ts +85 -0
- package/dist/plugins/workflow-run-store-sql.js +299 -0
- package/dist/plugins/write-behind-counter.d.ts +14 -3
- package/dist/plugins/write-behind-counter.js +39 -12
- package/dist/principal-jwt.d.ts +44 -0
- package/dist/principal-jwt.js +95 -0
- package/dist/security.d.ts +2 -58
- package/dist/security.js +3 -118
- package/dist/sema-registry.d.ts +5 -200
- package/dist/sema-registry.js +4 -567
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError } from "@sema-agent/core";
|
|
2
|
+
import { foldKeyFamily, MAX_PENDING_PER_SESSION, PURGE_FENCE_MS, SERVED_FENCE_MS, } from "../orchestration/workflow-completion-inbox.js";
|
|
3
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
4
|
+
export const MAX_RUN_BLOB_BYTES = 4 * 1024 * 1024;
|
|
5
|
+
const onWarnType = (msg, meta) => void [msg, meta];
|
|
6
|
+
export function slimOversizeRun(run, maxBytes) {
|
|
7
|
+
const MARK = "…[truncated: run blob exceeded the store cap]";
|
|
8
|
+
const cut = (s) => s.slice(0, 4000) + MARK;
|
|
9
|
+
const step1 = {
|
|
10
|
+
...run,
|
|
11
|
+
...(typeof run.result === "string" && run.result.length > 4000 ? { result: cut(run.result) } : {}),
|
|
12
|
+
...(typeof run.error === "string" && run.error.length > 4000 ? { error: cut(run.error) } : {}),
|
|
13
|
+
};
|
|
14
|
+
let blob = JSON.stringify(step1);
|
|
15
|
+
if (Buffer.byteLength(blob) <= maxBytes)
|
|
16
|
+
return blob;
|
|
17
|
+
const step2 = { ...step1, phases: [], agents: [], groups: [], error: step1.error ? String(step1.error).slice(0, 1000) + MARK : MARK };
|
|
18
|
+
blob = JSON.stringify(step2);
|
|
19
|
+
return Buffer.byteLength(blob) <= maxBytes ? blob : null;
|
|
20
|
+
}
|
|
21
|
+
export class SqlWorkflowRunStore {
|
|
22
|
+
db;
|
|
23
|
+
constructor(db) {
|
|
24
|
+
this.db = db;
|
|
25
|
+
}
|
|
26
|
+
q(tidb, pg) {
|
|
27
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
28
|
+
}
|
|
29
|
+
async put(id, run) {
|
|
30
|
+
const stored = { ...run, id, rev: run.rev ?? 0 };
|
|
31
|
+
try {
|
|
32
|
+
await this.db.query(this.q("INSERT INTO workflow_run (id, scope, status, run, rev, created_at, ended_at) VALUES (?,?,?,?,?,?,?)", "INSERT INTO workflow_run (id, scope, status, run, rev, created_at, ended_at) VALUES ($1,$2,$3,$4,$5,$6,$7)"), [id, run.scope, run.status, JSON.stringify(stored), stored.rev, run.createdAt, run.endedAt ?? null]);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
const isDupKey = this.db.dialect === "tidb" ? e.code === "ER_DUP_ENTRY" : e.code === "23505";
|
|
36
|
+
if (isDupKey) {
|
|
37
|
+
throw new WorkflowRunStoreError("workflow_run.already_exists", "workflow run id already exists");
|
|
38
|
+
}
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async get(id) {
|
|
43
|
+
const { rows } = await this.db.query(this.q("SELECT run, rev FROM workflow_run WHERE id = ?", "SELECT run, rev FROM workflow_run WHERE id = $1"), [id]);
|
|
44
|
+
const r = rows[0];
|
|
45
|
+
if (!r)
|
|
46
|
+
return null;
|
|
47
|
+
const run = JSON.parse(String(r.run));
|
|
48
|
+
run.rev = Number(r.rev);
|
|
49
|
+
return run;
|
|
50
|
+
}
|
|
51
|
+
async update(id, scope, run, expect) {
|
|
52
|
+
let blob = JSON.stringify({ ...run, id, scope });
|
|
53
|
+
if (Buffer.byteLength(blob) > MAX_RUN_BLOB_BYTES) {
|
|
54
|
+
const slim = slimOversizeRun({ ...run, id, scope }, MAX_RUN_BLOB_BYTES);
|
|
55
|
+
if (slim === null)
|
|
56
|
+
return false;
|
|
57
|
+
blob = slim;
|
|
58
|
+
}
|
|
59
|
+
const res = await this.db.query(this.q(`UPDATE workflow_run SET run = ?, rev = rev + 1, status = ?, ended_at = ? WHERE id = ? AND scope = ?${expect !== undefined ? " AND rev = ?" : ""}`, `UPDATE workflow_run SET run = $1, rev = rev + 1, status = $2, ended_at = $3 WHERE id = $4 AND scope = $5${expect !== undefined ? " AND rev = $6" : ""}`), expect !== undefined
|
|
60
|
+
? [blob, run.status, run.endedAt ?? null, id, scope, expect.rev]
|
|
61
|
+
: [blob, run.status, run.endedAt ?? null, id, scope]);
|
|
62
|
+
return res.affected === 1;
|
|
63
|
+
}
|
|
64
|
+
async listByScope(scope, opts) {
|
|
65
|
+
const params = [scope];
|
|
66
|
+
let where;
|
|
67
|
+
let limit = "";
|
|
68
|
+
if (this.db.dialect === "tidb") {
|
|
69
|
+
where = "scope = ?";
|
|
70
|
+
if (opts?.status !== undefined) {
|
|
71
|
+
where += " AND status = ?";
|
|
72
|
+
params.push(opts.status);
|
|
73
|
+
}
|
|
74
|
+
if (opts?.session !== undefined) {
|
|
75
|
+
where += " AND JSON_UNQUOTE(JSON_EXTRACT(run, '$.originatingSessionId')) = ?";
|
|
76
|
+
params.push(opts.session);
|
|
77
|
+
}
|
|
78
|
+
if (opts?.limit !== undefined) {
|
|
79
|
+
limit = " LIMIT ?";
|
|
80
|
+
params.push(Math.max(0, opts.limit));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
where = "scope = $1";
|
|
85
|
+
if (opts?.status !== undefined) {
|
|
86
|
+
params.push(opts.status);
|
|
87
|
+
where += ` AND status = $${params.length}`;
|
|
88
|
+
}
|
|
89
|
+
if (opts?.session !== undefined) {
|
|
90
|
+
params.push(opts.session);
|
|
91
|
+
where += ` AND run::json->>'originatingSessionId' = $${params.length}`;
|
|
92
|
+
}
|
|
93
|
+
if (opts?.limit !== undefined) {
|
|
94
|
+
params.push(Math.max(0, opts.limit));
|
|
95
|
+
limit = ` LIMIT $${params.length}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const { rows } = await this.db.query(`SELECT run, rev FROM workflow_run WHERE ${where} ORDER BY created_at DESC, id DESC${limit}`, params);
|
|
99
|
+
return rows.map((r) => {
|
|
100
|
+
const run = JSON.parse(String(r.run));
|
|
101
|
+
run.rev = Number(r.rev);
|
|
102
|
+
return summarizeWorkflowRun(run);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async reapAllScopes(now, opts) {
|
|
106
|
+
const { rows } = await this.db.query("SELECT DISTINCT scope FROM workflow_run");
|
|
107
|
+
let total = 0;
|
|
108
|
+
for (const r of rows)
|
|
109
|
+
total += await this.reap(String(r.scope), now, opts);
|
|
110
|
+
return total;
|
|
111
|
+
}
|
|
112
|
+
async reap(scope, now, opts) {
|
|
113
|
+
if (opts?.maxAgeMs === undefined && opts?.keep === undefined)
|
|
114
|
+
return 0;
|
|
115
|
+
const { rows } = await this.db.query(this.q("SELECT id, status, created_at, ended_at FROM workflow_run WHERE scope = ? ORDER BY created_at DESC, id DESC", "SELECT id, status, created_at, ended_at FROM workflow_run WHERE scope = $1 ORDER BY created_at DESC, id DESC"), [scope]);
|
|
116
|
+
const terminal = rows.filter((r) => isTerminalWorkflowStatus(String(r.status)));
|
|
117
|
+
const doomed = [];
|
|
118
|
+
for (let i = 0; i < terminal.length; i++) {
|
|
119
|
+
const r = terminal[i];
|
|
120
|
+
const anchor = r.ended_at != null ? Number(r.ended_at) : Number(r.created_at);
|
|
121
|
+
const tooOld = opts.maxAgeMs !== undefined && anchor < now - opts.maxAgeMs;
|
|
122
|
+
const overKeep = opts.keep !== undefined && i >= opts.keep;
|
|
123
|
+
if (tooOld || overKeep)
|
|
124
|
+
doomed.push(String(r.id));
|
|
125
|
+
}
|
|
126
|
+
if (doomed.length === 0)
|
|
127
|
+
return 0;
|
|
128
|
+
const res = await this.db.query(this.q(`DELETE FROM workflow_run WHERE scope = ? AND id IN (${doomed.map(() => "?").join(",")})`, "DELETE FROM workflow_run WHERE scope = $1 AND id = ANY($2::text[])"), this.db.dialect === "tidb" ? [scope, ...doomed] : [scope, doomed]);
|
|
129
|
+
return res.affected;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const KIND_SERVED = "served";
|
|
133
|
+
const KIND_PURGE = "purge";
|
|
134
|
+
export class SqlWorkflowCompletionInbox {
|
|
135
|
+
db;
|
|
136
|
+
onWarn;
|
|
137
|
+
constructor(db, onWarn) {
|
|
138
|
+
this.db = db;
|
|
139
|
+
this.onWarn = onWarn;
|
|
140
|
+
}
|
|
141
|
+
q(tidb, pg) {
|
|
142
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
143
|
+
}
|
|
144
|
+
async sweepFences(now) {
|
|
145
|
+
await this.db.query(this.q("DELETE FROM workflow_inbox_fence WHERE until_ms < ?", "DELETE FROM workflow_inbox_fence WHERE until_ms < $1"), [now]);
|
|
146
|
+
}
|
|
147
|
+
async enqueue(entry) {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
await this.sweepFences(now);
|
|
150
|
+
const served = await this.db.query(this.q("SELECT 1 FROM workflow_inbox_fence WHERE session_id = ? AND kind = ? AND run_id IN (?, ?) AND until_ms >= ?", "SELECT 1 FROM workflow_inbox_fence WHERE session_id = $1 AND kind = $2 AND run_id IN ($3, $4) AND until_ms >= $5"), [entry.sessionId, KIND_SERVED, entry.runId, foldKeyFamily(entry.runId), now]);
|
|
151
|
+
if (served.rows.length > 0) {
|
|
152
|
+
this.onWarn?.("workflow_completion_enqueue_after_poll_served", { sessionId: entry.sessionId, runId: entry.runId });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const purge = await this.db.query(this.q("SELECT owner, owner_known FROM workflow_inbox_fence WHERE session_id = ? AND kind = ? AND run_id = '' AND until_ms >= ?", "SELECT owner, owner_known FROM workflow_inbox_fence WHERE session_id = $1 AND kind = $2 AND run_id = '' AND until_ms >= $3"), [entry.sessionId, KIND_PURGE, now]);
|
|
156
|
+
const pf = purge.rows[0];
|
|
157
|
+
if (pf && (Number(pf.owner_known) === 0 || (pf.owner === null ? entry.owner === null : String(pf.owner) === entry.owner))) {
|
|
158
|
+
this.onWarn?.("workflow_completion_enqueue_after_purge", { sessionId: entry.sessionId, runId: entry.runId });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await this.db.query(this.q("REPLACE INTO workflow_completion_inbox (session_id, run_id, owner, status, summary, enqueued_at, kind, payload) VALUES (?,?,?,?,?,?,?,?)", "INSERT INTO workflow_completion_inbox (session_id, run_id, owner, status, summary, enqueued_at, kind, payload) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) " +
|
|
162
|
+
"ON CONFLICT (session_id, run_id) DO UPDATE SET seq = nextval(pg_get_serial_sequence('workflow_completion_inbox','seq')), " +
|
|
163
|
+
"owner = EXCLUDED.owner, status = EXCLUDED.status, summary = EXCLUDED.summary, enqueued_at = EXCLUDED.enqueued_at, kind = EXCLUDED.kind, payload = EXCLUDED.payload"), [entry.sessionId, entry.runId, entry.owner, entry.status, entry.summary, entry.enqueuedAt, entry.kind ?? null, entry.payload ?? null]);
|
|
164
|
+
const confirm = await this.db.query(this.q("SELECT kind, owner, owner_known FROM workflow_inbox_fence WHERE session_id = ? AND until_ms >= ? AND (run_id IN (?, ?) OR (kind = ? AND run_id = ''))", "SELECT kind, owner, owner_known FROM workflow_inbox_fence WHERE session_id = $1 AND until_ms >= $2 AND (run_id IN ($3, $4) OR (kind = $5 AND run_id = ''))"), [entry.sessionId, now, entry.runId, foldKeyFamily(entry.runId), KIND_PURGE]);
|
|
165
|
+
for (const f of confirm.rows) {
|
|
166
|
+
const isServed = String(f.kind) === KIND_SERVED;
|
|
167
|
+
const purgeFencesUs = !isServed && (Number(f.owner_known) === 0 || (f.owner === null ? entry.owner === null : String(f.owner) === entry.owner));
|
|
168
|
+
if (isServed || purgeFencesUs) {
|
|
169
|
+
await this.ack(entry.sessionId, entry.runId);
|
|
170
|
+
this.onWarn?.(isServed ? "workflow_completion_enqueue_after_poll_served" : "workflow_completion_enqueue_after_purge", {
|
|
171
|
+
sessionId: entry.sessionId,
|
|
172
|
+
runId: entry.runId,
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const over = await this.db.query(this.q("SELECT seq, run_id FROM workflow_completion_inbox WHERE session_id = ? ORDER BY seq DESC LIMIT 100000 OFFSET ?", "SELECT seq, run_id FROM workflow_completion_inbox WHERE session_id = $1 ORDER BY seq DESC OFFSET $2"), [entry.sessionId, MAX_PENDING_PER_SESSION]);
|
|
178
|
+
if (over.rows.length > 0) {
|
|
179
|
+
await this.db.query(this.q(`DELETE FROM workflow_completion_inbox WHERE session_id = ? AND seq IN (${over.rows.map(() => "?").join(",")})`, "DELETE FROM workflow_completion_inbox WHERE session_id = $1 AND seq = ANY($2::bigint[])"), this.db.dialect === "tidb" ? [entry.sessionId, ...over.rows.map((r) => r.seq)] : [entry.sessionId, over.rows.map((r) => r.seq)]);
|
|
180
|
+
for (const r of over.rows)
|
|
181
|
+
this.onWarn?.("workflow_completion_inbox_overflow", { sessionId: entry.sessionId, droppedRunId: String(r.run_id) });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async pending(sessionId) {
|
|
185
|
+
const { rows } = await this.db.query(this.q("SELECT i.run_id, i.owner, i.status, i.summary, i.enqueued_at, i.kind, i.payload FROM workflow_completion_inbox i " +
|
|
186
|
+
"WHERE i.session_id = ? AND NOT EXISTS (SELECT 1 FROM workflow_inbox_fence f WHERE f.session_id = i.session_id AND f.kind = ? AND f.run_id IN (i.run_id, REGEXP_REPLACE(i.run_id, '#[0-9]+$', '')) AND f.until_ms >= ?) " +
|
|
187
|
+
"ORDER BY i.seq ASC", "SELECT i.run_id, i.owner, i.status, i.summary, i.enqueued_at, i.kind, i.payload FROM workflow_completion_inbox i " +
|
|
188
|
+
"WHERE i.session_id = $1 AND NOT EXISTS (SELECT 1 FROM workflow_inbox_fence f WHERE f.session_id = i.session_id AND f.kind = $2 AND f.run_id IN (i.run_id, regexp_replace(i.run_id, '#[0-9]+$', '')) AND f.until_ms >= $3) " +
|
|
189
|
+
"ORDER BY i.seq ASC"), [sessionId, KIND_SERVED, Date.now()]);
|
|
190
|
+
return rows.map((r) => ({
|
|
191
|
+
sessionId,
|
|
192
|
+
runId: String(r.run_id),
|
|
193
|
+
owner: r.owner === null ? null : String(r.owner),
|
|
194
|
+
status: String(r.status),
|
|
195
|
+
summary: String(r.summary),
|
|
196
|
+
enqueuedAt: Number(r.enqueued_at),
|
|
197
|
+
...(r.kind != null ? { kind: String(r.kind) } : {}),
|
|
198
|
+
...(r.payload != null ? { payload: String(r.payload) } : {}),
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
async ack(sessionId, runId) {
|
|
202
|
+
await this.db.query(this.q("DELETE FROM workflow_completion_inbox WHERE session_id = ? AND run_id = ?", "DELETE FROM workflow_completion_inbox WHERE session_id = $1 AND run_id = $2"), [sessionId, runId]);
|
|
203
|
+
}
|
|
204
|
+
async purge(sessionId, purgedOwner) {
|
|
205
|
+
const now = Date.now();
|
|
206
|
+
await this.sweepFences(now);
|
|
207
|
+
await this.db.query(this.q("INSERT INTO workflow_inbox_fence (session_id, kind, run_id, owner, owner_known, until_ms) VALUES (?,?,?,?,?,?) " +
|
|
208
|
+
"ON DUPLICATE KEY UPDATE owner = VALUES(owner), owner_known = VALUES(owner_known), until_ms = VALUES(until_ms)", "INSERT INTO workflow_inbox_fence (session_id, kind, run_id, owner, owner_known, until_ms) VALUES ($1,$2,'',$3,$4,$5) " +
|
|
209
|
+
"ON CONFLICT (session_id, kind, run_id) DO UPDATE SET owner = EXCLUDED.owner, owner_known = EXCLUDED.owner_known, until_ms = EXCLUDED.until_ms"), this.db.dialect === "tidb"
|
|
210
|
+
? [sessionId, KIND_PURGE, "", purgedOwner ?? null, purgedOwner !== undefined ? 1 : 0, now + PURGE_FENCE_MS]
|
|
211
|
+
: [sessionId, KIND_PURGE, purgedOwner ?? null, purgedOwner !== undefined ? 1 : 0, now + PURGE_FENCE_MS]);
|
|
212
|
+
await this.db.query(this.q("DELETE FROM workflow_completion_inbox WHERE session_id = ?", "DELETE FROM workflow_completion_inbox WHERE session_id = $1"), [sessionId]);
|
|
213
|
+
}
|
|
214
|
+
async markTerminalServed(sessionId, runId) {
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
await this.sweepFences(now);
|
|
217
|
+
await this.db.query(this.q("INSERT INTO workflow_inbox_fence (session_id, kind, run_id, owner, owner_known, until_ms) VALUES (?,?,?,NULL,0,?) " +
|
|
218
|
+
"ON DUPLICATE KEY UPDATE until_ms = VALUES(until_ms)", "INSERT INTO workflow_inbox_fence (session_id, kind, run_id, owner, owner_known, until_ms) VALUES ($1,$2,$3,NULL,0,$4) " +
|
|
219
|
+
"ON CONFLICT (session_id, kind, run_id) DO UPDATE SET until_ms = EXCLUDED.until_ms"), [sessionId, KIND_SERVED, runId, now + SERVED_FENCE_MS]);
|
|
220
|
+
await this.ack(sessionId, runId);
|
|
221
|
+
await this.db.query(this.q("DELETE FROM workflow_completion_inbox WHERE session_id = ? AND run_id <> ? AND REGEXP_REPLACE(run_id, '#[0-9]+$', '') = ?", "DELETE FROM workflow_completion_inbox WHERE session_id = $1 AND run_id <> $2 AND regexp_replace(run_id, '#[0-9]+$', '') = $3"), [sessionId, runId, runId]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export class SqlWorkflowNotifyJournalStore {
|
|
225
|
+
db;
|
|
226
|
+
constructor(db) {
|
|
227
|
+
this.db = db;
|
|
228
|
+
}
|
|
229
|
+
q(tidb, pg) {
|
|
230
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
231
|
+
}
|
|
232
|
+
async record(input) {
|
|
233
|
+
await this.db.query(this.q("INSERT IGNORE INTO workflow_notify_journal (run_id, scope, acked, source_task_id, principal, created_at) VALUES (?,?,0,?,?,?)", "INSERT INTO workflow_notify_journal (run_id, scope, acked, source_task_id, principal, created_at) VALUES ($1,$2,0,$3,$4,$5) ON CONFLICT (run_id) DO NOTHING"), [input.runId, input.scope, input.sourceTaskId ?? null, input.principal ?? null, input.createdAt]);
|
|
234
|
+
}
|
|
235
|
+
async ack(runId, ackedAt) {
|
|
236
|
+
await this.db.query(this.q("UPDATE workflow_notify_journal SET acked = 1, acked_at = ? WHERE run_id = ? AND acked = 0", "UPDATE workflow_notify_journal SET acked = 1, acked_at = $1 WHERE run_id = $2 AND acked = 0"), [ackedAt, runId]);
|
|
237
|
+
}
|
|
238
|
+
async listPending() {
|
|
239
|
+
const { rows } = await this.db.query("SELECT run_id, scope, source_task_id, principal, created_at FROM workflow_notify_journal WHERE acked = 0");
|
|
240
|
+
return rows.map((r) => ({
|
|
241
|
+
runId: String(r.run_id),
|
|
242
|
+
scope: String(r.scope),
|
|
243
|
+
acked: false,
|
|
244
|
+
...(r.source_task_id != null ? { sourceTaskId: String(r.source_task_id) } : {}),
|
|
245
|
+
...(r.principal != null ? { principal: String(r.principal) } : {}),
|
|
246
|
+
createdAt: Number(r.created_at),
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
async reapAcked(before) {
|
|
250
|
+
const res = await this.db.query(this.q("DELETE FROM workflow_notify_journal WHERE acked = 1 AND acked_at < ?", "DELETE FROM workflow_notify_journal WHERE acked = 1 AND acked_at < $1"), [before]);
|
|
251
|
+
return res.affected;
|
|
252
|
+
}
|
|
253
|
+
async get(runId) {
|
|
254
|
+
const { rows } = await this.db.query(this.q("SELECT run_id, scope, acked, source_task_id, principal, created_at, acked_at FROM workflow_notify_journal WHERE run_id = ?", "SELECT run_id, scope, acked, source_task_id, principal, created_at, acked_at FROM workflow_notify_journal WHERE run_id = $1"), [runId]);
|
|
255
|
+
const r = rows[0];
|
|
256
|
+
if (!r)
|
|
257
|
+
return null;
|
|
258
|
+
return {
|
|
259
|
+
runId: String(r.run_id),
|
|
260
|
+
scope: String(r.scope),
|
|
261
|
+
acked: Number(r.acked) === 1,
|
|
262
|
+
...(r.source_task_id != null ? { sourceTaskId: String(r.source_task_id) } : {}),
|
|
263
|
+
...(r.principal != null ? { principal: String(r.principal) } : {}),
|
|
264
|
+
createdAt: Number(r.created_at),
|
|
265
|
+
...(r.acked_at != null ? { ackedAt: Number(r.acked_at) } : {}),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export class TiDBWorkflowRunStore extends SqlWorkflowRunStore {
|
|
270
|
+
constructor(pool) {
|
|
271
|
+
super(mysqlDriver(pool));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
export class TiDBWorkflowCompletionInbox extends SqlWorkflowCompletionInbox {
|
|
275
|
+
constructor(pool, onWarn) {
|
|
276
|
+
super(mysqlDriver(pool), onWarn);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
export class TiDBWorkflowNotifyJournalStore extends SqlWorkflowNotifyJournalStore {
|
|
280
|
+
constructor(pool) {
|
|
281
|
+
super(mysqlDriver(pool));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
export class PgWorkflowRunStore extends SqlWorkflowRunStore {
|
|
285
|
+
constructor(pool) {
|
|
286
|
+
super(pgDriver(pool));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
export class PgWorkflowCompletionInbox extends SqlWorkflowCompletionInbox {
|
|
290
|
+
constructor(pool, onWarn) {
|
|
291
|
+
super(pgDriver(pool), onWarn);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export class PgWorkflowNotifyJournalStore extends SqlWorkflowNotifyJournalStore {
|
|
295
|
+
constructor(pool) {
|
|
296
|
+
super(pgDriver(pool));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=workflow-run-store-sql.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Pool } from "mysql2/promise";
|
|
1
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
|
+
import type { Pool as PgPool } from "pg";
|
|
2
3
|
export interface CounterTable {
|
|
3
4
|
table: string;
|
|
4
5
|
keyCol: string;
|
|
@@ -13,7 +14,9 @@ export interface CounterDegradeInfo {
|
|
|
13
14
|
}
|
|
14
15
|
export type CounterDegradeHook = (info: CounterDegradeInfo) => void;
|
|
15
16
|
export declare const DEFAULT_COUNTER_QUERY_TIMEOUT_MS = 30000;
|
|
16
|
-
|
|
17
|
+
type CounterDialect = "tidb" | "pg";
|
|
18
|
+
export declare class SqlWriteBehindCounter {
|
|
19
|
+
private readonly dialect;
|
|
17
20
|
private readonly pool;
|
|
18
21
|
private readonly t;
|
|
19
22
|
private readonly windowMs;
|
|
@@ -26,7 +29,8 @@ export declare class WriteBehindCounter {
|
|
|
26
29
|
private flushing;
|
|
27
30
|
private flushStartedAt;
|
|
28
31
|
private failStreak;
|
|
29
|
-
constructor(pool:
|
|
32
|
+
constructor(dialect: CounterDialect, pool: MySqlPool | PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook | undefined, queryTimeoutMs?: number);
|
|
33
|
+
private exec;
|
|
30
34
|
private bump;
|
|
31
35
|
bucket(): number;
|
|
32
36
|
private rk;
|
|
@@ -40,4 +44,11 @@ export declare class WriteBehindCounter {
|
|
|
40
44
|
startRefresh(intervalMs?: number): this;
|
|
41
45
|
stop(): void;
|
|
42
46
|
}
|
|
47
|
+
export declare class WriteBehindCounter extends SqlWriteBehindCounter {
|
|
48
|
+
constructor(pool: MySqlPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook, queryTimeoutMs?: number);
|
|
49
|
+
}
|
|
50
|
+
export declare class PgWriteBehindCounter extends SqlWriteBehindCounter {
|
|
51
|
+
constructor(pool: PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook, queryTimeoutMs?: number);
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
43
54
|
//# sourceMappingURL=write-behind-counter.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const SEP = "\u0000";
|
|
2
2
|
export const DEFAULT_COUNTER_QUERY_TIMEOUT_MS = 30_000;
|
|
3
|
-
export class
|
|
3
|
+
export class SqlWriteBehindCounter {
|
|
4
|
+
dialect;
|
|
4
5
|
pool;
|
|
5
6
|
t;
|
|
6
7
|
windowMs;
|
|
@@ -13,7 +14,8 @@ export class WriteBehindCounter {
|
|
|
13
14
|
flushing = false;
|
|
14
15
|
flushStartedAt = 0;
|
|
15
16
|
failStreak = new Map();
|
|
16
|
-
constructor(pool, t, windowMs, now = () => Date.now(), onDegraded, queryTimeoutMs = DEFAULT_COUNTER_QUERY_TIMEOUT_MS) {
|
|
17
|
+
constructor(dialect, pool, t, windowMs, now = () => Date.now(), onDegraded, queryTimeoutMs = DEFAULT_COUNTER_QUERY_TIMEOUT_MS) {
|
|
18
|
+
this.dialect = dialect;
|
|
17
19
|
this.pool = pool;
|
|
18
20
|
this.t = t;
|
|
19
21
|
this.windowMs = windowMs;
|
|
@@ -21,6 +23,17 @@ export class WriteBehindCounter {
|
|
|
21
23
|
this.onDegraded = onDegraded;
|
|
22
24
|
this.queryTimeoutMs = queryTimeoutMs;
|
|
23
25
|
}
|
|
26
|
+
async exec(sql, params) {
|
|
27
|
+
if (this.dialect === "tidb") {
|
|
28
|
+
const [r] = (await this.pool.query({ sql, timeout: this.queryTimeoutMs }, params));
|
|
29
|
+
if (Array.isArray(r))
|
|
30
|
+
return { rows: r, affected: 0 };
|
|
31
|
+
return { rows: [], affected: Number(r?.affectedRows ?? 0) };
|
|
32
|
+
}
|
|
33
|
+
const q = { text: sql, values: params, query_timeout: this.queryTimeoutMs };
|
|
34
|
+
const res = await this.pool.query(q);
|
|
35
|
+
return { rows: (res.rows ?? []), affected: res.rowCount ?? 0 };
|
|
36
|
+
}
|
|
24
37
|
bump(kind, failed, error) {
|
|
25
38
|
if (!this.onDegraded)
|
|
26
39
|
return;
|
|
@@ -72,6 +85,11 @@ export class WriteBehindCounter {
|
|
|
72
85
|
}
|
|
73
86
|
async flushOnce() {
|
|
74
87
|
const { table, keyCol, valCol } = this.t;
|
|
88
|
+
const sql = this.dialect === "tidb"
|
|
89
|
+
? `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES (?,?,?,?) ` +
|
|
90
|
+
`ON DUPLICATE KEY UPDATE ${valCol} = ${valCol} + VALUES(${valCol}), updated_at = VALUES(updated_at)`
|
|
91
|
+
: `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES ($1,$2,$3,$4) ` +
|
|
92
|
+
`ON CONFLICT (${keyCol}, window_bucket) DO UPDATE SET ${valCol} = ${table}.${valCol} + EXCLUDED.${valCol}, updated_at = EXCLUDED.updated_at`;
|
|
75
93
|
let attempted = false;
|
|
76
94
|
let failure;
|
|
77
95
|
let hadFailure = false;
|
|
@@ -83,11 +101,7 @@ export class WriteBehindCounter {
|
|
|
83
101
|
const windowBucket = Number(rk.slice(sep + 1));
|
|
84
102
|
attempted = true;
|
|
85
103
|
try {
|
|
86
|
-
await this.
|
|
87
|
-
sql: `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES (?,?,?,?) ` +
|
|
88
|
-
`ON DUPLICATE KEY UPDATE ${valCol} = ${valCol} + VALUES(${valCol}), updated_at = VALUES(updated_at)`,
|
|
89
|
-
timeout: this.queryTimeoutMs,
|
|
90
|
-
}, [key, windowBucket, delta, new Date()]);
|
|
104
|
+
await this.exec(sql, [key, windowBucket, delta, new Date()]);
|
|
91
105
|
const remaining = (this.pendingDelta.get(rk) ?? 0) - delta;
|
|
92
106
|
if (remaining > 0)
|
|
93
107
|
this.pendingDelta.set(rk, remaining);
|
|
@@ -108,10 +122,11 @@ export class WriteBehindCounter {
|
|
|
108
122
|
const { table, keyCol, valCol } = this.t;
|
|
109
123
|
const b = this.bucket();
|
|
110
124
|
try {
|
|
111
|
-
const
|
|
125
|
+
const { rows } = await this.exec(this.dialect === "tidb"
|
|
126
|
+
? `SELECT ${keyCol}, ${valCol} FROM ${table} WHERE window_bucket = ?`
|
|
127
|
+
: `SELECT ${keyCol}, ${valCol} FROM ${table} WHERE window_bucket = $1`, [b]);
|
|
112
128
|
const next = new Map();
|
|
113
|
-
for (const
|
|
114
|
-
const row = r;
|
|
129
|
+
for (const row of rows) {
|
|
115
130
|
next.set(`${String(row[keyCol])}${SEP}${b}`, Number(row[valCol]));
|
|
116
131
|
}
|
|
117
132
|
this.fleetTotal = next;
|
|
@@ -123,8 +138,10 @@ export class WriteBehindCounter {
|
|
|
123
138
|
}
|
|
124
139
|
async reap() {
|
|
125
140
|
try {
|
|
126
|
-
const
|
|
127
|
-
|
|
141
|
+
const { affected } = await this.exec(this.dialect === "tidb"
|
|
142
|
+
? `DELETE FROM ${this.t.table} WHERE window_bucket < ?`
|
|
143
|
+
: `DELETE FROM ${this.t.table} WHERE window_bucket < $1`, [this.bucket()]);
|
|
144
|
+
return affected;
|
|
128
145
|
}
|
|
129
146
|
catch {
|
|
130
147
|
return 0;
|
|
@@ -141,4 +158,14 @@ export class WriteBehindCounter {
|
|
|
141
158
|
this.timer = undefined;
|
|
142
159
|
}
|
|
143
160
|
}
|
|
161
|
+
export class WriteBehindCounter extends SqlWriteBehindCounter {
|
|
162
|
+
constructor(pool, t, windowMs, now, onDegraded, queryTimeoutMs) {
|
|
163
|
+
super("tidb", pool, t, windowMs, now, onDegraded, queryTimeoutMs);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export class PgWriteBehindCounter extends SqlWriteBehindCounter {
|
|
167
|
+
constructor(pool, t, windowMs, now, onDegraded, queryTimeoutMs) {
|
|
168
|
+
super("pg", pool, t, windowMs, now, onDegraded, queryTimeoutMs);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
144
171
|
//# sourceMappingURL=write-behind-counter.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
|
|
2
|
+
export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[], expect: {
|
|
3
|
+
iss: string;
|
|
4
|
+
aud: string;
|
|
5
|
+
nowSec?: number;
|
|
6
|
+
skewSec?: number;
|
|
7
|
+
maxTtlSec?: number;
|
|
8
|
+
}): {
|
|
9
|
+
sub: string;
|
|
10
|
+
bnd?: string;
|
|
11
|
+
} | null;
|
|
12
|
+
export declare function approvalBnd(env: {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
boundCallId?: string | null;
|
|
15
|
+
boundInputHash?: string | null;
|
|
16
|
+
}): string;
|
|
17
|
+
export declare function verifyDirectDoorProof(proof: {
|
|
18
|
+
jwt?: string;
|
|
19
|
+
mac?: string;
|
|
20
|
+
kid?: string;
|
|
21
|
+
}, envelope: {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
boundCallId?: string | null;
|
|
24
|
+
boundInputHash?: string | null;
|
|
25
|
+
decision: string;
|
|
26
|
+
reason?: string | null;
|
|
27
|
+
}, config: {
|
|
28
|
+
principalJwtPubkeys: PrincipalJwtKey[];
|
|
29
|
+
principalJwtIss?: string;
|
|
30
|
+
principalJwtAud?: string;
|
|
31
|
+
principalJwtMaxTtlSec?: number;
|
|
32
|
+
approvalHmacKeys: ApprovalHmacKey[];
|
|
33
|
+
}, opts?: {
|
|
34
|
+
actionBinding?: boolean;
|
|
35
|
+
}): {
|
|
36
|
+
ok: true;
|
|
37
|
+
principal: string;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
status: number;
|
|
41
|
+
errorCode: string;
|
|
42
|
+
error: string;
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=principal-jwt.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
2
|
+
import { verifyApprovalHmac } from "./approval-hmac.js";
|
|
3
|
+
function b64urlToBuf(s) {
|
|
4
|
+
return Buffer.from(s, "base64url");
|
|
5
|
+
}
|
|
6
|
+
function verifyJwsSignature(alg, signingInput, sig, pub) {
|
|
7
|
+
try {
|
|
8
|
+
if (alg === "EdDSA")
|
|
9
|
+
return cryptoVerify(null, signingInput, pub, sig);
|
|
10
|
+
if (alg === "RS256")
|
|
11
|
+
return cryptoVerify("sha256", signingInput, pub, sig);
|
|
12
|
+
if (alg === "ES256")
|
|
13
|
+
return cryptoVerify("sha256", signingInput, { key: pub, dsaEncoding: "ieee-p1363" }, sig);
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function verifyPrincipalJwt(token, keys, expect) {
|
|
21
|
+
if (keys.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
const parts = token.split(".");
|
|
24
|
+
if (parts.length !== 3)
|
|
25
|
+
return null;
|
|
26
|
+
const [h, p, s] = parts;
|
|
27
|
+
let header;
|
|
28
|
+
let payload;
|
|
29
|
+
let sig;
|
|
30
|
+
try {
|
|
31
|
+
header = JSON.parse(b64urlToBuf(h).toString("utf8"));
|
|
32
|
+
payload = JSON.parse(b64urlToBuf(p).toString("utf8"));
|
|
33
|
+
sig = b64urlToBuf(s);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (typeof header !== "object" || header === null || typeof payload !== "object" || payload === null)
|
|
39
|
+
return null;
|
|
40
|
+
if (typeof header.alg !== "string" || typeof header.kid !== "string" || sig.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
const key = keys.find((k) => k.kid === header.kid);
|
|
43
|
+
if (!key || key.alg !== header.alg)
|
|
44
|
+
return null;
|
|
45
|
+
let pub;
|
|
46
|
+
try {
|
|
47
|
+
pub = createPublicKey(key.key);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const signingInput = Buffer.from(`${h}.${p}`, "utf8");
|
|
53
|
+
if (!verifyJwsSignature(key.alg, signingInput, sig, pub))
|
|
54
|
+
return null;
|
|
55
|
+
if (payload.iss !== expect.iss)
|
|
56
|
+
return null;
|
|
57
|
+
const aud = payload.aud;
|
|
58
|
+
const audOk = aud === expect.aud || (Array.isArray(aud) && aud.includes(expect.aud));
|
|
59
|
+
if (!audOk)
|
|
60
|
+
return null;
|
|
61
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
const now = expect.nowSec ?? Math.floor(Date.now() / 1000);
|
|
64
|
+
const skew = expect.skewSec ?? 30;
|
|
65
|
+
if (typeof payload.exp !== "number" || payload.exp + skew < now)
|
|
66
|
+
return null;
|
|
67
|
+
if (payload.nbf !== undefined && (typeof payload.nbf !== "number" || payload.nbf - skew > now))
|
|
68
|
+
return null;
|
|
69
|
+
if (expect.maxTtlSec && payload.exp - now > expect.maxTtlSec + skew)
|
|
70
|
+
return null;
|
|
71
|
+
const bnd = typeof payload.cnf?.bnd === "string" ? payload.cnf.bnd : undefined;
|
|
72
|
+
return { sub: payload.sub, bnd };
|
|
73
|
+
}
|
|
74
|
+
export function approvalBnd(env) {
|
|
75
|
+
return createHash("sha256").update(JSON.stringify([env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null])).digest("hex");
|
|
76
|
+
}
|
|
77
|
+
export function verifyDirectDoorProof(proof, envelope, config, opts) {
|
|
78
|
+
const actionBinding = opts?.actionBinding ?? true;
|
|
79
|
+
if (!proof.jwt)
|
|
80
|
+
return { ok: false, status: 401, errorCode: "principal_unverified", error: "direct door requires a signed principal token (X-Approval-Principal-Token)" };
|
|
81
|
+
const verified = verifyPrincipalJwt(proof.jwt, config.principalJwtPubkeys, { iss: config.principalJwtIss ?? "\0", aud: config.principalJwtAud ?? "\0", maxTtlSec: config.principalJwtMaxTtlSec });
|
|
82
|
+
if (!verified)
|
|
83
|
+
return { ok: false, status: 401, errorCode: "principal_unverified", error: "principal token failed verification" };
|
|
84
|
+
if (actionBinding && !envelope.boundInputHash)
|
|
85
|
+
return { ok: false, status: 400, errorCode: "approval_mac_required", error: "direct door requires boundInputHash (the bound action) in the body" };
|
|
86
|
+
if (verified.bnd !== approvalBnd(envelope))
|
|
87
|
+
return { ok: false, status: 401, errorCode: "principal_unbound", error: "principal token is not bound to this decision (cnf.bnd mismatch)" };
|
|
88
|
+
if (!proof.mac)
|
|
89
|
+
return { ok: false, status: 401, errorCode: "approval_mac_required", error: "direct door requires an approval MAC (X-Approval-Mac)" };
|
|
90
|
+
const ok = verifyApprovalHmac({ sessionId: envelope.sessionId, boundCallId: envelope.boundCallId ?? null, boundInputHash: envelope.boundInputHash ?? null, decision: envelope.decision, reason: envelope.reason ?? null }, proof.mac, proof.kid, config.approvalHmacKeys);
|
|
91
|
+
if (!ok)
|
|
92
|
+
return { ok: false, status: 401, errorCode: "approval_mac_invalid", error: "approval envelope MAC invalid" };
|
|
93
|
+
return { ok: true, principal: verified.sub };
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=principal-jwt.js.map
|
package/dist/security.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { type SessionStore, type SessionTreeEntry } from "@sema-agent/core";
|
|
|
3
3
|
import type { ServiceConfig } from "./config-types.js";
|
|
4
4
|
import type { TaskRequestBody } from "./http/wire-types.js";
|
|
5
5
|
import type { StagingHandle } from "./session-sync-kernel.js";
|
|
6
|
-
import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
|
|
7
6
|
export { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
8
7
|
export type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
|
|
9
8
|
export interface SessionListItem {
|
|
@@ -81,61 +80,6 @@ export declare function setSsoScope(req: IncomingMessage, scope: string): void;
|
|
|
81
80
|
export declare function ssoVerifiedScope(req: IncomingMessage): string | undefined;
|
|
82
81
|
export declare function verifiedPrincipal(req: IncomingMessage, config: ServiceConfig): string | undefined;
|
|
83
82
|
export { memoryScopeFor, memoryEngineRemoteLanePosture, memoryEngineBackendFor, memorySpecForRequest } from "./memory-scope.js";
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
boundCallId?: string | null;
|
|
87
|
-
boundInputHash?: string | null;
|
|
88
|
-
decision: string;
|
|
89
|
-
reason?: string | null;
|
|
90
|
-
}): string;
|
|
91
|
-
export declare const MAX_APPROVAL_REASON_CHARS = 4096;
|
|
92
|
-
export declare function verifyApprovalHmac(env: {
|
|
93
|
-
sessionId: string;
|
|
94
|
-
boundCallId?: string | null;
|
|
95
|
-
boundInputHash?: string | null;
|
|
96
|
-
decision: string;
|
|
97
|
-
reason?: string | null;
|
|
98
|
-
}, mac: string, kid: string | undefined, keys: ApprovalHmacKey[]): boolean;
|
|
99
|
-
export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[], expect: {
|
|
100
|
-
iss: string;
|
|
101
|
-
aud: string;
|
|
102
|
-
nowSec?: number;
|
|
103
|
-
skewSec?: number;
|
|
104
|
-
maxTtlSec?: number;
|
|
105
|
-
}): {
|
|
106
|
-
sub: string;
|
|
107
|
-
bnd?: string;
|
|
108
|
-
} | null;
|
|
109
|
-
export declare function approvalBnd(env: {
|
|
110
|
-
sessionId: string;
|
|
111
|
-
boundCallId?: string | null;
|
|
112
|
-
boundInputHash?: string | null;
|
|
113
|
-
}): string;
|
|
114
|
-
export declare function verifyDirectDoorProof(proof: {
|
|
115
|
-
jwt?: string;
|
|
116
|
-
mac?: string;
|
|
117
|
-
kid?: string;
|
|
118
|
-
}, envelope: {
|
|
119
|
-
sessionId: string;
|
|
120
|
-
boundCallId?: string | null;
|
|
121
|
-
boundInputHash?: string | null;
|
|
122
|
-
decision: string;
|
|
123
|
-
reason?: string | null;
|
|
124
|
-
}, config: {
|
|
125
|
-
principalJwtPubkeys: PrincipalJwtKey[];
|
|
126
|
-
principalJwtIss?: string;
|
|
127
|
-
principalJwtAud?: string;
|
|
128
|
-
principalJwtMaxTtlSec?: number;
|
|
129
|
-
approvalHmacKeys: ApprovalHmacKey[];
|
|
130
|
-
}, opts?: {
|
|
131
|
-
actionBinding?: boolean;
|
|
132
|
-
}): {
|
|
133
|
-
ok: true;
|
|
134
|
-
principal: string;
|
|
135
|
-
} | {
|
|
136
|
-
ok: false;
|
|
137
|
-
status: number;
|
|
138
|
-
errorCode: string;
|
|
139
|
-
error: string;
|
|
140
|
-
};
|
|
83
|
+
export { approvalHmacMessage, MAX_APPROVAL_REASON_CHARS, verifyApprovalHmac } from "./approval-hmac.js";
|
|
84
|
+
export { verifyPrincipalJwt, approvalBnd, verifyDirectDoorProof } from "./principal-jwt.js";
|
|
141
85
|
//# sourceMappingURL=security.d.ts.map
|