@splitin/verification-adapter-sdk 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.
@@ -0,0 +1,727 @@
1
+ 'use strict';
2
+
3
+ var _2020_js = require('ajv/dist/2020.js');
4
+
5
+ // src/errors.ts
6
+ var ProviderError = class extends Error {
7
+ constructor(code, message, options = {}) {
8
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
9
+ this.code = code;
10
+ this.name = "ProviderError";
11
+ this.retryable = options.retryable ?? false;
12
+ this.safeCode = options.safeCode ?? code.toLowerCase();
13
+ this.retryAfterSeconds = options.retryAfterSeconds;
14
+ }
15
+ code;
16
+ retryable;
17
+ safeCode;
18
+ retryAfterSeconds;
19
+ };
20
+
21
+ // src/identifiers.ts
22
+ var VERIFICATION_ADAPTER_CONTRACT_VERSION = "1.0.0";
23
+ var ENGINE_CONTRACT_VERSION = "1.0.0";
24
+ var PROVIDER_MANIFEST_SCHEMA_URN = "urn:splitin:verification:provider-manifest:v1";
25
+ var STANDARD_PACKAGE_CODES = [
26
+ "human_idv",
27
+ "business_kyb",
28
+ "associated_person_idv",
29
+ "ownership_review"
30
+ ];
31
+ var CANONICAL_STATUSES = [
32
+ "created",
33
+ "pending_user_input",
34
+ "paused",
35
+ "processing",
36
+ "manual_review_required",
37
+ "verified",
38
+ "declined",
39
+ "failed",
40
+ "expired",
41
+ "canceled",
42
+ "provider_unavailable",
43
+ "redacted"
44
+ ];
45
+ var TERMINAL_STATUSES = [
46
+ "verified",
47
+ "declined",
48
+ "failed",
49
+ "expired",
50
+ "canceled",
51
+ "redacted"
52
+ ];
53
+ var STANDARD_PACKAGE_SET = new Set(STANDARD_PACKAGE_CODES);
54
+ var STANDARD_STATUS_SET = new Set(CANONICAL_STATUSES);
55
+ var TERMINAL_STATUS_SET = new Set(TERMINAL_STATUSES);
56
+ var PROVIDER_CODE = /^[a-z][a-z0-9_]{1,63}$/;
57
+ var LAUNCHER_KEY = /^[a-z][a-z0-9_]{1,63}$/;
58
+ var CUSTOM_PACKAGE = /^[a-z0-9][a-z0-9-]{0,32}(?:\.[a-z0-9][a-z0-9_-]{0,63}){1,6}$/;
59
+ var WEBHOOK_PROTOCOL = /^(?:[a-z][a-z0-9_]{1,63}|[a-z0-9][a-z0-9-]{0,32}(?:\.[a-z0-9][a-z0-9_-]{0,63}){1,6})$/;
60
+ var COUNTRY = /^[A-Z]{2}$/;
61
+ var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
62
+ var GOVERNMENT_ID = /\b(?:ssn|itin|nino|sin|aadhaar|passport|national[_-]?id|tax[_-]?id|ein|ssn_last4)\b/i;
63
+ function isProviderCode(value) {
64
+ return PROVIDER_CODE.test(value);
65
+ }
66
+ function isLauncherKey(value) {
67
+ return LAUNCHER_KEY.test(value);
68
+ }
69
+ function isStandardPackageCode(value) {
70
+ return STANDARD_PACKAGE_SET.has(value);
71
+ }
72
+ function isCustomPackageCode(value) {
73
+ return CUSTOM_PACKAGE.test(value) && !STANDARD_PACKAGE_SET.has(value);
74
+ }
75
+ function isPackageCode(value) {
76
+ return isStandardPackageCode(value) || isCustomPackageCode(value);
77
+ }
78
+ function isWebhookProtocol(value) {
79
+ return WEBHOOK_PROTOCOL.test(value);
80
+ }
81
+ function isCountryCode(value) {
82
+ return COUNTRY.test(value);
83
+ }
84
+ function isSemver(value) {
85
+ return SEMVER.test(value);
86
+ }
87
+ function isCanonicalStatus(value) {
88
+ return STANDARD_STATUS_SET.has(value);
89
+ }
90
+ function isTerminalStatus(value) {
91
+ return TERMINAL_STATUS_SET.has(value);
92
+ }
93
+ function metadataContainsForbiddenIdentifier(value) {
94
+ if (value == null) return false;
95
+ if (typeof value === "string") return GOVERNMENT_ID.test(value);
96
+ if (Array.isArray(value)) return value.some(metadataContainsForbiddenIdentifier);
97
+ if (typeof value === "object") {
98
+ return Object.entries(value).some(([key, nested]) => GOVERNMENT_ID.test(key) || metadataContainsForbiddenIdentifier(nested));
99
+ }
100
+ return false;
101
+ }
102
+ function majorsCompatible(left, right) {
103
+ return left.split(".")[0] === right.split(".")[0];
104
+ }
105
+
106
+ // src/schema.ts
107
+ var providerManifestV1JsonSchema = {
108
+ $schema: "https://json-schema.org/draft/2020-12/schema",
109
+ $id: PROVIDER_MANIFEST_SCHEMA_URN,
110
+ title: "Verification Provider Manifest V1",
111
+ type: "object",
112
+ additionalProperties: false,
113
+ required: [
114
+ "contractVersion",
115
+ "adapterVersion",
116
+ "engineCompatibility",
117
+ "provider",
118
+ "displayName",
119
+ "supportedPackages",
120
+ "supportedCountries",
121
+ "environments",
122
+ "capabilities",
123
+ "launcherKeys",
124
+ "launchPresentations",
125
+ "configurationSchemaVersion",
126
+ "configurationSchema",
127
+ "webhook",
128
+ "dataPolicy",
129
+ "retry",
130
+ "cancellation",
131
+ "redaction",
132
+ "apiHosts",
133
+ "testedApiVersions"
134
+ ],
135
+ properties: {
136
+ contractVersion: { const: "1.0.0" },
137
+ adapterVersion: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
138
+ engineCompatibility: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
139
+ provider: { type: "string", pattern: "^[a-z][a-z0-9_]{1,63}$" },
140
+ displayName: { type: "string", minLength: 1 },
141
+ description: { type: "string" },
142
+ supportedPackages: {
143
+ type: "array",
144
+ minItems: 1,
145
+ uniqueItems: true,
146
+ items: {
147
+ type: "string",
148
+ anyOf: [
149
+ { enum: ["human_idv", "business_kyb", "associated_person_idv", "ownership_review"] },
150
+ { pattern: "^[a-z0-9][a-z0-9-]{0,32}(?:\\.[a-z0-9][a-z0-9_-]{0,63}){1,6}$" }
151
+ ]
152
+ }
153
+ },
154
+ supportedCountries: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", pattern: "^[A-Z]{2}$" } },
155
+ environments: { type: "array", minItems: 1, uniqueItems: true, items: { enum: ["sandbox", "production"] } },
156
+ capabilities: {
157
+ type: "object",
158
+ additionalProperties: false,
159
+ required: ["presentations", "canResume", "canRetry", "canCancel", "canRedact"],
160
+ properties: {
161
+ presentations: { type: "array", minItems: 1, uniqueItems: true, items: { enum: ["embedded", "hosted", "qr", "none"] } },
162
+ canResume: { type: "boolean" },
163
+ canRetry: { type: "boolean" },
164
+ canCancel: { type: "boolean" },
165
+ canRedact: { type: "boolean" }
166
+ }
167
+ },
168
+ launcherKeys: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", pattern: "^[a-z][a-z0-9_]{1,63}$" } },
169
+ launchPresentations: { type: "array", minItems: 1, uniqueItems: true, items: { enum: ["embedded", "hosted", "qr", "none"] } },
170
+ configurationSchemaVersion: { type: "string", minLength: 1 },
171
+ configurationSchema: { $ref: "#/$defs/jsonSchema2020" },
172
+ webhook: {
173
+ type: "object",
174
+ additionalProperties: false,
175
+ required: ["protocol", "eventFamilies"],
176
+ properties: {
177
+ protocol: { type: "string", minLength: 1, maxLength: 128 },
178
+ eventFamilies: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 } },
179
+ toleranceSeconds: { type: "integer", minimum: 1 }
180
+ }
181
+ },
182
+ dataPolicy: {
183
+ type: "object",
184
+ additionalProperties: false,
185
+ required: [
186
+ "classifications",
187
+ "prohibitedPersistence",
188
+ "rawPayloadPersistence",
189
+ "browserSecretPersistence",
190
+ "governmentIdentifierPersistence"
191
+ ],
192
+ properties: {
193
+ classifications: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 } },
194
+ prohibitedPersistence: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 } },
195
+ rawPayloadPersistence: { const: false },
196
+ browserSecretPersistence: { const: false },
197
+ governmentIdentifierPersistence: { const: false }
198
+ }
199
+ },
200
+ retry: {
201
+ type: "object",
202
+ additionalProperties: false,
203
+ required: ["sameResourceWhenResumable", "newAttemptAfterTerminal"],
204
+ properties: {
205
+ sameResourceWhenResumable: { type: "boolean" },
206
+ newAttemptAfterTerminal: { type: "boolean" }
207
+ }
208
+ },
209
+ cancellation: {
210
+ type: "object",
211
+ additionalProperties: false,
212
+ required: ["supported", "terminal"],
213
+ properties: { supported: { type: "boolean" }, terminal: { type: "boolean" } }
214
+ },
215
+ redaction: {
216
+ type: "object",
217
+ additionalProperties: false,
218
+ required: ["supported", "asynchronous"],
219
+ properties: {
220
+ supported: { type: "boolean" },
221
+ asynchronous: { type: "boolean" },
222
+ notApplicable: { type: "boolean" }
223
+ }
224
+ },
225
+ apiHosts: {
226
+ type: "array",
227
+ minItems: 1,
228
+ uniqueItems: true,
229
+ items: { type: "string", pattern: "^[a-z0-9.-]+$" }
230
+ },
231
+ testedApiVersions: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", minLength: 1 } }
232
+ },
233
+ $defs: {
234
+ jsonSchema2020: {
235
+ type: "object",
236
+ required: ["$schema", "type", "additionalProperties", "required", "properties"],
237
+ properties: {
238
+ $schema: { const: "https://json-schema.org/draft/2020-12/schema" },
239
+ type: { const: "object" },
240
+ additionalProperties: { const: false },
241
+ required: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 } },
242
+ properties: { type: "object" }
243
+ }
244
+ }
245
+ }
246
+ };
247
+
248
+ // src/manifest.ts
249
+ var manifestValidator = new _2020_js.Ajv2020({ allErrors: true, strict: false }).compile(providerManifestV1JsonSchema);
250
+ var REQUIRED_ADAPTER_METHODS = [
251
+ "validateConfiguration",
252
+ "createAttempt",
253
+ "resumeAttempt",
254
+ "retrieveAttempt",
255
+ "retryAttempt",
256
+ "cancelAttempt",
257
+ "redactSubject",
258
+ "verifyWebhook",
259
+ "normalizeWebhook"
260
+ ];
261
+ function assertProviderManifest(manifest) {
262
+ if (!manifestValidator(manifest)) {
263
+ const first = manifestValidator.errors?.[0];
264
+ invalid(
265
+ `json_schema_${toSafeCode(first?.keyword ?? "invalid")}`,
266
+ `The provider adapter manifest does not satisfy ProviderManifestV1 JSON Schema at ${first?.instancePath || "/"}.`
267
+ );
268
+ }
269
+ if (manifest.contractVersion !== VERIFICATION_ADAPTER_CONTRACT_VERSION) invalid("contract_version");
270
+ if (!isProviderCode(manifest.provider)) invalid("provider_code");
271
+ if (!manifest.displayName.trim() || !isSemver(manifest.adapterVersion) || !isSemver(manifest.engineCompatibility)) {
272
+ invalid("identity");
273
+ }
274
+ if (!majorsCompatible(manifest.adapterVersion, VERIFICATION_ADAPTER_CONTRACT_VERSION)) {
275
+ invalid("adapter_version_incompatible", "Provider adapter major version must be compatible with contract V1.");
276
+ }
277
+ if (!majorsCompatible(manifest.engineCompatibility, ENGINE_CONTRACT_VERSION)) {
278
+ invalid("engine_incompatible", "Provider adapter is not compatible with this engine major version.");
279
+ }
280
+ if (!manifest.supportedPackages.length || manifest.supportedPackages.some((value) => !isPackageCode(value))) {
281
+ invalid("supported_packages");
282
+ }
283
+ if (manifest.supportedPackages.some((value) => !isStandardPackageCode(value) && !isCustomPackageCode(value))) {
284
+ invalid("supported_packages");
285
+ }
286
+ if (!manifest.supportedCountries.length || manifest.supportedCountries.some((value) => !isCountryCode(value))) {
287
+ invalid("supported_countries");
288
+ }
289
+ if (!manifest.launcherKeys.length || manifest.launcherKeys.some((value) => !isLauncherKey(value))) {
290
+ invalid("launcher_keys");
291
+ }
292
+ if (!isWebhookProtocol(manifest.webhook.protocol)) invalid("webhook_protocol");
293
+ if (!manifest.apiHosts.length || manifest.apiHosts.some((host) => host.includes("/") || host.includes(":"))) {
294
+ invalid("api_hosts", "Provider API hosts must be code-owned hostnames, not URLs or caller-supplied origins.");
295
+ }
296
+ if (manifest.dataPolicy.rawPayloadPersistence !== false || manifest.dataPolicy.browserSecretPersistence !== false || manifest.dataPolicy.governmentIdentifierPersistence !== false) {
297
+ invalid("data_policy");
298
+ }
299
+ if (manifest.capabilities.canCancel !== manifest.cancellation.supported) invalid("cancellation_capability");
300
+ if (manifest.capabilities.canRedact !== manifest.redaction.supported) invalid("redaction_capability");
301
+ assertConfigurationSecrets(manifest);
302
+ }
303
+ function assertConfigurationSecrets(manifest) {
304
+ const properties = manifest.configurationSchema.properties ?? {};
305
+ for (const [name, schema] of Object.entries(properties)) {
306
+ if (schema["x-secret"] === true && schema.type !== "string") {
307
+ invalid("secret_schema", `Configuration field "${name}" marked x-secret must be a string.`);
308
+ }
309
+ }
310
+ }
311
+ function assertAdapterConformsToManifest(adapter) {
312
+ const candidate = adapter;
313
+ const provider = typeof candidate.provider === "string" && candidate.provider ? candidate.provider : "unknown";
314
+ for (const method of REQUIRED_ADAPTER_METHODS) {
315
+ if (typeof candidate[method] !== "function") {
316
+ invalid(
317
+ `missing_method_${toSafeCode(method)}`,
318
+ `Provider adapter "${provider}" is missing required method "${method}".`
319
+ );
320
+ }
321
+ }
322
+ if (!candidate.manifest || typeof candidate.manifest !== "object") {
323
+ invalid("missing_manifest", `Provider adapter "${provider}" is missing its manifest.`);
324
+ }
325
+ assertProviderManifest(adapter.manifest);
326
+ if (adapter.contractVersion !== VERIFICATION_ADAPTER_CONTRACT_VERSION || adapter.provider !== adapter.manifest.provider || !adapter.manifest.environments.includes(adapter.environment)) {
327
+ invalid("adapter_identity", `Provider adapter "${provider}" does not match its manifest identity or environment.`);
328
+ }
329
+ if (adapter.manifest.capabilities.canResume === false && typeof candidate.resumeAttempt !== "function") {
330
+ invalid("resume_capability");
331
+ }
332
+ adapter.validateConfiguration();
333
+ }
334
+ function invalid(safeCode, message = "The provider adapter manifest is invalid.") {
335
+ throw new ProviderError("INVALID_CONFIGURATION", message, { safeCode });
336
+ }
337
+ function toSafeCode(value) {
338
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
339
+ }
340
+
341
+ // src/conformance.ts
342
+ var CANONICAL_STATUS_RANK = {
343
+ created: 10,
344
+ pending_user_input: 20,
345
+ paused: 20,
346
+ provider_unavailable: 25,
347
+ processing: 30,
348
+ manual_review_required: 40,
349
+ verified: 100,
350
+ declined: 100,
351
+ failed: 100,
352
+ expired: 100,
353
+ canceled: 100,
354
+ redacted: 200
355
+ };
356
+ function canonicalStatusRank(status) {
357
+ return CANONICAL_STATUS_RANK[status];
358
+ }
359
+ var providerConformanceScenarios = Object.freeze([
360
+ "success",
361
+ "input_required",
362
+ "processing",
363
+ "verified",
364
+ "decline",
365
+ "failure",
366
+ "manual_review",
367
+ "timeout",
368
+ "rate_limit",
369
+ "malformed_response",
370
+ "unknown_state",
371
+ "cancellation",
372
+ "resume",
373
+ "asynchronous_redaction",
374
+ "retryable_provider_failure"
375
+ ]);
376
+ async function runAdapterConformance(adapter, command, options = {}) {
377
+ const results = [];
378
+ const manifest = capture("manifest", () => assertAdapterConformsToManifest(adapter));
379
+ results.push(manifest);
380
+ if (!manifest.passed) return results;
381
+ results.push(capture("package", () => {
382
+ if (!adapter.manifest.supportedPackages.includes(command.packageCode)) {
383
+ throw new Error("The fixture package is not declared by the adapter.");
384
+ }
385
+ if (metadataContainsForbiddenIdentifier(command.metadata) || !command.subjectReference) {
386
+ throw new Error("Attempt command contains forbidden identifiers or a missing opaque subject.");
387
+ }
388
+ }));
389
+ let created = null;
390
+ try {
391
+ created = await adapter.createAttempt(command);
392
+ assertAttemptResult(adapter, created);
393
+ results.push({ name: "create", passed: true });
394
+ } catch (error) {
395
+ results.push(failure("create", error));
396
+ }
397
+ if (created) {
398
+ try {
399
+ const duplicate = await adapter.createAttempt(command);
400
+ if (duplicate.providerResourceId !== created.providerResourceId) {
401
+ throw new Error("Duplicate create produced a second provider resource.");
402
+ }
403
+ results.push({ name: "create_idempotency", passed: true });
404
+ } catch (error) {
405
+ results.push(failure("create_idempotency", error));
406
+ }
407
+ if (adapter.manifest.capabilities.canResume) {
408
+ try {
409
+ const resumed = await adapter.resumeAttempt(resourceCommand(command, created.providerResourceId));
410
+ assertLaunchResult(adapter, resumed);
411
+ results.push({ name: "resume", passed: true });
412
+ } catch (error) {
413
+ results.push(failure("resume", error));
414
+ }
415
+ } else {
416
+ results.push(skipped("resume", "canResume"));
417
+ }
418
+ try {
419
+ const snapshot = await adapter.retrieveAttempt(resourceCommand(command, created.providerResourceId));
420
+ assertNormalizedSnapshot(snapshot);
421
+ results.push({ name: "retrieve", passed: true });
422
+ try {
423
+ if (isTerminalStatus(snapshot.canonicalStatus)) {
424
+ const second = await adapter.retrieveAttempt(resourceCommand(command, created.providerResourceId));
425
+ assertNormalizedSnapshot(second);
426
+ const firstRank = canonicalStatusRank(snapshot.canonicalStatus);
427
+ const secondRank = canonicalStatusRank(second.canonicalStatus);
428
+ if (!isTerminalStatus(second.canonicalStatus) && secondRank < firstRank) {
429
+ throw new Error("A terminal snapshot must not regress to a non-terminal status with lower rank.");
430
+ }
431
+ }
432
+ results.push({ name: "terminal_monotonicity", passed: true });
433
+ } catch (error) {
434
+ results.push(failure("terminal_monotonicity", error));
435
+ }
436
+ } catch (error) {
437
+ results.push(failure("retrieve", error));
438
+ }
439
+ if (adapter.manifest.capabilities.canRetry) {
440
+ try {
441
+ const retried = await adapter.retryAttempt({ ...command, previousProviderResourceId: created.providerResourceId });
442
+ assertAttemptResult(adapter, retried);
443
+ results.push({ name: "retry", passed: true });
444
+ } catch (error) {
445
+ results.push(failure("retry", error));
446
+ }
447
+ } else {
448
+ results.push(skipped("retry", "canRetry"));
449
+ }
450
+ if (adapter.manifest.capabilities.canCancel) {
451
+ try {
452
+ const canceled = await adapter.cancelAttempt(resourceCommand(command, created.providerResourceId));
453
+ if (!canceled.accepted) throw new Error("Adapter did not acknowledge cancellation.");
454
+ results.push({ name: "cancel", passed: true });
455
+ } catch (error) {
456
+ results.push(failure("cancel", error));
457
+ }
458
+ } else {
459
+ results.push(skipped("cancel", "canCancel"));
460
+ }
461
+ if (adapter.manifest.capabilities.canRedact) {
462
+ try {
463
+ const redaction = await adapter.redactSubject(redactionCommand(command, created.providerResourceId));
464
+ assertRedactionResult(redaction);
465
+ results.push({ name: "redact", passed: true });
466
+ } catch (error) {
467
+ results.push(failure("redact", error));
468
+ }
469
+ } else {
470
+ results.push(skipped("redact", "canRedact"));
471
+ }
472
+ }
473
+ if (options.webhookRequest) {
474
+ try {
475
+ const verified = await adapter.verifyWebhook(options.webhookRequest);
476
+ const normalized = await adapter.normalizeWebhook(verified);
477
+ if (!verified.providerEventKey || !verified.receivedAt || !verified.bodySha256 || !(verified.opaquePayload instanceof Uint8Array)) {
478
+ throw new Error("The verified webhook envelope is incomplete.");
479
+ }
480
+ if (!normalized.providerEventKey || !normalized.providerResourceId || !normalized.eventType || !normalized.providerEventType || !normalized.occurredAt) {
481
+ throw new Error("The normalized webhook event is incomplete.");
482
+ }
483
+ if (normalized.canonicalStatus) assertCanonicalStatus(normalized.canonicalStatus);
484
+ assertProviderTypesDoNotEscape(normalized);
485
+ results.push({ name: "webhook", passed: true });
486
+ } catch (error) {
487
+ results.push(failure("webhook", error));
488
+ }
489
+ }
490
+ return results;
491
+ }
492
+ async function runAdapterConformanceScenarios(adapterForScenario, command) {
493
+ const results = [];
494
+ for (const scenario of providerConformanceScenarios) {
495
+ try {
496
+ const adapter = await adapterForScenario(scenario);
497
+ assertAdapterConformsToManifest(adapter);
498
+ if (!adapter.manifest.supportedPackages.includes(command.packageCode)) {
499
+ throw new Error(`Scenario "${scenario}" uses a package that the adapter does not declare.`);
500
+ }
501
+ await executeScenario(adapter, command, scenario);
502
+ results.push({ name: scenario, passed: true });
503
+ } catch (error) {
504
+ results.push(failure(scenario, error));
505
+ }
506
+ }
507
+ return results;
508
+ }
509
+ function validateManifestOnly(manifest) {
510
+ return capture("manifest", () => assertProviderManifest(manifest));
511
+ }
512
+ function capture(name, operation) {
513
+ try {
514
+ operation();
515
+ return { name, passed: true };
516
+ } catch (error) {
517
+ return failure(name, error);
518
+ }
519
+ }
520
+ function failure(name, error) {
521
+ return { name, passed: false, detail: error instanceof Error ? error.message : "Unknown conformance failure." };
522
+ }
523
+ function skipped(name, capability) {
524
+ return { name, passed: true, detail: `Skipped because the manifest declares ${capability}=false.` };
525
+ }
526
+ async function executeScenario(adapter, command, scenario) {
527
+ const providerResourceId = `conformance_${command.attemptId.replace(/[^a-zA-Z0-9]/g, "")}`;
528
+ const resource = resourceCommand(command, providerResourceId);
529
+ switch (scenario) {
530
+ case "success":
531
+ case "verified": {
532
+ const created = await adapter.createAttempt(command);
533
+ assertAttemptResult(adapter, created);
534
+ const snapshot = await adapter.retrieveAttempt(resourceCommand(command, created.providerResourceId));
535
+ assertNormalizedSnapshot(snapshot);
536
+ if (snapshot.canonicalStatus !== "verified") {
537
+ throw new Error('The success scenario must normalize to canonical status "verified".');
538
+ }
539
+ return;
540
+ }
541
+ case "input_required":
542
+ await expectCanonicalSnapshot(adapter, resource, "pending_user_input");
543
+ return;
544
+ case "processing":
545
+ await expectCanonicalSnapshot(adapter, resource, "processing");
546
+ return;
547
+ case "decline":
548
+ await expectCanonicalSnapshot(adapter, resource, "declined");
549
+ return;
550
+ case "failure":
551
+ await expectCanonicalSnapshot(adapter, resource, "failed");
552
+ return;
553
+ case "manual_review":
554
+ await expectCanonicalSnapshot(adapter, resource, "manual_review_required");
555
+ return;
556
+ case "timeout":
557
+ await expectProviderError(() => adapter.retrieveAttempt(resource), "TIMEOUT", { retryable: true });
558
+ return;
559
+ case "rate_limit":
560
+ await expectProviderError(() => adapter.retrieveAttempt(resource), "RATE_LIMITED", { retryable: true, retryAfterRequired: true });
561
+ return;
562
+ case "retryable_provider_failure":
563
+ await expectProviderError(() => adapter.retrieveAttempt(resource), "RETRYABLE_PROVIDER_FAILURE", { retryable: true });
564
+ return;
565
+ case "malformed_response":
566
+ await expectRejectedSnapshot(adapter, resource, "malformed_provider_response");
567
+ return;
568
+ case "unknown_state":
569
+ await expectRejectedSnapshot(adapter, resource, "unknown_provider_status");
570
+ return;
571
+ case "cancellation": {
572
+ const canceled = await adapter.cancelAttempt(resource);
573
+ if (!canceled.accepted) throw new Error("Cancellation was not accepted.");
574
+ return;
575
+ }
576
+ case "resume": {
577
+ const launched = await adapter.resumeAttempt(resource);
578
+ assertLaunchResult(adapter, launched);
579
+ return;
580
+ }
581
+ case "asynchronous_redaction": {
582
+ if (!adapter.manifest.capabilities.canRedact) {
583
+ throw new Error('The redaction scenario requires manifest capability "canRedact".');
584
+ }
585
+ const redaction = await adapter.redactSubject(redactionCommand(command, providerResourceId));
586
+ assertRedactionResult(redaction);
587
+ if (adapter.manifest.redaction.asynchronous) {
588
+ if (redaction.completed && redaction.disposition !== "redacted") {
589
+ throw new Error("Asynchronous redaction must not claim completion until the provider finishes.");
590
+ }
591
+ if (!["processing", "scheduled", "retryable"].includes(redaction.disposition ?? "")) {
592
+ throw new Error("Asynchronous redaction must report processing, scheduled, or retryable.");
593
+ }
594
+ } else if (!redaction.completed || redaction.retryable) {
595
+ throw new Error("Synchronous redaction must complete terminally without retry.");
596
+ }
597
+ }
598
+ }
599
+ }
600
+ async function expectCanonicalSnapshot(adapter, command, expected) {
601
+ const snapshot = await adapter.retrieveAttempt(command);
602
+ assertNormalizedSnapshot(snapshot);
603
+ if (snapshot.canonicalStatus !== expected) {
604
+ throw new Error(`The scenario must normalize to canonical status "${expected}".`);
605
+ }
606
+ }
607
+ async function expectRejectedSnapshot(adapter, command, expectedSafeCode) {
608
+ try {
609
+ const snapshot = await adapter.retrieveAttempt(command);
610
+ assertNormalizedSnapshot(snapshot);
611
+ } catch (error) {
612
+ if (isProviderErrorLike(error) && error.safeCode === expectedSafeCode) return;
613
+ throw new Error(`The scenario must be rejected with safe code "${expectedSafeCode}"; received ${describeError(error)}.`);
614
+ }
615
+ throw new Error(`The scenario was accepted instead of being rejected as "${expectedSafeCode}".`);
616
+ }
617
+ async function expectProviderError(operation, code, expectations) {
618
+ try {
619
+ await operation();
620
+ } catch (error) {
621
+ if (!isProviderErrorLike(error)) {
622
+ throw new Error(`Expected ProviderError ${code}; received ${describeError(error)}.`);
623
+ }
624
+ if (error.code !== code) throw new Error(`Expected ProviderError ${code}; received ${error.code}.`);
625
+ if (error.retryable !== expectations.retryable) {
626
+ throw new Error(`ProviderError ${code} must set retryable=${String(expectations.retryable)}.`);
627
+ }
628
+ if (!error.safeCode.trim()) throw new Error(`ProviderError ${code} must provide a safe code.`);
629
+ if (expectations.retryAfterRequired && (!Number.isSafeInteger(error.retryAfterSeconds) || Number(error.retryAfterSeconds) <= 0)) {
630
+ throw new Error(`ProviderError ${code} must provide a positive integer retryAfterSeconds.`);
631
+ }
632
+ return;
633
+ }
634
+ throw new Error(`Expected ProviderError ${code}, but the adapter operation succeeded.`);
635
+ }
636
+ function assertAttemptResult(adapter, result) {
637
+ if (!result || typeof result !== "object" || !isNonEmptyString(result.providerResourceId) || !isNonEmptyString(result.providerStatus) || result.attemptId == null) {
638
+ malformed("The adapter returned an incomplete attempt result.");
639
+ }
640
+ assertLaunchResult(adapter, result.launch);
641
+ assertProviderTypesDoNotEscape(result);
642
+ }
643
+ function assertLaunchResult(adapter, launch) {
644
+ if (!launch || typeof launch !== "object" || !isNonEmptyString(launch.launcherKey) || !adapter.manifest.launcherKeys.includes(launch.launcherKey) || !adapter.manifest.capabilities.presentations.includes(launch.presentation) || !isCanonicalStatus(launch.canonicalStatus)) {
645
+ malformed("The adapter returned an invalid launch envelope.");
646
+ }
647
+ const forbidden = ["inquiryOrSessionId", "environmentId", "opaqueLaunchSecret", "adapter", "clientSecret"];
648
+ if (forbidden.some((key) => key in launch)) {
649
+ throw new Error("Deprecated or secret launch fields escaped the adapter boundary.");
650
+ }
651
+ }
652
+ function assertNormalizedSnapshot(snapshot) {
653
+ if (!snapshot || typeof snapshot !== "object" || !isNonEmptyString(snapshot.providerResourceId) || !isNonEmptyString(snapshot.providerStatus) || !isIsoTimestamp(snapshot.occurredAt) || !Array.isArray(snapshot.normalizedReasonCodes) || snapshot.normalizedReasonCodes.some((code) => !isNonEmptyString(code)) || !isSafeMetadata(snapshot.safeMetadata)) {
654
+ malformed("The adapter returned a malformed normalized snapshot.");
655
+ }
656
+ assertCanonicalStatus(snapshot.canonicalStatus);
657
+ assertProviderTypesDoNotEscape(snapshot);
658
+ }
659
+ function assertRedactionResult(result) {
660
+ if (!result || typeof result !== "object" || typeof result.completed !== "boolean" || typeof result.retryable !== "boolean") {
661
+ malformed("Adapter returned an invalid redaction result.");
662
+ }
663
+ }
664
+ function assertCanonicalStatus(value) {
665
+ if (!isCanonicalStatus(value)) {
666
+ throw new ProviderError("UNKNOWN_PROVIDER_STATE", "Adapter returned an unknown canonical status.", {
667
+ safeCode: "unknown_provider_status"
668
+ });
669
+ }
670
+ }
671
+ function assertProviderTypesDoNotEscape(value) {
672
+ const serialized = JSON.stringify(value, (_key, nested) => nested instanceof Uint8Array ? void 0 : nested);
673
+ if (/"(?:access_token|api_key|client_secret|document_number|raw_payload|ssn|template_id|verification_session|webhook_secret|inquiry_status|document_front|selfie)"/i.test(serialized)) {
674
+ throw new Error("Provider SDK or raw provider fields escaped the adapter boundary.");
675
+ }
676
+ if (metadataContainsForbiddenIdentifier(value)) {
677
+ throw new Error("Government identifiers escaped the adapter boundary.");
678
+ }
679
+ }
680
+ function resourceCommand(command, providerResourceId) {
681
+ return {
682
+ attemptId: command.attemptId,
683
+ providerResourceId,
684
+ configurationRevision: command.configurationRevision,
685
+ requestOrigin: command.requestOrigin
686
+ };
687
+ }
688
+ function redactionCommand(command, providerResourceId) {
689
+ return {
690
+ subjectReference: command.subjectReference,
691
+ providerResourceId,
692
+ requestReference: command.idempotencyKey
693
+ };
694
+ }
695
+ function isNonEmptyString(value) {
696
+ return typeof value === "string" && value.trim().length > 0;
697
+ }
698
+ function isIsoTimestamp(value) {
699
+ return isNonEmptyString(value) && !Number.isNaN(Date.parse(value));
700
+ }
701
+ function isSafeMetadata(value) {
702
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((entry) => entry === null || ["string", "boolean", "number"].includes(typeof entry));
703
+ }
704
+ function malformed(message) {
705
+ throw new ProviderError("RETRYABLE_PROVIDER_FAILURE", message, {
706
+ retryable: true,
707
+ safeCode: "malformed_provider_response"
708
+ });
709
+ }
710
+ function isProviderErrorLike(error) {
711
+ if (error instanceof ProviderError) return true;
712
+ if (!error || typeof error !== "object") return false;
713
+ const candidate = error;
714
+ return candidate.name === "ProviderError" && typeof candidate.code === "string" && typeof candidate.safeCode === "string" && typeof candidate.retryable === "boolean";
715
+ }
716
+ function describeError(error) {
717
+ return error instanceof Error ? `${error.name}: ${error.message}` : "a non-Error value";
718
+ }
719
+
720
+ exports.assertProviderTypesDoNotEscape = assertProviderTypesDoNotEscape;
721
+ exports.canonicalStatusRank = canonicalStatusRank;
722
+ exports.providerConformanceScenarios = providerConformanceScenarios;
723
+ exports.runAdapterConformance = runAdapterConformance;
724
+ exports.runAdapterConformanceScenarios = runAdapterConformanceScenarios;
725
+ exports.validateManifestOnly = validateManifestOnly;
726
+ //# sourceMappingURL=conformance.cjs.map
727
+ //# sourceMappingURL=conformance.cjs.map