@sekyro/node 0.0.0-stage → 0.1.0-rc.1

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.
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Wire-level enums and shapes of the Sekyro partner API (`/api/v2`). These
3
+ * mirror the backend's frozen partner contract; they are plain string unions so
4
+ * a newer API value never throws at runtime — it simply widens the type in the
5
+ * next SDK release.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ /** Session lifecycle status. @public */
10
+ type SessionStatus = "created" | "in_progress" | "review_pending" | "completed" | "expired" | "failed";
11
+ /**
12
+ * Terminal verdict, orthogonal to {@link SessionStatus}. `null` until the session
13
+ * reaches a terminal status; `review` when a human reviewer must adjudicate.
14
+ * @public
15
+ */
16
+ type SessionOutcome = "approved" | "review" | "rejected";
17
+ /** The verification checks a flow may contain. @public */
18
+ type CheckType = "liveness" | "face_match" | "doc_ocr" | "population_registry" | "phone_otp";
19
+ /** Per-check execution state within a session. @public */
20
+ type CheckState = "pending" | "input_required" | "processing" | "passed" | "failed" | "review" | "skipped";
21
+ /** Hosted-flow languages. @public */
22
+ type Locale = "ar" | "fr" | "en";
23
+ /** The environment a webhook was emitted from. @public */
24
+ type SekyroEnvironment = "sandbox" | "live";
25
+ /**
26
+ * One check of a session, as the partner API projects it: enum/id fields only,
27
+ * never civil data.
28
+ * @public
29
+ */
30
+ interface SessionStep {
31
+ checkType: CheckType;
32
+ state: CheckState;
33
+ /** Machine-readable failure/review reason, when the check has one. */
34
+ reason?: string;
35
+ /** Calibrated model score in [0, 1] for scored checks (liveness, face match). */
36
+ calibratedScore?: number;
37
+ }
38
+ /**
39
+ * Input of {@link SessionsApi.create}. Name the recipe by EITHER `flow` (the
40
+ * flow slug) OR `templateId` — exactly one; the API rejects both or neither.
41
+ * @public
42
+ */
43
+ interface CreateSessionInput {
44
+ /** Flow slug configured for your organisation. */
45
+ flow?: string;
46
+ /** Legacy template id (pre-flows path). Prefer `flow`. */
47
+ templateId?: string;
48
+ /** Pins the hosted-flow language. Omit to let the person's device decide. */
49
+ locale?: Locale;
50
+ /**
51
+ * Step inputs consumed server-side. The known anchors are typed (all
52
+ * strings — an `nni` is a 10-digit string, never a number); further keys
53
+ * pass through to the API as-is. `nni` binds the session to that identity:
54
+ * the hosted flow may omit it but cannot substitute another.
55
+ */
56
+ input?: {
57
+ /** 10-digit Mauritanian NNI, as a string. */
58
+ nni?: string;
59
+ /** E.164 phone number, e.g. `+22236123456`. */
60
+ phone?: string;
61
+ fullName?: string;
62
+ /** ISO 8601 date (`YYYY-MM-DD`). */
63
+ birthDate?: string;
64
+ } & Record<string, unknown>;
65
+ }
66
+ /** Per-request options. @public */
67
+ interface RequestOptions {
68
+ /**
69
+ * Sent as `X-Idempotency-Key`. Replaying the same key within 24 h returns the
70
+ * original response instead of creating a second session.
71
+ */
72
+ idempotencyKey?: string;
73
+ }
74
+ /**
75
+ * Result of {@link SessionsApi.create}. `clientToken` is returned ONCE and never
76
+ * again — hand it to the browser (`@sekyro/web`) and do not persist it.
77
+ * @public
78
+ */
79
+ interface CreatedSession {
80
+ id: string;
81
+ status: SessionStatus;
82
+ /** One-time credential for the client SDK / hosted flow. */
83
+ clientToken: string;
84
+ /** Hosted verification link for this session. */
85
+ hostedUrl: string;
86
+ steps: SessionStep[];
87
+ /** ISO 8601 — when the session (and its link) stop accepting input. */
88
+ expiresAt: string;
89
+ }
90
+ /**
91
+ * The whitelisted session view returned by {@link SessionsApi.get}. Never
92
+ * contains the client token.
93
+ * @public
94
+ */
95
+ interface Session {
96
+ id: string;
97
+ status: SessionStatus;
98
+ outcome: SessionOutcome | null;
99
+ steps: SessionStep[];
100
+ /** ISO 8601. */
101
+ expiresAt: string;
102
+ /** ISO 8601. */
103
+ createdAt: string;
104
+ }
105
+ /**
106
+ * `data` of the terminal / review-pending session lifecycle events
107
+ * (`session.completed` | `session.failed` | `session.review_pending` |
108
+ * `session.expired` | `session.rejected`) — ids and enums only, no civil data,
109
+ * scores or links. `outcome` and `steps` travel on `completed` / `failed`;
110
+ * `review_pending` carries `steps` without an outcome; `expired` carries the
111
+ * status alone.
112
+ * @public
113
+ */
114
+ interface SessionLifecycleEventData {
115
+ sessionId: string;
116
+ status: SessionStatus;
117
+ outcome?: SessionOutcome | null;
118
+ steps?: Array<Pick<SessionStep, "checkType" | "state">>;
119
+ /** Machine-readable reason, when the transition has one. */
120
+ reason?: string;
121
+ }
122
+ /**
123
+ * `data` of `session.step.completed` — the check that just passed.
124
+ * @public
125
+ */
126
+ interface SessionStepCompletedEventData {
127
+ sessionId: string;
128
+ checkType: CheckType;
129
+ state: CheckState;
130
+ }
131
+ /**
132
+ * `data` of `session.review_decided` — a human (or the review-timeout sweep)
133
+ * decided a pending review. `outcome` speaks the REVIEW vocabulary
134
+ * (`pass` | `fail`), not the session outcome; `reason` is the reviewer's code
135
+ * (e.g. `review_timeout` for an expired review).
136
+ * @public
137
+ */
138
+ interface SessionReviewDecidedEventData {
139
+ sessionId: string;
140
+ outcome: "pass" | "fail";
141
+ reason: string;
142
+ }
143
+ /**
144
+ * Event type → the shape of its `data`. Session events are typed; the
145
+ * remaining events carry an open record until their payloads are frozen.
146
+ * @public
147
+ */
148
+ interface WebhookEventDataMap {
149
+ "session.created": Record<string, unknown>;
150
+ "session.started": Record<string, unknown>;
151
+ "session.step.completed": SessionStepCompletedEventData;
152
+ "session.review_pending": SessionLifecycleEventData;
153
+ "session.review_decided": SessionReviewDecidedEventData;
154
+ "session.completed": SessionLifecycleEventData;
155
+ "session.rejected": SessionLifecycleEventData;
156
+ "session.expired": SessionLifecycleEventData;
157
+ "session.failed": SessionLifecycleEventData;
158
+ "subject.enrolled": Record<string, unknown>;
159
+ "subject.deleted": Record<string, unknown>;
160
+ "aml.match_flagged": Record<string, unknown>;
161
+ "payment.received": Record<string, unknown>;
162
+ "invoice.generated": Record<string, unknown>;
163
+ "invoice.paid": Record<string, unknown>;
164
+ "invoice.overdue": Record<string, unknown>;
165
+ "webhook.test": Record<string, unknown>;
166
+ }
167
+ /** Every event a partner endpoint may receive. @public */
168
+ type WebhookEventType = keyof WebhookEventDataMap;
169
+ /**
170
+ * The signed webhook payload for ONE event type. There is no global API
171
+ * version; each event carries its own per-event `version`. Retried deliveries
172
+ * keep the same `id` — de-duplicate on it.
173
+ * @public
174
+ */
175
+ interface WebhookEnvelopeOf<E extends WebhookEventType> {
176
+ /** UUIDv7, stable across delivery retries. */
177
+ id: string;
178
+ event: E;
179
+ /** Per-event payload version. */
180
+ version: number;
181
+ /** ISO 8601. */
182
+ createdAt: string;
183
+ orgId: string;
184
+ env: SekyroEnvironment;
185
+ data: WebhookEventDataMap[E];
186
+ }
187
+ /**
188
+ * The signed webhook payload, discriminated on `event`: narrowing on
189
+ * `envelope.event` narrows `envelope.data` to that event's shape.
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * const envelope = verifyWebhook(rawBody, header, secret);
194
+ * switch (envelope.event) {
195
+ * case "session.completed":
196
+ * envelope.data.outcome; // SessionOutcome | null | undefined
197
+ * break;
198
+ * case "session.review_decided":
199
+ * envelope.data.outcome; // "pass" | "fail"
200
+ * break;
201
+ * }
202
+ * ```
203
+ * @public
204
+ */
205
+ type WebhookEnvelope<E extends WebhookEventType = WebhookEventType> = E extends WebhookEventType ? WebhookEnvelopeOf<E> : never;
206
+ /**
207
+ * `data` of any `session.*` event.
208
+ * @deprecated Use {@link SessionLifecycleEventData}, {@link SessionStepCompletedEventData}
209
+ * or {@link SessionReviewDecidedEventData}, or narrow a {@link WebhookEnvelope} on `event`.
210
+ * @public
211
+ */
212
+ type SessionEventData = SessionLifecycleEventData | SessionStepCompletedEventData | SessionReviewDecidedEventData;
213
+
214
+ /** Default partner API origin + version prefix. @public */
215
+ declare const DEFAULT_BASE_URL = "https://api.sekyro.ai/api/v2";
216
+ /** Default per-request timeout. @public */
217
+ declare const DEFAULT_TIMEOUT_MS = 30000;
218
+ /** `fetch`-compatible function; the global `fetch` by default. @public */
219
+ type FetchLike = typeof globalThis.fetch;
220
+ /** Constructor options of {@link Sekyro}. @public */
221
+ interface SekyroOptions {
222
+ /** Partner API key (server-side only). Sent as `Authorization: Bearer`. */
223
+ apiKey: string;
224
+ /** API base URL incl. the `/api/v2` prefix. Default {@link DEFAULT_BASE_URL}. */
225
+ baseUrl?: string;
226
+ /** Per-request timeout in ms. Default {@link DEFAULT_TIMEOUT_MS}. */
227
+ timeoutMs?: number;
228
+ /** Custom `fetch` (tests, proxies, custom agents). Default `globalThis.fetch`. */
229
+ fetch?: FetchLike;
230
+ }
231
+ /**
232
+ * `sekyro.sessions` — create and read verification sessions.
233
+ *
234
+ * Exported as a TYPE only (`import type { SessionsApi }`): the class value is
235
+ * not part of the package surface, so it can only be obtained through
236
+ * {@link Sekyro.sessions} — never constructed by hand with an unchecked
237
+ * transport.
238
+ * @public
239
+ */
240
+ declare class SessionsApi {
241
+ private readonly call;
242
+ /**
243
+ * Constructed by {@link Sekyro}; the transport is kept out of the public
244
+ * surface on purpose (only its bound `call` crosses this boundary).
245
+ * @internal
246
+ */
247
+ constructor(call: <T>(method: "GET" | "POST", path: string, body?: unknown, idempotencyKey?: string) => Promise<T>);
248
+ /**
249
+ * `POST /sessions`. Returns the one-time `clientToken` — hand it to the
250
+ * browser, never persist it. Pass an `idempotencyKey` so a retried call
251
+ * cannot create a second session.
252
+ */
253
+ create(input: CreateSessionInput, opts?: RequestOptions): Promise<CreatedSession>;
254
+ /** `GET /sessions/:id` — the whitelisted view (no client token). */
255
+ get(id: string): Promise<Session>;
256
+ }
257
+ /**
258
+ * Sekyro partner API client. Server-side only: it carries your API key.
259
+ *
260
+ * Throws {@link SekyroConfigError} synchronously when `apiKey` is missing or
261
+ * `baseUrl` is not `https:` (plain `http:` is accepted for `localhost`,
262
+ * `127.0.0.1` and `[::1]` only).
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * const sekyro = new Sekyro({ apiKey: process.env.SEKYRO_API_KEY! });
267
+ * const { id, clientToken } = await sekyro.sessions.create({ flow: "onboarding" });
268
+ * ```
269
+ * @public
270
+ */
271
+ declare class Sekyro {
272
+ readonly sessions: SessionsApi;
273
+ constructor(opts: SekyroOptions);
274
+ }
275
+
276
+ /** Why {@link verifyWebhook} rejected a delivery. @public */
277
+ type SekyroWebhookErrorCode = "invalid_signature" | "stale" | "malformed";
278
+ /**
279
+ * Thrown by {@link verifyWebhook}. Respond with a 4xx and do not process the
280
+ * body; Sekyro retries deliveries that do not receive a 2xx.
281
+ * @public
282
+ */
283
+ declare class SekyroWebhookError extends Error {
284
+ readonly name = "SekyroWebhookError";
285
+ readonly code: SekyroWebhookErrorCode;
286
+ constructor(code: SekyroWebhookErrorCode, message?: string);
287
+ }
288
+ /** Why the {@link Sekyro} constructor rejected its options. @public */
289
+ type SekyroConfigErrorCode = "missing_api_key" | "invalid_base_url";
290
+ /**
291
+ * Thrown synchronously by `new Sekyro(...)` for an unusable configuration —
292
+ * before any request is made. Extends `TypeError` so existing
293
+ * `instanceof TypeError` guards keep working.
294
+ * @public
295
+ */
296
+ declare class SekyroConfigError extends TypeError {
297
+ readonly name = "SekyroConfigError";
298
+ readonly code: SekyroConfigErrorCode;
299
+ constructor(code: SekyroConfigErrorCode, message?: string);
300
+ }
301
+ /** Constructor input of {@link SekyroApiError}. @public */
302
+ interface SekyroApiErrorInit {
303
+ /** HTTP status; `0` when no response was received (network / timeout). */
304
+ status: number;
305
+ /** API error code, or `network` | `timeout` | `http_error` | `malformed_response`. */
306
+ code: string;
307
+ message: string;
308
+ /** The API's `request_id`, when the response carried one. */
309
+ requestId?: string;
310
+ /** Underlying error (network failures, abort reasons, JSON parse errors). */
311
+ cause?: unknown;
312
+ }
313
+ /** Error codes the SDK itself produces (as opposed to codes from the API). @public */
314
+ type SekyroClientErrorCode = "network" | "timeout" | "http_error" | "malformed_response";
315
+ /**
316
+ * Any failed call through {@link Sekyro}: a non-2xx partner envelope
317
+ * `{ status: 'error', error: { code, message, request_id } }`, a non-JSON
318
+ * response, a network failure or a timeout.
319
+ *
320
+ * `retryable` is `true` for `network`, `timeout`, HTTP 429, HTTP 5xx and the
321
+ * `idempotency_in_progress` code — retry with the SAME idempotency key.
322
+ * @public
323
+ */
324
+ declare class SekyroApiError extends Error {
325
+ readonly name = "SekyroApiError";
326
+ readonly status: number;
327
+ readonly code: string;
328
+ readonly requestId: string | undefined;
329
+ readonly retryable: boolean;
330
+ constructor(init: SekyroApiErrorInit);
331
+ }
332
+
333
+ /**
334
+ * The HTTP header carrying the webhook signature:
335
+ * `t=<unix seconds>,v1=<hex>[,v1=<hex>]`.
336
+ * @public
337
+ */
338
+ declare const SEKYRO_SIGNATURE_HEADER = "x-sekyro-signature";
339
+ /**
340
+ * The pre-rebrand name of the signature header. Sekyro sends BOTH headers with
341
+ * the same value during the transition; prefer {@link SEKYRO_SIGNATURE_HEADER}
342
+ * and use {@link signatureFromHeaders} to accept either.
343
+ * @public
344
+ */
345
+ declare const LEGACY_SIGNATURE_HEADER = "x-ibtikar-signature";
346
+ /**
347
+ * Pick the signature header out of an incoming request's headers: the
348
+ * `x-sekyro-signature` value when present, else the legacy
349
+ * `x-ibtikar-signature`, else `undefined`. Accepts a Fetch `Headers` object or
350
+ * a Node-style plain record (lower- or mixed-case keys; a multi-valued header
351
+ * yields its first value).
352
+ *
353
+ * @example
354
+ * ```ts
355
+ * verifyWebhook(rawBody, signatureFromHeaders(req.headers), secret);
356
+ * ```
357
+ * @public
358
+ */
359
+ declare function signatureFromHeaders(headers: Headers | Record<string, string | string[] | undefined>): string | undefined;
360
+ /** Options of {@link verifyWebhook}. @public */
361
+ interface VerifyWebhookOptions {
362
+ /** Max |now − t| in seconds before a delivery is rejected as `stale`. Default 300. */
363
+ toleranceSec?: number;
364
+ /** Clock returning epoch milliseconds. Default `Date.now`. */
365
+ now?: () => number;
366
+ }
367
+ /**
368
+ * Verify a Sekyro webhook delivery and return its parsed envelope, discriminated
369
+ * on `event` (narrow with a `switch` to type `data`).
370
+ *
371
+ * - `rawBody` MUST be the request body exactly as received (before any JSON
372
+ * parsing / re-serialisation); the signature covers those bytes.
373
+ * - Accepts the delivery when ANY `v1` in the header matches, which is what
374
+ * makes secret rotation seamless (Sekyro signs with current + previous).
375
+ * - Uses `node:crypto` only; the digest compare is constant-time.
376
+ *
377
+ * Checks run in this order: header/secret present → header well-formed →
378
+ * timestamp within tolerance → signature → body is a JSON object.
379
+ *
380
+ * @param rawBody - The raw UTF-8 request body.
381
+ * @param header - The `x-sekyro-signature` header value (or the legacy
382
+ * `x-ibtikar-signature` — same format; see {@link signatureFromHeaders}).
383
+ * @param secret - The endpoint secret you hold (current or previous).
384
+ * @throws {@link SekyroWebhookError} with `code` `invalid_signature` | `stale` | `malformed`.
385
+ * @public
386
+ */
387
+ declare function verifyWebhook(rawBody: string, header: string | null | undefined, secret: string, opts?: VerifyWebhookOptions): WebhookEnvelope;
388
+
389
+ export { type CheckState, type CheckType, type CreateSessionInput, type CreatedSession, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, type FetchLike, LEGACY_SIGNATURE_HEADER, type Locale, type RequestOptions, SEKYRO_SIGNATURE_HEADER, Sekyro, SekyroApiError, type SekyroApiErrorInit, type SekyroClientErrorCode, SekyroConfigError, type SekyroConfigErrorCode, type SekyroEnvironment, type SekyroOptions, SekyroWebhookError, type SekyroWebhookErrorCode, type Session, type SessionEventData, type SessionLifecycleEventData, type SessionOutcome, type SessionReviewDecidedEventData, type SessionStatus, type SessionStep, type SessionStepCompletedEventData, SessionsApi, type VerifyWebhookOptions, type WebhookEnvelope, type WebhookEnvelopeOf, type WebhookEventDataMap, type WebhookEventType, signatureFromHeaders, verifyWebhook };