@runuai/host 0.9.148 → 0.9.150
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/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +312 -2
- package/images/standard/Dockerfile +44 -6
- package/images/standard/container/uai-init +85 -2
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/lib/agent-cli.ts +18 -1
- package/lib/agent.ts +99 -34
- package/lib/agents/codex.ts +9 -1
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +2 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/transport.ts +9 -5
- package/lib/browser-testing.ts +6 -0
- package/lib/codex-auth.ts +2 -0
- package/lib/credential-store.ts +141 -6
- package/lib/engine-accounts.ts +1 -0
- package/lib/env-file.ts +7 -0
- package/lib/env-schema.ts +841 -51
- package/lib/git-diff.ts +1 -0
- package/lib/git-identity.ts +2 -0
- package/lib/github-tokens.ts +1 -0
- package/lib/host-env.ts +98 -18
- package/lib/lease.ts +11 -3
- package/lib/machine-gateway-tunnel.ts +195 -17
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/managed-runtime.ts +270 -51
- package/lib/mcp-gateway.ts +777 -26
- package/lib/orchestrator.ts +408 -81
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-loader.ts +237 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/runtime-archive.ts +79 -16
- package/lib/runtime-authority.ts +44 -0
- package/lib/runtime-state.ts +175 -10
- package/lib/secrets-proxy/address.ts +72 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +60 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +141 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +574 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/shared-files-s3.ts +3 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +51 -3
- package/lib/standard-image.ts +50 -12
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +193 -0
- package/lib/task-environment/apple-container.ts +45 -11
- package/lib/task-environment/docker.ts +51 -12
- package/lib/task-environment/index.ts +6 -2
- package/lib/task-environment/machine-task-up.ts +258 -25
- package/lib/task-environment/machine.ts +80 -22
- package/lib/task-environment/registry.ts +3 -2
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +92 -16
- package/lib/task-environment/types.ts +63 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +44 -0
- package/lib/task-gateway-state.ts +497 -0
- package/lib/transcript.ts +1 -0
- package/package.json +13 -1
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/task-up.sh +476 -27
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/src/cli.ts +26 -3
- package/src/index.ts +273 -19
- package/src/main.ts +939 -48
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +281 -0
- package/src/project-env-wording.ts +47 -0
- package/src/protocol.ts +2219 -15
|
@@ -0,0 +1,1319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable owned-runner project-env authority (ADR-153).
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately has no websocket knowledge. It owns the one SQLite
|
|
5
|
+
* transaction that couples a value/head CAS, project record version, ordered
|
|
6
|
+
* cursor and replay receipt. Receipts contain a keyed MAC of a request and a
|
|
7
|
+
* value-free outcome; a plaintext set value is never durable outside its
|
|
8
|
+
* encrypted host_project_env row.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
import { and, asc, eq, sql } from "drizzle-orm";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
INITIAL_PROJECT_ENV_POLICY_EPOCH,
|
|
17
|
+
isHostOpId,
|
|
18
|
+
isProjectEnvBindingState,
|
|
19
|
+
isProjectEnvBindingSetFrame,
|
|
20
|
+
isProjectEnvDeleteFrame,
|
|
21
|
+
isProjectEnvDeliverySkipFrame,
|
|
22
|
+
isProjectEnvPolicyEpochs,
|
|
23
|
+
isProjectEnvReconciledFrame,
|
|
24
|
+
isProjectEnvReconciliationAttempt,
|
|
25
|
+
isProjectEnvRemoveFrame,
|
|
26
|
+
isProjectEnvSetFrame,
|
|
27
|
+
sameProjectEnvHead,
|
|
28
|
+
type ProjectEnvBinding,
|
|
29
|
+
type ProjectEnvBindingSetFrame,
|
|
30
|
+
type ProjectEnvBindingState,
|
|
31
|
+
type ProjectEnvDeleteFrame,
|
|
32
|
+
type ProjectEnvDeliveryCursor,
|
|
33
|
+
type ProjectEnvDeliverySkipFrame,
|
|
34
|
+
type ProjectEnvHead,
|
|
35
|
+
type ProjectEnvReconcileBeginFrame,
|
|
36
|
+
type ProjectEnvReconciledFrame,
|
|
37
|
+
type ProjectEnvRemoveFrame,
|
|
38
|
+
type ProjectEnvSetFrame,
|
|
39
|
+
type ProjectEnvWithholdReason,
|
|
40
|
+
} from "../src/protocol";
|
|
41
|
+
import { getDb, schema, type Db } from "./db";
|
|
42
|
+
import { PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY } from "./project-env-feature";
|
|
43
|
+
import {
|
|
44
|
+
mirrorProjectEnvDeletionToStore,
|
|
45
|
+
mirrorProjectEnvToStore,
|
|
46
|
+
noteProjectEnvAuthorityChange,
|
|
47
|
+
noteProjectEnvValueMutation,
|
|
48
|
+
openProjectEnvValueFromStorage,
|
|
49
|
+
sealProjectEnvValueForStorage,
|
|
50
|
+
} from "./host-env";
|
|
51
|
+
import { masterKey } from "./secrets";
|
|
52
|
+
|
|
53
|
+
/** Managed runtimes must claim this floor before the first begin transaction. */
|
|
54
|
+
export const PROJECT_ENV_AUTHORITY_RUNTIME_CAPABILITY =
|
|
55
|
+
PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY;
|
|
56
|
+
|
|
57
|
+
type ModernProjectEnvSetFrame = Extract<
|
|
58
|
+
ProjectEnvSetFrame,
|
|
59
|
+
{ delivery: ProjectEnvDeliveryCursor }
|
|
60
|
+
>;
|
|
61
|
+
type ModernProjectEnvDeleteFrame = Extract<
|
|
62
|
+
ProjectEnvDeleteFrame,
|
|
63
|
+
{ delivery: ProjectEnvDeliveryCursor }
|
|
64
|
+
>;
|
|
65
|
+
|
|
66
|
+
export type ProjectEnvOwnedDeliveryFrame =
|
|
67
|
+
| ModernProjectEnvSetFrame
|
|
68
|
+
| ModernProjectEnvDeleteFrame
|
|
69
|
+
| ProjectEnvRemoveFrame
|
|
70
|
+
| ProjectEnvBindingSetFrame
|
|
71
|
+
| ProjectEnvDeliverySkipFrame;
|
|
72
|
+
|
|
73
|
+
type ProjectEnvRecordRow = typeof schema.hostProjectEnvRecords.$inferSelect;
|
|
74
|
+
type ProjectEnvAuthorityRow = typeof schema.hostProjectEnvAuthority.$inferSelect;
|
|
75
|
+
type ProjectEnvReceiptRow =
|
|
76
|
+
typeof schema.hostProjectEnvDeliveryReceipts.$inferSelect;
|
|
77
|
+
type ProjectEnvTransaction = Parameters<Parameters<Db["transaction"]>[0]>[0];
|
|
78
|
+
|
|
79
|
+
export class ProjectEnvAuthorityCorruptError extends Error {
|
|
80
|
+
constructor(message: string) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.name = "ProjectEnvAuthorityCorruptError";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ProjectEnvAuthorityStatus {
|
|
87
|
+
activated: boolean;
|
|
88
|
+
gateOpen: boolean;
|
|
89
|
+
appliedThrough: ProjectEnvDeliveryCursor | null;
|
|
90
|
+
attemptId: string | null;
|
|
91
|
+
policyEpochs: number[];
|
|
92
|
+
legacyVersion: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ProjectEnvRecordInventory {
|
|
96
|
+
projectId: string;
|
|
97
|
+
recordVersion: number;
|
|
98
|
+
states: ProjectEnvBindingState[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ProjectEnvAuthorityEntry {
|
|
102
|
+
key: string;
|
|
103
|
+
state: ProjectEnvBindingState | null;
|
|
104
|
+
value: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ProjectEnvAuthorityProjectSnapshot {
|
|
108
|
+
projectId: string;
|
|
109
|
+
recordVersion: number;
|
|
110
|
+
entries: ProjectEnvAuthorityEntry[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ProjectEnvAuthoritySnapshot {
|
|
114
|
+
mode: "legacy" | "authority";
|
|
115
|
+
projects: ProjectEnvAuthorityProjectSnapshot[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type ProjectEnvDeliveryApplyResult =
|
|
119
|
+
| {
|
|
120
|
+
ok: true;
|
|
121
|
+
replayed: boolean;
|
|
122
|
+
appliedThrough: ProjectEnvDeliveryCursor;
|
|
123
|
+
state: ProjectEnvBindingState | null;
|
|
124
|
+
recordVersion: number | null;
|
|
125
|
+
}
|
|
126
|
+
| {
|
|
127
|
+
ok: false;
|
|
128
|
+
replayed: boolean;
|
|
129
|
+
consumed: boolean;
|
|
130
|
+
code: ProjectEnvWithholdReason;
|
|
131
|
+
appliedThrough: ProjectEnvDeliveryCursor;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type ProjectEnvReconciliationBeginResult = {
|
|
135
|
+
appliedThrough: ProjectEnvDeliveryCursor;
|
|
136
|
+
activatedNow: boolean;
|
|
137
|
+
foldedLegacyKeys: Array<{ projectId: string; key: string }>;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type ProjectEnvReconciliationCompleteResult =
|
|
141
|
+
| { ok: true; appliedThrough: ProjectEnvDeliveryCursor }
|
|
142
|
+
| {
|
|
143
|
+
ok: false;
|
|
144
|
+
code: ProjectEnvWithholdReason;
|
|
145
|
+
appliedThrough: ProjectEnvDeliveryCursor;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const RECEIPT_HMAC_DOMAIN = Buffer.from(
|
|
149
|
+
"uai.project_env.delivery_receipt.v1\0",
|
|
150
|
+
"utf8",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
function exactJson(value: unknown): string {
|
|
154
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
155
|
+
if (Array.isArray(value)) return `[${value.map(exactJson).join(",")}]`;
|
|
156
|
+
const object = value as Record<string, unknown>;
|
|
157
|
+
return `{${Object.keys(object)
|
|
158
|
+
.sort()
|
|
159
|
+
.map((key) => `${JSON.stringify(key)}:${exactJson(object[key])}`)
|
|
160
|
+
.join(",")}}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function canonicalDeliveryRequest(frame: ProjectEnvOwnedDeliveryFrame): unknown {
|
|
164
|
+
switch (frame.kind) {
|
|
165
|
+
case "env.var.set":
|
|
166
|
+
return {
|
|
167
|
+
kind: frame.kind,
|
|
168
|
+
projectId: frame.projectId,
|
|
169
|
+
key: frame.key,
|
|
170
|
+
value: frame.value,
|
|
171
|
+
orgId: frame.orgId ?? null,
|
|
172
|
+
expectedHead: frame.expectedHead,
|
|
173
|
+
...(Object.hasOwn(frame, "initialBinding")
|
|
174
|
+
? { initialBinding: frame.initialBinding }
|
|
175
|
+
: {}),
|
|
176
|
+
policyEpoch: frame.policyEpoch,
|
|
177
|
+
listingGeneration: frame.listingGeneration,
|
|
178
|
+
delivery: frame.delivery,
|
|
179
|
+
};
|
|
180
|
+
case "env.var.delete":
|
|
181
|
+
return {
|
|
182
|
+
kind: frame.kind,
|
|
183
|
+
projectId: frame.projectId,
|
|
184
|
+
key: frame.key,
|
|
185
|
+
expectedHead: frame.expectedHead,
|
|
186
|
+
policyEpoch: frame.policyEpoch,
|
|
187
|
+
listingGeneration: frame.listingGeneration,
|
|
188
|
+
delivery: frame.delivery,
|
|
189
|
+
};
|
|
190
|
+
case "env.var.remove":
|
|
191
|
+
return {
|
|
192
|
+
kind: frame.kind,
|
|
193
|
+
projectId: frame.projectId,
|
|
194
|
+
key: frame.key,
|
|
195
|
+
listingGeneration: frame.listingGeneration,
|
|
196
|
+
delivery: frame.delivery,
|
|
197
|
+
};
|
|
198
|
+
case "env.binding.set":
|
|
199
|
+
return {
|
|
200
|
+
kind: frame.kind,
|
|
201
|
+
projectId: frame.projectId,
|
|
202
|
+
key: frame.key,
|
|
203
|
+
binding: frame.binding,
|
|
204
|
+
expectedHead: frame.expectedHead,
|
|
205
|
+
policyEpoch: frame.policyEpoch,
|
|
206
|
+
listingGeneration: frame.listingGeneration,
|
|
207
|
+
orgId: frame.orgId ?? null,
|
|
208
|
+
delivery: frame.delivery,
|
|
209
|
+
};
|
|
210
|
+
case "env.delivery.skip":
|
|
211
|
+
return { kind: frame.kind, delivery: frame.delivery };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function requestMac(frame: ProjectEnvOwnedDeliveryFrame): string {
|
|
216
|
+
return createHmac("sha256", masterKey())
|
|
217
|
+
.update(RECEIPT_HMAC_DOMAIN)
|
|
218
|
+
.update(exactJson(canonicalDeliveryRequest(frame)))
|
|
219
|
+
.digest("hex");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sameMac(left: string, right: string): boolean {
|
|
223
|
+
if (!/^[0-9a-f]{64}$/.test(left) || !/^[0-9a-f]{64}$/.test(right)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseBinding(bindingJson: string | null): ProjectEnvBinding | null {
|
|
230
|
+
if (bindingJson === null) return null;
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(bindingJson) as unknown as ProjectEnvBinding;
|
|
233
|
+
} catch {
|
|
234
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
235
|
+
"stored project environment binding is malformed",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function rowState(row: ProjectEnvRecordRow): ProjectEnvBindingState {
|
|
241
|
+
const state: ProjectEnvBindingState = {
|
|
242
|
+
key: row.key,
|
|
243
|
+
head: row.tombstone
|
|
244
|
+
? {
|
|
245
|
+
revision: row.revision,
|
|
246
|
+
generation: row.generation,
|
|
247
|
+
tombstone: true,
|
|
248
|
+
valueRef: null,
|
|
249
|
+
}
|
|
250
|
+
: {
|
|
251
|
+
revision: row.revision,
|
|
252
|
+
generation: row.generation,
|
|
253
|
+
tombstone: false,
|
|
254
|
+
valueRef: { kind: "row" },
|
|
255
|
+
},
|
|
256
|
+
binding: parseBinding(row.bindingJson),
|
|
257
|
+
policyEpoch: row.policyEpoch,
|
|
258
|
+
listingGeneration: row.listingGeneration,
|
|
259
|
+
};
|
|
260
|
+
if (!isProjectEnvBindingState(state)) {
|
|
261
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
262
|
+
`stored project environment record is invalid (${row.projectId}/${row.key})`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return state;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function storedRecord(
|
|
269
|
+
tx: ProjectEnvTransaction,
|
|
270
|
+
projectId: string,
|
|
271
|
+
key: string,
|
|
272
|
+
): ProjectEnvRecordRow | null {
|
|
273
|
+
return (
|
|
274
|
+
tx
|
|
275
|
+
.select()
|
|
276
|
+
.from(schema.hostProjectEnvRecords)
|
|
277
|
+
.where(
|
|
278
|
+
and(
|
|
279
|
+
eq(schema.hostProjectEnvRecords.projectId, projectId),
|
|
280
|
+
eq(schema.hostProjectEnvRecords.key, key),
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
.get() ?? null
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function authorityRow(tx: ProjectEnvTransaction | Db): ProjectEnvAuthorityRow {
|
|
288
|
+
const row = tx
|
|
289
|
+
.select()
|
|
290
|
+
.from(schema.hostProjectEnvAuthority)
|
|
291
|
+
.where(eq(schema.hostProjectEnvAuthority.id, "host"))
|
|
292
|
+
.get();
|
|
293
|
+
if (!row) {
|
|
294
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
295
|
+
"project environment authority row is missing",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return row;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function parsePolicyEpochs(row: ProjectEnvAuthorityRow): number[] {
|
|
302
|
+
let value: unknown;
|
|
303
|
+
try {
|
|
304
|
+
value = JSON.parse(row.policyEpochsJson);
|
|
305
|
+
} catch {
|
|
306
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
307
|
+
"stored project environment policy epochs are malformed",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (!isProjectEnvPolicyEpochs(value)) {
|
|
311
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
312
|
+
"stored project environment policy epochs are invalid",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function cursorFor(row: ProjectEnvAuthorityRow): ProjectEnvDeliveryCursor | null {
|
|
319
|
+
if (!row.activated) return null;
|
|
320
|
+
const cursor = { streamId: row.streamId, sequence: row.appliedSequence };
|
|
321
|
+
if (
|
|
322
|
+
!isHostOpId(row.streamId) ||
|
|
323
|
+
!Number.isSafeInteger(row.appliedSequence) ||
|
|
324
|
+
row.appliedSequence < 0
|
|
325
|
+
) {
|
|
326
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
327
|
+
"stored project environment delivery cursor is invalid",
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return cursor as ProjectEnvDeliveryCursor;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function validateAuthorityRow(row: ProjectEnvAuthorityRow): void {
|
|
334
|
+
parsePolicyEpochs(row);
|
|
335
|
+
cursorFor(row);
|
|
336
|
+
if (
|
|
337
|
+
!Number.isSafeInteger(row.legacyVersion) ||
|
|
338
|
+
row.legacyVersion < 0 ||
|
|
339
|
+
row.modernWrite ||
|
|
340
|
+
(row.attemptId !== null && !isHostOpId(row.attemptId)) ||
|
|
341
|
+
(row.attemptId === null) !== (row.attemptLegacyVersion === null) ||
|
|
342
|
+
(row.gateOpen && row.attemptId !== null)
|
|
343
|
+
) {
|
|
344
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
345
|
+
"stored project environment authority state is invalid",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function readProjectEnvAuthorityStatus(): ProjectEnvAuthorityStatus {
|
|
351
|
+
const row = authorityRow(getDb());
|
|
352
|
+
validateAuthorityRow(row);
|
|
353
|
+
return {
|
|
354
|
+
activated: row.activated,
|
|
355
|
+
gateOpen: row.gateOpen,
|
|
356
|
+
appliedThrough: cursorFor(row),
|
|
357
|
+
attemptId: row.attemptId,
|
|
358
|
+
policyEpochs: parsePolicyEpochs(row),
|
|
359
|
+
legacyVersion: row.legacyVersion,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** A corrupted or closed activated authority never permits a value read. */
|
|
364
|
+
export function projectEnvAuthorityGateAllowsReads(): boolean {
|
|
365
|
+
try {
|
|
366
|
+
const status = readProjectEnvAuthorityStatus();
|
|
367
|
+
return !status.activated || status.gateOpen;
|
|
368
|
+
} catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function projectRecordVersion(
|
|
374
|
+
tx: ProjectEnvTransaction | Db,
|
|
375
|
+
projectId: string,
|
|
376
|
+
): number {
|
|
377
|
+
const row = tx
|
|
378
|
+
.select({ recordVersion: schema.hostProjectEnvProjects.recordVersion })
|
|
379
|
+
.from(schema.hostProjectEnvProjects)
|
|
380
|
+
.where(eq(schema.hostProjectEnvProjects.projectId, projectId))
|
|
381
|
+
.get();
|
|
382
|
+
const version = row?.recordVersion ?? 0;
|
|
383
|
+
if (!Number.isSafeInteger(version) || version < 0) {
|
|
384
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
385
|
+
`stored project environment record version is invalid (${projectId})`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return version;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function bumpProjectRecordVersion(
|
|
392
|
+
tx: ProjectEnvTransaction,
|
|
393
|
+
projectId: string,
|
|
394
|
+
): number {
|
|
395
|
+
tx.insert(schema.hostProjectEnvProjects)
|
|
396
|
+
.values({ projectId, recordVersion: 1 })
|
|
397
|
+
.onConflictDoUpdate({
|
|
398
|
+
target: schema.hostProjectEnvProjects.projectId,
|
|
399
|
+
set: {
|
|
400
|
+
recordVersion: sql`${schema.hostProjectEnvProjects.recordVersion} + 1`,
|
|
401
|
+
},
|
|
402
|
+
})
|
|
403
|
+
.run();
|
|
404
|
+
return projectRecordVersion(tx, projectId);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function putState(
|
|
408
|
+
tx: ProjectEnvTransaction,
|
|
409
|
+
projectId: string,
|
|
410
|
+
state: ProjectEnvBindingState,
|
|
411
|
+
): void {
|
|
412
|
+
if (!isProjectEnvBindingState(state)) {
|
|
413
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
414
|
+
"refusing to persist an invalid project environment record",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
tx.insert(schema.hostProjectEnvRecords)
|
|
418
|
+
.values({
|
|
419
|
+
projectId,
|
|
420
|
+
key: state.key,
|
|
421
|
+
revision: state.head.revision,
|
|
422
|
+
generation: state.head.generation,
|
|
423
|
+
tombstone: state.head.tombstone,
|
|
424
|
+
bindingJson: state.binding === null ? null : JSON.stringify(state.binding),
|
|
425
|
+
policyEpoch: state.policyEpoch,
|
|
426
|
+
listingGeneration: state.listingGeneration,
|
|
427
|
+
updatedAt: Date.now(),
|
|
428
|
+
})
|
|
429
|
+
.onConflictDoUpdate({
|
|
430
|
+
target: [
|
|
431
|
+
schema.hostProjectEnvRecords.projectId,
|
|
432
|
+
schema.hostProjectEnvRecords.key,
|
|
433
|
+
],
|
|
434
|
+
set: {
|
|
435
|
+
revision: state.head.revision,
|
|
436
|
+
generation: state.head.generation,
|
|
437
|
+
tombstone: state.head.tombstone,
|
|
438
|
+
bindingJson:
|
|
439
|
+
state.binding === null ? null : JSON.stringify(state.binding),
|
|
440
|
+
policyEpoch: state.policyEpoch,
|
|
441
|
+
listingGeneration: state.listingGeneration,
|
|
442
|
+
updatedAt: Date.now(),
|
|
443
|
+
},
|
|
444
|
+
})
|
|
445
|
+
.run();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function inventoryWith(
|
|
449
|
+
tx: ProjectEnvTransaction | Db,
|
|
450
|
+
projectId: string,
|
|
451
|
+
): ProjectEnvRecordInventory {
|
|
452
|
+
const rows = tx
|
|
453
|
+
.select()
|
|
454
|
+
.from(schema.hostProjectEnvRecords)
|
|
455
|
+
.where(eq(schema.hostProjectEnvRecords.projectId, projectId))
|
|
456
|
+
.orderBy(asc(schema.hostProjectEnvRecords.key))
|
|
457
|
+
.all();
|
|
458
|
+
const version = projectRecordVersion(tx, projectId);
|
|
459
|
+
if (rows.length > 0 && version === 0) {
|
|
460
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
461
|
+
`project environment records have no version (${projectId})`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
projectId,
|
|
466
|
+
recordVersion: version,
|
|
467
|
+
states: rows.map(rowState),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function readProjectEnvRecordInventory(
|
|
472
|
+
projectId: string,
|
|
473
|
+
): ProjectEnvRecordInventory {
|
|
474
|
+
return getDb().transaction((tx) => inventoryWith(tx, projectId));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* One consistent, fail-closed value/head snapshot for the loader/proxy. Before
|
|
479
|
+
* activation it preserves the legacy value-only behavior. Once activated it
|
|
480
|
+
* returns null unless the gate is open and every live record has exactly one
|
|
481
|
+
* decryptable value row (and no unrecorded row exists).
|
|
482
|
+
*/
|
|
483
|
+
export function readProjectEnvAuthoritySnapshot(
|
|
484
|
+
projectIds: readonly string[],
|
|
485
|
+
): ProjectEnvAuthoritySnapshot | null {
|
|
486
|
+
try {
|
|
487
|
+
return getDb().transaction((tx) => {
|
|
488
|
+
const authority = authorityRow(tx);
|
|
489
|
+
validateAuthorityRow(authority);
|
|
490
|
+
if (authority.activated && !authority.gateOpen) return null;
|
|
491
|
+
|
|
492
|
+
const projects: ProjectEnvAuthorityProjectSnapshot[] = [];
|
|
493
|
+
for (const projectId of projectIds) {
|
|
494
|
+
const valueRows = tx
|
|
495
|
+
.select()
|
|
496
|
+
.from(schema.hostProjectEnv)
|
|
497
|
+
.where(eq(schema.hostProjectEnv.projectId, projectId))
|
|
498
|
+
.orderBy(asc(schema.hostProjectEnv.key))
|
|
499
|
+
.all();
|
|
500
|
+
if (!authority.activated) {
|
|
501
|
+
projects.push({
|
|
502
|
+
projectId,
|
|
503
|
+
recordVersion: 0,
|
|
504
|
+
entries: valueRows.map((row) => ({
|
|
505
|
+
key: row.key,
|
|
506
|
+
state: null,
|
|
507
|
+
value: openProjectEnvValueFromStorage(row.valueEnc),
|
|
508
|
+
})),
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const inventory = inventoryWith(tx, projectId);
|
|
514
|
+
const values = new Map(valueRows.map((row) => [row.key, row.valueEnc]));
|
|
515
|
+
const entries: ProjectEnvAuthorityEntry[] = [];
|
|
516
|
+
for (const state of inventory.states) {
|
|
517
|
+
if (state.head.tombstone) {
|
|
518
|
+
if (values.has(state.key)) {
|
|
519
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
520
|
+
`tombstoned project environment record has a value (${projectId}/${state.key})`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const valueEnc = values.get(state.key);
|
|
526
|
+
if (valueEnc === undefined) {
|
|
527
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
528
|
+
`live project environment record has no value (${projectId}/${state.key})`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
entries.push({
|
|
532
|
+
key: state.key,
|
|
533
|
+
state,
|
|
534
|
+
value: openProjectEnvValueFromStorage(valueEnc),
|
|
535
|
+
});
|
|
536
|
+
values.delete(state.key);
|
|
537
|
+
}
|
|
538
|
+
if (values.size > 0) {
|
|
539
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
540
|
+
`project environment value has no authority record (${projectId})`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
projects.push({
|
|
544
|
+
projectId,
|
|
545
|
+
recordVersion: inventory.recordVersion,
|
|
546
|
+
entries,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return { mode: authority.activated ? "authority" : "legacy", projects };
|
|
550
|
+
});
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function setModernWrite(tx: ProjectEnvTransaction, value: boolean): void {
|
|
557
|
+
const changed = tx
|
|
558
|
+
.update(schema.hostProjectEnvAuthority)
|
|
559
|
+
.set({ modernWrite: value, updatedAt: Date.now() })
|
|
560
|
+
.where(eq(schema.hostProjectEnvAuthority.id, "host"))
|
|
561
|
+
.run().changes;
|
|
562
|
+
if (changed !== 1) {
|
|
563
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
564
|
+
"project environment authority row could not be fenced",
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function backfillLegacyValues(tx: ProjectEnvTransaction): void {
|
|
570
|
+
const record = tx
|
|
571
|
+
.select({ key: schema.hostProjectEnvRecords.key })
|
|
572
|
+
.from(schema.hostProjectEnvRecords)
|
|
573
|
+
.limit(1)
|
|
574
|
+
.get();
|
|
575
|
+
const project = tx
|
|
576
|
+
.select({ projectId: schema.hostProjectEnvProjects.projectId })
|
|
577
|
+
.from(schema.hostProjectEnvProjects)
|
|
578
|
+
.limit(1)
|
|
579
|
+
.get();
|
|
580
|
+
if (record || project) {
|
|
581
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
582
|
+
"unactivated project environment authority already has records",
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const rows = tx
|
|
586
|
+
.select({
|
|
587
|
+
projectId: schema.hostProjectEnv.projectId,
|
|
588
|
+
key: schema.hostProjectEnv.key,
|
|
589
|
+
})
|
|
590
|
+
.from(schema.hostProjectEnv)
|
|
591
|
+
.orderBy(
|
|
592
|
+
asc(schema.hostProjectEnv.projectId),
|
|
593
|
+
asc(schema.hostProjectEnv.key),
|
|
594
|
+
)
|
|
595
|
+
.all();
|
|
596
|
+
const projects = new Set<string>();
|
|
597
|
+
for (const row of rows) {
|
|
598
|
+
putState(tx, row.projectId, {
|
|
599
|
+
key: row.key,
|
|
600
|
+
head: {
|
|
601
|
+
revision: 1,
|
|
602
|
+
generation: 1,
|
|
603
|
+
tombstone: false,
|
|
604
|
+
valueRef: { kind: "row" },
|
|
605
|
+
},
|
|
606
|
+
binding: null,
|
|
607
|
+
policyEpoch: INITIAL_PROJECT_ENV_POLICY_EPOCH,
|
|
608
|
+
listingGeneration: 0,
|
|
609
|
+
});
|
|
610
|
+
projects.add(row.projectId);
|
|
611
|
+
}
|
|
612
|
+
for (const projectId of projects) {
|
|
613
|
+
tx.insert(schema.hostProjectEnvProjects)
|
|
614
|
+
.values({ projectId, recordVersion: 1 })
|
|
615
|
+
.run();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function foldLegacyJournal(
|
|
620
|
+
tx: ProjectEnvTransaction,
|
|
621
|
+
): Array<{ projectId: string; key: string }> {
|
|
622
|
+
const rows = tx
|
|
623
|
+
.select({
|
|
624
|
+
projectId: schema.hostProjectEnvLegacyJournal.projectId,
|
|
625
|
+
key: schema.hostProjectEnvLegacyJournal.key,
|
|
626
|
+
})
|
|
627
|
+
.from(schema.hostProjectEnvLegacyJournal)
|
|
628
|
+
.groupBy(
|
|
629
|
+
schema.hostProjectEnvLegacyJournal.projectId,
|
|
630
|
+
schema.hostProjectEnvLegacyJournal.key,
|
|
631
|
+
)
|
|
632
|
+
.orderBy(
|
|
633
|
+
asc(schema.hostProjectEnvLegacyJournal.projectId),
|
|
634
|
+
asc(schema.hostProjectEnvLegacyJournal.key),
|
|
635
|
+
)
|
|
636
|
+
.all();
|
|
637
|
+
for (const dirty of rows) {
|
|
638
|
+
const old = storedRecord(tx, dirty.projectId, dirty.key);
|
|
639
|
+
let state: ProjectEnvBindingState;
|
|
640
|
+
if (!old) {
|
|
641
|
+
// A post-activation legacy insert/delete has no authorized value. The
|
|
642
|
+
// migration tombstone shape makes every delayed legacy save stale.
|
|
643
|
+
state = {
|
|
644
|
+
key: dirty.key,
|
|
645
|
+
head: {
|
|
646
|
+
revision: 2,
|
|
647
|
+
generation: 2,
|
|
648
|
+
tombstone: true,
|
|
649
|
+
valueRef: null,
|
|
650
|
+
},
|
|
651
|
+
binding: null,
|
|
652
|
+
policyEpoch: INITIAL_PROJECT_ENV_POLICY_EPOCH,
|
|
653
|
+
listingGeneration: 0,
|
|
654
|
+
};
|
|
655
|
+
putState(tx, dirty.projectId, state);
|
|
656
|
+
bumpProjectRecordVersion(tx, dirty.projectId);
|
|
657
|
+
} else {
|
|
658
|
+
const current = rowState(old);
|
|
659
|
+
if (!current.head.tombstone) {
|
|
660
|
+
state = {
|
|
661
|
+
...current,
|
|
662
|
+
head: {
|
|
663
|
+
revision: current.head.revision + 1,
|
|
664
|
+
generation: current.head.generation + 1,
|
|
665
|
+
tombstone: true,
|
|
666
|
+
valueRef: null,
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
putState(tx, dirty.projectId, state);
|
|
670
|
+
bumpProjectRecordVersion(tx, dirty.projectId);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
tx.delete(schema.hostProjectEnv)
|
|
674
|
+
.where(
|
|
675
|
+
and(
|
|
676
|
+
eq(schema.hostProjectEnv.projectId, dirty.projectId),
|
|
677
|
+
eq(schema.hostProjectEnv.key, dirty.key),
|
|
678
|
+
),
|
|
679
|
+
)
|
|
680
|
+
.run();
|
|
681
|
+
}
|
|
682
|
+
tx.delete(schema.hostProjectEnvLegacyJournal).run();
|
|
683
|
+
return rows;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Activate (if needed), fold every legacy write, close the gate, and persist a
|
|
688
|
+
* fresh attempt. The caller must claim capability floor 4 before calling this
|
|
689
|
+
* on an unactivated managed runtime.
|
|
690
|
+
*/
|
|
691
|
+
export function beginProjectEnvReconciliation(
|
|
692
|
+
frame: ProjectEnvReconcileBeginFrame,
|
|
693
|
+
policyEpochs: readonly number[],
|
|
694
|
+
): ProjectEnvReconciliationBeginResult {
|
|
695
|
+
if (
|
|
696
|
+
frame.kind !== "env.reconcile.begin" ||
|
|
697
|
+
!isHostOpId(frame.opId) ||
|
|
698
|
+
!isProjectEnvReconciliationAttempt(frame.attempt) ||
|
|
699
|
+
!isProjectEnvPolicyEpochs(policyEpochs)
|
|
700
|
+
) {
|
|
701
|
+
throw new Error("invalid project environment reconciliation begin");
|
|
702
|
+
}
|
|
703
|
+
const result = getDb().transaction((tx) => {
|
|
704
|
+
const before = authorityRow(tx);
|
|
705
|
+
validateAuthorityRow(before);
|
|
706
|
+
setModernWrite(tx, true);
|
|
707
|
+
if (!before.activated) backfillLegacyValues(tx);
|
|
708
|
+
const foldedLegacyKeys = before.activated ? foldLegacyJournal(tx) : [];
|
|
709
|
+
const streamChanged = before.streamId !== frame.attempt.streamId;
|
|
710
|
+
tx.update(schema.hostProjectEnvAuthority)
|
|
711
|
+
.set({
|
|
712
|
+
activated: true,
|
|
713
|
+
gateOpen: false,
|
|
714
|
+
streamId: frame.attempt.streamId,
|
|
715
|
+
appliedSequence: streamChanged ? 0 : before.appliedSequence,
|
|
716
|
+
attemptId: frame.attempt.attemptId,
|
|
717
|
+
attemptLegacyVersion: before.legacyVersion,
|
|
718
|
+
policyEpochsJson: JSON.stringify(policyEpochs),
|
|
719
|
+
modernWrite: false,
|
|
720
|
+
updatedAt: Date.now(),
|
|
721
|
+
})
|
|
722
|
+
.where(eq(schema.hostProjectEnvAuthority.id, "host"))
|
|
723
|
+
.run();
|
|
724
|
+
return {
|
|
725
|
+
appliedThrough: {
|
|
726
|
+
streamId: frame.attempt.streamId,
|
|
727
|
+
sequence: streamChanged ? 0 : before.appliedSequence,
|
|
728
|
+
},
|
|
729
|
+
activatedNow: !before.activated,
|
|
730
|
+
foldedLegacyKeys,
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
for (const dirty of result.foldedLegacyKeys) {
|
|
734
|
+
noteProjectEnvValueMutation(dirty.projectId);
|
|
735
|
+
mirrorProjectEnvDeletionToStore(dirty.projectId, dirty.key);
|
|
736
|
+
}
|
|
737
|
+
noteProjectEnvAuthorityChange();
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function headEqual(
|
|
742
|
+
current: ProjectEnvBindingState | null,
|
|
743
|
+
expected: ProjectEnvHead | null,
|
|
744
|
+
): boolean {
|
|
745
|
+
if (current === null || expected === null) return current === null && expected === null;
|
|
746
|
+
return sameProjectEnvHead(current.head, expected);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function stateFor(
|
|
750
|
+
tx: ProjectEnvTransaction,
|
|
751
|
+
projectId: string,
|
|
752
|
+
key: string,
|
|
753
|
+
): ProjectEnvBindingState | null {
|
|
754
|
+
const row = storedRecord(tx, projectId, key);
|
|
755
|
+
return row ? rowState(row) : null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function receiptResult(
|
|
759
|
+
receipt: ProjectEnvReceiptRow,
|
|
760
|
+
cursor: ProjectEnvDeliveryCursor,
|
|
761
|
+
): ProjectEnvDeliveryApplyResult {
|
|
762
|
+
if (receipt.ok) {
|
|
763
|
+
let state: ProjectEnvBindingState | null = null;
|
|
764
|
+
if (receipt.stateJson !== null) {
|
|
765
|
+
try {
|
|
766
|
+
const parsed: unknown = JSON.parse(receipt.stateJson);
|
|
767
|
+
if (!isProjectEnvBindingState(parsed)) throw new Error("invalid");
|
|
768
|
+
state = parsed;
|
|
769
|
+
} catch {
|
|
770
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
771
|
+
"stored project environment delivery outcome is invalid",
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
ok: true,
|
|
777
|
+
replayed: true,
|
|
778
|
+
appliedThrough: cursor,
|
|
779
|
+
state,
|
|
780
|
+
recordVersion: receipt.recordVersion,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (receipt.code === null) {
|
|
784
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
785
|
+
"stored project environment refusal has no code",
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
ok: false,
|
|
790
|
+
replayed: true,
|
|
791
|
+
consumed: true,
|
|
792
|
+
code: receipt.code,
|
|
793
|
+
appliedThrough: cursor,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function transientResult(
|
|
798
|
+
row: ProjectEnvAuthorityRow,
|
|
799
|
+
code: ProjectEnvWithholdReason,
|
|
800
|
+
): ProjectEnvDeliveryApplyResult {
|
|
801
|
+
const cursor = cursorFor(row);
|
|
802
|
+
if (!cursor) {
|
|
803
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
804
|
+
"project environment delivery arrived before activation",
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
ok: false,
|
|
809
|
+
replayed: false,
|
|
810
|
+
consumed: false,
|
|
811
|
+
code,
|
|
812
|
+
appliedThrough: cursor,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function saveReceipt(
|
|
817
|
+
tx: ProjectEnvTransaction,
|
|
818
|
+
frame: ProjectEnvOwnedDeliveryFrame,
|
|
819
|
+
outcome:
|
|
820
|
+
| {
|
|
821
|
+
ok: true;
|
|
822
|
+
state: ProjectEnvBindingState | null;
|
|
823
|
+
recordVersion: number | null;
|
|
824
|
+
}
|
|
825
|
+
| { ok: false; code: ProjectEnvWithholdReason },
|
|
826
|
+
): void {
|
|
827
|
+
tx.insert(schema.hostProjectEnvDeliveryReceipts)
|
|
828
|
+
.values({
|
|
829
|
+
streamId: frame.delivery.streamId,
|
|
830
|
+
sequence: frame.delivery.sequence,
|
|
831
|
+
requestKind: frame.kind,
|
|
832
|
+
requestMac: requestMac(frame),
|
|
833
|
+
ok: outcome.ok,
|
|
834
|
+
stateJson:
|
|
835
|
+
outcome.ok && outcome.state !== null
|
|
836
|
+
? JSON.stringify(outcome.state)
|
|
837
|
+
: null,
|
|
838
|
+
recordVersion: outcome.ok ? outcome.recordVersion : null,
|
|
839
|
+
code: outcome.ok ? null : outcome.code,
|
|
840
|
+
createdAt: Date.now(),
|
|
841
|
+
})
|
|
842
|
+
.run();
|
|
843
|
+
tx.update(schema.hostProjectEnvAuthority)
|
|
844
|
+
.set({ appliedSequence: frame.delivery.sequence, updatedAt: Date.now() })
|
|
845
|
+
.where(eq(schema.hostProjectEnvAuthority.id, "host"))
|
|
846
|
+
.run();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function terminalRefusal(
|
|
850
|
+
tx: ProjectEnvTransaction,
|
|
851
|
+
frame: ProjectEnvOwnedDeliveryFrame,
|
|
852
|
+
code: ProjectEnvWithholdReason,
|
|
853
|
+
): ProjectEnvDeliveryApplyResult {
|
|
854
|
+
saveReceipt(tx, frame, { ok: false, code });
|
|
855
|
+
return {
|
|
856
|
+
ok: false,
|
|
857
|
+
replayed: false,
|
|
858
|
+
consumed: true,
|
|
859
|
+
code,
|
|
860
|
+
appliedThrough: frame.delivery,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function applySet(
|
|
865
|
+
tx: ProjectEnvTransaction,
|
|
866
|
+
frame: ModernProjectEnvSetFrame,
|
|
867
|
+
policyEpochs: readonly number[],
|
|
868
|
+
): { state: ProjectEnvBindingState; recordVersion: number } | string {
|
|
869
|
+
const current = stateFor(tx, frame.projectId, frame.key);
|
|
870
|
+
if (!headEqual(current, frame.expectedHead)) return "head_mismatch";
|
|
871
|
+
if (!policyEpochs.includes(frame.policyEpoch)) return "policy_epoch_unknown";
|
|
872
|
+
if (current && frame.listingGeneration < current.listingGeneration) {
|
|
873
|
+
return "listing_generation_stale";
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
let state: ProjectEnvBindingState;
|
|
877
|
+
if (current === null) {
|
|
878
|
+
state = {
|
|
879
|
+
key: frame.key,
|
|
880
|
+
head: {
|
|
881
|
+
revision: 1,
|
|
882
|
+
generation: 1,
|
|
883
|
+
tombstone: false,
|
|
884
|
+
valueRef: { kind: "row" },
|
|
885
|
+
},
|
|
886
|
+
binding: frame.initialBinding ?? null,
|
|
887
|
+
policyEpoch: frame.policyEpoch,
|
|
888
|
+
listingGeneration: frame.listingGeneration,
|
|
889
|
+
};
|
|
890
|
+
} else if (current.head.generation === 0) {
|
|
891
|
+
state = {
|
|
892
|
+
key: frame.key,
|
|
893
|
+
head: {
|
|
894
|
+
revision: 2,
|
|
895
|
+
generation: 1,
|
|
896
|
+
tombstone: false,
|
|
897
|
+
valueRef: { kind: "row" },
|
|
898
|
+
},
|
|
899
|
+
binding: frame.initialBinding ?? null,
|
|
900
|
+
policyEpoch: frame.policyEpoch,
|
|
901
|
+
listingGeneration: frame.listingGeneration,
|
|
902
|
+
};
|
|
903
|
+
} else {
|
|
904
|
+
if (frame.policyEpoch !== current.policyEpoch) return "policy_epoch_mismatch";
|
|
905
|
+
state = {
|
|
906
|
+
...current,
|
|
907
|
+
head: current.head.tombstone
|
|
908
|
+
? {
|
|
909
|
+
revision: current.head.revision + 1,
|
|
910
|
+
generation: current.head.generation + 1,
|
|
911
|
+
tombstone: false,
|
|
912
|
+
valueRef: { kind: "row" },
|
|
913
|
+
}
|
|
914
|
+
: {
|
|
915
|
+
revision: current.head.revision,
|
|
916
|
+
generation: current.head.generation + 1,
|
|
917
|
+
tombstone: false,
|
|
918
|
+
valueRef: { kind: "row" },
|
|
919
|
+
},
|
|
920
|
+
listingGeneration: frame.listingGeneration,
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
putState(tx, frame.projectId, state);
|
|
924
|
+
tx.insert(schema.hostProjectEnv)
|
|
925
|
+
.values({
|
|
926
|
+
projectId: frame.projectId,
|
|
927
|
+
key: frame.key,
|
|
928
|
+
valueEnc: sealProjectEnvValueForStorage(frame.value),
|
|
929
|
+
updatedAt: Date.now(),
|
|
930
|
+
...(frame.orgId ? { orgId: frame.orgId } : {}),
|
|
931
|
+
})
|
|
932
|
+
.onConflictDoUpdate({
|
|
933
|
+
target: [schema.hostProjectEnv.projectId, schema.hostProjectEnv.key],
|
|
934
|
+
set: {
|
|
935
|
+
valueEnc: sealProjectEnvValueForStorage(frame.value),
|
|
936
|
+
updatedAt: Date.now(),
|
|
937
|
+
...(frame.orgId ? { orgId: frame.orgId } : {}),
|
|
938
|
+
},
|
|
939
|
+
})
|
|
940
|
+
.run();
|
|
941
|
+
return { state, recordVersion: bumpProjectRecordVersion(tx, frame.projectId) };
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function applyDelete(
|
|
945
|
+
tx: ProjectEnvTransaction,
|
|
946
|
+
frame: ModernProjectEnvDeleteFrame,
|
|
947
|
+
policyEpochs: readonly number[],
|
|
948
|
+
): { state: ProjectEnvBindingState; recordVersion: number } | string {
|
|
949
|
+
const current = stateFor(tx, frame.projectId, frame.key);
|
|
950
|
+
if (!headEqual(current, frame.expectedHead) || current?.head.tombstone !== false) {
|
|
951
|
+
return "head_mismatch";
|
|
952
|
+
}
|
|
953
|
+
if (
|
|
954
|
+
!policyEpochs.includes(frame.policyEpoch) ||
|
|
955
|
+
frame.policyEpoch !== current.policyEpoch
|
|
956
|
+
) {
|
|
957
|
+
return "policy_epoch_unknown";
|
|
958
|
+
}
|
|
959
|
+
if (frame.listingGeneration < current.listingGeneration) {
|
|
960
|
+
return "listing_generation_stale";
|
|
961
|
+
}
|
|
962
|
+
const state: ProjectEnvBindingState = {
|
|
963
|
+
...current,
|
|
964
|
+
head: {
|
|
965
|
+
revision: current.head.revision + 1,
|
|
966
|
+
generation: current.head.generation + 1,
|
|
967
|
+
tombstone: true,
|
|
968
|
+
valueRef: null,
|
|
969
|
+
},
|
|
970
|
+
listingGeneration: frame.listingGeneration,
|
|
971
|
+
};
|
|
972
|
+
putState(tx, frame.projectId, state);
|
|
973
|
+
tx.delete(schema.hostProjectEnv)
|
|
974
|
+
.where(
|
|
975
|
+
and(
|
|
976
|
+
eq(schema.hostProjectEnv.projectId, frame.projectId),
|
|
977
|
+
eq(schema.hostProjectEnv.key, frame.key),
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
.run();
|
|
981
|
+
return { state, recordVersion: bumpProjectRecordVersion(tx, frame.projectId) };
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function applyRemove(
|
|
985
|
+
tx: ProjectEnvTransaction,
|
|
986
|
+
frame: ProjectEnvRemoveFrame,
|
|
987
|
+
): { state: ProjectEnvBindingState; recordVersion: number } | string {
|
|
988
|
+
const current = stateFor(tx, frame.projectId, frame.key);
|
|
989
|
+
if (current && frame.listingGeneration < current.listingGeneration) {
|
|
990
|
+
return "listing_generation_stale";
|
|
991
|
+
}
|
|
992
|
+
let state: ProjectEnvBindingState;
|
|
993
|
+
if (current === null) {
|
|
994
|
+
state = {
|
|
995
|
+
key: frame.key,
|
|
996
|
+
head: {
|
|
997
|
+
revision: 1,
|
|
998
|
+
generation: 0,
|
|
999
|
+
tombstone: true,
|
|
1000
|
+
valueRef: null,
|
|
1001
|
+
},
|
|
1002
|
+
binding: null,
|
|
1003
|
+
policyEpoch: null,
|
|
1004
|
+
listingGeneration: frame.listingGeneration,
|
|
1005
|
+
};
|
|
1006
|
+
} else if (!current.head.tombstone) {
|
|
1007
|
+
state = {
|
|
1008
|
+
...current,
|
|
1009
|
+
head: {
|
|
1010
|
+
revision: current.head.revision + 1,
|
|
1011
|
+
generation: current.head.generation + 1,
|
|
1012
|
+
tombstone: true,
|
|
1013
|
+
valueRef: null,
|
|
1014
|
+
},
|
|
1015
|
+
listingGeneration: frame.listingGeneration,
|
|
1016
|
+
};
|
|
1017
|
+
} else {
|
|
1018
|
+
state = { ...current, listingGeneration: frame.listingGeneration };
|
|
1019
|
+
}
|
|
1020
|
+
const changed = exactJson(state) !== exactJson(current);
|
|
1021
|
+
if (changed) putState(tx, frame.projectId, state);
|
|
1022
|
+
tx.delete(schema.hostProjectEnv)
|
|
1023
|
+
.where(
|
|
1024
|
+
and(
|
|
1025
|
+
eq(schema.hostProjectEnv.projectId, frame.projectId),
|
|
1026
|
+
eq(schema.hostProjectEnv.key, frame.key),
|
|
1027
|
+
),
|
|
1028
|
+
)
|
|
1029
|
+
.run();
|
|
1030
|
+
return {
|
|
1031
|
+
state,
|
|
1032
|
+
recordVersion: changed
|
|
1033
|
+
? bumpProjectRecordVersion(tx, frame.projectId)
|
|
1034
|
+
: projectRecordVersion(tx, frame.projectId),
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function applyBinding(
|
|
1039
|
+
tx: ProjectEnvTransaction,
|
|
1040
|
+
frame: ProjectEnvBindingSetFrame,
|
|
1041
|
+
policyEpochs: readonly number[],
|
|
1042
|
+
): { state: ProjectEnvBindingState; recordVersion: number } | string {
|
|
1043
|
+
const current = stateFor(tx, frame.projectId, frame.key);
|
|
1044
|
+
if (!headEqual(current, frame.expectedHead) || current?.head.tombstone !== false) {
|
|
1045
|
+
return "head_mismatch";
|
|
1046
|
+
}
|
|
1047
|
+
if (!policyEpochs.includes(frame.policyEpoch)) return "policy_epoch_unknown";
|
|
1048
|
+
if (frame.listingGeneration < current.listingGeneration) {
|
|
1049
|
+
return "listing_generation_stale";
|
|
1050
|
+
}
|
|
1051
|
+
const state: ProjectEnvBindingState = {
|
|
1052
|
+
...current,
|
|
1053
|
+
head: { ...current.head, revision: current.head.revision + 1 },
|
|
1054
|
+
binding: frame.binding,
|
|
1055
|
+
policyEpoch: frame.policyEpoch,
|
|
1056
|
+
listingGeneration: frame.listingGeneration,
|
|
1057
|
+
};
|
|
1058
|
+
putState(tx, frame.projectId, state);
|
|
1059
|
+
return { state, recordVersion: bumpProjectRecordVersion(tx, frame.projectId) };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function validOwnedDeliveryFrame(
|
|
1063
|
+
value: unknown,
|
|
1064
|
+
): value is ProjectEnvOwnedDeliveryFrame {
|
|
1065
|
+
if (isProjectEnvSetFrame(value)) {
|
|
1066
|
+
return "delivery" in value && value.delivery !== undefined;
|
|
1067
|
+
}
|
|
1068
|
+
if (isProjectEnvDeleteFrame(value)) {
|
|
1069
|
+
return "delivery" in value && value.delivery !== undefined;
|
|
1070
|
+
}
|
|
1071
|
+
return (
|
|
1072
|
+
isProjectEnvRemoveFrame(value) ||
|
|
1073
|
+
isProjectEnvBindingSetFrame(value) ||
|
|
1074
|
+
isProjectEnvDeliverySkipFrame(value)
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Apply exactly the next delivery, or replay its value-free receipt. Terminal
|
|
1080
|
+
* CAS/policy refusals consume their sequence. Gaps, stale attempts and stream
|
|
1081
|
+
* mismatches do not. Different content at an already-consumed sequence is
|
|
1082
|
+
* refused; the special late skip keeps the original operation's outcome.
|
|
1083
|
+
*/
|
|
1084
|
+
export function applyProjectEnvDelivery(
|
|
1085
|
+
frame: ProjectEnvOwnedDeliveryFrame,
|
|
1086
|
+
): ProjectEnvDeliveryApplyResult {
|
|
1087
|
+
if (!validOwnedDeliveryFrame(frame)) {
|
|
1088
|
+
throw new Error("invalid modern project environment delivery");
|
|
1089
|
+
}
|
|
1090
|
+
const result = getDb().transaction((tx) => {
|
|
1091
|
+
const authority = authorityRow(tx);
|
|
1092
|
+
validateAuthorityRow(authority);
|
|
1093
|
+
if (!authority.activated) return transientResult(authority, "not_activated");
|
|
1094
|
+
const current = cursorFor(authority)!;
|
|
1095
|
+
if (frame.delivery.streamId !== current.streamId) {
|
|
1096
|
+
return transientResult(authority, "stream_mismatch");
|
|
1097
|
+
}
|
|
1098
|
+
if (frame.delivery.sequence <= current.sequence) {
|
|
1099
|
+
const receipt = tx
|
|
1100
|
+
.select()
|
|
1101
|
+
.from(schema.hostProjectEnvDeliveryReceipts)
|
|
1102
|
+
.where(
|
|
1103
|
+
and(
|
|
1104
|
+
eq(
|
|
1105
|
+
schema.hostProjectEnvDeliveryReceipts.streamId,
|
|
1106
|
+
frame.delivery.streamId,
|
|
1107
|
+
),
|
|
1108
|
+
eq(
|
|
1109
|
+
schema.hostProjectEnvDeliveryReceipts.sequence,
|
|
1110
|
+
frame.delivery.sequence,
|
|
1111
|
+
),
|
|
1112
|
+
),
|
|
1113
|
+
)
|
|
1114
|
+
.get();
|
|
1115
|
+
if (!receipt) return transientResult(authority, "receipt_missing");
|
|
1116
|
+
if (frame.kind === "env.delivery.skip") {
|
|
1117
|
+
return receiptResult(receipt, current);
|
|
1118
|
+
}
|
|
1119
|
+
if (receipt.requestKind === "env.delivery.skip") {
|
|
1120
|
+
return {
|
|
1121
|
+
ok: false,
|
|
1122
|
+
replayed: true,
|
|
1123
|
+
consumed: true,
|
|
1124
|
+
code: "delivery_skipped",
|
|
1125
|
+
appliedThrough: current,
|
|
1126
|
+
} as const;
|
|
1127
|
+
}
|
|
1128
|
+
if (
|
|
1129
|
+
receipt.requestKind !== frame.kind ||
|
|
1130
|
+
!sameMac(receipt.requestMac, requestMac(frame))
|
|
1131
|
+
) {
|
|
1132
|
+
return {
|
|
1133
|
+
ok: false,
|
|
1134
|
+
replayed: true,
|
|
1135
|
+
consumed: true,
|
|
1136
|
+
code: "delivery_conflict",
|
|
1137
|
+
appliedThrough: current,
|
|
1138
|
+
} as const;
|
|
1139
|
+
}
|
|
1140
|
+
return receiptResult(receipt, current);
|
|
1141
|
+
}
|
|
1142
|
+
if (frame.delivery.sequence !== current.sequence + 1) {
|
|
1143
|
+
return transientResult(authority, "delivery_gap");
|
|
1144
|
+
}
|
|
1145
|
+
if (
|
|
1146
|
+
authority.gateOpen
|
|
1147
|
+
? frame.reconciliationAttemptId !== undefined
|
|
1148
|
+
: authority.attemptId === null ||
|
|
1149
|
+
frame.reconciliationAttemptId !== authority.attemptId
|
|
1150
|
+
) {
|
|
1151
|
+
return transientResult(authority, "reconciliation_attempt_mismatch");
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
setModernWrite(tx, true);
|
|
1155
|
+
const policyEpochs = parsePolicyEpochs(authority);
|
|
1156
|
+
let applied:
|
|
1157
|
+
| { state: ProjectEnvBindingState; recordVersion: number }
|
|
1158
|
+
| string
|
|
1159
|
+
| null;
|
|
1160
|
+
switch (frame.kind) {
|
|
1161
|
+
case "env.var.set":
|
|
1162
|
+
applied = applySet(tx, frame, policyEpochs);
|
|
1163
|
+
break;
|
|
1164
|
+
case "env.var.delete":
|
|
1165
|
+
applied = applyDelete(tx, frame, policyEpochs);
|
|
1166
|
+
break;
|
|
1167
|
+
case "env.var.remove":
|
|
1168
|
+
applied = applyRemove(tx, frame);
|
|
1169
|
+
break;
|
|
1170
|
+
case "env.binding.set":
|
|
1171
|
+
applied = applyBinding(tx, frame, policyEpochs);
|
|
1172
|
+
break;
|
|
1173
|
+
case "env.delivery.skip":
|
|
1174
|
+
applied = null;
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
if (typeof applied === "string") {
|
|
1178
|
+
setModernWrite(tx, false);
|
|
1179
|
+
return terminalRefusal(tx, frame, applied);
|
|
1180
|
+
}
|
|
1181
|
+
setModernWrite(tx, false);
|
|
1182
|
+
saveReceipt(tx, frame, {
|
|
1183
|
+
ok: true,
|
|
1184
|
+
state: applied?.state ?? null,
|
|
1185
|
+
recordVersion: applied?.recordVersion ?? null,
|
|
1186
|
+
});
|
|
1187
|
+
return {
|
|
1188
|
+
ok: true,
|
|
1189
|
+
replayed: false,
|
|
1190
|
+
appliedThrough: frame.delivery,
|
|
1191
|
+
state: applied?.state ?? null,
|
|
1192
|
+
recordVersion: applied?.recordVersion ?? null,
|
|
1193
|
+
} as const;
|
|
1194
|
+
});
|
|
1195
|
+
if (result.ok && !result.replayed) noteProjectEnvAuthorityChange();
|
|
1196
|
+
if (result.ok && !result.replayed && frame.kind === "env.var.set") {
|
|
1197
|
+
noteProjectEnvValueMutation(frame.projectId);
|
|
1198
|
+
mirrorProjectEnvToStore(frame.projectId, frame.key);
|
|
1199
|
+
} else if (
|
|
1200
|
+
result.ok &&
|
|
1201
|
+
!result.replayed &&
|
|
1202
|
+
(frame.kind === "env.var.delete" || frame.kind === "env.var.remove")
|
|
1203
|
+
) {
|
|
1204
|
+
noteProjectEnvValueMutation(frame.projectId);
|
|
1205
|
+
mirrorProjectEnvDeletionToStore(frame.projectId, frame.key);
|
|
1206
|
+
}
|
|
1207
|
+
return result;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
export const applyProjectEnvSet = (frame: ModernProjectEnvSetFrame) =>
|
|
1211
|
+
applyProjectEnvDelivery(frame);
|
|
1212
|
+
export const applyProjectEnvDelete = (frame: ModernProjectEnvDeleteFrame) =>
|
|
1213
|
+
applyProjectEnvDelivery(frame);
|
|
1214
|
+
export const applyProjectEnvRemove = (frame: ProjectEnvRemoveFrame) =>
|
|
1215
|
+
applyProjectEnvDelivery(frame);
|
|
1216
|
+
export const applyProjectEnvBinding = (frame: ProjectEnvBindingSetFrame) =>
|
|
1217
|
+
applyProjectEnvDelivery(frame);
|
|
1218
|
+
export const applyProjectEnvSkip = (frame: ProjectEnvDeliverySkipFrame) =>
|
|
1219
|
+
applyProjectEnvDelivery(frame);
|
|
1220
|
+
|
|
1221
|
+
/** Open the gate only for the exact current attempt, stream, cursor and record versions. */
|
|
1222
|
+
export function completeProjectEnvReconciliation(
|
|
1223
|
+
frame: ProjectEnvReconciledFrame,
|
|
1224
|
+
): ProjectEnvReconciliationCompleteResult {
|
|
1225
|
+
if (!isProjectEnvReconciledFrame(frame)) {
|
|
1226
|
+
throw new Error("invalid project environment reconciliation marker");
|
|
1227
|
+
}
|
|
1228
|
+
const result = getDb().transaction((tx): ProjectEnvReconciliationCompleteResult => {
|
|
1229
|
+
const authority = authorityRow(tx);
|
|
1230
|
+
validateAuthorityRow(authority);
|
|
1231
|
+
const cursor = cursorFor(authority);
|
|
1232
|
+
if (!cursor) {
|
|
1233
|
+
throw new ProjectEnvAuthorityCorruptError(
|
|
1234
|
+
"project environment reconciliation completed before activation",
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const refuse = (code: string): ProjectEnvReconciliationCompleteResult => ({
|
|
1238
|
+
ok: false,
|
|
1239
|
+
code,
|
|
1240
|
+
appliedThrough: cursor,
|
|
1241
|
+
});
|
|
1242
|
+
if (authority.attemptId !== frame.attemptId) {
|
|
1243
|
+
return refuse("reconciliation_attempt_mismatch");
|
|
1244
|
+
}
|
|
1245
|
+
if (
|
|
1246
|
+
frame.highWater.streamId !== cursor.streamId ||
|
|
1247
|
+
frame.highWater.sequence > cursor.sequence
|
|
1248
|
+
) {
|
|
1249
|
+
return refuse("delivery_incomplete");
|
|
1250
|
+
}
|
|
1251
|
+
if (authority.attemptLegacyVersion !== authority.legacyVersion) {
|
|
1252
|
+
return refuse("legacy_write_detected");
|
|
1253
|
+
}
|
|
1254
|
+
for (const version of frame.versions) {
|
|
1255
|
+
if (projectRecordVersion(tx, version.projectId) !== version.recordVersion) {
|
|
1256
|
+
return refuse("record_version_changed");
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
tx.update(schema.hostProjectEnvAuthority)
|
|
1260
|
+
.set({
|
|
1261
|
+
gateOpen: true,
|
|
1262
|
+
attemptId: null,
|
|
1263
|
+
attemptLegacyVersion: null,
|
|
1264
|
+
updatedAt: Date.now(),
|
|
1265
|
+
})
|
|
1266
|
+
.where(eq(schema.hostProjectEnvAuthority.id, "host"))
|
|
1267
|
+
.run();
|
|
1268
|
+
return { ok: true, appliedThrough: cursor };
|
|
1269
|
+
});
|
|
1270
|
+
if (result.ok) noteProjectEnvAuthorityChange();
|
|
1271
|
+
return result;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/** Explicit fail-closed transition for transport loss or storage diagnostics. */
|
|
1275
|
+
export function closeProjectEnvAuthorityGate(): void {
|
|
1276
|
+
getDb()
|
|
1277
|
+
.update(schema.hostProjectEnvAuthority)
|
|
1278
|
+
.set({ gateOpen: false, updatedAt: Date.now() })
|
|
1279
|
+
.where(eq(schema.hostProjectEnvAuthority.id, "host"))
|
|
1280
|
+
.run();
|
|
1281
|
+
noteProjectEnvAuthorityChange();
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/** Value-free inspection used by tests and crash recovery diagnostics. */
|
|
1285
|
+
export function listProjectEnvDeliveryReceipts(): Array<{
|
|
1286
|
+
streamId: string;
|
|
1287
|
+
sequence: number;
|
|
1288
|
+
requestKind: string;
|
|
1289
|
+
requestMac: string;
|
|
1290
|
+
ok: boolean;
|
|
1291
|
+
state: ProjectEnvBindingState | null;
|
|
1292
|
+
recordVersion: number | null;
|
|
1293
|
+
code: string | null;
|
|
1294
|
+
}> {
|
|
1295
|
+
return getDb()
|
|
1296
|
+
.select()
|
|
1297
|
+
.from(schema.hostProjectEnvDeliveryReceipts)
|
|
1298
|
+
.orderBy(
|
|
1299
|
+
asc(schema.hostProjectEnvDeliveryReceipts.streamId),
|
|
1300
|
+
asc(schema.hostProjectEnvDeliveryReceipts.sequence),
|
|
1301
|
+
)
|
|
1302
|
+
.all()
|
|
1303
|
+
.map((row) => {
|
|
1304
|
+
const result = receiptResult(row, {
|
|
1305
|
+
streamId: row.streamId,
|
|
1306
|
+
sequence: row.sequence,
|
|
1307
|
+
});
|
|
1308
|
+
return {
|
|
1309
|
+
streamId: row.streamId,
|
|
1310
|
+
sequence: row.sequence,
|
|
1311
|
+
requestKind: row.requestKind,
|
|
1312
|
+
requestMac: row.requestMac,
|
|
1313
|
+
ok: result.ok,
|
|
1314
|
+
state: result.ok ? result.state : null,
|
|
1315
|
+
recordVersion: result.ok ? result.recordVersion : null,
|
|
1316
|
+
code: result.ok ? null : result.code,
|
|
1317
|
+
};
|
|
1318
|
+
});
|
|
1319
|
+
}
|