@sekyro/node 0.1.0-rc.2 → 0.1.0-rc.3

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @sekyro/node
2
2
 
3
+ ## 0.1.0-rc.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 4e7a4a3: Brand: the product is now **Sekyro AI** — package descriptions, READMEs, LICENSE text and the `@sekyro/node` configuration error messages say "Sekyro AI". No API change: package names, the `Sekyro` class, exports and header names are unchanged.
8
+ - c3699fd: Type the end user's `network` (approximate location, operator, VPN/Tor/hosting flags, start IP) and `device` (platform, OS, browser, per-org device keys, signals) on `sessions.get()`, and the same context (without the IP) on `session.completed` / `session.failed` webhook data.
9
+ - 1b6bef2: Profile v2 policy reasons, round 2. New eligibility reason `age_unverifiable` (the app sets a minimum age but the birth date could not be read — `doc_ocr`: retryable with a clearer photo; `population_registry`: final). `failureReason` is widened to every policy reason a session can end on: the session-level reasons plus `nationality_not_allowed`, `underage`, `document_type_not_accepted`, `document_nni_mismatch` and `age_unverifiable`. `@sekyro/web`: `PolicyFailureReason` widened, `age_unverifiable` in `DocReason` / `PopulationReason` / `RETRYABLE_REASONS`. `@sekyro/react`: ar/fr/en copy for `age_unverifiable`. `@sekyro/node`: `EligibilityReason` gains `age_unverifiable`; new `PolicyFailureReason` type used by `Session.failureReason` and the `session.failed` webhook data.
10
+ - 1b6bef2: Profile v2 policy reasons. `@sekyro/web`: the eligibility check reasons `document_type_not_accepted` (retryable), `nationality_not_allowed` and `underage`; `SessionFailureReason` (`location_not_allowed`, `anonymizer_blocked`); `HostedSessionView.failureReason` and `acceptedDocumentTypes`. `@sekyro/react`: ar/fr/en copy for the five reasons. `@sekyro/node`: `SessionFailureReason`, `EligibilityReason`, and `failureReason` on `Session` and the `session.failed` webhook data.
11
+ - 13d512f: Profile v2 step 3: identity-method choice, document-NNI registry submit, matching reasons, `subject` input. `@sekyro/web`: `IdentityMethod`, `MethodChoiceView` and `HostedSessionView.methodChoice`; `HostedStepView.attemptsExhausted` / `nniFromDocument` (and `SessionStepView.attemptsExhausted`); `HostedClient.selectMethod()` (`POST /hosted/method`); error codes `method_required` / `method_locked` / `document_nni_mismatch` (409 when a typed NNI differs from the document's); matching reasons `name_mismatch`, `birth_date_mismatch`, `nationality_mismatch` (doc_ocr + population_registry) and `document_number_mismatch` (doc_ocr) as `MatchReason`. The headless runner stops on a pending choice (`unavailable` / `method_required`, `on('method')`), adds `verification.methodChoice()` / `selectMethod()`, and submits the registry with an empty input when the document gave the NNI. `@sekyro/react`: ar/fr/en copy for the four reasons and the method screen; `<LivenessCapture>` handles the method step while the session waits on the choice: it offers only the methods it can run (the national number — it has no document capture) and selects it without a screen when it is the only one left. `@sekyro/node`: `CreateSessionInput.subject` (`SessionSubjectInput`), `CreateSessionInput.identityMethod` and `sessions.selectMethod(id, method)` (`POST /sessions/:id/method`, `IdentityMethod`) so partners can choose the identity method of an optional-document session, `subjectId` / `externalRef` / `subjectConflict` on `Session` and session webhooks, `SessionStep.attemptsExhausted`, `MatchReason`, and the `subject.updated` webhook data (`{ subjectId, sessionId, status, outcome }`).
12
+ - acbf409: Profile v2 step 4: decision mode, desktop handoff, device stops, per-session profile overrides. `@sekyro/web`: decision-mode reasons `liveness_inconclusive`, `mobile_required`, `match_inconclusive`, `document_authenticity` (`DecisionReason`; the inconclusive three are retryable) and the registered `uncalibrated_capture_channel`; session failure reasons `capture_tampered`, `device_velocity_exceeded`, `device_cooldown`; `HostedStepView.requiresMobile` and `HostedSessionView.retryAfter`; the error code `inference_unavailable` (503, request-level, with `retryAfterSeconds` from `Retry-After` or the body). The headless runner retries `inference_unavailable` up to 3 times after `retryAfterSeconds` (default 10 s) under a fresh idempotency key and a fresh attestation, emitting `on('busy')` for each retry, then surfaces `unavailable / inference_unavailable`; it never captures a `requiresMobile` liveness step on desktop (`unavailable / mobile_required`, nothing uploaded) and adds `verification.mobileRequired()`; a `device_cooldown` result carries `retryAfter`. `@sekyro/react`: ar/fr/en coaching for the inconclusive reasons, "Continue on your phone" instead of the camera when liveness needs a phone, a "busy, trying again" notice, the device stop copy (the cooldown says when to try again — `retryAfterTime()`), no retry button for stops or `mobile_required`; bundle budget 23 → 24 KiB. `@sekyro/node`: `CreateSessionInput.profile` (`EkycProfileOverride`, a deep-partial of `EkycProfileSettings`; unknown keys are rejected server-side, widening answers `403 override_widens_profile` unless the app allows it), `Session.overriddenFields` / `riskSignals` / `retryAfter` (`overriddenFields` and `riskSignals` also on the session lifecycle webhooks), `DecisionReason`, the three device stops in `SessionFailureReason`, and the `subject.suspicious_activity` webhook data (`{ subjectId, sessionId, count }`).
13
+ - 58e220c: Published bundles are now minified and no longer include source maps. The public API and types are unchanged.
14
+
3
15
  ## 0.1.0-rc.2
4
16
 
5
17
  ### Minor Changes
package/LICENSE CHANGED
@@ -1,10 +1,10 @@
1
- Sekyro SDK — Proprietary License
1
+ Sekyro AI SDK — Proprietary License
2
2
 
3
3
  Copyright (c) 2026 Ibtikar Technologies. All rights reserved.
4
4
 
5
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
6
+ permitted only under a Sekyro AI customer agreement with Ibtikar Technologies, and
7
+ only to integrate the customer's own applications with the Sekyro AI
8
8
  identity-verification service. For that purpose you may install the SDK
9
9
  from the public npm registry and run it unmodified.
10
10
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sekyro/node
2
2
 
3
- Server SDK for [Sekyro](https://sekyro.ai) identity verification. Zero runtime
3
+ Server SDK for [Sekyro AI](https://sekyro.ai) identity verification. Zero runtime
4
4
  dependencies, Node.js ≥ 20, ESM + CommonJS, typed.
5
5
 
6
6
  Two jobs, nothing else:
@@ -59,7 +59,7 @@ session.steps; // [{ checkType, state, reason?, calibratedScore? }]
59
59
  ```
60
60
 
61
61
  There is no recipe argument. Your API key belongs to an **app**, and the app's
62
- eKYC profile (set in the Sekyro dashboard) decides which checks run. Every
62
+ eKYC profile (set in the Sekyro AI dashboard) decides which checks run. Every
63
63
  field is optional:
64
64
 
65
65
  | field | meaning |
@@ -138,7 +138,7 @@ app.post("/webhooks/sekyro", express.raw({ type: "application/json" }), (req, re
138
138
  ```
139
139
 
140
140
  - The header is `t=<unix seconds>,v1=<hex>[,v1=<hex>]`. During a secret
141
- rotation Sekyro signs with both the current and the previous secret, so the
141
+ rotation Sekyro AI signs with both the current and the previous secret, so the
142
142
  delivery is accepted when **any** `v1` matches the secret you hold.
143
143
  - **Header names.** Deliveries carry the signature in `x-sekyro-signature`
144
144
  (`SEKYRO_SIGNATURE_HEADER`) and, for the transition, the identical legacy
package/dist/index.cjs CHANGED
@@ -1,308 +1 @@
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.2" : "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
1
+ "use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var src_exports={};__export(src_exports,{DEFAULT_BASE_URL:()=>DEFAULT_BASE_URL,DEFAULT_TIMEOUT_MS:()=>DEFAULT_TIMEOUT_MS,LEGACY_SIGNATURE_HEADER:()=>LEGACY_SIGNATURE_HEADER,SEKYRO_SIGNATURE_HEADER:()=>SEKYRO_SIGNATURE_HEADER,Sekyro:()=>Sekyro,SekyroApiError:()=>SekyroApiError,SekyroConfigError:()=>SekyroConfigError,SekyroWebhookError:()=>SekyroWebhookError,signatureFromHeaders:()=>signatureFromHeaders,verifyWebhook:()=>verifyWebhook});module.exports=__toCommonJS(src_exports);var WEBHOOK_MESSAGES={invalid_signature:"No v1 signature in the header matches this body and secret",stale:"Delivery timestamp is outside the tolerance window",malformed:"Signature header or body is not in the expected format"},SekyroWebhookError=class extends Error{name="SekyroWebhookError";code;constructor(code,message=WEBHOOK_MESSAGES[code]){super(message),this.code=code}},CONFIG_MESSAGES={missing_api_key:"Sekyro AI: `apiKey` is required",invalid_base_url:"Sekyro AI: `baseUrl` must be an https: URL (http: is allowed only for localhost, 127.0.0.1 or [::1])"},SekyroConfigError=class extends TypeError{name="SekyroConfigError";code;constructor(code,message=CONFIG_MESSAGES[code]){super(message),this.code=code}},SekyroApiError=class extends Error{name="SekyroApiError";status;code;requestId;retryable;constructor(init){super(init.message,init.cause===void 0?void 0:{cause:init.cause}),this.status=init.status,this.code=init.code,this.requestId=init.requestId,this.retryable=isRetryable(init.status,init.code)}};function isRetryable(status,code){return code==="network"||code==="timeout"||code==="idempotency_in_progress"||status===429?!0:status>=500&&status<=599}var SDK_VERSION="0.1.0-rc.3";var USER_AGENT=`@sekyro/node/${SDK_VERSION}`,DEFAULT_BASE_URL="https://api.sekyro.ai/api/v2",DEFAULT_TIMEOUT_MS=3e4;function isSuccessEnvelope(value){return typeof value=="object"&&value!==null&&value.success===!0&&"data"in value}function isErrorEnvelope(value){if(typeof value!="object"||value===null)return!1;let v=value;if(v.status!=="error"||typeof v.error!="object"||v.error===null)return!1;let e=v.error;return typeof e.code=="string"&&typeof e.message=="string"}function isAbortLike(err){if(typeof err!="object"||err===null)return!1;let name=err.name;return name==="TimeoutError"||name==="AbortError"}var LOOPBACK_HOSTS=new Set(["localhost","127.0.0.1","[::1]"]);function assertSafeBaseUrl(baseUrl){let url;try{url=new URL(baseUrl)}catch{throw new SekyroConfigError("invalid_base_url")}if(url.protocol!=="https:"&&!(url.protocol==="http:"&&LOOPBACK_HOSTS.has(url.hostname)))throw new SekyroConfigError("invalid_base_url")}var Transport=class{apiKey;baseUrl;timeoutMs;fetchImpl;constructor(opts){let baseUrl=(opts.baseUrl??DEFAULT_BASE_URL).replace(/\/+$/,"");assertSafeBaseUrl(baseUrl),this.apiKey=opts.apiKey,this.baseUrl=baseUrl,this.timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS,this.fetchImpl=opts.fetch??globalThis.fetch}async call(method,path,body,idempotencyKey){let headers={authorization:`Bearer ${this.apiKey}`,accept:"application/json","user-agent":USER_AGENT};body!==void 0&&(headers["content-type"]="application/json"),idempotencyKey!==void 0&&(headers["x-idempotency-key"]=idempotencyKey);let init={method,headers,signal:AbortSignal.timeout(this.timeoutMs)};body!==void 0&&(init.body=JSON.stringify(body));let res;try{res=await this.fetchImpl(`${this.baseUrl}${path}`,init)}catch(err){throw isAbortLike(err)?new SekyroApiError({status:0,code:"timeout",message:`Request timed out after ${this.timeoutMs}ms`,cause:err}):new SekyroApiError({status:0,code:"network",message:`Network error: ${err instanceof Error?err.message:String(err)}`,cause:err})}let text=await res.text(),parsed,parseError;try{parsed=text.length>0?JSON.parse(text):void 0}catch(err){parseError=err}if(!res.ok){if(isErrorEnvelope(parsed)){let init2={status:res.status,code:parsed.error.code,message:parsed.error.message};throw parsed.error.request_id!==void 0&&(init2.requestId=parsed.error.request_id),new SekyroApiError(init2)}throw new SekyroApiError({status:res.status,code:"http_error",message:`HTTP ${res.status}${res.statusText?` ${res.statusText}`:""}`})}if(!isSuccessEnvelope(parsed))throw new SekyroApiError({status:res.status,code:"malformed_response",message:"Response is not a `{ success: true, data }` envelope",cause:parseError});return parsed.data}},SessionsApi=class{constructor(call){this.call=call}call;async create(input={},opts={}){let wire=await this.call("POST","/sessions",input,opts.idempotencyKey);return{id:wire.id,status:wire.status,clientToken:wire.client_token,hostedUrl:wire.hosted_url,steps:wire.steps,expiresAt:wire.expiresAt}}async selectMethod(id,method){if(typeof id!="string"||id.length===0)throw new TypeError("sessions.selectMethod: `id` must be a non-empty string");return this.call("POST",`/sessions/${encodeURIComponent(id)}/method`,{method})}async get(id){if(typeof id!="string"||id.length===0)throw new TypeError("sessions.get: `id` must be a non-empty string");return this.call("GET",`/sessions/${encodeURIComponent(id)}`)}},Sekyro=class{sessions;constructor(opts){if(typeof opts?.apiKey!="string"||opts.apiKey.length===0)throw new SekyroConfigError("missing_api_key");let transport=new Transport(opts);this.sessions=new SessionsApi(transport.call.bind(transport))}};var import_node_crypto=require("crypto");var SEKYRO_SIGNATURE_HEADER="x-sekyro-signature",LEGACY_SIGNATURE_HEADER="x-ibtikar-signature";function signatureFromHeaders(headers){return readHeader(headers,SEKYRO_SIGNATURE_HEADER)??readHeader(headers,LEGACY_SIGNATURE_HEADER)}function readHeader(headers,name){if(typeof headers.get=="function"){let v2=headers.get(name);return v2===null||v2===""?void 0:v2}let record=headers,key=Object.keys(record).find(k=>k.toLowerCase()===name);if(key===void 0)return;let raw=record[key],v=Array.isArray(raw)?raw[0]:raw;return v===void 0||v===""?void 0:v}var HEX64=/^[0-9a-f]{64}$/i;function verifyWebhook(rawBody,header,secret,opts={}){if(!header||!secret)throw new SekyroWebhookError("invalid_signature");let parts=header.split(",").map(part=>{let trimmed=part.trim(),eq=trimmed.indexOf("=");return eq===-1?[trimmed,""]:[trimmed.slice(0,eq).trim(),trimmed.slice(eq+1).trim()]}),tRaw=parts.find(([k])=>k==="t")?.[1],t=tRaw!==void 0&&/^\d+$/.test(tRaw)?Number(tRaw):Number.NaN,sigs=parts.filter(([k])=>k==="v1").map(([,v])=>v);if(!Number.isSafeInteger(t)||sigs.length===0)throw new SekyroWebhookError("malformed");let nowSec=Math.floor((opts.now?.()??Date.now())/1e3),tolerance=opts.toleranceSec??300;if(Math.abs(nowSec-t)>tolerance)throw new SekyroWebhookError("stale");let expected=(0,import_node_crypto.createHmac)("sha256",secret).update(`${t}.${rawBody}`).digest();if(!sigs.some(hex=>{if(!HEX64.test(hex))return!1;let got=Buffer.from(hex,"hex");return got.length===expected.length&&(0,import_node_crypto.timingSafeEqual)(got,expected)}))throw new SekyroWebhookError("invalid_signature");let parsed;try{parsed=JSON.parse(rawBody)}catch{throw new SekyroWebhookError("malformed","Signed body is not valid JSON")}if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))throw new SekyroWebhookError("malformed","Signed body is not a JSON object");return parsed}0&&(module.exports={DEFAULT_BASE_URL,DEFAULT_TIMEOUT_MS,LEGACY_SIGNATURE_HEADER,SEKYRO_SIGNATURE_HEADER,Sekyro,SekyroApiError,SekyroConfigError,SekyroWebhookError,signatureFromHeaders,verifyWebhook});