@sekyro/node 0.0.0-stage → 0.1.0-rc.2
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 +23 -0
- package/LICENSE +20 -0
- package/README.md +157 -2
- package/dist/index.cjs +308 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +404 -0
- package/dist/index.d.ts +404 -0
- package/dist/index.js +272 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @sekyro/node
|
|
2
|
+
|
|
3
|
+
## 0.1.0-rc.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d0dce5e: Breaking (0.x): `sessions.create` no longer takes `flow` or `templateId`. Use an app API key; the app's eKYC profile decides the steps.
|
|
8
|
+
|
|
9
|
+
- Create an app in the Sekyro dashboard, mint an app API key for it, and drop `flow` (or `templateId`) from your `sessions.create` calls.
|
|
10
|
+
- `sessions.create` gains `ttlMinutes` (within the profile's link range, otherwise `400 ttl_out_of_policy`) and `deliver` (`{ channel: "sms" | "whatsapp", to }`, sends the hosted link to the person). The `DeliverOptions` type is exported.
|
|
11
|
+
- Every field is optional, so `sekyro.sessions.create()` is a valid call.
|
|
12
|
+
|
|
13
|
+
## 0.1.0-rc.1
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 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).
|
|
18
|
+
- 880bde9: Webhook envelope typing and the signature header rename.
|
|
19
|
+
|
|
20
|
+
- `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`.
|
|
21
|
+
- `verifyWebhook()` returns a `WebhookEnvelope` **discriminated on `event`**: narrowing `envelope.event` narrows `envelope.data`. The `verifyWebhook<T>()` type parameter is gone — remove the generic argument.
|
|
22
|
+
- 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>`.
|
|
23
|
+
- `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,158 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sekyro/node
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
{ 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
|
+
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
|
|
63
|
+
field is optional:
|
|
64
|
+
|
|
65
|
+
| field | meaning |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `locale` | `ar`, `fr` or `en`: pins the hosted-flow language |
|
|
68
|
+
| `ttlMinutes` | session lifetime, within the profile's link range (`400 ttl_out_of_policy` otherwise); omit for the profile default |
|
|
69
|
+
| `input` | the step anchors: `nni`, `phone`, `fullName`, `birthDate`, all **strings** (an NNI is a 10-digit string, never a number), plus any further keys; `input.nni` binds the session to that identity |
|
|
70
|
+
| `deliver` | `{ channel: "sms" \| "whatsapp", to: "+222…" }`: also send the hosted link to the person |
|
|
71
|
+
|
|
72
|
+
> **0.2 breaking change.** `flow` and `templateId` are gone. Create an app in
|
|
73
|
+
> the dashboard, mint an app API key, and drop `flow` from your calls.
|
|
74
|
+
|
|
75
|
+
`sekyro.sessions` is a `SessionsApi` — exported as a type only; you get the
|
|
76
|
+
instance from the client, never by constructing one.
|
|
77
|
+
|
|
78
|
+
### Errors
|
|
79
|
+
|
|
80
|
+
Every non-2xx response, network failure and timeout throws `SekyroApiError`:
|
|
81
|
+
|
|
82
|
+
| field | meaning |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `status` | HTTP status (`0` for network / timeout) |
|
|
85
|
+
| `code` | API error code, or `network` / `timeout` / `http_error` / `malformed_response` |
|
|
86
|
+
| `message` | human-readable, from the API when available |
|
|
87
|
+
| `requestId` | the API's `request_id`, quote it to support |
|
|
88
|
+
| `retryable` | `true` for `network`, `timeout`, `429`, `5xx` and `idempotency_in_progress` |
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { SekyroApiError } from "@sekyro/node";
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await sekyro.sessions.create({ input: { nni } });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err instanceof SekyroApiError && err.retryable) {
|
|
97
|
+
// back off and retry with the SAME idempotencyKey
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Webhooks
|
|
103
|
+
|
|
104
|
+
Verify with the **raw** request body (bytes as received, before any JSON
|
|
105
|
+
parsing or re-serialisation) and the signature header: `x-sekyro-signature`
|
|
106
|
+
(primary), falling back to the legacy `x-ibtikar-signature` while both are
|
|
107
|
+
sent.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { verifyWebhook, signatureFromHeaders, SekyroWebhookError } from "@sekyro/node";
|
|
111
|
+
|
|
112
|
+
app.post("/webhooks/sekyro", express.raw({ type: "application/json" }), (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const envelope = verifyWebhook(
|
|
115
|
+
req.body.toString("utf8"),
|
|
116
|
+
signatureFromHeaders(req.headers), // x-sekyro-signature, else the legacy x-ibtikar-signature
|
|
117
|
+
process.env.SEKYRO_WEBHOOK_SECRET!,
|
|
118
|
+
);
|
|
119
|
+
// The envelope is discriminated on `event`: narrowing it types `data`.
|
|
120
|
+
switch (envelope.event) {
|
|
121
|
+
case "session.completed":
|
|
122
|
+
case "session.failed":
|
|
123
|
+
envelope.data; // SessionLifecycleEventData — { sessionId, status, outcome, steps }
|
|
124
|
+
break;
|
|
125
|
+
case "session.review_decided":
|
|
126
|
+
envelope.data; // SessionReviewDecidedEventData — { sessionId, outcome: 'pass' | 'fail', reason }
|
|
127
|
+
break;
|
|
128
|
+
case "session.step.completed":
|
|
129
|
+
envelope.data; // SessionStepCompletedEventData — { sessionId, checkType, state }
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
res.sendStatus(200);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err instanceof SekyroWebhookError) return res.sendStatus(400); // invalid_signature | stale | malformed
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
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
|
|
142
|
+
delivery is accepted when **any** `v1` matches the secret you hold.
|
|
143
|
+
- **Header names.** Deliveries carry the signature in `x-sekyro-signature`
|
|
144
|
+
(`SEKYRO_SIGNATURE_HEADER`) and, for the transition, the identical legacy
|
|
145
|
+
`x-ibtikar-signature` (`LEGACY_SIGNATURE_HEADER`). `signatureFromHeaders()`
|
|
146
|
+
takes a Fetch `Headers` or a Node headers record and returns the branded
|
|
147
|
+
header, else the legacy one. The event id/type travel the same way:
|
|
148
|
+
`x-sekyro-event-id` / `x-sekyro-event-type` alongside their `x-ibtikar-*`
|
|
149
|
+
twins.
|
|
150
|
+
- Each `v1` is `HMAC-SHA256(secret, "<t>.<rawBody>")`, compared in constant time.
|
|
151
|
+
- Deliveries older or newer than `toleranceSec` (default 300 s) are rejected as
|
|
152
|
+
`stale`; pass `{ now }` to inject a clock in tests.
|
|
153
|
+
- Retried deliveries carry the same envelope `id`; de-duplicate on it.
|
|
154
|
+
|
|
155
|
+
## Compatibility
|
|
156
|
+
|
|
157
|
+
Only `node:crypto` and the global `fetch` are used. You can inject a `fetch`
|
|
158
|
+
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.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
|
|
@@ -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 DeliverOptions,\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({ input: { nni: \"1357924632\" } });\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":"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","names":["init","v"]}
|