@splitin/verification-adapter-stripe-identity 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/LICENSE +21 -0
- package/NOTICE +32 -0
- package/README.md +49 -0
- package/dist/browser.cjs +43 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +21 -0
- package/dist/browser.d.ts +21 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +40 -0
- package/dist/browser.js.map +1 -0
- package/dist/index.cjs +766 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +56 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +760 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { defineProviderManifest, VERIFICATION_ADAPTER_CONTRACT_VERSION, secretStringProperty, plainStringProperty, ProviderError, ProviderRequiredInformationError, isOpaqueSubjectReference, metadataContainsForbiddenIdentifier, ProviderUnavailableError } from '@splitin/verification-adapter-sdk';
|
|
2
|
+
|
|
3
|
+
// src/adapter.ts
|
|
4
|
+
|
|
5
|
+
// src/constants.ts
|
|
6
|
+
var STRIPE_IDENTITY_API_VERSION = "2025-08-27.basil";
|
|
7
|
+
var STRIPE_IDENTITY_API_HOST = "api.stripe.com";
|
|
8
|
+
var STRIPE_IDENTITY_HOSTED_HOST = "verify.stripe.com";
|
|
9
|
+
var STRIPE_IDENTITY_DISCLOSURE = "Powered by Stripe";
|
|
10
|
+
var STRIPE_IDENTITY_EVENTS = /* @__PURE__ */ new Set([
|
|
11
|
+
"identity.verification_session.processing",
|
|
12
|
+
"identity.verification_session.verified",
|
|
13
|
+
"identity.verification_session.requires_input",
|
|
14
|
+
"identity.verification_session.canceled",
|
|
15
|
+
"identity.verification_session.redacted"
|
|
16
|
+
]);
|
|
17
|
+
var MANUAL_REVIEW_ERRORS = /* @__PURE__ */ new Set([
|
|
18
|
+
"consent_declined",
|
|
19
|
+
"country_not_supported",
|
|
20
|
+
"device_unsupported",
|
|
21
|
+
"document_unverified_other",
|
|
22
|
+
"selfie_document_missing_photo",
|
|
23
|
+
"selfie_face_mismatch",
|
|
24
|
+
"selfie_manipulated",
|
|
25
|
+
"selfie_unverified_other"
|
|
26
|
+
]);
|
|
27
|
+
var TERMINAL_DECLINE_ERRORS = /* @__PURE__ */ new Set(["under_supported_age"]);
|
|
28
|
+
|
|
29
|
+
// src/configuration.ts
|
|
30
|
+
function createStripeIdentityConfiguration(values) {
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
restrictedKey: firstValue(values, "restrictedKey", "STRIPE_IDENTITY_RESTRICTED_KEY"),
|
|
33
|
+
accountId: firstValue(values, "accountId", "STRIPE_IDENTITY_ACCOUNT_ID"),
|
|
34
|
+
webhookSecret: firstValue(values, "webhookSecret", "STRIPE_IDENTITY_WEBHOOK_SECRET"),
|
|
35
|
+
webhookSecretPrevious: optionalValue(values, "webhookSecretPrevious", "STRIPE_IDENTITY_WEBHOOK_SECRET_PREVIOUS"),
|
|
36
|
+
apiVersion: firstValue(values, "apiVersion", "STRIPE_IDENTITY_API_VERSION") || STRIPE_IDENTITY_API_VERSION,
|
|
37
|
+
returnUrl: optionalValue(values, "returnUrl", "STRIPE_IDENTITY_RETURN_URL"),
|
|
38
|
+
webhookToleranceSeconds: parseTolerance(
|
|
39
|
+
values.webhookToleranceSeconds ?? values.STRIPE_IDENTITY_WEBHOOK_TOLERANCE_SECONDS
|
|
40
|
+
),
|
|
41
|
+
requireMatchingSelfie: parseBoolean(
|
|
42
|
+
values.requireMatchingSelfie ?? values.STRIPE_IDENTITY_REQUIRE_MATCHING_SELFIE
|
|
43
|
+
)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function webhookSecretsFromConfig(config) {
|
|
47
|
+
return [config.webhookSecret, config.webhookSecretPrevious].map((value) => value?.trim() ?? "").filter((value) => value.startsWith("whsec_"));
|
|
48
|
+
}
|
|
49
|
+
function validateStripeIdentityConfiguration(config, environment) {
|
|
50
|
+
const expectedPrefix = environment === "production" ? "rk_live_" : "rk_test_";
|
|
51
|
+
if (!config.restrictedKey.startsWith(expectedPrefix)) {
|
|
52
|
+
throw new ProviderError("INVALID_CONFIGURATION", "Stripe Identity credentials do not match the pinned environment.", {
|
|
53
|
+
safeCode: "stripe_identity_credential_environment_mismatch"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (webhookSecretsFromConfig(config).length === 0) {
|
|
57
|
+
throw new ProviderError("INVALID_CONFIGURATION", "Stripe Identity webhook authentication is not configured.", {
|
|
58
|
+
safeCode: "stripe_identity_webhook_secret_missing"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (config.apiVersion !== STRIPE_IDENTITY_API_VERSION) {
|
|
62
|
+
throw new ProviderError("INVALID_CONFIGURATION", "Stripe Identity API version does not match the reviewed contract.", {
|
|
63
|
+
safeCode: "stripe_identity_api_version_mismatch"
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (!/^acct_[A-Za-z0-9]{8,252}$/.test(config.accountId)) {
|
|
67
|
+
throw new ProviderError("INVALID_CONFIGURATION", "Stripe Identity account is invalid.", {
|
|
68
|
+
safeCode: "stripe_identity_account_invalid"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (config.returnUrl) assertHttpsReturnUrl(config.returnUrl);
|
|
72
|
+
if (config.webhookToleranceSeconds !== void 0 && (!Number.isInteger(config.webhookToleranceSeconds) || config.webhookToleranceSeconds < 60 || config.webhookToleranceSeconds > 900)) {
|
|
73
|
+
throw new ProviderError("INVALID_CONFIGURATION", "Stripe Identity webhook tolerance is invalid.", {
|
|
74
|
+
safeCode: "stripe_identity_webhook_tolerance_invalid"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function assertHttpsReturnUrl(value) {
|
|
79
|
+
let url;
|
|
80
|
+
try {
|
|
81
|
+
url = new URL(value);
|
|
82
|
+
} catch {
|
|
83
|
+
throw new ProviderError("INVALID_CONFIGURATION", "Stripe Identity return URL is invalid.", {
|
|
84
|
+
safeCode: "stripe_identity_return_url_invalid"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (url.protocol !== "https:" || Boolean(url.username || url.password)) {
|
|
88
|
+
throw new ProviderError("INVALID_CONFIGURATION", "Stripe Identity return URL is invalid.", {
|
|
89
|
+
safeCode: "stripe_identity_return_url_invalid"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function firstValue(values, camel, conventional) {
|
|
94
|
+
return values[camel]?.trim() || values[conventional]?.trim() || "";
|
|
95
|
+
}
|
|
96
|
+
function optionalValue(values, camel, conventional) {
|
|
97
|
+
const value = firstValue(values, camel, conventional);
|
|
98
|
+
return value || void 0;
|
|
99
|
+
}
|
|
100
|
+
function parseTolerance(value) {
|
|
101
|
+
if (value == null || value === "") return void 0;
|
|
102
|
+
const parsed = Number(value);
|
|
103
|
+
return Number.isInteger(parsed) && parsed >= 60 && parsed <= 900 ? parsed : 300;
|
|
104
|
+
}
|
|
105
|
+
function parseBoolean(value) {
|
|
106
|
+
if (value == null || value === "") return void 0;
|
|
107
|
+
const normalized = value.trim().toLowerCase();
|
|
108
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
|
|
109
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") return false;
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
var stripeIdentityProviderManifest = defineProviderManifest({
|
|
113
|
+
contractVersion: VERIFICATION_ADAPTER_CONTRACT_VERSION,
|
|
114
|
+
adapterVersion: "1.0.0",
|
|
115
|
+
engineCompatibility: "1.0.0",
|
|
116
|
+
provider: "stripe_identity",
|
|
117
|
+
displayName: "Stripe Identity",
|
|
118
|
+
description: "Human document identity verification via Stripe Identity. Connect, Payments, banks, and payouts are out of scope.",
|
|
119
|
+
supportedPackages: ["human_idv"],
|
|
120
|
+
supportedCountries: ["US"],
|
|
121
|
+
environments: ["sandbox", "production"],
|
|
122
|
+
capabilities: {
|
|
123
|
+
presentations: ["embedded", "hosted", "none"],
|
|
124
|
+
canResume: true,
|
|
125
|
+
canRetry: true,
|
|
126
|
+
canCancel: true,
|
|
127
|
+
canRedact: true
|
|
128
|
+
},
|
|
129
|
+
launcherKeys: ["stripe_identity", "hosted"],
|
|
130
|
+
launchPresentations: ["embedded", "hosted", "none"],
|
|
131
|
+
configurationSchemaVersion: "urn:splitin:verification:config:stripe-identity:v1",
|
|
132
|
+
configurationSchema: {
|
|
133
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
134
|
+
type: "object",
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
required: ["restrictedKey", "accountId", "webhookSecret", "apiVersion"],
|
|
137
|
+
properties: {
|
|
138
|
+
restrictedKey: secretStringProperty(),
|
|
139
|
+
accountId: plainStringProperty(),
|
|
140
|
+
webhookSecret: secretStringProperty(),
|
|
141
|
+
webhookSecretPrevious: { type: "string", minLength: 1, "x-secret": true },
|
|
142
|
+
apiVersion: { type: "string", const: STRIPE_IDENTITY_API_VERSION },
|
|
143
|
+
returnUrl: { type: "string", format: "uri", "x-secret": false },
|
|
144
|
+
webhookToleranceSeconds: { type: "integer", minimum: 60, maximum: 900 },
|
|
145
|
+
requireMatchingSelfie: { type: "boolean" }
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
webhook: {
|
|
149
|
+
protocol: "stripe_v1_hmac",
|
|
150
|
+
eventFamilies: [
|
|
151
|
+
"identity.verification_session.processing",
|
|
152
|
+
"identity.verification_session.verified",
|
|
153
|
+
"identity.verification_session.requires_input",
|
|
154
|
+
"identity.verification_session.canceled",
|
|
155
|
+
"identity.verification_session.redacted"
|
|
156
|
+
],
|
|
157
|
+
toleranceSeconds: 300
|
|
158
|
+
},
|
|
159
|
+
dataPolicy: {
|
|
160
|
+
classifications: ["provider_resource_id", "normalized_status", "reason_codes"],
|
|
161
|
+
prohibitedPersistence: ["raw_webhook", "launch_secret", "document", "selfie", "client_secret"],
|
|
162
|
+
rawPayloadPersistence: false,
|
|
163
|
+
browserSecretPersistence: false,
|
|
164
|
+
governmentIdentifierPersistence: false
|
|
165
|
+
},
|
|
166
|
+
retry: { sameResourceWhenResumable: true, newAttemptAfterTerminal: true },
|
|
167
|
+
cancellation: { supported: true, terminal: true },
|
|
168
|
+
redaction: { supported: true, asynchronous: true },
|
|
169
|
+
apiHosts: ["api.stripe.com"],
|
|
170
|
+
testedApiVersions: [STRIPE_IDENTITY_API_VERSION]
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// src/status.ts
|
|
174
|
+
function normalizeStripeIdentityStatus(status, lastErrorCode, eventType, redactionStatus) {
|
|
175
|
+
const reasonCodes = lastErrorCode && /^[a-z0-9_]{1,96}$/.test(lastErrorCode) ? [lastErrorCode] : [];
|
|
176
|
+
if (eventType === "identity.verification_session.redacted" || redactionStatus === "redacted") {
|
|
177
|
+
return { canonicalStatus: "redacted", reasonCodes: [...reasonCodes, "provider_redacted"] };
|
|
178
|
+
}
|
|
179
|
+
if (redactionStatus === "processing") {
|
|
180
|
+
return { canonicalStatus: "processing", reasonCodes: [...reasonCodes, "provider_redaction_processing"] };
|
|
181
|
+
}
|
|
182
|
+
if (status === "verified") return { canonicalStatus: "verified", reasonCodes };
|
|
183
|
+
if (status === "processing") return { canonicalStatus: "processing", reasonCodes };
|
|
184
|
+
if (status === "canceled") return { canonicalStatus: "canceled", reasonCodes };
|
|
185
|
+
if (status === "requires_input") {
|
|
186
|
+
return {
|
|
187
|
+
canonicalStatus: lastErrorCode && TERMINAL_DECLINE_ERRORS.has(lastErrorCode) ? "declined" : lastErrorCode && MANUAL_REVIEW_ERRORS.has(lastErrorCode) ? "manual_review_required" : "pending_user_input",
|
|
188
|
+
reasonCodes
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return { canonicalStatus: "manual_review_required", reasonCodes: ["stripe_unknown_status"] };
|
|
192
|
+
}
|
|
193
|
+
function isRecord(value) {
|
|
194
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
195
|
+
}
|
|
196
|
+
function requireSession(value) {
|
|
197
|
+
if (!isRecord(value) || typeof value.id !== "string" || !/^vs_[A-Za-z0-9_]{8,252}$/.test(value.id) || typeof value.status !== "string" || typeof value.created !== "number" || typeof value.livemode !== "boolean") {
|
|
198
|
+
throw new ProviderError("UNKNOWN_PROVIDER_STATE", "Stripe Identity returned an invalid session.", {
|
|
199
|
+
safeCode: "malformed_provider_response"
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
function encodeSessionId(value) {
|
|
205
|
+
if (!/^vs_[A-Za-z0-9_]{8,252}$/.test(value)) throw new ProviderRequiredInformationError("Stripe Identity session id is invalid.");
|
|
206
|
+
return encodeURIComponent(value);
|
|
207
|
+
}
|
|
208
|
+
function safeClientSecret(value) {
|
|
209
|
+
return typeof value === "string" && /^vs_[A-Za-z0-9_]+_secret_[A-Za-z0-9_]+$/.test(value) ? value : void 0;
|
|
210
|
+
}
|
|
211
|
+
function safeStripeHostedUrl(value, hostedHost) {
|
|
212
|
+
if (typeof value !== "string") return void 0;
|
|
213
|
+
try {
|
|
214
|
+
const url = new URL(value);
|
|
215
|
+
return url.protocol === "https:" && url.hostname === hostedHost && !url.username && !url.password ? url.toString() : void 0;
|
|
216
|
+
} catch {
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function safeEmail(value) {
|
|
221
|
+
const normalized = value.trim().toLowerCase();
|
|
222
|
+
if (normalized.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
|
|
223
|
+
throw new ProviderRequiredInformationError();
|
|
224
|
+
}
|
|
225
|
+
return normalized;
|
|
226
|
+
}
|
|
227
|
+
function parseRetryAfter(value) {
|
|
228
|
+
if (!value || !/^\d{1,6}$/.test(value)) return void 0;
|
|
229
|
+
return Math.min(Number(value), 3600);
|
|
230
|
+
}
|
|
231
|
+
function isoExpiry(observedAt, ttlSeconds) {
|
|
232
|
+
return new Date(observedAt.getTime() + ttlSeconds * 1e3).toISOString();
|
|
233
|
+
}
|
|
234
|
+
async function sha256Hex(crypto, payload) {
|
|
235
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", Uint8Array.from(payload)));
|
|
236
|
+
return toHex(digest);
|
|
237
|
+
}
|
|
238
|
+
function timingSafeEqualHex(left, right) {
|
|
239
|
+
if (left.length !== right.length) return false;
|
|
240
|
+
let difference = 0;
|
|
241
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
242
|
+
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
243
|
+
}
|
|
244
|
+
return difference === 0;
|
|
245
|
+
}
|
|
246
|
+
function toHex(value) {
|
|
247
|
+
return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
248
|
+
}
|
|
249
|
+
function mapProviderHttpError(status, retryAfterHeader, providerLabel) {
|
|
250
|
+
const retryAfter = parseRetryAfter(retryAfterHeader);
|
|
251
|
+
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";
|
|
252
|
+
const prefix = providerLabel.replace(/[^a-z0-9]+/g, "_");
|
|
253
|
+
return new ProviderError(code, `${providerLabel} request failed.`, {
|
|
254
|
+
retryable: status === 408 || status === 429 || status >= 500,
|
|
255
|
+
retryAfterSeconds: retryAfter,
|
|
256
|
+
safeCode: code === "AUTHENTICATION_FAILED" ? `${prefix}_authentication_failed` : code === "RATE_LIMITED" ? `${prefix}_rate_limited` : code === "TIMEOUT" ? `${prefix}_timeout` : code === "RETRYABLE_PROVIDER_FAILURE" ? `${prefix}_provider_failure` : `${prefix}_terminal_input_failure`
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async function verifyStripeIdentityWebhook(request, options) {
|
|
260
|
+
const rawBody = new Uint8Array(await request.arrayBuffer());
|
|
261
|
+
if (rawBody.byteLength === 0 || rawBody.byteLength > 1048576) {
|
|
262
|
+
throw new ProviderError("SIGNATURE_INVALID", "Stripe Identity webhook body is invalid.", {
|
|
263
|
+
safeCode: "stripe_identity_webhook_body_invalid"
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const signature = parseStripeSignature(request.headers.get("stripe-signature"));
|
|
267
|
+
const nowSeconds = Math.floor(options.now.getTime() / 1e3);
|
|
268
|
+
if (Math.abs(nowSeconds - signature.timestamp) > options.toleranceSeconds) {
|
|
269
|
+
throw new ProviderError("SIGNATURE_INVALID", "Stripe Identity webhook signature is stale.", {
|
|
270
|
+
safeCode: "stripe_identity_webhook_replay"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const prefix = new TextEncoder().encode(`${signature.timestamp}.`);
|
|
274
|
+
const signedPayload = new Uint8Array(prefix.length + rawBody.length);
|
|
275
|
+
signedPayload.set(prefix);
|
|
276
|
+
signedPayload.set(rawBody, prefix.length);
|
|
277
|
+
let matched = false;
|
|
278
|
+
for (const secret of options.secrets) {
|
|
279
|
+
const digest = await hmacSha256Hex(options.crypto, secret, signedPayload);
|
|
280
|
+
if (signature.v1.some((candidate) => timingSafeEqualHex(digest, candidate))) matched = true;
|
|
281
|
+
}
|
|
282
|
+
if (!matched) {
|
|
283
|
+
throw new ProviderError("SIGNATURE_INVALID", "Stripe Identity webhook signature is invalid.", {
|
|
284
|
+
safeCode: "stripe_identity_webhook_signature_invalid"
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const bodySha256 = await sha256Hex(options.crypto, rawBody);
|
|
288
|
+
let providerEventKey = `stripe_${bodySha256}`;
|
|
289
|
+
try {
|
|
290
|
+
const parsed = parseStripeEvent(rawBody);
|
|
291
|
+
assertLivemode(parsed.livemode, options.environment);
|
|
292
|
+
providerEventKey = parsed.id;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (!(error instanceof ProviderError) || error.safeCode !== "stripe_webhook_payload_invalid") throw error;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
providerEventKey,
|
|
298
|
+
receivedAt: options.now.toISOString(),
|
|
299
|
+
bodySha256,
|
|
300
|
+
signatureIssuedAt: new Date(signature.timestamp * 1e3).toISOString(),
|
|
301
|
+
opaquePayload: rawBody
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function parseStripeEvent(rawBody) {
|
|
305
|
+
let parsed;
|
|
306
|
+
try {
|
|
307
|
+
parsed = JSON.parse(new TextDecoder().decode(rawBody));
|
|
308
|
+
} catch {
|
|
309
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "Stripe Identity webhook payload is invalid.", {
|
|
310
|
+
safeCode: "stripe_webhook_payload_invalid"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (!isRecord(parsed) || typeof parsed.id !== "string" || !/^evt_[A-Za-z0-9_]{8,252}$/.test(parsed.id) || typeof parsed.type !== "string" || typeof parsed.created !== "number" || typeof parsed.livemode !== "boolean" || !isRecord(parsed.data) || !isRecord(parsed.data.object)) {
|
|
314
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "Stripe Identity webhook event is invalid.", {
|
|
315
|
+
safeCode: "stripe_webhook_payload_invalid"
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return parsed;
|
|
319
|
+
}
|
|
320
|
+
function eventIsAllowlisted(eventType) {
|
|
321
|
+
return STRIPE_IDENTITY_EVENTS.has(eventType);
|
|
322
|
+
}
|
|
323
|
+
function sessionFromEvent(event) {
|
|
324
|
+
return requireSession(event.data.object);
|
|
325
|
+
}
|
|
326
|
+
function normalizeAllowlistedEvent(event) {
|
|
327
|
+
const session = sessionFromEvent(event);
|
|
328
|
+
const allowlisted = eventIsAllowlisted(event.type);
|
|
329
|
+
return allowlisted ? normalizeStripeIdentityStatus(
|
|
330
|
+
session.status,
|
|
331
|
+
session.last_error?.code ?? null,
|
|
332
|
+
event.type,
|
|
333
|
+
session.redaction?.status ?? null
|
|
334
|
+
) : { canonicalStatus: "manual_review_required", reasonCodes: ["stripe_unknown_event"] };
|
|
335
|
+
}
|
|
336
|
+
function parseStripeSignature(value) {
|
|
337
|
+
if (!value || value.length > 4096) {
|
|
338
|
+
throw new ProviderError("SIGNATURE_INVALID", "Stripe Identity signature is missing.", {
|
|
339
|
+
safeCode: "stripe_identity_signature_missing"
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
let timestamp = null;
|
|
343
|
+
const v1 = [];
|
|
344
|
+
for (const part of value.split(",")) {
|
|
345
|
+
const [key, candidate] = part.trim().split("=", 2);
|
|
346
|
+
if (key === "t" && /^\d{1,16}$/.test(candidate ?? "")) timestamp = Number(candidate);
|
|
347
|
+
if (key === "v1" && /^[a-f0-9]{64}$/i.test(candidate ?? "")) v1.push((candidate ?? "").toLowerCase());
|
|
348
|
+
}
|
|
349
|
+
if (!timestamp || !Number.isSafeInteger(timestamp) || v1.length === 0) {
|
|
350
|
+
throw new ProviderError("SIGNATURE_INVALID", "Stripe Identity signature is invalid.", {
|
|
351
|
+
safeCode: "stripe_identity_signature_invalid"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return { timestamp, v1 };
|
|
355
|
+
}
|
|
356
|
+
function assertLivemode(livemode, environment) {
|
|
357
|
+
if (livemode !== (environment === "production")) {
|
|
358
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "Stripe Identity mode does not match the pinned environment.", {
|
|
359
|
+
safeCode: "stripe_environment_mismatch"
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function hmacSha256Hex(crypto, secret, payload) {
|
|
364
|
+
const key = await crypto.subtle.importKey(
|
|
365
|
+
"raw",
|
|
366
|
+
new TextEncoder().encode(secret),
|
|
367
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
368
|
+
false,
|
|
369
|
+
["sign"]
|
|
370
|
+
);
|
|
371
|
+
const digest = new Uint8Array(await crypto.subtle.sign("HMAC", key, payload));
|
|
372
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/adapter.ts
|
|
376
|
+
var StripeIdentityVerificationAdapter = class {
|
|
377
|
+
contractVersion = VERIFICATION_ADAPTER_CONTRACT_VERSION;
|
|
378
|
+
manifest = stripeIdentityProviderManifest;
|
|
379
|
+
provider = "stripe_identity";
|
|
380
|
+
environment;
|
|
381
|
+
runtime;
|
|
382
|
+
constructor(runtime) {
|
|
383
|
+
this.runtime = runtime;
|
|
384
|
+
this.environment = runtime.environment;
|
|
385
|
+
this.validateConfiguration();
|
|
386
|
+
}
|
|
387
|
+
validateConfiguration() {
|
|
388
|
+
validateStripeIdentityConfiguration(this.runtime.configuration, this.environment);
|
|
389
|
+
}
|
|
390
|
+
async createAttempt(command) {
|
|
391
|
+
this.assertCommand(command);
|
|
392
|
+
const body = new URLSearchParams();
|
|
393
|
+
body.set("type", "document");
|
|
394
|
+
body.set("client_reference_id", await sha256Hex(this.runtime.crypto, new TextEncoder().encode(command.subjectReference)));
|
|
395
|
+
body.set("metadata[attempt_id]", command.attemptId);
|
|
396
|
+
body.set("options[document][require_matching_selfie]", this.runtime.configuration.requireMatchingSelfie === true ? "true" : "false");
|
|
397
|
+
if (command.email) body.set("provided_details[email]", safeEmail(command.email));
|
|
398
|
+
if (this.runtime.configuration.returnUrl) body.set("return_url", this.runtime.configuration.returnUrl);
|
|
399
|
+
const session = await this.call("/identity/verification_sessions", {
|
|
400
|
+
method: "POST",
|
|
401
|
+
operation: "create",
|
|
402
|
+
idempotencyScope: command.attemptId,
|
|
403
|
+
body,
|
|
404
|
+
idempotencyKey: command.idempotencyKey
|
|
405
|
+
});
|
|
406
|
+
return this.toAttemptResult(command.attemptId, session);
|
|
407
|
+
}
|
|
408
|
+
async resumeAttempt(command) {
|
|
409
|
+
const session = await this.getSession(command.providerResourceId, "resume");
|
|
410
|
+
return this.launchFor(command.attemptId, session);
|
|
411
|
+
}
|
|
412
|
+
async retrieveAttempt(command) {
|
|
413
|
+
return this.normalizeSnapshot(await this.getSession(command.providerResourceId, "retrieve"));
|
|
414
|
+
}
|
|
415
|
+
async retryAttempt(command) {
|
|
416
|
+
this.assertCommand(command);
|
|
417
|
+
if (command.previousProviderResourceId) {
|
|
418
|
+
const previous = await this.getSession(command.previousProviderResourceId, "retry");
|
|
419
|
+
if (previous.status === "requires_input") return this.toAttemptResult(command.attemptId, previous);
|
|
420
|
+
if (previous.redaction?.status === "redacted" || previous.status === "canceled") {
|
|
421
|
+
const idempotencyKey2 = this.runtime.idempotency.keyFor("retry", command.attemptId, command.idempotencyKey);
|
|
422
|
+
return this.createAttempt({ ...command, idempotencyKey: idempotencyKey2 });
|
|
423
|
+
}
|
|
424
|
+
if (previous.status === "processing") {
|
|
425
|
+
throw new ProviderError("RETRYABLE_PROVIDER_FAILURE", "Stripe Identity is still processing this attempt.", {
|
|
426
|
+
retryable: true,
|
|
427
|
+
retryAfterSeconds: 15,
|
|
428
|
+
safeCode: "stripe_identity_processing"
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
if (previous.status === "verified") {
|
|
432
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "A verified Stripe Identity attempt cannot be retried.", {
|
|
433
|
+
safeCode: "stripe_identity_already_verified"
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
throw new ProviderError("UNKNOWN_PROVIDER_STATE", "Stripe Identity retry state is not recognized.", {
|
|
437
|
+
safeCode: "stripe_identity_retry_state_unknown"
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const idempotencyKey = this.runtime.idempotency.keyFor("retry", command.attemptId, command.idempotencyKey);
|
|
441
|
+
return this.createAttempt({ ...command, idempotencyKey });
|
|
442
|
+
}
|
|
443
|
+
async cancelAttempt(command) {
|
|
444
|
+
const current = await this.getSession(command.providerResourceId, "cancel");
|
|
445
|
+
if (current.status === "canceled") return { accepted: true, providerStatus: current.status, canonicalStatus: "canceled" };
|
|
446
|
+
if (current.status === "processing") {
|
|
447
|
+
throw new ProviderError("RETRYABLE_PROVIDER_FAILURE", "Stripe Identity is already processing this attempt.", {
|
|
448
|
+
retryable: true,
|
|
449
|
+
retryAfterSeconds: 15,
|
|
450
|
+
safeCode: "stripe_identity_processing"
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
if (current.status !== "requires_input") {
|
|
454
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "Stripe Identity cannot cancel this attempt state.", {
|
|
455
|
+
safeCode: current.status === "verified" ? "stripe_identity_already_verified" : "stripe_identity_cancel_state_invalid"
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
const session = await this.call(`/identity/verification_sessions/${encodeSessionId(command.providerResourceId)}/cancel`, {
|
|
459
|
+
method: "POST",
|
|
460
|
+
operation: "cancel",
|
|
461
|
+
idempotencyScope: command.attemptId,
|
|
462
|
+
body: new URLSearchParams()
|
|
463
|
+
});
|
|
464
|
+
return {
|
|
465
|
+
accepted: session.status === "canceled",
|
|
466
|
+
providerStatus: session.status,
|
|
467
|
+
canonicalStatus: session.status === "canceled" ? "canceled" : void 0
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async redactSubject(command) {
|
|
471
|
+
if (!command.providerResourceId) throw new ProviderRequiredInformationError();
|
|
472
|
+
const current = await this.getSession(command.providerResourceId, "redact");
|
|
473
|
+
if (current.redaction?.status === "redacted") {
|
|
474
|
+
return { completed: true, retryable: false, disposition: "redacted" };
|
|
475
|
+
}
|
|
476
|
+
if (current.redaction?.status === "processing" || current.status === "processing") {
|
|
477
|
+
return { completed: false, retryable: true, disposition: "processing" };
|
|
478
|
+
}
|
|
479
|
+
if (current.status === "canceled") {
|
|
480
|
+
return { completed: true, retryable: false, disposition: "not_applicable" };
|
|
481
|
+
}
|
|
482
|
+
if (current.status !== "requires_input" && current.status !== "verified") {
|
|
483
|
+
return { completed: false, retryable: false, disposition: "failed" };
|
|
484
|
+
}
|
|
485
|
+
const session = await this.call(`/identity/verification_sessions/${encodeSessionId(command.providerResourceId)}/redact`, {
|
|
486
|
+
method: "POST",
|
|
487
|
+
operation: "redact",
|
|
488
|
+
idempotencyScope: command.requestReference,
|
|
489
|
+
body: new URLSearchParams(),
|
|
490
|
+
idempotencyKey: command.requestReference
|
|
491
|
+
});
|
|
492
|
+
const status = session.redaction?.status ?? "";
|
|
493
|
+
if (status === "redacted") return { completed: true, retryable: false, disposition: "redacted" };
|
|
494
|
+
if (status === "processing") return { completed: false, retryable: true, disposition: "processing" };
|
|
495
|
+
return { completed: false, retryable: false, disposition: "failed" };
|
|
496
|
+
}
|
|
497
|
+
async verifyWebhook(request) {
|
|
498
|
+
return verifyStripeIdentityWebhook(request, {
|
|
499
|
+
secrets: webhookSecretsFromConfig(this.runtime.configuration),
|
|
500
|
+
toleranceSeconds: this.runtime.configuration.webhookToleranceSeconds ?? this.manifest.webhook.toleranceSeconds ?? 300,
|
|
501
|
+
now: this.runtime.now(),
|
|
502
|
+
crypto: this.runtime.crypto,
|
|
503
|
+
environment: this.environment
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async normalizeWebhook(input) {
|
|
507
|
+
const event = parseStripeEvent(input.opaquePayload);
|
|
508
|
+
if (event.id !== input.providerEventKey) {
|
|
509
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "Stripe Identity webhook event identity is inconsistent.", {
|
|
510
|
+
safeCode: "stripe_webhook_event_identity_mismatch"
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const session = sessionFromEvent(event);
|
|
514
|
+
this.assertLivemode(event.livemode);
|
|
515
|
+
this.assertLivemode(session.livemode);
|
|
516
|
+
if (event.account && event.account !== this.runtime.configuration.accountId) {
|
|
517
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "Stripe Identity webhook account is inconsistent.", {
|
|
518
|
+
safeCode: "stripe_account_mismatch"
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (event.livemode !== session.livemode) {
|
|
522
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "Stripe Identity webhook mode is inconsistent.", {
|
|
523
|
+
safeCode: "stripe_environment_mismatch"
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const eventAllowlisted = eventIsAllowlisted(event.type);
|
|
527
|
+
const normalized = normalizeAllowlistedEvent(event);
|
|
528
|
+
return {
|
|
529
|
+
providerEventKey: event.id,
|
|
530
|
+
providerResourceId: session.id,
|
|
531
|
+
eventType: `verification.provider_event.${normalized.canonicalStatus}`,
|
|
532
|
+
providerEventType: event.type,
|
|
533
|
+
canonicalStatus: normalized.canonicalStatus,
|
|
534
|
+
occurredAt: new Date(event.created * 1e3).toISOString(),
|
|
535
|
+
normalizedReasonCodes: normalized.reasonCodes,
|
|
536
|
+
safeMetadata: {
|
|
537
|
+
livemode: event.livemode,
|
|
538
|
+
adapter_version: this.manifest.adapterVersion,
|
|
539
|
+
normalization_version: "stripe-identity-v1",
|
|
540
|
+
event_allowlisted: eventAllowlisted,
|
|
541
|
+
reconcile_required: !eventAllowlisted || normalized.reasonCodes.includes("stripe_unknown_status")
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
assertCommand(command) {
|
|
546
|
+
if (command.packageCode !== "human_idv") {
|
|
547
|
+
throw new ProviderError("UNSUPPORTED_CAPABILITY", "Stripe Identity does not support this verification package.", {
|
|
548
|
+
safeCode: "unsupported_package"
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (!isOpaqueSubjectReference(command.subjectReference)) {
|
|
552
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "The subject reference is not an opaque identifier.", {
|
|
553
|
+
safeCode: "subject_reference_invalid"
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (metadataContainsForbiddenIdentifier(command.metadata)) {
|
|
557
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "Attempt metadata contains a forbidden identifier.", {
|
|
558
|
+
safeCode: "forbidden_identifier"
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
assertLivemode(livemode) {
|
|
563
|
+
if (livemode !== (this.environment === "production")) {
|
|
564
|
+
throw new ProviderError("TERMINAL_INPUT_FAILURE", "Stripe Identity mode does not match the pinned environment.", {
|
|
565
|
+
safeCode: "stripe_environment_mismatch"
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async getSession(providerResourceId, operation) {
|
|
570
|
+
return this.call(`/identity/verification_sessions/${encodeSessionId(providerResourceId)}`, {
|
|
571
|
+
method: "GET",
|
|
572
|
+
operation
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
toAttemptResult(attemptId, session) {
|
|
576
|
+
const normalized = normalizeStripeIdentityStatus(
|
|
577
|
+
session.status,
|
|
578
|
+
session.last_error?.code ?? null,
|
|
579
|
+
void 0,
|
|
580
|
+
session.redaction?.status ?? null
|
|
581
|
+
);
|
|
582
|
+
return {
|
|
583
|
+
attemptId,
|
|
584
|
+
providerResourceId: session.id,
|
|
585
|
+
providerStatus: session.status,
|
|
586
|
+
canonicalStatus: normalized.canonicalStatus,
|
|
587
|
+
launch: this.launchFor(attemptId, session, normalized.canonicalStatus)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
launchFor(attemptId, session, canonicalStatus) {
|
|
591
|
+
const status = canonicalStatus ?? normalizeStripeIdentityStatus(
|
|
592
|
+
session.status,
|
|
593
|
+
session.last_error?.code ?? null,
|
|
594
|
+
void 0,
|
|
595
|
+
session.redaction?.status ?? null
|
|
596
|
+
).canonicalStatus;
|
|
597
|
+
if (session.status !== "requires_input") {
|
|
598
|
+
return {
|
|
599
|
+
attemptId,
|
|
600
|
+
canonicalStatus: status,
|
|
601
|
+
presentation: "none",
|
|
602
|
+
launcherKey: "hosted",
|
|
603
|
+
providerDisclosure: STRIPE_IDENTITY_DISCLOSURE,
|
|
604
|
+
continuationReference: session.id
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const secret = safeClientSecret(session.client_secret);
|
|
608
|
+
const hostedUrl = safeStripeHostedUrl(session.url, STRIPE_IDENTITY_HOSTED_HOST);
|
|
609
|
+
const transientSecretExpiresAt = secret ? isoExpiry(this.runtime.now(), 86400) : void 0;
|
|
610
|
+
const hostedFallbackExpiresAt = hostedUrl ? isoExpiry(this.runtime.now(), 172800) : void 0;
|
|
611
|
+
if (!secret && !hostedUrl) {
|
|
612
|
+
throw new ProviderUnavailableError("Stripe Identity did not return resumable launch material.", {
|
|
613
|
+
safeCode: "stripe_identity_launch_material_missing"
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
const presentation = secret ? "embedded" : "hosted";
|
|
617
|
+
return {
|
|
618
|
+
attemptId,
|
|
619
|
+
canonicalStatus: status,
|
|
620
|
+
presentation,
|
|
621
|
+
launcherKey: secret ? "stripe_identity" : "hosted",
|
|
622
|
+
providerDisclosure: STRIPE_IDENTITY_DISCLOSURE,
|
|
623
|
+
transientSecret: secret,
|
|
624
|
+
transientSecretExpiresAt,
|
|
625
|
+
hostedUrl,
|
|
626
|
+
hostedFallbackExpiresAt,
|
|
627
|
+
continuationReference: session.id
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
normalizeSnapshot(session) {
|
|
631
|
+
const normalized = normalizeStripeIdentityStatus(
|
|
632
|
+
session.status,
|
|
633
|
+
session.last_error?.code ?? null,
|
|
634
|
+
void 0,
|
|
635
|
+
session.redaction?.status ?? null
|
|
636
|
+
);
|
|
637
|
+
return {
|
|
638
|
+
providerResourceId: session.id,
|
|
639
|
+
providerStatus: session.status,
|
|
640
|
+
canonicalStatus: normalized.canonicalStatus,
|
|
641
|
+
occurredAt: this.runtime.now().toISOString(),
|
|
642
|
+
providerCreatedAt: new Date(session.created * 1e3).toISOString(),
|
|
643
|
+
normalizedReasonCodes: normalized.reasonCodes,
|
|
644
|
+
safeMetadata: {
|
|
645
|
+
source: "retrieve",
|
|
646
|
+
livemode: session.livemode,
|
|
647
|
+
adapter_version: this.manifest.adapterVersion,
|
|
648
|
+
normalization_version: "stripe-identity-v1"
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
async call(path, options) {
|
|
653
|
+
if (this.runtime.rateBudget) {
|
|
654
|
+
const budget = await this.runtime.rateBudget.acquire(options.operation);
|
|
655
|
+
if (!budget.allowed) {
|
|
656
|
+
throw new ProviderError("RATE_LIMITED", "Stripe Identity rate budget is exhausted.", {
|
|
657
|
+
retryable: true,
|
|
658
|
+
retryAfterSeconds: budget.retryAfterSeconds,
|
|
659
|
+
safeCode: "stripe_identity_rate_limited"
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const startedAt = this.runtime.now().getTime();
|
|
664
|
+
const idempotencyKey = options.method === "GET" ? void 0 : this.runtime.idempotency.keyFor(options.operation, options.idempotencyScope ?? path, options.idempotencyKey);
|
|
665
|
+
const controller = new AbortController();
|
|
666
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
667
|
+
const url = `https://${STRIPE_IDENTITY_API_HOST}/v1${path}`;
|
|
668
|
+
try {
|
|
669
|
+
const response = await this.runtime.http.fetch(url, {
|
|
670
|
+
method: options.method,
|
|
671
|
+
headers: {
|
|
672
|
+
Authorization: `Bearer ${this.runtime.configuration.restrictedKey}`,
|
|
673
|
+
"Stripe-Version": this.runtime.configuration.apiVersion || STRIPE_IDENTITY_API_VERSION,
|
|
674
|
+
...options.body ? { "Content-Type": "application/x-www-form-urlencoded" } : {},
|
|
675
|
+
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
|
|
676
|
+
},
|
|
677
|
+
body: options.body,
|
|
678
|
+
signal: controller.signal
|
|
679
|
+
});
|
|
680
|
+
const payload = await response.json().catch(() => null);
|
|
681
|
+
if (!response.ok) throw mapProviderHttpError(response.status, response.headers.get("retry-after"), "stripe_identity");
|
|
682
|
+
const result = requireSession(payload);
|
|
683
|
+
this.assertLivemode(result.livemode);
|
|
684
|
+
const normalized = normalizeStripeIdentityStatus(
|
|
685
|
+
result.status,
|
|
686
|
+
result.last_error?.code ?? null,
|
|
687
|
+
void 0,
|
|
688
|
+
result.redaction?.status ?? null
|
|
689
|
+
);
|
|
690
|
+
const unknown = normalized.reasonCodes.includes("stripe_unknown_status");
|
|
691
|
+
await this.recordObservation(
|
|
692
|
+
options.operation,
|
|
693
|
+
unknown ? "unknown_status" : "success",
|
|
694
|
+
unknown ? "stripe_unknown_status" : `stripe_identity_${options.operation}_ok`,
|
|
695
|
+
startedAt
|
|
696
|
+
);
|
|
697
|
+
return result;
|
|
698
|
+
} catch (error) {
|
|
699
|
+
const failure = error instanceof ProviderError ? error : error instanceof DOMException && error.name === "AbortError" ? new ProviderError("TIMEOUT", "Stripe Identity request timed out.", {
|
|
700
|
+
retryable: true,
|
|
701
|
+
safeCode: "stripe_timeout"
|
|
702
|
+
}) : new ProviderError("RETRYABLE_PROVIDER_FAILURE", "Stripe Identity request failed.", {
|
|
703
|
+
retryable: true,
|
|
704
|
+
safeCode: "stripe_provider_failure",
|
|
705
|
+
cause: error
|
|
706
|
+
});
|
|
707
|
+
await this.recordObservation(
|
|
708
|
+
options.operation,
|
|
709
|
+
failure.retryable ? "retryable_failure" : "terminal_failure",
|
|
710
|
+
failure.safeCode,
|
|
711
|
+
startedAt
|
|
712
|
+
);
|
|
713
|
+
throw failure;
|
|
714
|
+
} finally {
|
|
715
|
+
clearTimeout(timeoutId);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async recordObservation(operation, outcome, safeCode, startedAt) {
|
|
719
|
+
const observedAt = this.runtime.now();
|
|
720
|
+
const latencyMs = Math.max(0, observedAt.getTime() - startedAt);
|
|
721
|
+
const metadata = {
|
|
722
|
+
provider: this.provider,
|
|
723
|
+
environment: this.environment,
|
|
724
|
+
operation,
|
|
725
|
+
outcome,
|
|
726
|
+
safe_code: safeCode,
|
|
727
|
+
latency_ms: latencyMs
|
|
728
|
+
};
|
|
729
|
+
this.runtime.telemetry?.histogram?.("verification.provider.latency_ms", latencyMs, {
|
|
730
|
+
provider: this.provider,
|
|
731
|
+
operation
|
|
732
|
+
});
|
|
733
|
+
this.runtime.telemetry?.counter?.("verification.provider.calls", 1, {
|
|
734
|
+
provider: this.provider,
|
|
735
|
+
operation,
|
|
736
|
+
outcome
|
|
737
|
+
});
|
|
738
|
+
try {
|
|
739
|
+
await this.runtime.recordHealth({
|
|
740
|
+
operation,
|
|
741
|
+
outcome,
|
|
742
|
+
safeCode,
|
|
743
|
+
observedAt: observedAt.toISOString(),
|
|
744
|
+
latencyMs
|
|
745
|
+
});
|
|
746
|
+
if (outcome === "success") this.runtime.logger.info("verification_provider_operation", metadata);
|
|
747
|
+
else this.runtime.logger.warn("verification_provider_operation", metadata);
|
|
748
|
+
} catch {
|
|
749
|
+
this.runtime.logger.warn("verification_provider_health_record_failed", {
|
|
750
|
+
provider: this.provider,
|
|
751
|
+
environment: this.environment,
|
|
752
|
+
operation
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
export { STRIPE_IDENTITY_API_VERSION, StripeIdentityVerificationAdapter, createStripeIdentityConfiguration, normalizeStripeIdentityStatus, stripeIdentityProviderManifest };
|
|
759
|
+
//# sourceMappingURL=index.js.map
|
|
760
|
+
//# sourceMappingURL=index.js.map
|