chati-dev 4.5.16 → 4.5.28

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 (122) hide show
  1. package/README.md +8 -3
  2. package/bin/chati.js +124 -66
  3. package/framework/agents/build/dev.md +5 -5
  4. package/framework/agents/deploy/devops.md +7 -7
  5. package/framework/agents/discover/brief.md +4 -4
  6. package/framework/agents/discover/brownfield-wu.md +3 -3
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +4 -4
  10. package/framework/agents/plan/phases.md +2 -2
  11. package/framework/agents/plan/tasks.md +2 -2
  12. package/framework/agents/plan/ux.md +2 -2
  13. package/framework/agents/quality/qa-implementation.md +11 -10
  14. package/framework/agents/quality/qa-planning.md +3 -3
  15. package/framework/agents/quality/qa-visual.md +1 -1
  16. package/framework/config.yaml +3 -3
  17. package/framework/constitution.md +18 -18
  18. package/framework/context/protocols.md +1 -1
  19. package/framework/context/quality.md +1 -1
  20. package/framework/context/root.md +4 -4
  21. package/framework/data/entity-registry.yaml +1 -1
  22. package/framework/domains/agents/orchestrator.yaml +2 -2
  23. package/framework/domains/constitution.yaml +1 -1
  24. package/framework/hooks/advance-trigger.js +4 -6
  25. package/framework/hooks/git-push-authority.js +45 -37
  26. package/framework/hooks/mode-governance.js +149 -40
  27. package/framework/hooks/model-governance.js +13 -20
  28. package/framework/hooks/prism-engine.js +74 -92
  29. package/framework/hooks/reasoning-escalator.js +23 -40
  30. package/framework/hooks/session-digest.js +12 -13
  31. package/framework/hooks/session-reader.js +224 -0
  32. package/framework/hooks/session-writer.js +195 -0
  33. package/framework/hooks/team-quality-gate.js +34 -24
  34. package/framework/i18n/en.yaml +2 -2
  35. package/framework/i18n/es.yaml +2 -2
  36. package/framework/i18n/fr.yaml +2 -2
  37. package/framework/i18n/pt.yaml +2 -2
  38. package/framework/intelligence/context-engine.md +4 -4
  39. package/framework/intelligence/memory-layer.md +1 -1
  40. package/framework/manifest.json +129 -119
  41. package/framework/manifest.sig +1 -1
  42. package/framework/orchestrator/chati-router.js +252 -25
  43. package/framework/orchestrator/chati.md +238 -70
  44. package/framework/schemas/session.schema.json +1 -1
  45. package/framework/tasks/orchestrator-deviation.md +1 -1
  46. package/framework/tasks/orchestrator-escalate.md +1 -1
  47. package/framework/tasks/orchestrator-handoff.md +6 -6
  48. package/framework/tasks/orchestrator-health.md +5 -9
  49. package/framework/tasks/orchestrator-mode-switch.md +3 -7
  50. package/framework/tasks/orchestrator-resume.md +10 -14
  51. package/framework/tasks/orchestrator-route.md +3 -3
  52. package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
  53. package/framework/tasks/orchestrator-status.md +9 -9
  54. package/framework/tasks/orchestrator-suggest-mode.md +1 -1
  55. package/framework/tasks/qa-impl-consolidate.md +2 -2
  56. package/framework/tasks/qa-impl-performance-test.md +4 -4
  57. package/framework/tasks/qa-impl-regression-check.md +4 -4
  58. package/framework/tasks/qa-impl-sast-scan.md +1 -1
  59. package/framework/tasks/qa-impl-test-execute.md +1 -1
  60. package/framework/tasks/qa-impl-verdict.md +2 -2
  61. package/framework/tasks/qa-planning-consolidate.md +3 -3
  62. package/framework/tasks/qa-planning-coverage-plan.md +2 -2
  63. package/framework/tasks/qa-planning-gate-define.md +4 -4
  64. package/framework/tasks/qa-planning-risk-matrix.md +4 -4
  65. package/framework/tasks/qa-planning-test-strategy.md +2 -2
  66. package/node_modules/@chati/browser-capability/src/index.js +12 -2
  67. package/node_modules/@chati/planning/src/index.js +24 -4
  68. package/node_modules/@chati/provider-registry/src/index.js +11 -0
  69. package/node_modules/@chati/rail/src/index.js +1967 -83
  70. package/node_modules/@chati/release-lane/README.md +12 -8
  71. package/node_modules/@chati/release-lane/package.json +1 -1
  72. package/node_modules/@chati/release-lane/src/index.js +1426 -58
  73. package/node_modules/@chati/review-council/src/index.js +63 -0
  74. package/node_modules/@chati/tracking-clickup/README.md +13 -0
  75. package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
  76. package/package-artifact-manifest.json +1 -0
  77. package/package-artifact-manifest.sig +1 -0
  78. package/package.json +16 -7
  79. package/scripts/verify-real-harness-e2e.js +1581 -0
  80. package/src/config/framework-adapter.js +4 -4
  81. package/src/installer/core.js +148 -53
  82. package/src/installer/manifest.js +140 -9
  83. package/src/installer/package-artifact.js +249 -0
  84. package/src/installer/templates.js +65 -20
  85. package/src/installer-v2/catalog-client.js +531 -23
  86. package/src/installer-v2/index.js +91 -34
  87. package/src/installer-v2/installation-authority.js +327 -0
  88. package/src/installer-v2/provider-executable.js +247 -0
  89. package/src/installer-v2/wizard-installation.js +1 -1
  90. package/src/orchestrator/browser-runtime.js +44 -13
  91. package/src/orchestrator/cli.js +1545 -174
  92. package/src/orchestrator/clickup-projection.js +43 -1
  93. package/src/orchestrator/clickup-runtime.js +355 -39
  94. package/src/orchestrator/doctor.js +16 -3
  95. package/src/orchestrator/index.js +16 -0
  96. package/src/orchestrator/planning-runtime.js +20 -2
  97. package/src/orchestrator/rail-adjudication-evidence.js +234 -0
  98. package/src/orchestrator/rail-evidence-authority.js +147 -0
  99. package/src/orchestrator/rail-execution-evidence.js +45 -0
  100. package/src/orchestrator/rail-runtime.js +836 -56
  101. package/src/orchestrator/release-runtime.js +65 -6
  102. package/src/orchestrator/review-runtime.js +186 -41
  103. package/src/orchestrator/runtime-installation-v2.js +236 -20
  104. package/src/orchestrator/session-manager.js +1167 -53
  105. package/src/terminal/adapters/claude-adapter.js +3 -1
  106. package/src/terminal/adapters/codex-adapter.js +2 -0
  107. package/src/terminal/adapters/grok-adapter.js +7 -4
  108. package/src/terminal/handoff-parser.js +19 -1
  109. package/src/terminal/prompt-builder.js +9 -1
  110. package/src/terminal/provider-preflight.js +36 -3
  111. package/src/terminal/rail-execution-worktree.js +213 -0
  112. package/src/terminal/rail-prompts.js +169 -0
  113. package/src/terminal/rail-readonly-workspace.js +324 -0
  114. package/src/terminal/run-agent.js +384 -24
  115. package/src/terminal/run-parallel.js +5 -0
  116. package/src/terminal/run-rail-adjudication.js +323 -0
  117. package/src/terminal/run-rail-review.js +291 -0
  118. package/src/terminal/run-rail-rework.js +325 -0
  119. package/src/terminal/run-rail-task.js +380 -0
  120. package/src/terminal/run-team.js +5 -0
  121. package/src/terminal/spawner.js +1225 -77
  122. package/src/wizard/index.js +3 -2
@@ -1,7 +1,32 @@
1
+ import { createPublicKey, randomUUID, verify as verifySignature } from 'node:crypto';
2
+ import {
3
+ closeSync, constants, existsSync, fstatSync, fsyncSync, ftruncateSync, mkdirSync,
4
+ lstatSync, openSync, readSync, renameSync, unlinkSync, writeSync,
5
+ } from 'node:fs';
6
+ import { hostname } from 'node:os';
7
+ import { basename, dirname, join, parse, resolve, sep } from 'node:path';
1
8
  import { ContractError, canonicalize, sha256 } from '@chati/core';
2
9
 
3
10
  const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
11
+ const RELEASE_STATE_MAX_BYTES = 64 * 1024 * 1024;
12
+ const RELEASE_RECORD_MAX_BYTES = 4 * 1024 * 1024;
13
+ const RELEASE_INPUT_MAX_BYTES = 256 * 1024;
14
+ const RELEASE_EXTERNAL_RESERVE_BYTES = 2 * RELEASE_RECORD_MAX_BYTES;
15
+ const RELEASE_LOCK_RETRIES = 1000;
16
+ const RELEASE_LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
17
+ const RELEASE_LOCK_STALE_AFTER_MS = 30_000;
18
+ const RELEASE_CHECKPOINT_MAX_BYTES = 64 * 1024;
4
19
  const FAKE_TRANSPORT_CAPABILITIES = new WeakMap();
20
+ const RELEASE_TRANSPORT_CAPABILITIES = new WeakMap();
21
+ const AUTHORIZATION_VERIFIER_CAPABILITIES = new WeakMap();
22
+ const SMOKE_VERIFIER_CAPABILITIES = new WeakMap();
23
+ const RELEASE_STATE_AUTHORITY_CAPABILITIES = new WeakMap();
24
+ const RELEASE_SECURITY_TEST_HOOKS = Symbol.for('chati.release-lane.security-test-hooks');
25
+
26
+ function runSecurityTestHook(name, detail) {
27
+ const hook = globalThis[RELEASE_SECURITY_TEST_HOOKS]?.[name];
28
+ if (typeof hook === 'function') hook(detail);
29
+ }
5
30
 
6
31
  function fail(code, message, details = {}) { throw new ContractError(code, message, details); }
7
32
 
@@ -34,55 +59,1160 @@ function deepFreeze(value) {
34
59
  }
35
60
  return value;
36
61
  }
37
- function freezeData(value, code = 'UNSAFE_DATA', label = 'value') { plainData(value, code, label); return deepFreeze(canonicalize(value)); }
38
- function object(value, code, label) { plainData(value, code, label); if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code, `${label} must be an object`); }
39
- function string(value, code, label) { if (typeof value !== 'string' || value.trim() === '') fail(code, `${label} must be a non-empty string`); }
40
- function strings(value, code, label, { empty = false } = {}) { plainData(value, code, label); if (!Array.isArray(value) || (!empty && value.length === 0) || !value.every((item) => typeof item === 'string' && item.trim())) fail(code, `${label} must be an array of non-empty strings`); }
62
+
63
+ function freezeData(value, code = 'UNSAFE_DATA', label = 'value') {
64
+ plainData(value, code, label);
65
+ return deepFreeze(canonicalize(value));
66
+ }
67
+ function object(value, code, label) {
68
+ plainData(value, code, label);
69
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code, `${label} must be an object`);
70
+ }
71
+ function string(value, code, label) {
72
+ if (typeof value !== 'string' || value.trim() === '') fail(code, `${label} must be a non-empty string`);
73
+ }
74
+ function strings(value, code, label, { empty = false } = {}) {
75
+ plainData(value, code, label);
76
+ if (!Array.isArray(value) || (!empty && value.length === 0)
77
+ || !value.every((item) => typeof item === 'string' && item.trim())) {
78
+ fail(code, `${label} must be an array of non-empty strings`);
79
+ }
80
+ }
81
+ function boundedData(value, code, label, maxBytes = RELEASE_INPUT_MAX_BYTES) {
82
+ const bytes = Buffer.byteLength(JSON.stringify(value));
83
+ if (bytes > maxBytes) fail(code, `${label} exceeds the ${maxBytes}-byte safety limit`);
84
+ }
41
85
  function at(clock) {
42
86
  if (typeof clock !== 'function') fail('MISSING_CLOCK', 'a deterministic clock is required');
43
87
  const result = clock();
44
88
  const iso = result instanceof Date ? result.toISOString() : result;
45
- if (typeof iso !== 'string' || !RFC3339.test(iso) || Number.isNaN(Date.parse(iso))) fail('INVALID_CLOCK', 'clock must return a valid RFC3339 timestamp or Date');
89
+ if (typeof iso !== 'string' || !RFC3339.test(iso) || Number.isNaN(Date.parse(iso))) {
90
+ fail('INVALID_CLOCK', 'clock must return a valid RFC3339 timestamp or Date');
91
+ }
46
92
  return iso;
47
93
  }
48
- function releaseScope(request) { return freezeData({ action: 'release', environment: request.environment, release_attempt_id: request.release_attempt_id, release_plan_ref: request.release_plan_ref, rollback_plan_ref: request.rollback_plan_ref, source_completion_refs: [...request.source_completion_refs] }); }
49
- function rollbackScope(request) { return freezeData({ action: 'rollback', release_attempt_id: request.release_attempt_id, rollback_attempt_id: request.rollback_attempt_id, rollback_plan_ref: request.rollback_plan_ref }); }
50
- function authorization(input, scopeHash) {
94
+
95
+ function releaseScope(request, authorityContext = null) {
96
+ return freezeData({
97
+ action: 'release', environment: request.environment,
98
+ release_attempt_id: request.release_attempt_id,
99
+ release_plan_ref: request.release_plan_ref,
100
+ rollback_plan_ref: request.rollback_plan_ref,
101
+ source_completion_refs: [...request.source_completion_refs],
102
+ ...(authorityContext ? { authority_context: authorityContext } : {}),
103
+ });
104
+ }
105
+ function rollbackScope(request, authorityContext = null) {
106
+ return freezeData({
107
+ action: 'rollback', release_attempt_id: request.release_attempt_id,
108
+ rollback_attempt_id: request.rollback_attempt_id,
109
+ rollback_plan_ref: request.rollback_plan_ref,
110
+ ...(authorityContext ? { authority_context: authorityContext } : {}),
111
+ });
112
+ }
113
+
114
+ export function signedHumanAuthorizationPayload(input = {}) {
115
+ return freezeData({
116
+ schema_version: 1,
117
+ authorization_id: input.authorization_id,
118
+ actor_type: input.actor_type,
119
+ authorized_by: input.authorized_by,
120
+ authorized_at: input.authorized_at,
121
+ issuer_id: input.issuer_id,
122
+ scope_hash: input.scope_hash,
123
+ }, 'INVALID_HUMAN_AUTHORIZATION', 'authorization payload');
124
+ }
125
+
126
+ export function signedSmokeEvidencePayload(input = {}) {
127
+ return freezeData({
128
+ schema_version: 1,
129
+ evidence_id: input.evidence_id,
130
+ issuer_id: input.issuer_id,
131
+ observed_at: input.observed_at,
132
+ verdict: input.verdict,
133
+ evidence_refs: input.evidence_refs,
134
+ operation_scope_hash: input.operation_scope_hash,
135
+ environment: input.environment,
136
+ commit_refs: input.commit_refs,
137
+ });
138
+ }
139
+
140
+ export function signedReleaseReceiptPayload(input = {}) {
141
+ return freezeData({
142
+ schema_version: 1,
143
+ receipt_ref: input.receipt_ref,
144
+ deployment_ref: input.deployment_ref,
145
+ issuer_id: input.issuer_id,
146
+ transport_id: input.transport_id,
147
+ operation: input.operation,
148
+ operation_scope_hash: input.operation_scope_hash,
149
+ environment: input.environment,
150
+ idempotency_key: input.idempotency_key,
151
+ observed_at: input.observed_at,
152
+ }, 'INVALID_RELEASE_RECEIPT', 'release receipt payload');
153
+ }
154
+
155
+ /** Verifies receipts signed by a human boundary outside the model process. */
156
+ export class SignedHumanAuthorizationVerifier {
157
+ constructor({ issuer_id, public_key } = {}) {
158
+ string(issuer_id, 'INVALID_AUTHORIZATION_VERIFIER', 'issuer_id');
159
+ if (!public_key) fail('INVALID_AUTHORIZATION_VERIFIER', 'public_key is required');
160
+ let key;
161
+ try {
162
+ key = public_key?.type === 'public' ? public_key : createPublicKey(public_key);
163
+ if (key.type !== 'public' || key.asymmetricKeyType !== 'ed25519') throw new Error('not an Ed25519 public key');
164
+ } catch {
165
+ fail('INVALID_AUTHORIZATION_VERIFIER', 'public_key must be a valid public key');
166
+ }
167
+ AUTHORIZATION_VERIFIER_CAPABILITIES.set(this, Object.freeze({ issuer_id, key }));
168
+ Object.freeze(this);
169
+ }
170
+ }
171
+
172
+ export class SignedSmokeEvidenceVerifier {
173
+ constructor({ issuer_id, public_key } = {}) {
174
+ string(issuer_id, 'INVALID_SMOKE_VERIFIER', 'issuer_id');
175
+ let key;
176
+ try {
177
+ key = public_key?.type === 'public' ? public_key : createPublicKey(public_key);
178
+ } catch {
179
+ fail('INVALID_SMOKE_VERIFIER', 'public_key must be a valid Ed25519 public key');
180
+ }
181
+ if (key.asymmetricKeyType !== 'ed25519' || key.type !== 'public') fail('INVALID_SMOKE_VERIFIER', 'public_key must be Ed25519');
182
+ SMOKE_VERIFIER_CAPABILITIES.set(this, Object.freeze({ production: true, issuer_id, key }));
183
+ Object.freeze(this);
184
+ }
185
+ }
186
+
187
+ export class FakeSmokeEvidenceVerifier {
188
+ constructor() {
189
+ SMOKE_VERIFIER_CAPABILITIES.set(this, Object.freeze({
190
+ production: false,
191
+ verify: (input) => input?.verdict === 'passed' && Array.isArray(input?.evidence_refs) && input.evidence_refs.length > 0,
192
+ }));
193
+ Object.freeze(this);
194
+ }
195
+ }
196
+
197
+ /** Host-anchored signer/verifier for durable release journal records. */
198
+ export class ReleaseStateAuthority {
199
+ constructor({ sign: signRecord, verify: verifyRecord } = {}) {
200
+ if (typeof signRecord !== 'function' || typeof verifyRecord !== 'function') {
201
+ fail('INVALID_RELEASE_STATE_AUTHORITY', 'release state authority requires sign and verify callbacks');
202
+ }
203
+ RELEASE_STATE_AUTHORITY_CAPABILITIES.set(this, Object.freeze({ signRecord, verifyRecord }));
204
+ Object.freeze(this);
205
+ }
206
+ }
207
+
208
+ const AUTHORIZATION_MAX_AGE_MS = 30 * 60 * 1000;
209
+ const AUTHORIZATION_FUTURE_SKEW_MS = 5 * 60 * 1000;
210
+ const RELEASE_RECEIPT_CLOCK_SKEW_MS = 5 * 60 * 1000;
211
+
212
+ function authorizationFreshAt(input, referenceTime) {
213
+ const referenceMs = Date.parse(referenceTime);
214
+ const authorizedMs = Date.parse(input?.authorized_at);
215
+ return Number.isFinite(referenceMs) && Number.isFinite(authorizedMs)
216
+ && authorizedMs <= referenceMs + AUTHORIZATION_FUTURE_SKEW_MS
217
+ && referenceMs - authorizedMs <= AUTHORIZATION_MAX_AGE_MS;
218
+ }
219
+
220
+ function assertReleaseReceiptTime(receipt, attempt, referenceTime, {
221
+ code = 'RELEASE_RECEIPT_TIME_MISMATCH',
222
+ message = 'release receipt time is not ordered after authorization or is future-dated',
223
+ } = {}) {
224
+ const referenceMs = Date.parse(referenceTime);
225
+ const observedMs = Date.parse(receipt?.observed_at);
226
+ const authorizedMs = Date.parse(attempt?.authorization?.authorized_at);
227
+ if (!Number.isFinite(referenceMs) || !Number.isFinite(observedMs) || !Number.isFinite(authorizedMs)
228
+ || observedMs > referenceMs + RELEASE_RECEIPT_CLOCK_SKEW_MS
229
+ || observedMs < authorizedMs - RELEASE_RECEIPT_CLOCK_SKEW_MS) {
230
+ fail(code, message);
231
+ }
232
+ }
233
+
234
+ function authorization(input, scopeHash, verifier, consumedAuthorizationIds, clock) {
51
235
  object(input, 'MISSING_HUMAN_AUTHORIZATION', 'authorization');
52
- if (input.actor_type === undefined) fail('MISSING_HUMAN_AUTHORIZATION', 'authorization.actor_type is required');
236
+ const capability = AUTHORIZATION_VERIFIER_CAPABILITIES.get(verifier);
237
+ if (!capability) fail('UNTRUSTED_AUTHORIZATION_VERIFIER', 'a signed human authorization verifier is required');
238
+ for (const field of ['authorization_id', 'authorized_by', 'authorized_at', 'issuer_id', 'scope_hash', 'signature']) {
239
+ string(input[field], 'INVALID_HUMAN_AUTHORIZATION', `authorization.${field}`);
240
+ }
53
241
  if (input.actor_type !== 'human') fail('AUTHORIZATION_ACTOR_NOT_HUMAN', 'release authorization must be issued by a human actor');
54
- string(input.authorized_by, 'MISSING_HUMAN_AUTHORIZATION', 'authorization.authorized_by');
55
- string(input.authorized_at, 'INVALID_HUMAN_AUTHORIZATION', 'authorization.authorized_at');
56
- if (!RFC3339.test(input.authorized_at) || Number.isNaN(Date.parse(input.authorized_at))) fail('INVALID_HUMAN_AUTHORIZATION', 'authorization.authorized_at must be RFC3339');
57
- string(input.scope_hash, 'MISSING_HUMAN_AUTHORIZATION', 'authorization.scope_hash');
242
+ if (!RFC3339.test(input.authorized_at) || Number.isNaN(Date.parse(input.authorized_at))) {
243
+ fail('INVALID_HUMAN_AUTHORIZATION', 'authorization.authorized_at must be RFC3339');
244
+ }
245
+ const current = clock();
246
+ const referenceTime = current instanceof Date ? current.toISOString() : current;
247
+ if (!authorizationFreshAt(input, referenceTime)) {
248
+ fail('HUMAN_AUTHORIZATION_EXPIRED', 'release authorization must be recent and cannot be future-dated');
249
+ }
58
250
  if (input.scope_hash !== scopeHash) fail('AUTHORIZATION_SCOPE_MISMATCH', 'authorization scope_hash does not match the immutable operation scope');
251
+ if (input.issuer_id !== capability.issuer_id) fail('AUTHORIZATION_ISSUER_MISMATCH', 'authorization issuer is not trusted by this release lane');
252
+ if (consumedAuthorizationIds.has(input.authorization_id)) fail('AUTHORIZATION_ALREADY_CONSUMED', 'authorization receipt was already consumed');
253
+ boundedData(input, 'INVALID_HUMAN_AUTHORIZATION', 'authorization', 32 * 1024);
254
+ const payload = signedHumanAuthorizationPayload(input);
255
+ let valid;
256
+ try {
257
+ valid = verifySignature(null, Buffer.from(JSON.stringify(payload)), capability.key, Buffer.from(input.signature, 'base64'));
258
+ } catch { valid = false; }
259
+ if (!valid) fail('INVALID_HUMAN_AUTHORIZATION_SIGNATURE', 'authorization signature is invalid');
59
260
  return freezeData(input, 'INVALID_HUMAN_AUTHORIZATION', 'authorization');
60
261
  }
61
- function smoke(input) { object(input, 'MISSING_SMOKE_EVIDENCE', 'smoke'); if (input.verdict !== 'passed') fail('SMOKE_EVIDENCE_FAILED', 'release or rollback requires passed smoke evidence'); strings(input.evidence_refs, 'MISSING_SMOKE_EVIDENCE', 'smoke.evidence_refs'); return freezeData(input, 'MISSING_SMOKE_EVIDENCE', 'smoke'); }
62
- function fakeAdapter(adapter) { const capability = FAKE_TRANSPORT_CAPABILITIES.get(adapter); if (!capability) fail('EXTERNAL_TRANSPORT_FORBIDDEN', 'G3 release lane accepts only a capability minted by FakeReleaseTransport'); return capability; }
63
262
 
64
- /** C-11 fixture-only release authority. No network, credentials, Git command or deployment command exists here. */
263
+ const SMOKE_MAX_AGE_MS = 30 * 60 * 1000;
264
+ const SMOKE_FUTURE_SKEW_MS = 5 * 60 * 1000;
265
+
266
+ function smoke(input, { attempt, release, verifier, clock, consumedEvidenceIds }) {
267
+ object(input, 'MISSING_SMOKE_EVIDENCE', 'smoke');
268
+ const capability = SMOKE_VERIFIER_CAPABILITIES.get(verifier);
269
+ if (!capability) fail('UNTRUSTED_SMOKE_VERIFIER', 'smoke evidence requires an issuer-anchored verifier');
270
+ if (input.verdict !== 'passed') fail('SMOKE_EVIDENCE_FAILED', 'release or rollback requires passed smoke evidence');
271
+ strings(input.evidence_refs, 'MISSING_SMOKE_EVIDENCE', 'smoke.evidence_refs');
272
+ const expectedRelease = release ?? attempt;
273
+ const expectedCommits = [...new Set((expectedRelease.verified_completions || []).map((item) => item.result_commit_ref))].sort();
274
+ if (capability.production) {
275
+ strings(input.commit_refs, 'MISSING_SMOKE_EVIDENCE', 'smoke.commit_refs');
276
+ for (const field of ['evidence_id', 'issuer_id', 'observed_at', 'operation_scope_hash', 'environment', 'signature']) {
277
+ string(input[field], 'MISSING_SMOKE_EVIDENCE', `smoke.${field}`);
278
+ }
279
+ if (input.operation_scope_hash !== attempt.scope_hash) fail('SMOKE_SCOPE_MISMATCH', 'smoke evidence is not bound to the operation scope');
280
+ if (input.environment !== expectedRelease.environment) fail('SMOKE_ENVIRONMENT_MISMATCH', 'smoke evidence is not bound to the release environment');
281
+ if (sha256([...input.commit_refs].sort()) !== sha256(expectedCommits)) fail('SMOKE_COMMIT_MISMATCH', 'smoke evidence is not bound to the released commits');
282
+ if (input.issuer_id !== capability.issuer_id) fail('SMOKE_ISSUER_MISMATCH', 'smoke evidence issuer is not trusted');
283
+ const now = clock();
284
+ const nowMs = (now instanceof Date ? now : new Date(now)).getTime();
285
+ const observedMs = Date.parse(input.observed_at);
286
+ if (!Number.isFinite(nowMs) || !Number.isFinite(observedMs)
287
+ || observedMs > nowMs + SMOKE_FUTURE_SKEW_MS
288
+ || nowMs - observedMs > SMOKE_MAX_AGE_MS) {
289
+ fail('SMOKE_EVIDENCE_EXPIRED', 'smoke evidence must be recent and cannot be future-dated');
290
+ }
291
+ if (consumedEvidenceIds.has(input.evidence_id)) fail('SMOKE_EVIDENCE_ALREADY_CONSUMED', 'smoke evidence was already consumed by a release operation');
292
+ let valid;
293
+ try {
294
+ valid = verifySignature(null, Buffer.from(JSON.stringify(signedSmokeEvidencePayload(input))), capability.key, Buffer.from(input.signature, 'base64'));
295
+ } catch { valid = false; }
296
+ if (!valid) fail('INVALID_SMOKE_SIGNATURE', 'smoke evidence signature is invalid');
297
+ } else if (capability.verify(input, { attempt, release: expectedRelease, expected_commits: expectedCommits }) !== true) {
298
+ fail('INVALID_SMOKE_EVIDENCE', 'fake smoke evidence verifier rejected the fixture');
299
+ }
300
+ boundedData(input, 'MISSING_SMOKE_EVIDENCE', 'smoke', 64 * 1024);
301
+ return freezeData(input, 'MISSING_SMOKE_EVIDENCE', 'smoke');
302
+ }
303
+
304
+ function releaseTransport(adapter) {
305
+ const capability = RELEASE_TRANSPORT_CAPABILITIES.get(adapter) ?? FAKE_TRANSPORT_CAPABILITIES.get(adapter);
306
+ if (!capability) fail('UNMINTED_RELEASE_TRANSPORT', 'release lane accepts only a minted release transport capability');
307
+ return capability;
308
+ }
309
+
310
+ function lockName(name) { return `${name}.lock`; }
311
+
312
+ function sameIdentity(left, right) {
313
+ return left?.dev === right?.dev && left?.ino === right?.ino;
314
+ }
315
+
316
+ function sameSnapshot(left, right) {
317
+ return sameIdentity(left, right)
318
+ && left?.size === right?.size
319
+ && left?.mode === right?.mode
320
+ && left?.uid === right?.uid
321
+ && left?.gid === right?.gid
322
+ && left?.nlink === right?.nlink
323
+ && left?.mtimeMs === right?.mtimeMs
324
+ && left?.ctimeMs === right?.ctimeMs;
325
+ }
326
+
327
+ function descriptorDirectoryPath(descriptor) {
328
+ return `/proc/self/fd/${descriptor}`;
329
+ }
330
+
331
+ function openPinnedReleaseDirectory(path, { create = false } = {}) {
332
+ const absolute = resolve(path);
333
+ if (process.platform !== 'linux') {
334
+ fail(
335
+ 'UNSUPPORTED_RELEASE_STATE_PLATFORM',
336
+ 'durable release state requires descriptor-relative filesystem access, which is supported only on Linux',
337
+ );
338
+ }
339
+ const root = parse(absolute).root;
340
+ const segments = absolute.slice(root.length).split(sep).filter(Boolean);
341
+ let descriptor = openSync(root, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
342
+ try {
343
+ for (const segment of segments) {
344
+ const childPath = join(descriptorDirectoryPath(descriptor), segment);
345
+ let childDescriptor;
346
+ try {
347
+ childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
348
+ } catch (error) {
349
+ if (error?.code !== 'ENOENT' || !create) {
350
+ if (['ELOOP', 'ENOTDIR'].includes(error?.code)) {
351
+ fail('UNSAFE_RELEASE_STATE_DIRECTORY', 'release state directory chain must contain only real directories');
352
+ }
353
+ throw error;
354
+ }
355
+ try { mkdirSync(childPath, { mode: 0o700 }); }
356
+ catch (mkdirError) { if (mkdirError?.code !== 'EEXIST') throw mkdirError; }
357
+ childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
358
+ }
359
+ const childStat = fstatSync(childDescriptor);
360
+ if (!childStat.isDirectory()) {
361
+ closeSync(childDescriptor);
362
+ fail('UNSAFE_RELEASE_STATE_DIRECTORY', 'release state directory chain must contain only real directories');
363
+ }
364
+ closeSync(descriptor);
365
+ descriptor = childDescriptor;
366
+ }
367
+ return Object.freeze({
368
+ absolute, descriptor, identity: fstatSync(descriptor),
369
+ reference: descriptorDirectoryPath(descriptor),
370
+ });
371
+ } catch (error) {
372
+ closeSync(descriptor);
373
+ throw error;
374
+ }
375
+ }
376
+
377
+ function assertPinnedReleaseDirectory(storage) {
378
+ const descriptorStat = fstatSync(storage.descriptor);
379
+ let canonical;
380
+ try { canonical = openPinnedReleaseDirectory(storage.absolute); }
381
+ catch { fail('UNSAFE_RELEASE_STATE_DIRECTORY', 'release state directory became unreachable or unsafe during the operation'); }
382
+ const pathStat = canonical.identity;
383
+ if (!descriptorStat.isDirectory() || !sameIdentity(descriptorStat, storage.identity)
384
+ || !pathStat.isDirectory() || !sameIdentity(pathStat, storage.identity)) {
385
+ closeSync(canonical.descriptor);
386
+ fail('UNSAFE_RELEASE_STATE_DIRECTORY', 'release state directory changed during the operation');
387
+ }
388
+ closeSync(canonical.descriptor);
389
+ }
390
+
391
+ function releaseEntryPath(storage, name) {
392
+ if (basename(name) !== name || name === '.' || name === '..') {
393
+ fail('UNSAFE_RELEASE_STATE_FILE', 'release state entry must be one path segment');
394
+ }
395
+ return join(storage.reference, name);
396
+ }
397
+
398
+ function openReleaseEntry(storage, name, flags, mode) {
399
+ try { return openSync(releaseEntryPath(storage, name), flags, mode); }
400
+ catch (error) {
401
+ if (['ELOOP', 'ENXIO', 'EISDIR'].includes(error?.code)) {
402
+ fail('UNSAFE_RELEASE_STATE_FILE', 'release state must not be a symlink, FIFO or directory');
403
+ }
404
+ throw error;
405
+ }
406
+ }
407
+
408
+ function checkpointName(stateName) { return `${stateName}.checkpoint`; }
409
+
410
+ function releaseAnchor(records) {
411
+ return Object.freeze({
412
+ record_count: records.length,
413
+ last_sequence: records.at(-1)?.sequence ?? 0,
414
+ chain_hash: records.at(-1)?.chain_hash ?? 'GENESIS',
415
+ });
416
+ }
417
+
418
+ function recordAnchor(record) {
419
+ return Object.freeze({
420
+ record_count: record.sequence,
421
+ last_sequence: record.sequence,
422
+ chain_hash: record.chain_hash,
423
+ });
424
+ }
425
+
426
+ function priorRecordAnchor(record) {
427
+ return Object.freeze({
428
+ record_count: record.sequence - 1,
429
+ last_sequence: record.sequence - 1,
430
+ chain_hash: record.previous_hash,
431
+ });
432
+ }
433
+
434
+ function sameReleaseAnchor(left, right) {
435
+ return Boolean(left && right
436
+ && left.record_count === right.record_count
437
+ && left.last_sequence === right.last_sequence
438
+ && left.chain_hash === right.chain_hash);
439
+ }
440
+
441
+ function readReleaseEntryBounded(storage, name, maxBytes, { allowMissing = false } = {}) {
442
+ assertPinnedReleaseDirectory(storage);
443
+ let fd;
444
+ try {
445
+ fd = openReleaseEntry(storage, name, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
446
+ } catch (error) {
447
+ if (allowMissing && error?.code === 'ENOENT') return null;
448
+ throw error;
449
+ }
450
+ try {
451
+ const stat = fstatSync(fd);
452
+ if (!stat.isFile() || stat.nlink !== 1) fail('INVALID_RELEASE_STATE', 'release state metadata must be a regular single-link file');
453
+ if (stat.size > maxBytes) fail('INVALID_RELEASE_STATE', 'release state metadata exceeds its safety limit');
454
+ const buffer = Buffer.allocUnsafe(maxBytes + 1);
455
+ let total = 0;
456
+ while (total < buffer.length) {
457
+ const bytesRead = readSync(fd, buffer, total, buffer.length - total, null);
458
+ if (bytesRead === 0) break;
459
+ total += bytesRead;
460
+ }
461
+ if (total > maxBytes) fail('INVALID_RELEASE_STATE', 'release state metadata exceeds its safety limit');
462
+ const after = fstatSync(fd);
463
+ if (!sameSnapshot(stat, after)) fail('RELEASE_STATE_CONFLICT', 'release state metadata changed while it was read');
464
+ assertPinnedReleaseDirectory(storage);
465
+ return buffer.subarray(0, total).toString('utf8');
466
+ } finally { closeSync(fd); }
467
+ }
468
+
469
+ function writeReleaseCheckpoint(storage, stateName, checkpoint) {
470
+ const bytes = Buffer.from(JSON.stringify(checkpoint));
471
+ if (bytes.length > RELEASE_CHECKPOINT_MAX_BYTES) fail('INVALID_RELEASE_STATE', 'release checkpoint exceeds its safety limit');
472
+ const targetName = checkpointName(stateName);
473
+ const temporaryName = `${targetName}.${process.pid}.${randomUUID()}.tmp`;
474
+ const temporaryPath = releaseEntryPath(storage, temporaryName);
475
+ const targetPath = releaseEntryPath(storage, targetName);
476
+ let fd;
477
+ try {
478
+ assertPinnedReleaseDirectory(storage);
479
+ fd = openReleaseEntry(
480
+ storage, temporaryName,
481
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0),
482
+ 0o600,
483
+ );
484
+ let offset = 0;
485
+ while (offset < bytes.length) offset += writeSync(fd, bytes, offset, bytes.length - offset);
486
+ fsyncSync(fd);
487
+ closeSync(fd);
488
+ fd = undefined;
489
+ assertPinnedReleaseDirectory(storage);
490
+ renameSync(temporaryPath, targetPath);
491
+ fsyncSync(storage.descriptor);
492
+ assertPinnedReleaseDirectory(storage);
493
+ } finally {
494
+ if (fd !== undefined) closeSync(fd);
495
+ try { unlinkSync(temporaryPath); } catch { /* temporary may already be renamed or absent */ }
496
+ }
497
+ }
498
+
499
+ function parseReleaseCheckpoint(raw) {
500
+ let checkpoint;
501
+ try { checkpoint = JSON.parse(raw); }
502
+ catch { return fail('CORRUPT_RELEASE_STATE', 'release checkpoint contains malformed JSON'); }
503
+ object(checkpoint, 'CORRUPT_RELEASE_STATE', 'release checkpoint');
504
+ if (checkpoint.schema_version !== 1) fail('CORRUPT_RELEASE_STATE', 'release checkpoint schema is unsupported');
505
+ object(checkpoint.committed, 'CORRUPT_RELEASE_STATE', 'release checkpoint committed anchor');
506
+ if (checkpoint.pending !== null) object(checkpoint.pending, 'CORRUPT_RELEASE_STATE', 'release checkpoint pending anchor');
507
+ for (const [label, anchor] of [['committed', checkpoint.committed], ['pending', checkpoint.pending]]) {
508
+ if (anchor === null) continue;
509
+ if (!Number.isSafeInteger(anchor.record_count) || anchor.record_count < 0
510
+ || !Number.isSafeInteger(anchor.last_sequence) || anchor.last_sequence !== anchor.record_count
511
+ || typeof anchor.chain_hash !== 'string' || !anchor.chain_hash) {
512
+ fail('CORRUPT_RELEASE_STATE', `release checkpoint ${label} anchor is invalid`);
513
+ }
514
+ }
515
+ return checkpoint;
516
+ }
517
+
518
+ function processIsAlive(pid) {
519
+ if (!Number.isInteger(pid) || pid <= 0) return false;
520
+ try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
521
+ }
522
+
523
+ function readLockOwner(storage, name, { assertCanonical = true } = {}) {
524
+ try {
525
+ if (assertCanonical) assertPinnedReleaseDirectory(storage);
526
+ const fd = openReleaseEntry(storage, name, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
527
+ try {
528
+ const stat = fstatSync(fd);
529
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 16 * 1024) return null;
530
+ const buffer = Buffer.allocUnsafe((16 * 1024) + 1);
531
+ let total = 0;
532
+ while (total < buffer.length) {
533
+ const bytesRead = readSync(fd, buffer, total, buffer.length - total, null);
534
+ if (bytesRead === 0) break;
535
+ total += bytesRead;
536
+ }
537
+ if (total > 16 * 1024 || !sameSnapshot(stat, fstatSync(fd))) return null;
538
+ const owner = JSON.parse(buffer.subarray(0, total).toString('utf8'));
539
+ return owner && typeof owner === 'object' && typeof owner.token === 'string' ? owner : null;
540
+ } finally { closeSync(fd); }
541
+ } catch { return null; }
542
+ }
543
+
544
+ function recoverStaleLock(storage, name) {
545
+ assertPinnedReleaseDirectory(storage);
546
+ const path = releaseEntryPath(storage, name);
547
+ let stat;
548
+ try { stat = lstatSync(path); } catch (error) { return error?.code === 'ENOENT'; }
549
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) return false;
550
+ const owner = readLockOwner(storage, name);
551
+ if (owner?.hostname === hostname() && processIsAlive(owner.pid)) return false;
552
+ // A PID from another host cannot be proven dead locally. Never steal a
553
+ // remote lock automatically; explicit operator reconciliation is required.
554
+ if (owner?.hostname && owner.hostname !== hostname()) return false;
555
+ if (!owner && Date.now() - stat.mtimeMs < RELEASE_LOCK_STALE_AFTER_MS) return false;
556
+ const recoveryName = `${name}.recovery`;
557
+ const recoveryPath = releaseEntryPath(storage, recoveryName);
558
+ let recoveryFd;
559
+ try {
560
+ recoveryFd = openSync(recoveryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
561
+ const bytes = Buffer.from(JSON.stringify({ pid: process.pid, hostname: hostname(), token: randomUUID() }));
562
+ writeSync(recoveryFd, bytes, 0, bytes.length);
563
+ fsyncSync(recoveryFd);
564
+ } catch {
565
+ if (recoveryFd !== undefined) closeSync(recoveryFd);
566
+ return false;
567
+ }
568
+ const quarantineName = `${name}.stale.${process.pid}.${randomUUID()}`;
569
+ const quarantine = releaseEntryPath(storage, quarantineName);
570
+ try {
571
+ const current = lstatSync(path);
572
+ const currentOwner = readLockOwner(storage, name);
573
+ if (!sameIdentity(stat, current) || currentOwner?.token !== owner?.token) return false;
574
+ renameSync(path, quarantine);
575
+ const quarantined = lstatSync(quarantine);
576
+ const quarantinedOwner = readLockOwner(storage, quarantineName);
577
+ if (!sameIdentity(stat, quarantined) || quarantinedOwner?.token !== owner?.token) {
578
+ try { if (!existsSync(path)) renameSync(quarantine, path); } catch { /* preserve quarantine fail-closed */ }
579
+ return false;
580
+ }
581
+ unlinkSync(quarantine);
582
+ return true;
583
+ } catch (error) { return error?.code === 'ENOENT'; }
584
+ finally {
585
+ if (recoveryFd !== undefined) closeSync(recoveryFd);
586
+ try { unlinkSync(recoveryPath); } catch { /* fail closed if recovery ownership changed */ }
587
+ }
588
+ }
589
+
590
+ function acquireReleaseLock(statePath) {
591
+ const storage = openPinnedReleaseDirectory(dirname(statePath), { create: true });
592
+ const stateName = basename(statePath);
593
+ const name = lockName(stateName);
594
+ const path = releaseEntryPath(storage, name);
595
+ let fd;
596
+ let token;
597
+ try {
598
+ for (let retry = 0; retry < RELEASE_LOCK_RETRIES; retry += 1) {
599
+ assertPinnedReleaseDirectory(storage);
600
+ let candidate;
601
+ try {
602
+ candidate = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
603
+ } catch (error) {
604
+ if (error?.code !== 'EEXIST') throw error;
605
+ if (recoverStaleLock(storage, name)) continue;
606
+ Atomics.wait(RELEASE_LOCK_WAIT, 0, 0, 10);
607
+ continue;
608
+ }
609
+ token = `${process.pid}-${Date.now()}-${randomUUID()}`;
610
+ try {
611
+ const bytes = Buffer.from(JSON.stringify({ schema_version: 1, pid: process.pid, hostname: hostname(), token, acquired_at: new Date().toISOString() }));
612
+ writeSync(candidate, bytes, 0, bytes.length);
613
+ fsyncSync(candidate);
614
+ fsyncSync(storage.descriptor);
615
+ fd = candidate;
616
+ break;
617
+ } catch (error) {
618
+ closeSync(candidate);
619
+ try { unlinkSync(path); } catch { /* the acquisition failed closed */ }
620
+ throw error;
621
+ }
622
+ }
623
+ if (fd === undefined) fail('RELEASE_STATE_LOCK_BUSY', 'release state mutation lock is already held');
624
+ } catch (error) {
625
+ closeSync(storage.descriptor);
626
+ throw error;
627
+ }
628
+ return Object.freeze({
629
+ storage,
630
+ release() {
631
+ closeSync(fd);
632
+ let safetyError = null;
633
+ try { assertPinnedReleaseDirectory(storage); } catch (error) { safetyError = error; }
634
+ try {
635
+ if (readLockOwner(storage, name, { assertCanonical: false })?.token === token) {
636
+ unlinkSync(path);
637
+ fsyncSync(storage.descriptor);
638
+ }
639
+ } finally { closeSync(storage.descriptor); }
640
+ if (safetyError) throw safetyError;
641
+ },
642
+ });
643
+ }
644
+
645
+ function appendDurable(path, record, expectedIdentity, storage, stateAuthority, requireAuthenticatedState) {
646
+ const bytes = Buffer.from(`${JSON.stringify(record)}\n`);
647
+ if (bytes.length > RELEASE_RECORD_MAX_BYTES) fail('INVALID_RELEASE_STATE', 'release state record exceeds the 4 MiB safety limit');
648
+ if (!storage) fail('MISSING_RELEASE_STATE_STORAGE', 'durable release append requires a pinned storage directory');
649
+ const name = basename(path);
650
+ const existed = expectedIdentity !== null;
651
+ writeReleaseCheckpoint(storage, name, Object.freeze({
652
+ schema_version: 1,
653
+ committed: priorRecordAnchor(record),
654
+ pending: recordAnchor(record),
655
+ }));
656
+ runSecurityTestHook('beforeReleaseAppend', { path });
657
+ assertPinnedReleaseDirectory(storage);
658
+ let fd;
659
+ try {
660
+ fd = openReleaseEntry(storage, name, constants.O_WRONLY | constants.O_APPEND
661
+ | (existed ? 0 : constants.O_CREAT | constants.O_EXCL)
662
+ | (constants.O_NOFOLLOW || 0), 0o600);
663
+ } catch (error) {
664
+ if (!existed && error?.code === 'EEXIST') fail('RELEASE_STATE_CONFLICT', 'release state appeared after it was loaded');
665
+ throw error;
666
+ }
667
+ let identity;
668
+ try {
669
+ const stat = fstatSync(fd);
670
+ if (!stat.isFile() || stat.nlink !== 1) fail('INVALID_RELEASE_STATE', 'release state must be a regular single-link file');
671
+ if (existed && !sameSnapshot(stat, expectedIdentity)) fail('RELEASE_STATE_CONFLICT', 'release state changed between load and append');
672
+ if (stat.size + bytes.length > RELEASE_STATE_MAX_BYTES) fail('INVALID_RELEASE_STATE', 'release state exceeds the 64 MiB safety limit');
673
+ let offset = 0;
674
+ while (offset < bytes.length) offset += writeSync(fd, bytes, offset, bytes.length - offset);
675
+ fsyncSync(fd);
676
+ const after = fstatSync(fd);
677
+ if (!sameIdentity(stat, after)
678
+ || after.mode !== stat.mode || after.uid !== stat.uid || after.gid !== stat.gid
679
+ || after.nlink !== stat.nlink || after.size !== stat.size + bytes.length) {
680
+ fail('RELEASE_STATE_CONFLICT', 'release state changed during append');
681
+ }
682
+ identity = after;
683
+ } finally { closeSync(fd); }
684
+ fsyncSync(storage.descriptor);
685
+ assertPinnedReleaseDirectory(storage);
686
+ const verified = readDurable(path, storage, stateAuthority, requireAuthenticatedState);
687
+ if (verified.records.at(-1)?.chain_hash !== record.chain_hash
688
+ || verified.records.at(-1)?.sequence !== record.sequence) {
689
+ fail('RELEASE_STATE_CONFLICT', 'release state content changed during append');
690
+ }
691
+ return verified.identity ?? identity;
692
+ }
693
+
694
+ function assertExternalCapacity(path, storage, expectedIdentity) {
695
+ if (!path) return;
696
+ if (!storage) fail('MISSING_RELEASE_STATE_STORAGE', 'external release requires pinned durable storage');
697
+ assertPinnedReleaseDirectory(storage);
698
+ let fd;
699
+ try { fd = openReleaseEntry(storage, basename(path), constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0)); }
700
+ catch (error) { if (error?.code === 'ENOENT') return; throw error; }
701
+ try {
702
+ const stat = fstatSync(fd);
703
+ if (!stat.isFile() || stat.nlink !== 1) fail('INVALID_RELEASE_STATE', 'release state must be a regular single-link file');
704
+ if (expectedIdentity && !sameSnapshot(stat, expectedIdentity)) {
705
+ fail('RELEASE_STATE_CONFLICT', 'release state changed before the external boundary');
706
+ }
707
+ if (stat.size > RELEASE_STATE_MAX_BYTES - RELEASE_EXTERNAL_RESERVE_BYTES) {
708
+ fail('RELEASE_STATE_CAPACITY_EXHAUSTED', 'release state lacks reserved capacity for an external intent and its outcome');
709
+ }
710
+ } finally { closeSync(fd); }
711
+ assertPinnedReleaseDirectory(storage);
712
+ }
713
+
714
+ function readDurable(path, storage, stateAuthority = null, requireAuthenticatedState = false) {
715
+ if (!path) return { records: [], identity: null };
716
+ if (!storage) fail('MISSING_RELEASE_STATE_STORAGE', 'durable release read requires a pinned storage directory');
717
+ assertPinnedReleaseDirectory(storage);
718
+ const stateName = basename(path);
719
+ let fd;
720
+ try { fd = openReleaseEntry(storage, stateName, constants.O_RDWR | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0)); }
721
+ catch (error) { if (error?.code !== 'ENOENT') throw error; }
722
+ try {
723
+ const stat = fd === undefined ? null : fstatSync(fd);
724
+ if (stat && (!stat.isFile() || stat.nlink !== 1)) fail('INVALID_RELEASE_STATE', 'release state must be a regular single-link file');
725
+ if (stat?.size > RELEASE_STATE_MAX_BYTES) fail('INVALID_RELEASE_STATE', 'release state exceeds the 64 MiB safety limit');
726
+ if (stat) runSecurityTestHook('afterReleaseStateStat', { path });
727
+ const chunks = [];
728
+ let total = 0;
729
+ while (fd !== undefined) {
730
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, RELEASE_STATE_MAX_BYTES + 1 - total));
731
+ const bytesRead = readSync(fd, chunk, 0, chunk.length, null);
732
+ if (bytesRead === 0) break;
733
+ total += bytesRead;
734
+ if (total > RELEASE_STATE_MAX_BYTES) fail('INVALID_RELEASE_STATE', 'release state exceeds the 64 MiB safety limit');
735
+ chunks.push(chunk.subarray(0, bytesRead));
736
+ }
737
+ const after = fd === undefined ? null : fstatSync(fd);
738
+ if (stat && !sameSnapshot(stat, after)) fail('RELEASE_STATE_CONFLICT', 'release state changed while it was being read');
739
+ const raw = Buffer.concat(chunks, total).toString('utf8');
740
+ const complete = raw.endsWith('\n') ? raw : raw.slice(0, raw.lastIndexOf('\n') + 1);
741
+ let values = complete === '' ? [] : complete.slice(0, -1).split('\n').map((line) => {
742
+ try { return JSON.parse(line); } catch { return fail('CORRUPT_RELEASE_STATE', 'release state contains malformed JSON'); }
743
+ });
744
+ validateReleaseChain(values, stateAuthority, requireAuthenticatedState);
745
+ const tail = raw && !raw.endsWith('\n') ? raw.slice(raw.lastIndexOf('\n') + 1) : null;
746
+ let completeTail = false;
747
+ if (tail !== null) {
748
+ try {
749
+ const candidate = JSON.parse(tail);
750
+ validateReleaseChain([...values, candidate], stateAuthority, requireAuthenticatedState);
751
+ values = [...values, candidate];
752
+ completeTail = true;
753
+ } catch { /* a checkpoint decides whether the incomplete append is repairable */ }
754
+ }
755
+ const rawCheckpoint = readReleaseEntryBounded(
756
+ storage, checkpointName(stateName), RELEASE_CHECKPOINT_MAX_BYTES, { allowMissing: true },
757
+ );
758
+ const checkpoint = rawCheckpoint === null ? null : parseReleaseCheckpoint(rawCheckpoint);
759
+ const anchor = releaseAnchor(values);
760
+ const validCommitted = checkpoint !== null && sameReleaseAnchor(anchor, checkpoint.committed);
761
+ const validPending = checkpoint !== null && checkpoint.pending !== null
762
+ && sameReleaseAnchor(anchor, checkpoint.pending);
763
+ const incompleteTail = tail !== null && !completeTail;
764
+ const missingDelimiter = tail !== null && completeTail;
765
+ if (checkpoint && !validCommitted && !validPending) {
766
+ fail('RELEASE_STATE_TRUNCATION_DETECTED', 'release state does not match its durable checkpoint');
767
+ }
768
+ if (!checkpoint && (incompleteTail || missingDelimiter)) {
769
+ fail('CORRUPT_RELEASE_STATE', 'release state contains an uncheckpointed trailing record');
770
+ }
771
+ const safeTailRepair = incompleteTail && validCommitted && checkpoint.pending !== null;
772
+ if (incompleteTail && !safeTailRepair) {
773
+ fail('RELEASE_STATE_TRUNCATION_DETECTED', 'release state was truncated below its durable high-water mark');
774
+ }
775
+ let identity = after;
776
+ if (safeTailRepair) {
777
+ const repairedBytes = Buffer.byteLength(complete);
778
+ ftruncateSync(fd, repairedBytes);
779
+ fsyncSync(fd);
780
+ const repaired = fstatSync(fd);
781
+ if (!sameIdentity(after, repaired)
782
+ || repaired.mode !== after.mode || repaired.uid !== after.uid || repaired.gid !== after.gid
783
+ || repaired.nlink !== after.nlink || repaired.size !== repairedBytes) {
784
+ fail('RELEASE_STATE_CONFLICT', 'release state changed during tail repair');
785
+ }
786
+ const verify = Buffer.allocUnsafe(repairedBytes);
787
+ let offset = 0;
788
+ while (offset < verify.length) {
789
+ const bytesRead = readSync(fd, verify, offset, verify.length - offset, offset);
790
+ if (bytesRead === 0) break;
791
+ offset += bytesRead;
792
+ }
793
+ const verified = fstatSync(fd);
794
+ if (offset !== repairedBytes || verify.toString('utf8') !== complete || !sameSnapshot(repaired, verified)) {
795
+ fail('RELEASE_STATE_CONFLICT', 'release state tail repair could not be verified');
796
+ }
797
+ identity = verified;
798
+ } else if (missingDelimiter) {
799
+ if (after.size + 1 > RELEASE_STATE_MAX_BYTES) fail('INVALID_RELEASE_STATE', 'release state exceeds the 64 MiB safety limit');
800
+ const newline = Buffer.from('\n');
801
+ const written = writeSync(fd, newline, 0, newline.length, after.size);
802
+ if (written !== newline.length) fail('RELEASE_STATE_CONFLICT', 'release state delimiter repair was incomplete');
803
+ fsyncSync(fd);
804
+ const normalized = fstatSync(fd);
805
+ if (!sameIdentity(after, normalized)
806
+ || normalized.mode !== after.mode || normalized.uid !== after.uid || normalized.gid !== after.gid
807
+ || normalized.nlink !== after.nlink || normalized.size !== after.size + 1) {
808
+ fail('RELEASE_STATE_CONFLICT', 'release state changed during delimiter repair');
809
+ }
810
+ const expected = Buffer.from(`${raw}\n`);
811
+ const verify = Buffer.allocUnsafe(expected.length);
812
+ let offset = 0;
813
+ while (offset < verify.length) {
814
+ const bytesRead = readSync(fd, verify, offset, verify.length - offset, offset);
815
+ if (bytesRead === 0) break;
816
+ offset += bytesRead;
817
+ }
818
+ const verified = fstatSync(fd);
819
+ if (offset !== expected.length || !verify.equals(expected) || !sameSnapshot(normalized, verified)) {
820
+ fail('RELEASE_STATE_CONFLICT', 'release state delimiter repair could not be verified');
821
+ }
822
+ identity = verified;
823
+ }
824
+ if (checkpoint !== null && checkpoint.pending !== null) {
825
+ writeReleaseCheckpoint(storage, stateName, Object.freeze({
826
+ schema_version: 1,
827
+ committed: releaseAnchor(values),
828
+ pending: null,
829
+ }));
830
+ }
831
+ if (fd === undefined && checkpoint && !sameReleaseAnchor(releaseAnchor([]), checkpoint.committed)) {
832
+ fail('RELEASE_STATE_TRUNCATION_DETECTED', 'release state file is missing below its durable high-water mark');
833
+ }
834
+ if (fd !== undefined) fsyncSync(storage.descriptor);
835
+ assertPinnedReleaseDirectory(storage);
836
+ return { records: values, identity };
837
+ } finally { if (fd !== undefined) closeSync(fd); }
838
+ }
839
+
840
+ function releaseRecordCore(value) {
841
+ return freezeData({
842
+ schema_version: value.schema_version,
843
+ sequence: value.sequence,
844
+ type: value.type,
845
+ occurred_at: value.occurred_at,
846
+ payload: value.payload,
847
+ record_hash: value.record_hash,
848
+ previous_hash: value.previous_hash,
849
+ chain_hash: value.chain_hash,
850
+ }, 'CORRUPT_RELEASE_STATE', 'release authenticated record');
851
+ }
852
+
853
+ function authenticateReleaseRecord(core, stateAuthority) {
854
+ if (!stateAuthority) return null;
855
+ const capability = RELEASE_STATE_AUTHORITY_CAPABILITIES.get(stateAuthority);
856
+ if (!capability) fail('INVALID_RELEASE_STATE_AUTHORITY', 'release state authority capability is not minted');
857
+ const receipt = capability.signRecord(core);
858
+ object(receipt, 'INVALID_RELEASE_STATE_AUTHORITY', 'release state authority receipt');
859
+ string(receipt.authority_id, 'INVALID_RELEASE_STATE_AUTHORITY', 'release state authority receipt.authority_id');
860
+ string(receipt.authority_signature, 'INVALID_RELEASE_STATE_AUTHORITY', 'release state authority receipt.authority_signature');
861
+ if (capability.verifyRecord(core, receipt) !== true) {
862
+ fail('INVALID_RELEASE_STATE_AUTHORITY', 'release state authority did not verify its own receipt');
863
+ }
864
+ return freezeData(receipt, 'INVALID_RELEASE_STATE_AUTHORITY', 'release state authority receipt');
865
+ }
866
+
867
+ function validateReleaseChain(values, stateAuthority = null, requireAuthenticatedState = false) {
868
+ let previousHash = 'GENESIS';
869
+ let sequence = 0;
870
+ const capability = stateAuthority ? RELEASE_STATE_AUTHORITY_CAPABILITIES.get(stateAuthority) : null;
871
+ if (requireAuthenticatedState && !capability) {
872
+ fail('RELEASE_STATE_AUTHORITY_REQUIRED', 'durable production release state requires a host authority');
873
+ }
874
+ for (const value of values) {
875
+ object(value, 'CORRUPT_RELEASE_STATE', 'release record');
876
+ const material = freezeData({
877
+ schema_version: value.schema_version, sequence: value.sequence,
878
+ type: value.type, occurred_at: value.occurred_at, payload: value.payload,
879
+ }, 'CORRUPT_RELEASE_STATE', 'release record');
880
+ const expectedRecordHash = sha256(material);
881
+ const expectedChainHash = sha256({ previous_hash: previousHash, record_hash: expectedRecordHash });
882
+ if (material.schema_version !== 1 || material.sequence !== sequence + 1
883
+ || value.previous_hash !== previousHash || value.record_hash !== expectedRecordHash
884
+ || value.chain_hash !== expectedChainHash) {
885
+ fail('CORRUPT_RELEASE_STATE', 'release state hash chain is invalid');
886
+ }
887
+ const authenticated = typeof value.authority_id === 'string'
888
+ && typeof value.authority_signature === 'string';
889
+ const expectedKeys = authenticated
890
+ ? ['authority_id', 'authority_signature', 'chain_hash', 'occurred_at', 'payload', 'previous_hash', 'record_hash', 'schema_version', 'sequence', 'type']
891
+ : ['chain_hash', 'occurred_at', 'payload', 'previous_hash', 'record_hash', 'schema_version', 'sequence', 'type'];
892
+ if (Object.keys(value).sort().join(',') !== expectedKeys.sort().join(',')) {
893
+ fail('CORRUPT_RELEASE_STATE', 'release state record has ambiguous or unsupported fields');
894
+ }
895
+ if (requireAuthenticatedState && !authenticated) {
896
+ fail('UNAUTHENTICATED_RELEASE_STATE', 'production release state contains an unsigned record');
897
+ }
898
+ if (authenticated) {
899
+ if (!capability || capability.verifyRecord(releaseRecordCore(value), {
900
+ authority_id: value.authority_id,
901
+ authority_signature: value.authority_signature,
902
+ }) !== true) {
903
+ fail('INVALID_RELEASE_STATE_SIGNATURE', 'release state record signature is invalid');
904
+ }
905
+ }
906
+ previousHash = value.chain_hash;
907
+ sequence = value.sequence;
908
+ }
909
+ return values;
910
+ }
911
+
912
+ /** Durable release and rollback authority. */
65
913
  export class ReleaseLane {
66
914
  #clock;
67
915
  #verifyCompletion;
916
+ #authorizationVerifier;
917
+ #smokeVerifier;
918
+ #statePath;
68
919
  #records = [];
69
920
  #releases = new Map();
70
921
  #rollbacks = new Map();
922
+ #consumedAuthorizationIds = new Set();
923
+ #consumedSmokeEvidenceIds = new Set();
924
+ #stateIdentity = null;
925
+ #activeStorage = null;
926
+ #requireProductionTransport;
927
+ #stateAuthority;
928
+ #requireAuthenticatedState;
929
+ #authorityContext;
71
930
 
72
- constructor({ clock, verify_completion } = {}) {
931
+ constructor({
932
+ clock, verify_completion, authorization_verifier, smoke_verifier,
933
+ state_path = null, require_production_transport = false,
934
+ state_authority = null, require_authenticated_state = false,
935
+ authority_context = null, require_authority_context = false,
936
+ } = {}) {
73
937
  if (typeof verify_completion !== 'function') fail('MISSING_COMPLETION_VERIFIER', 'verify_completion must validate C-06 completion evidence');
938
+ if (!AUTHORIZATION_VERIFIER_CAPABILITIES.has(authorization_verifier)) {
939
+ fail('UNTRUSTED_AUTHORIZATION_VERIFIER', 'authorization_verifier must be a SignedHumanAuthorizationVerifier');
940
+ }
941
+ if (!SMOKE_VERIFIER_CAPABILITIES.has(smoke_verifier)) fail('UNTRUSTED_SMOKE_VERIFIER', 'smoke_verifier must be a SignedSmokeEvidenceVerifier');
942
+ if (state_path !== null) string(state_path, 'INVALID_RELEASE_STATE', 'state_path');
74
943
  this.#clock = clock;
75
944
  this.#verifyCompletion = verify_completion;
945
+ this.#authorizationVerifier = authorization_verifier;
946
+ this.#smokeVerifier = smoke_verifier;
947
+ this.#requireProductionTransport = require_production_transport === true;
948
+ if (state_authority !== null && !RELEASE_STATE_AUTHORITY_CAPABILITIES.has(state_authority)) {
949
+ fail('INVALID_RELEASE_STATE_AUTHORITY', 'state_authority must be a minted ReleaseStateAuthority');
950
+ }
951
+ this.#stateAuthority = state_authority;
952
+ this.#requireAuthenticatedState = require_authenticated_state === true;
953
+ if (this.#requireAuthenticatedState && !this.#stateAuthority) {
954
+ fail('RELEASE_STATE_AUTHORITY_REQUIRED', 'authenticated release state requires a host authority');
955
+ }
956
+ if (authority_context !== null) {
957
+ object(authority_context, 'INVALID_RELEASE_AUTHORITY_CONTEXT', 'authority_context');
958
+ for (const field of ['project_id', 'handoff_id']) {
959
+ string(authority_context[field], 'INVALID_RELEASE_AUTHORITY_CONTEXT', `authority_context.${field}`);
960
+ }
961
+ this.#authorityContext = freezeData(authority_context, 'INVALID_RELEASE_AUTHORITY_CONTEXT', 'authority_context');
962
+ } else {
963
+ this.#authorityContext = null;
964
+ }
965
+ if (require_authority_context === true && !this.#authorityContext) {
966
+ fail('RELEASE_AUTHORITY_CONTEXT_REQUIRED', 'production release scope requires project and handoff authority context');
967
+ }
968
+ this.#statePath = state_path;
969
+ if (this.#statePath) this.#exclusive(() => undefined);
970
+ else this.#load();
971
+ }
972
+
973
+ get records() {
974
+ return this.#statePath
975
+ ? this.#exclusive(() => freezeData([...this.#records]))
976
+ : freezeData([...this.#records]);
977
+ }
978
+
979
+ #assertReleaseIdentity(previous, next) {
980
+ const immutable = (value) => ({
981
+ release_attempt_id: value.release_attempt_id,
982
+ source_completion_refs: value.source_completion_refs,
983
+ environment: value.environment,
984
+ release_plan_ref: value.release_plan_ref,
985
+ rollback_plan_ref: value.rollback_plan_ref,
986
+ verified_completions: value.verified_completions,
987
+ scope: value.scope,
988
+ scope_hash: value.scope_hash,
989
+ });
990
+ if (sha256(immutable(previous)) !== sha256(immutable(next))) {
991
+ fail('CORRUPT_RELEASE_STATE', 'release journal changed immutable release attempt material');
992
+ }
993
+ }
994
+
995
+ #assertRollbackIdentity(previous, next) {
996
+ const immutable = (value) => ({
997
+ rollback_attempt_id: value.rollback_attempt_id,
998
+ release_attempt_id: value.release_attempt_id,
999
+ rollback_plan_ref: value.rollback_plan_ref,
1000
+ scope: value.scope,
1001
+ scope_hash: value.scope_hash,
1002
+ });
1003
+ if (sha256(immutable(previous)) !== sha256(immutable(next))) {
1004
+ fail('CORRUPT_RELEASE_STATE', 'release journal changed immutable rollback attempt material');
1005
+ }
1006
+ }
1007
+
1008
+ #validateRecordSemantics(record) {
1009
+ if (!RFC3339.test(record.occurred_at || '') || Number.isNaN(Date.parse(record.occurred_at))) {
1010
+ fail('CORRUPT_RELEASE_STATE', 'release journal record timestamp is invalid');
1011
+ }
1012
+ object(record.payload, 'CORRUPT_RELEASE_STATE', 'release record payload');
1013
+ const releaseTransitions = {
1014
+ release_attempt_created: { from: null, to: 'created' },
1015
+ release_rejected: { from: ['created'], to: 'rejected' },
1016
+ release_intent_recorded: { from: ['created'], to: 'pending_external' },
1017
+ release_completed: { from: ['pending_external'], to: 'released' },
1018
+ release_sent_unconfirmed: { from: ['pending_external'], to: 'sent_unconfirmed' },
1019
+ release_reconciled: { from: ['pending_external', 'sent_unconfirmed'], to: 'released' },
1020
+ release_reconciled_not_found: { from: ['pending_external', 'sent_unconfirmed'], to: 'created' },
1021
+ release_reconciliation_failed: { from: ['pending_external', 'sent_unconfirmed'], to: 'same' },
1022
+ release_reconciliation_inconclusive: { from: ['pending_external', 'sent_unconfirmed'], to: 'same' },
1023
+ };
1024
+ const rollbackTransitions = {
1025
+ rollback_attempt_created: { from: null, to: 'created' },
1026
+ rollback_rejected: { from: ['created'], to: 'rejected' },
1027
+ rollback_intent_recorded: { from: ['created'], to: 'pending_external' },
1028
+ rollback_completed: { from: ['pending_external'], to: 'rolled_back' },
1029
+ rollback_sent_unconfirmed: { from: ['pending_external'], to: 'sent_unconfirmed' },
1030
+ rollback_reconciled: { from: ['pending_external', 'sent_unconfirmed'], to: 'rolled_back' },
1031
+ rollback_reconciled_not_found: { from: ['pending_external', 'sent_unconfirmed'], to: 'created' },
1032
+ rollback_reconciliation_failed: { from: ['pending_external', 'sent_unconfirmed'], to: 'same' },
1033
+ rollback_reconciliation_inconclusive: { from: ['pending_external', 'sent_unconfirmed'], to: 'same' },
1034
+ };
1035
+ if (Object.hasOwn(releaseTransitions, record.type)) {
1036
+ if (Object.keys(record.payload).filter((key) => key === 'release').length !== 1) {
1037
+ fail('CORRUPT_RELEASE_STATE', 'release journal transition lacks one release payload');
1038
+ }
1039
+ const next = record.payload.release;
1040
+ object(next, 'CORRUPT_RELEASE_STATE', 'release transition');
1041
+ string(next.release_attempt_id, 'CORRUPT_RELEASE_STATE', 'release.release_attempt_id');
1042
+ const previous = this.#releases.get(next.release_attempt_id) ?? null;
1043
+ const transition = releaseTransitions[record.type];
1044
+ if (transition.from === null ? previous !== null : !previous || !transition.from.includes(previous.state)) {
1045
+ fail('CORRUPT_RELEASE_STATE', `invalid ${record.type} predecessor`);
1046
+ }
1047
+ const expectedState = transition.to === 'same' ? previous.state : transition.to;
1048
+ if (next.state !== expectedState) fail('CORRUPT_RELEASE_STATE', `invalid ${record.type} state`);
1049
+ if (previous) this.#assertReleaseIdentity(previous, next);
1050
+ if (record.type === 'release_attempt_created') {
1051
+ const expectedScope = releaseScope(next, this.#authorityContext);
1052
+ if (sha256(next.scope) !== sha256(expectedScope) || next.scope_hash !== sha256(expectedScope)) {
1053
+ fail('CORRUPT_RELEASE_STATE', 'release attempt scope is not canonical');
1054
+ }
1055
+ }
1056
+ if (record.type === 'release_intent_recorded') {
1057
+ if (next.idempotency_key !== sha256({ operation: 'release', scope_hash: next.scope_hash })
1058
+ || record.payload.idempotency_key !== next.idempotency_key
1059
+ || record.payload.authorization?.authorization_id !== next.authorization?.authorization_id
1060
+ || next.authorization?.scope_hash !== next.scope_hash
1061
+ || next.authorization?.actor_type !== 'human'
1062
+ || !authorizationFreshAt(next.authorization, record.occurred_at)
1063
+ || next.smoke?.verdict !== 'passed') {
1064
+ fail('CORRUPT_RELEASE_STATE', 'release intent authority or idempotency binding is invalid');
1065
+ }
1066
+ }
1067
+ if (['release_completed', 'release_reconciled'].includes(record.type)) {
1068
+ const receipt = record.type === 'release_reconciled' ? record.payload.lookup?.receipt : record.payload.receipt;
1069
+ if (!receipt || next.receipt_ref !== receipt.receipt_ref
1070
+ || next.release_receipt?.receipt_ref !== receipt.receipt_ref
1071
+ || (receipt.operation && receipt.operation !== 'release')
1072
+ || (receipt.operation_scope_hash && receipt.operation_scope_hash !== next.scope_hash)
1073
+ || (receipt.idempotency_key && receipt.idempotency_key !== next.idempotency_key)
1074
+ || (receipt.environment && receipt.environment !== next.environment)
1075
+ || (receipt.transport_id && receipt.transport_id !== next.transport_id)) {
1076
+ fail('CORRUPT_RELEASE_STATE', 'release completion receipt is not bound to its attempt');
1077
+ }
1078
+ if (receipt.observed_at) {
1079
+ assertReleaseReceiptTime(receipt, next, record.occurred_at, {
1080
+ code: 'CORRUPT_RELEASE_STATE',
1081
+ message: 'release completion receipt timestamp is inconsistent with its authorization and journal record',
1082
+ });
1083
+ }
1084
+ }
1085
+ return true;
1086
+ }
1087
+ if (Object.hasOwn(rollbackTransitions, record.type)) {
1088
+ const next = record.payload.rollback;
1089
+ object(next, 'CORRUPT_RELEASE_STATE', 'rollback transition');
1090
+ string(next.rollback_attempt_id, 'CORRUPT_RELEASE_STATE', 'rollback.rollback_attempt_id');
1091
+ const previous = this.#rollbacks.get(next.rollback_attempt_id) ?? null;
1092
+ const transition = rollbackTransitions[record.type];
1093
+ if (transition.from === null ? previous !== null : !previous || !transition.from.includes(previous.state)) {
1094
+ fail('CORRUPT_RELEASE_STATE', `invalid ${record.type} predecessor`);
1095
+ }
1096
+ const expectedState = transition.to === 'same' ? previous.state : transition.to;
1097
+ if (next.state !== expectedState) fail('CORRUPT_RELEASE_STATE', `invalid ${record.type} state`);
1098
+ if (previous) this.#assertRollbackIdentity(previous, next);
1099
+ const sourceRelease = this.#releases.get(next.release_attempt_id);
1100
+ if (!sourceRelease || sourceRelease.state !== 'released') {
1101
+ fail('CORRUPT_RELEASE_STATE', 'rollback transition lacks its released source attempt');
1102
+ }
1103
+ if (record.type === 'rollback_attempt_created') {
1104
+ const expectedScope = rollbackScope(next, this.#authorityContext);
1105
+ if (sha256(next.scope) !== sha256(expectedScope) || next.scope_hash !== sha256(expectedScope)) {
1106
+ fail('CORRUPT_RELEASE_STATE', 'rollback attempt scope is not canonical');
1107
+ }
1108
+ }
1109
+ if (record.type === 'rollback_intent_recorded') {
1110
+ if (next.idempotency_key !== sha256({ operation: 'rollback', scope_hash: next.scope_hash })
1111
+ || record.payload.idempotency_key !== next.idempotency_key
1112
+ || record.payload.authorization?.authorization_id !== next.authorization?.authorization_id
1113
+ || next.authorization?.scope_hash !== next.scope_hash
1114
+ || next.authorization?.actor_type !== 'human'
1115
+ || !authorizationFreshAt(next.authorization, record.occurred_at)
1116
+ || next.smoke?.verdict !== 'passed') {
1117
+ fail('CORRUPT_RELEASE_STATE', 'rollback intent authority or idempotency binding is invalid');
1118
+ }
1119
+ }
1120
+ if (['rollback_completed', 'rollback_reconciled'].includes(record.type)) {
1121
+ const receipt = record.type === 'rollback_reconciled' ? record.payload.lookup?.receipt : record.payload.receipt;
1122
+ if (!receipt || next.receipt_ref !== receipt.receipt_ref
1123
+ || next.release_receipt?.receipt_ref !== receipt.receipt_ref
1124
+ || (receipt.operation && receipt.operation !== 'rollback')
1125
+ || (receipt.operation_scope_hash && receipt.operation_scope_hash !== next.scope_hash)
1126
+ || (receipt.idempotency_key && receipt.idempotency_key !== next.idempotency_key)
1127
+ || (receipt.environment && receipt.environment !== sourceRelease.environment)
1128
+ || (receipt.transport_id && receipt.transport_id !== next.transport_id)) {
1129
+ fail('CORRUPT_RELEASE_STATE', 'rollback completion receipt is not bound to its attempt');
1130
+ }
1131
+ if (receipt.observed_at) {
1132
+ assertReleaseReceiptTime(receipt, next, record.occurred_at, {
1133
+ code: 'CORRUPT_RELEASE_STATE',
1134
+ message: 'rollback completion receipt timestamp is inconsistent with its authorization and journal record',
1135
+ });
1136
+ }
1137
+ }
1138
+ return true;
1139
+ }
1140
+ fail('CORRUPT_RELEASE_STATE', `unsupported release journal record type ${record.type}`);
1141
+ }
1142
+
1143
+ #apply(record) {
1144
+ this.#validateRecordSemantics(record);
1145
+ if (record.payload.release) this.#releases.set(record.payload.release.release_attempt_id, record.payload.release);
1146
+ if (record.payload.rollback) this.#rollbacks.set(record.payload.rollback.rollback_attempt_id, record.payload.rollback);
1147
+ if (record.type.endsWith('_intent_recorded') && record.payload.authorization?.authorization_id) {
1148
+ this.#consumedAuthorizationIds.add(record.payload.authorization.authorization_id);
1149
+ }
1150
+ const smokeEvidenceId = record.payload.release?.smoke?.evidence_id ?? record.payload.rollback?.smoke?.evidence_id;
1151
+ if (record.type.endsWith('_intent_recorded') && smokeEvidenceId) this.#consumedSmokeEvidenceIds.add(smokeEvidenceId);
1152
+ }
1153
+
1154
+ #load() {
1155
+ this.#records = [];
1156
+ this.#releases = new Map();
1157
+ this.#rollbacks = new Map();
1158
+ this.#consumedAuthorizationIds = new Set();
1159
+ this.#consumedSmokeEvidenceIds = new Set();
1160
+ const loaded = readDurable(
1161
+ this.#statePath, this.#activeStorage,
1162
+ this.#stateAuthority, this.#requireAuthenticatedState,
1163
+ );
1164
+ this.#stateIdentity = loaded.identity;
1165
+ for (const value of loaded.records) {
1166
+ const record = freezeData(value, 'CORRUPT_RELEASE_STATE', 'release record');
1167
+ this.#records.push(record);
1168
+ this.#apply(record);
1169
+ }
1170
+ }
1171
+
1172
+ #exclusive(action) {
1173
+ if (!this.#statePath) return action();
1174
+ if (this.#activeStorage) fail('RELEASE_STATE_REENTRANCY', 'release state mutation cannot nest an exclusive storage operation');
1175
+ const lock = acquireReleaseLock(this.#statePath);
1176
+ this.#activeStorage = lock.storage;
1177
+ try {
1178
+ this.#load();
1179
+ return action();
1180
+ } finally {
1181
+ this.#activeStorage = null;
1182
+ lock.release();
1183
+ }
76
1184
  }
77
- get records() { return freezeData([...this.#records]); }
1185
+
78
1186
  #record(type, payload) {
79
1187
  const previous_hash = this.#records.at(-1)?.chain_hash ?? 'GENESIS';
80
1188
  const material = freezeData({ schema_version: 1, sequence: this.#records.length + 1, type, occurred_at: at(this.#clock), payload });
81
- const recordHash = sha256(material);
82
- const record = freezeData({ ...material, record_hash: recordHash, previous_hash, chain_hash: sha256({ previous_hash, record_hash: recordHash }) });
1189
+ const record_hash = sha256(material);
1190
+ const core = freezeData({
1191
+ ...material,
1192
+ record_hash,
1193
+ previous_hash,
1194
+ chain_hash: sha256({ previous_hash, record_hash }),
1195
+ });
1196
+ const authority = authenticateReleaseRecord(core, this.#stateAuthority);
1197
+ const record = freezeData({ ...core, ...(authority ?? {}) });
1198
+ this.#validateRecordSemantics(record);
1199
+ if (this.#statePath) {
1200
+ this.#stateIdentity = appendDurable(
1201
+ this.#statePath, record, this.#stateIdentity, this.#activeStorage,
1202
+ this.#stateAuthority, this.#requireAuthenticatedState,
1203
+ );
1204
+ }
83
1205
  this.#records.push(record);
1206
+ this.#apply(record);
84
1207
  return record;
85
1208
  }
1209
+
1210
+ #assertExternalBoundary() {
1211
+ if (!this.#statePath) return;
1212
+ assertExternalCapacity(this.#statePath, this.#activeStorage, this.#stateIdentity);
1213
+ assertPinnedReleaseDirectory(this.#activeStorage);
1214
+ }
1215
+
86
1216
  #verifyCompletions(refs) {
87
1217
  return freezeData(refs.map((ref) => {
88
1218
  const completion = this.#verifyCompletion(ref);
@@ -90,82 +1220,320 @@ export class ReleaseLane {
90
1220
  if (completion.state !== 'merged') fail('SOURCE_COMPLETION_NOT_MERGED', 'source completion must be C-06 merged evidence', { ref });
91
1221
  string(completion.completion_id, 'UNVERIFIABLE_SOURCE_COMPLETION', `completion ${ref}.completion_id`);
92
1222
  string(completion.immutable_ref, 'UNVERIFIABLE_SOURCE_COMPLETION', `completion ${ref}.immutable_ref`);
1223
+ string(completion.git?.result_ref, 'UNVERIFIABLE_SOURCE_COMPLETION', `completion ${ref}.git.result_ref`);
93
1224
  if (completion.immutable_ref !== ref) fail('SOURCE_COMPLETION_REF_MISMATCH', 'completion verifier did not bind the requested immutable reference', { ref });
94
- return { completion_id: completion.completion_id, immutable_ref: completion.immutable_ref, state: completion.state };
1225
+ return {
1226
+ completion_id: completion.completion_id, immutable_ref: completion.immutable_ref,
1227
+ state: completion.state, result_commit_ref: completion.git.result_ref,
1228
+ };
95
1229
  }));
96
1230
  }
1231
+
97
1232
  createReleaseAttempt(request = {}) {
1233
+ return this.#exclusive(() => {
98
1234
  object(request, 'INVALID_RELEASE_ATTEMPT', 'release request');
1235
+ boundedData(request, 'INVALID_RELEASE_ATTEMPT', 'release request');
99
1236
  for (const field of ['release_attempt_id', 'environment', 'release_plan_ref', 'rollback_plan_ref']) string(request[field], 'INVALID_RELEASE_ATTEMPT', field);
100
1237
  strings(request.source_completion_refs, 'INVALID_RELEASE_ATTEMPT', 'source_completion_refs');
101
1238
  if (this.#releases.has(request.release_attempt_id)) fail('RELEASE_ATTEMPT_EXISTS', 'release_attempt_id already exists');
102
- const verifiedCompletions = this.#verifyCompletions(request.source_completion_refs);
103
- const scope = releaseScope(request);
104
- const attempt = freezeData({ ...request, verified_completions: verifiedCompletions, scope, scope_hash: sha256(scope), state: 'created' });
105
- this.#releases.set(attempt.release_attempt_id, attempt);
106
- this.#record('release_attempt_created', { attempt });
107
- return attempt;
1239
+ const verified_completions = this.#verifyCompletions(request.source_completion_refs);
1240
+ const scope = releaseScope(request, this.#authorityContext);
1241
+ const release = freezeData({ ...request, verified_completions, scope, scope_hash: sha256(scope), state: 'created' });
1242
+ this.#record('release_attempt_created', { release });
1243
+ return release;
1244
+ });
1245
+ }
1246
+
1247
+ #rejectRelease(attempt, error) {
1248
+ const release = freezeData({ ...attempt, state: 'rejected', rejection_code: error.code ?? 'RELEASE_PREFLIGHT_FAILED' });
1249
+ this.#record('release_rejected', { release, code: release.rejection_code });
108
1250
  }
1251
+
109
1252
  executeRelease({ release_attempt_id, authorization: approval, smoke: smokeInput, adapter } = {}) {
1253
+ return this.#exclusive(() => {
110
1254
  string(release_attempt_id, 'INVALID_RELEASE_ATTEMPT', 'release_attempt_id');
111
1255
  const attempt = this.#releases.get(release_attempt_id);
112
1256
  if (!attempt) fail('RELEASE_ATTEMPT_NOT_FOUND', 'release attempt does not exist');
1257
+ if (['pending_external', 'sent_unconfirmed'].includes(attempt.state)) fail('RELEASE_RECONCILIATION_REQUIRED', 'release result must be reconciled before another send');
113
1258
  if (attempt.state !== 'created') fail('RELEASE_ATTEMPT_TERMINAL', 'release attempt is no longer executable');
1259
+ let humanAuthorization;
1260
+ let smokeEvidence;
1261
+ let capability;
114
1262
  try {
115
- const humanAuthorization = authorization(approval, attempt.scope_hash);
116
- const smokeEvidence = smoke(smokeInput);
117
- const capability = fakeAdapter(adapter);
118
- this.#record('release_authorized', { release_attempt_id, authorization: humanAuthorization });
119
- this.#record('release_smoke_verified', { release_attempt_id, smoke: smokeEvidence });
120
- const receipt = capability.release(freezeData({ attempt, authorization: humanAuthorization, smoke: smokeEvidence }));
121
- string(receipt?.receipt_ref, 'INVALID_FAKE_RECEIPT', 'fake release receipt_ref');
122
- const released = freezeData({ ...attempt, state: 'released', authorization: humanAuthorization, smoke: smokeEvidence, receipt_ref: receipt.receipt_ref });
123
- this.#releases.set(release_attempt_id, released);
124
- this.#record('release_completed', { release_attempt_id, receipt_ref: receipt.receipt_ref });
1263
+ humanAuthorization = authorization(
1264
+ approval, attempt.scope_hash, this.#authorizationVerifier,
1265
+ this.#consumedAuthorizationIds, this.#clock,
1266
+ );
1267
+ smokeEvidence = smoke(smokeInput, {
1268
+ attempt, verifier: this.#smokeVerifier, clock: this.#clock,
1269
+ consumedEvidenceIds: this.#consumedSmokeEvidenceIds,
1270
+ });
1271
+ capability = releaseTransport(adapter);
1272
+ if (this.#requireProductionTransport && !capability.production) {
1273
+ fail('FAKE_RELEASE_TRANSPORT_FORBIDDEN', 'project release lanes require an externally certified production transport');
1274
+ }
1275
+ if (capability.production && !this.#statePath) fail('MISSING_RELEASE_STATE_PATH', 'production release requires durable state_path');
1276
+ } catch (error) {
1277
+ this.#rejectRelease(attempt, error);
1278
+ throw error;
1279
+ }
1280
+ const idempotency_key = sha256({ operation: 'release', scope_hash: attempt.scope_hash });
1281
+ if (attempt.idempotency_key && attempt.idempotency_key !== idempotency_key) {
1282
+ fail('RELEASE_IDEMPOTENCY_CONFLICT', 'release retry changed its scope-bound idempotency key');
1283
+ }
1284
+ this.#assertExternalBoundary();
1285
+ const pending = freezeData({ ...attempt, state: 'pending_external', authorization: humanAuthorization, smoke: smokeEvidence, idempotency_key, transport_id: capability.transport_id });
1286
+ this.#record('release_intent_recorded', { release: pending, authorization: humanAuthorization, idempotency_key });
1287
+ this.#assertExternalBoundary();
1288
+ try {
1289
+ const receipt = capability.release(freezeData({ attempt: pending, authorization: humanAuthorization, smoke: smokeEvidence, idempotency_key }));
1290
+ if (capability.production) assertReleaseReceiptTime(receipt, pending, at(this.#clock));
1291
+ const released = freezeData({ ...pending, state: 'released', receipt_ref: receipt.receipt_ref, release_receipt: receipt });
1292
+ this.#record('release_completed', { release: released, receipt, idempotency_key });
125
1293
  return released;
126
1294
  } catch (error) {
127
- this.#releases.set(release_attempt_id, freezeData({ ...attempt, state: 'rejected', rejection_code: error.code ?? 'RELEASE_EXECUTION_FAILED' }));
128
- this.#record('release_rejected', { release_attempt_id, code: error.code ?? 'RELEASE_EXECUTION_FAILED' });
1295
+ const uncertain = freezeData({ ...pending, state: 'sent_unconfirmed', uncertainty_code: error.code ?? 'RELEASE_EXTERNAL_RESULT_UNKNOWN' });
1296
+ this.#record('release_sent_unconfirmed', { release: uncertain, code: uncertain.uncertainty_code, idempotency_key });
1297
+ return fail('RELEASE_RESULT_UNCONFIRMED', 'release may have completed externally and requires reconciliation', { cause_code: uncertain.uncertainty_code, idempotency_key });
1298
+ }
1299
+ });
1300
+ }
1301
+
1302
+ reconcileRelease({ release_attempt_id, adapter } = {}) {
1303
+ return this.#exclusive(() => {
1304
+ string(release_attempt_id, 'INVALID_RELEASE_ATTEMPT', 'release_attempt_id');
1305
+ const attempt = this.#releases.get(release_attempt_id);
1306
+ if (!attempt) fail('RELEASE_ATTEMPT_NOT_FOUND', 'release attempt does not exist');
1307
+ if (!['pending_external', 'sent_unconfirmed'].includes(attempt.state)) fail('RELEASE_RECONCILIATION_NOT_REQUIRED', 'release is not awaiting reconciliation');
1308
+ const capability = releaseTransport(adapter);
1309
+ if (this.#requireProductionTransport && !capability.production) {
1310
+ fail('FAKE_RELEASE_TRANSPORT_FORBIDDEN', 'project release lanes require an externally certified production transport');
1311
+ }
1312
+ if (capability.transport_id !== attempt.transport_id) fail('RELEASE_TRANSPORT_MISMATCH', 'reconciliation must use the original transport');
1313
+ this.#assertExternalBoundary();
1314
+ let lookup;
1315
+ try {
1316
+ lookup = capability.lookup(freezeData({ operation: 'release', idempotency_key: attempt.idempotency_key, attempt }));
1317
+ } catch (error) {
1318
+ this.#record('release_reconciliation_failed', { release: attempt, code: error.code ?? 'RELEASE_LOOKUP_FAILED' });
129
1319
  throw error;
130
1320
  }
1321
+ if (lookup.status === 'confirmed') {
1322
+ const receipt = lookup.receipt;
1323
+ object(receipt, 'INVALID_RELEASE_RECEIPT', 'release lookup receipt');
1324
+ string(receipt.receipt_ref, 'INVALID_RELEASE_RECEIPT', 'release lookup receipt_ref');
1325
+ if (capability.production) assertReleaseReceiptTime(receipt, attempt, at(this.#clock));
1326
+ const released = freezeData({ ...attempt, state: 'released', receipt_ref: receipt.receipt_ref, release_receipt: receipt });
1327
+ this.#record('release_reconciled', { release: released, lookup });
1328
+ return released;
1329
+ }
1330
+ if (lookup.status === 'not_found') {
1331
+ const retryable = freezeData({ ...attempt, state: 'created' });
1332
+ this.#record('release_reconciled_not_found', { release: retryable, lookup });
1333
+ return retryable;
1334
+ }
1335
+ if (lookup.status !== 'unknown') fail('INVALID_RELEASE_LOOKUP', 'lookup status must be confirmed, not_found or unknown');
1336
+ this.#record('release_reconciliation_inconclusive', { release: attempt, lookup });
1337
+ return attempt;
1338
+ });
131
1339
  }
1340
+
132
1341
  executeRollback({ rollback_attempt_id, release_attempt_id, rollback_plan_ref, authorization: approval, smoke: smokeInput, adapter } = {}) {
1342
+ return this.#exclusive(() => {
133
1343
  string(rollback_attempt_id, 'INVALID_ROLLBACK_ATTEMPT', 'rollback_attempt_id');
134
1344
  string(release_attempt_id, 'INVALID_ROLLBACK_ATTEMPT', 'release_attempt_id');
135
1345
  string(rollback_plan_ref, 'INVALID_ROLLBACK_ATTEMPT', 'rollback_plan_ref');
136
- if (this.#rollbacks.has(rollback_attempt_id)) fail('ROLLBACK_ATTEMPT_EXISTS', 'rollback_attempt_id already exists');
1346
+ let attempt = this.#rollbacks.get(rollback_attempt_id);
1347
+ if (attempt) {
1348
+ if (['pending_external', 'sent_unconfirmed'].includes(attempt.state)) fail('ROLLBACK_RECONCILIATION_REQUIRED', 'rollback result must be reconciled before another send');
1349
+ if (attempt.state !== 'created') fail('ROLLBACK_ATTEMPT_EXISTS', 'rollback_attempt_id already exists');
1350
+ }
137
1351
  const release = this.#releases.get(release_attempt_id);
138
1352
  if (!release || release.state !== 'released') fail('ROLLBACK_SOURCE_NOT_RELEASED', 'rollback requires a separately recorded released attempt');
139
- const scope = rollbackScope({ rollback_attempt_id, release_attempt_id, rollback_plan_ref });
140
- const attempt = freezeData({ rollback_attempt_id, release_attempt_id, rollback_plan_ref, scope, scope_hash: sha256(scope), state: 'created' });
141
- this.#rollbacks.set(rollback_attempt_id, attempt);
142
- this.#record('rollback_attempt_created', { attempt });
1353
+ if (!attempt) {
1354
+ const scope = rollbackScope(
1355
+ { rollback_attempt_id, release_attempt_id, rollback_plan_ref },
1356
+ this.#authorityContext,
1357
+ );
1358
+ attempt = freezeData({ rollback_attempt_id, release_attempt_id, rollback_plan_ref, scope, scope_hash: sha256(scope), state: 'created' });
1359
+ this.#record('rollback_attempt_created', { rollback: attempt });
1360
+ } else if (attempt.release_attempt_id !== release_attempt_id || attempt.rollback_plan_ref !== rollback_plan_ref) {
1361
+ fail('ROLLBACK_ATTEMPT_MISMATCH', 'retry must preserve the reconciled rollback scope');
1362
+ }
1363
+ let humanAuthorization;
1364
+ let smokeEvidence;
1365
+ let capability;
1366
+ try {
1367
+ humanAuthorization = authorization(
1368
+ approval, attempt.scope_hash, this.#authorizationVerifier,
1369
+ this.#consumedAuthorizationIds, this.#clock,
1370
+ );
1371
+ smokeEvidence = smoke(smokeInput, {
1372
+ attempt, release, verifier: this.#smokeVerifier, clock: this.#clock,
1373
+ consumedEvidenceIds: this.#consumedSmokeEvidenceIds,
1374
+ });
1375
+ capability = releaseTransport(adapter);
1376
+ if (this.#requireProductionTransport && !capability.production) {
1377
+ fail('FAKE_RELEASE_TRANSPORT_FORBIDDEN', 'project release lanes require an externally certified production transport');
1378
+ }
1379
+ if (capability.production && !this.#statePath) fail('MISSING_RELEASE_STATE_PATH', 'production rollback requires durable state_path');
1380
+ } catch (error) {
1381
+ const rejected = freezeData({ ...attempt, state: 'rejected', rejection_code: error.code ?? 'ROLLBACK_PREFLIGHT_FAILED' });
1382
+ this.#record('rollback_rejected', { rollback: rejected, code: rejected.rejection_code });
1383
+ throw error;
1384
+ }
1385
+ const idempotency_key = sha256({ operation: 'rollback', scope_hash: attempt.scope_hash });
1386
+ if (attempt.idempotency_key && attempt.idempotency_key !== idempotency_key) {
1387
+ fail('ROLLBACK_IDEMPOTENCY_CONFLICT', 'rollback retry changed its scope-bound idempotency key');
1388
+ }
1389
+ this.#assertExternalBoundary();
1390
+ const pending = freezeData({ ...attempt, state: 'pending_external', authorization: humanAuthorization, smoke: smokeEvidence, idempotency_key, transport_id: capability.transport_id });
1391
+ this.#record('rollback_intent_recorded', { rollback: pending, authorization: humanAuthorization, idempotency_key });
1392
+ this.#assertExternalBoundary();
143
1393
  try {
144
- const humanAuthorization = authorization(approval, attempt.scope_hash);
145
- const smokeEvidence = smoke(smokeInput);
146
- const capability = fakeAdapter(adapter);
147
- this.#record('rollback_authorized', { rollback_attempt_id, authorization: humanAuthorization });
148
- this.#record('rollback_smoke_verified', { rollback_attempt_id, smoke: smokeEvidence });
149
- const receipt = capability.rollback(freezeData({ attempt, release, authorization: humanAuthorization, smoke: smokeEvidence }));
150
- string(receipt?.receipt_ref, 'INVALID_FAKE_RECEIPT', 'fake rollback receipt_ref');
151
- const rolledBack = freezeData({ ...attempt, state: 'rolled_back', authorization: humanAuthorization, smoke: smokeEvidence, receipt_ref: receipt.receipt_ref });
152
- this.#rollbacks.set(rollback_attempt_id, rolledBack);
153
- this.#record('rollback_completed', { rollback_attempt_id, receipt_ref: receipt.receipt_ref });
1394
+ const receipt = capability.rollback(freezeData({ attempt: pending, release, authorization: humanAuthorization, smoke: smokeEvidence, idempotency_key }));
1395
+ if (capability.production) assertReleaseReceiptTime(receipt, pending, at(this.#clock));
1396
+ const rolledBack = freezeData({ ...pending, state: 'rolled_back', receipt_ref: receipt.receipt_ref, release_receipt: receipt });
1397
+ this.#record('rollback_completed', { rollback: rolledBack, receipt, idempotency_key });
154
1398
  return rolledBack;
155
1399
  } catch (error) {
156
- this.#rollbacks.set(rollback_attempt_id, freezeData({ ...attempt, state: 'rejected', rejection_code: error.code ?? 'ROLLBACK_EXECUTION_FAILED' }));
157
- this.#record('rollback_rejected', { rollback_attempt_id, code: error.code ?? 'ROLLBACK_EXECUTION_FAILED' });
1400
+ const uncertain = freezeData({ ...pending, state: 'sent_unconfirmed', uncertainty_code: error.code ?? 'ROLLBACK_EXTERNAL_RESULT_UNKNOWN' });
1401
+ this.#record('rollback_sent_unconfirmed', { rollback: uncertain, code: uncertain.uncertainty_code, idempotency_key });
1402
+ return fail('ROLLBACK_RESULT_UNCONFIRMED', 'rollback may have completed externally and requires reconciliation', { cause_code: uncertain.uncertainty_code, idempotency_key });
1403
+ }
1404
+ });
1405
+ }
1406
+
1407
+ reconcileRollback({ rollback_attempt_id, adapter } = {}) {
1408
+ return this.#exclusive(() => {
1409
+ string(rollback_attempt_id, 'INVALID_ROLLBACK_ATTEMPT', 'rollback_attempt_id');
1410
+ const attempt = this.#rollbacks.get(rollback_attempt_id);
1411
+ if (!attempt) fail('ROLLBACK_ATTEMPT_NOT_FOUND', 'rollback attempt does not exist');
1412
+ if (!['pending_external', 'sent_unconfirmed'].includes(attempt.state)) fail('ROLLBACK_RECONCILIATION_NOT_REQUIRED', 'rollback is not awaiting reconciliation');
1413
+ const release = this.#releases.get(attempt.release_attempt_id);
1414
+ if (!release || release.state !== 'released') fail('ROLLBACK_SOURCE_NOT_RELEASED', 'rollback reconciliation requires its released source');
1415
+ const capability = releaseTransport(adapter);
1416
+ if (this.#requireProductionTransport && !capability.production) {
1417
+ fail('FAKE_RELEASE_TRANSPORT_FORBIDDEN', 'project release lanes require an externally certified production transport');
1418
+ }
1419
+ if (capability.transport_id !== attempt.transport_id) fail('RELEASE_TRANSPORT_MISMATCH', 'reconciliation must use the original transport');
1420
+ this.#assertExternalBoundary();
1421
+ let lookup;
1422
+ try {
1423
+ lookup = capability.lookup(freezeData({
1424
+ operation: 'rollback', idempotency_key: attempt.idempotency_key, attempt, release,
1425
+ }));
1426
+ } catch (error) {
1427
+ this.#record('rollback_reconciliation_failed', { rollback: attempt, code: error.code ?? 'ROLLBACK_LOOKUP_FAILED' });
158
1428
  throw error;
159
1429
  }
1430
+ if (lookup.status === 'confirmed') {
1431
+ const receipt = lookup.receipt;
1432
+ object(receipt, 'INVALID_RELEASE_RECEIPT', 'rollback lookup receipt');
1433
+ string(receipt.receipt_ref, 'INVALID_RELEASE_RECEIPT', 'rollback lookup receipt_ref');
1434
+ if (capability.production) assertReleaseReceiptTime(receipt, attempt, at(this.#clock));
1435
+ const rolledBack = freezeData({ ...attempt, state: 'rolled_back', receipt_ref: receipt.receipt_ref, release_receipt: receipt });
1436
+ this.#record('rollback_reconciled', { rollback: rolledBack, lookup });
1437
+ return rolledBack;
1438
+ }
1439
+ if (lookup.status === 'not_found') {
1440
+ const retryable = freezeData({ ...attempt, state: 'created' });
1441
+ this.#record('rollback_reconciled_not_found', { rollback: retryable, lookup });
1442
+ return retryable;
1443
+ }
1444
+ if (lookup.status !== 'unknown') fail('INVALID_RELEASE_LOOKUP', 'lookup status must be confirmed, not_found or unknown');
1445
+ this.#record('rollback_reconciliation_inconclusive', { rollback: attempt, lookup });
1446
+ return attempt;
1447
+ });
1448
+ }
1449
+ }
1450
+
1451
+ /** Production transport capability with idempotent status lookup. */
1452
+ export class ReleaseTransport {
1453
+ constructor({ transport_id, authority_id, public_key, release, rollback, lookup } = {}) {
1454
+ string(transport_id, 'INVALID_RELEASE_TRANSPORT', 'transport_id');
1455
+ string(authority_id, 'INVALID_RELEASE_TRANSPORT', 'authority_id');
1456
+ let key;
1457
+ try {
1458
+ key = public_key?.type === 'public' ? public_key : createPublicKey(public_key);
1459
+ } catch {
1460
+ fail('INVALID_RELEASE_TRANSPORT', 'public_key must be a valid Ed25519 deployment authority key');
1461
+ }
1462
+ if (key.type !== 'public' || key.asymmetricKeyType !== 'ed25519') {
1463
+ fail('INVALID_RELEASE_TRANSPORT', 'public_key must be a valid Ed25519 deployment authority key');
1464
+ }
1465
+ if (typeof release !== 'function' || typeof rollback !== 'function' || typeof lookup !== 'function') {
1466
+ fail('INVALID_RELEASE_TRANSPORT', 'release, rollback and lookup must be functions');
1467
+ }
1468
+ const verifyReceipt = (operation, input, receipt) => {
1469
+ object(receipt, 'INVALID_RELEASE_RECEIPT', `${operation} receipt`);
1470
+ for (const field of [
1471
+ 'receipt_ref', 'deployment_ref', 'issuer_id', 'transport_id', 'operation',
1472
+ 'operation_scope_hash', 'environment', 'idempotency_key', 'observed_at', 'signature',
1473
+ ]) string(receipt[field], 'INVALID_RELEASE_RECEIPT', `${operation} receipt.${field}`);
1474
+ const environment = operation === 'release' ? input.attempt.environment : input.release.environment;
1475
+ if (receipt.issuer_id !== authority_id || receipt.transport_id !== transport_id
1476
+ || receipt.operation !== operation || receipt.operation_scope_hash !== input.attempt.scope_hash
1477
+ || receipt.environment !== environment || receipt.idempotency_key !== input.idempotency_key) {
1478
+ fail('RELEASE_RECEIPT_BINDING_MISMATCH', `${operation} receipt is not bound to the exact remote operation`);
1479
+ }
1480
+ if (!RFC3339.test(receipt.observed_at) || Number.isNaN(Date.parse(receipt.observed_at))) {
1481
+ fail('INVALID_RELEASE_RECEIPT', `${operation} receipt.observed_at must be RFC3339`);
1482
+ }
1483
+ boundedData(receipt, 'INVALID_RELEASE_RECEIPT', `${operation} receipt`, 64 * 1024);
1484
+ let valid;
1485
+ try {
1486
+ valid = verifySignature(
1487
+ null,
1488
+ Buffer.from(JSON.stringify(signedReleaseReceiptPayload(receipt))),
1489
+ key,
1490
+ Buffer.from(receipt.signature, 'base64'),
1491
+ );
1492
+ } catch { valid = false; }
1493
+ if (!valid) fail('INVALID_RELEASE_RECEIPT_SIGNATURE', `${operation} receipt signature is invalid`);
1494
+ return freezeData(receipt, 'INVALID_RELEASE_RECEIPT', `${operation} receipt`);
1495
+ };
1496
+ const invoke = (operation, handler, input) => {
1497
+ const receipt = handler(input);
1498
+ return verifyReceipt(operation, input, receipt);
1499
+ };
1500
+ const invokeLookup = (input) => {
1501
+ const result = lookup(input);
1502
+ object(result, 'INVALID_RELEASE_LOOKUP', 'release lookup');
1503
+ if (!['confirmed', 'not_found', 'unknown'].includes(result.status)) fail('INVALID_RELEASE_LOOKUP', 'lookup status must be confirmed, not_found or unknown');
1504
+ if (result.status !== 'confirmed') return freezeData(result, 'INVALID_RELEASE_LOOKUP', 'release lookup');
1505
+ return freezeData({ status: 'confirmed', receipt: verifyReceipt(input.operation, input, result.receipt) }, 'INVALID_RELEASE_LOOKUP', 'release lookup');
1506
+ };
1507
+ RELEASE_TRANSPORT_CAPABILITIES.set(this, Object.freeze({
1508
+ production: true, transport_id,
1509
+ release: (input) => invoke('release', release, input),
1510
+ rollback: (input) => invoke('rollback', rollback, input),
1511
+ lookup: invokeLookup,
1512
+ }));
1513
+ Object.freeze(this);
160
1514
  }
161
1515
  }
162
1516
 
163
1517
  export class FakeReleaseTransport {
164
1518
  #attempts = [];
1519
+ #receipts = new Map();
165
1520
  constructor() {
1521
+ const transport_id = 'transport://fake/local';
1522
+ const execute = (operation, input) => {
1523
+ const id = operation === 'release' ? input.attempt.release_attempt_id : input.attempt.rollback_attempt_id;
1524
+ const receipt_ref = `fake://${operation}/${id}`;
1525
+ this.#attempts.push(freezeData({ operation, idempotency_key: input.idempotency_key, [`${operation}_attempt_id`]: id }));
1526
+ this.#receipts.set(input.idempotency_key, receipt_ref);
1527
+ return freezeData({ receipt_ref, transport_id });
1528
+ };
166
1529
  FAKE_TRANSPORT_CAPABILITIES.set(this, Object.freeze({
167
- release: (input) => { this.#attempts.push(freezeData({ operation: 'release', release_attempt_id: input.attempt.release_attempt_id })); return freezeData({ receipt_ref: `fake://release/${input.attempt.release_attempt_id}` }); },
168
- rollback: (input) => { this.#attempts.push(freezeData({ operation: 'rollback', rollback_attempt_id: input.attempt.rollback_attempt_id })); return freezeData({ receipt_ref: `fake://rollback/${input.attempt.rollback_attempt_id}` }); },
1530
+ production: false, transport_id,
1531
+ release: (input) => execute('release', input),
1532
+ rollback: (input) => execute('rollback', input),
1533
+ lookup: ({ idempotency_key }) => {
1534
+ const receipt_ref = this.#receipts.get(idempotency_key);
1535
+ return freezeData(receipt_ref ? { status: 'confirmed', receipt_ref } : { status: 'not_found' });
1536
+ },
169
1537
  }));
170
1538
  }
171
1539
  get attempts() { return freezeData([...this.#attempts]); }