@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.
package/dist/index.js ADDED
@@ -0,0 +1,1124 @@
1
+ import { Ajv2020 } from 'ajv/dist/2020.js';
2
+
3
+ // src/identifiers.ts
4
+ var VERIFICATION_ADAPTER_CONTRACT_VERSION = "1.0.0";
5
+ var ENGINE_CONTRACT_VERSION = "1.0.0";
6
+ var PROVIDER_MANIFEST_SCHEMA_URN = "urn:splitin:verification:provider-manifest:v1";
7
+ var STANDARD_PACKAGE_CODES = [
8
+ "human_idv",
9
+ "business_kyb",
10
+ "associated_person_idv",
11
+ "ownership_review"
12
+ ];
13
+ var STANDARD_WEBHOOK_PROTOCOLS = [
14
+ "none",
15
+ "stripe_v1_hmac",
16
+ "persona_hmac_sha256",
17
+ "plaid_es256_jwk"
18
+ ];
19
+ var CANONICAL_STATUSES = [
20
+ "created",
21
+ "pending_user_input",
22
+ "paused",
23
+ "processing",
24
+ "manual_review_required",
25
+ "verified",
26
+ "declined",
27
+ "failed",
28
+ "expired",
29
+ "canceled",
30
+ "provider_unavailable",
31
+ "redacted"
32
+ ];
33
+ var TERMINAL_STATUSES = [
34
+ "verified",
35
+ "declined",
36
+ "failed",
37
+ "expired",
38
+ "canceled",
39
+ "redacted"
40
+ ];
41
+ var LAUNCH_PRESENTATIONS = ["embedded", "hosted", "qr", "none"];
42
+ var PROVIDER_ENVIRONMENTS = ["sandbox", "production"];
43
+ var PROVIDER_OPERATIONS = [
44
+ "create",
45
+ "resume",
46
+ "retrieve",
47
+ "retry",
48
+ "cancel",
49
+ "redact",
50
+ "webhook_verify",
51
+ "webhook_normalize",
52
+ "health"
53
+ ];
54
+ var STANDARD_RELATIONSHIP_KINDS = [
55
+ "ubo",
56
+ "director",
57
+ "officer",
58
+ "authorized_representative",
59
+ "associated_person"
60
+ ];
61
+ var STANDARD_PACKAGE_SET = new Set(STANDARD_PACKAGE_CODES);
62
+ var STANDARD_STATUS_SET = new Set(CANONICAL_STATUSES);
63
+ var TERMINAL_STATUS_SET = new Set(TERMINAL_STATUSES);
64
+ var PROVIDER_CODE = /^[a-z][a-z0-9_]{1,63}$/;
65
+ var LAUNCHER_KEY = /^[a-z][a-z0-9_]{1,63}$/;
66
+ var CUSTOM_PACKAGE = /^[a-z0-9][a-z0-9-]{0,32}(?:\.[a-z0-9][a-z0-9_-]{0,63}){1,6}$/;
67
+ 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})$/;
68
+ var RESOURCE_TYPE = /^[a-z][a-z0-9_]{1,63}$/;
69
+ var COUNTRY = /^[A-Z]{2}$/;
70
+ var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
71
+ var OPAQUE_SUBJECT = /^[A-Za-z0-9._:~-]{8,256}$/;
72
+ var GOVERNMENT_ID = /\b(?:ssn|itin|nino|sin|aadhaar|passport|national[_-]?id|tax[_-]?id|ein|ssn_last4)\b/i;
73
+ var SSN_LIKE = /\b\d{3}-\d{2}-\d{4}\b/;
74
+ function isProviderCode(value) {
75
+ return PROVIDER_CODE.test(value);
76
+ }
77
+ function isLauncherKey(value) {
78
+ return LAUNCHER_KEY.test(value);
79
+ }
80
+ function isStandardPackageCode(value) {
81
+ return STANDARD_PACKAGE_SET.has(value);
82
+ }
83
+ function isCustomPackageCode(value) {
84
+ return CUSTOM_PACKAGE.test(value) && !STANDARD_PACKAGE_SET.has(value);
85
+ }
86
+ function isPackageCode(value) {
87
+ return isStandardPackageCode(value) || isCustomPackageCode(value);
88
+ }
89
+ function assertPackageCode(value) {
90
+ if (isStandardPackageCode(value) || isCustomPackageCode(value)) return value;
91
+ throw new Error("Unsupported verification package identifier.");
92
+ }
93
+ function isWebhookProtocol(value) {
94
+ return WEBHOOK_PROTOCOL.test(value);
95
+ }
96
+ function isResourceType(value) {
97
+ return RESOURCE_TYPE.test(value);
98
+ }
99
+ function isCountryCode(value) {
100
+ return COUNTRY.test(value);
101
+ }
102
+ function isSemver(value) {
103
+ return SEMVER.test(value);
104
+ }
105
+ function isCanonicalStatus(value) {
106
+ return STANDARD_STATUS_SET.has(value);
107
+ }
108
+ function isTerminalStatus(value) {
109
+ return TERMINAL_STATUS_SET.has(value);
110
+ }
111
+ function isOpaqueSubjectReference(value) {
112
+ return OPAQUE_SUBJECT.test(value) && !GOVERNMENT_ID.test(value) && !SSN_LIKE.test(value);
113
+ }
114
+ function metadataContainsForbiddenIdentifier(value) {
115
+ if (value == null) return false;
116
+ if (typeof value === "string") return GOVERNMENT_ID.test(value);
117
+ if (Array.isArray(value)) return value.some(metadataContainsForbiddenIdentifier);
118
+ if (typeof value === "object") {
119
+ return Object.entries(value).some(([key, nested]) => GOVERNMENT_ID.test(key) || metadataContainsForbiddenIdentifier(nested));
120
+ }
121
+ return false;
122
+ }
123
+ function compareSemver(left, right) {
124
+ const parse = (value) => value.split("-")[0].split(".").map((part) => Number.parseInt(part, 10));
125
+ const a = parse(left);
126
+ const b = parse(right);
127
+ for (let index = 0; index < 3; index += 1) {
128
+ const delta = (a[index] ?? 0) - (b[index] ?? 0);
129
+ if (delta !== 0) return delta;
130
+ }
131
+ return 0;
132
+ }
133
+ function majorsCompatible(left, right) {
134
+ return left.split(".")[0] === right.split(".")[0];
135
+ }
136
+
137
+ // src/errors.ts
138
+ var ProviderError = class extends Error {
139
+ constructor(code, message, options = {}) {
140
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
141
+ this.code = code;
142
+ this.name = "ProviderError";
143
+ this.retryable = options.retryable ?? false;
144
+ this.safeCode = options.safeCode ?? code.toLowerCase();
145
+ this.retryAfterSeconds = options.retryAfterSeconds;
146
+ }
147
+ code;
148
+ retryable;
149
+ safeCode;
150
+ retryAfterSeconds;
151
+ };
152
+ var ProviderUnavailableError = class extends ProviderError {
153
+ constructor(message = "No eligible verification provider is available.", options = {}) {
154
+ super("PROVIDER_UNAVAILABLE", message, { retryable: true, ...options, safeCode: options.safeCode ?? "provider_unavailable" });
155
+ this.name = "ProviderUnavailableError";
156
+ }
157
+ };
158
+ var ProviderOperationPendingError = class extends ProviderError {
159
+ constructor(message = "The verification provider operation is already in progress.") {
160
+ super("RETRYABLE_PROVIDER_FAILURE", message, { retryable: true, safeCode: "operation_pending" });
161
+ this.name = "ProviderOperationPendingError";
162
+ }
163
+ };
164
+ var VerificationAttemptLimitError = class extends ProviderError {
165
+ constructor(retryAfterSeconds) {
166
+ super("RATE_LIMITED", "The verification session allowance has been reached.", {
167
+ retryable: true,
168
+ safeCode: "attempt_limit",
169
+ retryAfterSeconds
170
+ });
171
+ this.name = "VerificationAttemptLimitError";
172
+ }
173
+ };
174
+ var ProviderRequiredInformationError = class extends ProviderError {
175
+ constructor(message = "Required verification information is missing.") {
176
+ super("TERMINAL_INPUT_FAILURE", message, { retryable: false, safeCode: "required_information_missing" });
177
+ this.name = "ProviderRequiredInformationError";
178
+ }
179
+ };
180
+ function toSafeProviderFailure(error) {
181
+ if (error instanceof ProviderError) {
182
+ return {
183
+ code: error.code,
184
+ safeCode: error.safeCode,
185
+ retryable: error.retryable,
186
+ retryAfterSeconds: error.retryAfterSeconds
187
+ };
188
+ }
189
+ return {
190
+ code: "RETRYABLE_PROVIDER_FAILURE",
191
+ safeCode: "unexpected_provider_failure",
192
+ retryable: true
193
+ };
194
+ }
195
+
196
+ // src/schema.ts
197
+ var providerManifestV1JsonSchema = {
198
+ $schema: "https://json-schema.org/draft/2020-12/schema",
199
+ $id: PROVIDER_MANIFEST_SCHEMA_URN,
200
+ title: "Verification Provider Manifest V1",
201
+ type: "object",
202
+ additionalProperties: false,
203
+ required: [
204
+ "contractVersion",
205
+ "adapterVersion",
206
+ "engineCompatibility",
207
+ "provider",
208
+ "displayName",
209
+ "supportedPackages",
210
+ "supportedCountries",
211
+ "environments",
212
+ "capabilities",
213
+ "launcherKeys",
214
+ "launchPresentations",
215
+ "configurationSchemaVersion",
216
+ "configurationSchema",
217
+ "webhook",
218
+ "dataPolicy",
219
+ "retry",
220
+ "cancellation",
221
+ "redaction",
222
+ "apiHosts",
223
+ "testedApiVersions"
224
+ ],
225
+ properties: {
226
+ contractVersion: { const: "1.0.0" },
227
+ adapterVersion: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
228
+ engineCompatibility: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
229
+ provider: { type: "string", pattern: "^[a-z][a-z0-9_]{1,63}$" },
230
+ displayName: { type: "string", minLength: 1 },
231
+ description: { type: "string" },
232
+ supportedPackages: {
233
+ type: "array",
234
+ minItems: 1,
235
+ uniqueItems: true,
236
+ items: {
237
+ type: "string",
238
+ anyOf: [
239
+ { enum: ["human_idv", "business_kyb", "associated_person_idv", "ownership_review"] },
240
+ { pattern: "^[a-z0-9][a-z0-9-]{0,32}(?:\\.[a-z0-9][a-z0-9_-]{0,63}){1,6}$" }
241
+ ]
242
+ }
243
+ },
244
+ supportedCountries: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", pattern: "^[A-Z]{2}$" } },
245
+ environments: { type: "array", minItems: 1, uniqueItems: true, items: { enum: ["sandbox", "production"] } },
246
+ capabilities: {
247
+ type: "object",
248
+ additionalProperties: false,
249
+ required: ["presentations", "canResume", "canRetry", "canCancel", "canRedact"],
250
+ properties: {
251
+ presentations: { type: "array", minItems: 1, uniqueItems: true, items: { enum: ["embedded", "hosted", "qr", "none"] } },
252
+ canResume: { type: "boolean" },
253
+ canRetry: { type: "boolean" },
254
+ canCancel: { type: "boolean" },
255
+ canRedact: { type: "boolean" }
256
+ }
257
+ },
258
+ launcherKeys: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", pattern: "^[a-z][a-z0-9_]{1,63}$" } },
259
+ launchPresentations: { type: "array", minItems: 1, uniqueItems: true, items: { enum: ["embedded", "hosted", "qr", "none"] } },
260
+ configurationSchemaVersion: { type: "string", minLength: 1 },
261
+ configurationSchema: { $ref: "#/$defs/jsonSchema2020" },
262
+ webhook: {
263
+ type: "object",
264
+ additionalProperties: false,
265
+ required: ["protocol", "eventFamilies"],
266
+ properties: {
267
+ protocol: { type: "string", minLength: 1, maxLength: 128 },
268
+ eventFamilies: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 } },
269
+ toleranceSeconds: { type: "integer", minimum: 1 }
270
+ }
271
+ },
272
+ dataPolicy: {
273
+ type: "object",
274
+ additionalProperties: false,
275
+ required: [
276
+ "classifications",
277
+ "prohibitedPersistence",
278
+ "rawPayloadPersistence",
279
+ "browserSecretPersistence",
280
+ "governmentIdentifierPersistence"
281
+ ],
282
+ properties: {
283
+ classifications: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 } },
284
+ prohibitedPersistence: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 } },
285
+ rawPayloadPersistence: { const: false },
286
+ browserSecretPersistence: { const: false },
287
+ governmentIdentifierPersistence: { const: false }
288
+ }
289
+ },
290
+ retry: {
291
+ type: "object",
292
+ additionalProperties: false,
293
+ required: ["sameResourceWhenResumable", "newAttemptAfterTerminal"],
294
+ properties: {
295
+ sameResourceWhenResumable: { type: "boolean" },
296
+ newAttemptAfterTerminal: { type: "boolean" }
297
+ }
298
+ },
299
+ cancellation: {
300
+ type: "object",
301
+ additionalProperties: false,
302
+ required: ["supported", "terminal"],
303
+ properties: { supported: { type: "boolean" }, terminal: { type: "boolean" } }
304
+ },
305
+ redaction: {
306
+ type: "object",
307
+ additionalProperties: false,
308
+ required: ["supported", "asynchronous"],
309
+ properties: {
310
+ supported: { type: "boolean" },
311
+ asynchronous: { type: "boolean" },
312
+ notApplicable: { type: "boolean" }
313
+ }
314
+ },
315
+ apiHosts: {
316
+ type: "array",
317
+ minItems: 1,
318
+ uniqueItems: true,
319
+ items: { type: "string", pattern: "^[a-z0-9.-]+$" }
320
+ },
321
+ testedApiVersions: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", minLength: 1 } }
322
+ },
323
+ $defs: {
324
+ jsonSchema2020: {
325
+ type: "object",
326
+ required: ["$schema", "type", "additionalProperties", "required", "properties"],
327
+ properties: {
328
+ $schema: { const: "https://json-schema.org/draft/2020-12/schema" },
329
+ type: { const: "object" },
330
+ additionalProperties: { const: false },
331
+ required: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 } },
332
+ properties: { type: "object" }
333
+ }
334
+ }
335
+ }
336
+ };
337
+ var emptyConfigurationSchema = {
338
+ $schema: "https://json-schema.org/draft/2020-12/schema",
339
+ type: "object",
340
+ additionalProperties: false,
341
+ required: [],
342
+ properties: {}
343
+ };
344
+ function secretStringProperty(minLength = 1) {
345
+ return { type: "string", minLength, "x-secret": true };
346
+ }
347
+ function plainStringProperty(minLength = 1) {
348
+ return { type: "string", minLength, "x-secret": false };
349
+ }
350
+ var manifestValidator = new Ajv2020({ allErrors: true, strict: false }).compile(providerManifestV1JsonSchema);
351
+ var REQUIRED_ADAPTER_METHODS = [
352
+ "validateConfiguration",
353
+ "createAttempt",
354
+ "resumeAttempt",
355
+ "retrieveAttempt",
356
+ "retryAttempt",
357
+ "cancelAttempt",
358
+ "redactSubject",
359
+ "verifyWebhook",
360
+ "normalizeWebhook"
361
+ ];
362
+ function defineProviderManifest(input) {
363
+ assertProviderManifest(input);
364
+ return deepFreeze(JSON.parse(JSON.stringify(input)));
365
+ }
366
+ function assertProviderManifest(manifest) {
367
+ if (!manifestValidator(manifest)) {
368
+ const first = manifestValidator.errors?.[0];
369
+ invalid(
370
+ `json_schema_${toSafeCode(first?.keyword ?? "invalid")}`,
371
+ `The provider adapter manifest does not satisfy ProviderManifestV1 JSON Schema at ${first?.instancePath || "/"}.`
372
+ );
373
+ }
374
+ if (manifest.contractVersion !== VERIFICATION_ADAPTER_CONTRACT_VERSION) invalid("contract_version");
375
+ if (!isProviderCode(manifest.provider)) invalid("provider_code");
376
+ if (!manifest.displayName.trim() || !isSemver(manifest.adapterVersion) || !isSemver(manifest.engineCompatibility)) {
377
+ invalid("identity");
378
+ }
379
+ if (!majorsCompatible(manifest.adapterVersion, VERIFICATION_ADAPTER_CONTRACT_VERSION)) {
380
+ invalid("adapter_version_incompatible", "Provider adapter major version must be compatible with contract V1.");
381
+ }
382
+ if (!majorsCompatible(manifest.engineCompatibility, ENGINE_CONTRACT_VERSION)) {
383
+ invalid("engine_incompatible", "Provider adapter is not compatible with this engine major version.");
384
+ }
385
+ if (!manifest.supportedPackages.length || manifest.supportedPackages.some((value) => !isPackageCode(value))) {
386
+ invalid("supported_packages");
387
+ }
388
+ if (manifest.supportedPackages.some((value) => !isStandardPackageCode(value) && !isCustomPackageCode(value))) {
389
+ invalid("supported_packages");
390
+ }
391
+ if (!manifest.supportedCountries.length || manifest.supportedCountries.some((value) => !isCountryCode(value))) {
392
+ invalid("supported_countries");
393
+ }
394
+ if (!manifest.launcherKeys.length || manifest.launcherKeys.some((value) => !isLauncherKey(value))) {
395
+ invalid("launcher_keys");
396
+ }
397
+ if (!isWebhookProtocol(manifest.webhook.protocol)) invalid("webhook_protocol");
398
+ if (!manifest.apiHosts.length || manifest.apiHosts.some((host) => host.includes("/") || host.includes(":"))) {
399
+ invalid("api_hosts", "Provider API hosts must be code-owned hostnames, not URLs or caller-supplied origins.");
400
+ }
401
+ if (manifest.dataPolicy.rawPayloadPersistence !== false || manifest.dataPolicy.browserSecretPersistence !== false || manifest.dataPolicy.governmentIdentifierPersistence !== false) {
402
+ invalid("data_policy");
403
+ }
404
+ if (manifest.capabilities.canCancel !== manifest.cancellation.supported) invalid("cancellation_capability");
405
+ if (manifest.capabilities.canRedact !== manifest.redaction.supported) invalid("redaction_capability");
406
+ assertConfigurationSecrets(manifest);
407
+ }
408
+ function assertConfigurationSecrets(manifest) {
409
+ const properties = manifest.configurationSchema.properties ?? {};
410
+ for (const [name, schema] of Object.entries(properties)) {
411
+ if (schema["x-secret"] === true && schema.type !== "string") {
412
+ invalid("secret_schema", `Configuration field "${name}" marked x-secret must be a string.`);
413
+ }
414
+ }
415
+ }
416
+ function assertAdapterConformsToManifest(adapter) {
417
+ const candidate = adapter;
418
+ const provider = typeof candidate.provider === "string" && candidate.provider ? candidate.provider : "unknown";
419
+ for (const method of REQUIRED_ADAPTER_METHODS) {
420
+ if (typeof candidate[method] !== "function") {
421
+ invalid(
422
+ `missing_method_${toSafeCode(method)}`,
423
+ `Provider adapter "${provider}" is missing required method "${method}".`
424
+ );
425
+ }
426
+ }
427
+ if (!candidate.manifest || typeof candidate.manifest !== "object") {
428
+ invalid("missing_manifest", `Provider adapter "${provider}" is missing its manifest.`);
429
+ }
430
+ assertProviderManifest(adapter.manifest);
431
+ if (adapter.contractVersion !== VERIFICATION_ADAPTER_CONTRACT_VERSION || adapter.provider !== adapter.manifest.provider || !adapter.manifest.environments.includes(adapter.environment)) {
432
+ invalid("adapter_identity", `Provider adapter "${provider}" does not match its manifest identity or environment.`);
433
+ }
434
+ if (adapter.manifest.capabilities.canResume === false && typeof candidate.resumeAttempt !== "function") {
435
+ invalid("resume_capability");
436
+ }
437
+ adapter.validateConfiguration();
438
+ }
439
+ function createAllowlistedHttp(allowedHosts, fetchImpl, timeoutMs = 1e4) {
440
+ const hosts = new Set(allowedHosts.map((host) => host.toLowerCase()));
441
+ return {
442
+ async fetch(input, init) {
443
+ const url = new URL(typeof input === "string" || input instanceof URL ? String(input) : input.url);
444
+ if (url.protocol !== "https:" && url.hostname !== "127.0.0.1" && url.hostname !== "localhost") {
445
+ throw new ProviderError("INVALID_CONFIGURATION", "Provider HTTP is limited to HTTPS or loopback.", {
446
+ safeCode: "http_origin_denied"
447
+ });
448
+ }
449
+ if (!hosts.has(url.hostname.toLowerCase())) {
450
+ throw new ProviderError("INVALID_CONFIGURATION", "Provider HTTP origin is not allowlisted in the adapter manifest.", {
451
+ safeCode: "http_origin_denied"
452
+ });
453
+ }
454
+ const controller = new AbortController();
455
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
456
+ try {
457
+ const parentSignal = init?.signal;
458
+ if (parentSignal) {
459
+ if (parentSignal.aborted) controller.abort();
460
+ else parentSignal.addEventListener("abort", () => controller.abort(), { once: true });
461
+ }
462
+ return await fetchImpl(input, { ...init, signal: controller.signal });
463
+ } catch (error) {
464
+ if (controller.signal.aborted) {
465
+ throw new ProviderError("TIMEOUT", "The provider HTTP request timed out.", { retryable: true, safeCode: "timeout" });
466
+ }
467
+ throw error;
468
+ } finally {
469
+ clearTimeout(timeout);
470
+ }
471
+ }
472
+ };
473
+ }
474
+ function createDefaultRuntime(environment, configuration, options = {}) {
475
+ const fetchImpl = options.http?.fetch ?? fetch;
476
+ return {
477
+ environment,
478
+ configuration: Object.freeze({ ...configuration }),
479
+ http: options.http ?? createAllowlistedHttp(options.allowedHosts ?? ["127.0.0.1"], fetchImpl),
480
+ now: options.now ?? (() => /* @__PURE__ */ new Date()),
481
+ crypto: options.crypto ?? globalThis.crypto,
482
+ idempotency: options.idempotency ?? {
483
+ keyFor: (operation, attemptId, suppliedKey) => suppliedKey ?? `${operation}:${attemptId}`
484
+ },
485
+ logger: options.logger ?? silentLogger,
486
+ telemetry: options.telemetry,
487
+ recordHealth: options.recordHealth ?? (async () => void 0),
488
+ rateBudget: options.rateBudget
489
+ };
490
+ }
491
+ var silentLogger = Object.freeze({
492
+ info: () => void 0,
493
+ warn: () => void 0,
494
+ error: () => void 0
495
+ });
496
+ function invalid(safeCode, message = "The provider adapter manifest is invalid.") {
497
+ throw new ProviderError("INVALID_CONFIGURATION", message, { safeCode });
498
+ }
499
+ function toSafeCode(value) {
500
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
501
+ }
502
+ function deepFreeze(value) {
503
+ if (value && typeof value === "object") {
504
+ Object.freeze(value);
505
+ for (const nested of Object.values(value)) deepFreeze(nested);
506
+ }
507
+ return value;
508
+ }
509
+
510
+ // src/conformance.ts
511
+ var CANONICAL_STATUS_RANK = {
512
+ created: 10,
513
+ pending_user_input: 20,
514
+ paused: 20,
515
+ provider_unavailable: 25,
516
+ processing: 30,
517
+ manual_review_required: 40,
518
+ verified: 100,
519
+ declined: 100,
520
+ failed: 100,
521
+ expired: 100,
522
+ canceled: 100,
523
+ redacted: 200
524
+ };
525
+ function canonicalStatusRank(status) {
526
+ return CANONICAL_STATUS_RANK[status];
527
+ }
528
+ var providerConformanceScenarios = Object.freeze([
529
+ "success",
530
+ "input_required",
531
+ "processing",
532
+ "verified",
533
+ "decline",
534
+ "failure",
535
+ "manual_review",
536
+ "timeout",
537
+ "rate_limit",
538
+ "malformed_response",
539
+ "unknown_state",
540
+ "cancellation",
541
+ "resume",
542
+ "asynchronous_redaction",
543
+ "retryable_provider_failure"
544
+ ]);
545
+ async function runAdapterConformance(adapter, command, options = {}) {
546
+ const results = [];
547
+ const manifest = capture("manifest", () => assertAdapterConformsToManifest(adapter));
548
+ results.push(manifest);
549
+ if (!manifest.passed) return results;
550
+ results.push(capture("package", () => {
551
+ if (!adapter.manifest.supportedPackages.includes(command.packageCode)) {
552
+ throw new Error("The fixture package is not declared by the adapter.");
553
+ }
554
+ if (metadataContainsForbiddenIdentifier(command.metadata) || !command.subjectReference) {
555
+ throw new Error("Attempt command contains forbidden identifiers or a missing opaque subject.");
556
+ }
557
+ }));
558
+ let created = null;
559
+ try {
560
+ created = await adapter.createAttempt(command);
561
+ assertAttemptResult(adapter, created);
562
+ results.push({ name: "create", passed: true });
563
+ } catch (error) {
564
+ results.push(failure("create", error));
565
+ }
566
+ if (created) {
567
+ try {
568
+ const duplicate = await adapter.createAttempt(command);
569
+ if (duplicate.providerResourceId !== created.providerResourceId) {
570
+ throw new Error("Duplicate create produced a second provider resource.");
571
+ }
572
+ results.push({ name: "create_idempotency", passed: true });
573
+ } catch (error) {
574
+ results.push(failure("create_idempotency", error));
575
+ }
576
+ if (adapter.manifest.capabilities.canResume) {
577
+ try {
578
+ const resumed = await adapter.resumeAttempt(resourceCommand(command, created.providerResourceId));
579
+ assertLaunchResult(adapter, resumed);
580
+ results.push({ name: "resume", passed: true });
581
+ } catch (error) {
582
+ results.push(failure("resume", error));
583
+ }
584
+ } else {
585
+ results.push(skipped("resume", "canResume"));
586
+ }
587
+ try {
588
+ const snapshot = await adapter.retrieveAttempt(resourceCommand(command, created.providerResourceId));
589
+ assertNormalizedSnapshot(snapshot);
590
+ results.push({ name: "retrieve", passed: true });
591
+ try {
592
+ if (isTerminalStatus(snapshot.canonicalStatus)) {
593
+ const second = await adapter.retrieveAttempt(resourceCommand(command, created.providerResourceId));
594
+ assertNormalizedSnapshot(second);
595
+ const firstRank = canonicalStatusRank(snapshot.canonicalStatus);
596
+ const secondRank = canonicalStatusRank(second.canonicalStatus);
597
+ if (!isTerminalStatus(second.canonicalStatus) && secondRank < firstRank) {
598
+ throw new Error("A terminal snapshot must not regress to a non-terminal status with lower rank.");
599
+ }
600
+ }
601
+ results.push({ name: "terminal_monotonicity", passed: true });
602
+ } catch (error) {
603
+ results.push(failure("terminal_monotonicity", error));
604
+ }
605
+ } catch (error) {
606
+ results.push(failure("retrieve", error));
607
+ }
608
+ if (adapter.manifest.capabilities.canRetry) {
609
+ try {
610
+ const retried = await adapter.retryAttempt({ ...command, previousProviderResourceId: created.providerResourceId });
611
+ assertAttemptResult(adapter, retried);
612
+ results.push({ name: "retry", passed: true });
613
+ } catch (error) {
614
+ results.push(failure("retry", error));
615
+ }
616
+ } else {
617
+ results.push(skipped("retry", "canRetry"));
618
+ }
619
+ if (adapter.manifest.capabilities.canCancel) {
620
+ try {
621
+ const canceled = await adapter.cancelAttempt(resourceCommand(command, created.providerResourceId));
622
+ if (!canceled.accepted) throw new Error("Adapter did not acknowledge cancellation.");
623
+ results.push({ name: "cancel", passed: true });
624
+ } catch (error) {
625
+ results.push(failure("cancel", error));
626
+ }
627
+ } else {
628
+ results.push(skipped("cancel", "canCancel"));
629
+ }
630
+ if (adapter.manifest.capabilities.canRedact) {
631
+ try {
632
+ const redaction = await adapter.redactSubject(redactionCommand(command, created.providerResourceId));
633
+ assertRedactionResult(redaction);
634
+ results.push({ name: "redact", passed: true });
635
+ } catch (error) {
636
+ results.push(failure("redact", error));
637
+ }
638
+ } else {
639
+ results.push(skipped("redact", "canRedact"));
640
+ }
641
+ }
642
+ if (options.webhookRequest) {
643
+ try {
644
+ const verified = await adapter.verifyWebhook(options.webhookRequest);
645
+ const normalized = await adapter.normalizeWebhook(verified);
646
+ if (!verified.providerEventKey || !verified.receivedAt || !verified.bodySha256 || !(verified.opaquePayload instanceof Uint8Array)) {
647
+ throw new Error("The verified webhook envelope is incomplete.");
648
+ }
649
+ if (!normalized.providerEventKey || !normalized.providerResourceId || !normalized.eventType || !normalized.providerEventType || !normalized.occurredAt) {
650
+ throw new Error("The normalized webhook event is incomplete.");
651
+ }
652
+ if (normalized.canonicalStatus) assertCanonicalStatus(normalized.canonicalStatus);
653
+ assertProviderTypesDoNotEscape(normalized);
654
+ results.push({ name: "webhook", passed: true });
655
+ } catch (error) {
656
+ results.push(failure("webhook", error));
657
+ }
658
+ }
659
+ return results;
660
+ }
661
+ async function runAdapterConformanceScenarios(adapterForScenario, command) {
662
+ const results = [];
663
+ for (const scenario of providerConformanceScenarios) {
664
+ try {
665
+ const adapter = await adapterForScenario(scenario);
666
+ assertAdapterConformsToManifest(adapter);
667
+ if (!adapter.manifest.supportedPackages.includes(command.packageCode)) {
668
+ throw new Error(`Scenario "${scenario}" uses a package that the adapter does not declare.`);
669
+ }
670
+ await executeScenario(adapter, command, scenario);
671
+ results.push({ name: scenario, passed: true });
672
+ } catch (error) {
673
+ results.push(failure(scenario, error));
674
+ }
675
+ }
676
+ return results;
677
+ }
678
+ function validateManifestOnly(manifest) {
679
+ return capture("manifest", () => assertProviderManifest(manifest));
680
+ }
681
+ function capture(name, operation) {
682
+ try {
683
+ operation();
684
+ return { name, passed: true };
685
+ } catch (error) {
686
+ return failure(name, error);
687
+ }
688
+ }
689
+ function failure(name, error) {
690
+ return { name, passed: false, detail: error instanceof Error ? error.message : "Unknown conformance failure." };
691
+ }
692
+ function skipped(name, capability) {
693
+ return { name, passed: true, detail: `Skipped because the manifest declares ${capability}=false.` };
694
+ }
695
+ async function executeScenario(adapter, command, scenario) {
696
+ const providerResourceId = `conformance_${command.attemptId.replace(/[^a-zA-Z0-9]/g, "")}`;
697
+ const resource = resourceCommand(command, providerResourceId);
698
+ switch (scenario) {
699
+ case "success":
700
+ case "verified": {
701
+ const created = await adapter.createAttempt(command);
702
+ assertAttemptResult(adapter, created);
703
+ const snapshot = await adapter.retrieveAttempt(resourceCommand(command, created.providerResourceId));
704
+ assertNormalizedSnapshot(snapshot);
705
+ if (snapshot.canonicalStatus !== "verified") {
706
+ throw new Error('The success scenario must normalize to canonical status "verified".');
707
+ }
708
+ return;
709
+ }
710
+ case "input_required":
711
+ await expectCanonicalSnapshot(adapter, resource, "pending_user_input");
712
+ return;
713
+ case "processing":
714
+ await expectCanonicalSnapshot(adapter, resource, "processing");
715
+ return;
716
+ case "decline":
717
+ await expectCanonicalSnapshot(adapter, resource, "declined");
718
+ return;
719
+ case "failure":
720
+ await expectCanonicalSnapshot(adapter, resource, "failed");
721
+ return;
722
+ case "manual_review":
723
+ await expectCanonicalSnapshot(adapter, resource, "manual_review_required");
724
+ return;
725
+ case "timeout":
726
+ await expectProviderError(() => adapter.retrieveAttempt(resource), "TIMEOUT", { retryable: true });
727
+ return;
728
+ case "rate_limit":
729
+ await expectProviderError(() => adapter.retrieveAttempt(resource), "RATE_LIMITED", { retryable: true, retryAfterRequired: true });
730
+ return;
731
+ case "retryable_provider_failure":
732
+ await expectProviderError(() => adapter.retrieveAttempt(resource), "RETRYABLE_PROVIDER_FAILURE", { retryable: true });
733
+ return;
734
+ case "malformed_response":
735
+ await expectRejectedSnapshot(adapter, resource, "malformed_provider_response");
736
+ return;
737
+ case "unknown_state":
738
+ await expectRejectedSnapshot(adapter, resource, "unknown_provider_status");
739
+ return;
740
+ case "cancellation": {
741
+ const canceled = await adapter.cancelAttempt(resource);
742
+ if (!canceled.accepted) throw new Error("Cancellation was not accepted.");
743
+ return;
744
+ }
745
+ case "resume": {
746
+ const launched = await adapter.resumeAttempt(resource);
747
+ assertLaunchResult(adapter, launched);
748
+ return;
749
+ }
750
+ case "asynchronous_redaction": {
751
+ if (!adapter.manifest.capabilities.canRedact) {
752
+ throw new Error('The redaction scenario requires manifest capability "canRedact".');
753
+ }
754
+ const redaction = await adapter.redactSubject(redactionCommand(command, providerResourceId));
755
+ assertRedactionResult(redaction);
756
+ if (adapter.manifest.redaction.asynchronous) {
757
+ if (redaction.completed && redaction.disposition !== "redacted") {
758
+ throw new Error("Asynchronous redaction must not claim completion until the provider finishes.");
759
+ }
760
+ if (!["processing", "scheduled", "retryable"].includes(redaction.disposition ?? "")) {
761
+ throw new Error("Asynchronous redaction must report processing, scheduled, or retryable.");
762
+ }
763
+ } else if (!redaction.completed || redaction.retryable) {
764
+ throw new Error("Synchronous redaction must complete terminally without retry.");
765
+ }
766
+ }
767
+ }
768
+ }
769
+ async function expectCanonicalSnapshot(adapter, command, expected) {
770
+ const snapshot = await adapter.retrieveAttempt(command);
771
+ assertNormalizedSnapshot(snapshot);
772
+ if (snapshot.canonicalStatus !== expected) {
773
+ throw new Error(`The scenario must normalize to canonical status "${expected}".`);
774
+ }
775
+ }
776
+ async function expectRejectedSnapshot(adapter, command, expectedSafeCode) {
777
+ try {
778
+ const snapshot = await adapter.retrieveAttempt(command);
779
+ assertNormalizedSnapshot(snapshot);
780
+ } catch (error) {
781
+ if (isProviderErrorLike(error) && error.safeCode === expectedSafeCode) return;
782
+ throw new Error(`The scenario must be rejected with safe code "${expectedSafeCode}"; received ${describeError(error)}.`);
783
+ }
784
+ throw new Error(`The scenario was accepted instead of being rejected as "${expectedSafeCode}".`);
785
+ }
786
+ async function expectProviderError(operation, code, expectations) {
787
+ try {
788
+ await operation();
789
+ } catch (error) {
790
+ if (!isProviderErrorLike(error)) {
791
+ throw new Error(`Expected ProviderError ${code}; received ${describeError(error)}.`);
792
+ }
793
+ if (error.code !== code) throw new Error(`Expected ProviderError ${code}; received ${error.code}.`);
794
+ if (error.retryable !== expectations.retryable) {
795
+ throw new Error(`ProviderError ${code} must set retryable=${String(expectations.retryable)}.`);
796
+ }
797
+ if (!error.safeCode.trim()) throw new Error(`ProviderError ${code} must provide a safe code.`);
798
+ if (expectations.retryAfterRequired && (!Number.isSafeInteger(error.retryAfterSeconds) || Number(error.retryAfterSeconds) <= 0)) {
799
+ throw new Error(`ProviderError ${code} must provide a positive integer retryAfterSeconds.`);
800
+ }
801
+ return;
802
+ }
803
+ throw new Error(`Expected ProviderError ${code}, but the adapter operation succeeded.`);
804
+ }
805
+ function assertAttemptResult(adapter, result) {
806
+ if (!result || typeof result !== "object" || !isNonEmptyString(result.providerResourceId) || !isNonEmptyString(result.providerStatus) || result.attemptId == null) {
807
+ malformed("The adapter returned an incomplete attempt result.");
808
+ }
809
+ assertLaunchResult(adapter, result.launch);
810
+ assertProviderTypesDoNotEscape(result);
811
+ }
812
+ function assertLaunchResult(adapter, launch) {
813
+ if (!launch || typeof launch !== "object" || !isNonEmptyString(launch.launcherKey) || !adapter.manifest.launcherKeys.includes(launch.launcherKey) || !adapter.manifest.capabilities.presentations.includes(launch.presentation) || !isCanonicalStatus(launch.canonicalStatus)) {
814
+ malformed("The adapter returned an invalid launch envelope.");
815
+ }
816
+ const forbidden = ["inquiryOrSessionId", "environmentId", "opaqueLaunchSecret", "adapter", "clientSecret"];
817
+ if (forbidden.some((key) => key in launch)) {
818
+ throw new Error("Deprecated or secret launch fields escaped the adapter boundary.");
819
+ }
820
+ }
821
+ function assertNormalizedSnapshot(snapshot) {
822
+ 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)) {
823
+ malformed("The adapter returned a malformed normalized snapshot.");
824
+ }
825
+ assertCanonicalStatus(snapshot.canonicalStatus);
826
+ assertProviderTypesDoNotEscape(snapshot);
827
+ }
828
+ function assertRedactionResult(result) {
829
+ if (!result || typeof result !== "object" || typeof result.completed !== "boolean" || typeof result.retryable !== "boolean") {
830
+ malformed("Adapter returned an invalid redaction result.");
831
+ }
832
+ }
833
+ function assertCanonicalStatus(value) {
834
+ if (!isCanonicalStatus(value)) {
835
+ throw new ProviderError("UNKNOWN_PROVIDER_STATE", "Adapter returned an unknown canonical status.", {
836
+ safeCode: "unknown_provider_status"
837
+ });
838
+ }
839
+ }
840
+ function assertProviderTypesDoNotEscape(value) {
841
+ const serialized = JSON.stringify(value, (_key, nested) => nested instanceof Uint8Array ? void 0 : nested);
842
+ 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)) {
843
+ throw new Error("Provider SDK or raw provider fields escaped the adapter boundary.");
844
+ }
845
+ if (metadataContainsForbiddenIdentifier(value)) {
846
+ throw new Error("Government identifiers escaped the adapter boundary.");
847
+ }
848
+ }
849
+ function resourceCommand(command, providerResourceId) {
850
+ return {
851
+ attemptId: command.attemptId,
852
+ providerResourceId,
853
+ configurationRevision: command.configurationRevision,
854
+ requestOrigin: command.requestOrigin
855
+ };
856
+ }
857
+ function redactionCommand(command, providerResourceId) {
858
+ return {
859
+ subjectReference: command.subjectReference,
860
+ providerResourceId,
861
+ requestReference: command.idempotencyKey
862
+ };
863
+ }
864
+ function isNonEmptyString(value) {
865
+ return typeof value === "string" && value.trim().length > 0;
866
+ }
867
+ function isIsoTimestamp(value) {
868
+ return isNonEmptyString(value) && !Number.isNaN(Date.parse(value));
869
+ }
870
+ function isSafeMetadata(value) {
871
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((entry) => entry === null || ["string", "boolean", "number"].includes(typeof entry));
872
+ }
873
+ function malformed(message) {
874
+ throw new ProviderError("RETRYABLE_PROVIDER_FAILURE", message, {
875
+ retryable: true,
876
+ safeCode: "malformed_provider_response"
877
+ });
878
+ }
879
+ function isProviderErrorLike(error) {
880
+ if (error instanceof ProviderError) return true;
881
+ if (!error || typeof error !== "object") return false;
882
+ const candidate = error;
883
+ return candidate.name === "ProviderError" && typeof candidate.code === "string" && typeof candidate.safeCode === "string" && typeof candidate.retryable === "boolean";
884
+ }
885
+ function describeError(error) {
886
+ return error instanceof Error ? `${error.name}: ${error.message}` : "a non-Error value";
887
+ }
888
+
889
+ // src/fakes.ts
890
+ var fakeProviderManifest = defineProviderManifest({
891
+ contractVersion: VERIFICATION_ADAPTER_CONTRACT_VERSION,
892
+ adapterVersion: "1.0.0",
893
+ engineCompatibility: "1.0.0",
894
+ provider: "test_fake",
895
+ displayName: "Conformance Fake Provider",
896
+ supportedPackages: ["human_idv", "com.example.employee_check"],
897
+ supportedCountries: ["US"],
898
+ environments: ["sandbox"],
899
+ capabilities: { presentations: ["embedded", "hosted"], canResume: true, canRetry: true, canCancel: true, canRedact: true },
900
+ launcherKeys: ["test_embedded", "hosted"],
901
+ launchPresentations: ["embedded", "hosted"],
902
+ configurationSchemaVersion: "urn:splitin:verification:config:test-fake:v1",
903
+ configurationSchema: emptyConfigurationSchema,
904
+ webhook: { protocol: "none", eventFamilies: ["test"] },
905
+ dataPolicy: {
906
+ classifications: ["normalized_status"],
907
+ prohibitedPersistence: ["raw_webhook", "launch_secret", "document", "selfie"],
908
+ rawPayloadPersistence: false,
909
+ browserSecretPersistence: false,
910
+ governmentIdentifierPersistence: false
911
+ },
912
+ retry: { sameResourceWhenResumable: true, newAttemptAfterTerminal: true },
913
+ cancellation: { supported: true, terminal: true },
914
+ redaction: { supported: true, asynchronous: true },
915
+ apiHosts: ["127.0.0.1"],
916
+ testedApiVersions: ["fake-1"]
917
+ });
918
+ var SCENARIO_STATUS = {
919
+ success: "verified",
920
+ verified: "verified",
921
+ input_required: "pending_user_input",
922
+ processing: "processing",
923
+ decline: "declined",
924
+ failure: "failed",
925
+ manual_review: "manual_review_required",
926
+ timeout: "processing",
927
+ rate_limit: "processing",
928
+ malformed_response: "processing",
929
+ unknown_state: "processing",
930
+ cancellation: "canceled",
931
+ resume: "pending_user_input",
932
+ asynchronous_redaction: "processing",
933
+ retryable_provider_failure: "processing"
934
+ };
935
+ var FakeVerificationAdapter = class {
936
+ contractVersion = VERIFICATION_ADAPTER_CONTRACT_VERSION;
937
+ manifest = fakeProviderManifest;
938
+ provider = "test_fake";
939
+ environment = "sandbox";
940
+ runtime;
941
+ scenario;
942
+ resources = /* @__PURE__ */ new Map();
943
+ constructor(scenario = "input_required", runtime) {
944
+ this.scenario = scenario;
945
+ this.runtime = runtime ?? createDefaultRuntime("sandbox", {}, { allowedHosts: ["127.0.0.1"] });
946
+ }
947
+ validateConfiguration() {
948
+ if (this.runtime.environment !== "sandbox") {
949
+ throw new ProviderError("INVALID_CONFIGURATION", "The fake provider is sandbox-only.");
950
+ }
951
+ }
952
+ async createAttempt(command) {
953
+ this.runtime.idempotency.keyFor("create", command.attemptId, command.idempotencyKey);
954
+ const existing = this.resources.get(command.idempotencyKey);
955
+ const providerResourceId = existing ?? `tfr_${command.attemptId.replace(/[^a-zA-Z0-9]/g, "")}`;
956
+ this.resources.set(command.idempotencyKey, providerResourceId);
957
+ const canonicalStatus = this.scenario === "success" || this.scenario === "verified" ? "verified" : "pending_user_input";
958
+ return {
959
+ attemptId: command.attemptId,
960
+ providerResourceId,
961
+ providerStatus: canonicalStatus,
962
+ canonicalStatus,
963
+ launch: this.launch(command.attemptId, canonicalStatus)
964
+ };
965
+ }
966
+ async resumeAttempt(command) {
967
+ return this.launch(command.attemptId, "pending_user_input");
968
+ }
969
+ async retrieveAttempt(command) {
970
+ this.throwIfErrorScenario();
971
+ return {
972
+ providerResourceId: command.providerResourceId,
973
+ providerStatus: SCENARIO_STATUS[this.scenario],
974
+ canonicalStatus: SCENARIO_STATUS[this.scenario],
975
+ occurredAt: this.runtime.now().toISOString(),
976
+ normalizedReasonCodes: [],
977
+ safeMetadata: { source: "fake", scenario: this.scenario }
978
+ };
979
+ }
980
+ async retryAttempt(command) {
981
+ return this.createAttempt({ ...command, attemptId: `${command.attemptId}_retry` });
982
+ }
983
+ async cancelAttempt(_command) {
984
+ return { accepted: true, providerStatus: "canceled", canonicalStatus: "canceled" };
985
+ }
986
+ async redactSubject(_command) {
987
+ if (this.scenario === "asynchronous_redaction") {
988
+ return { completed: false, retryable: true, disposition: "processing" };
989
+ }
990
+ return { completed: true, retryable: false, disposition: "redacted" };
991
+ }
992
+ async verifyWebhook() {
993
+ throw new ProviderError("UNSUPPORTED_CAPABILITY", "The fake provider does not receive webhooks.", {
994
+ safeCode: "webhooks_not_supported"
995
+ });
996
+ }
997
+ async normalizeWebhook(_input) {
998
+ throw new ProviderError("UNSUPPORTED_CAPABILITY", "The fake provider does not receive webhooks.", {
999
+ safeCode: "webhooks_not_supported"
1000
+ });
1001
+ }
1002
+ throwIfErrorScenario() {
1003
+ if (this.scenario === "timeout") {
1004
+ throw new ProviderError("TIMEOUT", "The fake provider timed out.", { retryable: true, safeCode: "timeout" });
1005
+ }
1006
+ if (this.scenario === "rate_limit") {
1007
+ throw new ProviderError("RATE_LIMITED", "The fake provider is rate limited.", {
1008
+ retryable: true,
1009
+ safeCode: "rate_limited",
1010
+ retryAfterSeconds: 30
1011
+ });
1012
+ }
1013
+ if (this.scenario === "retryable_provider_failure") {
1014
+ throw new ProviderError("RETRYABLE_PROVIDER_FAILURE", "The fake provider failed retryably.", {
1015
+ retryable: true,
1016
+ safeCode: "retryable_provider_failure"
1017
+ });
1018
+ }
1019
+ if (this.scenario === "malformed_response") {
1020
+ throw new ProviderError("RETRYABLE_PROVIDER_FAILURE", "The fake provider returned a malformed body.", {
1021
+ retryable: true,
1022
+ safeCode: "malformed_provider_response"
1023
+ });
1024
+ }
1025
+ if (this.scenario === "unknown_state") {
1026
+ throw new ProviderError("UNKNOWN_PROVIDER_STATE", "The fake provider returned an unknown state.", {
1027
+ retryable: false,
1028
+ safeCode: "unknown_provider_status"
1029
+ });
1030
+ }
1031
+ }
1032
+ launch(attemptId, canonicalStatus) {
1033
+ return {
1034
+ attemptId,
1035
+ canonicalStatus,
1036
+ launcherKey: "test_embedded",
1037
+ presentation: "embedded",
1038
+ providerDisclosure: "Test provider",
1039
+ transientSecret: `test_launch_${attemptId.replace(/-/g, "")}`,
1040
+ transientSecretExpiresAt: new Date(this.runtime.now().getTime() + 5 * 6e4).toISOString(),
1041
+ continuationReference: `cont_${attemptId}`
1042
+ };
1043
+ }
1044
+ };
1045
+ function createFakeAdapterForScenario(scenario) {
1046
+ return new FakeVerificationAdapter(scenario);
1047
+ }
1048
+ var IncompleteVerificationAdapter = class {
1049
+ provider = "incomplete_fixture";
1050
+ environment = "sandbox";
1051
+ contractVersion = VERIFICATION_ADAPTER_CONTRACT_VERSION;
1052
+ manifest = fakeProviderManifest;
1053
+ runtime = createDefaultRuntime("sandbox", {}, { allowedHosts: ["127.0.0.1"] });
1054
+ validateConfiguration() {
1055
+ throw new ProviderError("INVALID_CONFIGURATION", "Incomplete adapter is missing createAttempt, resumeAttempt, retrieveAttempt, retryAttempt, cancelAttempt, redactSubject, verifyWebhook, and normalizeWebhook.", {
1056
+ safeCode: "missing_method_create_attempt"
1057
+ });
1058
+ }
1059
+ };
1060
+
1061
+ // src/helpers.ts
1062
+ var FakeClock = class {
1063
+ constructor(epochMs = Date.parse("2026-01-01T00:00:00.000Z")) {
1064
+ this.epochMs = epochMs;
1065
+ }
1066
+ epochMs;
1067
+ now = () => new Date(this.epochMs);
1068
+ advance(ms) {
1069
+ this.epochMs += ms;
1070
+ }
1071
+ };
1072
+ function deterministicId(prefix, seed) {
1073
+ let hash = 0;
1074
+ for (let index = 0; index < seed.length; index += 1) {
1075
+ hash = (hash << 5) - hash + seed.charCodeAt(index) | 0;
1076
+ }
1077
+ return `${prefix}_${Math.abs(hash).toString(16).padStart(8, "0")}`;
1078
+ }
1079
+ function createControlledFetch(handler) {
1080
+ return (async (input, init) => {
1081
+ const request = input instanceof Request ? input : new Request(String(input), init);
1082
+ return handler(request);
1083
+ });
1084
+ }
1085
+ async function signHmacSha256Hex(secret, payload) {
1086
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1087
+ return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, payload)));
1088
+ }
1089
+ async function sha256Hex(payload) {
1090
+ return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", payload)));
1091
+ }
1092
+ function bytesToHex(value) {
1093
+ return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
1094
+ }
1095
+ async function createSignedWebhookFixture(input) {
1096
+ const now = input.now ?? /* @__PURE__ */ new Date();
1097
+ const timestamp = Math.floor(now.getTime() / 1e3);
1098
+ const raw = new TextEncoder().encode(input.body);
1099
+ const headers = new Headers({ "content-type": "application/json" });
1100
+ if (input.protocol === "stripe_v1_hmac") {
1101
+ const signature = await signHmacSha256Hex(input.secret, new TextEncoder().encode(`${timestamp}.${input.body}`));
1102
+ headers.set("stripe-signature", `t=${timestamp},v1=${signature}`);
1103
+ }
1104
+ if (input.protocol === "persona_hmac_sha256") {
1105
+ const prefix = new TextEncoder().encode(`${timestamp}.`);
1106
+ const payload = new Uint8Array(prefix.length + raw.length);
1107
+ payload.set(prefix);
1108
+ payload.set(raw, prefix.length);
1109
+ const signature = await signHmacSha256Hex(input.secret, payload);
1110
+ headers.set("Persona-Signature", `t=${timestamp},v1=${signature}`);
1111
+ }
1112
+ return new Request("https://example.test/webhooks/provider", { method: "POST", headers, body: raw });
1113
+ }
1114
+ function denySecretLogging(value) {
1115
+ if (/sk_|rk_|whsec_|secret|password/i.test(value)) {
1116
+ throw new ProviderError("INVALID_CONFIGURATION", "A secret would have been printed or logged.", {
1117
+ safeCode: "secret_leak"
1118
+ });
1119
+ }
1120
+ }
1121
+
1122
+ export { CANONICAL_STATUSES, ENGINE_CONTRACT_VERSION, FakeClock, FakeVerificationAdapter, IncompleteVerificationAdapter, LAUNCH_PRESENTATIONS, PROVIDER_ENVIRONMENTS, PROVIDER_MANIFEST_SCHEMA_URN, PROVIDER_OPERATIONS, ProviderError, ProviderOperationPendingError, ProviderRequiredInformationError, ProviderUnavailableError, REQUIRED_ADAPTER_METHODS, STANDARD_PACKAGE_CODES, STANDARD_RELATIONSHIP_KINDS, STANDARD_WEBHOOK_PROTOCOLS, TERMINAL_STATUSES, VERIFICATION_ADAPTER_CONTRACT_VERSION, VerificationAttemptLimitError, assertAdapterConformsToManifest, assertPackageCode, assertProviderManifest, assertProviderTypesDoNotEscape, bytesToHex, canonicalStatusRank, compareSemver, createAllowlistedHttp, createControlledFetch, createDefaultRuntime, createFakeAdapterForScenario, createSignedWebhookFixture, defineProviderManifest, denySecretLogging, deterministicId, emptyConfigurationSchema, fakeProviderManifest, isCanonicalStatus, isCountryCode, isCustomPackageCode, isLauncherKey, isOpaqueSubjectReference, isPackageCode, isProviderCode, isResourceType, isSemver, isStandardPackageCode, isTerminalStatus, isWebhookProtocol, majorsCompatible, metadataContainsForbiddenIdentifier, plainStringProperty, providerConformanceScenarios, providerManifestV1JsonSchema, runAdapterConformance, runAdapterConformanceScenarios, secretStringProperty, sha256Hex, signHmacSha256Hex, silentLogger, toSafeProviderFailure, validateManifestOnly };
1123
+ //# sourceMappingURL=index.js.map
1124
+ //# sourceMappingURL=index.js.map