@pryzr/verify 0.3.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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # Pryzr Verify TypeScript client
2
+
3
+ This server-side client handles authentication, safe retries, timeouts, and Pryzr’s standard error responses for you. Keep your Pryzr API key on the server—never include it in browser or mobile-app code.
4
+
5
+ ## Start a verification
6
+
7
+ ```bash
8
+ npm install @pryzr/verify
9
+ ```
10
+
11
+ ```ts
12
+ import { PryzrVerifyClient } from "@pryzr/verify";
13
+
14
+ const pryzr = new PryzrVerifyClient({
15
+ apiKey: process.env.PRYZR_API_KEY!,
16
+ });
17
+
18
+ const { playerUrl, verification } = await pryzr.startVerification({
19
+ playerReference: "player-1042",
20
+ caseReference: "signup-1042",
21
+ jurisdiction: "US-NJ",
22
+ minimumAge: 21,
23
+ });
24
+
25
+ // Redirect the intended player to playerUrl from your authenticated flow.
26
+ // Do not log this short-lived URL or send it to analytics.
27
+ ```
28
+
29
+ That single method creates the Pryzr player record and verification session. Store `verification.id` in your system so you can match later status updates to the correct player.
30
+
31
+ ## Advanced control
32
+
33
+ Use `createApplicant` and `createVerificationCase` separately if your integration needs to control or retry each step itself. Mutation methods accept an idempotency key; reuse the same key and the same request after a timeout with an uncertain outcome.
34
+
35
+ The client also supports:
36
+
37
+ - verification status, history, resubmission, and cancellation
38
+ - signed webhook setup, secret rotation, testing, and redelivery
39
+ - transaction events
40
+ - consented PlayPrint summaries
41
+
42
+ The API key selects the correct workspace and environment automatically. You do not need to send workspace, environment, verification-provider, or workflow IDs in normal API requests.
43
+
44
+ Webhook secrets are shown once. Save them in a server-side secret manager before continuing. Never log API keys, webhook secrets, player verification URLs, or identity data.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Server-side client for the Pryzr Verify API. API credentials must never be
3
+ * shipped to browser code.
4
+ */
5
+ export { PRYZR_WEBHOOK_TOLERANCE_SECONDS, verifyPryzrWebhookSignature, verifyPryzrWebhookWithSecrets, } from "./webhooks";
6
+ export type PryzrEnvironment = "sandbox" | "production";
7
+ export declare const operatorApiOperationAvailability: {
8
+ readonly createApplicant: "implemented";
9
+ readonly getApplicant: "implemented";
10
+ readonly createVerificationCase: "implemented";
11
+ readonly listVerificationCases: "implemented";
12
+ readonly getVerificationCase: "implemented";
13
+ readonly resubmitVerificationCase: "implemented";
14
+ readonly cancelVerificationCase: "implemented";
15
+ readonly listVerificationCaseEvents: "implemented";
16
+ readonly createWebhookEndpoint: "implemented";
17
+ readonly updateWebhookEndpoint: "implemented";
18
+ readonly rotateWebhookSigningSecret: "implemented";
19
+ readonly sendTestWebhook: "implemented";
20
+ readonly listWebhookDeliveries: "implemented";
21
+ readonly redeliverWebhook: "implemented";
22
+ readonly submitTransactionEvent: "implemented";
23
+ readonly submitTelemetry: "implemented";
24
+ };
25
+ export type VerificationCaseStatus = "created" | "ready" | "in_progress" | "processing" | "manual_review" | "resubmission_required" | "approved" | "declined" | "expired" | "abandoned" | "verification_expired";
26
+ export interface Applicant {
27
+ id: string;
28
+ operator_applicant_ref: string;
29
+ created_at: string;
30
+ }
31
+ export interface VerificationCase {
32
+ id: string;
33
+ applicant_id: string;
34
+ operator_case_ref: string;
35
+ environment: PryzrEnvironment;
36
+ jurisdiction: string;
37
+ status: VerificationCaseStatus;
38
+ result: {
39
+ outcome: "approve" | "decline" | "review" | null;
40
+ };
41
+ created_at: string;
42
+ updated_at: string;
43
+ }
44
+ export interface PlayerLaunch {
45
+ url: string;
46
+ expires_at: string;
47
+ }
48
+ export interface VerificationCaseWithPlayer extends VerificationCase {
49
+ player: PlayerLaunch;
50
+ }
51
+ export interface VerificationCaseEvent {
52
+ id: string;
53
+ type: string;
54
+ occurred_at: string;
55
+ }
56
+ export interface CursorPage<T> {
57
+ data: T[];
58
+ next_cursor: string | null;
59
+ }
60
+ export declare const operatorWebhookEventTypes: readonly ["verification_case.created", "verification_case.processing", "verification_case.review_required", "verification_case.resubmission_required", "verification_case.approved", "verification_case.declined", "verification_case.expired", "verification_case.abandoned", "verification_case.verification_expired", "verification_case.reopened_for_review", "verification_case.deleted"];
61
+ export type OperatorWebhookEventType = (typeof operatorWebhookEventTypes)[number];
62
+ export type WebhookEndpointStatus = "active" | "paused" | "disabled";
63
+ export type WebhookDeliveryStatus = "pending" | "retry_wait" | "succeeded" | "failed" | "dead_letter" | "cancelled";
64
+ export interface PryzrVerifyClientOptions {
65
+ apiKey: string;
66
+ baseUrl?: string;
67
+ fetch?: typeof globalThis.fetch;
68
+ requestTimeoutMs?: number;
69
+ maxRetries?: number;
70
+ retryDelayMs?: number;
71
+ sleep?: (milliseconds: number) => Promise<void>;
72
+ }
73
+ export interface StartVerificationInput {
74
+ playerReference: string;
75
+ caseReference: string;
76
+ jurisdiction: string;
77
+ minimumAge: number;
78
+ applicantIdempotencyKey?: string;
79
+ caseIdempotencyKey?: string;
80
+ }
81
+ export interface StartedVerification {
82
+ applicant: Applicant;
83
+ verification: VerificationCaseWithPlayer;
84
+ playerUrl: string;
85
+ }
86
+ export interface TransactionEventInput {
87
+ operatorEventRef: string;
88
+ eventType: "DEPOSIT" | "PURCHASE" | "REDEMPTION" | "REFUND" | "CHARGEBACK" | "BONUS" | "PAYMENT_FAILURE" | "REVERSAL";
89
+ status: "PENDING" | "SUCCEEDED" | "FAILED" | "REVERSED";
90
+ amountMinor?: number;
91
+ currency?: string;
92
+ operatorAccountRef: string;
93
+ paymentInstrumentRef?: string;
94
+ deviceRef?: string;
95
+ sessionRef?: string;
96
+ networkRef?: string;
97
+ countryCode?: string;
98
+ occurredAt: string;
99
+ }
100
+ export interface PlayPrintTelemetryEvent {
101
+ eventId: string;
102
+ sequence: number;
103
+ occurredAt: string;
104
+ subjectRef: string;
105
+ sessionRef: string;
106
+ deviceRef: string;
107
+ consentReceiptRef?: string;
108
+ signals: Readonly<Record<string, unknown>>;
109
+ }
110
+ export type SigningSecret = {
111
+ id: string;
112
+ key_label: string;
113
+ secret: string;
114
+ secret_omitted?: never;
115
+ } | {
116
+ id: string;
117
+ key_label: string;
118
+ secret?: never;
119
+ secret_omitted: true;
120
+ };
121
+ export interface WebhookEndpoint {
122
+ id: string;
123
+ url: string;
124
+ description: string | null;
125
+ environment: PryzrEnvironment;
126
+ status: WebhookEndpointStatus;
127
+ event_types: OperatorWebhookEventType[];
128
+ disabled_reason_code?: string | null;
129
+ created_at: string;
130
+ updated_at?: string;
131
+ }
132
+ export interface CreatedWebhookEndpoint extends WebhookEndpoint {
133
+ signing_secret: SigningSecret;
134
+ }
135
+ export interface WebhookDelivery {
136
+ id: string;
137
+ endpoint_id: string;
138
+ event_id: string;
139
+ event_type: OperatorWebhookEventType | "webhook_endpoint.test";
140
+ environment: PryzrEnvironment;
141
+ status: WebhookDeliveryStatus;
142
+ test: boolean;
143
+ attempt_count: number;
144
+ max_attempts: number;
145
+ last_response_status: number | null;
146
+ last_error_code: string | null;
147
+ next_attempt_at: string;
148
+ completed_at: string | null;
149
+ created_at: string;
150
+ redelivery_of_id: string | null;
151
+ }
152
+ export interface WebhookDeliveryPage {
153
+ data: WebhookDelivery[];
154
+ next_cursor: string | null;
155
+ }
156
+ export interface ApiErrorBody {
157
+ error: {
158
+ code: string;
159
+ message: string;
160
+ request_id: string;
161
+ details?: Array<{
162
+ field: string;
163
+ reason: string;
164
+ }>;
165
+ };
166
+ }
167
+ export declare class PryzrVerifyApiError extends Error {
168
+ readonly status: number;
169
+ readonly code: string;
170
+ readonly requestId: string | null;
171
+ readonly details: ApiErrorBody["error"]["details"];
172
+ constructor(status: number, code: string, requestId: string | null, details: ApiErrorBody["error"]["details"], message: string);
173
+ }
174
+ export interface ListWebhookDeliveriesQuery {
175
+ limit?: number;
176
+ cursor?: string;
177
+ endpoint_id?: string;
178
+ status?: WebhookDeliveryStatus;
179
+ event_type?: OperatorWebhookEventType | "webhook_endpoint.test";
180
+ created_after?: string;
181
+ created_before?: string;
182
+ include_test?: boolean;
183
+ }
184
+ export interface ListVerificationCasesQuery {
185
+ limit?: number;
186
+ cursor?: string;
187
+ status?: VerificationCaseStatus;
188
+ }
189
+ export interface ListVerificationCaseEventsQuery {
190
+ limit?: number;
191
+ cursor?: string;
192
+ }
193
+ export declare class PryzrVerifyClient {
194
+ private readonly apiKey;
195
+ private readonly baseUrl;
196
+ private readonly fetchImplementation;
197
+ private readonly requestTimeoutMs;
198
+ private readonly maxRetries;
199
+ private readonly retryDelayMs;
200
+ private readonly sleep;
201
+ constructor(options: PryzrVerifyClientOptions);
202
+ createApplicant(input: {
203
+ operator_applicant_ref: string;
204
+ }, idempotencyKey: string): Promise<Applicant>;
205
+ /** Create the player record and verification session in one call. */
206
+ startVerification(input: StartVerificationInput): Promise<StartedVerification>;
207
+ getApplicant(applicantId: string): Promise<Applicant>;
208
+ createVerificationCase(input: {
209
+ applicant_id: string;
210
+ operator_case_ref: string;
211
+ jurisdiction: string;
212
+ required_minimum_age: number;
213
+ }, idempotencyKey: string): Promise<VerificationCaseWithPlayer>;
214
+ listVerificationCases(query?: ListVerificationCasesQuery): Promise<CursorPage<VerificationCase>>;
215
+ getVerificationCase(caseId: string): Promise<VerificationCase>;
216
+ resubmitVerificationCase(caseId: string, idempotencyKey: string): Promise<VerificationCaseWithPlayer>;
217
+ cancelVerificationCase(caseId: string, idempotencyKey: string): Promise<VerificationCase>;
218
+ listVerificationCaseEvents(caseId: string, query?: ListVerificationCaseEventsQuery): Promise<CursorPage<VerificationCaseEvent>>;
219
+ createWebhookEndpoint(input: {
220
+ url: string;
221
+ description?: string | null;
222
+ event_types: OperatorWebhookEventType[];
223
+ }, idempotencyKey: string): Promise<CreatedWebhookEndpoint>;
224
+ updateWebhookEndpoint(endpointId: string, input: {
225
+ status?: "active" | "paused";
226
+ event_types?: OperatorWebhookEventType[];
227
+ }, idempotencyKey: string): Promise<WebhookEndpoint>;
228
+ rotateWebhookSigningSecret(endpointId: string, input: {
229
+ overlap_seconds?: number;
230
+ }, idempotencyKey: string): Promise<{
231
+ endpoint_id: string;
232
+ overlap_seconds: number;
233
+ signing_secret: SigningSecret;
234
+ }>;
235
+ sendTestWebhook(endpointId: string, idempotencyKey: string): Promise<{
236
+ delivery_id: string;
237
+ endpoint_id: string;
238
+ event_id: string;
239
+ event_type: "webhook_endpoint.test";
240
+ test: true;
241
+ status: "queued";
242
+ }>;
243
+ listWebhookDeliveries(query?: ListWebhookDeliveriesQuery): Promise<WebhookDeliveryPage>;
244
+ redeliverWebhook(deliveryId: string, input: {
245
+ reason_code?: string;
246
+ }, idempotencyKey: string): Promise<{
247
+ delivery_id: string;
248
+ source_delivery_id: string;
249
+ created: boolean;
250
+ status: "queued";
251
+ }>;
252
+ submitTransactionEvent(input: TransactionEventInput, idempotencyKey: string): Promise<{
253
+ event_id: string;
254
+ replayed: boolean;
255
+ findings: Array<{
256
+ reason_code: string;
257
+ severity: string;
258
+ explanation: string;
259
+ }>;
260
+ }>;
261
+ submitTelemetry(events: PlayPrintTelemetryEvent[], idempotencyKey: string): Promise<{
262
+ events: unknown[];
263
+ }>;
264
+ private request;
265
+ }
package/dist/index.js ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Server-side client for the Pryzr Verify API. API credentials must never be
3
+ * shipped to browser code.
4
+ */
5
+ export { PRYZR_WEBHOOK_TOLERANCE_SECONDS, verifyPryzrWebhookSignature, verifyPryzrWebhookWithSecrets, } from "./webhooks";
6
+ export const operatorApiOperationAvailability = {
7
+ createApplicant: "implemented",
8
+ getApplicant: "implemented",
9
+ createVerificationCase: "implemented",
10
+ listVerificationCases: "implemented",
11
+ getVerificationCase: "implemented",
12
+ resubmitVerificationCase: "implemented",
13
+ cancelVerificationCase: "implemented",
14
+ listVerificationCaseEvents: "implemented",
15
+ createWebhookEndpoint: "implemented",
16
+ updateWebhookEndpoint: "implemented",
17
+ rotateWebhookSigningSecret: "implemented",
18
+ sendTestWebhook: "implemented",
19
+ listWebhookDeliveries: "implemented",
20
+ redeliverWebhook: "implemented",
21
+ submitTransactionEvent: "implemented",
22
+ submitTelemetry: "implemented",
23
+ };
24
+ export const operatorWebhookEventTypes = [
25
+ "verification_case.created",
26
+ "verification_case.processing",
27
+ "verification_case.review_required",
28
+ "verification_case.resubmission_required",
29
+ "verification_case.approved",
30
+ "verification_case.declined",
31
+ "verification_case.expired",
32
+ "verification_case.abandoned",
33
+ "verification_case.verification_expired",
34
+ "verification_case.reopened_for_review",
35
+ "verification_case.deleted",
36
+ ];
37
+ export class PryzrVerifyApiError extends Error {
38
+ status;
39
+ code;
40
+ requestId;
41
+ details;
42
+ constructor(status, code, requestId, details, message) {
43
+ super(message);
44
+ this.status = status;
45
+ this.code = code;
46
+ this.requestId = requestId;
47
+ this.details = details;
48
+ this.name = "PryzrVerifyApiError";
49
+ }
50
+ }
51
+ export class PryzrVerifyClient {
52
+ apiKey;
53
+ baseUrl;
54
+ fetchImplementation;
55
+ requestTimeoutMs;
56
+ maxRetries;
57
+ retryDelayMs;
58
+ sleep;
59
+ constructor(options) {
60
+ if (!/^pzk_(sandbox|production)_[0-9a-f]{16}_[A-Za-z0-9_-]{43}$/.test(options.apiKey)) {
61
+ throw new TypeError("A Pryzr Verify server-side API credential is required.");
62
+ }
63
+ this.apiKey = options.apiKey;
64
+ this.baseUrl = (options.baseUrl ?? "https://verify.pryzr.tech/api/v1").replace(/\/$/, "");
65
+ this.fetchImplementation = options.fetch ?? globalThis.fetch;
66
+ if (!this.fetchImplementation)
67
+ throw new TypeError("A Fetch API implementation is required.");
68
+ this.requestTimeoutMs = boundedInteger(options.requestTimeoutMs ?? 10_000, 1, 120_000);
69
+ this.maxRetries = boundedInteger(options.maxRetries ?? 2, 0, 5);
70
+ this.retryDelayMs = boundedInteger(options.retryDelayMs ?? 250, 0, 10_000);
71
+ this.sleep =
72
+ options.sleep ??
73
+ ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
74
+ }
75
+ createApplicant(input, idempotencyKey) {
76
+ return this.request("/applicants", {
77
+ method: "POST",
78
+ body: input,
79
+ idempotencyKey,
80
+ });
81
+ }
82
+ /** Create the player record and verification session in one call. */
83
+ async startVerification(input) {
84
+ const applicant = await this.createApplicant({ operator_applicant_ref: input.playerReference }, input.applicantIdempotencyKey ?? crypto.randomUUID());
85
+ const verification = await this.createVerificationCase({
86
+ applicant_id: applicant.id,
87
+ operator_case_ref: input.caseReference,
88
+ jurisdiction: input.jurisdiction,
89
+ required_minimum_age: input.minimumAge,
90
+ }, input.caseIdempotencyKey ?? crypto.randomUUID());
91
+ return { applicant, verification, playerUrl: verification.player.url };
92
+ }
93
+ getApplicant(applicantId) {
94
+ return this.request(`/applicants/${encodeURIComponent(applicantId)}`, { method: "GET" });
95
+ }
96
+ createVerificationCase(input, idempotencyKey) {
97
+ return this.request("/verification-cases", {
98
+ method: "POST",
99
+ body: input,
100
+ idempotencyKey,
101
+ });
102
+ }
103
+ listVerificationCases(query = {}) {
104
+ return this.request("/verification-cases", { method: "GET", query });
105
+ }
106
+ getVerificationCase(caseId) {
107
+ return this.request(`/verification-cases/${encodeURIComponent(caseId)}`, { method: "GET" });
108
+ }
109
+ resubmitVerificationCase(caseId, idempotencyKey) {
110
+ return this.request(`/verification-cases/${encodeURIComponent(caseId)}/resubmissions`, {
111
+ method: "POST",
112
+ body: {},
113
+ idempotencyKey,
114
+ });
115
+ }
116
+ cancelVerificationCase(caseId, idempotencyKey) {
117
+ return this.request(`/verification-cases/${encodeURIComponent(caseId)}/cancel`, {
118
+ method: "POST",
119
+ body: {},
120
+ idempotencyKey,
121
+ });
122
+ }
123
+ listVerificationCaseEvents(caseId, query = {}) {
124
+ return this.request(`/verification-cases/${encodeURIComponent(caseId)}/events`, {
125
+ method: "GET",
126
+ query,
127
+ });
128
+ }
129
+ createWebhookEndpoint(input, idempotencyKey) {
130
+ return this.request("/webhook-endpoints", {
131
+ method: "POST",
132
+ body: input,
133
+ idempotencyKey,
134
+ });
135
+ }
136
+ updateWebhookEndpoint(endpointId, input, idempotencyKey) {
137
+ return this.request(`/webhook-endpoints/${encodeURIComponent(endpointId)}`, {
138
+ method: "PATCH",
139
+ body: input,
140
+ idempotencyKey,
141
+ });
142
+ }
143
+ rotateWebhookSigningSecret(endpointId, input, idempotencyKey) {
144
+ return this.request(`/webhook-endpoints/${encodeURIComponent(endpointId)}/rotate-secret`, {
145
+ method: "POST",
146
+ body: input,
147
+ idempotencyKey,
148
+ });
149
+ }
150
+ sendTestWebhook(endpointId, idempotencyKey) {
151
+ return this.request(`/webhook-endpoints/${encodeURIComponent(endpointId)}/test`, {
152
+ method: "POST",
153
+ body: {},
154
+ idempotencyKey,
155
+ });
156
+ }
157
+ listWebhookDeliveries(query = {}) {
158
+ return this.request("/webhook-deliveries", { method: "GET", query });
159
+ }
160
+ redeliverWebhook(deliveryId, input, idempotencyKey) {
161
+ return this.request(`/webhook-deliveries/${encodeURIComponent(deliveryId)}/redeliver`, {
162
+ method: "POST",
163
+ body: input,
164
+ idempotencyKey,
165
+ });
166
+ }
167
+ submitTransactionEvent(input, idempotencyKey) {
168
+ return this.request("/transaction-events", {
169
+ method: "POST",
170
+ body: input,
171
+ idempotencyKey,
172
+ });
173
+ }
174
+ submitTelemetry(events, idempotencyKey) {
175
+ return this.request("/telemetry", {
176
+ method: "POST",
177
+ body: { events },
178
+ idempotencyKey,
179
+ });
180
+ }
181
+ async request(path, options) {
182
+ const url = new URL(`${this.baseUrl}${path}`);
183
+ for (const [key, value] of Object.entries(options.query ?? {})) {
184
+ if (value !== undefined)
185
+ url.searchParams.set(key, String(value));
186
+ }
187
+ const headers = new Headers({
188
+ accept: "application/json",
189
+ authorization: `Bearer ${this.apiKey}`,
190
+ });
191
+ if (options.body !== undefined)
192
+ headers.set("content-type", "application/json");
193
+ if (options.idempotencyKey)
194
+ headers.set("idempotency-key", options.idempotencyKey);
195
+ const body = options.body === undefined ? undefined : JSON.stringify(options.body);
196
+ let response;
197
+ for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
198
+ response = undefined;
199
+ const controller = new AbortController();
200
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
201
+ try {
202
+ response = await this.fetchImplementation(url, {
203
+ method: options.method,
204
+ headers,
205
+ signal: controller.signal,
206
+ ...(body !== undefined ? { body } : {}),
207
+ });
208
+ }
209
+ catch {
210
+ if (attempt === this.maxRetries) {
211
+ const timedOut = controller.signal.aborted;
212
+ throw new PryzrVerifyApiError(0, timedOut ? "REQUEST_TIMEOUT" : "NETWORK_ERROR", null, undefined, timedOut ? "Pryzr Verify request timed out." : "Pryzr Verify could not be reached.");
213
+ }
214
+ }
215
+ finally {
216
+ clearTimeout(timeout);
217
+ }
218
+ if (response && ![429, 503].includes(response.status))
219
+ break;
220
+ if (attempt < this.maxRetries)
221
+ await this.sleep(this.retryDelayMs * 2 ** attempt);
222
+ }
223
+ if (!response)
224
+ throw new PryzrVerifyApiError(0, "NETWORK_ERROR", null, undefined, "Pryzr Verify could not be reached.");
225
+ const payload = await readJson(response);
226
+ if (!response.ok) {
227
+ const error = isApiErrorBody(payload) ? payload.error : null;
228
+ throw new PryzrVerifyApiError(response.status, error?.code ?? "HTTP_ERROR", error?.request_id ?? response.headers.get("x-request-id"), error?.details, redactErrorMessage(error?.message ?? `Pryzr Verify returned HTTP ${response.status}.`));
229
+ }
230
+ return payload;
231
+ }
232
+ }
233
+ function boundedInteger(value, minimum, maximum) {
234
+ if (!Number.isInteger(value) || value < minimum || value > maximum)
235
+ throw new TypeError(`Expected an integer between ${minimum} and ${maximum}.`);
236
+ return value;
237
+ }
238
+ function redactErrorMessage(message) {
239
+ return message
240
+ .replace(/pzk_(sandbox|production)_[A-Za-z0-9_-]+/g, "[REDACTED_CREDENTIAL]")
241
+ .replace(/Bearer\s+[A-Za-z0-9._~-]+/gi, "Bearer [REDACTED]")
242
+ .replace(/[\r\n\t]+/g, " ")
243
+ .slice(0, 500);
244
+ }
245
+ async function readJson(response) {
246
+ const contentType = response.headers.get("content-type") ?? "";
247
+ if (!contentType.toLowerCase().includes("application/json")) {
248
+ throw new PryzrVerifyApiError(response.status, "INVALID_RESPONSE", response.headers.get("x-request-id"), undefined, "Pryzr Verify returned a non-JSON response.");
249
+ }
250
+ return response.json();
251
+ }
252
+ function isApiErrorBody(value) {
253
+ if (typeof value !== "object" || value === null || !("error" in value))
254
+ return false;
255
+ const error = value.error;
256
+ return (typeof error === "object" &&
257
+ error !== null &&
258
+ typeof error.code === "string" &&
259
+ typeof error.message === "string" &&
260
+ typeof error.request_id === "string");
261
+ }
@@ -0,0 +1,27 @@
1
+ export declare const PRYZR_WEBHOOK_TOLERANCE_SECONDS = 300;
2
+ /**
3
+ * Verify a Pryzr webhook against the exact raw request body. Call this before
4
+ * JSON parsing and before any side effect. Multiple space-separated v1 values
5
+ * are accepted during signing-secret rotation.
6
+ */
7
+ export declare function verifyPryzrWebhookSignature(input: {
8
+ secret: string;
9
+ rawBody: string;
10
+ signatureHeader: string;
11
+ timestampHeader: string;
12
+ now?: Date;
13
+ toleranceSeconds?: number;
14
+ }): boolean;
15
+ /**
16
+ * Verify against every currently accepted secret during a bounded rotation
17
+ * overlap. Callers should remove the retiring secret as soon as the overlap
18
+ * ends. Secret values are never returned or included in errors.
19
+ */
20
+ export declare function verifyPryzrWebhookWithSecrets(input: {
21
+ secrets: readonly string[];
22
+ rawBody: string;
23
+ signatureHeader: string;
24
+ timestampHeader: string;
25
+ now?: Date;
26
+ toleranceSeconds?: number;
27
+ }): boolean;
@@ -0,0 +1,61 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export const PRYZR_WEBHOOK_TOLERANCE_SECONDS = 300;
3
+ const signaturePattern = /^v1=[0-9a-f]{64}$/;
4
+ /**
5
+ * Verify a Pryzr webhook against the exact raw request body. Call this before
6
+ * JSON parsing and before any side effect. Multiple space-separated v1 values
7
+ * are accepted during signing-secret rotation.
8
+ */
9
+ export function verifyPryzrWebhookSignature(input) {
10
+ if (Buffer.byteLength(input.secret, "utf8") < 32) {
11
+ throw new TypeError("Pryzr webhook secrets contain at least 32 bytes.");
12
+ }
13
+ const tolerance = input.toleranceSeconds ?? PRYZR_WEBHOOK_TOLERANCE_SECONDS;
14
+ if (!Number.isSafeInteger(tolerance) || tolerance < 0 || tolerance > 3_600) {
15
+ throw new TypeError("Webhook replay tolerance must be between 0 and 3600 seconds.");
16
+ }
17
+ if (!/^\d{1,15}$/.test(input.timestampHeader))
18
+ return false;
19
+ const timestamp = Number(input.timestampHeader);
20
+ if (!Number.isSafeInteger(timestamp) || timestamp <= 0)
21
+ return false;
22
+ const now = input.now ?? new Date();
23
+ if (Number.isNaN(now.getTime()))
24
+ return false;
25
+ if (Math.abs(Math.floor(now.getTime() / 1_000) - timestamp) > tolerance)
26
+ return false;
27
+ const expected = `v1=${createHmac("sha256", input.secret)
28
+ .update(`${timestamp}.${input.rawBody}`, "utf8")
29
+ .digest("hex")}`;
30
+ const expectedBytes = Buffer.from(expected, "utf8");
31
+ return input.signatureHeader.split(" ").some((candidate) => {
32
+ if (!signaturePattern.test(candidate))
33
+ return false;
34
+ const candidateBytes = Buffer.from(candidate, "utf8");
35
+ return (candidateBytes.length === expectedBytes.length &&
36
+ timingSafeEqual(candidateBytes, expectedBytes));
37
+ });
38
+ }
39
+ /**
40
+ * Verify against every currently accepted secret during a bounded rotation
41
+ * overlap. Callers should remove the retiring secret as soon as the overlap
42
+ * ends. Secret values are never returned or included in errors.
43
+ */
44
+ export function verifyPryzrWebhookWithSecrets(input) {
45
+ if (input.secrets.length < 1 || input.secrets.length > 2) {
46
+ throw new TypeError("Provide one active secret or active and retiring secrets.");
47
+ }
48
+ let accepted = false;
49
+ for (const secret of input.secrets) {
50
+ const valid = verifyPryzrWebhookSignature({
51
+ secret,
52
+ rawBody: input.rawBody,
53
+ signatureHeader: input.signatureHeader,
54
+ timestampHeader: input.timestampHeader,
55
+ ...(input.now ? { now: input.now } : {}),
56
+ ...(input.toleranceSeconds === undefined ? {} : { toleranceSeconds: input.toleranceSeconds }),
57
+ });
58
+ accepted = valid || accepted;
59
+ }
60
+ return accepted;
61
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@pryzr/verify",
3
+ "version": "0.3.0",
4
+ "description": "Server-side TypeScript SDK for Pryzr Verify",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json"
21
+ },
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "license": "UNLICENSED",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/Pryzr/pryzr-verify.git",
29
+ "directory": "sdk/typescript"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "provenance": true
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.9.0"
37
+ }
38
+ }