@emilia-protocol/gate 0.15.0 → 0.15.1

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,449 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Durable relying-party custody for accepted EP-STATUS-v1 heads.
4
+ *
5
+ * The presenter supplies only a candidate status artifact. The store loads the
6
+ * authenticated predecessor, passes that predecessor to trusted verification
7
+ * code, and advances the exact tenant/relying-party/target head only when one
8
+ * database-side compare-and-advance still observes that predecessor. This
9
+ * keeps cryptographic work outside database transactions without opening a
10
+ * time-of-check/time-of-use acceptance race.
11
+ */
12
+
13
+ import {
14
+ statusArtifactDigest,
15
+ type StatusTarget,
16
+ type StatusVerification,
17
+ } from '@emilia-protocol/verify/status';
18
+
19
+ export const PROPOSAL_TO_EFFECT_STATUS_HEAD_STORE_VERSION =
20
+ 'EP-GATE-PTE-STATUS-HEAD-PG-v1';
21
+ export const PROPOSAL_TO_EFFECT_STATUS_HEAD_TABLE = 'ep_aeb_status_heads';
22
+
23
+ export const PROPOSAL_TO_EFFECT_STATUS_HEAD_SQL = Object.freeze({
24
+ get: `SELECT status_digest, sequence, status_state, previous_status_digest,
25
+ issued_at, next_update, status_json, predecessor_status_json
26
+ FROM ep_aeb_private.get_status_head(
27
+ $1::text, $2::text, $3::text, $4::text, $5::text, $6::text
28
+ )`,
29
+ compareAndAdvance: `SELECT accepted, reason
30
+ FROM ep_aeb_private.compare_and_advance_status_head(
31
+ $1::text, $2::text, $3::text, $4::text, $5::text, $6::text,
32
+ $7::text, $8::text, $9::bigint, $10::text, $11::text,
33
+ $12::timestamptz, $13::timestamptz, $14::text
34
+ )`,
35
+ });
36
+
37
+ type MaybePromise<T> = T | Promise<T>;
38
+
39
+ type QueryResult = {
40
+ rowCount: number | null;
41
+ rows?: Record<string, unknown>[];
42
+ };
43
+
44
+ export type ProposalToEffectStatusHeadPgClient = {
45
+ query: (text: string, params?: any[]) => Promise<QueryResult>;
46
+ release: () => void;
47
+ };
48
+
49
+ export type ProposalToEffectStatusHeadPgPool = {
50
+ connect: () => Promise<ProposalToEffectStatusHeadPgClient>;
51
+ };
52
+
53
+ export interface ProposalToEffectStatusHeadAcceptance {
54
+ accepted: boolean;
55
+ source: 'advanced' | 'existing' | null;
56
+ reason: string | null;
57
+ verification: StatusVerification;
58
+ }
59
+
60
+ export interface ProposalToEffectStatusHeadAcceptanceInput {
61
+ target: Readonly<StatusTarget>;
62
+ status: unknown;
63
+ /**
64
+ * Trusted verification callback. Its argument is loaded from durable
65
+ * relying-party custody; no presenter-provided predecessor is accepted.
66
+ */
67
+ verify(previousStatus: unknown | undefined): MaybePromise<StatusVerification>;
68
+ }
69
+
70
+ export interface ProposalToEffectStatusHeadStore {
71
+ durable: true;
72
+ readonly tenantId: string;
73
+ readonly relyingPartyId: string;
74
+ accept(
75
+ input: ProposalToEffectStatusHeadAcceptanceInput,
76
+ ): Promise<ProposalToEffectStatusHeadAcceptance>;
77
+ }
78
+
79
+ export interface PostgresProposalToEffectStatusHeadStoreOptions {
80
+ /** Pool authenticated as a tenant-bound member of ep_aeb_executor. */
81
+ pool?: ProposalToEffectStatusHeadPgPool;
82
+ tenantId?: string;
83
+ relyingPartyId?: string;
84
+ }
85
+
86
+ interface StoredHead {
87
+ statusDigest: string;
88
+ sequence: number;
89
+ statusState: 'not_revoked' | 'revoked';
90
+ previousStatusDigest: string | null;
91
+ issuedAt: string;
92
+ nextUpdate: string | null;
93
+ status: unknown;
94
+ predecessorStatus: unknown | undefined;
95
+ }
96
+
97
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
98
+ const TARGET_TYPES = new Set(['receipt', 'commit', 'delegation']);
99
+ const TARGET_USAGES = new Set(['authorization', 'execution', 'delegation']);
100
+
101
+ function dataRecord(value: unknown): value is Record<string, unknown> {
102
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
103
+ const prototype = Object.getPrototypeOf(value);
104
+ if (prototype !== Object.prototype && prototype !== null) return false;
105
+ return Reflect.ownKeys(value).every((key) => {
106
+ if (typeof key !== 'string') return false;
107
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
108
+ return Boolean(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
109
+ });
110
+ }
111
+
112
+ function denseDataArray(value: unknown): value is unknown[] {
113
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) return false;
114
+ const keys = Reflect.ownKeys(value);
115
+ if (keys.length !== value.length + 1 || !keys.includes('length')) return false;
116
+ for (let index = 0; index < value.length; index += 1) {
117
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
118
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) return false;
119
+ }
120
+ return true;
121
+ }
122
+
123
+ function snapshotJson(value: unknown, seen = new WeakSet<object>()): unknown {
124
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
125
+ if (typeof value === 'number') {
126
+ if (Number.isFinite(value)) return value;
127
+ throw new TypeError('status head contains a non-finite JSON number');
128
+ }
129
+ if (typeof value !== 'object' || seen.has(value)) {
130
+ throw new TypeError('status head is outside the JSON data model');
131
+ }
132
+ seen.add(value);
133
+ if (denseDataArray(value)) {
134
+ return value.map((member) => snapshotJson(member, seen));
135
+ }
136
+ if (!dataRecord(value)) throw new TypeError('status head is not a plain data object');
137
+ const snapshot: Record<string, unknown> = {};
138
+ for (const key of Object.keys(value)) snapshot[key] = snapshotJson(value[key], seen);
139
+ return snapshot;
140
+ }
141
+
142
+ function assertText(
143
+ value: unknown,
144
+ label: string,
145
+ maximumBytes: number,
146
+ ): asserts value is string {
147
+ if (typeof value !== 'string'
148
+ || Buffer.byteLength(value, 'utf8') < 1
149
+ || Buffer.byteLength(value, 'utf8') > maximumBytes
150
+ || /[\u0000-\u001f\u007f]/.test(value)) {
151
+ throw new TypeError(`proposal-to-effect status head ${label} is invalid`);
152
+ }
153
+ }
154
+
155
+ function digest(value: unknown): value is string {
156
+ return typeof value === 'string' && DIGEST_PATTERN.test(value);
157
+ }
158
+
159
+ function normalizedInstant(value: unknown, nullable = false): string | null {
160
+ if (nullable && value === null) return null;
161
+ if (typeof value !== 'string') return null;
162
+ const milliseconds = Date.parse(value);
163
+ if (!Number.isFinite(milliseconds)) return null;
164
+ return new Date(milliseconds).toISOString();
165
+ }
166
+
167
+ function safeInteger(value: unknown): number | null {
168
+ const parsed = typeof value === 'string' && /^[0-9]+$/.test(value)
169
+ ? Number(value) : value;
170
+ return Number.isSafeInteger(parsed) && (parsed as number) >= 0
171
+ ? parsed as number : null;
172
+ }
173
+
174
+ function targetSnapshot(value: unknown): Readonly<StatusTarget> {
175
+ if (!dataRecord(value)
176
+ || Reflect.ownKeys(value).length !== 4
177
+ || !Object.hasOwn(value, 'type')
178
+ || !Object.hasOwn(value, 'id')
179
+ || !Object.hasOwn(value, 'digest')
180
+ || !Object.hasOwn(value, 'usage')
181
+ || !TARGET_TYPES.has(value.type as string)
182
+ || !TARGET_USAGES.has(value.usage as string)
183
+ || typeof value.id !== 'string'
184
+ || Buffer.byteLength(value.id, 'utf8') < 1
185
+ || Buffer.byteLength(value.id, 'utf8') > 512
186
+ || /[\u0000-\u001f\u007f]/.test(value.id)
187
+ || !digest(value.digest)) {
188
+ throw new TypeError('proposal-to-effect status head target is invalid');
189
+ }
190
+ return Object.freeze({
191
+ type: value.type as StatusTarget['type'],
192
+ id: value.id,
193
+ digest: value.digest,
194
+ usage: value.usage as StatusTarget['usage'],
195
+ });
196
+ }
197
+
198
+ function exactRowCount(result: QueryResult, operation: string): number {
199
+ if (!result || !Number.isSafeInteger(result.rowCount) || (result.rowCount as number) < 0) {
200
+ throw new Error(`${operation}: malformed PostgreSQL result`);
201
+ }
202
+ return result.rowCount as number;
203
+ }
204
+
205
+ function parseJsonText(value: unknown, label: string): unknown {
206
+ if (typeof value !== 'string'
207
+ || Buffer.byteLength(value, 'utf8') < 2
208
+ || Buffer.byteLength(value, 'utf8') > 1_048_576) {
209
+ throw new Error(`malformed PostgreSQL status head: ${label}`);
210
+ }
211
+ try {
212
+ return snapshotJson(JSON.parse(value));
213
+ } catch {
214
+ throw new Error(`malformed PostgreSQL status head: ${label}`);
215
+ }
216
+ }
217
+
218
+ function storedHead(result: QueryResult): StoredHead | null {
219
+ const rows = exactRowCount(result, 'load status head');
220
+ if (rows === 0 && (!result.rows || result.rows.length === 0)) return null;
221
+ if (rows !== 1 || result.rows?.length !== 1 || !dataRecord(result.rows[0])) {
222
+ throw new Error('malformed PostgreSQL status head: row cardinality');
223
+ }
224
+ const row = result.rows[0];
225
+ const sequence = safeInteger(row.sequence);
226
+ const issuedAt = normalizedInstant(row.issued_at);
227
+ const nextUpdate = normalizedInstant(row.next_update, true);
228
+ const statusState = row.status_state;
229
+ const previousStatusDigest = row.previous_status_digest;
230
+ if (!digest(row.status_digest)
231
+ || sequence === null
232
+ || (statusState !== 'not_revoked' && statusState !== 'revoked')
233
+ || (previousStatusDigest !== null && !digest(previousStatusDigest))
234
+ || !issuedAt
235
+ || (statusState === 'not_revoked' && !nextUpdate)
236
+ || (statusState === 'revoked' && nextUpdate !== null)) {
237
+ throw new Error('malformed PostgreSQL status head: metadata');
238
+ }
239
+ const status = parseJsonText(row.status_json, 'status_json');
240
+ const predecessorStatus = row.predecessor_status_json === null
241
+ ? undefined : parseJsonText(row.predecessor_status_json, 'predecessor_status_json');
242
+ if (statusArtifactDigest(status) !== row.status_digest
243
+ || (sequence === 0
244
+ && (previousStatusDigest !== null || predecessorStatus !== undefined))
245
+ || (sequence > 0
246
+ && (previousStatusDigest === null
247
+ || predecessorStatus === undefined
248
+ || statusArtifactDigest(predecessorStatus) !== previousStatusDigest))) {
249
+ throw new Error('malformed PostgreSQL status head: chain');
250
+ }
251
+ return {
252
+ statusDigest: row.status_digest,
253
+ sequence,
254
+ statusState,
255
+ previousStatusDigest,
256
+ issuedAt,
257
+ nextUpdate,
258
+ status,
259
+ predecessorStatus,
260
+ };
261
+ }
262
+
263
+ function candidateMetadata(
264
+ status: unknown,
265
+ verification: StatusVerification,
266
+ ) {
267
+ if (!dataRecord(status)
268
+ || !digest(verification.status_digest)
269
+ || !Number.isSafeInteger(verification.sequence)
270
+ || verification.sequence === null
271
+ || verification.sequence < 0
272
+ || verification.status_digest !== statusArtifactDigest(status)
273
+ || status.sequence !== verification.sequence
274
+ || (status.status !== 'not_revoked' && status.status !== 'revoked')
275
+ || (status.previous_status_digest !== null
276
+ && !digest(status.previous_status_digest))) {
277
+ throw new Error('verified status head metadata is inconsistent');
278
+ }
279
+ const issuedAt = normalizedInstant(status.issued_at);
280
+ const nextUpdate = normalizedInstant(status.next_update, true);
281
+ const verificationNextUpdate = normalizedInstant(verification.next_update, true);
282
+ if (!issuedAt
283
+ || nextUpdate !== verificationNextUpdate
284
+ || (status.status === 'not_revoked' && !nextUpdate)
285
+ || (status.status === 'revoked' && nextUpdate !== null)) {
286
+ throw new Error('verified status head timing is inconsistent');
287
+ }
288
+ return {
289
+ statusDigest: verification.status_digest,
290
+ sequence: verification.sequence,
291
+ statusState: status.status,
292
+ previousStatusDigest: status.previous_status_digest,
293
+ issuedAt,
294
+ nextUpdate,
295
+ };
296
+ }
297
+
298
+ function verificationRefusal(
299
+ verification: StatusVerification,
300
+ ): ProposalToEffectStatusHeadAcceptance {
301
+ return {
302
+ accepted: false,
303
+ source: null,
304
+ reason: verification.reasons[0] ?? 'status_verification_failed',
305
+ verification,
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Create a durable accepted-head store. The PostgreSQL principal must be bound
311
+ * to tenantId in ep_aeb_private.tenant_principals and inherit ep_aeb_executor.
312
+ */
313
+ export function createPostgresProposalToEffectStatusHeadStore({
314
+ pool,
315
+ tenantId,
316
+ relyingPartyId,
317
+ }: PostgresProposalToEffectStatusHeadStoreOptions = {}): ProposalToEffectStatusHeadStore {
318
+ if (!pool || typeof pool.connect !== 'function') {
319
+ throw new TypeError(
320
+ 'createPostgresProposalToEffectStatusHeadStore requires an ep_aeb_executor pg pool',
321
+ );
322
+ }
323
+ assertText(tenantId, 'tenantId', 512);
324
+ assertText(relyingPartyId, 'relyingPartyId', 512);
325
+
326
+ const store: ProposalToEffectStatusHeadStore = {
327
+ durable: true,
328
+ tenantId,
329
+ relyingPartyId,
330
+
331
+ async accept(input): Promise<ProposalToEffectStatusHeadAcceptance> {
332
+ const target = targetSnapshot(input?.target);
333
+ const candidate = snapshotJson(input?.status);
334
+ if (typeof input?.verify !== 'function') {
335
+ throw new TypeError('proposal-to-effect status head verify callback is required');
336
+ }
337
+ const candidateDigest = statusArtifactDigest(candidate);
338
+ if (!digest(candidateDigest)) {
339
+ throw new TypeError('proposal-to-effect status head digest is invalid');
340
+ }
341
+
342
+ const client = await pool.connect();
343
+ if (!client || typeof client.query !== 'function' || typeof client.release !== 'function') {
344
+ throw new TypeError('status head pg pool returned an invalid client');
345
+ }
346
+ try {
347
+ const current = storedHead(await client.query(
348
+ PROPOSAL_TO_EFFECT_STATUS_HEAD_SQL.get,
349
+ [tenantId, relyingPartyId, target.type, target.id, target.digest, target.usage],
350
+ ));
351
+ const exactReplay = current?.statusDigest === candidateDigest;
352
+ const statusForVerification = exactReplay ? current.status : candidate;
353
+ const previousForVerification = exactReplay
354
+ ? current.predecessorStatus
355
+ : current?.status;
356
+ const verification = await input.verify(previousForVerification);
357
+ if (!dataRecord(verification)
358
+ || typeof verification.valid !== 'boolean'
359
+ || !Array.isArray(verification.reasons)) {
360
+ throw new Error('status head verifier returned a malformed result');
361
+ }
362
+ if (!verification.valid || verification.outcome === 'indeterminate') {
363
+ return verificationRefusal(verification);
364
+ }
365
+
366
+ const metadata = candidateMetadata(statusForVerification, verification);
367
+ if (exactReplay) {
368
+ if (metadata.statusDigest !== current.statusDigest
369
+ || metadata.sequence !== current.sequence
370
+ || metadata.statusState !== current.statusState
371
+ || metadata.previousStatusDigest !== current.previousStatusDigest
372
+ || metadata.issuedAt !== current.issuedAt
373
+ || metadata.nextUpdate !== current.nextUpdate) {
374
+ throw new Error('authenticated status head replay metadata mismatch');
375
+ }
376
+ }
377
+
378
+ if (!exactReplay && ((!current
379
+ && (metadata.sequence !== 0 || metadata.previousStatusDigest !== null))
380
+ || (current
381
+ && (current.statusState === 'revoked'
382
+ || metadata.sequence !== current.sequence + 1
383
+ || metadata.previousStatusDigest !== current.statusDigest)))) {
384
+ return {
385
+ accepted: false,
386
+ source: null,
387
+ reason: 'status_head_conflict',
388
+ verification,
389
+ };
390
+ }
391
+
392
+ const result = await client.query(
393
+ PROPOSAL_TO_EFFECT_STATUS_HEAD_SQL.compareAndAdvance,
394
+ [
395
+ tenantId,
396
+ relyingPartyId,
397
+ target.type,
398
+ target.id,
399
+ target.digest,
400
+ target.usage,
401
+ current?.statusDigest ?? null,
402
+ metadata.statusDigest,
403
+ metadata.sequence,
404
+ metadata.statusState,
405
+ metadata.previousStatusDigest,
406
+ metadata.issuedAt,
407
+ metadata.nextUpdate,
408
+ JSON.stringify(exactReplay ? current.status : candidate),
409
+ ],
410
+ );
411
+ const rows = exactRowCount(result, 'advance status head');
412
+ const accepted = result.rows?.[0]?.accepted;
413
+ const reason = result.rows?.[0]?.reason;
414
+ if (rows !== 1
415
+ || result.rows?.length !== 1
416
+ || typeof accepted !== 'boolean'
417
+ || (reason !== null && typeof reason !== 'string')
418
+ || (accepted && reason !== null)) {
419
+ throw new Error('advance status head: malformed PostgreSQL result');
420
+ }
421
+ if (!accepted) {
422
+ return {
423
+ accepted: false,
424
+ source: null,
425
+ reason: 'status_head_conflict',
426
+ verification,
427
+ };
428
+ }
429
+ return {
430
+ accepted: true,
431
+ source: exactReplay ? 'existing' : 'advanced',
432
+ reason: null,
433
+ verification,
434
+ };
435
+ } finally {
436
+ client.release();
437
+ }
438
+ },
439
+ };
440
+
441
+ return Object.freeze(store);
442
+ }
443
+
444
+ export default {
445
+ PROPOSAL_TO_EFFECT_STATUS_HEAD_STORE_VERSION,
446
+ PROPOSAL_TO_EFFECT_STATUS_HEAD_TABLE,
447
+ PROPOSAL_TO_EFFECT_STATUS_HEAD_SQL,
448
+ createPostgresProposalToEffectStatusHeadStore,
449
+ };
@@ -13,12 +13,31 @@ import {
13
13
  verifyStatusArtifact,
14
14
  type RevokerAuthorityPin,
15
15
  type StatusTarget,
16
+ type StatusVerification,
16
17
  } from '@emilia-protocol/verify/status';
17
18
 
18
19
  import type {
19
20
  ProposalToEffectCurrentStatusVerification,
20
21
  ProposalToEffectOptions,
21
22
  } from './proposal-to-effect.js';
23
+ import type {
24
+ ProposalToEffectStatusHeadStore,
25
+ } from './proposal-to-effect-status-head-store.js';
26
+
27
+ export {
28
+ PROPOSAL_TO_EFFECT_STATUS_HEAD_STORE_VERSION,
29
+ PROPOSAL_TO_EFFECT_STATUS_HEAD_TABLE,
30
+ PROPOSAL_TO_EFFECT_STATUS_HEAD_SQL,
31
+ createPostgresProposalToEffectStatusHeadStore,
32
+ } from './proposal-to-effect-status-head-store.js';
33
+ export type {
34
+ PostgresProposalToEffectStatusHeadStoreOptions,
35
+ ProposalToEffectStatusHeadAcceptance,
36
+ ProposalToEffectStatusHeadAcceptanceInput,
37
+ ProposalToEffectStatusHeadPgClient,
38
+ ProposalToEffectStatusHeadPgPool,
39
+ ProposalToEffectStatusHeadStore,
40
+ } from './proposal-to-effect-status-head-store.js';
22
41
 
23
42
  type MaybePromise<T> = T | Promise<T>;
24
43
  type ProposalToEffectStatusVerifier = ProposalToEffectOptions['aeb']['statusVerifier'];
@@ -33,13 +52,6 @@ export interface ProposalToEffectStatusResolverContext {
33
52
  target: Readonly<StatusTarget>;
34
53
  }
35
54
 
36
- export interface ProposalToEffectPreviousHeadResolution {
37
- /** Confirms this result came from the relying party's authenticated store. */
38
- authenticated: boolean;
39
- /** Null means the store authoritatively has no accepted head for the target. */
40
- status: unknown | null;
41
- }
42
-
43
55
  export interface ProposalToEffectConsumptionState {
44
56
  /** Must be true; presenter assertions and unauthenticated cache data fail. */
45
57
  authenticated: boolean;
@@ -63,10 +75,12 @@ export interface ProposalToEffectStatusVerifierOptions {
63
75
  certificateResolver(
64
76
  input: ProposalToEffectStatusResolverContext,
65
77
  ): MaybePromise<unknown>;
66
- /** Server-side lookup of the relying party's previously accepted status head. */
67
- previousHeadResolver(
68
- input: ProposalToEffectStatusResolverContext,
69
- ): MaybePromise<ProposalToEffectPreviousHeadResolution>;
78
+ /**
79
+ * Durable relying-party status custody. It loads the accepted predecessor,
80
+ * verifies the candidate against that predecessor, and compare-and-advances
81
+ * one fixed tenant/relying-party/target head atomically.
82
+ */
83
+ statusHeadStore: ProposalToEffectStatusHeadStore;
70
84
  /** Authenticated local consumption lookup; this is not inferred from EP-STATUS-v1. */
71
85
  consumptionStateResolver(
72
86
  input: ProposalToEffectConsumptionResolverContext,
@@ -229,7 +243,11 @@ function verifierConfiguration(
229
243
  if (!authorityPin
230
244
  || typeof options.targetMapper !== 'function'
231
245
  || typeof options.certificateResolver !== 'function'
232
- || typeof options.previousHeadResolver !== 'function'
246
+ || !dataRecord(options.statusHeadStore)
247
+ || options.statusHeadStore.durable !== true
248
+ || !boundedString(options.statusHeadStore.tenantId)
249
+ || !boundedString(options.statusHeadStore.relyingPartyId)
250
+ || typeof options.statusHeadStore.accept !== 'function'
233
251
  || typeof options.consumptionStateResolver !== 'function') {
234
252
  throw new TypeError('proposal_to_effect_status_configuration_invalid');
235
253
  }
@@ -246,7 +264,7 @@ export function createProposalToEffectStatusVerifier(
246
264
  const authorityPin = verifierConfiguration(options);
247
265
  const targetMapper = options.targetMapper;
248
266
  const certificateResolver = options.certificateResolver;
249
- const previousHeadResolver = options.previousHeadResolver;
267
+ const statusHeadStore = options.statusHeadStore;
250
268
  const consumptionStateResolver = options.consumptionStateResolver;
251
269
 
252
270
  return async (input): Promise<ProposalToEffectCurrentStatusVerification> => {
@@ -270,7 +288,6 @@ export function createProposalToEffectStatusVerifier(
270
288
 
271
289
  const context = Object.freeze({ expected, target });
272
290
  let certificate: unknown;
273
- let previousStatus: unknown;
274
291
  try {
275
292
  const resolvedCertificate = await certificateResolver(context);
276
293
  if (resolvedCertificate === undefined || resolvedCertificate === null) {
@@ -280,26 +297,40 @@ export function createProposalToEffectStatusVerifier(
280
297
  } catch {
281
298
  return refusal('indeterminate', 'status_certificate_unavailable');
282
299
  }
300
+
301
+ if (statusHeadStore.tenantId !== expected.tenant_id) {
302
+ return refusal('indeterminate', 'status_head_scope_mismatch');
303
+ }
304
+
305
+ let acceptance;
283
306
  try {
284
- const resolution = await previousHeadResolver(context);
285
- if (!dataRecord(resolution)
286
- || resolution.authenticated !== true
287
- || !Object.hasOwn(resolution, 'status')
288
- || resolution.status === undefined) {
289
- return refusal('indeterminate', 'status_previous_head_unavailable');
290
- }
291
- previousStatus = resolution.status === null
292
- ? undefined : snapshotJson(resolution.status);
307
+ acceptance = await statusHeadStore.accept({
308
+ target,
309
+ status: statusArtifact,
310
+ verify: (previousStatus) => verifyStatusArtifact(target, statusArtifact, {
311
+ authorityPin,
312
+ certificate,
313
+ previousStatus,
314
+ now: input.now,
315
+ }),
316
+ });
293
317
  } catch {
294
- return refusal('indeterminate', 'status_previous_head_unavailable');
318
+ return refusal('indeterminate', 'status_head_store_unavailable');
295
319
  }
296
-
297
- const verification = verifyStatusArtifact(target, statusArtifact, {
298
- authorityPin,
299
- certificate,
300
- previousStatus,
301
- now: input.now,
302
- });
320
+ if (!dataRecord(acceptance)
321
+ || typeof acceptance.accepted !== 'boolean'
322
+ || !Object.hasOwn(acceptance, 'verification')
323
+ || !dataRecord(acceptance.verification)) {
324
+ return refusal('indeterminate', 'status_head_store_invalid');
325
+ }
326
+ if (!acceptance.accepted) {
327
+ return refusal(
328
+ 'indeterminate',
329
+ boundedString(acceptance.reason)
330
+ ? acceptance.reason : 'status_head_store_refused',
331
+ );
332
+ }
333
+ const verification = acceptance.verification as unknown as StatusVerification;
303
334
  if (!verification.valid || verification.outcome === 'indeterminate') {
304
335
  return refusal(
305
336
  'indeterminate',