@tiangong-lca/cli 0.0.28 → 0.0.30

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.
Files changed (38) hide show
  1. package/README.md +90 -2
  2. package/dist/src/cli.js +553 -4
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/lib/dataset-command.js +11 -0
  5. package/dist/src/lib/dataset-command.js.map +1 -1
  6. package/dist/src/lib/dataset-maintenance-contract.js.map +1 -1
  7. package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js +175 -0
  8. package/dist/src/lib/dataset-maintenance-flow-identity-approval-claim.js.map +1 -0
  9. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js +511 -0
  10. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js.map +1 -0
  11. package/dist/src/lib/dataset-maintenance-flow-identity-command.js +26 -0
  12. package/dist/src/lib/dataset-maintenance-flow-identity-command.js.map +1 -0
  13. package/dist/src/lib/dataset-maintenance-flow-identity-contract.js +784 -0
  14. package/dist/src/lib/dataset-maintenance-flow-identity-contract.js.map +1 -0
  15. package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js +1317 -0
  16. package/dist/src/lib/dataset-maintenance-flow-identity-execution-contract.js.map +1 -0
  17. package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js +342 -0
  18. package/dist/src/lib/dataset-maintenance-flow-identity-freeze.js.map +1 -0
  19. package/dist/src/lib/dataset-maintenance-flow-identity-plan.js +900 -0
  20. package/dist/src/lib/dataset-maintenance-flow-identity-plan.js.map +1 -0
  21. package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js +688 -0
  22. package/dist/src/lib/dataset-maintenance-flow-identity-recovery.js.map +1 -0
  23. package/dist/src/lib/dataset-maintenance-flow-identity-run.js +1369 -0
  24. package/dist/src/lib/dataset-maintenance-flow-identity-run.js.map +1 -0
  25. package/dist/src/lib/dataset-maintenance-flow-identity-seal.js +144 -0
  26. package/dist/src/lib/dataset-maintenance-flow-identity-seal.js.map +1 -0
  27. package/dist/src/lib/dataset-maintenance-flow-identity-verify.js +377 -0
  28. package/dist/src/lib/dataset-maintenance-flow-identity-verify.js.map +1 -0
  29. package/dist/src/lib/dataset-maintenance-flow-identity-wire.js +178 -0
  30. package/dist/src/lib/dataset-maintenance-flow-identity-wire.js.map +1 -0
  31. package/dist/src/lib/dataset-maintenance-remote.js +156 -10
  32. package/dist/src/lib/dataset-maintenance-remote.js.map +1 -1
  33. package/dist/src/lib/dataset-save-draft-run.js +667 -0
  34. package/dist/src/lib/dataset-save-draft-run.js.map +1 -1
  35. package/dist/src/lib/http.js.map +1 -1
  36. package/dist/src/lib/lca-release.js +683 -0
  37. package/dist/src/lib/lca-release.js.map +1 -0
  38. package/package.json +1 -1
@@ -0,0 +1,688 @@
1
+ import path from 'node:path';
2
+ import { materializePrivateArtifactDirectoryAtomically, readProtectedJsonArtifact, readProtectedTextArtifact, writePrivateImmutableJson, writePrivateImmutableText, } from './dataset-maintenance-protected-artifacts.js';
3
+ import { buildFlowIdentityExecutionIdentity, buildFlowIdentityScopeLookupRequest, parseFlowIdentityScopeLookupProof, parseFlowIdentityScopePreflightProof, parseFlowIdentityScopeStatus, prepareFlowIdentityExecution, } from './dataset-maintenance-flow-identity-execution-contract.js';
4
+ import { isJsonObject, sha256Json, sha256Text, stableJsonText, } from './dataset-maintenance-contract.js';
5
+ import { flowIdentityRestrictedSha256 } from './dataset-maintenance-flow-identity-wire.js';
6
+ import { isMaintenanceRpcDomainFailure, lookupMaintenanceFlowIdentityScope, readMaintenanceFlowIdentityScope, resolveMaintenanceRemoteContext, } from './dataset-maintenance-remote.js';
7
+ import { parseProtectedToolchainEvidence } from './dataset-maintenance-protected-toolchain.js';
8
+ import { CliError } from './errors.js';
9
+ const HASH = /^[a-f0-9]{64}$/u;
10
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u;
11
+ const USER_STATE_CLAIM = 'authenticated_actor_state_100_plus_own_state_0';
12
+ const FLOW_IDENTITY_RECOVERY_REASONS = [
13
+ 'wrapper_exited_without_permit',
14
+ 'process_response_ambiguous',
15
+ 'process_domain_rejected',
16
+ 'finalize_response_ambiguous',
17
+ 'derivatives_became_ready_after_wrapper_exit',
18
+ ];
19
+ export const FLOW_IDENTITY_RECOVERY_FREEZE_ARTIFACTS = {
20
+ freeze: 'flow-identity-recovery-freeze.json',
21
+ scope_proof: 'flow-identity-recovery-scope-proof.json',
22
+ approval_request: 'flow-identity-recovery-approval-request.json',
23
+ approval_text: 'flow-identity-recovery-approval-request.txt',
24
+ report: 'flow-identity-recovery-freeze-report.json',
25
+ };
26
+ export const FLOW_IDENTITY_RECOVERY_APPROVAL_ARTIFACTS = {
27
+ human_approval: 'flow-identity-recovery-human-approval.txt',
28
+ approval: 'flow-identity-recovery-approval.json',
29
+ report: 'flow-identity-recovery-approval-seal-report.json',
30
+ };
31
+ function fail(message, code = 'DATASET_FLOW_IDENTITY_RECOVERY_INVALID') {
32
+ throw new CliError(message, { code, exitCode: 1 });
33
+ }
34
+ function canonicalTimestamp(value, label) {
35
+ if (!Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value) {
36
+ fail(`${label} must be a canonical RFC3339 UTC timestamp.`);
37
+ }
38
+ return value;
39
+ }
40
+ function requireHash(value, label) {
41
+ if (typeof value !== 'string' || !HASH.test(value))
42
+ fail(`${label} must be a SHA-256.`);
43
+ return value;
44
+ }
45
+ function requireUuid(value, label) {
46
+ if (typeof value !== 'string' || !UUID.test(value))
47
+ fail(`${label} must be a UUID.`);
48
+ return value;
49
+ }
50
+ function assertExactKeys(value, expected, label) {
51
+ const actual = Object.keys(value).sort();
52
+ const required = [...expected].sort();
53
+ if (actual.length !== required.length ||
54
+ actual.some((entry, index) => entry !== required[index])) {
55
+ fail(`${label} has an unexpected wire shape.`);
56
+ }
57
+ }
58
+ function requireCanonicalJson(filePath, label) {
59
+ const artifact = readProtectedJsonArtifact({ filePath, label });
60
+ if (artifact.text !== `${stableJsonText(artifact.value)}\n`) {
61
+ fail(`${label} must be canonical JSON with one trailing newline.`);
62
+ }
63
+ return artifact;
64
+ }
65
+ function parseRecoveryScopeProof(value, plan, identity) {
66
+ if (isJsonObject(value) &&
67
+ value.schema_version === 'dataset-flow-identity-scope-lookup-result.v1') {
68
+ return parseFlowIdentityScopeLookupProof(value, plan, identity);
69
+ }
70
+ return parseFlowIdentityScopePreflightProof(value, plan);
71
+ }
72
+ function readRecoveryScopeProof(options) {
73
+ for (const [name, label] of [
74
+ ['scope-preflight-proof.json', 'Flow identity scope preflight proof'],
75
+ ['scope-lookup-proof.json', 'Flow identity scope lookup proof'],
76
+ ]) {
77
+ const filePath = path.join(path.resolve(options.runDir), name);
78
+ try {
79
+ const artifact = requireCanonicalJson(filePath, label);
80
+ return parseRecoveryScopeProof(artifact.value, options.plan, options.identity);
81
+ }
82
+ catch (error) {
83
+ if (error.code !== 'ENOENT')
84
+ throw error;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ function recoveryBaseline(status) {
90
+ return {
91
+ status: status.status,
92
+ completed_process_count: status.completed_process_count,
93
+ next_ordinal: status.next_ordinal,
94
+ primary_complete: status.primary_complete,
95
+ primary_current: status.primary_current,
96
+ live_guard_current: status.live_guard_current,
97
+ protected_closure_current: status.protected_closure_current,
98
+ derivatives_current: status.derivatives_current,
99
+ whole_scope_proof_sha256: status.whole_scope_proof_sha256,
100
+ };
101
+ }
102
+ function assertRecoveryBaseline(value) {
103
+ if (!isJsonObject(value))
104
+ fail('Recovery baseline is invalid.');
105
+ const keys = [
106
+ 'status',
107
+ 'completed_process_count',
108
+ 'next_ordinal',
109
+ 'primary_complete',
110
+ 'primary_current',
111
+ 'live_guard_current',
112
+ 'protected_closure_current',
113
+ 'derivatives_current',
114
+ 'whole_scope_proof_sha256',
115
+ ];
116
+ if (Object.keys(value).length !== keys.length ||
117
+ keys.some((key) => !(key in value)) ||
118
+ !['sealed', 'running', 'primary_complete', 'derivatives_pending'].includes(String(value.status)) ||
119
+ !Number.isSafeInteger(value.completed_process_count) ||
120
+ Number(value.completed_process_count) < 0 ||
121
+ !Number.isSafeInteger(value.next_ordinal) ||
122
+ Number(value.next_ordinal) < 1 ||
123
+ ![
124
+ value.primary_complete,
125
+ value.primary_current,
126
+ value.live_guard_current,
127
+ value.protected_closure_current,
128
+ value.derivatives_current,
129
+ ].every((entry) => typeof entry === 'boolean') ||
130
+ !HASH.test(String(value.whole_scope_proof_sha256))) {
131
+ fail('Recovery baseline is inconsistent.');
132
+ }
133
+ }
134
+ export function computeFlowIdentityRecoveryFreezeSha256(freeze) {
135
+ return sha256Json({ ...freeze, recovery_freeze_sha256: '' });
136
+ }
137
+ function recoveryRequestCore(value) {
138
+ const core = { ...value };
139
+ delete core.request_sha256;
140
+ return core;
141
+ }
142
+ export function computeFlowIdentityRecoveryApprovalRequestSha256(request) {
143
+ return sha256Json(recoveryRequestCore(request));
144
+ }
145
+ export function computeFlowIdentityRecoveryApprovalIdentitySha256(approval) {
146
+ return sha256Json({ ...approval, recovery_approval_identity_sha256: '' });
147
+ }
148
+ export function renderFlowIdentityRecoveryApprovalText(core, requestSha256) {
149
+ requireHash(requestSha256, 'request_sha256');
150
+ return [
151
+ 'BAFU Step 3 protected owner-draft recovery approval request',
152
+ `schema_version=${core.schema_version}`,
153
+ `approved_at_utc=${core.approved_at_utc}`,
154
+ `request_sha256=${requestSha256}`,
155
+ `environment=${core.environment}`,
156
+ `project_ref=${core.project_ref}`,
157
+ `account_email=${core.actor.email}`,
158
+ `account_user_id=${core.actor.user_id}`,
159
+ `target_visibility=${core.target_visibility}`,
160
+ `user_state_claim=${core.user_state_claim}`,
161
+ `scope_id=${core.scope_id}`,
162
+ `scope_proof_sha256=${core.scope_proof_sha256}`,
163
+ `operation_id=${core.operation_id}`,
164
+ `plan_sha256=${core.plan_sha256}`,
165
+ `original_freeze_sha256=${core.original_freeze_sha256}`,
166
+ `original_execution_request_id=${core.original_execution_request_id}`,
167
+ `original_execution_identity_sha256=${core.original_execution_identity_sha256}`,
168
+ `original_execution_approval_request_sha256=${core.original_execution_approval_request_sha256}`,
169
+ `original_execution_approval_text_sha256=${core.original_execution_approval_text_sha256}`,
170
+ `original_execution_approval_identity_sha256=${core.original_execution_approval_identity_sha256}`,
171
+ `recovery_reason=${core.recovery_reason}`,
172
+ `recovery_mode=${core.recovery_mode}`,
173
+ `baseline_status=${core.baseline.status}`,
174
+ `baseline_completed_process_count=${core.baseline.completed_process_count}`,
175
+ `baseline_next_ordinal=${core.baseline.next_ordinal}`,
176
+ `baseline_primary_complete=${String(core.baseline.primary_complete)}`,
177
+ `baseline_derivatives_current=${String(core.baseline.derivatives_current)}`,
178
+ `baseline_whole_scope_proof_sha256=${core.baseline.whole_scope_proof_sha256}`,
179
+ `recovery_freeze_file_sha256=${core.recovery_freeze_file_sha256}`,
180
+ `recovery_freeze_sha256=${core.recovery_freeze_sha256}`,
181
+ `toolchain_evidence_sha256=${core.toolchain_evidence_sha256}`,
182
+ `approval_reusable=${String(core.approval_reusable)}`,
183
+ `maximum_wrapper_invocations=${core.maximum_wrapper_invocations}`,
184
+ `maximum_cli_apply_spawns=${core.maximum_cli_apply_spawns}`,
185
+ `maximum_process_posts=${core.maximum_process_posts}`,
186
+ `maximum_finalize_posts=${core.maximum_finalize_posts}`,
187
+ `automatic_retry=${String(core.automatic_retry)}`,
188
+ 'Approve only by returning this text byte-for-byte without edits.',
189
+ '',
190
+ ].join('\n');
191
+ }
192
+ export function parseFlowIdentityRecoveryFreeze(value) {
193
+ if (!isJsonObject(value) || !isJsonObject(value.actor))
194
+ fail('Recovery freeze is invalid.');
195
+ const freeze = value;
196
+ assertRecoveryBaseline(freeze.baseline);
197
+ if (freeze.schema_version !== 'dataset-flow-identity-recovery-freeze.v1' ||
198
+ freeze.environment !== 'production' ||
199
+ freeze.target_visibility !== 'owner_draft' ||
200
+ freeze.user_state_claim !== USER_STATE_CLAIM ||
201
+ !UUID.test(freeze.actor.user_id) ||
202
+ freeze.actor.email !== freeze.actor.email.trim().toLowerCase() ||
203
+ !UUID.test(freeze.scope_id) ||
204
+ ![
205
+ freeze.scope_proof_sha256,
206
+ freeze.plan_sha256,
207
+ freeze.original_freeze_sha256,
208
+ freeze.original_execution_identity_sha256,
209
+ freeze.original_execution_approval_request_sha256,
210
+ freeze.original_execution_approval_text_sha256,
211
+ freeze.original_execution_approval_identity_sha256,
212
+ freeze.toolchain_evidence_sha256,
213
+ freeze.recovery_freeze_sha256,
214
+ ].every((entry) => HASH.test(entry)) ||
215
+ !Number.isFinite(Date.parse(freeze.generated_at_utc)) ||
216
+ freeze.approval_reusable !== false ||
217
+ freeze.maximum_wrapper_invocations !== 1 ||
218
+ freeze.maximum_cli_apply_spawns !== 1 ||
219
+ !Number.isSafeInteger(freeze.maximum_process_posts) ||
220
+ freeze.maximum_process_posts < 0 ||
221
+ freeze.maximum_finalize_posts !== 1 ||
222
+ freeze.automatic_retry !== false ||
223
+ !FLOW_IDENTITY_RECOVERY_REASONS.includes(freeze.recovery_reason) ||
224
+ freeze.recovery_mode !==
225
+ (freeze.maximum_process_posts === 0 ? 'finalize_only' : 'resume_and_finalize') ||
226
+ freeze.recovery_freeze_sha256 !== computeFlowIdentityRecoveryFreezeSha256(freeze)) {
227
+ fail('Recovery freeze is inconsistent or tampered.');
228
+ }
229
+ return freeze;
230
+ }
231
+ export function parseFlowIdentityRecoveryApprovalRequest(value) {
232
+ if (!isJsonObject(value) || !isJsonObject(value.actor)) {
233
+ fail('Recovery approval request is invalid.');
234
+ }
235
+ const request = value;
236
+ assertRecoveryBaseline(request.baseline);
237
+ if (request.schema_version !== 'dataset-flow-identity-recovery-approval-request.v1' ||
238
+ request.environment !== 'production' ||
239
+ request.target_visibility !== 'owner_draft' ||
240
+ request.user_state_claim !== USER_STATE_CLAIM ||
241
+ !UUID.test(request.actor.user_id) ||
242
+ !UUID.test(request.scope_id) ||
243
+ ![
244
+ request.scope_proof_sha256,
245
+ request.plan_sha256,
246
+ request.original_freeze_sha256,
247
+ request.original_execution_identity_sha256,
248
+ request.original_execution_approval_request_sha256,
249
+ request.original_execution_approval_text_sha256,
250
+ request.original_execution_approval_identity_sha256,
251
+ request.recovery_freeze_file_sha256,
252
+ request.recovery_freeze_sha256,
253
+ request.toolchain_evidence_sha256,
254
+ request.request_sha256,
255
+ ].every((entry) => HASH.test(entry)) ||
256
+ !canonicalTimestamp(request.approved_at_utc, 'approved_at_utc') ||
257
+ request.approval_reusable !== false ||
258
+ request.maximum_wrapper_invocations !== 1 ||
259
+ request.maximum_cli_apply_spawns !== 1 ||
260
+ !Number.isSafeInteger(request.maximum_process_posts) ||
261
+ request.maximum_process_posts < 0 ||
262
+ request.maximum_finalize_posts !== 1 ||
263
+ request.automatic_retry !== false ||
264
+ !FLOW_IDENTITY_RECOVERY_REASONS.includes(request.recovery_reason) ||
265
+ request.recovery_mode !==
266
+ (request.maximum_process_posts === 0 ? 'finalize_only' : 'resume_and_finalize') ||
267
+ request.request_sha256 !== computeFlowIdentityRecoveryApprovalRequestSha256(request)) {
268
+ fail('Recovery approval request is inconsistent or tampered.');
269
+ }
270
+ return request;
271
+ }
272
+ export function parseFlowIdentityRecoveryApproval(value, freeze) {
273
+ if (!isJsonObject(value) || !isJsonObject(value.actor))
274
+ fail('Recovery approval is invalid.');
275
+ const approval = value;
276
+ if (approval.schema_version !== 'dataset-flow-identity-recovery-approval.v1' ||
277
+ approval.actor.user_id !== freeze.actor.user_id ||
278
+ approval.actor.email !== freeze.actor.email ||
279
+ approval.plan_sha256 !== freeze.plan_sha256 ||
280
+ approval.scope_id !== freeze.scope_id ||
281
+ approval.scope_proof_sha256 !== freeze.scope_proof_sha256 ||
282
+ approval.recovery_freeze_sha256 !== freeze.recovery_freeze_sha256 ||
283
+ approval.toolchain_evidence_sha256 !== freeze.toolchain_evidence_sha256 ||
284
+ ![
285
+ approval.recovery_approval_request_sha256,
286
+ approval.recovery_approval_text_sha256,
287
+ approval.recovery_approval_identity_sha256,
288
+ ].every((entry) => HASH.test(entry)) ||
289
+ !Number.isFinite(Date.parse(approval.approved_at_utc)) ||
290
+ Date.parse(approval.approved_at_utc) < Date.parse(freeze.generated_at_utc) ||
291
+ approval.recovery_approval_identity_sha256 !==
292
+ computeFlowIdentityRecoveryApprovalIdentitySha256(approval)) {
293
+ fail('Recovery approval does not bind the exact recovery freeze.');
294
+ }
295
+ return approval;
296
+ }
297
+ function assertContext(plan, context) {
298
+ if (context.project_ref !== plan.project_ref ||
299
+ context.account.user_id !== plan.account.user_id ||
300
+ context.account.email.trim().toLowerCase() !== plan.account.email) {
301
+ fail('Authenticated production context does not match the recovery plan.');
302
+ }
303
+ }
304
+ function assertRecoverableStatus(status, plan) {
305
+ if (!['sealed', 'running', 'primary_complete', 'derivatives_pending'].includes(status.status) ||
306
+ !status.primary_current ||
307
+ !status.live_guard_current ||
308
+ !status.protected_closure_current ||
309
+ status.compensation_required === true ||
310
+ status.completed_process_count < 0 ||
311
+ status.completed_process_count > plan.processes.length ||
312
+ status.next_ordinal !== Math.min(status.completed_process_count + 1, plan.processes.length + 1)) {
313
+ fail('Live scope is not eligible for an exact continuation recovery approval.');
314
+ }
315
+ }
316
+ export async function freezeFlowIdentityRecovery(options) {
317
+ const planArtifact = requireCanonicalJson(options.planPath, 'Flow identity plan');
318
+ const originalFreezeArtifact = requireCanonicalJson(options.freezePath, 'Flow identity original freeze');
319
+ const originalApprovalArtifact = requireCanonicalJson(options.approvalPath, 'Flow identity original approval');
320
+ const prepared = prepareFlowIdentityExecution({
321
+ plan: planArtifact.value,
322
+ freeze: originalFreezeArtifact.value,
323
+ approval: originalApprovalArtifact.value,
324
+ });
325
+ if (prepared.plan.project_ref !== options.expectedProjectRef ||
326
+ prepared.plan.account.email !== options.confirm) {
327
+ fail('Recovery freeze requires the exact production project and account confirmation.');
328
+ }
329
+ let scope = readRecoveryScopeProof({
330
+ runDir: options.runDir,
331
+ plan: prepared.plan,
332
+ identity: prepared.identity,
333
+ });
334
+ let lookupPerformed = false;
335
+ const toolchainArtifact = requireCanonicalJson(options.toolchainEvidencePath, 'Protected toolchain evidence');
336
+ parseProtectedToolchainEvidence(toolchainArtifact.value, {
337
+ projectRef: options.expectedProjectRef,
338
+ cliVersion: options.cliVersion,
339
+ });
340
+ const generatedAt = (options.now ?? new Date()).toISOString();
341
+ const approvedAt = canonicalTimestamp(options.approvedAtUtc, 'approvedAtUtc');
342
+ if (Date.parse(approvedAt) < Date.parse(generatedAt)) {
343
+ fail('approvedAtUtc cannot precede the recovery freeze.');
344
+ }
345
+ const context = await (options.dependencies?.resolveContext ?? resolveMaintenanceRemoteContext)({
346
+ env: options.env,
347
+ fetchImpl: options.fetchImpl,
348
+ timeoutMs: options.timeoutMs,
349
+ now: options.now,
350
+ });
351
+ assertContext(prepared.plan, context);
352
+ if (scope === null) {
353
+ lookupPerformed = true;
354
+ const lookupRaw = await (options.dependencies?.lookup ?? lookupMaintenanceFlowIdentityScope)({
355
+ context,
356
+ request: buildFlowIdentityScopeLookupRequest({ identity: prepared.identity }),
357
+ });
358
+ if (isMaintenanceRpcDomainFailure(lookupRaw)) {
359
+ fail('Read-only scope lookup could not recover the lost preflight response.');
360
+ }
361
+ scope = parseFlowIdentityScopeLookupProof(lookupRaw, prepared.plan, prepared.identity);
362
+ }
363
+ const raw = await (options.dependencies?.read ?? readMaintenanceFlowIdentityScope)({
364
+ context,
365
+ scopeId: scope.scope_id,
366
+ });
367
+ if (isMaintenanceRpcDomainFailure(raw)) {
368
+ fail('Database rejected the read-only recovery scope snapshot.');
369
+ }
370
+ const status = parseFlowIdentityScopeStatus(raw, prepared.plan, scope.scope_id, scope.scope_proof_sha256);
371
+ assertRecoverableStatus(status, prepared.plan);
372
+ const remainingProcessPosts = prepared.plan.processes.length - status.completed_process_count;
373
+ const freeze = {
374
+ schema_version: 'dataset-flow-identity-recovery-freeze.v1',
375
+ generated_at_utc: generatedAt,
376
+ environment: 'production',
377
+ project_ref: prepared.plan.project_ref,
378
+ actor: prepared.plan.account,
379
+ target_visibility: 'owner_draft',
380
+ user_state_claim: USER_STATE_CLAIM,
381
+ scope_id: scope.scope_id,
382
+ scope_proof_sha256: scope.scope_proof_sha256,
383
+ operation_id: prepared.plan.operation_id,
384
+ plan_sha256: prepared.plan.plan_sha256,
385
+ original_freeze_sha256: prepared.freeze.freeze_sha256,
386
+ original_execution_request_id: prepared.identity.request_id,
387
+ original_execution_identity_sha256: prepared.identity.identity_sha256,
388
+ original_execution_approval_request_sha256: prepared.approval.execution_approval_request_sha256,
389
+ original_execution_approval_text_sha256: prepared.approval.execution_approval_text_sha256,
390
+ original_execution_approval_identity_sha256: prepared.approval.execution_approval_identity_sha256,
391
+ recovery_reason: options.recoveryReason,
392
+ recovery_mode: remainingProcessPosts === 0 ? 'finalize_only' : 'resume_and_finalize',
393
+ baseline: recoveryBaseline(status),
394
+ toolchain_evidence_sha256: toolchainArtifact.file_sha256,
395
+ approval_reusable: false,
396
+ maximum_wrapper_invocations: 1,
397
+ maximum_cli_apply_spawns: 1,
398
+ maximum_process_posts: remainingProcessPosts,
399
+ maximum_finalize_posts: 1,
400
+ automatic_retry: false,
401
+ recovery_freeze_sha256: '',
402
+ };
403
+ freeze.recovery_freeze_sha256 = computeFlowIdentityRecoveryFreezeSha256(freeze);
404
+ const freezeText = `${stableJsonText(freeze)}\n`;
405
+ const freezeFileSha256 = sha256Text(freezeText);
406
+ const core = {
407
+ schema_version: 'dataset-flow-identity-recovery-approval-request.v1',
408
+ approved_at_utc: approvedAt,
409
+ environment: 'production',
410
+ project_ref: freeze.project_ref,
411
+ actor: freeze.actor,
412
+ target_visibility: 'owner_draft',
413
+ user_state_claim: USER_STATE_CLAIM,
414
+ scope_id: freeze.scope_id,
415
+ scope_proof_sha256: freeze.scope_proof_sha256,
416
+ operation_id: freeze.operation_id,
417
+ plan_sha256: freeze.plan_sha256,
418
+ original_freeze_sha256: freeze.original_freeze_sha256,
419
+ original_execution_request_id: freeze.original_execution_request_id,
420
+ original_execution_identity_sha256: freeze.original_execution_identity_sha256,
421
+ original_execution_approval_request_sha256: freeze.original_execution_approval_request_sha256,
422
+ original_execution_approval_text_sha256: freeze.original_execution_approval_text_sha256,
423
+ original_execution_approval_identity_sha256: freeze.original_execution_approval_identity_sha256,
424
+ recovery_reason: freeze.recovery_reason,
425
+ recovery_mode: freeze.recovery_mode,
426
+ baseline: freeze.baseline,
427
+ recovery_freeze_file_sha256: freezeFileSha256,
428
+ recovery_freeze_sha256: freeze.recovery_freeze_sha256,
429
+ toolchain_evidence_sha256: freeze.toolchain_evidence_sha256,
430
+ approval_reusable: false,
431
+ maximum_wrapper_invocations: 1,
432
+ maximum_cli_apply_spawns: 1,
433
+ maximum_process_posts: remainingProcessPosts,
434
+ maximum_finalize_posts: 1,
435
+ automatic_retry: false,
436
+ };
437
+ const request = parseFlowIdentityRecoveryApprovalRequest({
438
+ ...core,
439
+ request_sha256: sha256Json(core),
440
+ });
441
+ const approvalText = renderFlowIdentityRecoveryApprovalText(core, request.request_sha256);
442
+ const approvalTextSha256 = sha256Text(approvalText);
443
+ const outDir = path.resolve(options.outDir);
444
+ const artifacts = Object.fromEntries(Object.entries(FLOW_IDENTITY_RECOVERY_FREEZE_ARTIFACTS).map(([key, name]) => [
445
+ key,
446
+ path.join(outDir, name),
447
+ ]));
448
+ const report = {
449
+ schema_version: 'dataset-flow-identity-recovery-freeze-report.v1',
450
+ generated_at_utc: generatedAt,
451
+ status: 'frozen',
452
+ execution_submitted: false,
453
+ network_calls: lookupPerformed ? 3 : 2,
454
+ database_calls: lookupPerformed ? 2 : 1,
455
+ scope_id: freeze.scope_id,
456
+ plan_sha256: freeze.plan_sha256,
457
+ recovery_freeze_sha256: freeze.recovery_freeze_sha256,
458
+ recovery_freeze_file_sha256: freezeFileSha256,
459
+ recovery_approval_request_sha256: request.request_sha256,
460
+ recovery_approval_text_sha256: approvalTextSha256,
461
+ artifacts,
462
+ };
463
+ materializePrivateArtifactDirectoryAtomically(outDir, (staging) => {
464
+ writePrivateImmutableText(path.join(staging, FLOW_IDENTITY_RECOVERY_FREEZE_ARTIFACTS.freeze), freezeText);
465
+ writePrivateImmutableJson(path.join(staging, FLOW_IDENTITY_RECOVERY_FREEZE_ARTIFACTS.approval_request), request);
466
+ writePrivateImmutableText(path.join(staging, FLOW_IDENTITY_RECOVERY_FREEZE_ARTIFACTS.approval_text), approvalText);
467
+ writePrivateImmutableJson(path.join(staging, FLOW_IDENTITY_RECOVERY_FREEZE_ARTIFACTS.scope_proof), scope);
468
+ writePrivateImmutableJson(path.join(staging, FLOW_IDENTITY_RECOVERY_FREEZE_ARTIFACTS.report), report);
469
+ });
470
+ return report;
471
+ }
472
+ export function sealFlowIdentityRecoveryApproval(options) {
473
+ requireHash(options.approveFreezeFile, 'approveFreezeFile');
474
+ requireHash(options.approveRequest, 'approveRequest');
475
+ requireHash(options.approveText, 'approveText');
476
+ const freezeArtifact = requireCanonicalJson(options.recoveryFreezePath, 'Recovery freeze');
477
+ const freeze = parseFlowIdentityRecoveryFreeze(freezeArtifact.value);
478
+ const requestArtifact = requireCanonicalJson(options.approvalRequestPath, 'Recovery approval request');
479
+ const request = parseFlowIdentityRecoveryApprovalRequest(requestArtifact.value);
480
+ const humanApproval = readProtectedTextArtifact(options.humanApprovalPath);
481
+ const expectedText = renderFlowIdentityRecoveryApprovalText(recoveryRequestCore(request), request.request_sha256);
482
+ if (freezeArtifact.file_sha256 !== options.approveFreezeFile ||
483
+ request.request_sha256 !== options.approveRequest ||
484
+ sha256Text(expectedText) !== options.approveText ||
485
+ humanApproval.text !== expectedText ||
486
+ freeze.actor.email !== options.confirm ||
487
+ options.approvedAtUtc !== request.approved_at_utc ||
488
+ request.actor.user_id !== freeze.actor.user_id ||
489
+ request.actor.email !== freeze.actor.email ||
490
+ request.plan_sha256 !== freeze.plan_sha256 ||
491
+ request.scope_id !== freeze.scope_id ||
492
+ request.scope_proof_sha256 !== freeze.scope_proof_sha256 ||
493
+ request.recovery_freeze_file_sha256 !== freezeArtifact.file_sha256 ||
494
+ request.recovery_freeze_sha256 !== freeze.recovery_freeze_sha256 ||
495
+ request.toolchain_evidence_sha256 !== freeze.toolchain_evidence_sha256 ||
496
+ sha256Json(request.baseline) !== sha256Json(freeze.baseline)) {
497
+ fail('Recovery human approval does not exactly bind the freeze/request/status baseline.');
498
+ }
499
+ const approval = {
500
+ schema_version: 'dataset-flow-identity-recovery-approval.v1',
501
+ approved_at_utc: request.approved_at_utc,
502
+ actor: request.actor,
503
+ plan_sha256: request.plan_sha256,
504
+ scope_id: request.scope_id,
505
+ scope_proof_sha256: request.scope_proof_sha256,
506
+ recovery_freeze_sha256: request.recovery_freeze_sha256,
507
+ toolchain_evidence_sha256: request.toolchain_evidence_sha256,
508
+ recovery_approval_request_sha256: request.request_sha256,
509
+ recovery_approval_text_sha256: sha256Text(expectedText),
510
+ recovery_approval_identity_sha256: '',
511
+ };
512
+ approval.recovery_approval_identity_sha256 =
513
+ computeFlowIdentityRecoveryApprovalIdentitySha256(approval);
514
+ const outDir = path.resolve(options.outDir);
515
+ const artifacts = Object.fromEntries(Object.entries(FLOW_IDENTITY_RECOVERY_APPROVAL_ARTIFACTS).map(([key, name]) => [
516
+ key,
517
+ path.join(outDir, name),
518
+ ]));
519
+ const report = {
520
+ schema_version: 'dataset-flow-identity-recovery-approval-seal-report.v1',
521
+ generated_at_utc: (options.now ?? new Date()).toISOString(),
522
+ status: 'sealed',
523
+ execution_submitted: false,
524
+ network_calls: 0,
525
+ database_calls: 0,
526
+ scope_id: freeze.scope_id,
527
+ plan_sha256: freeze.plan_sha256,
528
+ recovery_freeze_sha256: freeze.recovery_freeze_sha256,
529
+ recovery_approval_request_sha256: approval.recovery_approval_request_sha256,
530
+ recovery_approval_text_sha256: approval.recovery_approval_text_sha256,
531
+ recovery_approval_identity_sha256: approval.recovery_approval_identity_sha256,
532
+ artifacts,
533
+ };
534
+ materializePrivateArtifactDirectoryAtomically(outDir, (staging) => {
535
+ writePrivateImmutableText(path.join(staging, FLOW_IDENTITY_RECOVERY_APPROVAL_ARTIFACTS.human_approval), humanApproval.text);
536
+ writePrivateImmutableJson(path.join(staging, FLOW_IDENTITY_RECOVERY_APPROVAL_ARTIFACTS.approval), approval);
537
+ writePrivateImmutableJson(path.join(staging, FLOW_IDENTITY_RECOVERY_APPROVAL_ARTIFACTS.report), report);
538
+ });
539
+ return report;
540
+ }
541
+ function deterministicUuidFromSha256(digest) {
542
+ const chars = digest.slice(0, 32).split('');
543
+ chars[12] = '5';
544
+ chars[16] = ((Number.parseInt(chars[16], 16) & 0x3) | 0x8).toString(16);
545
+ const hex = chars.join('');
546
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
547
+ }
548
+ export function buildFlowIdentityRecoveryRequest(options) {
549
+ return {
550
+ schema_version: 'dataset-flow-identity-scope-recovery.v1',
551
+ request_id: deterministicUuidFromSha256(sha256Text(`dataset-flow-identity-recovery.v1\u0000${options.approval.recovery_approval_identity_sha256}`)),
552
+ approved_at_utc: options.approval.approved_at_utc,
553
+ environment: options.freeze.environment,
554
+ project_ref: options.freeze.project_ref,
555
+ actor: options.freeze.actor,
556
+ target_visibility: options.freeze.target_visibility,
557
+ user_state_claim: options.freeze.user_state_claim,
558
+ operation_id: options.freeze.operation_id,
559
+ plan_sha256: options.freeze.plan_sha256,
560
+ freeze_sha256: options.freeze.original_freeze_sha256,
561
+ original_execution_approval_identity_sha256: options.freeze.original_execution_approval_identity_sha256,
562
+ scope_proof_sha256: options.freeze.scope_proof_sha256,
563
+ observed_scope_status: options.freeze.baseline.status,
564
+ observed_completed_process_count: options.freeze.baseline.completed_process_count,
565
+ observed_next_ordinal: options.freeze.baseline.next_ordinal,
566
+ observed_whole_scope_proof_sha256: options.freeze.baseline.whole_scope_proof_sha256,
567
+ recovery_mode: options.freeze.recovery_mode,
568
+ recovery_reason: options.freeze.recovery_reason,
569
+ toolchain_evidence_sha256: options.freeze.toolchain_evidence_sha256,
570
+ maximum_wrapper_invocations: 1,
571
+ maximum_cli_apply_spawns: 1,
572
+ maximum_process_posts: options.freeze.maximum_process_posts,
573
+ maximum_finalize_posts: 1,
574
+ approval_reusable: false,
575
+ automatic_retry: false,
576
+ recovery_approval_request_sha256: options.approval.recovery_approval_request_sha256,
577
+ recovery_approval_text_sha256: options.approval.recovery_approval_text_sha256,
578
+ recovery_approval_identity_sha256: options.approval.recovery_approval_identity_sha256,
579
+ };
580
+ }
581
+ export function parseFlowIdentityRecoveryProof(options) {
582
+ if (!isJsonObject(options.value))
583
+ fail('Recovery admission proof is invalid.');
584
+ const value = options.value;
585
+ assertExactKeys(value, [
586
+ 'ok',
587
+ 'command',
588
+ 'schema_version',
589
+ 'scope_id',
590
+ 'scope_proof_sha256',
591
+ 'status',
592
+ 'completed_process_count',
593
+ 'next_ordinal',
594
+ 'whole_scope_proof_sha256',
595
+ 'recovery_wire_request_sha256',
596
+ 'recovery_approval_identity_sha256',
597
+ 'invocation_id',
598
+ 'audit_id',
599
+ 'replay',
600
+ ], 'Recovery admission proof');
601
+ const invocationId = requireUuid(value.invocation_id, 'Recovery invocation_id');
602
+ if (value.ok !== true ||
603
+ value.command !== 'cmd_dataset_flow_identity_scope_recover_guarded' ||
604
+ value.schema_version !== 'dataset-flow-identity-scope-recovery-result.v1' ||
605
+ value.scope_id !== options.freeze.scope_id ||
606
+ value.scope_proof_sha256 !== options.freeze.scope_proof_sha256 ||
607
+ value.status !== options.freeze.baseline.status ||
608
+ value.completed_process_count !== options.freeze.baseline.completed_process_count ||
609
+ value.next_ordinal !== options.freeze.baseline.next_ordinal ||
610
+ value.whole_scope_proof_sha256 !== options.freeze.baseline.whole_scope_proof_sha256 ||
611
+ value.recovery_wire_request_sha256 !== flowIdentityRestrictedSha256(options.request) ||
612
+ value.recovery_approval_identity_sha256 !==
613
+ options.approval.recovery_approval_identity_sha256 ||
614
+ (options.expectedInvocationId !== undefined && invocationId !== options.expectedInvocationId) ||
615
+ typeof value.audit_id !== 'string' ||
616
+ !value.audit_id.trim() ||
617
+ typeof value.replay !== 'boolean') {
618
+ fail('Recovery admission proof does not bind the approved live baseline and wire request.');
619
+ }
620
+ requireUuid(value.scope_id, 'Recovery scope_id');
621
+ requireHash(value.scope_proof_sha256, 'Recovery scope_proof_sha256');
622
+ requireHash(value.whole_scope_proof_sha256, 'Recovery whole_scope_proof_sha256');
623
+ requireHash(value.recovery_wire_request_sha256, 'Recovery wire request SHA-256');
624
+ requireHash(value.recovery_approval_identity_sha256, 'Recovery approval identity SHA-256');
625
+ return value;
626
+ }
627
+ export function prepareFlowIdentityRecoveryExecution(options) {
628
+ const original = prepareFlowIdentityExecution({
629
+ plan: options.plan,
630
+ freeze: options.originalFreeze,
631
+ approval: options.originalApproval,
632
+ });
633
+ const scope = parseRecoveryScopeProof(options.scope, original.plan, original.identity);
634
+ const recoveryFreeze = parseFlowIdentityRecoveryFreeze(options.recoveryFreeze);
635
+ const recoveryApproval = parseFlowIdentityRecoveryApproval(options.recoveryApproval, recoveryFreeze);
636
+ if (recoveryFreeze.project_ref !== original.plan.project_ref ||
637
+ recoveryFreeze.actor.user_id !== original.plan.account.user_id ||
638
+ recoveryFreeze.actor.email !== original.plan.account.email ||
639
+ recoveryFreeze.scope_id !== scope.scope_id ||
640
+ recoveryFreeze.scope_proof_sha256 !== scope.scope_proof_sha256 ||
641
+ recoveryFreeze.operation_id !== original.plan.operation_id ||
642
+ recoveryFreeze.plan_sha256 !== original.plan.plan_sha256 ||
643
+ recoveryFreeze.original_freeze_sha256 !== original.freeze.freeze_sha256 ||
644
+ recoveryFreeze.original_execution_request_id !== original.identity.request_id ||
645
+ recoveryFreeze.original_execution_identity_sha256 !== original.identity.identity_sha256 ||
646
+ recoveryFreeze.original_execution_approval_request_sha256 !==
647
+ original.approval.execution_approval_request_sha256 ||
648
+ recoveryFreeze.original_execution_approval_text_sha256 !==
649
+ original.approval.execution_approval_text_sha256 ||
650
+ recoveryFreeze.original_execution_approval_identity_sha256 !==
651
+ original.approval.execution_approval_identity_sha256 ||
652
+ recoveryFreeze.maximum_process_posts !==
653
+ original.plan.processes.length - recoveryFreeze.baseline.completed_process_count) {
654
+ fail('Recovery artifacts do not bind the original immutable execution and durable scope.');
655
+ }
656
+ return {
657
+ plan: original.plan,
658
+ originalFreeze: original.freeze,
659
+ originalApproval: original.approval,
660
+ originalIdentity: buildFlowIdentityExecutionIdentity({
661
+ plan: original.plan,
662
+ freeze: original.freeze,
663
+ approval: original.approval,
664
+ }),
665
+ scope,
666
+ recoveryFreeze,
667
+ recoveryApproval,
668
+ recoveryRequest: buildFlowIdentityRecoveryRequest({
669
+ freeze: recoveryFreeze,
670
+ approval: recoveryApproval,
671
+ }),
672
+ };
673
+ }
674
+ export function assertFreshRecoveryBaseline(status, freeze) {
675
+ if (sha256Json(recoveryBaseline(status)) !== sha256Json(freeze.baseline)) {
676
+ fail('Live scope changed after recovery freeze; generate a fresh exact recovery approval.', 'DATASET_FLOW_IDENTITY_RECOVERY_BASELINE_DRIFT');
677
+ }
678
+ }
679
+ export const __testInternals = {
680
+ assertContext,
681
+ assertRecoverableStatus,
682
+ canonicalTimestamp,
683
+ deterministicUuidFromSha256,
684
+ readRecoveryScopeProof,
685
+ recoveryBaseline,
686
+ recoveryRequestCore,
687
+ };
688
+ //# sourceMappingURL=dataset-maintenance-flow-identity-recovery.js.map