@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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @sekyro/node
2
+
3
+ ## 0.1.0-rc.1
4
+
5
+ ### Minor Changes
6
+
7
+ - 466d3e1: Initial release line of the Sekyro SDKs: `@sekyro/web` (browser liveness capture, hosted-verification client and popup embed), `@sekyro/react` (the themeable, accessible `<LivenessCapture />` component over `@sekyro/web`, ar/fr/en, RTL-safe) and `@sekyro/node` (server-side sessions client and signed-webhook verification, zero runtime dependencies). Pre-releases ship as `0.1.0-rc.N` under the `next` dist-tag from `develop`; the first stable `0.1.0` ships from `main`. All three packages are proprietary (`UNLICENSED` — see each package's LICENSE).
8
+ - 880bde9: Webhook envelope typing and the signature header rename.
9
+
10
+ - `SEKYRO_SIGNATURE_HEADER` now equals `x-sekyro-signature` (it was `x-ibtikar-signature`). Sekyro sends both headers during the transition; the old name is exported as `LEGACY_SIGNATURE_HEADER`, and the new `signatureFromHeaders(headers)` helper picks whichever is present. Code that read `req.headers[SEKYRO_SIGNATURE_HEADER]` keeps working against current deliveries but should move to `signatureFromHeaders`.
11
+ - `verifyWebhook()` returns a `WebhookEnvelope` **discriminated on `event`**: narrowing `envelope.event` narrows `envelope.data`. The `verifyWebhook<T>()` type parameter is gone — remove the generic argument.
12
+ - New per-event payload types: `SessionLifecycleEventData` (`session.completed` / `failed` / `review_pending` / `expired` / `rejected`), `SessionStepCompletedEventData` (`session.step.completed`), `SessionReviewDecidedEventData` (`session.review_decided` — `outcome: 'pass' | 'fail'`, `reason`), plus `WebhookEventDataMap` and `WebhookEnvelopeOf<E>`.
13
+ - `SessionEventData` is kept as a **deprecated** alias of the union of the session payload types so existing imports compile.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Sekyro SDK — Proprietary License
2
+
3
+ Copyright (c) 2026 Ibtikar Technologies. All rights reserved.
4
+
5
+ This software (the "SDK") is proprietary to Ibtikar Technologies. Use is
6
+ permitted only under a Sekyro customer agreement with Ibtikar Technologies, and
7
+ only to integrate the customer's own applications with the Sekyro
8
+ identity-verification service. For that purpose you may install the SDK
9
+ from the public npm registry and run it unmodified.
10
+
11
+ No other right or license, express or implied, is granted. You may not
12
+ modify, reverse-engineer, redistribute, sublicense, resell or create
13
+ derivative works of the SDK, nor remove or alter this notice. The source
14
+ code is published for transparency (build provenance, security review); its
15
+ publication grants no additional rights.
16
+
17
+ THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED. TO THE EXTENT PERMITTED BY LAW, IBTIKAR TECHNOLOGIES SHALL NOT BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE
20
+ SDK, EXCEPT AS PROVIDED IN THE APPLICABLE CUSTOMER AGREEMENT.
package/README.md CHANGED
@@ -1,3 +1,150 @@
1
- # Temporary Holding Version
1
+ # @sekyro/node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Server SDK for [Sekyro](https://sekyro.ai) identity verification. Zero runtime
4
+ dependencies, Node.js ≥ 20, ESM + CommonJS, typed.
5
+
6
+ Two jobs, nothing else:
7
+
8
+ 1. **Sessions** — create a verification session from your backend and read its
9
+ result. The API key never leaves your server.
10
+ 2. **Webhooks** — verify the `x-sekyro-signature` header on deliveries (the
11
+ legacy `x-ibtikar-signature` name is still sent) and get a typed envelope
12
+ back.
13
+
14
+ The SDK does **not** orchestrate checks or retry for you; the partner owns the
15
+ retry policy (`SekyroApiError.retryable` tells you when a retry is sensible).
16
+
17
+ **Integration guide:** [docs/partner/web-sdk.md](../../docs/partner/web-sdk.md)
18
+ (server-bound NNI, display vs authoritative results, webhook receivers for
19
+ Next.js and Express, billing). The hosted-flow path is in the
20
+ [quickstart](../../docs/partner/quickstart.md).
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ npm install @sekyro/node
26
+ ```
27
+
28
+ ## Sessions
29
+
30
+ ```ts
31
+ import { Sekyro } from "@sekyro/node";
32
+
33
+ const sekyro = new Sekyro({
34
+ apiKey: process.env.SEKYRO_API_KEY!,
35
+ // baseUrl: "https://api.sekyro.ai/api/v2", // default; use the staging origin for sandbox work
36
+ // timeoutMs: 30_000, // default
37
+ });
38
+ // Throws `SekyroConfigError` (a TypeError; `code` = 'missing_api_key' | 'invalid_base_url')
39
+ // when apiKey is missing or baseUrl is not https: — plain http: is accepted only for
40
+ // localhost / 127.0.0.1 / [::1], since the API key rides on every request.
41
+
42
+ // Create — returns the one-time client token your front end hands to @sekyro/web.
43
+ // `input.nni` BINDS the session to that identity: the browser never sends the
44
+ // NNI (a different one is refused with 409 nni_locked).
45
+ const created = await sekyro.sessions.create(
46
+ { flow: "biometric", locale: "ar", input: { nni: "1357924632" } },
47
+ { idempotencyKey: `signup:${userId}` },
48
+ );
49
+ created.id; // persist against your user before returning to the page
50
+ created.clientToken; // give this to the browser; it is returned ONCE
51
+ created.hostedUrl; // the hosted-flow link, if you use that path instead
52
+ created.expiresAt; // ISO 8601
53
+
54
+ // Read — the whitelisted session view (never contains the client token).
55
+ const session = await sekyro.sessions.get(created.id);
56
+ session.status; // 'created' | 'in_progress' | 'review_pending' | 'completed' | 'expired' | 'failed'
57
+ session.outcome; // 'approved' | 'review' | 'rejected' | null
58
+ session.steps; // [{ checkType, state, reason?, calibratedScore? }]
59
+ ```
60
+
61
+ Exactly one of `flow` or `templateId` must be given (the API rejects both or
62
+ neither with `400`). `input` carries the step anchors — `nni`, `phone`,
63
+ `fullName`, `birthDate`, all **strings** (an NNI is a 10-digit string, never a
64
+ number) — plus any further keys your flow needs; `input.nni` binds the session
65
+ to that identity.
66
+
67
+ `sekyro.sessions` is a `SessionsApi` — exported as a type only; you get the
68
+ instance from the client, never by constructing one.
69
+
70
+ ### Errors
71
+
72
+ Every non-2xx response, network failure and timeout throws `SekyroApiError`:
73
+
74
+ | field | meaning |
75
+ |---|---|
76
+ | `status` | HTTP status (`0` for network / timeout) |
77
+ | `code` | API error code, or `network` / `timeout` / `http_error` / `malformed_response` |
78
+ | `message` | human-readable, from the API when available |
79
+ | `requestId` | the API's `request_id`, quote it to support |
80
+ | `retryable` | `true` for `network`, `timeout`, `429`, `5xx` and `idempotency_in_progress` |
81
+
82
+ ```ts
83
+ import { SekyroApiError } from "@sekyro/node";
84
+
85
+ try {
86
+ await sekyro.sessions.create({ flow: "onboarding" });
87
+ } catch (err) {
88
+ if (err instanceof SekyroApiError && err.retryable) {
89
+ // back off and retry with the SAME idempotencyKey
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## Webhooks
95
+
96
+ Verify with the **raw** request body (bytes as received, before any JSON
97
+ parsing or re-serialisation) and the signature header: `x-sekyro-signature`
98
+ (primary), falling back to the legacy `x-ibtikar-signature` while both are
99
+ sent.
100
+
101
+ ```ts
102
+ import { verifyWebhook, signatureFromHeaders, SekyroWebhookError } from "@sekyro/node";
103
+
104
+ app.post("/webhooks/sekyro", express.raw({ type: "application/json" }), (req, res) => {
105
+ try {
106
+ const envelope = verifyWebhook(
107
+ req.body.toString("utf8"),
108
+ signatureFromHeaders(req.headers), // x-sekyro-signature, else the legacy x-ibtikar-signature
109
+ process.env.SEKYRO_WEBHOOK_SECRET!,
110
+ );
111
+ // The envelope is discriminated on `event`: narrowing it types `data`.
112
+ switch (envelope.event) {
113
+ case "session.completed":
114
+ case "session.failed":
115
+ envelope.data; // SessionLifecycleEventData — { sessionId, status, outcome, steps }
116
+ break;
117
+ case "session.review_decided":
118
+ envelope.data; // SessionReviewDecidedEventData — { sessionId, outcome: 'pass' | 'fail', reason }
119
+ break;
120
+ case "session.step.completed":
121
+ envelope.data; // SessionStepCompletedEventData — { sessionId, checkType, state }
122
+ break;
123
+ }
124
+ res.sendStatus(200);
125
+ } catch (err) {
126
+ if (err instanceof SekyroWebhookError) return res.sendStatus(400); // invalid_signature | stale | malformed
127
+ throw err;
128
+ }
129
+ });
130
+ ```
131
+
132
+ - The header is `t=<unix seconds>,v1=<hex>[,v1=<hex>]`. During a secret
133
+ rotation Sekyro signs with both the current and the previous secret, so the
134
+ delivery is accepted when **any** `v1` matches the secret you hold.
135
+ - **Header names.** Deliveries carry the signature in `x-sekyro-signature`
136
+ (`SEKYRO_SIGNATURE_HEADER`) and, for the transition, the identical legacy
137
+ `x-ibtikar-signature` (`LEGACY_SIGNATURE_HEADER`). `signatureFromHeaders()`
138
+ takes a Fetch `Headers` or a Node headers record and returns the branded
139
+ header, else the legacy one. The event id/type travel the same way:
140
+ `x-sekyro-event-id` / `x-sekyro-event-type` alongside their `x-ibtikar-*`
141
+ twins.
142
+ - Each `v1` is `HMAC-SHA256(secret, "<t>.<rawBody>")`, compared in constant time.
143
+ - Deliveries older or newer than `toleranceSec` (default 300 s) are rejected as
144
+ `stale`; pass `{ now }` to inject a clock in tests.
145
+ - Retried deliveries carry the same envelope `id`; de-duplicate on it.
146
+
147
+ ## Compatibility
148
+
149
+ Only `node:crypto` and the global `fetch` are used. You can inject a `fetch`
150
+ implementation via `new Sekyro({ fetch })` for tests or custom agents.
package/dist/index.cjs ADDED
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
24
+ DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
25
+ LEGACY_SIGNATURE_HEADER: () => LEGACY_SIGNATURE_HEADER,
26
+ SEKYRO_SIGNATURE_HEADER: () => SEKYRO_SIGNATURE_HEADER,
27
+ Sekyro: () => Sekyro,
28
+ SekyroApiError: () => SekyroApiError,
29
+ SekyroConfigError: () => SekyroConfigError,
30
+ SekyroWebhookError: () => SekyroWebhookError,
31
+ signatureFromHeaders: () => signatureFromHeaders,
32
+ verifyWebhook: () => verifyWebhook
33
+ });
34
+ module.exports = __toCommonJS(src_exports);
35
+
36
+ // src/errors.ts
37
+ var WEBHOOK_MESSAGES = {
38
+ invalid_signature: "No v1 signature in the header matches this body and secret",
39
+ stale: "Delivery timestamp is outside the tolerance window",
40
+ malformed: "Signature header or body is not in the expected format"
41
+ };
42
+ var SekyroWebhookError = class extends Error {
43
+ name = "SekyroWebhookError";
44
+ code;
45
+ constructor(code, message = WEBHOOK_MESSAGES[code]) {
46
+ super(message);
47
+ this.code = code;
48
+ }
49
+ };
50
+ var CONFIG_MESSAGES = {
51
+ missing_api_key: "Sekyro: `apiKey` is required",
52
+ invalid_base_url: "Sekyro: `baseUrl` must be an https: URL (http: is allowed only for localhost, 127.0.0.1 or [::1])"
53
+ };
54
+ var SekyroConfigError = class extends TypeError {
55
+ name = "SekyroConfigError";
56
+ code;
57
+ constructor(code, message = CONFIG_MESSAGES[code]) {
58
+ super(message);
59
+ this.code = code;
60
+ }
61
+ };
62
+ var SekyroApiError = class extends Error {
63
+ name = "SekyroApiError";
64
+ status;
65
+ code;
66
+ requestId;
67
+ retryable;
68
+ constructor(init) {
69
+ super(init.message, init.cause === void 0 ? void 0 : { cause: init.cause });
70
+ this.status = init.status;
71
+ this.code = init.code;
72
+ this.requestId = init.requestId;
73
+ this.retryable = isRetryable(init.status, init.code);
74
+ }
75
+ };
76
+ function isRetryable(status, code) {
77
+ if (code === "network" || code === "timeout") return true;
78
+ if (code === "idempotency_in_progress") return true;
79
+ if (status === 429) return true;
80
+ return status >= 500 && status <= 599;
81
+ }
82
+
83
+ // src/version.ts
84
+ var SDK_VERSION = true ? "0.1.0-rc.1" : "0.0.0-dev";
85
+
86
+ // src/client.ts
87
+ var USER_AGENT = `@sekyro/node/${SDK_VERSION}`;
88
+ var DEFAULT_BASE_URL = "https://api.sekyro.ai/api/v2";
89
+ var DEFAULT_TIMEOUT_MS = 3e4;
90
+ function isSuccessEnvelope(value) {
91
+ return typeof value === "object" && value !== null && value.success === true && "data" in value;
92
+ }
93
+ function isErrorEnvelope(value) {
94
+ if (typeof value !== "object" || value === null) return false;
95
+ const v = value;
96
+ if (v.status !== "error" || typeof v.error !== "object" || v.error === null) return false;
97
+ const e = v.error;
98
+ return typeof e.code === "string" && typeof e.message === "string";
99
+ }
100
+ function isAbortLike(err) {
101
+ if (typeof err !== "object" || err === null) return false;
102
+ const name = err.name;
103
+ return name === "TimeoutError" || name === "AbortError";
104
+ }
105
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
106
+ function assertSafeBaseUrl(baseUrl) {
107
+ let url;
108
+ try {
109
+ url = new URL(baseUrl);
110
+ } catch {
111
+ throw new SekyroConfigError("invalid_base_url");
112
+ }
113
+ if (url.protocol === "https:") return;
114
+ if (url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname)) return;
115
+ throw new SekyroConfigError("invalid_base_url");
116
+ }
117
+ var Transport = class {
118
+ apiKey;
119
+ baseUrl;
120
+ timeoutMs;
121
+ fetchImpl;
122
+ constructor(opts) {
123
+ const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
124
+ assertSafeBaseUrl(baseUrl);
125
+ this.apiKey = opts.apiKey;
126
+ this.baseUrl = baseUrl;
127
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
128
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
129
+ }
130
+ async call(method, path, body, idempotencyKey) {
131
+ const headers = {
132
+ authorization: `Bearer ${this.apiKey}`,
133
+ accept: "application/json",
134
+ "user-agent": USER_AGENT
135
+ };
136
+ if (body !== void 0) headers["content-type"] = "application/json";
137
+ if (idempotencyKey !== void 0) headers["x-idempotency-key"] = idempotencyKey;
138
+ const init = {
139
+ method,
140
+ headers,
141
+ signal: AbortSignal.timeout(this.timeoutMs)
142
+ };
143
+ if (body !== void 0) init.body = JSON.stringify(body);
144
+ let res;
145
+ try {
146
+ res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
147
+ } catch (err) {
148
+ if (isAbortLike(err)) {
149
+ throw new SekyroApiError({
150
+ status: 0,
151
+ code: "timeout",
152
+ message: `Request timed out after ${this.timeoutMs}ms`,
153
+ cause: err
154
+ });
155
+ }
156
+ throw new SekyroApiError({
157
+ status: 0,
158
+ code: "network",
159
+ message: `Network error: ${err instanceof Error ? err.message : String(err)}`,
160
+ cause: err
161
+ });
162
+ }
163
+ const text = await res.text();
164
+ let parsed;
165
+ let parseError;
166
+ try {
167
+ parsed = text.length > 0 ? JSON.parse(text) : void 0;
168
+ } catch (err) {
169
+ parseError = err;
170
+ }
171
+ if (!res.ok) {
172
+ if (isErrorEnvelope(parsed)) {
173
+ const init2 = {
174
+ status: res.status,
175
+ code: parsed.error.code,
176
+ message: parsed.error.message
177
+ };
178
+ if (parsed.error.request_id !== void 0) init2.requestId = parsed.error.request_id;
179
+ throw new SekyroApiError(init2);
180
+ }
181
+ throw new SekyroApiError({
182
+ status: res.status,
183
+ code: "http_error",
184
+ message: `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`
185
+ });
186
+ }
187
+ if (!isSuccessEnvelope(parsed)) {
188
+ throw new SekyroApiError({
189
+ status: res.status,
190
+ code: "malformed_response",
191
+ message: "Response is not a `{ success: true, data }` envelope",
192
+ cause: parseError
193
+ });
194
+ }
195
+ return parsed.data;
196
+ }
197
+ };
198
+ var SessionsApi = class {
199
+ /**
200
+ * Constructed by {@link Sekyro}; the transport is kept out of the public
201
+ * surface on purpose (only its bound `call` crosses this boundary).
202
+ * @internal
203
+ */
204
+ constructor(call) {
205
+ this.call = call;
206
+ }
207
+ call;
208
+ /**
209
+ * `POST /sessions`. Returns the one-time `clientToken` — hand it to the
210
+ * browser, never persist it. Pass an `idempotencyKey` so a retried call
211
+ * cannot create a second session.
212
+ */
213
+ async create(input, opts = {}) {
214
+ const wire = await this.call("POST", "/sessions", input, opts.idempotencyKey);
215
+ return {
216
+ id: wire.id,
217
+ status: wire.status,
218
+ clientToken: wire.client_token,
219
+ hostedUrl: wire.hosted_url,
220
+ steps: wire.steps,
221
+ expiresAt: wire.expiresAt
222
+ };
223
+ }
224
+ /** `GET /sessions/:id` — the whitelisted view (no client token). */
225
+ async get(id) {
226
+ if (typeof id !== "string" || id.length === 0) {
227
+ throw new TypeError("sessions.get: `id` must be a non-empty string");
228
+ }
229
+ return this.call("GET", `/sessions/${encodeURIComponent(id)}`);
230
+ }
231
+ };
232
+ var Sekyro = class {
233
+ sessions;
234
+ constructor(opts) {
235
+ if (typeof opts?.apiKey !== "string" || opts.apiKey.length === 0) {
236
+ throw new SekyroConfigError("missing_api_key");
237
+ }
238
+ const transport = new Transport(opts);
239
+ this.sessions = new SessionsApi(transport.call.bind(transport));
240
+ }
241
+ };
242
+
243
+ // src/webhooks.ts
244
+ var import_node_crypto = require("crypto");
245
+ var SEKYRO_SIGNATURE_HEADER = "x-sekyro-signature";
246
+ var LEGACY_SIGNATURE_HEADER = "x-ibtikar-signature";
247
+ function signatureFromHeaders(headers) {
248
+ return readHeader(headers, SEKYRO_SIGNATURE_HEADER) ?? readHeader(headers, LEGACY_SIGNATURE_HEADER);
249
+ }
250
+ function readHeader(headers, name) {
251
+ if (typeof headers.get === "function") {
252
+ const v2 = headers.get(name);
253
+ return v2 === null || v2 === "" ? void 0 : v2;
254
+ }
255
+ const record = headers;
256
+ const key = Object.keys(record).find((k) => k.toLowerCase() === name);
257
+ if (key === void 0) return void 0;
258
+ const raw = record[key];
259
+ const v = Array.isArray(raw) ? raw[0] : raw;
260
+ return v === void 0 || v === "" ? void 0 : v;
261
+ }
262
+ var HEX64 = /^[0-9a-f]{64}$/i;
263
+ function verifyWebhook(rawBody, header, secret, opts = {}) {
264
+ if (!header || !secret) throw new SekyroWebhookError("invalid_signature");
265
+ const parts = header.split(",").map((part) => {
266
+ const trimmed = part.trim();
267
+ const eq = trimmed.indexOf("=");
268
+ return eq === -1 ? [trimmed, ""] : [trimmed.slice(0, eq).trim(), trimmed.slice(eq + 1).trim()];
269
+ });
270
+ const tRaw = parts.find(([k]) => k === "t")?.[1];
271
+ const t = tRaw !== void 0 && /^\d+$/.test(tRaw) ? Number(tRaw) : Number.NaN;
272
+ const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
273
+ if (!Number.isSafeInteger(t) || sigs.length === 0) throw new SekyroWebhookError("malformed");
274
+ const nowSec = Math.floor((opts.now?.() ?? Date.now()) / 1e3);
275
+ const tolerance = opts.toleranceSec ?? 300;
276
+ if (Math.abs(nowSec - t) > tolerance) throw new SekyroWebhookError("stale");
277
+ const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(`${t}.${rawBody}`).digest();
278
+ const ok = sigs.some((hex) => {
279
+ if (!HEX64.test(hex)) return false;
280
+ const got = Buffer.from(hex, "hex");
281
+ return got.length === expected.length && (0, import_node_crypto.timingSafeEqual)(got, expected);
282
+ });
283
+ if (!ok) throw new SekyroWebhookError("invalid_signature");
284
+ let parsed;
285
+ try {
286
+ parsed = JSON.parse(rawBody);
287
+ } catch {
288
+ throw new SekyroWebhookError("malformed", "Signed body is not valid JSON");
289
+ }
290
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
291
+ throw new SekyroWebhookError("malformed", "Signed body is not a JSON object");
292
+ }
293
+ return parsed;
294
+ }
295
+ // Annotate the CommonJS export names for ESM import in node:
296
+ 0 && (module.exports = {
297
+ DEFAULT_BASE_URL,
298
+ DEFAULT_TIMEOUT_MS,
299
+ LEGACY_SIGNATURE_HEADER,
300
+ SEKYRO_SIGNATURE_HEADER,
301
+ Sekyro,
302
+ SekyroApiError,
303
+ SekyroConfigError,
304
+ SekyroWebhookError,
305
+ signatureFromHeaders,
306
+ verifyWebhook
307
+ });
308
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/version.ts","../src/client.ts","../src/webhooks.ts"],"sourcesContent":["/**\n * `@sekyro/node` — Sekyro server SDK: sessions client + webhook verification.\n * Zero runtime dependencies; Node ≥ 20.\n *\n * @packageDocumentation\n */\n\nexport {\n DEFAULT_BASE_URL,\n DEFAULT_TIMEOUT_MS,\n Sekyro,\n // Type-only on purpose: `sekyro.sessions` is the only way to get one.\n type SessionsApi,\n type FetchLike,\n type SekyroOptions,\n} from \"./client\";\nexport {\n SekyroApiError,\n SekyroConfigError,\n SekyroWebhookError,\n type SekyroApiErrorInit,\n type SekyroClientErrorCode,\n type SekyroConfigErrorCode,\n type SekyroWebhookErrorCode,\n} from \"./errors\";\nexport type {\n CheckState,\n CheckType,\n CreatedSession,\n CreateSessionInput,\n Locale,\n RequestOptions,\n SekyroEnvironment,\n Session,\n SessionEventData,\n SessionLifecycleEventData,\n SessionOutcome,\n SessionReviewDecidedEventData,\n SessionStatus,\n SessionStep,\n SessionStepCompletedEventData,\n WebhookEnvelope,\n WebhookEnvelopeOf,\n WebhookEventDataMap,\n WebhookEventType,\n} from \"./types\";\nexport {\n LEGACY_SIGNATURE_HEADER,\n SEKYRO_SIGNATURE_HEADER,\n signatureFromHeaders,\n verifyWebhook,\n type VerifyWebhookOptions,\n} from \"./webhooks\";\n","/** Why {@link verifyWebhook} rejected a delivery. @public */\nexport type SekyroWebhookErrorCode = \"invalid_signature\" | \"stale\" | \"malformed\";\n\nconst WEBHOOK_MESSAGES: Record<SekyroWebhookErrorCode, string> = {\n invalid_signature: \"No v1 signature in the header matches this body and secret\",\n stale: \"Delivery timestamp is outside the tolerance window\",\n malformed: \"Signature header or body is not in the expected format\",\n};\n\n/**\n * Thrown by {@link verifyWebhook}. Respond with a 4xx and do not process the\n * body; Sekyro retries deliveries that do not receive a 2xx.\n * @public\n */\nexport class SekyroWebhookError extends Error {\n override readonly name = \"SekyroWebhookError\";\n readonly code: SekyroWebhookErrorCode;\n\n constructor(code: SekyroWebhookErrorCode, message: string = WEBHOOK_MESSAGES[code]) {\n super(message);\n this.code = code;\n }\n}\n\n/** Why the {@link Sekyro} constructor rejected its options. @public */\nexport type SekyroConfigErrorCode = \"missing_api_key\" | \"invalid_base_url\";\n\nconst CONFIG_MESSAGES: Record<SekyroConfigErrorCode, string> = {\n missing_api_key: \"Sekyro: `apiKey` is required\",\n invalid_base_url:\n \"Sekyro: `baseUrl` must be an https: URL (http: is allowed only for localhost, 127.0.0.1 or [::1])\",\n};\n\n/**\n * Thrown synchronously by `new Sekyro(...)` for an unusable configuration —\n * before any request is made. Extends `TypeError` so existing\n * `instanceof TypeError` guards keep working.\n * @public\n */\nexport class SekyroConfigError extends TypeError {\n override readonly name = \"SekyroConfigError\";\n readonly code: SekyroConfigErrorCode;\n\n constructor(code: SekyroConfigErrorCode, message: string = CONFIG_MESSAGES[code]) {\n super(message);\n this.code = code;\n }\n}\n\n/** Constructor input of {@link SekyroApiError}. @public */\nexport interface SekyroApiErrorInit {\n /** HTTP status; `0` when no response was received (network / timeout). */\n status: number;\n /** API error code, or `network` | `timeout` | `http_error` | `malformed_response`. */\n code: string;\n message: string;\n /** The API's `request_id`, when the response carried one. */\n requestId?: string;\n /** Underlying error (network failures, abort reasons, JSON parse errors). */\n cause?: unknown;\n}\n\n/** Error codes the SDK itself produces (as opposed to codes from the API). @public */\nexport type SekyroClientErrorCode = \"network\" | \"timeout\" | \"http_error\" | \"malformed_response\";\n\n/**\n * Any failed call through {@link Sekyro}: a non-2xx partner envelope\n * `{ status: 'error', error: { code, message, request_id } }`, a non-JSON\n * response, a network failure or a timeout.\n *\n * `retryable` is `true` for `network`, `timeout`, HTTP 429, HTTP 5xx and the\n * `idempotency_in_progress` code — retry with the SAME idempotency key.\n * @public\n */\nexport class SekyroApiError extends Error {\n override readonly name = \"SekyroApiError\";\n readonly status: number;\n readonly code: string;\n readonly requestId: string | undefined;\n readonly retryable: boolean;\n\n constructor(init: SekyroApiErrorInit) {\n super(init.message, init.cause === undefined ? undefined : { cause: init.cause });\n this.status = init.status;\n this.code = init.code;\n this.requestId = init.requestId;\n this.retryable = isRetryable(init.status, init.code);\n }\n}\n\n/** The retry taxonomy, in one place. */\nexport function isRetryable(status: number, code: string): boolean {\n if (code === \"network\" || code === \"timeout\") return true;\n if (code === \"idempotency_in_progress\") return true;\n if (status === 429) return true;\n return status >= 500 && status <= 599;\n}\n","/**\n * Build-time constant injected from `package.json#version` by tsup (`define`,\n * see tsup.config.ts) and by vitest (vitest.config.ts), so a Changesets version\n * bump flows into the `User-Agent` with no hand edit. `test/version.test.ts`\n * asserts both the injection and that the built dist embeds the version.\n * The fallback only appears when the source runs without either (bare `tsx`).\n */\ndeclare const __SEKYRO_NODE_VERSION__: string | undefined;\n\nexport const SDK_VERSION: string =\n typeof __SEKYRO_NODE_VERSION__ === \"string\" ? __SEKYRO_NODE_VERSION__ : \"0.0.0-dev\";\n","import { SekyroApiError, SekyroConfigError } from \"./errors\";\nimport type { CreatedSession, CreateSessionInput, RequestOptions, Session } from \"./types\";\nimport { SDK_VERSION } from \"./version\";\n\n/** Sent as the `User-Agent` so support can tell SDK versions apart (`SDK_VERSION` is injected from package.json at build time). */\nconst USER_AGENT = `@sekyro/node/${SDK_VERSION}`;\n\n/** Default partner API origin + version prefix. @public */\nexport const DEFAULT_BASE_URL = \"https://api.sekyro.ai/api/v2\";\n\n/** Default per-request timeout. @public */\nexport const DEFAULT_TIMEOUT_MS = 30_000;\n\n/** `fetch`-compatible function; the global `fetch` by default. @public */\nexport type FetchLike = typeof globalThis.fetch;\n\n/** Constructor options of {@link Sekyro}. @public */\nexport interface SekyroOptions {\n /** Partner API key (server-side only). Sent as `Authorization: Bearer`. */\n apiKey: string;\n /** API base URL incl. the `/api/v2` prefix. Default {@link DEFAULT_BASE_URL}. */\n baseUrl?: string;\n /** Per-request timeout in ms. Default {@link DEFAULT_TIMEOUT_MS}. */\n timeoutMs?: number;\n /** Custom `fetch` (tests, proxies, custom agents). Default `globalThis.fetch`. */\n fetch?: FetchLike;\n}\n\ntype HttpMethod = \"GET\" | \"POST\";\n\n/** The successful partner envelope. */\ninterface SuccessEnvelope<T> {\n success: true;\n data: T;\n}\n\n/** The partner error envelope shaped by the API for the `partner` audience. */\ninterface ErrorEnvelope {\n status: \"error\";\n error: { code: string; message: string; request_id?: string };\n}\n\n/** Wire shape of `POST /sessions` (snake_case fields are mapped by the SDK). */\ninterface CreateSessionWire {\n id: string;\n status: CreatedSession[\"status\"];\n steps: CreatedSession[\"steps\"];\n hosted_url: string;\n client_token: string;\n expiresAt: string;\n}\n\nfunction isSuccessEnvelope(value: unknown): value is SuccessEnvelope<unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { success?: unknown }).success === true &&\n \"data\" in value\n );\n}\n\nfunction isErrorEnvelope(value: unknown): value is ErrorEnvelope {\n if (typeof value !== \"object\" || value === null) return false;\n const v = value as { status?: unknown; error?: unknown };\n if (v.status !== \"error\" || typeof v.error !== \"object\" || v.error === null) return false;\n const e = v.error as { code?: unknown; message?: unknown };\n return typeof e.code === \"string\" && typeof e.message === \"string\";\n}\n\nfunction isAbortLike(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) return false;\n const name = (err as { name?: unknown }).name;\n return name === \"TimeoutError\" || name === \"AbortError\";\n}\n\n/** Hosts that may be reached over plain `http:` — a developer's own machine only. */\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/**\n * The API key travels as a Bearer header on every call, so the base URL MUST be\n * `https:` — except a loopback host (local API during development), where\n * `http:` is accepted. Anything else (an `http:` production host, a non-URL,\n * another scheme) is a configuration error thrown at construction.\n */\nfunction assertSafeBaseUrl(baseUrl: string): void {\n let url: URL;\n try {\n url = new URL(baseUrl);\n } catch {\n throw new SekyroConfigError(\"invalid_base_url\");\n }\n if (url.protocol === \"https:\") return;\n if (url.protocol === \"http:\" && LOOPBACK_HOSTS.has(url.hostname)) return;\n throw new SekyroConfigError(\"invalid_base_url\");\n}\n\n/**\n * Low-level transport shared by the resources: one `fetch` per call with a\n * timeout, Bearer auth, optional idempotency key, the `{ success, data }`\n * unwrap and the error mapping to {@link SekyroApiError}.\n */\nclass Transport {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private readonly fetchImpl: FetchLike;\n\n constructor(opts: SekyroOptions) {\n const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n assertSafeBaseUrl(baseUrl);\n this.apiKey = opts.apiKey;\n this.baseUrl = baseUrl;\n this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.fetchImpl = opts.fetch ?? globalThis.fetch;\n }\n\n async call<T>(method: HttpMethod, path: string, body?: unknown, idempotencyKey?: string): Promise<T> {\n const headers: Record<string, string> = {\n authorization: `Bearer ${this.apiKey}`,\n accept: \"application/json\",\n \"user-agent\": USER_AGENT,\n };\n if (body !== undefined) headers[\"content-type\"] = \"application/json\";\n if (idempotencyKey !== undefined) headers[\"x-idempotency-key\"] = idempotencyKey;\n\n const init: RequestInit = {\n method,\n headers,\n signal: AbortSignal.timeout(this.timeoutMs),\n };\n if (body !== undefined) init.body = JSON.stringify(body);\n\n let res: Response;\n try {\n res = await this.fetchImpl(`${this.baseUrl}${path}`, init);\n } catch (err) {\n if (isAbortLike(err)) {\n throw new SekyroApiError({\n status: 0,\n code: \"timeout\",\n message: `Request timed out after ${this.timeoutMs}ms`,\n cause: err,\n });\n }\n throw new SekyroApiError({\n status: 0,\n code: \"network\",\n message: `Network error: ${err instanceof Error ? err.message : String(err)}`,\n cause: err,\n });\n }\n\n const text = await res.text();\n let parsed: unknown;\n let parseError: unknown;\n try {\n parsed = text.length > 0 ? JSON.parse(text) : undefined;\n } catch (err) {\n parseError = err;\n }\n\n if (!res.ok) {\n if (isErrorEnvelope(parsed)) {\n const init: ConstructorParameters<typeof SekyroApiError>[0] = {\n status: res.status,\n code: parsed.error.code,\n message: parsed.error.message,\n };\n if (parsed.error.request_id !== undefined) init.requestId = parsed.error.request_id;\n throw new SekyroApiError(init);\n }\n throw new SekyroApiError({\n status: res.status,\n code: \"http_error\",\n message: `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : \"\"}`,\n });\n }\n\n if (!isSuccessEnvelope(parsed)) {\n throw new SekyroApiError({\n status: res.status,\n code: \"malformed_response\",\n message: \"Response is not a `{ success: true, data }` envelope\",\n cause: parseError,\n });\n }\n return parsed.data as T;\n }\n}\n\n/**\n * `sekyro.sessions` — create and read verification sessions.\n *\n * Exported as a TYPE only (`import type { SessionsApi }`): the class value is\n * not part of the package surface, so it can only be obtained through\n * {@link Sekyro.sessions} — never constructed by hand with an unchecked\n * transport.\n * @public\n */\nexport class SessionsApi {\n /**\n * Constructed by {@link Sekyro}; the transport is kept out of the public\n * surface on purpose (only its bound `call` crosses this boundary).\n * @internal\n */\n constructor(\n private readonly call: <T>(\n method: \"GET\" | \"POST\",\n path: string,\n body?: unknown,\n idempotencyKey?: string,\n ) => Promise<T>,\n ) {}\n\n /**\n * `POST /sessions`. Returns the one-time `clientToken` — hand it to the\n * browser, never persist it. Pass an `idempotencyKey` so a retried call\n * cannot create a second session.\n */\n async create(input: CreateSessionInput, opts: RequestOptions = {}): Promise<CreatedSession> {\n const wire = await this.call<CreateSessionWire>(\"POST\", \"/sessions\", input, opts.idempotencyKey);\n return {\n id: wire.id,\n status: wire.status,\n clientToken: wire.client_token,\n hostedUrl: wire.hosted_url,\n steps: wire.steps,\n expiresAt: wire.expiresAt,\n };\n }\n\n /** `GET /sessions/:id` — the whitelisted view (no client token). */\n async get(id: string): Promise<Session> {\n if (typeof id !== \"string\" || id.length === 0) {\n throw new TypeError(\"sessions.get: `id` must be a non-empty string\");\n }\n return this.call<Session>(\"GET\", `/sessions/${encodeURIComponent(id)}`);\n }\n}\n\n/**\n * Sekyro partner API client. Server-side only: it carries your API key.\n *\n * Throws {@link SekyroConfigError} synchronously when `apiKey` is missing or\n * `baseUrl` is not `https:` (plain `http:` is accepted for `localhost`,\n * `127.0.0.1` and `[::1]` only).\n *\n * @example\n * ```ts\n * const sekyro = new Sekyro({ apiKey: process.env.SEKYRO_API_KEY! });\n * const { id, clientToken } = await sekyro.sessions.create({ flow: \"onboarding\" });\n * ```\n * @public\n */\nexport class Sekyro {\n readonly sessions: SessionsApi;\n\n constructor(opts: SekyroOptions) {\n if (typeof opts?.apiKey !== \"string\" || opts.apiKey.length === 0) {\n throw new SekyroConfigError(\"missing_api_key\");\n }\n const transport = new Transport(opts);\n this.sessions = new SessionsApi(transport.call.bind(transport));\n }\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { SekyroWebhookError } from \"./errors\";\nimport type { WebhookEnvelope } from \"./types\";\n\n/**\n * The HTTP header carrying the webhook signature:\n * `t=<unix seconds>,v1=<hex>[,v1=<hex>]`.\n * @public\n */\nexport const SEKYRO_SIGNATURE_HEADER = \"x-sekyro-signature\";\n\n/**\n * The pre-rebrand name of the signature header. Sekyro sends BOTH headers with\n * the same value during the transition; prefer {@link SEKYRO_SIGNATURE_HEADER}\n * and use {@link signatureFromHeaders} to accept either.\n * @public\n */\nexport const LEGACY_SIGNATURE_HEADER = \"x-ibtikar-signature\";\n\n/**\n * Pick the signature header out of an incoming request's headers: the\n * `x-sekyro-signature` value when present, else the legacy\n * `x-ibtikar-signature`, else `undefined`. Accepts a Fetch `Headers` object or\n * a Node-style plain record (lower- or mixed-case keys; a multi-valued header\n * yields its first value).\n *\n * @example\n * ```ts\n * verifyWebhook(rawBody, signatureFromHeaders(req.headers), secret);\n * ```\n * @public\n */\nexport function signatureFromHeaders(\n headers: Headers | Record<string, string | string[] | undefined>,\n): string | undefined {\n return readHeader(headers, SEKYRO_SIGNATURE_HEADER) ?? readHeader(headers, LEGACY_SIGNATURE_HEADER);\n}\n\nfunction readHeader(\n headers: Headers | Record<string, string | string[] | undefined>,\n name: string,\n): string | undefined {\n if (typeof (headers as Headers).get === \"function\") {\n const v = (headers as Headers).get(name);\n return v === null || v === \"\" ? undefined : v;\n }\n const record = headers as Record<string, string | string[] | undefined>;\n const key = Object.keys(record).find((k) => k.toLowerCase() === name);\n if (key === undefined) return undefined;\n const raw = record[key];\n const v = Array.isArray(raw) ? raw[0] : raw;\n return v === undefined || v === \"\" ? undefined : v;\n}\n\n/** Options of {@link verifyWebhook}. @public */\nexport interface VerifyWebhookOptions {\n /** Max |now − t| in seconds before a delivery is rejected as `stale`. Default 300. */\n toleranceSec?: number;\n /** Clock returning epoch milliseconds. Default `Date.now`. */\n now?: () => number;\n}\n\nconst HEX64 = /^[0-9a-f]{64}$/i;\n\n/**\n * Verify a Sekyro webhook delivery and return its parsed envelope, discriminated\n * on `event` (narrow with a `switch` to type `data`).\n *\n * - `rawBody` MUST be the request body exactly as received (before any JSON\n * parsing / re-serialisation); the signature covers those bytes.\n * - Accepts the delivery when ANY `v1` in the header matches, which is what\n * makes secret rotation seamless (Sekyro signs with current + previous).\n * - Uses `node:crypto` only; the digest compare is constant-time.\n *\n * Checks run in this order: header/secret present → header well-formed →\n * timestamp within tolerance → signature → body is a JSON object.\n *\n * @param rawBody - The raw UTF-8 request body.\n * @param header - The `x-sekyro-signature` header value (or the legacy\n * `x-ibtikar-signature` — same format; see {@link signatureFromHeaders}).\n * @param secret - The endpoint secret you hold (current or previous).\n * @throws {@link SekyroWebhookError} with `code` `invalid_signature` | `stale` | `malformed`.\n * @public\n */\nexport function verifyWebhook(\n rawBody: string,\n header: string | null | undefined,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): WebhookEnvelope {\n if (!header || !secret) throw new SekyroWebhookError(\"invalid_signature\");\n\n const parts = header.split(\",\").map((part): [string, string] => {\n const trimmed = part.trim();\n const eq = trimmed.indexOf(\"=\");\n return eq === -1 ? [trimmed, \"\"] : [trimmed.slice(0, eq).trim(), trimmed.slice(eq + 1).trim()];\n });\n const tRaw = parts.find(([k]) => k === \"t\")?.[1];\n const t = tRaw !== undefined && /^\\d+$/.test(tRaw) ? Number(tRaw) : Number.NaN;\n const sigs = parts.filter(([k]) => k === \"v1\").map(([, v]) => v);\n if (!Number.isSafeInteger(t) || sigs.length === 0) throw new SekyroWebhookError(\"malformed\");\n\n const nowSec = Math.floor((opts.now?.() ?? Date.now()) / 1000);\n const tolerance = opts.toleranceSec ?? 300;\n if (Math.abs(nowSec - t) > tolerance) throw new SekyroWebhookError(\"stale\");\n\n const expected = createHmac(\"sha256\", secret).update(`${t}.${rawBody}`).digest();\n const ok = sigs.some((hex) => {\n if (!HEX64.test(hex)) return false;\n const got = Buffer.from(hex, \"hex\");\n return got.length === expected.length && timingSafeEqual(got, expected);\n });\n if (!ok) throw new SekyroWebhookError(\"invalid_signature\");\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawBody);\n } catch {\n throw new SekyroWebhookError(\"malformed\", \"Signed body is not valid JSON\");\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new SekyroWebhookError(\"malformed\", \"Signed body is not a JSON object\");\n }\n return parsed as WebhookEnvelope;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGA,IAAM,mBAA2D;AAAA,EAC/D,mBAAmB;AAAA,EACnB,OAAO;AAAA,EACP,WAAW;AACb;AAOO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC1B,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,MAA8B,UAAkB,iBAAiB,IAAI,GAAG;AAClF,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAKA,IAAM,kBAAyD;AAAA,EAC7D,iBAAiB;AAAA,EACjB,kBACE;AACJ;AAQO,IAAM,oBAAN,cAAgC,UAAU;AAAA,EAC7B,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,MAA6B,UAAkB,gBAAgB,IAAI,GAAG;AAChF,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AA2BO,IAAM,iBAAN,cAA6B,MAAM;AAAA,EACtB,OAAO;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAA0B;AACpC,UAAM,KAAK,SAAS,KAAK,UAAU,SAAY,SAAY,EAAE,OAAO,KAAK,MAAM,CAAC;AAChF,SAAK,SAAS,KAAK;AACnB,SAAK,OAAO,KAAK;AACjB,SAAK,YAAY,KAAK;AACtB,SAAK,YAAY,YAAY,KAAK,QAAQ,KAAK,IAAI;AAAA,EACrD;AACF;AAGO,SAAS,YAAY,QAAgB,MAAuB;AACjE,MAAI,SAAS,aAAa,SAAS,UAAW,QAAO;AACrD,MAAI,SAAS,0BAA2B,QAAO;AAC/C,MAAI,WAAW,IAAK,QAAO;AAC3B,SAAO,UAAU,OAAO,UAAU;AACpC;;;ACvFO,IAAM,cACX,OAA8C,eAA0B;;;ACL1E,IAAM,aAAa,gBAAgB,WAAW;AAGvC,IAAM,mBAAmB;AAGzB,IAAM,qBAAqB;AAyClC,SAAS,kBAAkB,OAAmD;AAC5E,SACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAgC,YAAY,QAC7C,UAAU;AAEd;AAEA,SAAS,gBAAgB,OAAwC;AAC/D,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,IAAI;AACV,MAAI,EAAE,WAAW,WAAW,OAAO,EAAE,UAAU,YAAY,EAAE,UAAU,KAAM,QAAO;AACpF,QAAM,IAAI,EAAE;AACZ,SAAO,OAAO,EAAE,SAAS,YAAY,OAAO,EAAE,YAAY;AAC5D;AAEA,SAAS,YAAY,KAAuB;AAC1C,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO;AACpD,QAAM,OAAQ,IAA2B;AACzC,SAAO,SAAS,kBAAkB,SAAS;AAC7C;AAGA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,CAAC;AAQlE,SAAS,kBAAkB,SAAuB;AAChD,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,OAAO;AAAA,EACvB,QAAQ;AACN,UAAM,IAAI,kBAAkB,kBAAkB;AAAA,EAChD;AACA,MAAI,IAAI,aAAa,SAAU;AAC/B,MAAI,IAAI,aAAa,WAAW,eAAe,IAAI,IAAI,QAAQ,EAAG;AAClE,QAAM,IAAI,kBAAkB,kBAAkB;AAChD;AAOA,IAAM,YAAN,MAAgB;AAAA,EACG;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAqB;AAC/B,UAAM,WAAW,KAAK,WAAW,kBAAkB,QAAQ,QAAQ,EAAE;AACrE,sBAAkB,OAAO;AACzB,SAAK,SAAS,KAAK;AACnB,SAAK,UAAU;AACf,SAAK,YAAY,KAAK,aAAa;AACnC,SAAK,YAAY,KAAK,SAAS,WAAW;AAAA,EAC5C;AAAA,EAEA,MAAM,KAAQ,QAAoB,MAAc,MAAgB,gBAAqC;AACnG,UAAM,UAAkC;AAAA,MACtC,eAAe,UAAU,KAAK,MAAM;AAAA,MACpC,QAAQ;AAAA,MACR,cAAc;AAAA,IAChB;AACA,QAAI,SAAS,OAAW,SAAQ,cAAc,IAAI;AAClD,QAAI,mBAAmB,OAAW,SAAQ,mBAAmB,IAAI;AAEjE,UAAM,OAAoB;AAAA,MACxB;AAAA,MACA;AAAA,MACA,QAAQ,YAAY,QAAQ,KAAK,SAAS;AAAA,IAC5C;AACA,QAAI,SAAS,OAAW,MAAK,OAAO,KAAK,UAAU,IAAI;AAEvD,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,KAAK,UAAU,GAAG,KAAK,OAAO,GAAG,IAAI,IAAI,IAAI;AAAA,IAC3D,SAAS,KAAK;AACZ,UAAI,YAAY,GAAG,GAAG;AACpB,cAAM,IAAI,eAAe;AAAA,UACvB,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,SAAS,2BAA2B,KAAK,SAAS;AAAA,UAClD,OAAO;AAAA,QACT,CAAC;AAAA,MACH;AACA,YAAM,IAAI,eAAe;AAAA,QACvB,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,SAAS,kBAAkB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,QAC3E,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAEA,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,SAAS,IAAI,KAAK,MAAM,IAAI,IAAI;AAAA,IAChD,SAAS,KAAK;AACZ,mBAAa;AAAA,IACf;AAEA,QAAI,CAAC,IAAI,IAAI;AACX,UAAI,gBAAgB,MAAM,GAAG;AAC3B,cAAMA,QAAwD;AAAA,UAC5D,QAAQ,IAAI;AAAA,UACZ,MAAM,OAAO,MAAM;AAAA,UACnB,SAAS,OAAO,MAAM;AAAA,QACxB;AACA,YAAI,OAAO,MAAM,eAAe,OAAW,CAAAA,MAAK,YAAY,OAAO,MAAM;AACzE,cAAM,IAAI,eAAeA,KAAI;AAAA,MAC/B;AACA,YAAM,IAAI,eAAe;AAAA,QACvB,QAAQ,IAAI;AAAA,QACZ,MAAM;AAAA,QACN,SAAS,QAAQ,IAAI,MAAM,GAAG,IAAI,aAAa,IAAI,IAAI,UAAU,KAAK,EAAE;AAAA,MAC1E,CAAC;AAAA,IACH;AAEA,QAAI,CAAC,kBAAkB,MAAM,GAAG;AAC9B,YAAM,IAAI,eAAe;AAAA,QACvB,QAAQ,IAAI;AAAA,QACZ,MAAM;AAAA,QACN,SAAS;AAAA,QACT,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AACA,WAAO,OAAO;AAAA,EAChB;AACF;AAWO,IAAM,cAAN,MAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMvB,YACmB,MAMjB;AANiB;AAAA,EAMhB;AAAA,EANgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAanB,MAAM,OAAO,OAA2B,OAAuB,CAAC,GAA4B;AAC1F,UAAM,OAAO,MAAM,KAAK,KAAwB,QAAQ,aAAa,OAAO,KAAK,cAAc;AAC/F,WAAO;AAAA,MACL,IAAI,KAAK;AAAA,MACT,QAAQ,KAAK;AAAA,MACb,aAAa,KAAK;AAAA,MAClB,WAAW,KAAK;AAAA,MAChB,OAAO,KAAK;AAAA,MACZ,WAAW,KAAK;AAAA,IAClB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,IAAI,IAA8B;AACtC,QAAI,OAAO,OAAO,YAAY,GAAG,WAAW,GAAG;AAC7C,YAAM,IAAI,UAAU,+CAA+C;AAAA,IACrE;AACA,WAAO,KAAK,KAAc,OAAO,aAAa,mBAAmB,EAAE,CAAC,EAAE;AAAA,EACxE;AACF;AAgBO,IAAM,SAAN,MAAa;AAAA,EACT;AAAA,EAET,YAAY,MAAqB;AAC/B,QAAI,OAAO,MAAM,WAAW,YAAY,KAAK,OAAO,WAAW,GAAG;AAChE,YAAM,IAAI,kBAAkB,iBAAiB;AAAA,IAC/C;AACA,UAAM,YAAY,IAAI,UAAU,IAAI;AACpC,SAAK,WAAW,IAAI,YAAY,UAAU,KAAK,KAAK,SAAS,CAAC;AAAA,EAChE;AACF;;;ACxQA,yBAA4C;AASrC,IAAM,0BAA0B;AAQhC,IAAM,0BAA0B;AAehC,SAAS,qBACd,SACoB;AACpB,SAAO,WAAW,SAAS,uBAAuB,KAAK,WAAW,SAAS,uBAAuB;AACpG;AAEA,SAAS,WACP,SACA,MACoB;AACpB,MAAI,OAAQ,QAAoB,QAAQ,YAAY;AAClD,UAAMC,KAAK,QAAoB,IAAI,IAAI;AACvC,WAAOA,OAAM,QAAQA,OAAM,KAAK,SAAYA;AAAA,EAC9C;AACA,QAAM,SAAS;AACf,QAAM,MAAM,OAAO,KAAK,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,MAAM,IAAI;AACpE,MAAI,QAAQ,OAAW,QAAO;AAC9B,QAAM,MAAM,OAAO,GAAG;AACtB,QAAM,IAAI,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,IAAI;AACxC,SAAO,MAAM,UAAa,MAAM,KAAK,SAAY;AACnD;AAUA,IAAM,QAAQ;AAsBP,SAAS,cACd,SACA,QACA,QACA,OAA6B,CAAC,GACb;AACjB,MAAI,CAAC,UAAU,CAAC,OAAQ,OAAM,IAAI,mBAAmB,mBAAmB;AAExE,QAAM,QAAQ,OAAO,MAAM,GAAG,EAAE,IAAI,CAAC,SAA2B;AAC9D,UAAM,UAAU,KAAK,KAAK;AAC1B,UAAM,KAAK,QAAQ,QAAQ,GAAG;AAC9B,WAAO,OAAO,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK,GAAG,QAAQ,MAAM,KAAK,CAAC,EAAE,KAAK,CAAC;AAAA,EAC/F,CAAC;AACD,QAAM,OAAO,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC;AAC/C,QAAM,IAAI,SAAS,UAAa,QAAQ,KAAK,IAAI,IAAI,OAAO,IAAI,IAAI,OAAO;AAC3E,QAAM,OAAO,MAAM,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;AAC/D,MAAI,CAAC,OAAO,cAAc,CAAC,KAAK,KAAK,WAAW,EAAG,OAAM,IAAI,mBAAmB,WAAW;AAE3F,QAAM,SAAS,KAAK,OAAO,KAAK,MAAM,KAAK,KAAK,IAAI,KAAK,GAAI;AAC7D,QAAM,YAAY,KAAK,gBAAgB;AACvC,MAAI,KAAK,IAAI,SAAS,CAAC,IAAI,UAAW,OAAM,IAAI,mBAAmB,OAAO;AAE1E,QAAM,eAAW,+BAAW,UAAU,MAAM,EAAE,OAAO,GAAG,CAAC,IAAI,OAAO,EAAE,EAAE,OAAO;AAC/E,QAAM,KAAK,KAAK,KAAK,CAAC,QAAQ;AAC5B,QAAI,CAAC,MAAM,KAAK,GAAG,EAAG,QAAO;AAC7B,UAAM,MAAM,OAAO,KAAK,KAAK,KAAK;AAClC,WAAO,IAAI,WAAW,SAAS,cAAU,oCAAgB,KAAK,QAAQ;AAAA,EACxE,CAAC;AACD,MAAI,CAAC,GAAI,OAAM,IAAI,mBAAmB,mBAAmB;AAEzD,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,OAAO;AAAA,EAC7B,QAAQ;AACN,UAAM,IAAI,mBAAmB,aAAa,+BAA+B;AAAA,EAC3E;AACA,MAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAC1E,UAAM,IAAI,mBAAmB,aAAa,kCAAkC;AAAA,EAC9E;AACA,SAAO;AACT;","names":["init","v"]}