@selfxyz/enterprise-sdk 0.1.1 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @selfxyz/enterprise-sdk
2
2
 
3
- Official Node/TypeScript SDK for the [Self.xyz](https://self.xyz) Enterprise API create verification sessions and verify webhooks with typed payloads.
3
+ Official Node/TypeScript SDK for the [Self.xyz](https://self.xyz) Enterprise API. Create verification sessions and verify webhooks with typed payloads.
4
4
 
5
5
  > **Pre-1.0 stability**: this SDK is under active development. Minor versions may include breaking changes until `1.0.0`. Pin to an exact version in production.
6
6
 
@@ -54,7 +54,7 @@ import { SelfWebhooks } from '@selfxyz/enterprise-sdk';
54
54
  app.post('/webhooks/self', (req, res) => {
55
55
  try {
56
56
  const event = SelfWebhooks.verify(
57
- req.body, // raw string or Buffer do NOT pre-parse
57
+ req.body, // raw string or Buffer (do NOT pre-parse)
58
58
  req.headers, // must include the signature headers as received
59
59
  process.env.SELF_WEBHOOK_SECRET!, // whsec_... from the Self dashboard
60
60
  );
@@ -67,29 +67,132 @@ app.post('/webhooks/self', (req, res) => {
67
67
  });
68
68
  ```
69
69
 
70
- The raw request body must be passed exactly as received middleware that JSON-parses the body before signature verification will cause it to fail.
70
+ The raw request body must be passed exactly as received. Middleware that JSON-parses the body before signature verification will cause it to fail.
71
+
72
+ ## Proof re-verification
73
+
74
+ `verifyProof` lets you independently re-verify a proof you received from Self.
75
+
76
+ Persist `event.proof`, `event.proof_attributes`, and `event.environment` from the `verification.completed` payload — those three fields are everything `verifyProof` needs to re-run offline whenever you want.
77
+
78
+ ```ts
79
+ import { verifyProof } from '@selfxyz/enterprise-sdk';
80
+
81
+ // `verificationData` is whatever you stored when the
82
+ // `verification.completed` webhook arrived — keep `event.proof`,
83
+ // `event.proof_attributes`, and `event.environment` so this call
84
+ // can run offline at any time.
85
+ const { isValid } = await verifyProof({
86
+ orgId: process.env.SELF_ORG_ID!,
87
+ proof: verificationData.proof,
88
+ proofAttributes: verificationData.proof_attributes,
89
+ environment: verificationData.environment,
90
+ });
91
+ ```
92
+
93
+ `isValid` is true only when the Groth16 proof verifies, the minimum-age predicate is satisfied (if configured), and the user is not on an OFAC list (if OFAC checking is configured).
94
+
95
+ ### `orgId`
96
+
97
+ `verifyProof` requires your organization UUID — it's needed to verify the proof. You can find it on the dashboard under any product's **Deploy** tab → **Re-verify proofs** step.
71
98
 
72
99
  ## Error handling
73
100
 
74
- The SDK throws two distinct error types:
101
+ The SDK throws three distinct error types:
102
+
103
+ - **`SelfValidationError`**: bad arguments. Raised before any network call when an SDK input (e.g. `flowId`) fails the schema check, or by `SelfWebhooks.verify` when a webhook payload doesn't match a known event shape.
104
+ - **`SelfApiError`**: non-2xx response from the API.
105
+ - **`WebhookVerificationError`**: invalid or missing webhook signature (re-exported from `svix`).
106
+
107
+ Network-level failures (DNS errors, connection refused, request aborts) propagate as the underlying `TypeError`/`AbortError` from `fetch`. They are **not** wrapped in `SelfApiError`. Handle them separately if you need to distinguish network problems from API responses.
75
108
 
76
109
  ```ts
77
- import { SelfApiError, WebhookVerificationError } from '@selfxyz/enterprise-sdk';
110
+ import {
111
+ SelfApiError,
112
+ SelfValidationError,
113
+ WebhookVerificationError,
114
+ } from '@selfxyz/enterprise-sdk';
78
115
 
79
116
  try {
80
117
  await self.sessions.create({ flowId, externalUuid });
81
118
  } catch (err) {
82
- if (err instanceof SelfApiError) {
83
- err.statusCode; // number HTTP status
84
- err.code; // string machine-readable error code
85
- err.message; // string — human-readable
86
- err.details; // Record<string, unknown> | undefined
119
+ if (err instanceof SelfValidationError) {
120
+ err.message; // 'Invalid sessions.create input: flowId (Invalid uuid)'
121
+ err.issues; // ZodIssue[] (raw issues for programmatic handling)
122
+ } else if (err instanceof SelfApiError) {
123
+ err.statusCode; // number (HTTP status)
124
+ err.code; // string (machine-readable error code; see table below)
125
+ err.message; // string (human-readable)
126
+ err.details; // Record<string, unknown> | undefined (see "When details is populated")
127
+ err.requestId; // string | undefined (X-Request-Id from the response)
87
128
  }
88
129
  throw err;
89
130
  }
90
131
  ```
91
132
 
92
- `WebhookVerificationError` is thrown by `SelfWebhooks.verify` when a signature is invalid or missing.
133
+ ### Error codes
134
+
135
+ `SelfApiError.code` carries a machine-readable error code so consumers can branch on the failure mode:
136
+
137
+ | Code | HTTP status | Meaning |
138
+ | -------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
139
+ | `validation_failed` | 400 | Request body failed server-side validation. |
140
+ | `unauthenticated` | 401 | Missing, invalid, or revoked API key. |
141
+ | `unauthenticated` | 402 | Insufficient credits / billing gate triggered. Disambiguate from 401 via `statusCode` and check `details`. |
142
+ | `forbidden` | 403 | API key lacks permission for the resource. |
143
+ | `not_found` | 404 | Flow or session not found. |
144
+ | `conflict` | 409 | Flow has no published version, or other state conflict. |
145
+ | `rate_limited` | 429 | Rate limit exceeded. Back off and retry. |
146
+ | `internal_error` | 500 | Unhandled server error. |
147
+ | `vendor_unavailable` | 503 | Upstream dependency is degraded (e.g. circuit breaker open). |
148
+ | `unknown_error` | any | API response didn't match the expected envelope. The `message` field includes a body preview. |
149
+
150
+ The set of codes may grow over time. Always include a default branch in any `switch (err.code)` so future codes don't fall through silently.
151
+
152
+ ```ts
153
+ try {
154
+ await self.sessions.create({ flowId, externalUuid });
155
+ } catch (err) {
156
+ if (!(err instanceof SelfApiError)) throw err;
157
+ switch (err.code) {
158
+ case 'rate_limited':
159
+ // back off and retry
160
+ break;
161
+ case 'unauthenticated':
162
+ if (err.statusCode === 402) {
163
+ // billing gate. err.details is { balance, required, planTier, creditGateMode }
164
+ } else {
165
+ // bad API key (401)
166
+ }
167
+ break;
168
+ case 'not_found':
169
+ // flow or session doesn't exist
170
+ break;
171
+ default:
172
+ // unknown_error or any future code. Log err.requestId and err.message
173
+ console.error('Self API error', { code: err.code, requestId: err.requestId });
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### When `details` is populated
179
+
180
+ `SelfApiError.details` is the server's optional context object. It's set on the following errors:
181
+
182
+ **`unauthenticated` + `statusCode: 402` (billing gate)**: emitted when your organization's credit balance is below what the requested verification costs:
183
+
184
+ ```ts
185
+ {
186
+ balance: number; // spendable credits at request time
187
+ required: number; // credits needed for this request
188
+ planTier: 'free' | 'starter' | 'enterprise';
189
+ creditGateMode: 'hard'; // always 'hard' (soft-gate orgs do not see this error)
190
+ }
191
+ ```
192
+
193
+ **`validation_failed` + `statusCode: 400`**: `{ issues: ZodIssue[] }`. The SDK pre-validates request bodies and throws `SelfValidationError` before sending, so this server-side variant is normally caught client-side. It can surface if the SDK and server schemas fall out of sync; pin an exact SDK version in production to avoid this.
194
+
195
+ For every other code, `details` is `undefined`.
93
196
 
94
197
  ## License
95
198
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import { z, ZodIssue, ZodError } from 'zod';
2
2
  export { WebhookVerificationError } from 'svix';
3
3
 
4
4
  declare const verificationCompleted: z.ZodObject<{
@@ -305,6 +305,10 @@ interface SelfClientOptions {
305
305
  * });
306
306
  * console.log(session.verificationUrl);
307
307
  * ```
308
+ *
309
+ * Methods throw `SelfValidationError` for bad arguments and `SelfApiError`
310
+ * for non-2xx API responses. See the package README for the full error-code
311
+ * table.
308
312
  */
309
313
  declare class SelfClient {
310
314
  private readonly apiKey;
@@ -347,7 +351,7 @@ declare class SelfWebhooks {
347
351
  * @param secret Webhook signing secret (`whsec_...` from the Self dashboard).
348
352
  * @returns Parsed and verified {@link WebhookEvent}.
349
353
  * @throws `WebhookVerificationError` if the signature is invalid.
350
- * @throws `ZodError` if the payload shape doesn't match any known event type.
354
+ * @throws `SelfValidationError` if the payload shape doesn't match any known event type.
351
355
  */
352
356
  static verify(payload: string | Buffer, headers: WebhookHeaders | Record<string, string>, secret: string): WebhookEvent;
353
357
  }
@@ -357,15 +361,69 @@ interface ApiErrorBody {
357
361
  message: string;
358
362
  details?: Record<string, unknown>;
359
363
  }
364
+ interface SelfApiErrorOptions {
365
+ /** Value of the `X-Request-Id` response header, if present. */
366
+ requestId?: string;
367
+ }
360
368
  /**
361
369
  * Thrown when the Self API returns a non-2xx response.
362
370
  * Wraps the standard `{ error: { code, message, details? } }` envelope.
363
371
  */
364
372
  declare class SelfApiError extends Error {
373
+ readonly name: "SelfApiError";
365
374
  readonly statusCode: number;
366
375
  readonly code: string;
367
376
  readonly details: Record<string, unknown> | undefined;
368
- constructor(statusCode: number, body: ApiErrorBody);
377
+ /** `X-Request-Id` from the response. Include when reporting issues to Self support. */
378
+ readonly requestId: string | undefined;
379
+ constructor(statusCode: number, body: ApiErrorBody, options?: SelfApiErrorOptions);
380
+ }
381
+ /**
382
+ * Thrown when arguments passed to the SDK (session inputs, webhook payloads)
383
+ * fail schema validation before the request is sent / after a webhook is
384
+ * verified. Wraps a `ZodError` with a human-readable message.
385
+ */
386
+ declare class SelfValidationError extends Error {
387
+ readonly name: "SelfValidationError";
388
+ readonly issues: ZodIssue[];
389
+ constructor(zodError: ZodError, context: string);
390
+ }
391
+
392
+ interface VerifiableProofEnvelope {
393
+ attestationId: number;
394
+ proof: {
395
+ a: [string, string];
396
+ b: [[string, string], [string, string]];
397
+ c: [string, string];
398
+ curve: string;
399
+ protocol: 'groth16';
400
+ };
401
+ publicSignals: string[];
402
+ userContextData: string;
403
+ }
404
+ interface VerifyProofParams {
405
+ /** Your organization UUID — required to verify the proof. Find it on the
406
+ * dashboard under any product's Deploy tab → Re-verify proofs step. */
407
+ orgId: string;
408
+ /** Raw proof envelope as received in `verification.completed.proof`. */
409
+ proof: VerifiableProofEnvelope;
410
+ /** Predicates the proof was checked against (from `verification.completed.proof_attributes`). */
411
+ proofAttributes: Record<string, unknown>;
412
+ /** From `verification.completed.environment`. Picks the right verifier endpoint constant. */
413
+ environment: 'test' | 'live';
369
414
  }
415
+ interface VerifyProofResult {
416
+ isValid: boolean;
417
+ }
418
+ /**
419
+ * Independently re-verify a proof you received from Self.
420
+ *
421
+ * `isValid` is true iff the Groth16 proof verifies, the minimum-age
422
+ * predicate is satisfied (if configured), and the user is not on an OFAC
423
+ * list (if OFAC checking is configured).
424
+ *
425
+ * Throws an `Error` if `orgId` is missing.
426
+ */
427
+ declare function verifyProof(params: VerifyProofParams): Promise<VerifyProofResult>;
370
428
 
371
- export { type ApiErrorBody, type CreateSessionBody, type CreateSessionInput, SelfApiError, SelfClient, type SelfClientOptions, SelfWebhooks, type CreateSessionResponse as Session, type SessionDetailResponse as SessionDetail, type VerificationCompletedPayload, type VerificationStorageCommittedPayload, type VerificationStorageFailedPayload, type WebhookEvent, type WebhookHeaders, createSessionBody, createSessionResponse, sessionDetailResponse, verificationCompleted, verificationStorageCommitted, verificationStorageFailed, webhookEvent };
429
+ export { type ApiErrorBody, type CreateSessionBody, type CreateSessionInput, SelfApiError, SelfClient, type SelfClientOptions, SelfValidationError, SelfWebhooks, type CreateSessionResponse as Session, type SessionDetailResponse as SessionDetail, type VerifiableProofEnvelope, type VerificationCompletedPayload, type VerificationStorageCommittedPayload, type VerificationStorageFailedPayload, type VerifyProofParams, type VerifyProofResult, type WebhookEvent, type WebhookHeaders, createSessionBody, createSessionResponse, sessionDetailResponse, verificationCompleted, verificationStorageCommitted, verificationStorageFailed, verifyProof, webhookEvent };
package/dist/index.js CHANGED
@@ -1,17 +1,39 @@
1
1
  // src/errors.ts
2
2
  import { WebhookVerificationError } from "svix";
3
3
  var SelfApiError = class extends Error {
4
+ name = "SelfApiError";
4
5
  statusCode;
5
6
  code;
6
7
  details;
7
- constructor(statusCode, body) {
8
+ /** `X-Request-Id` from the response. Include when reporting issues to Self support. */
9
+ requestId;
10
+ constructor(statusCode, body, options) {
8
11
  super(body.message);
9
- this.name = "SelfApiError";
10
12
  this.statusCode = statusCode;
11
13
  this.code = body.code;
12
14
  this.details = body.details;
15
+ this.requestId = options?.requestId;
13
16
  }
14
17
  };
18
+ var SelfValidationError = class extends Error {
19
+ name = "SelfValidationError";
20
+ issues;
21
+ constructor(zodError, context) {
22
+ super(formatZodIssues(zodError.issues, context));
23
+ this.issues = zodError.issues;
24
+ }
25
+ };
26
+ function formatZodIssues(issues, context) {
27
+ if (issues.length === 0) {
28
+ return `Invalid ${context}`;
29
+ }
30
+ const formatted = issues.map(formatIssue).join("; ");
31
+ return `Invalid ${context}: ${formatted}`;
32
+ }
33
+ function formatIssue(issue) {
34
+ const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
35
+ return `${path} (${issue.message})`;
36
+ }
15
37
 
16
38
  // ../schemas/dist/stripe-events.js
17
39
  import { z } from "zod";
@@ -280,22 +302,39 @@ var SelfClient = class {
280
302
  }
281
303
  if (!res.ok) {
282
304
  const envelope = json;
283
- throw new SelfApiError(res.status, {
284
- code: envelope?.error?.code ?? "unknown_error",
285
- message: envelope?.error?.message ?? (text || res.statusText),
286
- ...envelope?.error?.details ? { details: envelope.error.details } : {}
287
- });
305
+ const requestId = res.headers.get("x-request-id") ?? void 0;
306
+ throw new SelfApiError(
307
+ res.status,
308
+ {
309
+ code: envelope?.error?.code ?? "unknown_error",
310
+ message: envelope?.error?.message ?? fallbackMessage(text, res.statusText, json !== void 0),
311
+ ...envelope?.error?.details ? { details: envelope.error.details } : {}
312
+ },
313
+ requestId !== void 0 ? { requestId } : void 0
314
+ );
288
315
  }
289
316
  return json;
290
317
  }
291
318
  async createSession(params) {
292
- createSessionBody.parse(params);
293
- return this.request("POST", "/v1/sessions", params);
319
+ const result = createSessionBody.safeParse(params);
320
+ if (!result.success) {
321
+ throw new SelfValidationError(result.error, "sessions.create input");
322
+ }
323
+ return this.request("POST", "/v1/sessions", result.data);
294
324
  }
295
325
  getSession(id) {
296
326
  return this.request("GET", `/v1/sessions/${encodeURIComponent(id)}`);
297
327
  }
298
328
  };
329
+ var BODY_PREVIEW_LIMIT = 200;
330
+ function fallbackMessage(text, statusText, jsonParsed) {
331
+ if (text.length === 0) {
332
+ return `${statusText} (empty response body)`;
333
+ }
334
+ const truncated = text.length > BODY_PREVIEW_LIMIT ? `${text.slice(0, BODY_PREVIEW_LIMIT)}\u2026` : text;
335
+ const label = jsonParsed ? "response body" : "non-JSON response body";
336
+ return `${statusText} (${label}: ${JSON.stringify(truncated)})`;
337
+ }
299
338
 
300
339
  // src/webhooks.ts
301
340
  import { z as z6 } from "zod";
@@ -314,7 +353,7 @@ var SelfWebhooks = class {
314
353
  * @param secret Webhook signing secret (`whsec_...` from the Self dashboard).
315
354
  * @returns Parsed and verified {@link WebhookEvent}.
316
355
  * @throws `WebhookVerificationError` if the signature is invalid.
317
- * @throws `ZodError` if the payload shape doesn't match any known event type.
356
+ * @throws `SelfValidationError` if the payload shape doesn't match any known event type.
318
357
  */
319
358
  static verify(payload, headers, secret) {
320
359
  const wh = new Webhook(secret);
@@ -322,12 +361,52 @@ var SelfWebhooks = class {
322
361
  typeof payload === "string" ? payload : payload.toString("utf-8"),
323
362
  headers
324
363
  );
325
- return forwardCompatibleEvent.parse(verified);
364
+ const result = forwardCompatibleEvent.safeParse(verified);
365
+ if (!result.success) {
366
+ throw new SelfValidationError(result.error, "webhook payload");
367
+ }
368
+ return result.data;
326
369
  }
327
370
  };
371
+
372
+ // src/verify-proof.ts
373
+ import { AllIds, DefaultConfigStore, SelfBackendVerifier } from "@selfxyz/core";
374
+ var VERIFIER_ENDPOINTS = {
375
+ live: "https://verifier.self.xyz/verify",
376
+ test: "https://verifier.staging.self.xyz/verify"
377
+ };
378
+ function orgIdToScope(orgId) {
379
+ return orgId.replaceAll("-", "").slice(0, 30);
380
+ }
381
+ async function verifyProof(params) {
382
+ if (!params.orgId) {
383
+ throw new Error("verifyProof: orgId is required");
384
+ }
385
+ const verifier = new SelfBackendVerifier(
386
+ orgIdToScope(params.orgId),
387
+ VERIFIER_ENDPOINTS[params.environment],
388
+ params.environment === "test",
389
+ AllIds,
390
+ new DefaultConfigStore(params.proofAttributes),
391
+ "uuid"
392
+ );
393
+ try {
394
+ const result = await verifier.verify(
395
+ params.proof.attestationId,
396
+ params.proof.proof,
397
+ params.proof.publicSignals,
398
+ params.proof.userContextData
399
+ );
400
+ const { isValid, isMinimumAgeValid, isOfacValid } = result.isValidDetails;
401
+ return { isValid: isValid && isMinimumAgeValid && !isOfacValid };
402
+ } catch {
403
+ return { isValid: false };
404
+ }
405
+ }
328
406
  export {
329
407
  SelfApiError,
330
408
  SelfClient,
409
+ SelfValidationError,
331
410
  SelfWebhooks,
332
411
  WebhookVerificationError,
333
412
  createSessionBody,
@@ -336,5 +415,6 @@ export {
336
415
  verificationCompleted,
337
416
  verificationStorageCommitted,
338
417
  verificationStorageFailed,
418
+ verifyProof,
339
419
  webhookEvent
340
420
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@selfxyz/enterprise-sdk",
3
- "version": "0.1.1",
4
- "description": "Node/TS SDK for Self.xyz enterprise verification session creation, webhook signature verification, typed payloads.",
3
+ "version": "0.2.0",
4
+ "description": "Node/TS SDK for Self.xyz enterprise verification: session creation, webhook signature verification, typed payloads.",
5
5
  "license": "BUSL-1.1",
6
6
  "homepage": "https://self.xyz",
7
7
  "repository": {
@@ -43,6 +43,7 @@
43
43
  "clean": "rm -rf dist .turbo *.tsbuildinfo"
44
44
  },
45
45
  "dependencies": {
46
+ "@selfxyz/core": "^1.2.0-beta.1",
46
47
  "svix": "^1.92.2",
47
48
  "zod": "^3.24.1"
48
49
  },