@sentropic/artifacts 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/adapters/postgres-commit.d.ts +15 -0
- package/dist/adapters/postgres-commit.js +80 -0
- package/dist/adapters/postgres.d.ts +5 -2
- package/dist/adapters/postgres.js +39 -0
- package/dist/http.d.ts +41 -30
- package/dist/http.js +168 -507
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp-core.d.ts +29 -39
- package/dist/mcp-core.js +83 -244
- package/dist/mcp.js +5 -5
- package/dist/profile/contracts.d.ts +67 -0
- package/dist/profile/contracts.js +31 -0
- package/dist/profile/index.d.ts +6 -0
- package/dist/profile/index.js +117 -0
- package/dist/profile/json.d.ts +10 -0
- package/dist/profile/json.js +76 -0
- package/dist/profile/radar.d.ts +3 -0
- package/dist/profile/radar.js +21 -0
- package/dist/profile/schemas.d.ts +6 -0
- package/dist/profile/schemas.js +76 -0
- package/dist/profile/validation.d.ts +5 -0
- package/dist/profile/validation.js +104 -0
- package/dist/profile/validators.d.ts +3 -0
- package/dist/profile/validators.js +51 -0
- package/dist/server/authorized-replay.d.ts +33 -0
- package/dist/server/authorized-replay.js +31 -0
- package/dist/server/command-identity.d.ts +52 -0
- package/dist/server/command-identity.js +71 -0
- package/dist/server/commit/brands.d.ts +7 -0
- package/dist/server/commit/brands.js +3 -0
- package/dist/server/commit/coordinator.d.ts +2 -0
- package/dist/server/commit/coordinator.js +729 -0
- package/dist/server/commit/types.d.ts +379 -0
- package/dist/server/commit/types.js +94 -0
- package/dist/server/commit-contract.d.ts +4 -0
- package/dist/server/commit-contract.js +2 -0
- package/dist/server/failures.d.ts +84 -0
- package/dist/server/failures.js +133 -0
- package/dist/server/gc.d.ts +42 -1
- package/dist/server/gc.js +168 -5
- package/dist/server/idempotency.d.ts +4 -0
- package/dist/server/idempotency.js +14 -0
- package/dist/server/lifecycle.d.ts +2 -1
- package/dist/server/lifecycle.js +15 -1
- package/dist/server/object-intents.d.ts +14 -1
- package/dist/server/object-intents.js +31 -0
- package/dist/server/outcome-contract.d.ts +3 -0
- package/dist/server/outcome-contract.js +3 -0
- package/dist/server/outcome-projection.d.ts +21 -0
- package/dist/server/outcome-projection.js +29 -0
- package/dist/server/preparation-cleanup.d.ts +31 -0
- package/dist/server/preparation-cleanup.js +106 -0
- package/dist/server/publication.d.ts +76 -0
- package/dist/server/publication.js +270 -31
- package/dist/server/service.d.ts +60 -0
- package/dist/server/service.js +181 -0
- package/dist/testing.d.ts +50 -0
- package/dist/testing.js +86 -0
- package/docs/migration-l6.md +129 -0
- package/examples/radar-commit-setup.ts +299 -0
- package/l0-freeze/command-identity-contract.ts +303 -0
- package/l0-freeze/commit-contract.ts +421 -0
- package/l0-freeze/failure-contract.ts +174 -0
- package/l0-freeze/profile-contract.ts +219 -0
- package/migrations/0003_coordinator.sql +28 -0
- package/package.json +27 -1
- package/src/contracts.ts +125 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3 (immo #861) — atomic finalization coordinator (spec r2 §3.1 A4).
|
|
3
|
+
*
|
|
4
|
+
* One atomic database finalization unit per (scope, internal key):
|
|
5
|
+
* coordination row, key recheck, ordered locks (object, authority, quota,
|
|
6
|
+
* blob-claim), provisional writes, the host guard after all waits, fenced
|
|
7
|
+
* final predicate, one success insert and one COMMIT. The quota wait always
|
|
8
|
+
* precedes the guard decision and its locks are retained through COMMIT; a
|
|
9
|
+
* second guard invocation revalidates authority after the post-guard
|
|
10
|
+
* receipt/intent/barrier waits, so an approval expiring mid-unit denies
|
|
11
|
+
* instead of committing. Denial or genuine conflict rolls the unit back
|
|
12
|
+
* with no committed residue. A connection loss around COMMIT acknowledgment
|
|
13
|
+
* is a separate unknown outcome recovered by retrying the same key. The
|
|
14
|
+
* success table is distinct from idempotency_keys, which the coordinator
|
|
15
|
+
* never touches.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { COMMAND_CODEC_VERSION, COMMIT_PROTOCOL_VERSION, COORDINATOR_TEST_POINTS, PARTICIPANT_LOCK_ORDER, CommitQuarantineError, FAILURE_PROJECTIONS, } from './types.js';
|
|
19
|
+
import { commitFenceBrand, commitTransactionBrand } from './brands.js';
|
|
20
|
+
import { classifyArtifactFailure } from '../failures.js';
|
|
21
|
+
function failure(category, request, phase, commitState, cause, overrides) {
|
|
22
|
+
const row = FAILURE_PROJECTIONS.find((entry) => entry.category === category && entry.variant === overrides?.variant) ??
|
|
23
|
+
FAILURE_PROJECTIONS.find((entry) => entry.category === category);
|
|
24
|
+
if (row === undefined)
|
|
25
|
+
throw new Error(`unknown failure category ${category}`);
|
|
26
|
+
return {
|
|
27
|
+
kind: 'operational',
|
|
28
|
+
category,
|
|
29
|
+
variant: overrides?.variant ?? row.variant,
|
|
30
|
+
httpStatus: overrides?.status ?? row.httpStatus,
|
|
31
|
+
publicCode: overrides?.publicCode ?? row.publicCode,
|
|
32
|
+
retriable: overrides?.retriable ?? row.retriable,
|
|
33
|
+
invocationId: request.invocationId,
|
|
34
|
+
operation: request.operation,
|
|
35
|
+
phase,
|
|
36
|
+
commitState,
|
|
37
|
+
cause,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const isUniqueViolation = (error) => typeof error === 'object' && error !== null && error.code === '23505';
|
|
41
|
+
/**
|
|
42
|
+
* Shared acknowledgment-loss contract (r2 E3): PG connection loss plus the
|
|
43
|
+
* adapter-level unacknowledged-COMMIT code. Any of these around COMMIT
|
|
44
|
+
* means the outcome is unknown, never not_committed.
|
|
45
|
+
*/
|
|
46
|
+
const CONNECTION_LOSS_CODES = new Set([
|
|
47
|
+
'57P06',
|
|
48
|
+
'ECONNRESET',
|
|
49
|
+
'ECONNREFUSED',
|
|
50
|
+
'ETIMEDOUT',
|
|
51
|
+
'artifact_connection_lost',
|
|
52
|
+
]);
|
|
53
|
+
const isConnectionLoss = (error) => {
|
|
54
|
+
if (typeof error !== 'object' || error === null)
|
|
55
|
+
return false;
|
|
56
|
+
const code = error.code;
|
|
57
|
+
return (typeof code === 'string' && (CONNECTION_LOSS_CODES.has(code) || /^08[A-Z0-9]{3}$/.test(code)));
|
|
58
|
+
};
|
|
59
|
+
const LOCK_TIMEOUT_CODE = '55P03';
|
|
60
|
+
/** Serialization/deadlock/cancel outcomes: retriable unavailability (r2 E3/P2-2). */
|
|
61
|
+
const RETRIABLE_SQLSTATES = new Set(['40001', '40P01', '57014']);
|
|
62
|
+
const classifyOperational = (request, phase, commitState, error) => {
|
|
63
|
+
const classified = classifyArtifactFailure(error, {
|
|
64
|
+
invocationId: request.invocationId, operation: request.operation, phase, commitState,
|
|
65
|
+
});
|
|
66
|
+
if (classified.kind === 'operational')
|
|
67
|
+
return classified;
|
|
68
|
+
return failure('provider_error', request, phase, commitState, error, { variant: 'unclassified' });
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Every leased transaction is settled exactly once, including exceptional
|
|
72
|
+
* paths: rollback failure destroys the client instead of returning an
|
|
73
|
+
* uncertain session to the pool, and repeated calls are no-ops.
|
|
74
|
+
*/
|
|
75
|
+
const trackSettlement = (connection) => {
|
|
76
|
+
let settled = false;
|
|
77
|
+
return {
|
|
78
|
+
async abort() {
|
|
79
|
+
if (settled)
|
|
80
|
+
return undefined;
|
|
81
|
+
settled = true;
|
|
82
|
+
try {
|
|
83
|
+
await connection.rollback();
|
|
84
|
+
}
|
|
85
|
+
catch (rollbackError) {
|
|
86
|
+
try {
|
|
87
|
+
connection.release(rollbackError);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* release already attempted */
|
|
91
|
+
}
|
|
92
|
+
return rollbackError;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
connection.release();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* release already attempted */
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
},
|
|
102
|
+
releaseClean() {
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
settled = true;
|
|
106
|
+
try {
|
|
107
|
+
connection.release();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* release already attempted */
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Pre-quota lock classes, in A4.4 order. Guarded replay runs exactly these:
|
|
117
|
+
* object/ACL/policy serialization without quota charge or blob claims.
|
|
118
|
+
*/
|
|
119
|
+
const PRE_QUOTA_LOCK_CLASSES = PARTICIPANT_LOCK_ORDER.filter((entry) => entry !== 'quota' && entry !== 'blob-reference-deletion-claim');
|
|
120
|
+
export function createArtifactCommitCoordinator(options) {
|
|
121
|
+
const nowMs = options.nowMs ?? (() => Date.now());
|
|
122
|
+
for (const point of options.testPoints ?? []) {
|
|
123
|
+
if (!COORDINATOR_TEST_POINTS.includes(point)) {
|
|
124
|
+
throw new Error(`unknown coordinator test point ${point}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const armed = new Set(options.testPoints ?? COORDINATOR_TEST_POINTS);
|
|
128
|
+
const fired = new Set();
|
|
129
|
+
const observeAt = async (point, invocationId) => {
|
|
130
|
+
const key = `${point}:${invocationId}`;
|
|
131
|
+
if (options.testObserver === undefined || !armed.has(point) || fired.has(key))
|
|
132
|
+
return;
|
|
133
|
+
fired.add(key);
|
|
134
|
+
const observer = options.testObserver;
|
|
135
|
+
await (typeof observer === 'function'
|
|
136
|
+
? observer(point, invocationId) : observer.arrive(point, invocationId));
|
|
137
|
+
};
|
|
138
|
+
const commitContext = (request, purpose) => ({
|
|
139
|
+
invocationId: request.invocationId,
|
|
140
|
+
actorId: request.actorId,
|
|
141
|
+
storageScopeId: request.scopeId,
|
|
142
|
+
operation: request.operation,
|
|
143
|
+
artifactId: request.artifactId ?? '',
|
|
144
|
+
bindingId: request.bindingName ?? '',
|
|
145
|
+
commandIdentity: {
|
|
146
|
+
internalKey: request.internalKey,
|
|
147
|
+
commandHash: request.commandHash,
|
|
148
|
+
codecVersion: request.codecVersion,
|
|
149
|
+
},
|
|
150
|
+
policyRevision: request.policyRevision,
|
|
151
|
+
purpose,
|
|
152
|
+
quotaBytes: request.quotaBytes ?? 0,
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* Best-effort preparation-abandonment hook. Its failure is isolated: it
|
|
156
|
+
* never replaces the primary authorization decision with a cleanup error,
|
|
157
|
+
* and it never settles the connection a second time.
|
|
158
|
+
*/
|
|
159
|
+
const abandon = async (request) => {
|
|
160
|
+
if (request.noteAbandonedAttempt === undefined)
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
await request.noteAbandonedAttempt(request.attemptId ?? request.internalKey);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
/* abandonment hooks are best-effort; the primary decision stands */
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const unknownOutcome = (request, phase, cause) => ({
|
|
170
|
+
ok: false,
|
|
171
|
+
failure: failure('provider_unavailable', request, phase, 'unknown', cause),
|
|
172
|
+
});
|
|
173
|
+
/**
|
|
174
|
+
* Settle the unit, record abandonment, and produce either the primary
|
|
175
|
+
* failure or an unknown outcome when settlement itself failed (the primary
|
|
176
|
+
* cause is preserved inside the unknown outcome).
|
|
177
|
+
*/
|
|
178
|
+
const abortWith = async (request, phase, settle, primary) => {
|
|
179
|
+
const settleError = await settle.abort();
|
|
180
|
+
await abandon(request);
|
|
181
|
+
if (settleError === undefined)
|
|
182
|
+
return primary;
|
|
183
|
+
return unknownOutcome(request, phase, { decision: primary, settlement: settleError });
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Acquire the scoped key row. Only a lock timeout on this exact
|
|
187
|
+
* acquisition qualifies as key contention; every other lock/read timeout
|
|
188
|
+
* stays retriable unavailability via the shared classifier.
|
|
189
|
+
*/
|
|
190
|
+
const acquireKeyLock = async (request, connection, settle) => {
|
|
191
|
+
try {
|
|
192
|
+
await connection.lockOperationKey(request.scopeId, request.internalKey);
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const code = typeof error === 'object' && error !== null
|
|
197
|
+
? error.code
|
|
198
|
+
: undefined;
|
|
199
|
+
if (code === LOCK_TIMEOUT_CODE) {
|
|
200
|
+
return abortWith(request, 'lock', settle, {
|
|
201
|
+
ok: false,
|
|
202
|
+
failure: failure('conflict', request, 'lock', 'not_committed', error, {
|
|
203
|
+
variant: 'key_contention',
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Settle the unit before propagating quarantine, regardless of source.
|
|
212
|
+
* A hook raising quarantine before settlement must not leak the lease;
|
|
213
|
+
* settling twice is a no-op.
|
|
214
|
+
*/
|
|
215
|
+
const settleAndPropagateQuarantine = async (request, settle, error) => {
|
|
216
|
+
await settle.abort();
|
|
217
|
+
await abandon(request);
|
|
218
|
+
throw error;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Explicit SQL aborts certify no commit once settlement succeeds. Other
|
|
222
|
+
* COMMIT errors remain unknown even when a later ROLLBACK succeeds.
|
|
223
|
+
*/
|
|
224
|
+
const unknownAfterCommitAttempt = async (request, settle, error) => {
|
|
225
|
+
const settleError = await settle.abort();
|
|
226
|
+
await abandon(request);
|
|
227
|
+
const code = typeof error === 'object' && error !== null
|
|
228
|
+
? error.code : undefined;
|
|
229
|
+
if (settleError === undefined && typeof code === 'string' && RETRIABLE_SQLSTATES.has(code)) {
|
|
230
|
+
return { ok: false, failure: classifyOperational(request, 'commit', 'not_committed', error) };
|
|
231
|
+
}
|
|
232
|
+
return unknownOutcome(request, 'commit', settleError === undefined ? error : { commit: error, settlement: settleError });
|
|
233
|
+
};
|
|
234
|
+
const deny = (request, decision) => ({
|
|
235
|
+
ok: false,
|
|
236
|
+
failure: failure('unauthorized_at_commit', request, 'guard', 'not_committed', { denialReason: decision.reason }, {
|
|
237
|
+
publicCode: decision.publicCode,
|
|
238
|
+
status: decision.status,
|
|
239
|
+
retriable: decision.retriable,
|
|
240
|
+
variant: decision.status === 503 ? 'authority_unavailable' : 'policy_denial',
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
const revokedOutcome = (request) => ({
|
|
244
|
+
ok: false,
|
|
245
|
+
failure: failure('unauthorized_at_commit', request, 'guard', 'not_committed', { denialReason: 'revoked' }, { publicCode: 'access_denied', status: 403, retriable: false, variant: 'policy_denial' }),
|
|
246
|
+
});
|
|
247
|
+
const invalidFence = (request, detail) => ({
|
|
248
|
+
ok: false,
|
|
249
|
+
failure: failure('unauthorized_at_commit', request, 'guard', 'not_committed', {
|
|
250
|
+
denialReason: 'invalid_object',
|
|
251
|
+
fence: detail,
|
|
252
|
+
}, { variant: 'policy_denial' }),
|
|
253
|
+
});
|
|
254
|
+
/**
|
|
255
|
+
* A fence is valid only when minted by this transaction: its transactionId
|
|
256
|
+
* must equal the token the coordinator issued to the participant factory,
|
|
257
|
+
* its fenceId must be non-empty, its invocation must match the request,
|
|
258
|
+
* and its revision must match the freshly re-read policy revision.
|
|
259
|
+
* Fabricated, foreign or stale fences deny before any success work.
|
|
260
|
+
* The credential must be one of the transaction-local mint outputs:
|
|
261
|
+
* matching fields alone never prove minting.
|
|
262
|
+
*/
|
|
263
|
+
const bindTransaction = (request, connection, transactionToken, purpose) => {
|
|
264
|
+
const minted = new Set();
|
|
265
|
+
const raw = options.policy.createParticipant(connection, transactionToken);
|
|
266
|
+
let reservation;
|
|
267
|
+
const participant = {
|
|
268
|
+
policyRevision: request.policyRevision,
|
|
269
|
+
async readAdmission(input) {
|
|
270
|
+
if (input.actorId !== request.actorId || input.operation !== request.operation ||
|
|
271
|
+
input.artifactId !== (request.artifactId ?? '') || input.bindingId !== (request.bindingName ?? '')) {
|
|
272
|
+
return { allowed: false, reason: 'invalid_object' };
|
|
273
|
+
}
|
|
274
|
+
const revision = await raw.readRevision();
|
|
275
|
+
if (revision !== input.policyRevision)
|
|
276
|
+
return { allowed: false, reason: 'revoked' };
|
|
277
|
+
const decision = await raw.checkApproval({ actorId: input.actorId, operation: input.operation });
|
|
278
|
+
return { allowed: decision.approved, reason: decision.reason ?? 'denied' };
|
|
279
|
+
},
|
|
280
|
+
readRevision: () => raw.readRevision(),
|
|
281
|
+
checkApproval: (input) => raw.checkApproval(input),
|
|
282
|
+
reserveQuota(input) {
|
|
283
|
+
const bytes = request.quotaBytes ?? 0;
|
|
284
|
+
if (input.operation !== request.operation || ('storageScopeId' in input &&
|
|
285
|
+
(input.storageScopeId !== request.scopeId || input.bytes !== bytes)) ||
|
|
286
|
+
('actorId' in input && input.actorId !== request.actorId))
|
|
287
|
+
return Promise.resolve({ reserved: false });
|
|
288
|
+
if (purpose === 'replay')
|
|
289
|
+
return Promise.resolve({ reserved: true });
|
|
290
|
+
return reservation ??= raw.reserveQuota({ actorId: request.actorId, operation: request.operation,
|
|
291
|
+
storageScopeId: request.scopeId, bytes });
|
|
292
|
+
},
|
|
293
|
+
mintFence: (revision, invocationId) => {
|
|
294
|
+
const fence = Object.freeze({
|
|
295
|
+
...raw.mintFence(revision, invocationId), [commitFenceBrand]: true,
|
|
296
|
+
});
|
|
297
|
+
minted.add(fence);
|
|
298
|
+
return fence;
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
transaction: {
|
|
303
|
+
[commitTransactionBrand]: true,
|
|
304
|
+
transactionId: transactionToken,
|
|
305
|
+
invocationId: request.invocationId,
|
|
306
|
+
policyRevision: request.policyRevision,
|
|
307
|
+
participant,
|
|
308
|
+
},
|
|
309
|
+
minted,
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
const verifyFence = (request, transaction, minted, fence, currentRevision) => {
|
|
313
|
+
if (!minted.has(fence)) {
|
|
314
|
+
return { ok: false, detail: 'unminted_fence' };
|
|
315
|
+
}
|
|
316
|
+
if (fence.transactionId !== transaction.transactionId) {
|
|
317
|
+
return { ok: false, detail: 'foreign_fence' };
|
|
318
|
+
}
|
|
319
|
+
if (fence.fenceId.length === 0)
|
|
320
|
+
return { ok: false, detail: 'empty_fence_id' };
|
|
321
|
+
if (fence.invocationId !== request.invocationId) {
|
|
322
|
+
return { ok: false, detail: 'mismatched_invocation' };
|
|
323
|
+
}
|
|
324
|
+
if (fence.policyRevision !== currentRevision)
|
|
325
|
+
return { ok: false, detail: 'stale_revision' };
|
|
326
|
+
return { ok: true };
|
|
327
|
+
};
|
|
328
|
+
const runLockGroup = async (request, connection, classes) => {
|
|
329
|
+
for (const lockClass of classes) {
|
|
330
|
+
const callbacks = request.locks?.[lockClass];
|
|
331
|
+
if (callbacks === undefined)
|
|
332
|
+
continue;
|
|
333
|
+
for (const acquire of callbacks)
|
|
334
|
+
await acquire(connection);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
const validateRequest = (request, operation) => {
|
|
338
|
+
if (request.scopeId.length === 0 || request.internalKey.length === 0) {
|
|
339
|
+
throw new Error(`${operation} requires scope and internal key`);
|
|
340
|
+
}
|
|
341
|
+
if (request.commandHash.length === 0 || request.codecVersion.length === 0) {
|
|
342
|
+
throw new Error(`${operation} requires command hash and codec version`);
|
|
343
|
+
}
|
|
344
|
+
if (request.codecVersion !== COMMAND_CODEC_VERSION) {
|
|
345
|
+
throw new Error(`${operation} requires supported command codec ${COMMAND_CODEC_VERSION}`);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
/**
|
|
349
|
+
* COMMIT with acknowledgment-loss isolation. Returns undefined when
|
|
350
|
+
* committed and the unknown failure when the acknowledgment was lost;
|
|
351
|
+
* any other commit error is rethrown for phase-aware classification.
|
|
352
|
+
*/
|
|
353
|
+
const commitUnit = async (request, connection, settle) => {
|
|
354
|
+
try {
|
|
355
|
+
await connection.commit();
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (!isConnectionLoss(error))
|
|
359
|
+
throw error;
|
|
360
|
+
const settleError = await settle.abort();
|
|
361
|
+
return unknownOutcome(request, 'commit', settleError === undefined ? error : { commit: error, settlement: settleError });
|
|
362
|
+
}
|
|
363
|
+
settle.releaseClean();
|
|
364
|
+
return undefined;
|
|
365
|
+
};
|
|
366
|
+
const resolveUnderKey = async (request) => {
|
|
367
|
+
const phase = 'resolve';
|
|
368
|
+
let connection;
|
|
369
|
+
try {
|
|
370
|
+
connection = await options.executor.beginFinalization();
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
// Nothing leased: the executor disposes a client whose BEGIN fails.
|
|
374
|
+
await abandon(request);
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const settle = trackSettlement(connection);
|
|
381
|
+
try {
|
|
382
|
+
const keyFailure = await acquireKeyLock(request, connection, settle);
|
|
383
|
+
if (keyFailure !== undefined)
|
|
384
|
+
return keyFailure;
|
|
385
|
+
const found = await connection.readReceipt(request.scopeId, request.internalKey);
|
|
386
|
+
const settleError = await settle.abort();
|
|
387
|
+
if (settleError !== undefined) {
|
|
388
|
+
await abandon(request);
|
|
389
|
+
return unknownOutcome(request, phase, { cause: 'resolve_settlement', settlement: settleError });
|
|
390
|
+
}
|
|
391
|
+
if (found === undefined) {
|
|
392
|
+
await abandon(request);
|
|
393
|
+
return {
|
|
394
|
+
ok: false,
|
|
395
|
+
failure: failure('provider_error', request, phase, 'not_committed', {
|
|
396
|
+
lost: 'expected receipt after uniqueness violation',
|
|
397
|
+
}, { variant: 'unclassified' }),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (found.codecVersion !== COMMAND_CODEC_VERSION) {
|
|
401
|
+
// Settled above; the catch records abandonment before propagating.
|
|
402
|
+
throw new CommitQuarantineError(request.scopeId, request.internalKey, found.codecVersion);
|
|
403
|
+
}
|
|
404
|
+
if (found.commandHash !== request.commandHash) {
|
|
405
|
+
await abandon(request);
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
failure: failure('conflict', request, phase, 'not_committed', {
|
|
409
|
+
key: request.internalKey,
|
|
410
|
+
}, { variant: 'key_contention' }),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
return coordinator.replayPrepared(request);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
if (error instanceof CommitQuarantineError) {
|
|
417
|
+
return settleAndPropagateQuarantine(request, settle, error);
|
|
418
|
+
}
|
|
419
|
+
const settleError = await settle.abort();
|
|
420
|
+
await abandon(request);
|
|
421
|
+
if (settleError !== undefined) {
|
|
422
|
+
return unknownOutcome(request, phase, { cause: error, settlement: settleError });
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const coordinator = {
|
|
431
|
+
protocolVersion: COMMIT_PROTOCOL_VERSION,
|
|
432
|
+
guardedOperations: Object.freeze([...(options.operations?.guardedOperations ?? ['ensure', 'publish'])]),
|
|
433
|
+
...(options.testObserver === undefined ? {} : { testObserver: {
|
|
434
|
+
arrive(point, invocationId) {
|
|
435
|
+
const observer = options.testObserver;
|
|
436
|
+
return typeof observer === 'function' ? observer(point, invocationId) : observer.arrive(point, invocationId);
|
|
437
|
+
},
|
|
438
|
+
} }),
|
|
439
|
+
async finalizeOperation(input) {
|
|
440
|
+
if (!options.operations)
|
|
441
|
+
throw new Error('coordinator requires a trusted operation binding');
|
|
442
|
+
const request = await options.operations.finalize(input);
|
|
443
|
+
if (request.scopeId !== input.invocation.storageScopeId ||
|
|
444
|
+
request.actorId !== input.invocation.actorId || request.invocationId !== input.invocation.invocationId ||
|
|
445
|
+
request.internalKey !== input.command.identity.internalKey ||
|
|
446
|
+
request.commandHash !== input.command.identity.commandHash ||
|
|
447
|
+
request.codecVersion !== input.command.identity.codecVersion ||
|
|
448
|
+
request.operation !== input.command.operation || input.command.scopeBinding !== request.scopeId) {
|
|
449
|
+
throw new Error('operation binding identity mismatch');
|
|
450
|
+
}
|
|
451
|
+
const outcome = await coordinator.finalizePrepared({ ...request, guard: input.invocation.authorizeCommit });
|
|
452
|
+
return outcome.ok ? { ...outcome, result: {
|
|
453
|
+
internalKey: request.internalKey, commandHash: request.commandHash,
|
|
454
|
+
codecVersion: COMMAND_CODEC_VERSION, replayed: outcome.replayed,
|
|
455
|
+
} } : outcome;
|
|
456
|
+
},
|
|
457
|
+
async replayOperation(input) {
|
|
458
|
+
if (!options.operations)
|
|
459
|
+
throw new Error('coordinator requires a trusted operation binding');
|
|
460
|
+
const request = await options.operations.replay(input);
|
|
461
|
+
if (request.scopeId !== input.invocation.storageScopeId || request.actorId !== input.invocation.actorId ||
|
|
462
|
+
request.invocationId !== input.invocation.invocationId || request.internalKey !== input.internalKey) {
|
|
463
|
+
throw new Error('replay binding identity mismatch');
|
|
464
|
+
}
|
|
465
|
+
const outcome = await coordinator.replayPrepared({ ...request, guard: input.invocation.authorizeCommit });
|
|
466
|
+
return outcome.ok ? { ...outcome, result: {
|
|
467
|
+
internalKey: request.internalKey, commandHash: request.commandHash,
|
|
468
|
+
codecVersion: COMMAND_CODEC_VERSION, replayed: outcome.replayed,
|
|
469
|
+
} } : outcome;
|
|
470
|
+
},
|
|
471
|
+
async finalizePrepared(request) {
|
|
472
|
+
validateRequest(request, 'finalize');
|
|
473
|
+
await observeAt('before_connection_acquire', request.invocationId);
|
|
474
|
+
let phase = 'acquire';
|
|
475
|
+
let connection;
|
|
476
|
+
try {
|
|
477
|
+
await options.executor.ensureCoordinationRow(request.scopeId, request.internalKey);
|
|
478
|
+
connection = await options.executor.beginFinalization();
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
// Nothing leased: the executor disposes a client whose BEGIN fails.
|
|
482
|
+
await abandon(request);
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
failure: isConnectionLoss(error)
|
|
486
|
+
? failure('provider_unavailable', request, phase, 'not_committed', error)
|
|
487
|
+
: classifyOperational(request, phase, 'not_committed', error),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const active = connection;
|
|
491
|
+
const settle = trackSettlement(active);
|
|
492
|
+
try {
|
|
493
|
+
phase = 'lock';
|
|
494
|
+
await observeAt('before_key_lock', request.invocationId);
|
|
495
|
+
const keyFailure = await acquireKeyLock(request, active, settle);
|
|
496
|
+
if (keyFailure !== undefined)
|
|
497
|
+
return keyFailure;
|
|
498
|
+
const existing = await active.readReceipt(request.scopeId, request.internalKey);
|
|
499
|
+
if (existing !== undefined) {
|
|
500
|
+
const settleError = await settle.abort();
|
|
501
|
+
if (settleError !== undefined) {
|
|
502
|
+
await abandon(request);
|
|
503
|
+
return unknownOutcome(request, phase, { cause: 'resolve_settlement', settlement: settleError });
|
|
504
|
+
}
|
|
505
|
+
if (existing.codecVersion !== COMMAND_CODEC_VERSION) {
|
|
506
|
+
// Settled above; the catch records abandonment before propagating.
|
|
507
|
+
throw new CommitQuarantineError(request.scopeId, request.internalKey, existing.codecVersion);
|
|
508
|
+
}
|
|
509
|
+
if (existing.commandHash === request.commandHash) {
|
|
510
|
+
return coordinator.replayPrepared(request);
|
|
511
|
+
}
|
|
512
|
+
await abandon(request);
|
|
513
|
+
return {
|
|
514
|
+
ok: false,
|
|
515
|
+
failure: failure('conflict', request, 'lock', 'not_committed', {
|
|
516
|
+
key: request.internalKey,
|
|
517
|
+
}, { variant: 'key_contention' }),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
phase = 'guard';
|
|
521
|
+
await observeAt('before_finalization_row_lock', request.invocationId);
|
|
522
|
+
const transactionToken = randomUUID();
|
|
523
|
+
const { transaction, minted } = bindTransaction(request, active, transactionToken, 'finalize');
|
|
524
|
+
await runLockGroup(request, active, PRE_QUOTA_LOCK_CLASSES);
|
|
525
|
+
// Authority re-read before the quota wait: A4.4 class order.
|
|
526
|
+
await transaction.participant.readRevision();
|
|
527
|
+
await runLockGroup(request, active, ['quota']);
|
|
528
|
+
// Provisional quota reservation precedes the guard decision; the row
|
|
529
|
+
// lock is retained through COMMIT and rolls back on denial.
|
|
530
|
+
const reserved = await transaction.participant.reserveQuota({
|
|
531
|
+
actorId: request.actorId,
|
|
532
|
+
operation: request.operation,
|
|
533
|
+
});
|
|
534
|
+
if (!reserved.reserved) {
|
|
535
|
+
return abortWith(request, phase, settle, {
|
|
536
|
+
ok: false,
|
|
537
|
+
failure: failure('conflict', request, 'guard', 'not_committed', {
|
|
538
|
+
quota: 'exhausted',
|
|
539
|
+
}, { variant: 'quota_exhausted' }),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
await runLockGroup(request, active, ['blob-reference-deletion-claim']);
|
|
543
|
+
await request.provisional?.(active);
|
|
544
|
+
await observeAt('after_finalization_locks_before_guard', request.invocationId);
|
|
545
|
+
const decision = await request.guard(commitContext(request, 'finalize'), transaction);
|
|
546
|
+
if (!decision.allowed) {
|
|
547
|
+
return abortWith(request, phase, settle, deny(request, decision));
|
|
548
|
+
}
|
|
549
|
+
const currentRevision = await transaction.participant.readRevision();
|
|
550
|
+
const fenceCheck = verifyFence(request, transaction, minted, decision.fence, currentRevision);
|
|
551
|
+
if (!fenceCheck.ok) {
|
|
552
|
+
return abortWith(request, phase, settle, invalidFence(request, fenceCheck.detail));
|
|
553
|
+
}
|
|
554
|
+
if (currentRevision !== request.policyRevision) {
|
|
555
|
+
return abortWith(request, phase, settle, revokedOutcome(request));
|
|
556
|
+
}
|
|
557
|
+
const receipt = {
|
|
558
|
+
scopeId: request.scopeId,
|
|
559
|
+
internalKey: request.internalKey,
|
|
560
|
+
commandHash: request.commandHash,
|
|
561
|
+
codecVersion: request.codecVersion,
|
|
562
|
+
receipt: request.receiptData,
|
|
563
|
+
createdAt: new Date(nowMs()).toISOString(),
|
|
564
|
+
};
|
|
565
|
+
await active.insertReceipt(receipt);
|
|
566
|
+
await request.finishIntents?.(active);
|
|
567
|
+
await observeAt('before_commit', request.invocationId);
|
|
568
|
+
// Run deferred constraint work inside the unit, before the final
|
|
569
|
+
// decision: locks it acquires must not land during COMMIT.
|
|
570
|
+
await active.query('SET CONSTRAINTS ALL IMMEDIATE', []);
|
|
571
|
+
// Revalidate after the post-guard receipt/intent/barrier waits: a
|
|
572
|
+
// revocation, expiry or foreign fence landing here denies instead of
|
|
573
|
+
// committing an authorized-then-stale decision.
|
|
574
|
+
const recheck = await request.guard(commitContext(request, 'finalize'), transaction);
|
|
575
|
+
if (!recheck.allowed) {
|
|
576
|
+
return abortWith(request, phase, settle, deny(request, recheck));
|
|
577
|
+
}
|
|
578
|
+
const recheckRevision = await transaction.participant.readRevision();
|
|
579
|
+
const recheckFence = verifyFence(request, transaction, minted, recheck.fence, recheckRevision);
|
|
580
|
+
if (!recheckFence.ok) {
|
|
581
|
+
return abortWith(request, phase, settle, invalidFence(request, recheckFence.detail));
|
|
582
|
+
}
|
|
583
|
+
if (recheckRevision !== request.policyRevision) {
|
|
584
|
+
return abortWith(request, phase, settle, revokedOutcome(request));
|
|
585
|
+
}
|
|
586
|
+
phase = 'commit';
|
|
587
|
+
const commitFailure = await commitUnit(request, active, settle);
|
|
588
|
+
if (commitFailure !== undefined)
|
|
589
|
+
return commitFailure;
|
|
590
|
+
return {
|
|
591
|
+
ok: true,
|
|
592
|
+
result: receipt,
|
|
593
|
+
invocationId: request.invocationId,
|
|
594
|
+
commitState: 'committed',
|
|
595
|
+
replayed: false,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
if (error instanceof CommitQuarantineError) {
|
|
600
|
+
return settleAndPropagateQuarantine(request, settle, error);
|
|
601
|
+
}
|
|
602
|
+
if (isUniqueViolation(error)) {
|
|
603
|
+
await settle.abort();
|
|
604
|
+
return resolveUnderKey(request);
|
|
605
|
+
}
|
|
606
|
+
if (phase === 'commit') {
|
|
607
|
+
return unknownAfterCommitAttempt(request, settle, error);
|
|
608
|
+
}
|
|
609
|
+
const settleError = await settle.abort();
|
|
610
|
+
await abandon(request);
|
|
611
|
+
if (settleError !== undefined) {
|
|
612
|
+
return unknownOutcome(request, phase, { cause: error, settlement: settleError });
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
ok: false,
|
|
616
|
+
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
async replayPrepared(request) {
|
|
621
|
+
validateRequest(request, 'replay');
|
|
622
|
+
await observeAt('before_connection_acquire', request.invocationId);
|
|
623
|
+
let phase = 'acquire';
|
|
624
|
+
let connection;
|
|
625
|
+
try {
|
|
626
|
+
connection = await options.executor.beginFinalization();
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
// Nothing leased: the executor disposes a client whose BEGIN fails.
|
|
630
|
+
await abandon(request);
|
|
631
|
+
return {
|
|
632
|
+
ok: false,
|
|
633
|
+
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const active = connection;
|
|
637
|
+
const settle = trackSettlement(active);
|
|
638
|
+
try {
|
|
639
|
+
phase = 'lock';
|
|
640
|
+
await observeAt('before_key_lock', request.invocationId);
|
|
641
|
+
const replayKeyFailure = await acquireKeyLock(request, active, settle);
|
|
642
|
+
if (replayKeyFailure !== undefined)
|
|
643
|
+
return replayKeyFailure;
|
|
644
|
+
const existing = await active.readReceipt(request.scopeId, request.internalKey);
|
|
645
|
+
// Fires after the receipt lookup (not before): a revocation racing
|
|
646
|
+
// the barrier denies on the looked-up row instead of vanishing it.
|
|
647
|
+
await observeAt('after_replay_lookup_before_guard', request.invocationId);
|
|
648
|
+
if (existing === undefined) {
|
|
649
|
+
return abortWith(request, phase, settle, {
|
|
650
|
+
ok: false,
|
|
651
|
+
failure: failure('provider_error', request, phase, 'not_committed', {
|
|
652
|
+
lost: 'replay without a success row',
|
|
653
|
+
}, { variant: 'unclassified' }),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (existing.codecVersion !== COMMAND_CODEC_VERSION) {
|
|
657
|
+
const settleError = await settle.abort();
|
|
658
|
+
if (settleError !== undefined) {
|
|
659
|
+
await abandon(request);
|
|
660
|
+
return unknownOutcome(request, phase, { settlement: settleError });
|
|
661
|
+
}
|
|
662
|
+
// Settled above; the catch records abandonment before propagating.
|
|
663
|
+
throw new CommitQuarantineError(request.scopeId, request.internalKey, existing.codecVersion);
|
|
664
|
+
}
|
|
665
|
+
if (existing.commandHash !== request.commandHash) {
|
|
666
|
+
return abortWith(request, phase, settle, {
|
|
667
|
+
ok: false,
|
|
668
|
+
failure: failure('conflict', request, phase, 'not_committed', {
|
|
669
|
+
key: request.internalKey,
|
|
670
|
+
}, { variant: 'key_contention' }),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
phase = 'guard';
|
|
674
|
+
const transactionToken = randomUUID();
|
|
675
|
+
const { transaction, minted } = bindTransaction(request, active, transactionToken, 'replay');
|
|
676
|
+
// Same key/object/ACL/policy lock order as finalization, without
|
|
677
|
+
// quota charging, provisional publication writes or intent completion.
|
|
678
|
+
await runLockGroup(request, active, PRE_QUOTA_LOCK_CLASSES);
|
|
679
|
+
// Run deferred constraint work inside the unit, before the decision:
|
|
680
|
+
// locks it acquires must not land during COMMIT.
|
|
681
|
+
await active.query('SET CONSTRAINTS ALL IMMEDIATE', []);
|
|
682
|
+
if (request.validateReplayReceipt && !request.validateReplayReceipt(existing)) {
|
|
683
|
+
return abortWith(request, phase, settle, invalidFence(request, 'replay_target_mismatch'));
|
|
684
|
+
}
|
|
685
|
+
const decision = await request.guard(commitContext(request, 'replay'), transaction);
|
|
686
|
+
if (!decision.allowed) {
|
|
687
|
+
return abortWith(request, phase, settle, deny(request, decision));
|
|
688
|
+
}
|
|
689
|
+
const currentRevision = await transaction.participant.readRevision();
|
|
690
|
+
const fenceCheck = verifyFence(request, transaction, minted, decision.fence, currentRevision);
|
|
691
|
+
if (!fenceCheck.ok) {
|
|
692
|
+
return abortWith(request, phase, settle, invalidFence(request, fenceCheck.detail));
|
|
693
|
+
}
|
|
694
|
+
if (currentRevision !== request.policyRevision) {
|
|
695
|
+
return abortWith(request, phase, settle, revokedOutcome(request));
|
|
696
|
+
}
|
|
697
|
+
phase = 'commit';
|
|
698
|
+
const commitFailure = await commitUnit(request, active, settle);
|
|
699
|
+
if (commitFailure !== undefined)
|
|
700
|
+
return commitFailure;
|
|
701
|
+
return {
|
|
702
|
+
ok: true,
|
|
703
|
+
result: existing,
|
|
704
|
+
invocationId: request.invocationId,
|
|
705
|
+
commitState: 'committed',
|
|
706
|
+
replayed: true,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
if (error instanceof CommitQuarantineError) {
|
|
711
|
+
return settleAndPropagateQuarantine(request, settle, error);
|
|
712
|
+
}
|
|
713
|
+
if (phase === 'commit') {
|
|
714
|
+
return unknownAfterCommitAttempt(request, settle, error);
|
|
715
|
+
}
|
|
716
|
+
const settleError = await settle.abort();
|
|
717
|
+
await abandon(request);
|
|
718
|
+
if (settleError !== undefined) {
|
|
719
|
+
return unknownOutcome(request, phase, { cause: error, settlement: settleError });
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
ok: false,
|
|
723
|
+
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
};
|
|
728
|
+
return coordinator;
|
|
729
|
+
}
|