@emilia-protocol/gate 0.15.2 → 0.17.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.
@@ -0,0 +1,984 @@
1
+ // @ts-nocheck
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Runtime counterpart for Conservation of Authority.
5
+ *
6
+ * This module keeps path containment and aggregate sibling conservation
7
+ * separate. A relying party pins one complete parent allocation snapshot to an
8
+ * exact authority head and epoch. Every sibling is checked against that parent,
9
+ * both resource dimensions are summed independently, and reservations cross a
10
+ * single atomic store boundary before they can be committed.
11
+ *
12
+ * The memory store is deterministic conformance infrastructure, not durable
13
+ * custody. The PostgreSQL adapter requires a real transaction callback and
14
+ * serializes every mutation for one relying-party/parent pair.
15
+ */
16
+ import crypto from 'node:crypto';
17
+ export const AUTHORITY_ALLOCATION_VERSION = 'EP-AUTHORITY-ALLOCATION-v1';
18
+ export const AUTHORITY_ALLOCATION_CURRENT_TABLE = 'ep_authority_allocation_current';
19
+ export const AUTHORITY_ALLOCATION_SNAPSHOT_TABLE = 'ep_authority_allocation_snapshots';
20
+ export const AUTHORITY_ALLOCATION_BRANCH_TABLE = 'ep_authority_allocation_branches';
21
+ export const AUTHORITY_ALLOCATION_RESERVATION_TABLE = 'ep_authority_allocation_reservations';
22
+ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
23
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,511}$/;
24
+ const OWNER_PATTERN = /^authority-owner:v1:[A-Za-z0-9_-]{16,128}$/;
25
+ const RFC3339_INSTANT = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(?:Z|([+-])(\d{2}):(\d{2}))$/;
26
+ const OWNER_DIGEST_DOMAIN = 'EMILIA-AUTHORITY-ALLOCATION-v1:OWNER\0';
27
+ export class AuthorityAllocationValidationError extends TypeError {
28
+ code;
29
+ constructor(code, message) {
30
+ super(message);
31
+ this.name = 'AuthorityAllocationValidationError';
32
+ this.code = code;
33
+ }
34
+ }
35
+ function fail(code, message) {
36
+ throw new AuthorityAllocationValidationError(code, message);
37
+ }
38
+ function identifier(value, field) {
39
+ if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
40
+ fail('invalid_identifier', `${field} must be a non-empty bounded identifier`);
41
+ }
42
+ return value;
43
+ }
44
+ function authorityHead(value, field) {
45
+ if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
46
+ fail('missing_authority_pin', `${field} must be an exact lowercase SHA-256 authority head`);
47
+ }
48
+ return value;
49
+ }
50
+ function authorityEpoch(value, field) {
51
+ if (!Number.isSafeInteger(value) || value < 0) {
52
+ fail('missing_authority_pin', `${field} must be a non-negative safe integer`);
53
+ }
54
+ return value;
55
+ }
56
+ function budget(value, field) {
57
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
58
+ fail('invalid_budget', `${field} must contain independent cents and calls limits`);
59
+ }
60
+ const candidate = value;
61
+ for (const dimension of ['cents', 'calls']) {
62
+ if (!Number.isSafeInteger(candidate[dimension]) || candidate[dimension] < 0) {
63
+ fail('invalid_budget', `${field}.${dimension} must be a non-negative safe integer`);
64
+ }
65
+ }
66
+ return {
67
+ cents: candidate.cents,
68
+ calls: candidate.calls,
69
+ };
70
+ }
71
+ function checkedAdd(left, right, field) {
72
+ const sum = left + right;
73
+ if (!Number.isSafeInteger(sum))
74
+ fail('budget_overflow', `${field} exceeds safe integer range`);
75
+ return sum;
76
+ }
77
+ function addBudget(left, right, field = 'budget') {
78
+ return {
79
+ cents: checkedAdd(left.cents, right.cents, `${field}.cents`),
80
+ calls: checkedAdd(left.calls, right.calls, `${field}.calls`),
81
+ };
82
+ }
83
+ function subtractBudget(ceiling, used) {
84
+ return {
85
+ cents: ceiling.cents - used.cents,
86
+ calls: ceiling.calls - used.calls,
87
+ };
88
+ }
89
+ function withinBudget(candidate, ceiling) {
90
+ return candidate.cents <= ceiling.cents && candidate.calls <= ceiling.calls;
91
+ }
92
+ function strictInstantMs(value, field) {
93
+ if (typeof value !== 'string')
94
+ fail('invalid_expiry', `${field} must be an RFC 3339 instant`);
95
+ const match = value.match(RFC3339_INSTANT);
96
+ if (!match)
97
+ fail('invalid_expiry', `${field} must be an RFC 3339 instant`);
98
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText, , offsetHourText, offsetMinuteText,] = match;
99
+ const localText = `${yearText}-${monthText}-${dayText}T${hourText}:${minuteText}:${secondText}`;
100
+ const calendar = new Date(0);
101
+ calendar.setUTCFullYear(Number(yearText), Number(monthText) - 1, Number(dayText));
102
+ calendar.setUTCHours(Number(hourText), Number(minuteText), Number(secondText), 0);
103
+ if (calendar.toISOString().slice(0, 19) !== localText) {
104
+ fail('invalid_expiry', `${field} contains an impossible calendar instant`);
105
+ }
106
+ if (offsetHourText !== undefined
107
+ && (Number(offsetHourText) > 23 || Number(offsetMinuteText) > 59)) {
108
+ fail('invalid_expiry', `${field} contains an invalid offset`);
109
+ }
110
+ const parsed = Date.parse(value);
111
+ if (!Number.isFinite(parsed))
112
+ fail('invalid_expiry', `${field} must be an RFC 3339 instant`);
113
+ return parsed;
114
+ }
115
+ function operationTimeMs(value) {
116
+ if (value === undefined)
117
+ return Date.now();
118
+ const parsed = value instanceof Date
119
+ ? value.getTime()
120
+ : typeof value === 'string'
121
+ ? strictInstantMs(value, 'now')
122
+ : value;
123
+ if (!Number.isFinite(parsed))
124
+ fail('invalid_time', 'now must identify a finite instant');
125
+ return parsed;
126
+ }
127
+ function exactSet(value, field) {
128
+ if (!Array.isArray(value))
129
+ fail('invalid_selector_set', `${field} must be an array`);
130
+ const result = value.map((entry, index) => identifier(entry, `${field}[${index}]`));
131
+ if (new Set(result).size !== result.length) {
132
+ fail('duplicate_selector', `${field} must not contain duplicate selectors`);
133
+ }
134
+ return result.sort();
135
+ }
136
+ function isSubset(child, parent) {
137
+ const parentSet = new Set(parent);
138
+ return child.every((entry) => parentSet.has(entry));
139
+ }
140
+ function cloneSnapshot(snapshot) {
141
+ return {
142
+ ...snapshot,
143
+ actions: [...snapshot.actions],
144
+ audiences: [...snapshot.audiences],
145
+ budget: { ...snapshot.budget },
146
+ sibling_allocations: snapshot.sibling_allocations.map((allocation) => ({
147
+ ...allocation,
148
+ actions: [...allocation.actions],
149
+ audiences: [...allocation.audiences],
150
+ budget: { ...allocation.budget },
151
+ })),
152
+ };
153
+ }
154
+ function canonicalSnapshot(snapshot) {
155
+ return JSON.stringify({
156
+ version: snapshot.version,
157
+ relying_party_id: snapshot.relying_party_id,
158
+ parent_id: snapshot.parent_id,
159
+ authority_head: snapshot.authority_head,
160
+ authority_epoch: snapshot.authority_epoch,
161
+ actions: snapshot.actions,
162
+ audiences: snapshot.audiences,
163
+ budget: snapshot.budget,
164
+ expires_at: snapshot.expires_at,
165
+ sibling_allocations: snapshot.sibling_allocations,
166
+ });
167
+ }
168
+ function snapshotFingerprint(snapshot) {
169
+ return `sha256:${crypto.createHash('sha256').update(canonicalSnapshot(snapshot)).digest('hex')}`;
170
+ }
171
+ function parentKey(relyingPartyId, parentId) {
172
+ return JSON.stringify([relyingPartyId, parentId]);
173
+ }
174
+ function emptyBudget() {
175
+ return { cents: 0, calls: 0 };
176
+ }
177
+ function assertExactPin(actual, expected) {
178
+ const relyingPartyId = identifier(expected?.relying_party_id, 'pin.relying_party_id');
179
+ const head = authorityHead(expected?.authority_head, 'pin.authority_head');
180
+ const epoch = authorityEpoch(expected?.authority_epoch, 'pin.authority_epoch');
181
+ if (actual.relying_party_id !== relyingPartyId
182
+ || actual.authority_head !== head
183
+ || actual.authority_epoch !== epoch) {
184
+ fail('authority_pin_mismatch', 'snapshot does not match the relying-party authority pin');
185
+ }
186
+ }
187
+ /**
188
+ * Validate and normalize a complete authoritative allocation snapshot.
189
+ * Throws AuthorityAllocationValidationError on every malformed or widening
190
+ * condition and returns a detached, deterministically ordered snapshot.
191
+ */
192
+ export function validateAuthorityAllocationSnapshot(input, pin) {
193
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
194
+ fail('invalid_snapshot', 'authority allocation snapshot must be an object');
195
+ }
196
+ if (input.version !== AUTHORITY_ALLOCATION_VERSION) {
197
+ fail('unsupported_version', `snapshot.version must be ${AUTHORITY_ALLOCATION_VERSION}`);
198
+ }
199
+ const relyingPartyId = identifier(input.relying_party_id, 'snapshot.relying_party_id');
200
+ const parentId = identifier(input.parent_id, 'snapshot.parent_id');
201
+ const head = authorityHead(input.authority_head, 'snapshot.authority_head');
202
+ const epoch = authorityEpoch(input.authority_epoch, 'snapshot.authority_epoch');
203
+ const parentActions = exactSet(input.actions, 'snapshot.actions');
204
+ const parentAudiences = exactSet(input.audiences, 'snapshot.audiences');
205
+ const parentBudget = budget(input.budget, 'snapshot.budget');
206
+ const parentExpiryMs = strictInstantMs(input.expires_at, 'snapshot.expires_at');
207
+ if (!Array.isArray(input.sibling_allocations)) {
208
+ fail('invalid_siblings', 'snapshot.sibling_allocations must be an array');
209
+ }
210
+ const seen = new Set();
211
+ let aggregate = emptyBudget();
212
+ const siblings = input.sibling_allocations.map((raw, index) => {
213
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
214
+ fail('invalid_branch', `snapshot.sibling_allocations[${index}] must be an object`);
215
+ }
216
+ const allocationId = identifier(raw.allocation_id, `sibling[${index}].allocation_id`);
217
+ if (seen.has(allocationId))
218
+ fail('duplicate_branch', `duplicate allocation branch ${allocationId}`);
219
+ seen.add(allocationId);
220
+ const childParentId = identifier(raw.parent_id, `sibling[${index}].parent_id`);
221
+ if (childParentId !== parentId) {
222
+ fail('parent_mismatch', `allocation ${allocationId} does not name the authoritative parent`);
223
+ }
224
+ const actions = exactSet(raw.actions, `sibling[${index}].actions`);
225
+ if (!isSubset(actions, parentActions)) {
226
+ fail('child_action_widening', `allocation ${allocationId} widens the parent action set`);
227
+ }
228
+ const audiences = exactSet(raw.audiences, `sibling[${index}].audiences`);
229
+ if (!isSubset(audiences, parentAudiences)) {
230
+ fail('child_audience_widening', `allocation ${allocationId} widens the parent audience set`);
231
+ }
232
+ const childBudget = budget(raw.budget, `sibling[${index}].budget`);
233
+ if (!withinBudget(childBudget, parentBudget)) {
234
+ fail('child_budget_widening', `allocation ${allocationId} widens a parent budget dimension`);
235
+ }
236
+ const childExpiryMs = strictInstantMs(raw.expires_at, `sibling[${index}].expires_at`);
237
+ if (childExpiryMs > parentExpiryMs) {
238
+ fail('child_expiry_widening', `allocation ${allocationId} outlives its parent`);
239
+ }
240
+ aggregate = addBudget(aggregate, childBudget, 'aggregate_sibling_budget');
241
+ return {
242
+ allocation_id: allocationId,
243
+ parent_id: childParentId,
244
+ actions,
245
+ audiences,
246
+ budget: childBudget,
247
+ expires_at: raw.expires_at,
248
+ };
249
+ }).sort((left, right) => left.allocation_id.localeCompare(right.allocation_id));
250
+ if (!withinBudget(aggregate, parentBudget)) {
251
+ fail('aggregate_sibling_overspend', 'aggregate sibling cents or calls exceed the parent budget');
252
+ }
253
+ const normalized = {
254
+ version: AUTHORITY_ALLOCATION_VERSION,
255
+ relying_party_id: relyingPartyId,
256
+ parent_id: parentId,
257
+ authority_head: head,
258
+ authority_epoch: epoch,
259
+ actions: parentActions,
260
+ audiences: parentAudiences,
261
+ budget: parentBudget,
262
+ expires_at: input.expires_at,
263
+ sibling_allocations: siblings,
264
+ };
265
+ assertExactPin(normalized, pin);
266
+ return normalized;
267
+ }
268
+ function validateReservationRequest(request) {
269
+ return {
270
+ relying_party_id: identifier(request?.relying_party_id, 'request.relying_party_id'),
271
+ parent_id: identifier(request?.parent_id, 'request.parent_id'),
272
+ allocation_id: identifier(request?.allocation_id, 'request.allocation_id'),
273
+ reservation_id: identifier(request?.reservation_id, 'request.reservation_id'),
274
+ authority_head: authorityHead(request?.authority_head, 'request.authority_head'),
275
+ authority_epoch: authorityEpoch(request?.authority_epoch, 'request.authority_epoch'),
276
+ budget: budget(request?.budget, 'request.budget'),
277
+ ...(request?.now === undefined ? {} : { now: request.now }),
278
+ };
279
+ }
280
+ function validateFinalizeRequest(request) {
281
+ const ownerToken = request?.owner_token;
282
+ if (typeof ownerToken !== 'string' || !OWNER_PATTERN.test(ownerToken)) {
283
+ fail('invalid_owner_token', 'owner_token must be an authority-allocation owner capability');
284
+ }
285
+ if (!Number.isSafeInteger(request?.fencing_token) || request.fencing_token < 1) {
286
+ fail('invalid_fencing_token', 'fencing_token must be a positive safe integer');
287
+ }
288
+ return {
289
+ relying_party_id: identifier(request.relying_party_id, 'request.relying_party_id'),
290
+ parent_id: identifier(request.parent_id, 'request.parent_id'),
291
+ allocation_id: identifier(request.allocation_id, 'request.allocation_id'),
292
+ reservation_id: identifier(request.reservation_id, 'request.reservation_id'),
293
+ authority_head: authorityHead(request.authority_head, 'request.authority_head'),
294
+ authority_epoch: authorityEpoch(request.authority_epoch, 'request.authority_epoch'),
295
+ owner_token: ownerToken,
296
+ fencing_token: request.fencing_token,
297
+ };
298
+ }
299
+ function pinMatches(state, head, epoch) {
300
+ return state.snapshot.authority_head === head && state.snapshot.authority_epoch === epoch;
301
+ }
302
+ function usageFor(state, allocationId) {
303
+ let reserved = emptyBudget();
304
+ let committed = emptyBudget();
305
+ for (const reservation of state.reservations.values()) {
306
+ if (allocationId !== undefined && reservation.allocation_id !== allocationId)
307
+ continue;
308
+ if (reservation.authority_head !== state.snapshot.authority_head
309
+ || reservation.authority_epoch !== state.snapshot.authority_epoch)
310
+ continue;
311
+ if (reservation.state === 'reserved') {
312
+ reserved = addBudget(reserved, reservation.budget);
313
+ }
314
+ else if (reservation.state === 'committed') {
315
+ committed = addBudget(committed, reservation.budget);
316
+ }
317
+ }
318
+ return { reserved, committed };
319
+ }
320
+ function stateView(state) {
321
+ const branches = Object.create(null);
322
+ for (const allocation of state.snapshot.sibling_allocations) {
323
+ branches[allocation.allocation_id] = usageFor(state, allocation.allocation_id);
324
+ }
325
+ return {
326
+ snapshot: cloneSnapshot(state.snapshot),
327
+ snapshot_fingerprint: state.fingerprint,
328
+ usage: {
329
+ parent: usageFor(state),
330
+ branches,
331
+ },
332
+ reservations: [...state.reservations.values()]
333
+ .map(({ owner_token: _ownerToken, ...reservation }) => ({
334
+ ...reservation,
335
+ budget: { ...reservation.budget },
336
+ }))
337
+ .sort((left, right) => left.fencing_token - right.fencing_token),
338
+ };
339
+ }
340
+ /**
341
+ * Deterministic linearizable in-memory implementation for conformance tests.
342
+ * Owner capabilities and fences are deterministic and therefore unsuitable
343
+ * for production. The store is explicitly marked non-durable.
344
+ */
345
+ export function createMemoryAuthorityAllocationStore() {
346
+ const parents = new Map();
347
+ let queue = Promise.resolve();
348
+ const atomic = (operation) => {
349
+ const result = queue.then(operation, operation);
350
+ queue = result.then(() => undefined, () => undefined);
351
+ return result;
352
+ };
353
+ return {
354
+ durable: false,
355
+ installSnapshot(snapshot, pin) {
356
+ return atomic(() => {
357
+ const normalized = validateAuthorityAllocationSnapshot(snapshot, pin);
358
+ const fingerprint = snapshotFingerprint(normalized);
359
+ const key = parentKey(normalized.relying_party_id, normalized.parent_id);
360
+ const current = parents.get(key);
361
+ if (current) {
362
+ if (current.snapshot.authority_head === normalized.authority_head
363
+ && current.snapshot.authority_epoch === normalized.authority_epoch) {
364
+ return current.fingerprint === fingerprint
365
+ ? { ok: true, installed: false, snapshot_fingerprint: fingerprint }
366
+ : { ok: false, reason: 'snapshot_conflict' };
367
+ }
368
+ if (normalized.authority_epoch <= current.snapshot.authority_epoch) {
369
+ return { ok: false, reason: 'stale_authority_epoch' };
370
+ }
371
+ if ([...current.reservations.values()].some((entry) => entry.state === 'reserved')) {
372
+ return { ok: false, reason: 'reservations_in_flight' };
373
+ }
374
+ }
375
+ parents.set(key, {
376
+ snapshot: cloneSnapshot(normalized),
377
+ fingerprint,
378
+ nextFence: current?.nextFence ?? 1,
379
+ reservations: current?.reservations ?? new Map(),
380
+ });
381
+ return { ok: true, installed: true, snapshot_fingerprint: fingerprint };
382
+ });
383
+ },
384
+ reserve(rawRequest) {
385
+ return atomic(() => {
386
+ const request = validateReservationRequest(rawRequest);
387
+ const state = parents.get(parentKey(request.relying_party_id, request.parent_id));
388
+ if (!state)
389
+ return { ok: false, reason: 'allocation_not_found' };
390
+ if (!pinMatches(state, request.authority_head, request.authority_epoch)) {
391
+ return { ok: false, reason: 'authority_pin_mismatch' };
392
+ }
393
+ if (state.reservations.has(request.reservation_id)) {
394
+ return { ok: false, reason: 'reservation_replayed' };
395
+ }
396
+ const allocation = state.snapshot.sibling_allocations.find((entry) => entry.allocation_id === request.allocation_id);
397
+ if (!allocation)
398
+ return { ok: false, reason: 'allocation_not_found' };
399
+ const at = operationTimeMs(request.now);
400
+ if (at >= strictInstantMs(state.snapshot.expires_at, 'snapshot.expires_at')
401
+ || at >= strictInstantMs(allocation.expires_at, 'allocation.expires_at')) {
402
+ return { ok: false, reason: 'allocation_expired' };
403
+ }
404
+ const branchUsage = usageFor(state, allocation.allocation_id);
405
+ const branchUsed = addBudget(branchUsage.reserved, branchUsage.committed);
406
+ const branchCandidate = addBudget(branchUsed, request.budget);
407
+ const parentUsage = usageFor(state);
408
+ const parentUsed = addBudget(parentUsage.reserved, parentUsage.committed);
409
+ const parentCandidate = addBudget(parentUsed, request.budget);
410
+ if (!withinBudget(branchCandidate, allocation.budget)
411
+ || !withinBudget(parentCandidate, state.snapshot.budget)) {
412
+ return { ok: false, reason: 'budget_exceeded' };
413
+ }
414
+ const fencingToken = state.nextFence;
415
+ state.nextFence += 1;
416
+ const ownerToken = `authority-owner:v1:memory_${fencingToken.toString(36).padStart(16, '0')}`;
417
+ state.reservations.set(request.reservation_id, {
418
+ reservation_id: request.reservation_id,
419
+ allocation_id: request.allocation_id,
420
+ authority_head: request.authority_head,
421
+ authority_epoch: request.authority_epoch,
422
+ budget: { ...request.budget },
423
+ state: 'reserved',
424
+ fencing_token: fencingToken,
425
+ owner_token: ownerToken,
426
+ });
427
+ return {
428
+ ok: true,
429
+ reservation_id: request.reservation_id,
430
+ allocation_id: request.allocation_id,
431
+ budget: { ...request.budget },
432
+ remaining: subtractBudget(allocation.budget, branchCandidate),
433
+ owner: {
434
+ owner_token: ownerToken,
435
+ fencing_token: fencingToken,
436
+ authority_head: request.authority_head,
437
+ authority_epoch: request.authority_epoch,
438
+ },
439
+ };
440
+ });
441
+ },
442
+ commit(rawRequest) {
443
+ return atomic(() => {
444
+ const request = validateFinalizeRequest(rawRequest);
445
+ const state = parents.get(parentKey(request.relying_party_id, request.parent_id));
446
+ if (!state)
447
+ return { ok: false, reason: 'reservation_not_found' };
448
+ if (!pinMatches(state, request.authority_head, request.authority_epoch)) {
449
+ return { ok: false, reason: 'authority_pin_mismatch' };
450
+ }
451
+ const reservation = state.reservations.get(request.reservation_id);
452
+ if (!reservation || reservation.allocation_id !== request.allocation_id) {
453
+ return { ok: false, reason: 'reservation_not_found' };
454
+ }
455
+ if (reservation.state === 'committed') {
456
+ return { ok: false, reason: 'reservation_already_committed' };
457
+ }
458
+ if (reservation.state === 'released') {
459
+ return { ok: false, reason: 'reservation_already_released' };
460
+ }
461
+ if (reservation.owner_token !== request.owner_token
462
+ || reservation.fencing_token !== request.fencing_token
463
+ || reservation.authority_head !== request.authority_head
464
+ || reservation.authority_epoch !== request.authority_epoch) {
465
+ return { ok: false, reason: 'reservation_owner_mismatch' };
466
+ }
467
+ reservation.state = 'committed';
468
+ return { ok: true, state: 'committed' };
469
+ });
470
+ },
471
+ release(rawRequest) {
472
+ return atomic(() => {
473
+ const request = validateFinalizeRequest(rawRequest);
474
+ const state = parents.get(parentKey(request.relying_party_id, request.parent_id));
475
+ if (!state)
476
+ return { ok: false, reason: 'reservation_not_found' };
477
+ if (!pinMatches(state, request.authority_head, request.authority_epoch)) {
478
+ return { ok: false, reason: 'authority_pin_mismatch' };
479
+ }
480
+ const reservation = state.reservations.get(request.reservation_id);
481
+ if (!reservation || reservation.allocation_id !== request.allocation_id) {
482
+ return { ok: false, reason: 'reservation_not_found' };
483
+ }
484
+ if (reservation.state === 'committed') {
485
+ return { ok: false, reason: 'reservation_already_committed' };
486
+ }
487
+ if (reservation.state === 'released') {
488
+ return { ok: false, reason: 'reservation_already_released' };
489
+ }
490
+ if (reservation.owner_token !== request.owner_token
491
+ || reservation.fencing_token !== request.fencing_token
492
+ || reservation.authority_head !== request.authority_head
493
+ || reservation.authority_epoch !== request.authority_epoch) {
494
+ return { ok: false, reason: 'reservation_owner_mismatch' };
495
+ }
496
+ reservation.state = 'released';
497
+ return { ok: true, state: 'released' };
498
+ });
499
+ },
500
+ inspect(rawPin) {
501
+ return atomic(() => {
502
+ const relyingPartyId = identifier(rawPin?.relying_party_id, 'pin.relying_party_id');
503
+ const parentId = identifier(rawPin?.parent_id, 'pin.parent_id');
504
+ const head = authorityHead(rawPin?.authority_head, 'pin.authority_head');
505
+ const epoch = authorityEpoch(rawPin?.authority_epoch, 'pin.authority_epoch');
506
+ const state = parents.get(parentKey(relyingPartyId, parentId));
507
+ if (!state)
508
+ return null;
509
+ if (!pinMatches(state, head, epoch)) {
510
+ fail('authority_pin_mismatch', 'inspection requires the exact current authority pin');
511
+ }
512
+ return stateView(state);
513
+ });
514
+ },
515
+ };
516
+ }
517
+ /**
518
+ * PostgreSQL schema contract. Historical snapshots are append-only;
519
+ * ep_authority_allocation_current identifies the one exact head+epoch that can
520
+ * accept reservations. Reservation IDs are replay-fenced across all epochs for
521
+ * one relying-party/parent pair, and owner capabilities are stored only as
522
+ * domain-separated SHA-256 digests.
523
+ */
524
+ export const AUTHORITY_ALLOCATION_DDL = `CREATE TABLE IF NOT EXISTS ${AUTHORITY_ALLOCATION_SNAPSHOT_TABLE} (
525
+ relying_party_id TEXT NOT NULL CHECK (octet_length(relying_party_id) BETWEEN 1 AND 512),
526
+ parent_id TEXT NOT NULL CHECK (octet_length(parent_id) BETWEEN 1 AND 512),
527
+ authority_head TEXT NOT NULL CHECK (authority_head ~ '^sha256:[0-9a-f]{64}$'),
528
+ authority_epoch BIGINT NOT NULL CHECK (authority_epoch >= 0),
529
+ snapshot_fingerprint TEXT NOT NULL CHECK (snapshot_fingerprint ~ '^sha256:[0-9a-f]{64}$'),
530
+ snapshot_json JSONB NOT NULL CHECK (jsonb_typeof(snapshot_json) = 'object'),
531
+ installed_at TIMESTAMPTZ NOT NULL DEFAULT transaction_timestamp(),
532
+ PRIMARY KEY (relying_party_id, parent_id, authority_head, authority_epoch)
533
+ );
534
+ CREATE TABLE IF NOT EXISTS ${AUTHORITY_ALLOCATION_CURRENT_TABLE} (
535
+ relying_party_id TEXT NOT NULL,
536
+ parent_id TEXT NOT NULL,
537
+ authority_head TEXT NOT NULL,
538
+ authority_epoch BIGINT NOT NULL,
539
+ snapshot_fingerprint TEXT NOT NULL,
540
+ next_fencing_token BIGINT NOT NULL DEFAULT 1 CHECK (next_fencing_token > 0),
541
+ PRIMARY KEY (relying_party_id, parent_id),
542
+ FOREIGN KEY (relying_party_id, parent_id, authority_head, authority_epoch)
543
+ REFERENCES ${AUTHORITY_ALLOCATION_SNAPSHOT_TABLE}
544
+ (relying_party_id, parent_id, authority_head, authority_epoch)
545
+ );
546
+ CREATE TABLE IF NOT EXISTS ${AUTHORITY_ALLOCATION_BRANCH_TABLE} (
547
+ relying_party_id TEXT NOT NULL,
548
+ parent_id TEXT NOT NULL,
549
+ authority_head TEXT NOT NULL,
550
+ authority_epoch BIGINT NOT NULL,
551
+ allocation_id TEXT NOT NULL CHECK (octet_length(allocation_id) BETWEEN 1 AND 512),
552
+ budget_cents BIGINT NOT NULL CHECK (budget_cents >= 0),
553
+ budget_calls BIGINT NOT NULL CHECK (budget_calls >= 0),
554
+ expires_at TIMESTAMPTZ NOT NULL,
555
+ PRIMARY KEY (relying_party_id, parent_id, authority_head, authority_epoch, allocation_id),
556
+ FOREIGN KEY (relying_party_id, parent_id, authority_head, authority_epoch)
557
+ REFERENCES ${AUTHORITY_ALLOCATION_SNAPSHOT_TABLE}
558
+ (relying_party_id, parent_id, authority_head, authority_epoch)
559
+ );
560
+ CREATE TABLE IF NOT EXISTS ${AUTHORITY_ALLOCATION_RESERVATION_TABLE} (
561
+ relying_party_id TEXT NOT NULL,
562
+ parent_id TEXT NOT NULL,
563
+ authority_head TEXT NOT NULL,
564
+ authority_epoch BIGINT NOT NULL,
565
+ allocation_id TEXT NOT NULL,
566
+ reservation_id TEXT NOT NULL CHECK (octet_length(reservation_id) BETWEEN 1 AND 512),
567
+ budget_cents BIGINT NOT NULL CHECK (budget_cents >= 0),
568
+ budget_calls BIGINT NOT NULL CHECK (budget_calls >= 0),
569
+ state TEXT NOT NULL CHECK (state IN ('reserved', 'committed', 'released')),
570
+ owner_digest TEXT NOT NULL CHECK (owner_digest ~ '^sha256:[0-9a-f]{64}$'),
571
+ fencing_token BIGINT NOT NULL CHECK (fencing_token > 0),
572
+ reserved_at TIMESTAMPTZ NOT NULL DEFAULT transaction_timestamp(),
573
+ finalized_at TIMESTAMPTZ NULL,
574
+ PRIMARY KEY (relying_party_id, parent_id, reservation_id),
575
+ UNIQUE (relying_party_id, parent_id, fencing_token),
576
+ FOREIGN KEY (relying_party_id, parent_id, authority_head, authority_epoch, allocation_id)
577
+ REFERENCES ${AUTHORITY_ALLOCATION_BRANCH_TABLE}
578
+ (relying_party_id, parent_id, authority_head, authority_epoch, allocation_id),
579
+ CHECK (
580
+ (state = 'reserved' AND finalized_at IS NULL)
581
+ OR (state IN ('committed', 'released') AND finalized_at IS NOT NULL)
582
+ )
583
+ );
584
+ REVOKE ALL ON ${AUTHORITY_ALLOCATION_SNAPSHOT_TABLE} FROM PUBLIC;
585
+ REVOKE ALL ON ${AUTHORITY_ALLOCATION_CURRENT_TABLE} FROM PUBLIC;
586
+ REVOKE ALL ON ${AUTHORITY_ALLOCATION_BRANCH_TABLE} FROM PUBLIC;
587
+ REVOKE ALL ON ${AUTHORITY_ALLOCATION_RESERVATION_TABLE} FROM PUBLIC;`;
588
+ /** Exact statements used by createPostgresAuthorityAllocationStore(). */
589
+ export const AUTHORITY_ALLOCATION_SQL = Object.freeze({
590
+ lockParent: `SELECT pg_advisory_xact_lock(pg_catalog.hashtextextended(pg_catalog.jsonb_build_array($1::text, $2::text)::text, 0))`,
591
+ readCurrent: `SELECT authority_head, authority_epoch, snapshot_fingerprint, next_fencing_token, clock_timestamp() AS database_now FROM ${AUTHORITY_ALLOCATION_CURRENT_TABLE} WHERE relying_party_id = $1 AND parent_id = $2 FOR UPDATE`,
592
+ activeReservations: `SELECT count(*)::bigint AS active FROM ${AUTHORITY_ALLOCATION_RESERVATION_TABLE} WHERE relying_party_id = $1 AND parent_id = $2 AND state = 'reserved'`,
593
+ insertSnapshot: `INSERT INTO ${AUTHORITY_ALLOCATION_SNAPSHOT_TABLE} (relying_party_id, parent_id, authority_head, authority_epoch, snapshot_fingerprint, snapshot_json) VALUES ($1, $2, $3, $4, $5, $6::jsonb)`,
594
+ insertBranch: `INSERT INTO ${AUTHORITY_ALLOCATION_BRANCH_TABLE} (relying_party_id, parent_id, authority_head, authority_epoch, allocation_id, budget_cents, budget_calls, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::timestamptz)`,
595
+ insertCurrent: `INSERT INTO ${AUTHORITY_ALLOCATION_CURRENT_TABLE} (relying_party_id, parent_id, authority_head, authority_epoch, snapshot_fingerprint) VALUES ($1, $2, $3, $4, $5)`,
596
+ advanceCurrent: `UPDATE ${AUTHORITY_ALLOCATION_CURRENT_TABLE} SET authority_head = $3, authority_epoch = $4, snapshot_fingerprint = $5 WHERE relying_party_id = $1 AND parent_id = $2 AND authority_epoch < $4`,
597
+ readSnapshot: `SELECT snapshot_json, snapshot_fingerprint FROM ${AUTHORITY_ALLOCATION_SNAPSHOT_TABLE} WHERE relying_party_id = $1 AND parent_id = $2 AND authority_head = $3 AND authority_epoch = $4`,
598
+ readBranch: `SELECT budget_cents, budget_calls, expires_at FROM ${AUTHORITY_ALLOCATION_BRANCH_TABLE} WHERE relying_party_id = $1 AND parent_id = $2 AND authority_head = $3 AND authority_epoch = $4 AND allocation_id = $5`,
599
+ readReservation: `SELECT allocation_id, authority_head, authority_epoch, budget_cents, budget_calls, state, owner_digest, fencing_token FROM ${AUTHORITY_ALLOCATION_RESERVATION_TABLE} WHERE relying_party_id = $1 AND parent_id = $2 AND reservation_id = $3`,
600
+ readUsage: `SELECT allocation_id, state, COALESCE(sum(budget_cents), 0)::bigint AS cents, COALESCE(sum(budget_calls), 0)::bigint AS calls FROM ${AUTHORITY_ALLOCATION_RESERVATION_TABLE} WHERE relying_party_id = $1 AND parent_id = $2 AND authority_head = $3 AND authority_epoch = $4 AND state IN ('reserved', 'committed') GROUP BY allocation_id, state`,
601
+ nextFence: `UPDATE ${AUTHORITY_ALLOCATION_CURRENT_TABLE} SET next_fencing_token = next_fencing_token + 1 WHERE relying_party_id = $1 AND parent_id = $2 AND authority_head = $3 AND authority_epoch = $4 RETURNING next_fencing_token - 1 AS fencing_token`,
602
+ insertReservation: `INSERT INTO ${AUTHORITY_ALLOCATION_RESERVATION_TABLE} (relying_party_id, parent_id, authority_head, authority_epoch, allocation_id, reservation_id, budget_cents, budget_calls, state, owner_digest, fencing_token) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'reserved', $9, $10)`,
603
+ commitReservation: `UPDATE ${AUTHORITY_ALLOCATION_RESERVATION_TABLE} SET state = 'committed', finalized_at = transaction_timestamp() WHERE relying_party_id = $1 AND parent_id = $2 AND reservation_id = $3 AND allocation_id = $4 AND authority_head = $5 AND authority_epoch = $6 AND state = 'reserved' AND owner_digest = $7 AND fencing_token = $8`,
604
+ releaseReservation: `UPDATE ${AUTHORITY_ALLOCATION_RESERVATION_TABLE} SET state = 'released', finalized_at = transaction_timestamp() WHERE relying_party_id = $1 AND parent_id = $2 AND reservation_id = $3 AND allocation_id = $4 AND authority_head = $5 AND authority_epoch = $6 AND state = 'reserved' AND owner_digest = $7 AND fencing_token = $8`,
605
+ inspectReservations: `SELECT reservation_id, allocation_id, authority_head, authority_epoch, budget_cents, budget_calls, state, fencing_token FROM ${AUTHORITY_ALLOCATION_RESERVATION_TABLE} WHERE relying_party_id = $1 AND parent_id = $2 ORDER BY fencing_token`,
606
+ });
607
+ function row(result) {
608
+ return result.rows[0];
609
+ }
610
+ function integerFromDatabase(value, field) {
611
+ const parsed = typeof value === 'number' ? value : Number(value);
612
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
613
+ throw new Error(`${field} returned an unsafe database integer`);
614
+ }
615
+ return parsed;
616
+ }
617
+ function ownerDigest(ownerToken) {
618
+ return `sha256:${crypto.createHash('sha256')
619
+ .update(OWNER_DIGEST_DOMAIN)
620
+ .update(ownerToken)
621
+ .digest('hex')}`;
622
+ }
623
+ function secureOwnerToken() {
624
+ return `authority-owner:v1:${crypto.randomBytes(32).toString('base64url')}`;
625
+ }
626
+ function databaseInstantMs(value) {
627
+ const parsed = value instanceof Date ? value.getTime() : Date.parse(String(value));
628
+ if (!Number.isFinite(parsed))
629
+ throw new Error('PostgreSQL returned an invalid database clock');
630
+ return parsed;
631
+ }
632
+ function currentPinMatches(current, head, epoch) {
633
+ return current.authority_head === head
634
+ && integerFromDatabase(current.authority_epoch, 'authority_epoch') === epoch;
635
+ }
636
+ function reservationReason(reservation, request) {
637
+ if (!reservation || reservation.allocation_id !== request.allocation_id) {
638
+ return 'reservation_not_found';
639
+ }
640
+ if (reservation.state === 'committed')
641
+ return 'reservation_already_committed';
642
+ if (reservation.state === 'released')
643
+ return 'reservation_already_released';
644
+ if (reservation.owner_digest !== ownerDigest(request.owner_token)
645
+ || integerFromDatabase(reservation.fencing_token, 'fencing_token') !== request.fencing_token
646
+ || reservation.authority_head !== request.authority_head
647
+ || integerFromDatabase(reservation.authority_epoch, 'authority_epoch') !== request.authority_epoch) {
648
+ return 'reservation_owner_mismatch';
649
+ }
650
+ return 'reservation_not_found';
651
+ }
652
+ function usageRows(rows) {
653
+ const usage = new Map();
654
+ for (const entry of rows) {
655
+ const allocationId = String(entry.allocation_id);
656
+ const current = usage.get(allocationId) ?? {
657
+ reserved: emptyBudget(),
658
+ committed: emptyBudget(),
659
+ };
660
+ const value = {
661
+ cents: integerFromDatabase(entry.cents, 'usage.cents'),
662
+ calls: integerFromDatabase(entry.calls, 'usage.calls'),
663
+ };
664
+ if (entry.state === 'reserved')
665
+ current.reserved = value;
666
+ if (entry.state === 'committed')
667
+ current.committed = value;
668
+ usage.set(allocationId, current);
669
+ }
670
+ return usage;
671
+ }
672
+ function aggregateUsage(usage) {
673
+ let reserved = emptyBudget();
674
+ let committed = emptyBudget();
675
+ for (const entry of usage.values()) {
676
+ reserved = addBudget(reserved, entry.reserved);
677
+ committed = addBudget(committed, entry.committed);
678
+ }
679
+ return { reserved, committed };
680
+ }
681
+ /**
682
+ * Durable adapter boundary for PostgreSQL. Atomicity depends on the supplied
683
+ * transaction callback actually using one PostgreSQL transaction; the adapter
684
+ * additionally takes a transaction-scoped advisory lock for every parent.
685
+ */
686
+ export function createPostgresAuthorityAllocationStore(options) {
687
+ if (!options || typeof options.transaction !== 'function') {
688
+ throw new TypeError('createPostgresAuthorityAllocationStore requires transaction(callback)');
689
+ }
690
+ const withParentLock = async (relyingPartyId, parentId, operation) => options.transaction(async (query) => {
691
+ await query(AUTHORITY_ALLOCATION_SQL.lockParent, [relyingPartyId, parentId]);
692
+ return operation(query);
693
+ });
694
+ const finalize = async (rawRequest, state) => {
695
+ const request = validateFinalizeRequest(rawRequest);
696
+ return withParentLock(request.relying_party_id, request.parent_id, async (query) => {
697
+ const current = row(await query(AUTHORITY_ALLOCATION_SQL.readCurrent, [
698
+ request.relying_party_id,
699
+ request.parent_id,
700
+ ]));
701
+ if (!current)
702
+ return { ok: false, reason: 'reservation_not_found' };
703
+ if (!currentPinMatches(current, request.authority_head, request.authority_epoch)) {
704
+ return { ok: false, reason: 'authority_pin_mismatch' };
705
+ }
706
+ const params = [
707
+ request.relying_party_id,
708
+ request.parent_id,
709
+ request.reservation_id,
710
+ request.allocation_id,
711
+ request.authority_head,
712
+ request.authority_epoch,
713
+ ownerDigest(request.owner_token),
714
+ request.fencing_token,
715
+ ];
716
+ const statement = state === 'committed'
717
+ ? AUTHORITY_ALLOCATION_SQL.commitReservation
718
+ : AUTHORITY_ALLOCATION_SQL.releaseReservation;
719
+ const result = await query(statement, params);
720
+ if (result.rowCount === 1)
721
+ return { ok: true, state };
722
+ const existing = row(await query(AUTHORITY_ALLOCATION_SQL.readReservation, [
723
+ request.relying_party_id,
724
+ request.parent_id,
725
+ request.reservation_id,
726
+ ]));
727
+ return { ok: false, reason: reservationReason(existing, request) };
728
+ });
729
+ };
730
+ return {
731
+ durable: true,
732
+ async installSnapshot(snapshot, pin) {
733
+ const normalized = validateAuthorityAllocationSnapshot(snapshot, pin);
734
+ const fingerprint = snapshotFingerprint(normalized);
735
+ return withParentLock(normalized.relying_party_id, normalized.parent_id, async (query) => {
736
+ const current = row(await query(AUTHORITY_ALLOCATION_SQL.readCurrent, [
737
+ normalized.relying_party_id,
738
+ normalized.parent_id,
739
+ ]));
740
+ if (current) {
741
+ const currentEpoch = integerFromDatabase(current.authority_epoch, 'authority_epoch');
742
+ if (current.authority_head === normalized.authority_head
743
+ && currentEpoch === normalized.authority_epoch) {
744
+ return current.snapshot_fingerprint === fingerprint
745
+ ? { ok: true, installed: false, snapshot_fingerprint: fingerprint }
746
+ : { ok: false, reason: 'snapshot_conflict' };
747
+ }
748
+ if (normalized.authority_epoch <= currentEpoch) {
749
+ return { ok: false, reason: 'stale_authority_epoch' };
750
+ }
751
+ const active = row(await query(AUTHORITY_ALLOCATION_SQL.activeReservations, [
752
+ normalized.relying_party_id,
753
+ normalized.parent_id,
754
+ ]));
755
+ if (integerFromDatabase(active?.active ?? 0, 'active reservations') > 0) {
756
+ return { ok: false, reason: 'reservations_in_flight' };
757
+ }
758
+ }
759
+ await query(AUTHORITY_ALLOCATION_SQL.insertSnapshot, [
760
+ normalized.relying_party_id,
761
+ normalized.parent_id,
762
+ normalized.authority_head,
763
+ normalized.authority_epoch,
764
+ fingerprint,
765
+ canonicalSnapshot(normalized),
766
+ ]);
767
+ for (const allocation of normalized.sibling_allocations) {
768
+ await query(AUTHORITY_ALLOCATION_SQL.insertBranch, [
769
+ normalized.relying_party_id,
770
+ normalized.parent_id,
771
+ normalized.authority_head,
772
+ normalized.authority_epoch,
773
+ allocation.allocation_id,
774
+ allocation.budget.cents,
775
+ allocation.budget.calls,
776
+ allocation.expires_at,
777
+ ]);
778
+ }
779
+ if (current) {
780
+ const advanced = await query(AUTHORITY_ALLOCATION_SQL.advanceCurrent, [
781
+ normalized.relying_party_id,
782
+ normalized.parent_id,
783
+ normalized.authority_head,
784
+ normalized.authority_epoch,
785
+ fingerprint,
786
+ ]);
787
+ if (advanced.rowCount !== 1)
788
+ return { ok: false, reason: 'stale_authority_epoch' };
789
+ }
790
+ else {
791
+ await query(AUTHORITY_ALLOCATION_SQL.insertCurrent, [
792
+ normalized.relying_party_id,
793
+ normalized.parent_id,
794
+ normalized.authority_head,
795
+ normalized.authority_epoch,
796
+ fingerprint,
797
+ ]);
798
+ }
799
+ return { ok: true, installed: true, snapshot_fingerprint: fingerprint };
800
+ });
801
+ },
802
+ async reserve(rawRequest) {
803
+ const request = validateReservationRequest(rawRequest);
804
+ return withParentLock(request.relying_party_id, request.parent_id, async (query) => {
805
+ const current = row(await query(AUTHORITY_ALLOCATION_SQL.readCurrent, [
806
+ request.relying_party_id,
807
+ request.parent_id,
808
+ ]));
809
+ if (!current)
810
+ return { ok: false, reason: 'allocation_not_found' };
811
+ if (!currentPinMatches(current, request.authority_head, request.authority_epoch)) {
812
+ return { ok: false, reason: 'authority_pin_mismatch' };
813
+ }
814
+ const existing = row(await query(AUTHORITY_ALLOCATION_SQL.readReservation, [
815
+ request.relying_party_id,
816
+ request.parent_id,
817
+ request.reservation_id,
818
+ ]));
819
+ if (existing)
820
+ return { ok: false, reason: 'reservation_replayed' };
821
+ const branch = row(await query(AUTHORITY_ALLOCATION_SQL.readBranch, [
822
+ request.relying_party_id,
823
+ request.parent_id,
824
+ request.authority_head,
825
+ request.authority_epoch,
826
+ request.allocation_id,
827
+ ]));
828
+ if (!branch)
829
+ return { ok: false, reason: 'allocation_not_found' };
830
+ const snapshotRow = row(await query(AUTHORITY_ALLOCATION_SQL.readSnapshot, [
831
+ request.relying_party_id,
832
+ request.parent_id,
833
+ request.authority_head,
834
+ request.authority_epoch,
835
+ ]));
836
+ if (!snapshotRow || !snapshotRow.snapshot_json) {
837
+ throw new Error('current authority allocation snapshot is missing');
838
+ }
839
+ const snapshot = snapshotRow.snapshot_json;
840
+ const nowMs = databaseInstantMs(current.database_now);
841
+ if (nowMs >= strictInstantMs(snapshot.expires_at, 'snapshot.expires_at')
842
+ || nowMs >= databaseInstantMs(branch.expires_at)) {
843
+ return { ok: false, reason: 'allocation_expired' };
844
+ }
845
+ const usage = usageRows((await query(AUTHORITY_ALLOCATION_SQL.readUsage, [
846
+ request.relying_party_id,
847
+ request.parent_id,
848
+ request.authority_head,
849
+ request.authority_epoch,
850
+ ])).rows);
851
+ const branchUsage = usage.get(request.allocation_id) ?? {
852
+ reserved: emptyBudget(),
853
+ committed: emptyBudget(),
854
+ };
855
+ const branchCandidate = addBudget(addBudget(branchUsage.reserved, branchUsage.committed), request.budget);
856
+ const parentUsage = aggregateUsage(usage);
857
+ const parentCandidate = addBudget(addBudget(parentUsage.reserved, parentUsage.committed), request.budget);
858
+ const branchCeiling = {
859
+ cents: integerFromDatabase(branch.budget_cents, 'branch.budget_cents'),
860
+ calls: integerFromDatabase(branch.budget_calls, 'branch.budget_calls'),
861
+ };
862
+ if (!withinBudget(branchCandidate, branchCeiling)
863
+ || !withinBudget(parentCandidate, budget(snapshot.budget, 'snapshot.budget'))) {
864
+ return { ok: false, reason: 'budget_exceeded' };
865
+ }
866
+ const fenceRow = row(await query(AUTHORITY_ALLOCATION_SQL.nextFence, [
867
+ request.relying_party_id,
868
+ request.parent_id,
869
+ request.authority_head,
870
+ request.authority_epoch,
871
+ ]));
872
+ if (!fenceRow)
873
+ return { ok: false, reason: 'authority_pin_mismatch' };
874
+ const fencingToken = integerFromDatabase(fenceRow.fencing_token, 'fencing_token');
875
+ const ownerToken = secureOwnerToken();
876
+ await query(AUTHORITY_ALLOCATION_SQL.insertReservation, [
877
+ request.relying_party_id,
878
+ request.parent_id,
879
+ request.authority_head,
880
+ request.authority_epoch,
881
+ request.allocation_id,
882
+ request.reservation_id,
883
+ request.budget.cents,
884
+ request.budget.calls,
885
+ ownerDigest(ownerToken),
886
+ fencingToken,
887
+ ]);
888
+ return {
889
+ ok: true,
890
+ reservation_id: request.reservation_id,
891
+ allocation_id: request.allocation_id,
892
+ budget: { ...request.budget },
893
+ remaining: subtractBudget(branchCeiling, branchCandidate),
894
+ owner: {
895
+ owner_token: ownerToken,
896
+ fencing_token: fencingToken,
897
+ authority_head: request.authority_head,
898
+ authority_epoch: request.authority_epoch,
899
+ },
900
+ };
901
+ });
902
+ },
903
+ commit(request) {
904
+ return finalize(request, 'committed');
905
+ },
906
+ release(request) {
907
+ return finalize(request, 'released');
908
+ },
909
+ async inspect(rawPin) {
910
+ const relyingPartyId = identifier(rawPin?.relying_party_id, 'pin.relying_party_id');
911
+ const parentId = identifier(rawPin?.parent_id, 'pin.parent_id');
912
+ const head = authorityHead(rawPin?.authority_head, 'pin.authority_head');
913
+ const epoch = authorityEpoch(rawPin?.authority_epoch, 'pin.authority_epoch');
914
+ return withParentLock(relyingPartyId, parentId, async (query) => {
915
+ const current = row(await query(AUTHORITY_ALLOCATION_SQL.readCurrent, [
916
+ relyingPartyId,
917
+ parentId,
918
+ ]));
919
+ if (!current)
920
+ return null;
921
+ if (!currentPinMatches(current, head, epoch)) {
922
+ fail('authority_pin_mismatch', 'inspection requires the exact current authority pin');
923
+ }
924
+ const snapshotResult = await query(AUTHORITY_ALLOCATION_SQL.readSnapshot, [
925
+ relyingPartyId,
926
+ parentId,
927
+ head,
928
+ epoch,
929
+ ]);
930
+ const snapshotRow = row(snapshotResult);
931
+ if (!snapshotRow)
932
+ throw new Error('current authority allocation snapshot is missing');
933
+ const snapshot = snapshotRow.snapshot_json;
934
+ const usage = usageRows((await query(AUTHORITY_ALLOCATION_SQL.readUsage, [
935
+ relyingPartyId,
936
+ parentId,
937
+ head,
938
+ epoch,
939
+ ])).rows);
940
+ const parent = aggregateUsage(usage);
941
+ const branches = Object.create(null);
942
+ for (const allocation of snapshot.sibling_allocations) {
943
+ branches[allocation.allocation_id] = usage.get(allocation.allocation_id) ?? {
944
+ reserved: emptyBudget(),
945
+ committed: emptyBudget(),
946
+ };
947
+ }
948
+ const reservationRows = (await query(AUTHORITY_ALLOCATION_SQL.inspectReservations, [
949
+ relyingPartyId,
950
+ parentId,
951
+ ])).rows;
952
+ return {
953
+ snapshot: cloneSnapshot(snapshot),
954
+ snapshot_fingerprint: String(snapshotRow.snapshot_fingerprint),
955
+ usage: { parent, branches },
956
+ reservations: reservationRows.map((entry) => ({
957
+ reservation_id: String(entry.reservation_id),
958
+ allocation_id: String(entry.allocation_id),
959
+ authority_head: String(entry.authority_head),
960
+ authority_epoch: integerFromDatabase(entry.authority_epoch, 'authority_epoch'),
961
+ budget: {
962
+ cents: integerFromDatabase(entry.budget_cents, 'reservation.budget_cents'),
963
+ calls: integerFromDatabase(entry.budget_calls, 'reservation.budget_calls'),
964
+ },
965
+ state: entry.state,
966
+ fencing_token: integerFromDatabase(entry.fencing_token, 'fencing_token'),
967
+ })),
968
+ };
969
+ });
970
+ },
971
+ };
972
+ }
973
+ export function isDurableAuthorityAllocationStore(store) {
974
+ if (!store || typeof store !== 'object')
975
+ return false;
976
+ const candidate = store;
977
+ return candidate.durable === true
978
+ && typeof candidate.installSnapshot === 'function'
979
+ && typeof candidate.reserve === 'function'
980
+ && typeof candidate.commit === 'function'
981
+ && typeof candidate.release === 'function'
982
+ && typeof candidate.inspect === 'function';
983
+ }
984
+ //# sourceMappingURL=authority-allocation.js.map