@splitin/verification-adapter-persona 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,1356 @@
1
+ import { defineProviderManifest, VERIFICATION_ADAPTER_CONTRACT_VERSION, secretStringProperty, plainStringProperty, ProviderError, isCanonicalStatus, isOpaqueSubjectReference, metadataContainsForbiddenIdentifier, ProviderRequiredInformationError } from '@splitin/verification-adapter-sdk';
2
+
3
+ // src/adapter.ts
4
+ var DEFAULT_KYB_FIELD_MAP = {
5
+ associatedPeople: "associated_people",
6
+ legalName: "business-legal-name",
7
+ registeredAddress: "business-registered-address",
8
+ physicalAddress: "business-physical-address",
9
+ jurisdictionCountryCode: "business-jurisdiction-country-code",
10
+ entityType: "business-entity-type",
11
+ evidenceReferences: "business-evidence-references",
12
+ relationshipReference: "relationship-reference",
13
+ relationshipKind: "relationship-kind",
14
+ claimedOwnershipPercentage: "claimed-ownership-percentage",
15
+ subjectReference: "subject-reference"
16
+ };
17
+ function createPersonaConfiguration(values) {
18
+ const kybCaseMode = parseKybCaseMode(values.kybCaseMode ?? values.PERSONA_KYB_CASE_MODE);
19
+ return Object.freeze({
20
+ apiKey: pick(values, "apiKey", "PERSONA_API_KEY"),
21
+ environmentId: pick(values, "environmentId", "PERSONA_ENVIRONMENT_ID"),
22
+ idvTemplateId: pick(values, "idvTemplateId", "PERSONA_IDV_TEMPLATE_ID"),
23
+ idvTemplateVersion: pick(values, "idvTemplateVersion", "PERSONA_IDV_TEMPLATE_VERSION"),
24
+ associatedPersonTemplateId: pick(values, "associatedPersonTemplateId", "PERSONA_ASSOCIATED_PERSON_TEMPLATE_ID"),
25
+ associatedPersonTemplateVersion: pick(values, "associatedPersonTemplateVersion", "PERSONA_ASSOCIATED_PERSON_TEMPLATE_VERSION"),
26
+ kybTransactionTypeId: pick(values, "kybTransactionTypeId", "PERSONA_KYB_TRANSACTION_TYPE_ID"),
27
+ kybWorkflowId: pick(values, "kybWorkflowId", "PERSONA_KYB_WORKFLOW_ID"),
28
+ kybWorkflowVersion: pick(values, "kybWorkflowVersion", "PERSONA_KYB_WORKFLOW_VERSION"),
29
+ caseTemplateId: pick(values, "caseTemplateId", "PERSONA_CASE_TEMPLATE_ID"),
30
+ caseType: optional(values, "caseType", "PERSONA_CASE_TYPE"),
31
+ ownershipCaseType: optional(values, "ownershipCaseType", "PERSONA_OWNERSHIP_CASE_TYPE"),
32
+ businessAuthorityCaseType: optional(values, "businessAuthorityCaseType", "PERSONA_BUSINESS_AUTHORITY_CASE_TYPE"),
33
+ kybCaseMode,
34
+ kybFieldMap: parseFieldMap(values.kybFieldMapJson ?? values.PERSONA_KYB_FIELD_MAP_JSON),
35
+ statusMappings: parseStatusMappings(values.statusMappingsJson ?? values.PERSONA_STATUS_MAPPINGS_JSON),
36
+ apiVersion: pick(values, "apiVersion", "PERSONA_API_VERSION"),
37
+ webhookSecretCurrent: pick(values, "webhookSecretCurrent", "PERSONA_WEBHOOK_SECRET_CURRENT"),
38
+ webhookSecretPrevious: optional(values, "webhookSecretPrevious", "PERSONA_WEBHOOK_SECRET_PREVIOUS"),
39
+ webhookToleranceSeconds: parseTolerance(values.webhookToleranceSeconds ?? values.PERSONA_WEBHOOK_TOLERANCE_SECONDS),
40
+ allowedOrigins: parseAllowedOrigins(values.allowedOrigins ?? values.PERSONA_ALLOWED_ORIGINS)
41
+ });
42
+ }
43
+ function personaWebhookSecrets(config) {
44
+ return [config.webhookSecretCurrent, config.webhookSecretPrevious].map((value) => value?.trim() ?? "").filter(Boolean);
45
+ }
46
+ function validatePersonaConfiguration(config, environment) {
47
+ const required = [
48
+ config.apiKey,
49
+ config.environmentId,
50
+ config.idvTemplateId,
51
+ config.idvTemplateVersion,
52
+ config.associatedPersonTemplateId,
53
+ config.associatedPersonTemplateVersion,
54
+ config.kybTransactionTypeId,
55
+ config.kybWorkflowId,
56
+ config.kybWorkflowVersion,
57
+ config.caseTemplateId,
58
+ config.apiVersion
59
+ ];
60
+ if (required.some((value) => !value.trim()) || personaWebhookSecrets(config).length === 0) {
61
+ throw invalid("Persona verification is not configured.", "persona_not_configured");
62
+ }
63
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(config.apiVersion)) {
64
+ throw invalid("Persona API version is invalid.", "persona_api_version_invalid");
65
+ }
66
+ const expectedKeyPrefix = environment === "production" ? "persona_production_" : "persona_sandbox_";
67
+ if (!config.apiKey.startsWith(expectedKeyPrefix)) {
68
+ throw invalid("Persona API key does not match the pinned environment.", "persona_credential_environment_mismatch");
69
+ }
70
+ assertPublishedVersion(config.idvTemplateVersion, "idv template");
71
+ assertPublishedVersion(config.associatedPersonTemplateVersion, "associated person template");
72
+ assertPublishedVersion(config.kybWorkflowVersion, "KYB workflow");
73
+ validateFieldMap(config.kybFieldMap);
74
+ validateStatusMappings(config.statusMappings);
75
+ if (environment === "production" && (!config.allowedOrigins || config.allowedOrigins.length === 0)) {
76
+ throw invalid("Persona production embedding origin allowlist is missing.", "persona_allowed_origins_missing");
77
+ }
78
+ }
79
+ function assertPublishedVersion(value, label) {
80
+ if (!/^[A-Za-z0-9._-]{3,128}$/.test(value) || /draft|latest|unpublished/i.test(value)) {
81
+ throw invalid(`Persona ${label} version must be a published version pin.`, "persona_unpublished_version");
82
+ }
83
+ }
84
+ function invalid(message, safeCode2) {
85
+ return new ProviderError("INVALID_CONFIGURATION", message, { safeCode: safeCode2 });
86
+ }
87
+ function pick(values, camel, conventional) {
88
+ return values[camel]?.trim() || values[conventional]?.trim() || "";
89
+ }
90
+ function optional(values, camel, conventional) {
91
+ const value = pick(values, camel, conventional);
92
+ return value || void 0;
93
+ }
94
+ function parseTolerance(value) {
95
+ if (value == null || value === "") return void 0;
96
+ const parsed = Number(value);
97
+ return Number.isInteger(parsed) && parsed >= 60 && parsed <= 900 ? parsed : 300;
98
+ }
99
+ function parseAllowedOrigins(value) {
100
+ if (!value) return void 0;
101
+ const result = [];
102
+ for (const candidate of value.split(",")) {
103
+ try {
104
+ const origin = new URL(candidate.trim()).origin;
105
+ if (!origin.startsWith("https:")) {
106
+ throw invalid("Persona embedding origins must be HTTPS.", "persona_origin_invalid");
107
+ }
108
+ if (!result.includes(origin)) result.push(origin);
109
+ } catch (error) {
110
+ if (error instanceof ProviderError) throw error;
111
+ throw invalid("Persona embedding origin allowlist is invalid.", "persona_origin_invalid");
112
+ }
113
+ }
114
+ return result;
115
+ }
116
+ function parseKybCaseMode(value) {
117
+ const normalized = value?.trim() || "workflow_managed";
118
+ if (normalized !== "workflow_managed" && normalized !== "engine_managed") {
119
+ throw invalid("Persona KYB case mode is invalid.", "persona_kyb_case_mode_invalid");
120
+ }
121
+ return normalized;
122
+ }
123
+ function parseFieldMap(value) {
124
+ if (!value) return DEFAULT_KYB_FIELD_MAP;
125
+ try {
126
+ const parsed = JSON.parse(value);
127
+ validateFieldMap(parsed);
128
+ return parsed;
129
+ } catch (error) {
130
+ if (error instanceof ProviderError) throw error;
131
+ throw invalid("Persona KYB field map is invalid.", "persona_field_map_invalid");
132
+ }
133
+ }
134
+ function validateFieldMap(value) {
135
+ const keys = Object.keys(DEFAULT_KYB_FIELD_MAP);
136
+ if (!value || keys.some((key) => typeof value[key] !== "string" || !/^[a-z][a-z0-9_-]{1,127}$/.test(value[key]))) {
137
+ throw invalid("Persona KYB field map is invalid.", "persona_field_map_invalid");
138
+ }
139
+ if (new Set(keys.map((key) => value[key])).size !== keys.length) {
140
+ throw invalid("Persona KYB field map contains duplicate fields.", "persona_field_map_duplicate");
141
+ }
142
+ }
143
+ function parseStatusMappings(value) {
144
+ if (!value) return void 0;
145
+ try {
146
+ const parsed = JSON.parse(value);
147
+ validateStatusMappings(parsed);
148
+ return parsed;
149
+ } catch (error) {
150
+ if (error instanceof ProviderError) throw error;
151
+ throw invalid("Persona status mappings are invalid.", "persona_status_mappings_invalid");
152
+ }
153
+ }
154
+ function validateStatusMappings(value) {
155
+ if (!value) return;
156
+ for (const [resource, mappings] of Object.entries(value)) {
157
+ if (!["inquiry", "transaction", "case", "report", "verification"].includes(resource) || !mappings || typeof mappings !== "object" || Array.isArray(mappings)) {
158
+ throw invalid("Persona status mappings are invalid.", "persona_status_mappings_invalid");
159
+ }
160
+ for (const [providerStatus, normalized] of Object.entries(mappings)) {
161
+ if (!/^[a-z0-9_-]{2,64}$/.test(providerStatus) || typeof normalized !== "string" || !isCanonicalStatus(normalized)) {
162
+ throw invalid("Persona status mappings are invalid.", "persona_status_mappings_invalid");
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ // src/constants.ts
169
+ var PERSONA_API_HOST = "api.withpersona.com";
170
+ var PERSONA_INQUIRY_HOST = "inquiry.withpersona.com";
171
+ var PERSONA_DISCLOSURE = "Powered by Persona";
172
+ var PERSONA_NORMALIZATION_VERSION = "persona-v1";
173
+ var PERSONA_ALLOWED_EVENTS = /* @__PURE__ */ new Set([
174
+ "inquiry.created",
175
+ "inquiry.started",
176
+ "inquiry.pending",
177
+ "inquiry.completed",
178
+ "inquiry.marked-for-review",
179
+ "inquiry.approved",
180
+ "inquiry.declined",
181
+ "inquiry.failed",
182
+ "inquiry.expired",
183
+ "inquiry.redacted",
184
+ "transaction.created",
185
+ "transaction.status-updated",
186
+ "transaction.updated",
187
+ "transaction.redacted",
188
+ "case.created",
189
+ "case.assigned",
190
+ "case.resolved",
191
+ "case.reopened",
192
+ "case.updated",
193
+ "case.status-updated",
194
+ "case.redacted",
195
+ "report.created",
196
+ "report.ready",
197
+ "report.failed",
198
+ "report.redacted",
199
+ "verification.created",
200
+ "verification.passed",
201
+ "verification.failed",
202
+ "verification.redacted"
203
+ ]);
204
+ var HUMAN_PACKAGES = /* @__PURE__ */ new Set(["human_idv"]);
205
+ var INQUIRY_PACKAGES = /* @__PURE__ */ new Set(["human_idv", "associated_person_idv"]);
206
+ var personaProviderManifest = defineProviderManifest({
207
+ contractVersion: VERIFICATION_ADAPTER_CONTRACT_VERSION,
208
+ adapterVersion: "1.0.0",
209
+ engineCompatibility: "1.0.0",
210
+ provider: "persona",
211
+ displayName: "Persona",
212
+ description: "Persona identity verification: human IDV, business KYB, associated people, and ownership review. Documents remain at Persona; adapters return opaque evidence references only.",
213
+ supportedPackages: ["human_idv", "business_kyb", "associated_person_idv", "ownership_review"],
214
+ supportedCountries: ["US"],
215
+ environments: ["sandbox", "production"],
216
+ capabilities: {
217
+ presentations: ["embedded", "hosted", "qr", "none"],
218
+ canResume: true,
219
+ canRetry: true,
220
+ canCancel: true,
221
+ canRedact: true
222
+ },
223
+ launcherKeys: ["persona_embedded", "hosted"],
224
+ launchPresentations: ["embedded", "hosted", "qr", "none"],
225
+ configurationSchemaVersion: "urn:splitin:verification:config:persona:v1",
226
+ configurationSchema: {
227
+ $schema: "https://json-schema.org/draft/2020-12/schema",
228
+ type: "object",
229
+ additionalProperties: false,
230
+ required: [
231
+ "apiKey",
232
+ "environmentId",
233
+ "idvTemplateId",
234
+ "idvTemplateVersion",
235
+ "associatedPersonTemplateId",
236
+ "associatedPersonTemplateVersion",
237
+ "kybTransactionTypeId",
238
+ "kybWorkflowId",
239
+ "kybWorkflowVersion",
240
+ "caseTemplateId",
241
+ "apiVersion",
242
+ "webhookSecretCurrent"
243
+ ],
244
+ properties: {
245
+ apiKey: secretStringProperty(),
246
+ environmentId: plainStringProperty(),
247
+ idvTemplateId: plainStringProperty(),
248
+ idvTemplateVersion: plainStringProperty(),
249
+ associatedPersonTemplateId: plainStringProperty(),
250
+ associatedPersonTemplateVersion: plainStringProperty(),
251
+ kybTransactionTypeId: plainStringProperty(),
252
+ kybWorkflowId: plainStringProperty(),
253
+ kybWorkflowVersion: plainStringProperty(),
254
+ caseTemplateId: plainStringProperty(),
255
+ caseType: plainStringProperty(),
256
+ ownershipCaseType: { type: "string", minLength: 1, "x-secret": false },
257
+ businessAuthorityCaseType: { type: "string", minLength: 1, "x-secret": false },
258
+ kybCaseMode: { type: "string", enum: ["workflow_managed", "engine_managed"] },
259
+ kybFieldMapJson: { type: "string", minLength: 1, "x-secret": false },
260
+ statusMappingsJson: { type: "string", minLength: 1, "x-secret": false },
261
+ apiVersion: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
262
+ webhookSecretCurrent: secretStringProperty(),
263
+ webhookSecretPrevious: { type: "string", minLength: 1, "x-secret": true },
264
+ webhookToleranceSeconds: { type: "integer", minimum: 60, maximum: 900 },
265
+ allowedOrigins: { type: "string", minLength: 1, "x-secret": false }
266
+ }
267
+ },
268
+ webhook: {
269
+ protocol: "persona_hmac_sha256",
270
+ eventFamilies: ["inquiry", "transaction", "case", "report", "verification"],
271
+ toleranceSeconds: 300
272
+ },
273
+ dataPolicy: {
274
+ classifications: ["provider_resource_id", "normalized_status", "reason_codes", "evidence_reference"],
275
+ prohibitedPersistence: ["raw_webhook", "launch_secret", "document", "selfie", "government_id"],
276
+ rawPayloadPersistence: false,
277
+ browserSecretPersistence: false,
278
+ governmentIdentifierPersistence: false
279
+ },
280
+ retry: { sameResourceWhenResumable: true, newAttemptAfterTerminal: true },
281
+ cancellation: { supported: true, terminal: true },
282
+ redaction: { supported: true, asynchronous: false },
283
+ apiHosts: ["api.withpersona.com"],
284
+ testedApiVersions: ["2023-01-05"]
285
+ });
286
+
287
+ // src/status.ts
288
+ function normalizePersonaStatus(value) {
289
+ switch (normalizeStatusKey(value)) {
290
+ case "created":
291
+ return { status: "created", reasonCodes: [] };
292
+ case "started":
293
+ case "pending":
294
+ return { status: "pending_user_input", reasonCodes: [] };
295
+ case "completed":
296
+ return { status: "processing", reasonCodes: ["persona_completed_awaiting_decision"] };
297
+ case "approved":
298
+ case "passed":
299
+ return { status: "verified", reasonCodes: [] };
300
+ case "declined":
301
+ return { status: "declined", reasonCodes: ["persona_declined"] };
302
+ case "marked_for_review":
303
+ case "needs_review":
304
+ case "in_review":
305
+ case "open":
306
+ return { status: "manual_review_required", reasonCodes: ["persona_manual_review_required"] };
307
+ case "failed":
308
+ case "errored":
309
+ return { status: "failed", reasonCodes: ["persona_verification_failed"] };
310
+ case "expired":
311
+ return { status: "expired", reasonCodes: ["persona_inquiry_expired"] };
312
+ case "canceled":
313
+ case "cancelled":
314
+ return { status: "canceled", reasonCodes: ["persona_inquiry_canceled"] };
315
+ case "redacted":
316
+ return { status: "redacted", reasonCodes: ["persona_redacted"] };
317
+ default:
318
+ return { status: "manual_review_required", reasonCodes: ["persona_unknown_status"] };
319
+ }
320
+ }
321
+ function resolvePersonaStatus(resourceKind, providerStatus, mappings) {
322
+ const configured = mappings?.[resourceKind]?.[normalizeStatusKey(providerStatus)];
323
+ return configured ? { status: configured, reasonCodes: [] } : normalizePersonaStatus(providerStatus);
324
+ }
325
+ function normalizeStatusKey(value) {
326
+ return String(value ?? "").trim().toLowerCase().replace(/[ -]+/g, "_");
327
+ }
328
+ function resourceCategory(value) {
329
+ if (value === "inquiry" || value === "transaction" || value === "case") return value;
330
+ if (value.startsWith("report")) return "report";
331
+ if (value.startsWith("verification")) return "verification";
332
+ return "verification";
333
+ }
334
+ function isRecord(value) {
335
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
336
+ }
337
+ function asOptionalRecord(value) {
338
+ return isRecord(value) ? value : null;
339
+ }
340
+ function asRecord(value) {
341
+ if (!isRecord(value)) throw new ProviderError("UNKNOWN_PROVIDER_STATE", "Persona resource is invalid.", {
342
+ safeCode: "malformed_provider_response"
343
+ });
344
+ return value;
345
+ }
346
+ function requireResource(value, type) {
347
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
348
+ throw new ProviderError("UNKNOWN_PROVIDER_STATE", "Persona resource is invalid.", {
349
+ safeCode: "malformed_provider_response"
350
+ });
351
+ }
352
+ if (type && value.type !== type) {
353
+ throw new ProviderError("UNKNOWN_PROVIDER_STATE", "Persona resource type is invalid.", {
354
+ safeCode: "malformed_provider_response"
355
+ });
356
+ }
357
+ return value;
358
+ }
359
+ function requireId(value) {
360
+ if (typeof value !== "string" || value.length < 4 || value.length > 256 || /\s/.test(value)) {
361
+ throw new ProviderError("UNKNOWN_PROVIDER_STATE", "Persona resource ID is invalid.", {
362
+ safeCode: "malformed_provider_response"
363
+ });
364
+ }
365
+ return value;
366
+ }
367
+ function requireText(value, label) {
368
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 256) {
369
+ throw new ProviderError("UNKNOWN_PROVIDER_STATE", `${label} is invalid.`, {
370
+ safeCode: "malformed_provider_response"
371
+ });
372
+ }
373
+ return value;
374
+ }
375
+ function assertOpaqueReference(value) {
376
+ if (!isOpaqueSubjectReference(value)) {
377
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "The subject reference is not an opaque identifier.", {
378
+ safeCode: "subject_reference_invalid"
379
+ });
380
+ }
381
+ return value;
382
+ }
383
+ function isTransactionId(value) {
384
+ return /^txn_[A-Za-z0-9_-]{4,252}$/.test(value);
385
+ }
386
+ function isCaseId(value) {
387
+ return /^case_[A-Za-z0-9_-]{4,251}$/.test(value);
388
+ }
389
+ function parseRetryAfter(value) {
390
+ if (!value || !/^\d{1,6}$/.test(value)) return void 0;
391
+ return Math.min(Number(value), 3600);
392
+ }
393
+ function mapPersonaHttpError(status, retryAfterHeader) {
394
+ const retryAfter = parseRetryAfter(retryAfterHeader);
395
+ const code = status === 401 || status === 403 ? "AUTHENTICATION_FAILED" : status === 429 ? "RATE_LIMITED" : status === 408 || status === 504 ? "TIMEOUT" : status >= 500 ? "RETRYABLE_PROVIDER_FAILURE" : "TERMINAL_INPUT_FAILURE";
396
+ return new ProviderError(code, "Persona request failed.", {
397
+ retryable: status === 408 || status === 429 || status >= 500,
398
+ retryAfterSeconds: retryAfter,
399
+ safeCode: code === "AUTHENTICATION_FAILED" ? "persona_authentication_failed" : code === "RATE_LIMITED" ? "persona_rate_limited" : code === "TIMEOUT" ? "persona_timeout" : code === "RETRYABLE_PROVIDER_FAILURE" ? "persona_provider_failure" : "persona_terminal_input_failure"
400
+ });
401
+ }
402
+ function legalPrefill(command) {
403
+ const fields = {};
404
+ const first = safePrefill(command.legalFirstName);
405
+ const last = safePrefill(command.legalLastName);
406
+ const email = safePrefill(command.email);
407
+ if (first) fields["name-first"] = first;
408
+ if (last) fields["name-last"] = last;
409
+ if (email) fields["email-address"] = email;
410
+ return fields;
411
+ }
412
+ function businessFields(command, map) {
413
+ const organization = command.organization;
414
+ if (!organization) throw new ProviderRequiredInformationError();
415
+ const fields = {
416
+ [map.legalName]: requiredCanonicalText(organization.legalName, 256),
417
+ [map.jurisdictionCountryCode]: normalizeCountryCode(organization.jurisdictionCountryCode)
418
+ };
419
+ if (organization.registeredAddress) fields[map.registeredAddress] = canonicalAddress(organization.registeredAddress);
420
+ if (organization.physicalAddress) fields[map.physicalAddress] = canonicalAddress(organization.physicalAddress);
421
+ const entityType = safePrefill(organization.entityType);
422
+ if (entityType) fields[map.entityType] = entityType;
423
+ const associatedPeople = (organization.associatedPeople ?? []).map((person) => {
424
+ const percentage = person.claimedOwnershipPercentage;
425
+ if (percentage !== null && percentage !== void 0 && (!Number.isFinite(percentage) || percentage < 0 || percentage > 100)) {
426
+ throw new ProviderRequiredInformationError("Ownership percentage is invalid.");
427
+ }
428
+ const result = {
429
+ subject_reference: assertOpaqueReference(person.subjectReference),
430
+ name_first: requiredCanonicalText(person.legalFirstName, 128),
431
+ name_last: requiredCanonicalText(person.legalLastName, 128),
432
+ association: person.relationshipKind
433
+ };
434
+ const email = safePrefill(person.email);
435
+ if (email) result.email_address = email;
436
+ if (percentage !== null && percentage !== void 0) result.percentage_ownership = percentage;
437
+ return result;
438
+ });
439
+ if (associatedPeople.length > 0) fields[map.associatedPeople] = associatedPeople;
440
+ const evidence = (organization.evidenceReferences ?? command.evidenceReferences ?? []).map((value) => ({
441
+ id: requireId(value),
442
+ type: "evidence"
443
+ }));
444
+ if (evidence.length > 0) fields[map.evidenceReferences] = evidence;
445
+ if (command.relationship) {
446
+ fields[map.relationshipReference] = assertOpaqueReference(command.relationship.relationshipReference);
447
+ fields[map.relationshipKind] = command.relationship.kind;
448
+ const percentage = command.relationship.claimedOwnershipPercentage;
449
+ if (percentage !== null && percentage !== void 0) {
450
+ if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) {
451
+ throw new ProviderRequiredInformationError("Ownership percentage is invalid.");
452
+ }
453
+ fields[map.claimedOwnershipPercentage] = percentage;
454
+ }
455
+ if (command.relationship.subjectReference) {
456
+ fields[map.subjectReference] = assertOpaqueReference(command.relationship.subjectReference);
457
+ }
458
+ }
459
+ return fields;
460
+ }
461
+ function canonicalAddress(value) {
462
+ const result = {
463
+ street_1: requiredCanonicalText(value.street1, 256),
464
+ city: requiredCanonicalText(value.city, 128),
465
+ postal_code: requiredCanonicalText(value.postalCode, 32),
466
+ country_code: normalizeCountryCode(value.countryCode)
467
+ };
468
+ const street2 = safePrefill(value.street2);
469
+ const subdivision = safePrefill(value.subdivision);
470
+ if (street2) result.street_2 = street2;
471
+ if (subdivision) result.subdivision = subdivision;
472
+ return result;
473
+ }
474
+ function parseAssociatedPersonRequirements(fieldsValue, fieldKey) {
475
+ const fields = asOptionalRecord(fieldsValue);
476
+ const field = fields?.[fieldKey];
477
+ const fieldRecord = asOptionalRecord(field);
478
+ const value = fieldRecord && "value" in fieldRecord ? fieldRecord.value : field;
479
+ if (!Array.isArray(value)) return [];
480
+ const result = [];
481
+ for (const candidate of value.slice(0, 100)) {
482
+ const item = asOptionalRecord(candidate);
483
+ if (!item) continue;
484
+ const subjectReference = firstString(item, ["subject_reference", "subject-reference", "account_reference_id"]);
485
+ const requirementKind = normalizeRequirementKind(firstString(item, ["requirement_kind", "requirement-kind", "association"]));
486
+ const verificationMode = normalizeVerificationMode(firstString(item, ["verification_mode", "verification-mode"]));
487
+ const providerStatus = firstString(item, ["status", "verification_status", "verification-status"]);
488
+ if (!subjectReference || !isOpaqueSubjectReference(subjectReference) || !requirementKind || !verificationMode || !providerStatus) continue;
489
+ const inquiryId = firstString(item, ["inquiry_id", "inquiry-id"]);
490
+ result.push({
491
+ subjectReference,
492
+ inquiryId: inquiryId && /^inq_[A-Za-z0-9_-]{4,252}$/.test(inquiryId) ? inquiryId : null,
493
+ requirementKind,
494
+ verificationMode,
495
+ normalizedStatus: normalizeAssociatedRequirementStatus(providerStatus, verificationMode),
496
+ mandatory: item.mandatory !== false && item.is_mandatory !== false && item["is-mandatory"] !== false,
497
+ claimedOwnershipPercentage: typeof item.percentage_ownership === "number" && Number.isFinite(item.percentage_ownership) && item.percentage_ownership >= 0 && item.percentage_ownership <= 100 ? item.percentage_ownership : null
498
+ });
499
+ }
500
+ return result;
501
+ }
502
+ function firstString(value, keys) {
503
+ for (const key of keys) {
504
+ if (typeof value[key] === "string" && String(value[key]).trim()) return String(value[key]).trim();
505
+ }
506
+ return null;
507
+ }
508
+ function normalizeRequirementKind(value) {
509
+ const normalized = normalizeStatusKey(value ?? "");
510
+ if (normalized === "ubo" || normalized === "director" || normalized === "officer" || normalized === "authorized_representative" || normalized === "associated_person") return normalized;
511
+ return null;
512
+ }
513
+ function normalizeVerificationMode(value) {
514
+ const normalized = normalizeStatusKey(value ?? "");
515
+ if (normalized === "not_required" || normalized === "database" || normalized === "inquiry") return normalized;
516
+ return null;
517
+ }
518
+ function normalizeAssociatedRequirementStatus(value, mode) {
519
+ const normalized = normalizeStatusKey(value);
520
+ if (mode === "not_required" || normalized === "not_required") return "not_required";
521
+ if (normalized === "approved" || normalized === "passed" || normalized === "verified") return "verified";
522
+ if (normalized === "completed" || normalized === "processing") return "processing";
523
+ if (normalized === "needs_review" || normalized === "marked_for_review" || normalized === "in_review") return "manual_review_required";
524
+ if (normalized === "declined") return "declined";
525
+ if (normalized === "failed" || normalized === "errored") return "failed";
526
+ if (normalized === "expired") return "expired";
527
+ if (normalized === "canceled" || normalized === "cancelled") return "canceled";
528
+ if (normalized === "required") return "required";
529
+ return "pending";
530
+ }
531
+ function requiredCanonicalText(value, maxLength) {
532
+ const trimmed = value?.trim();
533
+ if (!trimmed || trimmed.length > maxLength) throw new ProviderRequiredInformationError();
534
+ return trimmed;
535
+ }
536
+ function normalizeCountryCode(value) {
537
+ const normalized = value.trim().toUpperCase();
538
+ if (!/^[A-Z]{2}$/.test(normalized)) throw new ProviderRequiredInformationError();
539
+ return normalized;
540
+ }
541
+ function safePrefill(value) {
542
+ const trimmed = value?.trim();
543
+ return trimmed && trimmed.length <= 254 ? trimmed : null;
544
+ }
545
+ function parseObject(value) {
546
+ try {
547
+ const result = JSON.parse(value);
548
+ if (!isRecord(result)) throw new Error();
549
+ return result;
550
+ } catch {
551
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "Persona webhook JSON is invalid.", {
552
+ safeCode: "persona_webhook_payload_invalid"
553
+ });
554
+ }
555
+ }
556
+ function safeDate(value, fallback) {
557
+ const parsed = typeof value === "string" ? new Date(value) : fallback;
558
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : fallback.toISOString();
559
+ }
560
+ function safeOptionalDate(value) {
561
+ if (typeof value !== "string") return void 0;
562
+ const parsed = new Date(value);
563
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : void 0;
564
+ }
565
+ function statusFromEvent(name) {
566
+ return name.split(".").at(-1) ?? "needs_review";
567
+ }
568
+ function safeCode(value) {
569
+ if (typeof value !== "string") return null;
570
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").slice(0, 64);
571
+ return normalized || null;
572
+ }
573
+ function findAssociatedPerson(payload) {
574
+ if (!Array.isArray(payload.included)) return null;
575
+ for (const value of payload.included) {
576
+ const resource = asOptionalRecord(value);
577
+ if (resource?.type !== "inquiry") continue;
578
+ const attributes = asOptionalRecord(resource.attributes);
579
+ const reference = attributes?.["reference-id"];
580
+ if (typeof reference === "string" && isOpaqueSubjectReference(reference) && typeof resource.id === "string") {
581
+ return { reference, inquiryId: requireId(resource.id) };
582
+ }
583
+ }
584
+ return null;
585
+ }
586
+ function caseRelatedResourceType(value) {
587
+ if (value === "inquiry" || value === "transaction") return value;
588
+ if (value.startsWith("report")) return "report";
589
+ if (value.startsWith("verification")) return "verification";
590
+ return null;
591
+ }
592
+ function redactionCollection(resourceType) {
593
+ if (resourceType === "case") return "cases";
594
+ if (resourceType === "account") return "accounts";
595
+ if (resourceType === "transaction") return "transactions";
596
+ if (resourceType === "report") return "reports";
597
+ if (resourceType === "verification") return "verifications";
598
+ return "inquiries";
599
+ }
600
+ var RESUMABLE_STATUSES = /* @__PURE__ */ new Set(["created", "pending_user_input", "paused"]);
601
+ var HEX_64 = /^[0-9a-f]{64}$/i;
602
+ async function verifyPersonaWebhook(request, options) {
603
+ const signatureHeader = request.headers.get("Persona-Signature");
604
+ const environmentId = request.headers.get("Persona-Environment-Id");
605
+ const secrets = options.secrets.map((value) => value.trim()).filter(Boolean);
606
+ if (!signatureHeader || secrets.length === 0 || environmentId !== options.expectedEnvironmentId) {
607
+ throw signatureFailure("Persona webhook authentication is invalid.");
608
+ }
609
+ const candidates = parsePersonaSignatures(signatureHeader);
610
+ if (candidates.length === 0) throw signatureFailure("Persona webhook signature is invalid.");
611
+ const nowSeconds = Math.floor((options.now?.() ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
612
+ const tolerance = options.toleranceSeconds ?? 300;
613
+ const rawBytes = new Uint8Array(await request.arrayBuffer());
614
+ if (rawBytes.byteLength === 0 || rawBytes.byteLength > 1048576) {
615
+ throw payloadFailure("Persona webhook body size is invalid.");
616
+ }
617
+ let matched = null;
618
+ for (const candidate of candidates) {
619
+ if (Math.abs(nowSeconds - candidate.timestamp) > tolerance) continue;
620
+ for (const secret of secrets) {
621
+ const expected = await hmacSha256Hex(options.crypto, secret, signedPayload(candidate.timestamp, rawBytes));
622
+ if (timingSafeHexEqual(expected, candidate.signature)) {
623
+ matched = candidate;
624
+ break;
625
+ }
626
+ }
627
+ if (matched) break;
628
+ }
629
+ if (!matched) throw signatureFailure("Persona webhook signature is invalid or stale.");
630
+ const bodySha256 = await sha256HexBytes(options.crypto, rawBytes);
631
+ let transientJson;
632
+ try {
633
+ transientJson = JSON.parse(new TextDecoder().decode(rawBytes));
634
+ } catch {
635
+ transientJson = null;
636
+ }
637
+ let providerEventKey;
638
+ try {
639
+ providerEventKey = readPersonaEventId(transientJson);
640
+ } catch {
641
+ providerEventKey = `persona_${bodySha256}`;
642
+ }
643
+ return {
644
+ providerEventKey,
645
+ receivedAt: new Date(nowSeconds * 1e3).toISOString(),
646
+ signatureIssuedAt: new Date(matched.timestamp * 1e3).toISOString(),
647
+ bodySha256,
648
+ opaquePayload: rawBytes
649
+ };
650
+ }
651
+ function parsePersonaSignatures(header) {
652
+ const groups = header.trim().split(/\s+/);
653
+ const result = [];
654
+ for (const group of groups) {
655
+ let timestamp = null;
656
+ const signatures = [];
657
+ for (const part of group.split(",")) {
658
+ const [key, value] = part.split("=", 2);
659
+ if (key === "t" && /^\d{1,12}$/.test(value ?? "")) timestamp = Number(value);
660
+ if (key === "v1" && HEX_64.test(value ?? "")) signatures.push((value ?? "").toLowerCase());
661
+ }
662
+ if (timestamp !== null && Number.isSafeInteger(timestamp)) {
663
+ for (const signature of signatures) result.push({ timestamp, signature });
664
+ }
665
+ }
666
+ return result;
667
+ }
668
+ function signedPayload(timestamp, rawBody) {
669
+ const prefix = new TextEncoder().encode(`${timestamp}.`);
670
+ const result = new Uint8Array(prefix.length + rawBody.length);
671
+ result.set(prefix);
672
+ result.set(rawBody, prefix.length);
673
+ return result;
674
+ }
675
+ async function hmacSha256Hex(crypto, secret, value) {
676
+ const key = await crypto.subtle.importKey(
677
+ "raw",
678
+ new TextEncoder().encode(secret),
679
+ { name: "HMAC", hash: "SHA-256" },
680
+ false,
681
+ ["sign"]
682
+ );
683
+ return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, value)));
684
+ }
685
+ async function sha256HexBytes(crypto, value) {
686
+ return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", Uint8Array.from(value))));
687
+ }
688
+ function timingSafeHexEqual(left, right) {
689
+ if (!HEX_64.test(left) || !HEX_64.test(right)) return false;
690
+ const a = hexToBytes(left);
691
+ const b = hexToBytes(right);
692
+ let mismatch = a.length ^ b.length;
693
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
694
+ mismatch |= (a[index] ?? 0) ^ (b[index] ?? 0);
695
+ }
696
+ return mismatch === 0;
697
+ }
698
+ function hexToBytes(value) {
699
+ return Uint8Array.from(value.match(/.{2}/g) ?? [], (pair) => Number.parseInt(pair, 16));
700
+ }
701
+ function bytesToHex(value) {
702
+ return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
703
+ }
704
+ function readPersonaEventId(value) {
705
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
706
+ throw payloadFailure("Persona webhook JSON is invalid.");
707
+ }
708
+ const data = value.data;
709
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
710
+ throw payloadFailure("Persona webhook event is invalid.");
711
+ }
712
+ const id = data.id;
713
+ if (typeof id !== "string" || id.length < 8 || id.length > 256) {
714
+ throw payloadFailure("Persona webhook event ID is invalid.");
715
+ }
716
+ return id;
717
+ }
718
+ function signatureFailure(message) {
719
+ return new ProviderError("SIGNATURE_INVALID", message, {
720
+ retryable: false,
721
+ safeCode: "persona_webhook_signature_invalid"
722
+ });
723
+ }
724
+ function payloadFailure(message) {
725
+ return new ProviderError("TERMINAL_INPUT_FAILURE", message, {
726
+ retryable: false,
727
+ safeCode: "persona_webhook_payload_invalid"
728
+ });
729
+ }
730
+
731
+ // src/adapter.ts
732
+ var PersonaVerificationAdapter = class {
733
+ contractVersion = VERIFICATION_ADAPTER_CONTRACT_VERSION;
734
+ manifest = personaProviderManifest;
735
+ provider = "persona";
736
+ environment;
737
+ runtime;
738
+ constructor(runtime) {
739
+ this.runtime = runtime;
740
+ this.environment = runtime.environment;
741
+ this.validateConfiguration();
742
+ }
743
+ validateConfiguration() {
744
+ validatePersonaConfiguration(this.runtime.configuration, this.environment);
745
+ }
746
+ async createAttempt(command) {
747
+ this.assertCommand(command);
748
+ this.assertAllowedOrigin(command.requestOrigin);
749
+ if (command.packageCode === "business_kyb") return this.createBusinessTransaction(command);
750
+ if (command.packageCode === "ownership_review") return this.createOwnershipReviewCase(command);
751
+ const template = this.templateFor(command.packageCode);
752
+ const attributes = {
753
+ "inquiry-template-id": template.id,
754
+ "inquiry-template-version-id": template.version,
755
+ "reference-id": assertOpaqueReference(command.subjectReference)
756
+ };
757
+ const fields = legalPrefill(command);
758
+ if (Object.keys(fields).length > 0) attributes.fields = fields;
759
+ const result = await this.call("/inquiries", {
760
+ method: "POST",
761
+ operation: "create",
762
+ idempotencyKey: command.idempotencyKey,
763
+ body: { data: { attributes } }
764
+ });
765
+ const inquiry = requireResource(result.data, "inquiry");
766
+ const status = requireText(inquiry.attributes?.status, "Persona inquiry status");
767
+ return {
768
+ attemptId: command.attemptId,
769
+ providerResourceId: requireId(inquiry.id),
770
+ providerStatus: status,
771
+ canonicalStatus: this.normalizeStatus("inquiry", status).status,
772
+ launch: await this.launchFor(command.attemptId, inquiry)
773
+ };
774
+ }
775
+ async resumeAttempt(command) {
776
+ this.assertAllowedOrigin(command.requestOrigin);
777
+ if (isTransactionId(command.providerResourceId) || isCaseId(command.providerResourceId)) {
778
+ if (isTransactionId(command.providerResourceId)) await this.getTransaction(command.providerResourceId, "resume");
779
+ else await this.getCase(command.providerResourceId, "resume");
780
+ return this.nonInteractiveLaunch(command.attemptId, "processing");
781
+ }
782
+ const inquiry = await this.getInquiry(command.providerResourceId, "resume");
783
+ return this.launchFor(command.attemptId, inquiry);
784
+ }
785
+ async retrieveAttempt(command) {
786
+ const resourceKind = isTransactionId(command.providerResourceId) ? "transaction" : isCaseId(command.providerResourceId) ? "case" : "inquiry";
787
+ const resource = resourceKind === "transaction" ? await this.getTransaction(command.providerResourceId, "retrieve") : resourceKind === "case" ? await this.getCase(command.providerResourceId, "retrieve") : await this.getInquiry(command.providerResourceId, "retrieve");
788
+ const providerStatus = requireText(resource.attributes?.status, `Persona ${resourceKind} status`);
789
+ const normalized = this.normalizeStatus(resourceKind, providerStatus);
790
+ return {
791
+ providerResourceId: requireId(resource.id),
792
+ providerStatus,
793
+ canonicalStatus: normalized.status,
794
+ occurredAt: safeDate(resource.attributes?.["updated-at"] ?? resource.attributes?.["created-at"], this.runtime.now()),
795
+ normalizedReasonCodes: normalized.reasonCodes,
796
+ safeMetadata: {
797
+ source: "retrieve",
798
+ adapter_version: this.manifest.adapterVersion,
799
+ event_version: this.runtime.configuration.apiVersion,
800
+ normalization_version: PERSONA_NORMALIZATION_VERSION,
801
+ provider_environment: this.environment,
802
+ provider_event_category: resourceKind
803
+ }
804
+ };
805
+ }
806
+ async retryAttempt(command) {
807
+ this.assertCommand(command);
808
+ if (command.previousProviderResourceId) {
809
+ const previous = await this.retrieveAttempt({
810
+ attemptId: command.attemptId,
811
+ providerResourceId: command.previousProviderResourceId,
812
+ configurationRevision: command.configurationRevision,
813
+ requestOrigin: command.requestOrigin
814
+ });
815
+ if (RESUMABLE_STATUSES.has(previous.canonicalStatus)) {
816
+ return {
817
+ attemptId: command.attemptId,
818
+ providerResourceId: previous.providerResourceId,
819
+ providerStatus: previous.providerStatus,
820
+ canonicalStatus: previous.canonicalStatus,
821
+ launch: await this.resumeAttempt({
822
+ attemptId: command.attemptId,
823
+ providerResourceId: previous.providerResourceId,
824
+ configurationRevision: command.configurationRevision,
825
+ requestOrigin: command.requestOrigin
826
+ })
827
+ };
828
+ }
829
+ if (previous.canonicalStatus === "processing") {
830
+ throw new ProviderError("RETRYABLE_PROVIDER_FAILURE", "Persona is still processing this attempt.", {
831
+ retryable: true,
832
+ retryAfterSeconds: 15,
833
+ safeCode: "persona_attempt_processing"
834
+ });
835
+ }
836
+ if (previous.canonicalStatus === "manual_review_required" || previous.canonicalStatus === "verified") {
837
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "Persona review state cannot be retried.", {
838
+ safeCode: previous.canonicalStatus === "verified" ? "persona_attempt_already_verified" : "persona_manual_review_pending"
839
+ });
840
+ }
841
+ }
842
+ const idempotencyKey = this.runtime.idempotency.keyFor("retry", command.attemptId, command.idempotencyKey);
843
+ return this.createAttempt({ ...command, idempotencyKey });
844
+ }
845
+ async cancelAttempt(command) {
846
+ if (isTransactionId(command.providerResourceId) || isCaseId(command.providerResourceId)) {
847
+ throw new ProviderError("UNSUPPORTED_CAPABILITY", "Persona does not support canceling non-interactive transactions or cases.", {
848
+ safeCode: "persona_noninteractive_cancel_unsupported"
849
+ });
850
+ }
851
+ const result = await this.call(
852
+ `/inquiries/${encodeURIComponent(requireId(command.providerResourceId))}/expire`,
853
+ {
854
+ method: "POST",
855
+ operation: "cancel",
856
+ idempotencyScope: command.attemptId,
857
+ idempotencyKey: `cancel:${command.attemptId}`,
858
+ body: {}
859
+ }
860
+ );
861
+ const inquiry = requireResource(result.data, "inquiry");
862
+ const providerStatus = requireText(inquiry.attributes?.status, "Persona inquiry status");
863
+ return {
864
+ accepted: true,
865
+ providerStatus,
866
+ canonicalStatus: this.normalizeStatus("inquiry", providerStatus).status
867
+ };
868
+ }
869
+ async redactSubject(command) {
870
+ if (!command.providerResourceId) return { completed: true, retryable: false, disposition: "not_applicable" };
871
+ try {
872
+ const collection = redactionCollection(command.providerResourceType);
873
+ await this.call(`/${collection}/${encodeURIComponent(requireId(command.providerResourceId))}`, {
874
+ method: "DELETE",
875
+ operation: "redact",
876
+ idempotencyScope: command.requestReference,
877
+ idempotencyKey: command.requestReference,
878
+ allowNotFound: true
879
+ });
880
+ return { completed: true, retryable: false, disposition: "redacted" };
881
+ } catch (error) {
882
+ return {
883
+ completed: false,
884
+ retryable: error instanceof ProviderError && error.retryable,
885
+ disposition: error instanceof ProviderError && error.retryable ? "retryable" : "failed"
886
+ };
887
+ }
888
+ }
889
+ verifyWebhook(request) {
890
+ return verifyPersonaWebhook(request, {
891
+ secrets: personaWebhookSecrets(this.runtime.configuration),
892
+ expectedEnvironmentId: this.runtime.configuration.environmentId,
893
+ toleranceSeconds: this.runtime.configuration.webhookToleranceSeconds ?? this.manifest.webhook.toleranceSeconds,
894
+ now: this.runtime.now,
895
+ crypto: this.runtime.crypto
896
+ });
897
+ }
898
+ async normalizeWebhook(input) {
899
+ try {
900
+ const body = parseObject(new TextDecoder().decode(input.opaquePayload));
901
+ const event = requireResource(body.data, "event");
902
+ const name = requireText(event.attributes?.name, "Persona event name");
903
+ const eventAllowlisted = PERSONA_ALLOWED_EVENTS.has(name);
904
+ const payload = asRecord(event.attributes?.payload);
905
+ const resource = requireResource(payload.data);
906
+ const providerStatus = String(resource.attributes?.status ?? statusFromEvent(name));
907
+ const resourceKind = requireText(resource.type, "Persona resource type");
908
+ const normalized = eventAllowlisted ? this.normalizeStatus(resourceCategory(resourceKind), providerStatus) : { status: "manual_review_required", reasonCodes: ["persona_unknown_event"] };
909
+ const associatedPerson = findAssociatedPerson(payload);
910
+ const isCase = resourceKind === "case";
911
+ const isTransaction = resourceKind === "transaction";
912
+ return {
913
+ providerEventKey: input.providerEventKey,
914
+ providerResourceId: requireId(resource.id),
915
+ eventType: `verification.provider_event.${normalized.status}`,
916
+ providerEventType: name,
917
+ canonicalStatus: normalized.status,
918
+ occurredAt: safeDate(event.attributes?.["created-at"], this.runtime.now()),
919
+ normalizedReasonCodes: normalized.reasonCodes,
920
+ safeMetadata: {
921
+ adapter_version: this.manifest.adapterVersion,
922
+ event_version: this.runtime.configuration.apiVersion,
923
+ normalization_version: PERSONA_NORMALIZATION_VERSION,
924
+ provider_environment: this.environment,
925
+ provider_event_category: resourceKind,
926
+ provider_status: providerStatus,
927
+ event_allowlisted: eventAllowlisted,
928
+ reconcile_required: !eventAllowlisted || normalized.reasonCodes.includes("persona_unknown_status"),
929
+ ...isCase ? {
930
+ provider_case_id: requireId(resource.id),
931
+ case_status: providerStatus,
932
+ case_resolution: safeCode(resource.attributes?.resolution)
933
+ } : {},
934
+ ...isTransaction ? {
935
+ provider_transaction_id: requireId(resource.id),
936
+ transaction_status: providerStatus
937
+ } : {},
938
+ ...associatedPerson ? {
939
+ associated_subject_reference: associatedPerson.reference,
940
+ associated_inquiry_id: associatedPerson.inquiryId,
941
+ associated_requirement_kind: "associated_person"
942
+ } : {},
943
+ redacted: name.endsWith(".redacted")
944
+ }
945
+ };
946
+ } catch (error) {
947
+ if (error instanceof ProviderError && error.code === "TERMINAL_INPUT_FAILURE") throw error;
948
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "Persona webhook payload is invalid.", {
949
+ safeCode: "persona_webhook_payload_invalid",
950
+ cause: error
951
+ });
952
+ }
953
+ }
954
+ async retrieveCaseTree(providerCaseId) {
955
+ const result = await this.call(
956
+ `/cases/${encodeURIComponent(requireId(providerCaseId))}?include=inquiries,txns,reports,verifications`,
957
+ { method: "GET", operation: "retrieve" }
958
+ );
959
+ const reviewCase = requireResource(result.data, "case");
960
+ const relatedResources = [];
961
+ const associatedPersonRequirements = [];
962
+ let associatedPersonDiscoveryComplete = false;
963
+ for (const candidate of result.included ?? []) {
964
+ const rawType = requireText(candidate.type, "Persona related resource type");
965
+ const resourceType = caseRelatedResourceType(rawType);
966
+ if (!resourceType) continue;
967
+ const status = typeof candidate.attributes?.status === "string" ? candidate.attributes.status : "unknown";
968
+ const reference = candidate.attributes?.["reference-id"];
969
+ relatedResources.push({
970
+ resourceType,
971
+ resourceId: requireId(candidate.id),
972
+ providerStatus: status,
973
+ subjectReference: typeof reference === "string" && isOpaqueSubjectReference(reference) ? reference : null
974
+ });
975
+ if (resourceType === "transaction") {
976
+ const normalized = this.normalizeStatus("transaction", status).status;
977
+ if (["verified", "declined", "failed", "manual_review_required"].includes(normalized)) {
978
+ associatedPersonDiscoveryComplete = true;
979
+ }
980
+ associatedPersonRequirements.push(...parseAssociatedPersonRequirements(
981
+ candidate.attributes?.fields,
982
+ this.runtime.configuration.kybFieldMap.associatedPeople
983
+ ));
984
+ }
985
+ }
986
+ return {
987
+ caseId: requireId(reviewCase.id),
988
+ providerStatus: requireText(reviewCase.attributes?.status, "Persona case status"),
989
+ resolution: safeCode(reviewCase.attributes?.resolution),
990
+ occurredAt: safeDate(reviewCase.attributes?.["updated-at"] ?? reviewCase.attributes?.["created-at"], this.runtime.now()),
991
+ relatedResources,
992
+ associatedPersonRequirements,
993
+ associatedPersonDiscoveryComplete
994
+ };
995
+ }
996
+ assertCommand(command) {
997
+ if (!this.manifest.supportedPackages.includes(command.packageCode)) {
998
+ throw new ProviderError("UNSUPPORTED_CAPABILITY", "Persona does not support this verification package.", {
999
+ safeCode: "unsupported_package"
1000
+ });
1001
+ }
1002
+ if (!isOpaqueSubjectReference(command.subjectReference)) {
1003
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "The subject reference is not an opaque identifier.", {
1004
+ safeCode: "subject_reference_invalid"
1005
+ });
1006
+ }
1007
+ if (metadataContainsForbiddenIdentifier(command.metadata)) {
1008
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "Attempt metadata contains a forbidden identifier.", {
1009
+ safeCode: "forbidden_identifier"
1010
+ });
1011
+ }
1012
+ }
1013
+ assertAllowedOrigin(origin) {
1014
+ const allowed = this.runtime.configuration.allowedOrigins ?? [];
1015
+ if (!origin) {
1016
+ if (this.environment === "production") {
1017
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "Persona launch origin is missing.", {
1018
+ safeCode: "persona_origin_required"
1019
+ });
1020
+ }
1021
+ return;
1022
+ }
1023
+ let normalized;
1024
+ try {
1025
+ normalized = new URL(origin).origin;
1026
+ } catch {
1027
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "Persona launch origin is invalid.", {
1028
+ safeCode: "persona_origin_invalid"
1029
+ });
1030
+ }
1031
+ if (allowed.length > 0 && !allowed.includes(normalized)) {
1032
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "Persona launch origin is not allowlisted.", {
1033
+ safeCode: "persona_origin_denied"
1034
+ });
1035
+ }
1036
+ }
1037
+ templateFor(packageCode) {
1038
+ if (packageCode === "associated_person_idv") {
1039
+ return {
1040
+ id: this.runtime.configuration.associatedPersonTemplateId,
1041
+ version: this.runtime.configuration.associatedPersonTemplateVersion
1042
+ };
1043
+ }
1044
+ if (HUMAN_PACKAGES.has(packageCode) || INQUIRY_PACKAGES.has(packageCode)) {
1045
+ return {
1046
+ id: this.runtime.configuration.idvTemplateId,
1047
+ version: this.runtime.configuration.idvTemplateVersion
1048
+ };
1049
+ }
1050
+ throw new ProviderError("UNSUPPORTED_CAPABILITY", "Persona package is unsupported.", {
1051
+ safeCode: "unsupported_package"
1052
+ });
1053
+ }
1054
+ async getInquiry(providerResourceId, operation) {
1055
+ const result = await this.call(`/inquiries/${encodeURIComponent(requireId(providerResourceId))}`, {
1056
+ method: "GET",
1057
+ operation
1058
+ });
1059
+ return requireResource(result.data, "inquiry");
1060
+ }
1061
+ async getTransaction(providerResourceId, operation) {
1062
+ const result = await this.call(`/transactions/${encodeURIComponent(requireId(providerResourceId))}`, {
1063
+ method: "GET",
1064
+ operation
1065
+ });
1066
+ return requireResource(result.data, "transaction");
1067
+ }
1068
+ async getCase(providerResourceId, operation) {
1069
+ const result = await this.call(`/cases/${encodeURIComponent(requireId(providerResourceId))}`, {
1070
+ method: "GET",
1071
+ operation
1072
+ });
1073
+ return requireResource(result.data, "case");
1074
+ }
1075
+ async createOwnershipReviewCase(command) {
1076
+ const relationshipReference = assertOpaqueReference(command.subjectReference);
1077
+ const caseType = command.relationship?.kind === "property_owner" ? this.runtime.configuration.ownershipCaseType : this.runtime.configuration.businessAuthorityCaseType ?? this.runtime.configuration.caseType;
1078
+ const result = await this.call("/cases", {
1079
+ method: "POST",
1080
+ operation: "create",
1081
+ idempotencyKey: command.idempotencyKey,
1082
+ body: {
1083
+ data: {
1084
+ attributes: {
1085
+ "case-template-id": this.runtime.configuration.caseTemplateId,
1086
+ fields: {
1087
+ "case-type": caseType,
1088
+ "relationship-reference": relationshipReference
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ });
1094
+ const reviewCase = requireResource(result.data, "case");
1095
+ const providerStatus = requireText(reviewCase.attributes?.status, "Persona case status");
1096
+ return {
1097
+ attemptId: command.attemptId,
1098
+ providerResourceId: requireId(reviewCase.id),
1099
+ providerStatus,
1100
+ canonicalStatus: this.normalizeStatus("case", providerStatus).status,
1101
+ launch: this.nonInteractiveLaunch(command.attemptId, this.normalizeStatus("case", providerStatus).status),
1102
+ linkedResources: [{
1103
+ resourceType: "case",
1104
+ resourceId: requireId(reviewCase.id),
1105
+ relationshipCode: "initial_review_case",
1106
+ providerStatus,
1107
+ occurredAt: safeDate(reviewCase.attributes?.["created-at"], this.runtime.now())
1108
+ }]
1109
+ };
1110
+ }
1111
+ async createBusinessTransaction(command) {
1112
+ const fields = businessFields(command, this.runtime.configuration.kybFieldMap);
1113
+ const created = await this.call("/transactions", {
1114
+ method: "POST",
1115
+ operation: "create",
1116
+ idempotencyKey: command.idempotencyKey,
1117
+ body: {
1118
+ data: {
1119
+ attributes: {
1120
+ "transaction-type-id": this.runtime.configuration.kybTransactionTypeId,
1121
+ "workflow-id": this.runtime.configuration.kybWorkflowId,
1122
+ "workflow-version-id": this.runtime.configuration.kybWorkflowVersion,
1123
+ "reference-id": assertOpaqueReference(command.subjectReference),
1124
+ fields
1125
+ }
1126
+ }
1127
+ }
1128
+ });
1129
+ const transaction = requireResource(created.data, "transaction");
1130
+ const transactionId = requireId(transaction.id);
1131
+ const transactionStatus = requireText(transaction.attributes?.status, "Persona transaction status");
1132
+ let reviewCase = await this.findWorkflowManagedCase(transactionId);
1133
+ if (!reviewCase && (this.runtime.configuration.kybCaseMode ?? "workflow_managed") === "engine_managed") {
1134
+ const caseResponse = await this.call("/cases", {
1135
+ method: "POST",
1136
+ operation: "create",
1137
+ idempotencyKey: `${command.idempotencyKey}:case`,
1138
+ body: {
1139
+ data: {
1140
+ attributes: {
1141
+ "case-template-id": this.runtime.configuration.caseTemplateId,
1142
+ fields: { "case-type": this.runtime.configuration.caseType }
1143
+ }
1144
+ }
1145
+ }
1146
+ });
1147
+ reviewCase = requireResource(caseResponse.data, "case");
1148
+ await this.call(`/cases/${encodeURIComponent(requireId(reviewCase.id))}/add-objects`, {
1149
+ method: "POST",
1150
+ operation: "create",
1151
+ idempotencyKey: `${command.idempotencyKey}:case-link`,
1152
+ body: { meta: { "object-ids": [transactionId] } }
1153
+ });
1154
+ }
1155
+ const linkedResources = [{
1156
+ resourceType: "transaction",
1157
+ resourceId: transactionId,
1158
+ relationshipCode: "kyb_transaction",
1159
+ providerStatus: transactionStatus,
1160
+ occurredAt: safeDate(transaction.attributes?.["created-at"], this.runtime.now())
1161
+ }];
1162
+ if (reviewCase) {
1163
+ linkedResources.push({
1164
+ resourceType: "case",
1165
+ resourceId: requireId(reviewCase.id),
1166
+ relationshipCode: "initial_review_case",
1167
+ providerStatus: requireText(reviewCase.attributes?.status, "Persona case status"),
1168
+ occurredAt: safeDate(reviewCase.attributes?.["created-at"], this.runtime.now())
1169
+ });
1170
+ }
1171
+ return {
1172
+ attemptId: command.attemptId,
1173
+ providerResourceId: transactionId,
1174
+ providerStatus: transactionStatus,
1175
+ canonicalStatus: this.normalizeStatus("transaction", transactionStatus).status,
1176
+ launch: this.nonInteractiveLaunch(command.attemptId, this.normalizeStatus("transaction", transactionStatus).status),
1177
+ linkedResources
1178
+ };
1179
+ }
1180
+ async findWorkflowManagedCase(transactionId) {
1181
+ const result = await this.call(
1182
+ `/transactions/${encodeURIComponent(transactionId)}?include=related-objects`,
1183
+ { method: "GET", operation: "retrieve" }
1184
+ );
1185
+ for (const resource of result.included ?? []) {
1186
+ if (resource.type === "case") return requireResource(resource, "case");
1187
+ }
1188
+ const transaction = requireResource(result.data, "transaction");
1189
+ const related = asOptionalRecord(transaction.relationships?.["related-objects"]);
1190
+ if (Array.isArray(related?.data)) {
1191
+ const relationship = related.data.find((value) => asOptionalRecord(value)?.type === "case");
1192
+ const caseId = asOptionalRecord(relationship)?.id;
1193
+ if (typeof caseId === "string") return this.getCase(requireId(caseId), "retrieve");
1194
+ }
1195
+ return null;
1196
+ }
1197
+ normalizeStatus(resourceKind, providerStatus) {
1198
+ return resolvePersonaStatus(resourceKind, providerStatus, this.runtime.configuration.statusMappings);
1199
+ }
1200
+ async launchFor(attemptId, inquiry) {
1201
+ const inquiryId = requireId(inquiry.id);
1202
+ const status = requireText(inquiry.attributes?.status, "Persona inquiry status").toLowerCase();
1203
+ const canonical = this.normalizeStatus("inquiry", status).status;
1204
+ let sessionToken;
1205
+ if (status !== "created") {
1206
+ const resumed = await this.call(`/inquiries/${encodeURIComponent(inquiryId)}/resume`, {
1207
+ method: "POST",
1208
+ operation: "resume",
1209
+ idempotencyScope: inquiryId,
1210
+ body: {}
1211
+ });
1212
+ sessionToken = requireText(resumed.meta?.["session-token"], "Persona session token");
1213
+ }
1214
+ const hostedUrl = `https://${PERSONA_INQUIRY_HOST}/verify?inquiry-id=${encodeURIComponent(inquiryId)}`;
1215
+ return {
1216
+ attemptId,
1217
+ canonicalStatus: canonical,
1218
+ presentation: sessionToken ? "embedded" : "hosted",
1219
+ launcherKey: sessionToken ? "persona_embedded" : "hosted",
1220
+ providerDisclosure: PERSONA_DISCLOSURE,
1221
+ transientSecret: sessionToken,
1222
+ transientSecretExpiresAt: sessionToken ? safeOptionalDate(inquiry.attributes?.["expires-at"]) : void 0,
1223
+ hostedUrl: status === "created" || !sessionToken ? hostedUrl : hostedUrl,
1224
+ hostedFallbackExpiresAt: safeOptionalDate(inquiry.attributes?.["expires-at"]),
1225
+ continuationReference: inquiryId
1226
+ };
1227
+ }
1228
+ nonInteractiveLaunch(attemptId, canonicalStatus) {
1229
+ return {
1230
+ attemptId,
1231
+ canonicalStatus,
1232
+ presentation: "none",
1233
+ launcherKey: "hosted",
1234
+ providerDisclosure: PERSONA_DISCLOSURE,
1235
+ continuationReference: attemptId
1236
+ };
1237
+ }
1238
+ async call(path, options) {
1239
+ if (this.runtime.rateBudget) {
1240
+ const budget = await this.runtime.rateBudget.acquire(options.operation);
1241
+ if (!budget.allowed) {
1242
+ throw new ProviderError("RATE_LIMITED", "Persona rate budget is exhausted.", {
1243
+ retryable: true,
1244
+ retryAfterSeconds: budget.retryAfterSeconds,
1245
+ safeCode: "persona_rate_limited"
1246
+ });
1247
+ }
1248
+ }
1249
+ const startedAt = this.runtime.now().getTime();
1250
+ const idempotencyKey = options.method === "GET" ? void 0 : this.runtime.idempotency.keyFor(options.operation, options.idempotencyScope ?? path, options.idempotencyKey);
1251
+ const controller = new AbortController();
1252
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
1253
+ try {
1254
+ const response = await this.runtime.http.fetch(`https://${PERSONA_API_HOST}/api/v1${path}`, {
1255
+ method: options.method,
1256
+ headers: {
1257
+ Accept: "application/json",
1258
+ Authorization: `Bearer ${this.runtime.configuration.apiKey}`,
1259
+ "Content-Type": "application/json",
1260
+ "Persona-Version": this.runtime.configuration.apiVersion,
1261
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
1262
+ },
1263
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
1264
+ signal: controller.signal
1265
+ });
1266
+ if (!response.ok && !(options.allowNotFound && response.status === 404)) {
1267
+ throw mapPersonaHttpError(response.status, response.headers.get("retry-after"));
1268
+ }
1269
+ const environmentId = response.headers.get("Persona-Environment-Id");
1270
+ if (environmentId && environmentId !== this.runtime.configuration.environmentId) {
1271
+ throw new ProviderError("TERMINAL_INPUT_FAILURE", "Persona response environment did not match the pinned route.", {
1272
+ safeCode: "persona_environment_mismatch"
1273
+ });
1274
+ }
1275
+ if (response.status === 204 || options.allowNotFound && response.status === 404) {
1276
+ await this.recordObservation(options.operation, "success", `persona_${options.operation}_ok`, startedAt);
1277
+ return {};
1278
+ }
1279
+ const result = await response.json().catch(() => {
1280
+ throw new ProviderError("UNKNOWN_PROVIDER_STATE", "Persona returned a malformed response.", {
1281
+ safeCode: "malformed_provider_response"
1282
+ });
1283
+ });
1284
+ const unknown = this.responseHasUnknownStatus(result);
1285
+ await this.recordObservation(
1286
+ options.operation,
1287
+ unknown ? "unknown_status" : "success",
1288
+ unknown ? "persona_unknown_status" : `persona_${options.operation}_ok`,
1289
+ startedAt
1290
+ );
1291
+ return result;
1292
+ } catch (error) {
1293
+ const failure = error instanceof ProviderError ? error : error instanceof DOMException && error.name === "AbortError" ? new ProviderError("TIMEOUT", "Persona request timed out.", {
1294
+ retryable: true,
1295
+ safeCode: "persona_timeout"
1296
+ }) : new ProviderError("RETRYABLE_PROVIDER_FAILURE", "Persona verification is temporarily unavailable.", {
1297
+ retryable: true,
1298
+ safeCode: "persona_provider_failure",
1299
+ cause: error
1300
+ });
1301
+ await this.recordObservation(
1302
+ options.operation,
1303
+ failure.retryable ? "retryable_failure" : "terminal_failure",
1304
+ failure.safeCode,
1305
+ startedAt
1306
+ );
1307
+ throw failure;
1308
+ } finally {
1309
+ clearTimeout(timeoutId);
1310
+ }
1311
+ }
1312
+ responseHasUnknownStatus(value) {
1313
+ const result = asOptionalRecord(value);
1314
+ const resource = asOptionalRecord(result?.data);
1315
+ const attributes = asOptionalRecord(resource?.attributes);
1316
+ if (typeof resource?.type !== "string" || typeof attributes?.status !== "string") return false;
1317
+ return this.normalizeStatus(resourceCategory(resource.type), attributes.status).reasonCodes.includes("persona_unknown_status");
1318
+ }
1319
+ async recordObservation(operation, outcome, safeCode2, startedAt) {
1320
+ const observedAt = this.runtime.now();
1321
+ const latencyMs = Math.max(0, observedAt.getTime() - startedAt);
1322
+ const metadata = {
1323
+ provider: this.provider,
1324
+ environment: this.environment,
1325
+ operation,
1326
+ outcome,
1327
+ safe_code: safeCode2,
1328
+ latency_ms: latencyMs
1329
+ };
1330
+ this.runtime.telemetry?.histogram?.("verification.provider.latency_ms", latencyMs, {
1331
+ provider: this.provider,
1332
+ operation
1333
+ });
1334
+ try {
1335
+ await this.runtime.recordHealth({
1336
+ operation,
1337
+ outcome,
1338
+ safeCode: safeCode2,
1339
+ observedAt: observedAt.toISOString(),
1340
+ latencyMs
1341
+ });
1342
+ if (outcome === "success") this.runtime.logger.info("verification_provider_operation", metadata);
1343
+ else this.runtime.logger.warn("verification_provider_operation", metadata);
1344
+ } catch {
1345
+ this.runtime.logger.warn("verification_provider_health_record_failed", {
1346
+ provider: this.provider,
1347
+ environment: this.environment,
1348
+ operation
1349
+ });
1350
+ }
1351
+ }
1352
+ };
1353
+
1354
+ export { DEFAULT_KYB_FIELD_MAP, PersonaVerificationAdapter, createPersonaConfiguration, normalizePersonaStatus, personaProviderManifest, verifyPersonaWebhook };
1355
+ //# sourceMappingURL=index.js.map
1356
+ //# sourceMappingURL=index.js.map