@splitin/verification-engine 0.1.0-beta.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/dist/index.js ADDED
@@ -0,0 +1,2056 @@
1
+ import { assertAdapterConformsToManifest, isTerminalStatus, ProviderError, toSafeProviderFailure, ProviderOperationPendingError, isCountryCode, metadataContainsForbiddenIdentifier } from '@splitin/verification-adapter-sdk';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __esm = (fn, res) => function __init() {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ };
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+
13
+ // src/hash.ts
14
+ var hash_exports = {};
15
+ __export(hash_exports, {
16
+ bytesToHex: () => bytesToHex,
17
+ cohortBucket: () => cohortBucket,
18
+ hmacSha256Hex: () => hmacSha256Hex,
19
+ newId: () => newId,
20
+ randomToken: () => randomToken,
21
+ sha256Hex: () => sha256Hex
22
+ });
23
+ function bytesToHex(value) {
24
+ return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
25
+ }
26
+ async function sha256Hex(payload) {
27
+ const bytes = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
28
+ return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
29
+ }
30
+ async function hmacSha256Hex(secret, payload) {
31
+ const key = await crypto.subtle.importKey(
32
+ "raw",
33
+ new TextEncoder().encode(secret),
34
+ { name: "HMAC", hash: "SHA-256" },
35
+ false,
36
+ ["sign"]
37
+ );
38
+ const bytes = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
39
+ return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, bytes)));
40
+ }
41
+ function randomToken(cryptoImpl = globalThis.crypto) {
42
+ const bytes = new Uint8Array(32);
43
+ cryptoImpl.getRandomValues(bytes);
44
+ return bytesToHex(bytes);
45
+ }
46
+ function newId(prefix, cryptoImpl = globalThis.crypto) {
47
+ return `${prefix}_${cryptoImpl.randomUUID().replace(/-/g, "")}`;
48
+ }
49
+ async function cohortBucket(tenantKey, subjectHash) {
50
+ const hex = await sha256Hex(`${tenantKey}:${subjectHash}`);
51
+ return Number.parseInt(hex.slice(0, 8), 16) % 100;
52
+ }
53
+ var init_hash = __esm({
54
+ "src/hash.ts"() {
55
+ }
56
+ });
57
+
58
+ // src/errors.ts
59
+ var EngineError = class extends Error {
60
+ constructor(code, message, retryable = false, retryAfterSeconds) {
61
+ super(message);
62
+ this.code = code;
63
+ this.retryable = retryable;
64
+ this.retryAfterSeconds = retryAfterSeconds;
65
+ this.name = "EngineError";
66
+ }
67
+ code;
68
+ retryable;
69
+ retryAfterSeconds;
70
+ };
71
+ var AuthorizationError = class extends EngineError {
72
+ constructor(message = "The actor is not authorized for this verification operation.") {
73
+ super("AUTHORIZATION_DENIED", message, false);
74
+ this.name = "AuthorizationError";
75
+ }
76
+ };
77
+ var ClientRouteInjectionError = class extends EngineError {
78
+ constructor(message = "Verification routing is server-owned and cannot be selected by the client.") {
79
+ super("CLIENT_ROUTE_INJECTION", message, false);
80
+ this.name = "ClientRouteInjectionError";
81
+ }
82
+ };
83
+ var WebhookSecurityIncidentError = class extends EngineError {
84
+ constructor(message = "A webhook event key collided with a different body digest.") {
85
+ super("WEBHOOK_SECURITY_INCIDENT", message, false);
86
+ this.name = "WebhookSecurityIncidentError";
87
+ }
88
+ };
89
+
90
+ // src/canonical.ts
91
+ function canonicalize(value) {
92
+ return JSON.stringify(sortValue(value));
93
+ }
94
+ function sortValue(value) {
95
+ if (Array.isArray(value)) return value.map(sortValue);
96
+ if (value && typeof value === "object") {
97
+ const record = value;
98
+ return Object.fromEntries(
99
+ Object.keys(record).sort().map((key) => [key, sortValue(record[key])])
100
+ );
101
+ }
102
+ return value;
103
+ }
104
+ async function digestCanonical(value) {
105
+ const { sha256Hex: sha256Hex2 } = await Promise.resolve().then(() => (init_hash(), hash_exports));
106
+ return sha256Hex2(canonicalize(value));
107
+ }
108
+
109
+ // src/index.ts
110
+ init_hash();
111
+ function createProviderRegistry(input) {
112
+ if (!input.adapters.length) {
113
+ throw new EngineError("INVALID_COMMAND", "A verification provider registry requires at least one compiled-in adapter.");
114
+ }
115
+ const adapters = /* @__PURE__ */ new Map();
116
+ for (const adapter of input.adapters) {
117
+ assertAdapterConformsToManifest(adapter);
118
+ const key = registryKey(adapter.provider, adapter.environment);
119
+ if (adapters.has(key)) {
120
+ throw new EngineError("INVALID_COMMAND", `Duplicate compiled-in adapter for ${key}.`);
121
+ }
122
+ adapters.set(key, adapter);
123
+ }
124
+ return {
125
+ get(provider, environment) {
126
+ if (environment) {
127
+ const exact = adapters.get(registryKey(provider, environment));
128
+ if (exact) return exact;
129
+ }
130
+ const match = [...adapters.values()].find((adapter) => adapter.provider === provider && (!environment || adapter.environment === environment));
131
+ if (!match) {
132
+ throw new EngineError("NO_ELIGIBLE_ROUTE", `No compiled-in adapter is registered for provider "${provider}".`);
133
+ }
134
+ return match;
135
+ },
136
+ list: () => [...adapters.values()],
137
+ has: (provider) => [...adapters.values()].some((adapter) => adapter.provider === provider)
138
+ };
139
+ }
140
+ function registryKey(provider, environment) {
141
+ return `${provider}:${environment}`;
142
+ }
143
+
144
+ // src/memory-store.ts
145
+ init_hash();
146
+ var AsyncMutex = class {
147
+ chain = Promise.resolve();
148
+ run(fn) {
149
+ const run = this.chain.then(fn, fn);
150
+ this.chain = run.then(() => void 0, () => void 0);
151
+ return run;
152
+ }
153
+ };
154
+ function clone(value) {
155
+ return structuredClone(value);
156
+ }
157
+ function createMemoryStore(options = {}) {
158
+ const hashSecret = options.hashSecret ?? "test-verification-hash-secret";
159
+ const nowFn = options.now ?? (() => /* @__PURE__ */ new Date());
160
+ const seedTenantKey = options.seedTenantKey ?? "default";
161
+ const mutex = new AsyncMutex();
162
+ const keyLocks = /* @__PURE__ */ new Map();
163
+ const tenants = /* @__PURE__ */ new Map();
164
+ const configs = /* @__PURE__ */ new Map();
165
+ const providers = /* @__PURE__ */ new Map();
166
+ const routes = /* @__PURE__ */ new Map();
167
+ const routeChanges = /* @__PURE__ */ new Map();
168
+ const policies = /* @__PURE__ */ new Map();
169
+ const requirements = /* @__PURE__ */ new Map();
170
+ const attempts = /* @__PURE__ */ new Map();
171
+ const lineage = /* @__PURE__ */ new Map();
172
+ const decisions = /* @__PURE__ */ new Map();
173
+ const idempotency = /* @__PURE__ */ new Map();
174
+ const webhooks = /* @__PURE__ */ new Map();
175
+ const health = /* @__PURE__ */ new Map();
176
+ const circuits = /* @__PURE__ */ new Map();
177
+ const appeals = /* @__PURE__ */ new Map();
178
+ const reviews = /* @__PURE__ */ new Map();
179
+ const proposals = /* @__PURE__ */ new Map();
180
+ const continuations = /* @__PURE__ */ new Map();
181
+ const audit = /* @__PURE__ */ new Map();
182
+ const jobs = /* @__PURE__ */ new Map();
183
+ const iso = () => nowFn().toISOString();
184
+ const k = (...parts) => parts.join("::");
185
+ const lockFor = (key) => {
186
+ let lock = keyLocks.get(key);
187
+ if (!lock) {
188
+ lock = new AsyncMutex();
189
+ keyLocks.set(key, lock);
190
+ }
191
+ return lock;
192
+ };
193
+ const seedTenant = {
194
+ tenantKey: seedTenantKey,
195
+ displayName: "Default tenant",
196
+ continuationDestinations: ["verification.resume", "application.home"],
197
+ createdAt: iso()
198
+ };
199
+ tenants.set(seedTenantKey, seedTenant);
200
+ const sandboxPolicy = {
201
+ tenantKey: seedTenantKey,
202
+ id: "pol_sandbox_example",
203
+ version: "sandbox-example-1",
204
+ environment: "sandbox",
205
+ lifecycle: "active",
206
+ reason: "Seeded sandbox example policy",
207
+ expiresAt: null,
208
+ proposedByActorId: "system:seed",
209
+ approvedByActorId: "system:seed-approver",
210
+ approvedAt: iso(),
211
+ activatedAt: iso(),
212
+ createdAt: iso(),
213
+ decisionRetentionDays: null,
214
+ providerRedactionDelayDays: null,
215
+ appealHoldDays: null,
216
+ legalHold: false
217
+ };
218
+ policies.set(k(seedTenantKey, sandboxPolicy.id), sandboxPolicy);
219
+ const store = {
220
+ now: nowFn,
221
+ async hashSubject(tenantKey, subjectReference) {
222
+ return hmacSha256Hex(hashSecret, `subject:${tenantKey}:${subjectReference}`);
223
+ },
224
+ async hashResource(tenantKey, resourceType, resourceReference) {
225
+ return hmacSha256Hex(hashSecret, `resource:${tenantKey}:${resourceType}:${resourceReference}`);
226
+ },
227
+ transact(fn) {
228
+ return mutex.run(() => fn(store));
229
+ },
230
+ async getTenant(tenantKey) {
231
+ return tenants.get(tenantKey) ? clone(tenants.get(tenantKey)) : null;
232
+ },
233
+ async ensureTenant(tenantKey, displayName = tenantKey) {
234
+ const existing = tenants.get(tenantKey);
235
+ if (existing) return clone(existing);
236
+ const created = {
237
+ tenantKey,
238
+ displayName,
239
+ continuationDestinations: ["verification.resume"],
240
+ createdAt: iso()
241
+ };
242
+ tenants.set(tenantKey, created);
243
+ return clone(created);
244
+ },
245
+ async getConfigurationRevision(tenantKey, id) {
246
+ const row = configs.get(k(tenantKey, id));
247
+ return row ? clone(row) : null;
248
+ },
249
+ async listConfigurationRevisions(tenantKey) {
250
+ return [...configs.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
251
+ },
252
+ async saveConfigurationRevision(revision) {
253
+ configs.set(k(revision.tenantKey, revision.id), clone(revision));
254
+ },
255
+ async upsertProviderDefinition(definition) {
256
+ providers.set(k(definition.tenantKey, definition.provider, definition.environment), clone(definition));
257
+ },
258
+ async getProviderDefinition(tenantKey, provider, environment) {
259
+ const row = providers.get(k(tenantKey, provider, environment));
260
+ return row ? clone(row) : null;
261
+ },
262
+ async listProviderDefinitions(tenantKey) {
263
+ return [...providers.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
264
+ },
265
+ async getRoute(tenantKey, routeId) {
266
+ const row = routes.get(k(tenantKey, routeId));
267
+ return row ? clone(row) : null;
268
+ },
269
+ async listRoutes(tenantKey) {
270
+ return [...routes.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
271
+ },
272
+ async listActiveRoutes(tenantKey, environment) {
273
+ return [...routes.values()].filter((row) => row.tenantKey === tenantKey && row.environment === environment && row.lifecycle === "active").map(clone);
274
+ },
275
+ async saveRoute(route) {
276
+ routes.set(k(route.tenantKey, route.id), clone(route));
277
+ },
278
+ async saveRouteChangeRequest(request) {
279
+ routeChanges.set(k(request.tenantKey, request.id), clone(request));
280
+ },
281
+ async getRouteChangeRequest(tenantKey, id) {
282
+ const row = routeChanges.get(k(tenantKey, id));
283
+ return row ? clone(row) : null;
284
+ },
285
+ async listRouteChangeRequests(tenantKey) {
286
+ return [...routeChanges.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
287
+ },
288
+ async getActivePolicy(tenantKey, environment) {
289
+ const match = [...policies.values()].find(
290
+ (row) => row.tenantKey === tenantKey && row.environment === environment && row.lifecycle === "active"
291
+ );
292
+ return match ? clone(match) : null;
293
+ },
294
+ async getPolicyVersion(tenantKey, id) {
295
+ const row = policies.get(k(tenantKey, id));
296
+ return row ? clone(row) : null;
297
+ },
298
+ async listPolicyVersions(tenantKey) {
299
+ return [...policies.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
300
+ },
301
+ async savePolicyVersion(policy) {
302
+ if (policy.lifecycle === "active") {
303
+ for (const [key, row] of policies) {
304
+ if (row.tenantKey === policy.tenantKey && row.environment === policy.environment && row.lifecycle === "active" && row.id !== policy.id) {
305
+ policies.set(key, { ...row, lifecycle: "retired" });
306
+ }
307
+ }
308
+ }
309
+ policies.set(k(policy.tenantKey, policy.id), clone(policy));
310
+ },
311
+ async listProtectedActionRequirements(tenantKey, action, policyVersionId) {
312
+ return [...requirements.values()].filter((row) => row.tenantKey === tenantKey && row.action === action && row.policyVersionId === policyVersionId).map(clone);
313
+ },
314
+ async saveProtectedActionRequirement(requirement) {
315
+ requirements.set(k(requirement.tenantKey, requirement.id), clone(requirement));
316
+ },
317
+ async getContinuationDestinations(tenantKey) {
318
+ const tenant = tenants.get(tenantKey);
319
+ return tenant ? [...tenant.continuationDestinations] : ["verification.resume"];
320
+ },
321
+ async getAttempt(tenantKey, attemptId) {
322
+ const row = attempts.get(k(tenantKey, attemptId));
323
+ return row ? clone(row) : null;
324
+ },
325
+ async getAttemptByIdempotencyKey(tenantKey, key) {
326
+ const match = [...attempts.values()].find((row) => row.tenantKey === tenantKey && row.idempotencyKey === key);
327
+ return match ? clone(match) : null;
328
+ },
329
+ async findAttemptByProviderResource(tenantKey, provider, providerResourceId) {
330
+ const match = [...attempts.values()].find(
331
+ (row) => row.tenantKey === tenantKey && row.provider === provider && row.providerResourceId === providerResourceId
332
+ );
333
+ return match ? clone(match) : null;
334
+ },
335
+ async listAttempts(tenantKey) {
336
+ return [...attempts.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
337
+ },
338
+ async listLiveAttempts(tenantKey, subjectHash, packageCode) {
339
+ const live = /* @__PURE__ */ new Set(["created", "pending_user_input", "paused", "processing", "manual_review_required"]);
340
+ return [...attempts.values()].filter((row) => row.tenantKey === tenantKey && row.subjectHash === subjectHash && row.packageCode === packageCode && live.has(row.canonicalStatus)).map(clone);
341
+ },
342
+ async insertAttempt(attempt) {
343
+ const key = k(attempt.tenantKey, attempt.id);
344
+ if (attempts.has(key)) throw new Error("Attempt already exists.");
345
+ k(attempt.tenantKey, "idem", attempt.idempotencyKey);
346
+ if ([...attempts.values()].some((row) => row.tenantKey === attempt.tenantKey && row.idempotencyKey === attempt.idempotencyKey)) {
347
+ throw new Error("Idempotency key already used.");
348
+ }
349
+ attempts.set(key, clone(attempt));
350
+ return clone(attempt);
351
+ },
352
+ async updateAttempt(attempt) {
353
+ attempts.set(k(attempt.tenantKey, attempt.id), clone(attempt));
354
+ },
355
+ async insertLineage(row) {
356
+ lineage.set(k(row.tenantKey, row.id), clone(row));
357
+ },
358
+ async listLineage(tenantKey, attemptId) {
359
+ return [...lineage.values()].filter((row) => row.tenantKey === tenantKey && row.attemptId === attemptId).map(clone);
360
+ },
361
+ async getValidDecision(tenantKey, subjectHash, packageCode, at) {
362
+ const matches = [...decisions.values()].filter((row) => row.tenantKey === tenantKey && row.subjectHash === subjectHash && row.packageCode === packageCode && row.status === "verified" && !row.revokedAt && (!row.expiresAt || row.expiresAt > at.toISOString())).sort((left, right) => right.effectiveAt.localeCompare(left.effectiveAt));
363
+ return matches[0] ? clone(matches[0]) : null;
364
+ },
365
+ async insertDecision(decision) {
366
+ decisions.set(k(decision.tenantKey, decision.id), clone(decision));
367
+ },
368
+ async listDecisions(tenantKey, subjectHash) {
369
+ return [...decisions.values()].filter((row) => row.tenantKey === tenantKey && (!subjectHash || row.subjectHash === subjectHash)).map(clone);
370
+ },
371
+ async revokeDecision(tenantKey, decisionId, at) {
372
+ const row = decisions.get(k(tenantKey, decisionId));
373
+ if (row) decisions.set(k(tenantKey, decisionId), { ...row, status: "revoked", revokedAt: at });
374
+ },
375
+ async claimIdempotency(claim) {
376
+ return lockFor(k(claim.tenantKey, claim.claimKey)).run(async () => {
377
+ const existing = idempotency.get(k(claim.tenantKey, claim.claimKey));
378
+ if (existing) return { disposition: "existing", claim: clone(existing) };
379
+ idempotency.set(k(claim.tenantKey, claim.claimKey), clone(claim));
380
+ return { disposition: "claimed", claim: clone(claim) };
381
+ });
382
+ },
383
+ async completeIdempotency(tenantKey, key, resultRef) {
384
+ const row = idempotency.get(k(tenantKey, key));
385
+ if (row) {
386
+ idempotency.set(k(tenantKey, key), {
387
+ ...row,
388
+ state: "completed",
389
+ resultRef,
390
+ completedAt: iso()
391
+ });
392
+ }
393
+ },
394
+ async failIdempotency(tenantKey, key, errorCode) {
395
+ const row = idempotency.get(k(tenantKey, key));
396
+ if (row) {
397
+ idempotency.set(k(tenantKey, key), {
398
+ ...row,
399
+ state: "failed",
400
+ errorCode,
401
+ completedAt: iso()
402
+ });
403
+ }
404
+ },
405
+ async getIdempotencyClaim(tenantKey, key) {
406
+ const row = idempotency.get(k(tenantKey, key));
407
+ return row ? clone(row) : null;
408
+ },
409
+ async claimWebhookEvent(input) {
410
+ const key = k(input.tenantKey, input.provider, input.providerEventKey);
411
+ const existing = webhooks.get(key);
412
+ if (existing) {
413
+ if (existing.bodySha256 !== input.bodySha256) {
414
+ const dead = { ...existing, state: "dead_letter" };
415
+ webhooks.set(key, dead);
416
+ return { disposition: "mismatch", event: clone(dead) };
417
+ }
418
+ return { disposition: "duplicate", event: clone(existing) };
419
+ }
420
+ const event = {
421
+ tenantKey: input.tenantKey,
422
+ id: newId("wh"),
423
+ provider: input.provider,
424
+ providerEventKey: input.providerEventKey,
425
+ providerResourceId: input.providerResourceId,
426
+ eventType: input.eventType,
427
+ occurredAt: input.occurredAt,
428
+ bodySha256: input.bodySha256,
429
+ safeMetadata: input.safeMetadata,
430
+ state: "accepted",
431
+ receivedAt: iso()
432
+ };
433
+ webhooks.set(key, event);
434
+ return { disposition: "claimed", event: clone(event) };
435
+ },
436
+ async getWebhookEvent(tenantKey, provider, eventKey) {
437
+ const row = webhooks.get(k(tenantKey, provider, eventKey));
438
+ return row ? clone(row) : null;
439
+ },
440
+ async getWebhookEventById(tenantKey, eventId) {
441
+ const row = [...webhooks.values()].find((event) => event.tenantKey === tenantKey && event.id === eventId);
442
+ return row ? clone(row) : null;
443
+ },
444
+ async settleWebhookEvent(tenantKey, eventId, outcome) {
445
+ for (const [key, row] of webhooks) {
446
+ if (row.tenantKey === tenantKey && row.id === eventId) {
447
+ webhooks.set(key, { ...row, state: outcome });
448
+ }
449
+ }
450
+ },
451
+ async recordHealth(observation) {
452
+ health.set(k(observation.tenantKey, observation.id), clone(observation));
453
+ },
454
+ async listHealth(tenantKey, provider) {
455
+ return [...health.values()].filter((row) => row.tenantKey === tenantKey && (!provider || row.provider === provider)).map(clone);
456
+ },
457
+ async getCircuit(tenantKey, provider, environment) {
458
+ const key = k(tenantKey, provider, environment);
459
+ const existing = circuits.get(key);
460
+ if (existing) return clone(existing);
461
+ const created = {
462
+ tenantKey,
463
+ provider,
464
+ environment,
465
+ state: "closed",
466
+ reasonCode: null,
467
+ openUntil: null,
468
+ consecutiveFailures: 0,
469
+ drainedByActorId: null,
470
+ updatedAt: iso()
471
+ };
472
+ circuits.set(key, created);
473
+ return clone(created);
474
+ },
475
+ async saveCircuit(circuit) {
476
+ circuits.set(k(circuit.tenantKey, circuit.provider, circuit.environment), clone(circuit));
477
+ },
478
+ async listCircuits(tenantKey) {
479
+ return [...circuits.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
480
+ },
481
+ async saveAppeal(appeal) {
482
+ appeals.set(k(appeal.tenantKey, appeal.id), clone(appeal));
483
+ },
484
+ async getAppeal(tenantKey, id) {
485
+ const row = appeals.get(k(tenantKey, id));
486
+ return row ? clone(row) : null;
487
+ },
488
+ async listAppeals(tenantKey) {
489
+ return [...appeals.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
490
+ },
491
+ async saveReviewCase(reviewCase) {
492
+ reviews.set(k(reviewCase.tenantKey, reviewCase.id), clone(reviewCase));
493
+ },
494
+ async getReviewCase(tenantKey, id) {
495
+ const row = reviews.get(k(tenantKey, id));
496
+ return row ? clone(row) : null;
497
+ },
498
+ async listReviewCases(tenantKey) {
499
+ return [...reviews.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
500
+ },
501
+ async saveManualDecisionProposal(proposal) {
502
+ proposals.set(k(proposal.tenantKey, proposal.id), clone(proposal));
503
+ },
504
+ async getManualDecisionProposal(tenantKey, id) {
505
+ const row = proposals.get(k(tenantKey, id));
506
+ return row ? clone(row) : null;
507
+ },
508
+ async listManualDecisionProposals(tenantKey) {
509
+ return [...proposals.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
510
+ },
511
+ async saveContinuation(continuation) {
512
+ continuations.set(k(continuation.tenantKey, continuation.key), clone(continuation));
513
+ },
514
+ async getContinuation(tenantKey, key) {
515
+ const row = continuations.get(k(tenantKey, key));
516
+ return row ? clone(row) : null;
517
+ },
518
+ async appendAudit(event) {
519
+ audit.set(k(event.tenantKey, event.id), clone(event));
520
+ },
521
+ async listAudit(tenantKey) {
522
+ return [...audit.values()].filter((row) => row.tenantKey === tenantKey).map(clone);
523
+ },
524
+ async saveJob(job) {
525
+ jobs.set(k(job.tenantKey, job.id), clone(job));
526
+ },
527
+ async getJob(tenantKey, id) {
528
+ const row = jobs.get(k(tenantKey, id));
529
+ return row ? clone(row) : null;
530
+ },
531
+ async listJobs(tenantKey, kind) {
532
+ return [...jobs.values()].filter((row) => row.tenantKey === tenantKey && (!kind || row.kind === kind)).map(clone);
533
+ },
534
+ async claimJobs(input) {
535
+ const claimed = [];
536
+ const pending = ["scheduled", "retryable", "processing"];
537
+ for (const [key, job] of jobs) {
538
+ if (claimed.length >= input.limit) break;
539
+ if (job.tenantKey !== input.tenantKey || !input.kinds.includes(job.kind)) continue;
540
+ if (!pending.includes(job.state) && job.state !== "processing") continue;
541
+ if (job.state === "processing" && job.leaseExpiresAt && job.leaseExpiresAt > input.now.toISOString()) continue;
542
+ if (job.nextAttemptAt > input.now.toISOString()) continue;
543
+ const next = {
544
+ ...job,
545
+ state: job.kind === "redact" ? "processing" : "processing",
546
+ leaseId: newId("lease"),
547
+ leaseExpiresAt: new Date(input.now.getTime() + input.leaseSeconds * 1e3).toISOString(),
548
+ attemptCount: job.attemptCount + 1
549
+ };
550
+ jobs.set(key, next);
551
+ claimed.push(clone(next));
552
+ }
553
+ return claimed;
554
+ },
555
+ async updateJob(job) {
556
+ jobs.set(k(job.tenantKey, job.id), clone(job));
557
+ },
558
+ async updateRedactionStatus(tenantKey, jobId, status) {
559
+ const row = jobs.get(k(tenantKey, jobId));
560
+ if (row) jobs.set(k(tenantKey, jobId), { ...row, state: status });
561
+ }
562
+ };
563
+ return store;
564
+ }
565
+
566
+ // src/queue.ts
567
+ function backoffSeconds(attemptCount, retryAfterSeconds, random = Math.random) {
568
+ if (retryAfterSeconds && retryAfterSeconds > 0) return retryAfterSeconds;
569
+ const base = Math.min(2 ** Math.max(0, attemptCount), 300);
570
+ const jitter = random() * base * 0.25;
571
+ return Math.max(1, Math.round(base + jitter));
572
+ }
573
+
574
+ // src/memory-queue.ts
575
+ function createMemoryQueue(store, options = {}) {
576
+ const random = options.random ?? Math.random;
577
+ return {
578
+ async enqueue(job) {
579
+ await store.saveJob(job);
580
+ },
581
+ async claim(input) {
582
+ return store.claimJobs(input);
583
+ },
584
+ async complete(tenantKey, jobId, leaseId) {
585
+ const job = await store.getJob(tenantKey, jobId);
586
+ if (!job || job.leaseId !== leaseId) {
587
+ throw new EngineError("OPERATION_PENDING", "The queue lease is no longer valid.");
588
+ }
589
+ await store.updateJob({
590
+ ...job,
591
+ state: job.kind === "redact" ? "redacted" : "completed",
592
+ leaseId: null,
593
+ leaseExpiresAt: null
594
+ });
595
+ },
596
+ async retry(tenantKey, jobId, leaseId, retry) {
597
+ const job = await store.getJob(tenantKey, jobId);
598
+ if (!job || job.leaseId !== leaseId) {
599
+ throw new EngineError("OPERATION_PENDING", "The queue lease is no longer valid.");
600
+ }
601
+ const delay = backoffSeconds(job.attemptCount, retry.retryAfterSeconds, random);
602
+ await store.updateJob({
603
+ ...job,
604
+ state: retry.deadLetter ? "dead_letter" : "retryable",
605
+ leaseId: null,
606
+ leaseExpiresAt: null,
607
+ lastErrorCode: retry.errorCode,
608
+ nextAttemptAt: new Date(store.now().getTime() + delay * 1e3).toISOString()
609
+ });
610
+ }
611
+ };
612
+ }
613
+
614
+ // src/types.ts
615
+ var FORBIDDEN_CLIENT_ROUTE_KEYS = [
616
+ "provider",
617
+ "templateId",
618
+ "template_id",
619
+ "workflowId",
620
+ "workflow_id",
621
+ "apiOrigin",
622
+ "api_origin",
623
+ "configurationRevision",
624
+ "configuration_revision",
625
+ "adapterVersion",
626
+ "manifestDigest",
627
+ "policyVersion",
628
+ "routeId"
629
+ ];
630
+ var LIVE_ATTEMPT_STATUSES = [
631
+ "created",
632
+ "pending_user_input",
633
+ "paused",
634
+ "processing",
635
+ "manual_review_required"
636
+ ];
637
+ var GOVERNANCE_TRANSITIONS = [
638
+ "approve",
639
+ "deny",
640
+ "request_more_information",
641
+ "revoke",
642
+ "expire"
643
+ ];
644
+ var APPLICATION_REASON_CODES = [
645
+ "underage",
646
+ "unsupported_capability",
647
+ "biometric_alternative_requested",
648
+ "manual_review_required",
649
+ "document_unreadable",
650
+ "more_information_requested"
651
+ ];
652
+ function isApplicationReasonCode(value) {
653
+ return APPLICATION_REASON_CODES.includes(value);
654
+ }
655
+
656
+ // src/guards.ts
657
+ function assertNoClientRouting(command) {
658
+ const record = command;
659
+ for (const key of FORBIDDEN_CLIENT_ROUTE_KEYS) {
660
+ if (record[key] !== void 0 && record[key] !== null) {
661
+ throw new ClientRouteInjectionError();
662
+ }
663
+ }
664
+ }
665
+ function assertStartCommand(command) {
666
+ if (!command.packageCode || !command.subjectReference || !command.idempotencyKey) {
667
+ throw new EngineError("INVALID_COMMAND", "A verification start command is missing required fields.");
668
+ }
669
+ if (!isCountryCode(command.countryCode)) {
670
+ throw new EngineError("INVALID_COMMAND", "A verification start command must include an ISO country code.");
671
+ }
672
+ if (metadataContainsForbiddenIdentifier(command.metadata)) {
673
+ throw new EngineError("INVALID_COMMAND", "Attempt metadata must not contain government identifiers.");
674
+ }
675
+ }
676
+ function twoActorApproved(proposedBy, approvedBy) {
677
+ return Boolean(proposedBy && approvedBy && proposedBy !== approvedBy);
678
+ }
679
+
680
+ // src/platform.ts
681
+ init_hash();
682
+
683
+ // src/rate-budget.ts
684
+ function createRateBudget(limitPerSecond) {
685
+ const stamps = /* @__PURE__ */ new Map();
686
+ const limit = Math.max(1, Math.floor(limitPerSecond));
687
+ return {
688
+ consume(provider, at = /* @__PURE__ */ new Date()) {
689
+ const now = at.getTime();
690
+ const recent = (stamps.get(provider) ?? []).filter((stamp) => now - stamp < 1e3);
691
+ if (recent.length >= limit) {
692
+ const oldest = recent[0] ?? now;
693
+ return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil((oldest + 1e3 - now) / 1e3)) };
694
+ }
695
+ recent.push(now);
696
+ stamps.set(provider, recent);
697
+ return { allowed: true };
698
+ }
699
+ };
700
+ }
701
+
702
+ // src/routing.ts
703
+ init_hash();
704
+ async function selectRoute(input) {
705
+ const now = (input.runtime.now ?? (() => /* @__PURE__ */ new Date()))();
706
+ const policy = await input.store.getActivePolicy(input.tenantKey, input.environment);
707
+ if (input.environment === "production") {
708
+ if (!input.runtime.productionEnabled) {
709
+ throw new EngineError("PRODUCTION_NOT_ACTIVATED", "Production verification requires the runtime production key.");
710
+ }
711
+ if (!policy || policy.lifecycle !== "active" || !twoActorApproved(policy.proposedByActorId, policy.approvedByActorId)) {
712
+ throw new EngineError(
713
+ "PRODUCTION_NOT_ACTIVATED",
714
+ "Production verification requires an active database policy approved by a different actor."
715
+ );
716
+ }
717
+ if (policy.expiresAt && policy.expiresAt <= now.toISOString()) {
718
+ throw new EngineError("PRODUCTION_NOT_ACTIVATED", "The production verification policy has expired.");
719
+ }
720
+ if (policy.decisionRetentionDays == null || policy.providerRedactionDelayDays == null || policy.appealHoldDays == null) {
721
+ throw new EngineError(
722
+ "PRODUCTION_NOT_ACTIVATED",
723
+ "Production verification requires explicit decision retention, provider redaction timing, appeal holds, and legal-hold values."
724
+ );
725
+ }
726
+ } else if (!policy || policy.lifecycle !== "active") {
727
+ throw new EngineError("NO_ELIGIBLE_ROUTE", "No active sandbox verification policy is available.");
728
+ }
729
+ const cohort = await cohortBucket(input.tenantKey, input.subjectHash);
730
+ const routes = (await input.store.listActiveRoutes(input.tenantKey, input.environment)).filter((route) => route.packageCode === input.packageCode).filter((route) => route.countryCode === null || route.countryCode === input.countryCode).filter((route) => cohort >= route.cohortMin && cohort <= route.cohortMax).filter((route) => !route.windowStart || route.windowStart <= now.toISOString()).filter((route) => !route.windowEnd || route.windowEnd >= now.toISOString()).filter((route) => !route.allowlistRequired || route.allowlistedSubjectHashes.includes(input.subjectHash)).sort((left, right) => left.priority - right.priority || left.id.localeCompare(right.id));
731
+ let usedFailover = false;
732
+ const eligible = [];
733
+ for (const route of routes) {
734
+ const circuit = await input.store.getCircuit(input.tenantKey, route.provider, route.environment);
735
+ if (circuitIsBlocking(circuit, now)) {
736
+ usedFailover = true;
737
+ continue;
738
+ }
739
+ const adapter2 = input.adapters.find((candidate) => candidate.provider === route.provider && candidate.environment === route.environment);
740
+ if (!adapter2) continue;
741
+ if (!adapter2.manifest.supportedPackages.includes(input.packageCode)) continue;
742
+ if (!adapter2.manifest.supportedCountries.includes(input.countryCode)) continue;
743
+ if (!adapter2.manifest.environments.includes(input.environment)) continue;
744
+ if (input.requiredCapability && !adapter2.manifest.capabilities[input.requiredCapability]) continue;
745
+ const routeCapability = route.requiredCapability;
746
+ if (routeCapability && routeCapability in adapter2.manifest.capabilities && !adapter2.manifest.capabilities[routeCapability]) {
747
+ continue;
748
+ }
749
+ const observations = await input.store.listHealth(input.tenantKey, adapter2.provider);
750
+ const recent = observations.filter((row) => row.environment === input.environment).sort((left, right) => right.observedAt.localeCompare(left.observedAt)).slice(0, 5);
751
+ if (recent.length >= 3 && recent.every((row) => row.outcome !== "success")) {
752
+ usedFailover = true;
753
+ continue;
754
+ }
755
+ eligible.push(route);
756
+ }
757
+ const selected = eligible[0];
758
+ if (!selected) {
759
+ throw new EngineError("NO_ELIGIBLE_ROUTE", "No eligible verification provider route is available.", true, 30);
760
+ }
761
+ const adapter = input.adapters.find((candidate) => candidate.provider === selected.provider && candidate.environment === selected.environment);
762
+ if (!adapter) {
763
+ throw new EngineError("NO_ELIGIBLE_ROUTE", "The selected route has no compiled-in adapter.");
764
+ }
765
+ return {
766
+ route: selected,
767
+ adapter,
768
+ reason: usedFailover ? "new_attempt_failover" : "primary_route",
769
+ usedFailover
770
+ };
771
+ }
772
+ function circuitIsBlocking(circuit, now) {
773
+ if (circuit.state === "closed") return false;
774
+ if (circuit.state === "open") {
775
+ if (circuit.openUntil && circuit.openUntil <= now.toISOString()) return false;
776
+ return true;
777
+ }
778
+ return circuit.state === "half_open" && Boolean(circuit.drainedByActorId);
779
+ }
780
+
781
+ // src/seed.ts
782
+ init_hash();
783
+ async function seedSandboxExamples(store, registry, runtime, tenantKey = "default") {
784
+ const now = (runtime.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
785
+ await store.ensureTenant(tenantKey);
786
+ let sandboxPolicy = await store.getActivePolicy(tenantKey, "sandbox");
787
+ if (!sandboxPolicy) {
788
+ sandboxPolicy = {
789
+ tenantKey,
790
+ id: "pol_sandbox_example",
791
+ version: "sandbox-example-1",
792
+ environment: "sandbox",
793
+ lifecycle: "active",
794
+ reason: "Seeded sandbox example policy",
795
+ expiresAt: null,
796
+ proposedByActorId: "system:seed",
797
+ approvedByActorId: "system:seed-approver",
798
+ approvedAt: now,
799
+ activatedAt: now,
800
+ createdAt: now,
801
+ decisionRetentionDays: null,
802
+ providerRedactionDelayDays: null,
803
+ appealHoldDays: null,
804
+ legalHold: false
805
+ };
806
+ await store.savePolicyVersion(sandboxPolicy);
807
+ }
808
+ let sandboxPriority = 100;
809
+ for (const adapter of registry.list()) {
810
+ const manifestDigest = await digestCanonical(adapter.manifest);
811
+ await store.upsertProviderDefinition({
812
+ tenantKey,
813
+ provider: adapter.provider,
814
+ environment: adapter.environment,
815
+ adapterVersion: adapter.manifest.adapterVersion,
816
+ manifestDigest,
817
+ compiledInRegistry: true,
818
+ productionEligible: false,
819
+ createdAt: now,
820
+ updatedAt: now
821
+ });
822
+ const configId = `cfg_sandbox_${adapter.provider}`;
823
+ if (!await store.getConfigurationRevision(tenantKey, configId)) {
824
+ await store.saveConfigurationRevision({
825
+ tenantKey,
826
+ id: configId,
827
+ provider: adapter.provider,
828
+ environment: adapter.environment,
829
+ revision: 1,
830
+ configurationDigest: await digestCanonical({ provider: adapter.provider, environment: adapter.environment }),
831
+ lifecycle: "approved",
832
+ proposedByActorId: "system:seed",
833
+ approvedByActorId: "system:seed-approver",
834
+ approvedAt: now,
835
+ createdAt: now
836
+ });
837
+ }
838
+ if (adapter.environment !== "sandbox") continue;
839
+ for (const packageCode of adapter.manifest.supportedPackages) {
840
+ const routeId = `rte_sandbox_${adapter.provider}_${packageCode}`.replace(/[^a-z0-9_]/g, "_");
841
+ if (await store.getRoute(tenantKey, routeId)) continue;
842
+ const route = {
843
+ tenantKey,
844
+ id: routeId,
845
+ provider: adapter.provider,
846
+ environment: "sandbox",
847
+ packageCode,
848
+ countryCode: adapter.manifest.supportedCountries[0] ?? "US",
849
+ requiredCapability: null,
850
+ priority: sandboxPriority++,
851
+ cohortMin: 0,
852
+ cohortMax: 99,
853
+ windowStart: null,
854
+ windowEnd: null,
855
+ allowlistRequired: false,
856
+ allowlistedSubjectHashes: [],
857
+ configurationRevisionId: configId,
858
+ policyVersionId: sandboxPolicy.id,
859
+ lifecycle: "active",
860
+ proposedByActorId: "system:seed",
861
+ approvedByActorId: "system:seed-approver",
862
+ approvedAt: now,
863
+ activatedAt: now,
864
+ createdAt: now,
865
+ updatedAt: now
866
+ };
867
+ await store.saveRoute(route);
868
+ }
869
+ }
870
+ const productionPolicy = (await store.listPolicyVersions(tenantKey)).find((row) => row.environment === "production");
871
+ if (!productionPolicy) {
872
+ await store.savePolicyVersion({
873
+ tenantKey,
874
+ id: newId("pol"),
875
+ version: "production-unactivated",
876
+ environment: "production",
877
+ lifecycle: "draft",
878
+ reason: "Seeded production policy is never auto-activated",
879
+ expiresAt: null,
880
+ proposedByActorId: "system:seed",
881
+ approvedByActorId: null,
882
+ approvedAt: null,
883
+ activatedAt: null,
884
+ createdAt: now,
885
+ decisionRetentionDays: null,
886
+ providerRedactionDelayDays: null,
887
+ appealHoldDays: null,
888
+ legalHold: false
889
+ });
890
+ }
891
+ }
892
+
893
+ // src/store.ts
894
+ var TERMINAL_STATUS_RANK = {
895
+ created: 10,
896
+ pending_user_input: 20,
897
+ paused: 20,
898
+ processing: 30,
899
+ manual_review_required: 40,
900
+ provider_unavailable: 25,
901
+ verified: 100,
902
+ declined: 100,
903
+ failed: 100,
904
+ expired: 100,
905
+ canceled: 100,
906
+ redacted: 200
907
+ };
908
+
909
+ // src/status.ts
910
+ function canTransitionStatus(current, next) {
911
+ if (current === next) return true;
912
+ if (next === "redacted") return true;
913
+ if (isTerminalStatus(current)) return false;
914
+ if (current === "provider_unavailable" && !isTerminalStatus(next)) return true;
915
+ return TERMINAL_STATUS_RANK[next] >= TERMINAL_STATUS_RANK[current];
916
+ }
917
+ function applyMonotonicStatus(current, next) {
918
+ return canTransitionStatus(current, next) ? next : current;
919
+ }
920
+ function createVerificationPlatform(input) {
921
+ const runtime = input.runtime ?? {};
922
+ const store = input.store;
923
+ const queue = input.queue;
924
+ const policyStore = input.policyStore ?? store;
925
+ const registry = input.registry;
926
+ const now = () => (runtime.now ?? store.now ?? (() => /* @__PURE__ */ new Date()))();
927
+ const cryptoImpl = runtime.crypto ?? globalThis.crypto;
928
+ const createLocks = /* @__PURE__ */ new Map();
929
+ const rateBudget = createRateBudget(runtime.rateBudgetPerProvider ?? 25);
930
+ let boot = null;
931
+ const ready = () => {
932
+ if (!boot) boot = bootstrap();
933
+ return boot;
934
+ };
935
+ async function bootstrap() {
936
+ if (runtime.seedSandboxExamples !== false) {
937
+ await seedSandboxExamples(store, registry, runtime);
938
+ } else {
939
+ const iso = now().toISOString();
940
+ for (const adapter of registry.list()) {
941
+ await store.upsertProviderDefinition({
942
+ tenantKey: "default",
943
+ provider: adapter.provider,
944
+ environment: adapter.environment,
945
+ adapterVersion: adapter.manifest.adapterVersion,
946
+ manifestDigest: await digestCanonical(adapter.manifest),
947
+ compiledInRegistry: true,
948
+ productionEligible: false,
949
+ createdAt: iso,
950
+ updatedAt: iso
951
+ });
952
+ }
953
+ }
954
+ }
955
+ async function authorize(actor, operation, resource) {
956
+ const allowed = await input.authorize(actor, operation, resource);
957
+ if (!allowed) throw new AuthorizationError();
958
+ }
959
+ async function audit(actor, operation, resourceType, resourceId, reasonCode, safeMetadata = {}) {
960
+ await store.appendAudit({
961
+ tenantKey: actor.tenantKey,
962
+ id: newId("aud", cryptoImpl),
963
+ actorId: actor.actorId,
964
+ actorType: actor.actorType,
965
+ operation,
966
+ resourceType,
967
+ resourceId,
968
+ reasonCode,
969
+ safeMetadata,
970
+ occurredAt: now().toISOString()
971
+ });
972
+ }
973
+ function adapterFor(provider, environment) {
974
+ return registry.get(provider, environment);
975
+ }
976
+ async function recordHealth(adapter, actorTenant, observation) {
977
+ await store.recordHealth({
978
+ tenantKey: actorTenant,
979
+ id: newId("hlth", cryptoImpl),
980
+ provider: adapter.provider,
981
+ environment: adapter.environment,
982
+ operation: observation.operation,
983
+ outcome: observation.outcome,
984
+ safeCode: observation.safeCode,
985
+ observedAt: observation.observedAt,
986
+ latencyMs: observation.latencyMs ?? null
987
+ });
988
+ const circuit = await store.getCircuit(actorTenant, adapter.provider, adapter.environment);
989
+ if (observation.outcome === "success") {
990
+ await store.saveCircuit({
991
+ ...circuit,
992
+ consecutiveFailures: 0,
993
+ state: circuit.drainedByActorId ? circuit.state : "closed",
994
+ reasonCode: circuit.drainedByActorId ? circuit.reasonCode : null,
995
+ openUntil: circuit.drainedByActorId ? circuit.openUntil : null,
996
+ updatedAt: now().toISOString()
997
+ });
998
+ return;
999
+ }
1000
+ if (observation.outcome === "retryable_failure" || observation.outcome === "terminal_failure") {
1001
+ const failures = circuit.consecutiveFailures + 1;
1002
+ const open = failures >= 5;
1003
+ await store.saveCircuit({
1004
+ ...circuit,
1005
+ consecutiveFailures: failures,
1006
+ state: open ? "open" : circuit.state,
1007
+ reasonCode: observation.safeCode,
1008
+ openUntil: open ? new Date(now().getTime() + 3e5).toISOString() : circuit.openUntil,
1009
+ updatedAt: now().toISOString()
1010
+ });
1011
+ }
1012
+ }
1013
+ async function toView(attempt, launch = null) {
1014
+ return {
1015
+ attemptId: attempt.id,
1016
+ packageCode: attempt.packageCode,
1017
+ status: attempt.canonicalStatus,
1018
+ provider: attempt.provider,
1019
+ environment: attempt.environment,
1020
+ adapterVersion: attempt.adapterVersion,
1021
+ manifestDigest: attempt.manifestDigest,
1022
+ configurationRevision: attempt.configurationRevision,
1023
+ policyVersion: attempt.policyVersion,
1024
+ canResume: !isTerminalStatus(attempt.canonicalStatus),
1025
+ canRetry: ["declined", "failed", "expired", "canceled", "provider_unavailable"].includes(attempt.canonicalStatus),
1026
+ expiresAt: attempt.expiresAt,
1027
+ safeErrorCode: attempt.canonicalStatus === "provider_unavailable" ? "provider_unavailable" : null,
1028
+ retryAfter: null,
1029
+ supportPath: null,
1030
+ launch
1031
+ };
1032
+ }
1033
+ async function persistDecision(attempt) {
1034
+ if (attempt.canonicalStatus !== "verified") return;
1035
+ const existing = await store.getValidDecision(attempt.tenantKey, attempt.subjectHash, attempt.packageCode, now());
1036
+ if (existing) return;
1037
+ const ttl = (runtime.defaultDecisionTtlSeconds ?? 365 * 24 * 3600) * 1e3;
1038
+ await store.insertDecision({
1039
+ tenantKey: attempt.tenantKey,
1040
+ id: newId("dec", cryptoImpl),
1041
+ subjectHash: attempt.subjectHash,
1042
+ packageCode: attempt.packageCode,
1043
+ attemptId: attempt.id,
1044
+ status: "verified",
1045
+ source: "provider",
1046
+ policyVersion: attempt.policyVersion,
1047
+ reasonCodes: attempt.normalizedReasonCodes,
1048
+ effectiveAt: now().toISOString(),
1049
+ expiresAt: new Date(now().getTime() + ttl).toISOString(),
1050
+ revokedAt: null,
1051
+ proposerActorId: null,
1052
+ approverActorId: null,
1053
+ createdAt: now().toISOString()
1054
+ });
1055
+ }
1056
+ async function bindProviderResult(attempt, result) {
1057
+ const next = {
1058
+ ...attempt,
1059
+ providerResourceId: result.providerResourceId,
1060
+ providerStatus: result.providerStatus,
1061
+ canonicalStatus: applyMonotonicStatus(attempt.canonicalStatus, result.canonicalStatus),
1062
+ statusVersion: attempt.statusVersion + 1,
1063
+ createClaimId: null,
1064
+ createClaimExpiresAt: null,
1065
+ updatedAt: now().toISOString()
1066
+ };
1067
+ await store.updateAttempt(next);
1068
+ await store.insertLineage({
1069
+ tenantKey: attempt.tenantKey,
1070
+ id: newId("lin", cryptoImpl),
1071
+ attemptId: attempt.id,
1072
+ resourceType: "primary",
1073
+ providerResourceId: result.providerResourceId,
1074
+ relationshipCode: "primary",
1075
+ providerStatus: result.providerStatus,
1076
+ occurredAt: now().toISOString()
1077
+ });
1078
+ for (const linked of result.linkedResources ?? []) {
1079
+ await store.insertLineage({
1080
+ tenantKey: attempt.tenantKey,
1081
+ id: newId("lin", cryptoImpl),
1082
+ attemptId: attempt.id,
1083
+ resourceType: linked.resourceType,
1084
+ providerResourceId: linked.resourceId,
1085
+ relationshipCode: linked.relationshipCode,
1086
+ providerStatus: linked.providerStatus,
1087
+ occurredAt: linked.occurredAt
1088
+ });
1089
+ }
1090
+ await persistDecision(next);
1091
+ return next;
1092
+ }
1093
+ function launchCommand(attempt, command) {
1094
+ return {
1095
+ attemptId: attempt.id,
1096
+ subjectReference: command.subjectReference,
1097
+ organizationReference: command.organization?.legalName ?? null,
1098
+ packageCode: attempt.packageCode,
1099
+ countryCode: attempt.countryCode,
1100
+ idempotencyKey: attempt.idempotencyKey,
1101
+ configurationRevision: attempt.configurationRevision,
1102
+ legalFirstName: command.legalFirstName,
1103
+ legalLastName: command.legalLastName,
1104
+ email: command.email,
1105
+ organization: command.organization,
1106
+ relationship: command.relationship,
1107
+ associatedPerson: command.associatedPerson,
1108
+ evidenceReferences: command.evidenceReferences,
1109
+ requestOrigin: command.requestOrigin,
1110
+ metadata: command.metadata
1111
+ };
1112
+ }
1113
+ async function createProviderResource(actor, attempt, command, adapter) {
1114
+ const budget = rateBudget.consume(adapter.provider, now());
1115
+ if (!budget.allowed) {
1116
+ throw new EngineError(
1117
+ "PROVIDER_UNAVAILABLE",
1118
+ "The provider rate budget is exhausted.",
1119
+ true,
1120
+ budget.retryAfterSeconds
1121
+ );
1122
+ }
1123
+ const started = now();
1124
+ try {
1125
+ const created = await adapter.createAttempt(launchCommand(attempt, command));
1126
+ await recordHealth(adapter, actor.tenantKey, {
1127
+ operation: "create",
1128
+ outcome: "success",
1129
+ safeCode: "ok",
1130
+ observedAt: now().toISOString(),
1131
+ latencyMs: now().getTime() - started.getTime()
1132
+ });
1133
+ return created;
1134
+ } catch (error) {
1135
+ const safe = toSafeProviderFailure(error);
1136
+ await recordHealth(adapter, actor.tenantKey, {
1137
+ operation: "create",
1138
+ outcome: safe.retryable ? "retryable_failure" : "terminal_failure",
1139
+ safeCode: safe.safeCode,
1140
+ observedAt: now().toISOString(),
1141
+ latencyMs: now().getTime() - started.getTime()
1142
+ });
1143
+ throw error;
1144
+ }
1145
+ }
1146
+ async function startLocked(actor, command) {
1147
+ assertNoClientRouting(command);
1148
+ assertStartCommand(command);
1149
+ await authorize(actor, "start", { type: "package", id: command.packageCode });
1150
+ const subjectHash = await store.hashSubject(actor.tenantKey, command.subjectReference);
1151
+ const reused = await store.getValidDecision(actor.tenantKey, subjectHash, command.packageCode, now());
1152
+ if (reused?.attemptId) {
1153
+ const prior = await store.getAttempt(actor.tenantKey, reused.attemptId);
1154
+ if (prior) {
1155
+ await audit(actor, "start", "decision", reused.id, "reused_verified_decision", { attemptId: prior.id });
1156
+ return toView(prior);
1157
+ }
1158
+ }
1159
+ const resourceHash = command.resourceReference && command.resourceType ? await store.hashResource(actor.tenantKey, command.resourceType, command.resourceReference) : null;
1160
+ const existing = await store.getAttemptByIdempotencyKey(actor.tenantKey, command.idempotencyKey);
1161
+ if (existing?.providerResourceId) {
1162
+ return toView(existing);
1163
+ }
1164
+ if (existing && existing.createClaimExpiresAt && existing.createClaimExpiresAt > now().toISOString()) {
1165
+ throw new ProviderOperationPendingError();
1166
+ }
1167
+ const productionPolicy = await store.getActivePolicy(actor.tenantKey, "production");
1168
+ const environment = runtime.productionEnabled && productionPolicy?.lifecycle === "active" && twoActorApproved(productionPolicy.proposedByActorId, productionPolicy.approvedByActorId) ? "production" : "sandbox";
1169
+ const selected = await selectRoute({
1170
+ store,
1171
+ adapters: registry.list(),
1172
+ tenantKey: actor.tenantKey,
1173
+ packageCode: command.packageCode,
1174
+ countryCode: command.countryCode,
1175
+ subjectHash,
1176
+ environment: environment === "production" ? "production" : "sandbox",
1177
+ runtime
1178
+ });
1179
+ const adapter = selected.adapter;
1180
+ const manifestDigest = await digestCanonical(adapter.manifest);
1181
+ const iso = now().toISOString();
1182
+ const attemptId = newId("att", cryptoImpl);
1183
+ const claimKey = `start:${command.idempotencyKey}`;
1184
+ const prepared = await store.transact(async (tx) => {
1185
+ const claim = await tx.claimIdempotency({
1186
+ tenantKey: actor.tenantKey,
1187
+ claimKey,
1188
+ operation: "start",
1189
+ attemptId,
1190
+ state: "claimed",
1191
+ resultRef: null,
1192
+ errorCode: null,
1193
+ createdAt: iso,
1194
+ completedAt: null
1195
+ });
1196
+ if (claim.disposition === "existing" && claim.claim.resultRef) {
1197
+ const prior = await tx.getAttempt(actor.tenantKey, claim.claim.resultRef);
1198
+ if (prior) return { kind: "existing", attempt: prior };
1199
+ }
1200
+ if (claim.disposition === "existing" && claim.claim.state === "claimed") {
1201
+ throw new ProviderOperationPendingError();
1202
+ }
1203
+ const attempt = {
1204
+ tenantKey: actor.tenantKey,
1205
+ id: attemptId,
1206
+ subjectHash,
1207
+ packageCode: command.packageCode,
1208
+ countryCode: command.countryCode,
1209
+ provider: adapter.provider,
1210
+ environment: adapter.environment,
1211
+ adapterVersion: adapter.manifest.adapterVersion,
1212
+ manifestDigest,
1213
+ configurationRevision: selected.route.configurationRevisionId,
1214
+ policyVersion: selected.route.policyVersionId,
1215
+ providerResourceId: null,
1216
+ providerStatus: null,
1217
+ canonicalStatus: "created",
1218
+ statusVersion: 0,
1219
+ idempotencyKey: command.idempotencyKey,
1220
+ parentAttemptId: null,
1221
+ purposeAction: command.action ?? null,
1222
+ purposeResourceHash: resourceHash,
1223
+ routeId: selected.route.id,
1224
+ selectionReason: selected.reason,
1225
+ normalizedReasonCodes: [],
1226
+ expiresAt: null,
1227
+ createClaimId: newId("claim", cryptoImpl),
1228
+ createClaimExpiresAt: new Date(now().getTime() + 3e4).toISOString(),
1229
+ createdAt: iso,
1230
+ updatedAt: iso
1231
+ };
1232
+ await tx.insertAttempt(attempt);
1233
+ return { kind: "created", attempt };
1234
+ });
1235
+ if (prepared.kind === "existing") return toView(prepared.attempt);
1236
+ try {
1237
+ const created = await createProviderResource(actor, prepared.attempt, command, adapter);
1238
+ const bound = await store.transact(async (tx) => {
1239
+ const latest = await tx.getAttempt(actor.tenantKey, prepared.attempt.id);
1240
+ if (!latest) throw new EngineError("ATTEMPT_NOT_FOUND", "The verification attempt was lost after provider create.");
1241
+ const next = await bindProviderResult(latest, created);
1242
+ await tx.completeIdempotency(actor.tenantKey, claimKey, next.id);
1243
+ return next;
1244
+ });
1245
+ await audit(actor, "start", "attempt", bound.id, selected.reason, { provider: adapter.provider });
1246
+ return toView(bound, stripPersistedLaunch(created.launch));
1247
+ } catch (error) {
1248
+ await store.failIdempotency(actor.tenantKey, claimKey, toSafeProviderFailure(error).safeCode);
1249
+ await store.updateAttempt({
1250
+ ...prepared.attempt,
1251
+ canonicalStatus: "provider_unavailable",
1252
+ createClaimId: null,
1253
+ createClaimExpiresAt: null,
1254
+ updatedAt: now().toISOString()
1255
+ });
1256
+ throw error;
1257
+ }
1258
+ }
1259
+ async function start(actor, command) {
1260
+ await ready();
1261
+ const lockKey = `${actor.tenantKey}:${command.idempotencyKey}`;
1262
+ const pending = createLocks.get(lockKey);
1263
+ const run = (pending ?? Promise.resolve()).then(() => startLocked(actor, command), () => startLocked(actor, command));
1264
+ createLocks.set(lockKey, run.then(() => void 0, () => void 0));
1265
+ return run;
1266
+ }
1267
+ async function loadAttempt(actor, attemptId) {
1268
+ const attempt = await store.getAttempt(actor.tenantKey, attemptId);
1269
+ if (!attempt) throw new EngineError("ATTEMPT_NOT_FOUND", "The verification attempt was not found.");
1270
+ return attempt;
1271
+ }
1272
+ async function resume(actor, command) {
1273
+ await ready();
1274
+ assertNoClientRouting(command);
1275
+ await authorize(actor, "resume", { type: "attempt", id: command.attemptId });
1276
+ const attempt = await loadAttempt(actor, command.attemptId);
1277
+ if (isTerminalStatus(attempt.canonicalStatus)) {
1278
+ throw new EngineError("ATTEMPT_TERMINAL", "A terminal verification attempt cannot be resumed.");
1279
+ }
1280
+ if (!attempt.providerResourceId) {
1281
+ throw new EngineError("ATTEMPT_PINNED", "The pinned verification attempt is not yet bound to a provider resource.");
1282
+ }
1283
+ const adapter = adapterFor(attempt.provider, attempt.environment);
1284
+ const launch = await adapter.resumeAttempt({
1285
+ attemptId: attempt.id,
1286
+ providerResourceId: attempt.providerResourceId,
1287
+ configurationRevision: attempt.configurationRevision,
1288
+ requestOrigin: command.requestOrigin
1289
+ });
1290
+ await audit(actor, "resume", "attempt", attempt.id, "resumed_pinned", { provider: attempt.provider });
1291
+ return toView(attempt, stripPersistedLaunch(launch));
1292
+ }
1293
+ async function status(actor, attemptId) {
1294
+ await ready();
1295
+ await authorize(actor, "status", { type: "attempt", id: attemptId });
1296
+ return toView(await loadAttempt(actor, attemptId), null);
1297
+ }
1298
+ async function retry(actor, command) {
1299
+ await ready();
1300
+ assertNoClientRouting(command);
1301
+ await authorize(actor, "retry", { type: "attempt", id: command.parentAttemptId });
1302
+ const parent = await loadAttempt(actor, command.parentAttemptId);
1303
+ const subjectHash = await store.hashSubject(actor.tenantKey, command.subjectReference);
1304
+ const selected = await selectRoute({
1305
+ store,
1306
+ adapters: registry.list(),
1307
+ tenantKey: actor.tenantKey,
1308
+ packageCode: command.packageCode,
1309
+ countryCode: command.countryCode,
1310
+ subjectHash,
1311
+ environment: parent.environment,
1312
+ runtime,
1313
+ requiredCapability: "canRetry"
1314
+ });
1315
+ const child = await startLocked(actor, command);
1316
+ const latest = await store.getAttempt(actor.tenantKey, child.attemptId);
1317
+ if (latest) {
1318
+ await store.updateAttempt({
1319
+ ...latest,
1320
+ parentAttemptId: parent.id,
1321
+ selectionReason: selected.reason,
1322
+ updatedAt: now().toISOString()
1323
+ });
1324
+ }
1325
+ return child;
1326
+ }
1327
+ async function pause(actor, attemptId) {
1328
+ await ready();
1329
+ await authorize(actor, "pause", { type: "attempt", id: attemptId });
1330
+ const attempt = await loadAttempt(actor, attemptId);
1331
+ if (isTerminalStatus(attempt.canonicalStatus)) {
1332
+ throw new EngineError("ATTEMPT_TERMINAL", "A terminal verification attempt cannot be paused.");
1333
+ }
1334
+ const next = { ...attempt, canonicalStatus: applyMonotonicStatus(attempt.canonicalStatus, "paused"), updatedAt: now().toISOString() };
1335
+ await store.updateAttempt(next);
1336
+ await audit(actor, "pause", "attempt", attemptId, "paused");
1337
+ return toView(next);
1338
+ }
1339
+ async function cancel(actor, attemptId) {
1340
+ await ready();
1341
+ await authorize(actor, "cancel", { type: "attempt", id: attemptId });
1342
+ const attempt = await loadAttempt(actor, attemptId);
1343
+ if (isTerminalStatus(attempt.canonicalStatus) && attempt.canonicalStatus !== "canceled") {
1344
+ throw new EngineError("ATTEMPT_TERMINAL", "A terminal verification attempt cannot be canceled.");
1345
+ }
1346
+ if (attempt.providerResourceId) {
1347
+ const adapter = adapterFor(attempt.provider, attempt.environment);
1348
+ if (adapter.manifest.capabilities.canCancel) {
1349
+ await adapter.cancelAttempt({
1350
+ attemptId: attempt.id,
1351
+ providerResourceId: attempt.providerResourceId,
1352
+ configurationRevision: attempt.configurationRevision
1353
+ });
1354
+ }
1355
+ }
1356
+ const next = {
1357
+ ...attempt,
1358
+ canonicalStatus: applyMonotonicStatus(attempt.canonicalStatus, "canceled"),
1359
+ updatedAt: now().toISOString()
1360
+ };
1361
+ await store.updateAttempt(next);
1362
+ await audit(actor, "cancel", "attempt", attemptId, "canceled");
1363
+ return toView(next);
1364
+ }
1365
+ async function redact(actor, command) {
1366
+ await ready();
1367
+ await authorize(actor, "redact", { type: "subject" });
1368
+ const subjectHash = await store.hashSubject(actor.tenantKey, command.subjectReference);
1369
+ const job = {
1370
+ tenantKey: actor.tenantKey,
1371
+ id: newId("red", cryptoImpl),
1372
+ kind: "redact",
1373
+ attemptId: command.attemptId ?? null,
1374
+ eventId: null,
1375
+ subjectHash,
1376
+ providerResourceId: null,
1377
+ state: "scheduled",
1378
+ leaseId: null,
1379
+ leaseExpiresAt: null,
1380
+ attemptCount: 0,
1381
+ nextAttemptAt: now().toISOString(),
1382
+ lastErrorCode: null,
1383
+ createdAt: now().toISOString()
1384
+ };
1385
+ await queue.enqueue(job);
1386
+ const targets = command.attemptId ? [await loadAttempt(actor, command.attemptId)] : (await store.listAttempts(actor.tenantKey)).filter((row) => row.subjectHash === subjectHash);
1387
+ if (!targets.length) {
1388
+ await store.updateRedactionStatus(actor.tenantKey, job.id, "not_applicable");
1389
+ return { jobId: job.id, status: "not_applicable" };
1390
+ }
1391
+ let status2 = "scheduled";
1392
+ for (const attempt of targets) {
1393
+ const adapter = adapterFor(attempt.provider, attempt.environment);
1394
+ const result = await adapter.redactSubject({
1395
+ subjectReference: command.subjectReference,
1396
+ providerResourceId: attempt.providerResourceId,
1397
+ requestReference: command.requestReference ?? job.id
1398
+ });
1399
+ status2 = result.disposition ?? (result.completed ? "redacted" : result.retryable ? "retryable" : "dead_letter");
1400
+ await store.updateRedactionStatus(actor.tenantKey, job.id, toRedactionStatus(status2));
1401
+ if (result.completed) {
1402
+ await store.updateAttempt({
1403
+ ...attempt,
1404
+ canonicalStatus: "redacted",
1405
+ updatedAt: now().toISOString()
1406
+ });
1407
+ }
1408
+ await audit(actor, "redact", "attempt", attempt.id, status2);
1409
+ }
1410
+ return { jobId: job.id, status: status2 };
1411
+ }
1412
+ async function ingestWebhook(command) {
1413
+ await ready();
1414
+ const systemActor = {
1415
+ tenantKey: command.tenantKey,
1416
+ actorId: "system:webhook",
1417
+ actorType: "system",
1418
+ roles: ["webhook"],
1419
+ authorizedSubjectScope: ["*"]
1420
+ };
1421
+ await authorize(systemActor, "ingest_webhook", { type: "provider", id: command.provider });
1422
+ const adapter = registry.get(command.provider);
1423
+ let verified;
1424
+ try {
1425
+ verified = await adapter.verifyWebhook(command.request);
1426
+ } catch (error) {
1427
+ if (error instanceof ProviderError && error.code === "SIGNATURE_INVALID") {
1428
+ throw new EngineError("WEBHOOK_UNAUTHENTICATED", "The webhook signature is invalid.");
1429
+ }
1430
+ throw error;
1431
+ }
1432
+ const normalized = await adapter.normalizeWebhook(verified);
1433
+ const claim = await store.claimWebhookEvent({
1434
+ tenantKey: command.tenantKey,
1435
+ provider: command.provider,
1436
+ providerEventKey: normalized.providerEventKey,
1437
+ providerResourceId: normalized.providerResourceId,
1438
+ eventType: normalized.eventType,
1439
+ occurredAt: normalized.occurredAt,
1440
+ bodySha256: verified.bodySha256,
1441
+ safeMetadata: {
1442
+ ...normalized.safeMetadata,
1443
+ ...normalized.canonicalStatus ? { canonicalStatus: normalized.canonicalStatus } : {}
1444
+ }
1445
+ });
1446
+ if (claim.disposition === "mismatch") {
1447
+ throw new WebhookSecurityIncidentError();
1448
+ }
1449
+ if (claim.disposition === "claimed") {
1450
+ await queue.enqueue({
1451
+ tenantKey: command.tenantKey,
1452
+ id: newId("job", cryptoImpl),
1453
+ kind: "webhook",
1454
+ attemptId: null,
1455
+ eventId: claim.event.id,
1456
+ subjectHash: null,
1457
+ providerResourceId: normalized.providerResourceId,
1458
+ state: "scheduled",
1459
+ leaseId: null,
1460
+ leaseExpiresAt: null,
1461
+ attemptCount: 0,
1462
+ nextAttemptAt: now().toISOString(),
1463
+ lastErrorCode: null,
1464
+ createdAt: now().toISOString()
1465
+ });
1466
+ }
1467
+ await audit(systemActor, "ingest_webhook", "webhook_event", claim.event.id, claim.disposition, {
1468
+ provider: command.provider
1469
+ });
1470
+ return { accepted: true, duplicate: claim.disposition === "duplicate", eventId: claim.event.id };
1471
+ }
1472
+ async function processWebhookJob(tenantKey, job) {
1473
+ await ready();
1474
+ if (!job.eventId) return;
1475
+ const event = await store.getWebhookEventById(tenantKey, job.eventId);
1476
+ const resourceId = event?.providerResourceId ?? job.providerResourceId;
1477
+ let matched = resourceId ? await store.findAttemptByProviderResource(tenantKey, event?.provider ?? "test_fake", resourceId) : null;
1478
+ if (!matched && resourceId) {
1479
+ for (const adapter of registry.list()) {
1480
+ matched = await store.findAttemptByProviderResource(tenantKey, adapter.provider, resourceId);
1481
+ if (matched) break;
1482
+ }
1483
+ }
1484
+ if (!matched || !event) {
1485
+ if (job.leaseId) {
1486
+ await queue.retry(tenantKey, job.id, job.leaseId, {
1487
+ errorCode: "ATTEMPT_NOT_BOUND",
1488
+ retryAfterSeconds: backoffSeconds(job.attemptCount, void 0, runtime.random)
1489
+ });
1490
+ }
1491
+ return;
1492
+ }
1493
+ const canonicalFromEvent = event.safeMetadata.canonicalStatus;
1494
+ const canonical = typeof canonicalFromEvent === "string" ? canonicalFromEvent : matched.canonicalStatus;
1495
+ const updated = {
1496
+ ...matched,
1497
+ canonicalStatus: applyMonotonicStatus(matched.canonicalStatus, canonical),
1498
+ providerStatus: typeof event.safeMetadata.providerStatus === "string" ? event.safeMetadata.providerStatus : matched.providerStatus,
1499
+ statusVersion: matched.statusVersion + 1,
1500
+ updatedAt: now().toISOString()
1501
+ };
1502
+ await store.updateAttempt(updated);
1503
+ await persistDecision(updated);
1504
+ await store.settleWebhookEvent(tenantKey, event.id, "completed");
1505
+ if (job.leaseId) await queue.complete(tenantKey, job.id, job.leaseId);
1506
+ }
1507
+ async function reconcile(actor, attemptId) {
1508
+ await ready();
1509
+ await authorize(actor, "reconcile", { type: "attempt", id: attemptId });
1510
+ const targets = attemptId ? [await loadAttempt(actor, attemptId)] : (await store.listAttempts(actor.tenantKey)).filter((row) => !isTerminalStatus(row.canonicalStatus) && row.providerResourceId);
1511
+ let reconciled = 0;
1512
+ for (const attempt of targets) {
1513
+ if (!attempt.providerResourceId) continue;
1514
+ const adapter = adapterFor(attempt.provider, attempt.environment);
1515
+ const snapshot = await adapter.retrieveAttempt({
1516
+ attemptId: attempt.id,
1517
+ providerResourceId: attempt.providerResourceId,
1518
+ configurationRevision: attempt.configurationRevision
1519
+ });
1520
+ const next = {
1521
+ ...attempt,
1522
+ canonicalStatus: applyMonotonicStatus(attempt.canonicalStatus, snapshot.canonicalStatus),
1523
+ providerStatus: snapshot.providerStatus,
1524
+ normalizedReasonCodes: snapshot.normalizedReasonCodes,
1525
+ statusVersion: attempt.statusVersion + 1,
1526
+ updatedAt: now().toISOString()
1527
+ };
1528
+ await store.updateAttempt(next);
1529
+ await persistDecision(next);
1530
+ reconciled += 1;
1531
+ }
1532
+ await audit(actor, "reconcile", "attempt", attemptId ?? null, "reconciled", { count: reconciled });
1533
+ return { reconciled };
1534
+ }
1535
+ async function scheduleReconciliation(actor) {
1536
+ await ready();
1537
+ await authorize(actor, "reconcile", { type: "tenant" });
1538
+ const live = (await store.listAttempts(actor.tenantKey)).filter((row) => ["processing", "manual_review_required"].includes(row.canonicalStatus) && row.providerResourceId);
1539
+ let enqueued = 0;
1540
+ for (const attempt of live) {
1541
+ await queue.enqueue({
1542
+ tenantKey: actor.tenantKey,
1543
+ id: newId("job", cryptoImpl),
1544
+ kind: "reconcile",
1545
+ attemptId: attempt.id,
1546
+ eventId: null,
1547
+ subjectHash: attempt.subjectHash,
1548
+ providerResourceId: attempt.providerResourceId,
1549
+ state: "scheduled",
1550
+ leaseId: null,
1551
+ leaseExpiresAt: null,
1552
+ attemptCount: 0,
1553
+ nextAttemptAt: now().toISOString(),
1554
+ lastErrorCode: null,
1555
+ createdAt: now().toISOString()
1556
+ });
1557
+ enqueued += 1;
1558
+ }
1559
+ await audit(actor, "reconcile.schedule", "tenant", actor.tenantKey, "scheduled", { count: enqueued });
1560
+ return { enqueued };
1561
+ }
1562
+ async function claimWorkerJobs(actor, input2 = {}) {
1563
+ await ready();
1564
+ await authorize(actor, "reconcile", { type: "tenant" });
1565
+ const limit = Math.min(input2.limit ?? runtime.workerConcurrency ?? 8, 32);
1566
+ return queue.claim({
1567
+ tenantKey: actor.tenantKey,
1568
+ kinds: input2.kinds ?? ["webhook", "reconcile", "redact"],
1569
+ workerId: input2.workerId ?? actor.actorId,
1570
+ leaseSeconds: runtime.webhookLeaseSeconds ?? 30,
1571
+ limit,
1572
+ now: now()
1573
+ });
1574
+ }
1575
+ async function processWorkerJob(actor, jobOrRef) {
1576
+ await ready();
1577
+ const job = "kind" in jobOrRef ? jobOrRef : await store.getJob(actor.tenantKey, jobOrRef.id);
1578
+ if (!job) throw new EngineError("ATTEMPT_NOT_FOUND", "The worker job was not found.");
1579
+ await authorize(actor, "process_webhook", { type: "job", id: job.id });
1580
+ try {
1581
+ if (job.kind === "webhook") await processWebhookJob(actor.tenantKey, job);
1582
+ else if (job.kind === "reconcile") await reconcile(actor, job.attemptId ?? void 0);
1583
+ else if (job.kind === "redact" && job.attemptId) {
1584
+ const attempt = await store.getAttempt(actor.tenantKey, job.attemptId);
1585
+ if (attempt) {
1586
+ const adapter = adapterFor(attempt.provider, attempt.environment);
1587
+ const result = await adapter.redactSubject({
1588
+ subjectReference: attempt.subjectHash,
1589
+ providerResourceId: attempt.providerResourceId,
1590
+ requestReference: job.id
1591
+ });
1592
+ const status2 = result.disposition ?? (result.completed ? "redacted" : "retryable");
1593
+ await store.updateRedactionStatus(actor.tenantKey, job.id, toRedactionStatus(status2));
1594
+ if (result.completed) {
1595
+ await store.updateAttempt({ ...attempt, canonicalStatus: "redacted", updatedAt: now().toISOString() });
1596
+ }
1597
+ }
1598
+ }
1599
+ if (job.leaseId && job.kind !== "webhook") await queue.complete(actor.tenantKey, job.id, job.leaseId);
1600
+ return { processed: true, disposition: "completed" };
1601
+ } catch (error) {
1602
+ const safe = toSafeProviderFailure(error);
1603
+ if (job.leaseId) {
1604
+ const dead = job.attemptCount >= (runtime.maxWorkerAttempts ?? 8);
1605
+ await queue.retry(actor.tenantKey, job.id, job.leaseId, {
1606
+ errorCode: safe.safeCode,
1607
+ retryAfterSeconds: backoffSeconds(job.attemptCount, safe.retryAfterSeconds, runtime.random),
1608
+ deadLetter: dead
1609
+ });
1610
+ return { processed: true, disposition: dead ? "dead_letter" : "retryable" };
1611
+ }
1612
+ throw error;
1613
+ }
1614
+ }
1615
+ async function submitAppeal(actor, command) {
1616
+ await ready();
1617
+ await authorize(actor, "appeal", { type: "attempt", id: command.attemptId });
1618
+ const attempt = await loadAttempt(actor, command.attemptId);
1619
+ const appeal = {
1620
+ tenantKey: actor.tenantKey,
1621
+ id: newId("apl", cryptoImpl),
1622
+ attemptId: attempt.id,
1623
+ subjectHash: attempt.subjectHash,
1624
+ status: "open",
1625
+ reason: command.reason,
1626
+ policyVersion: attempt.policyVersion,
1627
+ proposedByActorId: actor.actorId,
1628
+ decidedByActorId: null,
1629
+ expiresAt: command.expiresAt ?? null,
1630
+ createdAt: now().toISOString(),
1631
+ updatedAt: now().toISOString()
1632
+ };
1633
+ await store.saveAppeal(appeal);
1634
+ if (isApplicationReasonCode(command.reason) && !attempt.normalizedReasonCodes.includes(command.reason)) {
1635
+ await store.updateAttempt({
1636
+ ...attempt,
1637
+ normalizedReasonCodes: [...attempt.normalizedReasonCodes, command.reason],
1638
+ updatedAt: now().toISOString()
1639
+ });
1640
+ }
1641
+ await audit(actor, "appeal.submit", "appeal", appeal.id, "open");
1642
+ return { appealId: appeal.id };
1643
+ }
1644
+ function applyGovernanceStatus(current, transition) {
1645
+ const map = {
1646
+ approve: "approved",
1647
+ deny: "denied",
1648
+ request_more_information: "more_information_requested",
1649
+ revoke: "revoked",
1650
+ expire: "expired"
1651
+ };
1652
+ return map[transition] ?? current;
1653
+ }
1654
+ async function transitionAppeal(actor, command) {
1655
+ await ready();
1656
+ await authorize(actor, "appeal", { type: "appeal", id: command.appealId });
1657
+ const appeal = await store.getAppeal(actor.tenantKey, command.appealId);
1658
+ if (!appeal) throw new EngineError("ATTEMPT_NOT_FOUND", "The appeal was not found.");
1659
+ if (command.transition === "approve" || command.transition === "deny" || command.transition === "revoke") {
1660
+ if (appeal.proposedByActorId === actor.actorId) {
1661
+ throw new EngineError("GOVERNANCE_TWO_ACTOR", "The proposing actor cannot approve or deny their own appeal.");
1662
+ }
1663
+ }
1664
+ const status2 = applyGovernanceStatus(appeal.status, command.transition);
1665
+ await store.saveAppeal({
1666
+ ...appeal,
1667
+ status: status2,
1668
+ reason: command.reason,
1669
+ decidedByActorId: actor.actorId,
1670
+ updatedAt: now().toISOString()
1671
+ });
1672
+ await audit(actor, "appeal.transition", "appeal", appeal.id, status2);
1673
+ return { status: status2 };
1674
+ }
1675
+ async function proposeReview(actor, command) {
1676
+ await ready();
1677
+ await authorize(actor, "review", { type: "attempt", id: command.attemptId });
1678
+ const attempt = await loadAttempt(actor, command.attemptId);
1679
+ const reviewCase = {
1680
+ tenantKey: actor.tenantKey,
1681
+ id: newId("rev", cryptoImpl),
1682
+ attemptId: attempt.id,
1683
+ subjectHash: attempt.subjectHash,
1684
+ status: "in_review",
1685
+ reason: command.reason,
1686
+ policyVersion: attempt.policyVersion,
1687
+ assignedActorId: actor.actorId,
1688
+ createdAt: now().toISOString(),
1689
+ updatedAt: now().toISOString()
1690
+ };
1691
+ await store.saveReviewCase(reviewCase);
1692
+ if (isApplicationReasonCode(command.reason) && !attempt.normalizedReasonCodes.includes(command.reason)) {
1693
+ await store.updateAttempt({
1694
+ ...attempt,
1695
+ normalizedReasonCodes: [...attempt.normalizedReasonCodes, command.reason],
1696
+ updatedAt: now().toISOString()
1697
+ });
1698
+ }
1699
+ const proposal = {
1700
+ tenantKey: actor.tenantKey,
1701
+ id: newId("prp", cryptoImpl),
1702
+ reviewCaseId: reviewCase.id,
1703
+ attemptId: attempt.id,
1704
+ proposedStatus: command.proposedStatus,
1705
+ reason: command.reason,
1706
+ policyVersion: attempt.policyVersion,
1707
+ expiresAt: command.expiresAt ?? null,
1708
+ proposedByActorId: actor.actorId,
1709
+ approvedByActorId: null,
1710
+ status: "proposed",
1711
+ createdAt: now().toISOString()
1712
+ };
1713
+ await store.saveManualDecisionProposal(proposal);
1714
+ await audit(actor, "review.propose", "manual_decision_proposal", proposal.id, "proposed");
1715
+ return { proposalId: proposal.id, reviewCaseId: reviewCase.id };
1716
+ }
1717
+ async function decideReview(actor, command) {
1718
+ await ready();
1719
+ await authorize(actor, "review", { type: "proposal", id: command.proposalId });
1720
+ const proposal = await store.getManualDecisionProposal(actor.tenantKey, command.proposalId);
1721
+ if (!proposal) throw new EngineError("ATTEMPT_NOT_FOUND", "The manual decision proposal was not found.");
1722
+ if (proposal.proposedByActorId === actor.actorId) {
1723
+ throw new EngineError("GOVERNANCE_TWO_ACTOR", "The proposer cannot approve their own manual decision.");
1724
+ }
1725
+ if (command.transition === "deny") {
1726
+ await store.saveManualDecisionProposal({ ...proposal, status: "rejected", approvedByActorId: actor.actorId });
1727
+ await audit(actor, "review.decide", "manual_decision_proposal", proposal.id, "rejected");
1728
+ return { status: "rejected" };
1729
+ }
1730
+ const attempt = await loadAttempt(actor, proposal.attemptId);
1731
+ await store.saveManualDecisionProposal({ ...proposal, status: "approved", approvedByActorId: actor.actorId });
1732
+ if (proposal.proposedStatus === "verified") {
1733
+ await store.insertDecision({
1734
+ tenantKey: actor.tenantKey,
1735
+ id: newId("dec", cryptoImpl),
1736
+ subjectHash: attempt.subjectHash,
1737
+ packageCode: attempt.packageCode,
1738
+ attemptId: attempt.id,
1739
+ status: "verified",
1740
+ source: "manual",
1741
+ policyVersion: proposal.policyVersion,
1742
+ reasonCodes: [command.reason],
1743
+ effectiveAt: now().toISOString(),
1744
+ expiresAt: proposal.expiresAt,
1745
+ revokedAt: null,
1746
+ proposerActorId: proposal.proposedByActorId,
1747
+ approverActorId: actor.actorId,
1748
+ createdAt: now().toISOString()
1749
+ });
1750
+ }
1751
+ if (proposal.proposedStatus === "declined" || proposal.proposedStatus === "revoked") {
1752
+ const nextStatus = proposal.proposedStatus === "declined" ? "declined" : attempt.canonicalStatus;
1753
+ await store.updateAttempt({
1754
+ ...attempt,
1755
+ canonicalStatus: applyMonotonicStatus(attempt.canonicalStatus, nextStatus),
1756
+ updatedAt: now().toISOString()
1757
+ });
1758
+ }
1759
+ await audit(actor, "review.decide", "manual_decision_proposal", proposal.id, "approved");
1760
+ return { status: "approved" };
1761
+ }
1762
+ async function transitionCase(actor, command) {
1763
+ await ready();
1764
+ await authorize(actor, "review", { type: "review_case", id: command.reviewCaseId });
1765
+ const reviewCase = await store.getReviewCase(actor.tenantKey, command.reviewCaseId);
1766
+ if (!reviewCase) throw new EngineError("ATTEMPT_NOT_FOUND", "The review case was not found.");
1767
+ const status2 = applyGovernanceStatus(reviewCase.status, command.transition);
1768
+ await store.saveReviewCase({
1769
+ ...reviewCase,
1770
+ status: status2,
1771
+ reason: command.reason,
1772
+ updatedAt: now().toISOString()
1773
+ });
1774
+ await audit(actor, "review.case", "review_case", reviewCase.id, status2);
1775
+ return { status: status2 };
1776
+ }
1777
+ async function evaluateProtectedAction(actor, command) {
1778
+ await ready();
1779
+ await authorize(actor, "evaluate_protected_action", { type: "action", id: command.action });
1780
+ const subjectHash = await store.hashSubject(actor.tenantKey, command.subjectReference);
1781
+ const resourceHash = await store.hashResource(actor.tenantKey, command.resourceType, command.resourceReference);
1782
+ const environment = runtime.productionEnabled ? "production" : "sandbox";
1783
+ const policy = await policyStore.getActivePolicy(actor.tenantKey, environment === "production" ? "production" : "sandbox") ?? await policyStore.getActivePolicy(actor.tenantKey, "sandbox");
1784
+ if (!policy) {
1785
+ throw new EngineError("NO_ELIGIBLE_ROUTE", "No active verification policy is available for protected actions.");
1786
+ }
1787
+ let requirements = await policyStore.listProtectedActionRequirements(actor.tenantKey, command.action, policy.id);
1788
+ if (!requirements.length) {
1789
+ requirements = [{
1790
+ tenantKey: actor.tenantKey,
1791
+ id: "implicit_human_idv",
1792
+ action: command.action,
1793
+ packageCode: "human_idv",
1794
+ policyVersionId: policy.id,
1795
+ createdAt: now().toISOString()
1796
+ }];
1797
+ }
1798
+ const missing = [];
1799
+ for (const requirement of requirements) {
1800
+ const decision = await store.getValidDecision(actor.tenantKey, subjectHash, requirement.packageCode, now());
1801
+ if (!decision) missing.push(requirement.packageCode);
1802
+ }
1803
+ if (!missing.length) return { allowed: true };
1804
+ const destinations = await policyStore.getContinuationDestinations(actor.tenantKey);
1805
+ const destinationKey = command.destinationKey ?? "verification.resume";
1806
+ if (!destinations.includes(destinationKey)) {
1807
+ throw new EngineError("DESTINATION_NOT_ALLOWLISTED", "The continuation destination is not allowlisted.");
1808
+ }
1809
+ const token = randomToken(cryptoImpl);
1810
+ const key = newId("cont", cryptoImpl);
1811
+ const expiresAt = new Date(now().getTime() + (runtime.continuationTtlSeconds ?? 900) * 1e3).toISOString();
1812
+ await store.saveContinuation({
1813
+ tenantKey: actor.tenantKey,
1814
+ key,
1815
+ tokenHash: await sha256Hex(token),
1816
+ action: command.action,
1817
+ resourceHash,
1818
+ subjectHash,
1819
+ destinationKey,
1820
+ expiresAt,
1821
+ consumedAt: null
1822
+ });
1823
+ const denial = {
1824
+ code: "VERIFICATION_REQUIRED",
1825
+ action: command.action,
1826
+ resourceHash,
1827
+ requiredPackages: missing,
1828
+ continuation: { key, token, expiresAt },
1829
+ retryAfter: null,
1830
+ supportPath: null
1831
+ };
1832
+ return denial;
1833
+ }
1834
+ const admin = {
1835
+ async health(actor) {
1836
+ await ready();
1837
+ await authorize(actor, "admin.health", { type: "tenant" });
1838
+ return {
1839
+ observations: await store.listHealth(actor.tenantKey),
1840
+ circuits: await store.listCircuits(actor.tenantKey)
1841
+ };
1842
+ },
1843
+ async routes(actor) {
1844
+ await ready();
1845
+ await authorize(actor, "admin.routes", { type: "tenant" });
1846
+ return store.listRoutes(actor.tenantKey);
1847
+ },
1848
+ async circuits(actor) {
1849
+ await ready();
1850
+ await authorize(actor, "admin.circuits", { type: "tenant" });
1851
+ return store.listCircuits(actor.tenantKey);
1852
+ },
1853
+ async attempts(actor) {
1854
+ await ready();
1855
+ await authorize(actor, "admin.attempts", { type: "tenant" });
1856
+ return store.listAttempts(actor.tenantKey);
1857
+ },
1858
+ async audit(actor) {
1859
+ await ready();
1860
+ await authorize(actor, "admin.audit", { type: "tenant" });
1861
+ return store.listAudit(actor.tenantKey);
1862
+ },
1863
+ async proposeRoute(actor, input2) {
1864
+ await ready();
1865
+ await authorize(actor, "admin.propose_route", { type: "route" });
1866
+ const request = {
1867
+ tenantKey: actor.tenantKey,
1868
+ id: newId("rcr", cryptoImpl),
1869
+ routeId: input2.route.id,
1870
+ proposedPayload: {
1871
+ provider: input2.route.provider,
1872
+ packageCode: String(input2.route.packageCode),
1873
+ environment: input2.route.environment,
1874
+ priority: input2.route.priority
1875
+ },
1876
+ status: "proposed",
1877
+ reason: input2.reason,
1878
+ policyVersion: input2.route.policyVersionId,
1879
+ proposedByActorId: actor.actorId,
1880
+ approvedByActorId: null,
1881
+ approvedAt: null,
1882
+ expiresAt: null,
1883
+ createdAt: now().toISOString()
1884
+ };
1885
+ await store.saveRouteChangeRequest(request);
1886
+ await store.saveRoute({
1887
+ ...input2.route,
1888
+ tenantKey: actor.tenantKey,
1889
+ lifecycle: "draft",
1890
+ proposedByActorId: actor.actorId,
1891
+ approvedByActorId: null,
1892
+ approvedAt: null,
1893
+ activatedAt: null,
1894
+ createdAt: now().toISOString(),
1895
+ updatedAt: now().toISOString()
1896
+ });
1897
+ await audit(actor, "admin.propose_route", "route", input2.route.id, "proposed");
1898
+ return { requestId: request.id };
1899
+ },
1900
+ async approveRoute(actor, requestId, reason) {
1901
+ await ready();
1902
+ await authorize(actor, "admin.approve_route", { type: "route_change_request", id: requestId });
1903
+ const request = await store.getRouteChangeRequest(actor.tenantKey, requestId);
1904
+ if (!request) throw new EngineError("ATTEMPT_NOT_FOUND", "The route change request was not found.");
1905
+ if (request.proposedByActorId === actor.actorId) {
1906
+ throw new EngineError("GOVERNANCE_TWO_ACTOR", "The proposer cannot approve their own route change.");
1907
+ }
1908
+ const route = request.routeId ? await store.getRoute(actor.tenantKey, request.routeId) : null;
1909
+ if (!route) throw new EngineError("NO_ELIGIBLE_ROUTE", "The proposed route was not found.");
1910
+ if (route.environment === "production" && !runtime.productionEnabled) {
1911
+ throw new EngineError("PRODUCTION_NOT_ACTIVATED", "Production routes cannot be activated without the runtime key.");
1912
+ }
1913
+ await store.saveRouteChangeRequest({
1914
+ ...request,
1915
+ status: "approved",
1916
+ reason,
1917
+ approvedByActorId: actor.actorId,
1918
+ approvedAt: now().toISOString()
1919
+ });
1920
+ await store.saveRoute({
1921
+ ...route,
1922
+ lifecycle: "active",
1923
+ approvedByActorId: actor.actorId,
1924
+ approvedAt: now().toISOString(),
1925
+ activatedAt: now().toISOString(),
1926
+ updatedAt: now().toISOString()
1927
+ });
1928
+ await audit(actor, "admin.approve_route", "route", route.id, "approved");
1929
+ return { routeId: route.id };
1930
+ },
1931
+ async proposePolicy(actor, input2) {
1932
+ await ready();
1933
+ await authorize(actor, "admin.propose_policy", { type: "policy" });
1934
+ const policy = {
1935
+ tenantKey: actor.tenantKey,
1936
+ id: newId("pol", cryptoImpl),
1937
+ version: input2.version,
1938
+ environment: input2.environment,
1939
+ lifecycle: "draft",
1940
+ reason: input2.reason,
1941
+ expiresAt: input2.expiresAt ?? null,
1942
+ proposedByActorId: actor.actorId,
1943
+ approvedByActorId: null,
1944
+ approvedAt: null,
1945
+ activatedAt: null,
1946
+ createdAt: now().toISOString(),
1947
+ decisionRetentionDays: input2.decisionRetentionDays ?? null,
1948
+ providerRedactionDelayDays: input2.providerRedactionDelayDays ?? null,
1949
+ appealHoldDays: input2.appealHoldDays ?? null,
1950
+ legalHold: input2.legalHold ?? false
1951
+ };
1952
+ await store.savePolicyVersion(policy);
1953
+ await audit(actor, "admin.propose_policy", "policy", policy.id, "draft");
1954
+ return { policyId: policy.id };
1955
+ },
1956
+ async approvePolicy(actor, policyId, reason) {
1957
+ await ready();
1958
+ await authorize(actor, "admin.approve_policy", { type: "policy", id: policyId });
1959
+ const policy = await store.getPolicyVersion(actor.tenantKey, policyId);
1960
+ if (!policy) throw new EngineError("ATTEMPT_NOT_FOUND", "The policy was not found.");
1961
+ if (policy.proposedByActorId === actor.actorId) {
1962
+ throw new EngineError("GOVERNANCE_TWO_ACTOR", "The proposer cannot approve their own policy.");
1963
+ }
1964
+ await store.savePolicyVersion({
1965
+ ...policy,
1966
+ lifecycle: "approved",
1967
+ reason,
1968
+ approvedByActorId: actor.actorId,
1969
+ approvedAt: now().toISOString()
1970
+ });
1971
+ await audit(actor, "admin.approve_policy", "policy", policy.id, "approved");
1972
+ return { policyId: policy.id };
1973
+ },
1974
+ async activatePolicy(actor, policyId) {
1975
+ await ready();
1976
+ await authorize(actor, "admin.activate_policy", { type: "policy", id: policyId });
1977
+ const policy = await store.getPolicyVersion(actor.tenantKey, policyId);
1978
+ if (!policy) throw new EngineError("ATTEMPT_NOT_FOUND", "The policy was not found.");
1979
+ if (!twoActorApproved(policy.proposedByActorId, policy.approvedByActorId)) {
1980
+ throw new EngineError("GOVERNANCE_TWO_ACTOR", "An active policy requires distinct proposer and approver.");
1981
+ }
1982
+ if (policy.environment === "production" && !runtime.productionEnabled) {
1983
+ throw new EngineError("PRODUCTION_NOT_ACTIVATED", "Production policy activation requires the runtime production key.");
1984
+ }
1985
+ if (policy.environment === "production" && (policy.decisionRetentionDays == null || policy.providerRedactionDelayDays == null || policy.appealHoldDays == null)) {
1986
+ throw new EngineError(
1987
+ "PRODUCTION_NOT_ACTIVATED",
1988
+ "Production policy activation requires explicit retention, redaction, appeal-hold, and legal-hold values."
1989
+ );
1990
+ }
1991
+ await store.savePolicyVersion({
1992
+ ...policy,
1993
+ lifecycle: "active",
1994
+ activatedAt: now().toISOString()
1995
+ });
1996
+ await audit(actor, "admin.activate_policy", "policy", policy.id, "active");
1997
+ return { policyId: policy.id };
1998
+ },
1999
+ async emergencyDrain(actor, provider, environment, reason) {
2000
+ await ready();
2001
+ await authorize(actor, "admin.emergency_drain", { type: "provider", id: provider });
2002
+ const circuit = await store.getCircuit(actor.tenantKey, provider, environment);
2003
+ await store.saveCircuit({
2004
+ ...circuit,
2005
+ state: "open",
2006
+ reasonCode: reason,
2007
+ openUntil: new Date(now().getTime() + 24 * 36e5).toISOString(),
2008
+ drainedByActorId: actor.actorId,
2009
+ updatedAt: now().toISOString()
2010
+ });
2011
+ await audit(actor, "admin.emergency_drain", "circuit", provider, reason);
2012
+ return { state: "open" };
2013
+ }
2014
+ };
2015
+ return {
2016
+ start,
2017
+ resume,
2018
+ status,
2019
+ retry,
2020
+ pause,
2021
+ cancel,
2022
+ redact,
2023
+ ingestWebhook,
2024
+ processWebhookJob,
2025
+ reconcile,
2026
+ appeal: { submit: submitAppeal, transition: transitionAppeal },
2027
+ review: { propose: proposeReview, decide: decideReview, transitionCase },
2028
+ admin,
2029
+ evaluateProtectedAction,
2030
+ workers: {
2031
+ claim: claimWorkerJobs,
2032
+ process: processWorkerJob,
2033
+ scheduleReconciliation
2034
+ }
2035
+ };
2036
+ }
2037
+ function stripPersistedLaunch(launch) {
2038
+ return launch;
2039
+ }
2040
+ var REDACTION_STATUSES = [
2041
+ "scheduled",
2042
+ "processing",
2043
+ "retryable",
2044
+ "redacted",
2045
+ "not_applicable",
2046
+ "dead_letter"
2047
+ ];
2048
+ function toRedactionStatus(value) {
2049
+ if (value === "failed") return "dead_letter";
2050
+ if (value && REDACTION_STATUSES.includes(value)) return value;
2051
+ return "retryable";
2052
+ }
2053
+
2054
+ export { APPLICATION_REASON_CODES, AuthorizationError, ClientRouteInjectionError, EngineError, FORBIDDEN_CLIENT_ROUTE_KEYS, GOVERNANCE_TRANSITIONS, LIVE_ATTEMPT_STATUSES, WebhookSecurityIncidentError, applyMonotonicStatus, assertNoClientRouting, backoffSeconds, canTransitionStatus, canonicalize, cohortBucket, createMemoryQueue, createMemoryStore, createProviderRegistry, createRateBudget, createVerificationPlatform, digestCanonical, hmacSha256Hex, isApplicationReasonCode, seedSandboxExamples, selectRoute, sha256Hex };
2055
+ //# sourceMappingURL=index.js.map
2056
+ //# sourceMappingURL=index.js.map