@emilia-protocol/gate 0.14.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.
- package/CHANGELOG.md +60 -1
- package/README.md +32 -1
- package/aeb-consumption-store.js +3 -0
- package/dist/aeb-consumption-store.d.ts +88 -0
- package/dist/aeb-consumption-store.d.ts.map +1 -0
- package/dist/aeb-consumption-store.js +471 -0
- package/dist/aeb-consumption-store.js.map +1 -0
- package/dist/capability-receipt.d.ts +8 -0
- package/dist/capability-receipt.d.ts.map +1 -1
- package/dist/capability-receipt.js +17 -0
- package/dist/capability-receipt.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/proposal-to-effect-postgres.d.ts +144 -0
- package/dist/proposal-to-effect-postgres.d.ts.map +1 -0
- package/dist/proposal-to-effect-postgres.js +1578 -0
- package/dist/proposal-to-effect-postgres.js.map +1 -0
- package/dist/proposal-to-effect-status-head-store.d.ts +72 -0
- package/dist/proposal-to-effect-status-head-store.d.ts.map +1 -0
- package/dist/proposal-to-effect-status-head-store.js +355 -0
- package/dist/proposal-to-effect-status-head-store.js.map +1 -0
- package/dist/proposal-to-effect-status.d.ts +55 -0
- package/dist/proposal-to-effect-status.d.ts.map +1 -0
- package/dist/proposal-to-effect-status.js +294 -0
- package/dist/proposal-to-effect-status.js.map +1 -0
- package/dist/proposal-to-effect.d.ts +151 -3
- package/dist/proposal-to-effect.d.ts.map +1 -1
- package/dist/proposal-to-effect.js +589 -46
- package/dist/proposal-to-effect.js.map +1 -1
- package/dist/remedy-case-set-postgres.d.ts +73 -0
- package/dist/remedy-case-set-postgres.d.ts.map +1 -0
- package/dist/remedy-case-set-postgres.js +846 -0
- package/dist/remedy-case-set-postgres.js.map +1 -0
- package/dist/remedy-case-set.d.ts +53 -0
- package/dist/remedy-case-set.d.ts.map +1 -0
- package/dist/remedy-case-set.js +571 -0
- package/dist/remedy-case-set.js.map +1 -0
- package/dist/remedy-program-adapters.d.ts +137 -0
- package/dist/remedy-program-adapters.d.ts.map +1 -0
- package/dist/remedy-program-adapters.js +590 -0
- package/dist/remedy-program-adapters.js.map +1 -0
- package/dist/trust-program-revocation.js.map +1 -1
- package/package.json +283 -63
- package/proposal-to-effect-postgres.js +3 -0
- package/proposal-to-effect-status-head-store.js +2 -0
- package/proposal-to-effect-status.js +2 -0
- package/remedy-case-set-postgres.js +3 -0
- package/remedy-case-set.js +3 -0
- package/remedy-program-adapters.js +3 -0
- package/src/aeb-consumption-store.ts +568 -0
- package/src/capability-receipt.ts +17 -0
- package/src/index.ts +66 -3
- package/src/proposal-to-effect-postgres.ts +1833 -0
- package/src/proposal-to-effect-status-head-store.ts +449 -0
- package/src/proposal-to-effect-status.ts +388 -0
- package/src/proposal-to-effect.ts +755 -50
- package/src/remedy-case-set-postgres.ts +1008 -0
- package/src/remedy-case-set.ts +603 -0
- package/src/remedy-program-adapters.ts +657 -0
- package/src/trust-program-revocation.ts +1 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Server-pinned EP-STATUS-v1 verification for Proposal-to-Effect AEB legs.
|
|
4
|
+
*
|
|
5
|
+
* Signed non-revocation status and relying-party consumption state answer
|
|
6
|
+
* different questions. This helper authenticates the former with
|
|
7
|
+
* verifyStatusArtifact and requires an authenticated local answer for the
|
|
8
|
+
* latter. The later atomic AEB reserve remains the race-closing operation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AebDigest, AebStatusInput } from '@emilia-protocol/verify/aeb-adapter-contract';
|
|
12
|
+
import {
|
|
13
|
+
verifyStatusArtifact,
|
|
14
|
+
type RevokerAuthorityPin,
|
|
15
|
+
type StatusTarget,
|
|
16
|
+
type StatusVerification,
|
|
17
|
+
} from '@emilia-protocol/verify/status';
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
ProposalToEffectCurrentStatusVerification,
|
|
21
|
+
ProposalToEffectOptions,
|
|
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';
|
|
41
|
+
|
|
42
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
43
|
+
type ProposalToEffectStatusVerifier = ProposalToEffectOptions['aeb']['statusVerifier'];
|
|
44
|
+
|
|
45
|
+
export type ProposalToEffectStatusVerifierInput =
|
|
46
|
+
Parameters<ProposalToEffectStatusVerifier>[0];
|
|
47
|
+
export type ProposalToEffectStatusExpected =
|
|
48
|
+
Readonly<ProposalToEffectStatusVerifierInput['expected']>;
|
|
49
|
+
|
|
50
|
+
export interface ProposalToEffectStatusResolverContext {
|
|
51
|
+
expected: ProposalToEffectStatusExpected;
|
|
52
|
+
target: Readonly<StatusTarget>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ProposalToEffectConsumptionState {
|
|
56
|
+
/** Must be true; presenter assertions and unauthenticated cache data fail. */
|
|
57
|
+
authenticated: boolean;
|
|
58
|
+
consumed: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ProposalToEffectConsumptionResolverContext
|
|
62
|
+
extends ProposalToEffectStatusResolverContext {
|
|
63
|
+
status_digest: AebDigest;
|
|
64
|
+
sequence: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ProposalToEffectStatusVerifierOptions {
|
|
68
|
+
/** Copied at factory construction; callers cannot swap the authority pin later. */
|
|
69
|
+
authorityPin: RevokerAuthorityPin;
|
|
70
|
+
/** Trusted code mapping the closed PTE expected binding to one exact status target. */
|
|
71
|
+
targetMapper(input: {
|
|
72
|
+
expected: ProposalToEffectStatusExpected;
|
|
73
|
+
}): MaybePromise<StatusTarget>;
|
|
74
|
+
/** Server-side certificate lookup. The presenter never supplies this certificate. */
|
|
75
|
+
certificateResolver(
|
|
76
|
+
input: ProposalToEffectStatusResolverContext,
|
|
77
|
+
): MaybePromise<unknown>;
|
|
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;
|
|
84
|
+
/** Authenticated local consumption lookup; this is not inferred from EP-STATUS-v1. */
|
|
85
|
+
consumptionStateResolver(
|
|
86
|
+
input: ProposalToEffectConsumptionResolverContext,
|
|
87
|
+
): MaybePromise<ProposalToEffectConsumptionState>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
91
|
+
const CAID_PATTERN = /^caid:1:[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*\.[1-9][0-9]*:[a-z0-9]+(?:-[a-z0-9]+)*:[A-Za-z0-9_-]{43}$/;
|
|
92
|
+
const EXPECTED_KEYS = [
|
|
93
|
+
'tenant_id',
|
|
94
|
+
'executor_id',
|
|
95
|
+
'operation_id',
|
|
96
|
+
'caid',
|
|
97
|
+
'artifact_ref',
|
|
98
|
+
'evidence_digest',
|
|
99
|
+
'replay_unit',
|
|
100
|
+
] as const;
|
|
101
|
+
const TARGET_KEYS = ['type', 'id', 'digest', 'usage'] as const;
|
|
102
|
+
const AUTHORITY_PIN_KEYS = [
|
|
103
|
+
'authority_domain',
|
|
104
|
+
'authority_id',
|
|
105
|
+
'key_id',
|
|
106
|
+
'public_key',
|
|
107
|
+
] as const;
|
|
108
|
+
|
|
109
|
+
function dataRecord(value: unknown): value is Record<string, unknown> {
|
|
110
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
111
|
+
const prototype = Object.getPrototypeOf(value);
|
|
112
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
113
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
114
|
+
if (typeof key !== 'string') return false;
|
|
115
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
116
|
+
return Boolean(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function exactDataRecord(
|
|
121
|
+
value: unknown,
|
|
122
|
+
keys: readonly string[],
|
|
123
|
+
): value is Record<string, unknown> {
|
|
124
|
+
if (!dataRecord(value)) return false;
|
|
125
|
+
const actual = Reflect.ownKeys(value);
|
|
126
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function boundedString(value: unknown, maximum = 512): value is string {
|
|
130
|
+
return typeof value === 'string' && value.length > 0 && value.length <= maximum;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function digest(value: unknown): value is AebDigest {
|
|
134
|
+
return typeof value === 'string' && DIGEST_PATTERN.test(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function snapshotAuthorityPin(value: unknown): Readonly<RevokerAuthorityPin> | null {
|
|
138
|
+
if (!exactDataRecord(value, AUTHORITY_PIN_KEYS)
|
|
139
|
+
|| !boundedString(value.authority_domain, 253)
|
|
140
|
+
|| !boundedString(value.authority_id)
|
|
141
|
+
|| !boundedString(value.key_id)
|
|
142
|
+
|| !boundedString(value.public_key, 4096)) return null;
|
|
143
|
+
return Object.freeze({
|
|
144
|
+
authority_domain: value.authority_domain,
|
|
145
|
+
authority_id: value.authority_id,
|
|
146
|
+
key_id: value.key_id,
|
|
147
|
+
public_key: value.public_key,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function snapshotExpected(value: unknown): ProposalToEffectStatusExpected | null {
|
|
152
|
+
if (!exactDataRecord(value, EXPECTED_KEYS)
|
|
153
|
+
|| !boundedString(value.tenant_id)
|
|
154
|
+
|| !boundedString(value.executor_id)
|
|
155
|
+
|| !boundedString(value.operation_id)
|
|
156
|
+
|| typeof value.caid !== 'string'
|
|
157
|
+
|| !CAID_PATTERN.test(value.caid)
|
|
158
|
+
|| !boundedString(value.artifact_ref)
|
|
159
|
+
|| !digest(value.evidence_digest)
|
|
160
|
+
|| !digest(value.replay_unit)) return null;
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
tenant_id: value.tenant_id,
|
|
163
|
+
executor_id: value.executor_id,
|
|
164
|
+
operation_id: value.operation_id,
|
|
165
|
+
caid: value.caid,
|
|
166
|
+
artifact_ref: value.artifact_ref,
|
|
167
|
+
evidence_digest: value.evidence_digest,
|
|
168
|
+
replay_unit: value.replay_unit,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function snapshotTarget(value: unknown): Readonly<StatusTarget> | null {
|
|
173
|
+
if (!exactDataRecord(value, TARGET_KEYS)
|
|
174
|
+
|| !['receipt', 'commit', 'delegation'].includes(value.type as string)
|
|
175
|
+
|| !boundedString(value.id)
|
|
176
|
+
|| !digest(value.digest)
|
|
177
|
+
|| !['authorization', 'execution', 'delegation'].includes(value.usage as string)) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
type: value.type as StatusTarget['type'],
|
|
182
|
+
id: value.id,
|
|
183
|
+
digest: value.digest,
|
|
184
|
+
usage: value.usage as StatusTarget['usage'],
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function denseDataArray(value: unknown): value is unknown[] {
|
|
189
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) return false;
|
|
190
|
+
const keys = Reflect.ownKeys(value);
|
|
191
|
+
if (keys.length !== value.length + 1 || !keys.includes('length')) return false;
|
|
192
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
193
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
194
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) return false;
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function snapshotJson(value: unknown, seen = new WeakSet<object>()): unknown {
|
|
200
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
201
|
+
if (typeof value === 'number') {
|
|
202
|
+
if (Number.isFinite(value)) return value;
|
|
203
|
+
throw new TypeError('non-finite JSON number');
|
|
204
|
+
}
|
|
205
|
+
if (typeof value !== 'object' || seen.has(value)) {
|
|
206
|
+
throw new TypeError('value is outside the JSON data model');
|
|
207
|
+
}
|
|
208
|
+
seen.add(value);
|
|
209
|
+
if (denseDataArray(value)) {
|
|
210
|
+
return value.map((member) => snapshotJson(member, seen));
|
|
211
|
+
}
|
|
212
|
+
if (!dataRecord(value)) throw new TypeError('value is not a plain data object');
|
|
213
|
+
const snapshot: Record<string, unknown> = {};
|
|
214
|
+
for (const key of Object.keys(value)) {
|
|
215
|
+
snapshot[key] = snapshotJson(value[key], seen);
|
|
216
|
+
}
|
|
217
|
+
return snapshot;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizedInstant(value: unknown): string | null {
|
|
221
|
+
if (typeof value !== 'string') return null;
|
|
222
|
+
const milliseconds = Date.parse(value);
|
|
223
|
+
if (!Number.isFinite(milliseconds)) return null;
|
|
224
|
+
return new Date(milliseconds).toISOString();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function refusal(
|
|
228
|
+
outcome: 'revoked' | 'indeterminate',
|
|
229
|
+
reason: string,
|
|
230
|
+
): ProposalToEffectCurrentStatusVerification {
|
|
231
|
+
return {
|
|
232
|
+
valid: false,
|
|
233
|
+
outcome,
|
|
234
|
+
status: null,
|
|
235
|
+
reason,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function verifierConfiguration(
|
|
240
|
+
options: ProposalToEffectStatusVerifierOptions,
|
|
241
|
+
): Readonly<RevokerAuthorityPin> {
|
|
242
|
+
const authorityPin = snapshotAuthorityPin(options?.authorityPin);
|
|
243
|
+
if (!authorityPin
|
|
244
|
+
|| typeof options.targetMapper !== 'function'
|
|
245
|
+
|| typeof options.certificateResolver !== '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'
|
|
251
|
+
|| typeof options.consumptionStateResolver !== 'function') {
|
|
252
|
+
throw new TypeError('proposal_to_effect_status_configuration_invalid');
|
|
253
|
+
}
|
|
254
|
+
return authorityPin;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Build the server-side verifier expected by
|
|
259
|
+
* `ProposalToEffectOptions.aeb.statusVerifier`.
|
|
260
|
+
*/
|
|
261
|
+
export function createProposalToEffectStatusVerifier(
|
|
262
|
+
options: ProposalToEffectStatusVerifierOptions,
|
|
263
|
+
): ProposalToEffectStatusVerifier {
|
|
264
|
+
const authorityPin = verifierConfiguration(options);
|
|
265
|
+
const targetMapper = options.targetMapper;
|
|
266
|
+
const certificateResolver = options.certificateResolver;
|
|
267
|
+
const statusHeadStore = options.statusHeadStore;
|
|
268
|
+
const consumptionStateResolver = options.consumptionStateResolver;
|
|
269
|
+
|
|
270
|
+
return async (input): Promise<ProposalToEffectCurrentStatusVerification> => {
|
|
271
|
+
const expected = snapshotExpected(input?.expected);
|
|
272
|
+
const checkedAt = normalizedInstant(input?.now);
|
|
273
|
+
if (!expected || !checkedAt) {
|
|
274
|
+
return refusal('indeterminate', 'status_expected_binding_invalid');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let statusArtifact: unknown;
|
|
278
|
+
let target: Readonly<StatusTarget>;
|
|
279
|
+
try {
|
|
280
|
+
statusArtifact = snapshotJson(input.status_artifact);
|
|
281
|
+
const mapped = await targetMapper({ expected });
|
|
282
|
+
const snapshot = snapshotTarget(mapped);
|
|
283
|
+
if (!snapshot) return refusal('indeterminate', 'status_target_resolution_failed');
|
|
284
|
+
target = snapshot;
|
|
285
|
+
} catch {
|
|
286
|
+
return refusal('indeterminate', 'status_target_resolution_failed');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const context = Object.freeze({ expected, target });
|
|
290
|
+
let certificate: unknown;
|
|
291
|
+
try {
|
|
292
|
+
const resolvedCertificate = await certificateResolver(context);
|
|
293
|
+
if (resolvedCertificate === undefined || resolvedCertificate === null) {
|
|
294
|
+
return refusal('indeterminate', 'status_certificate_unavailable');
|
|
295
|
+
}
|
|
296
|
+
certificate = snapshotJson(resolvedCertificate);
|
|
297
|
+
} catch {
|
|
298
|
+
return refusal('indeterminate', 'status_certificate_unavailable');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (statusHeadStore.tenantId !== expected.tenant_id) {
|
|
302
|
+
return refusal('indeterminate', 'status_head_scope_mismatch');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let acceptance;
|
|
306
|
+
try {
|
|
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
|
+
});
|
|
317
|
+
} catch {
|
|
318
|
+
return refusal('indeterminate', 'status_head_store_unavailable');
|
|
319
|
+
}
|
|
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;
|
|
334
|
+
if (!verification.valid || verification.outcome === 'indeterminate') {
|
|
335
|
+
return refusal(
|
|
336
|
+
'indeterminate',
|
|
337
|
+
verification.reasons[0] ?? 'status_verification_failed',
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (verification.outcome === 'revoked') {
|
|
341
|
+
return refusal('revoked', 'status_revoked');
|
|
342
|
+
}
|
|
343
|
+
if (!digest(verification.status_digest)
|
|
344
|
+
|| !Number.isSafeInteger(verification.sequence)
|
|
345
|
+
|| verification.sequence === null) {
|
|
346
|
+
return refusal('indeterminate', 'status_verification_incomplete');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let consumption: ProposalToEffectConsumptionState;
|
|
350
|
+
try {
|
|
351
|
+
consumption = await consumptionStateResolver(Object.freeze({
|
|
352
|
+
...context,
|
|
353
|
+
status_digest: verification.status_digest,
|
|
354
|
+
sequence: verification.sequence,
|
|
355
|
+
}));
|
|
356
|
+
} catch {
|
|
357
|
+
return refusal('indeterminate', 'status_consumption_state_unavailable');
|
|
358
|
+
}
|
|
359
|
+
if (!dataRecord(consumption)
|
|
360
|
+
|| consumption.authenticated !== true
|
|
361
|
+
|| typeof consumption.consumed !== 'boolean') {
|
|
362
|
+
return refusal('indeterminate', 'status_consumption_state_unknown');
|
|
363
|
+
}
|
|
364
|
+
if (consumption.consumed) {
|
|
365
|
+
return refusal('indeterminate', 'status_consumed');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const expiresAt = normalizedInstant(verification.next_update);
|
|
369
|
+
if (!expiresAt || Date.parse(checkedAt) >= Date.parse(expiresAt)) {
|
|
370
|
+
return refusal('indeterminate', 'status_expiry_invalid');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// This local answer is an authenticated pre-reservation observation. It
|
|
374
|
+
// does not claim race freedom; Proposal-to-Effect's atomic AEB reserve does.
|
|
375
|
+
const status: AebStatusInput = {
|
|
376
|
+
checked_at: checkedAt,
|
|
377
|
+
expires_at: expiresAt,
|
|
378
|
+
revocation_checked: true,
|
|
379
|
+
revoked: false,
|
|
380
|
+
consumed: false,
|
|
381
|
+
};
|
|
382
|
+
return {
|
|
383
|
+
valid: true,
|
|
384
|
+
outcome: 'current_not_revoked',
|
|
385
|
+
status,
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
}
|