@sekyro/node 0.1.0-rc.4 → 0.1.0-rc.6
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 +26 -0
- package/README.md +112 -27
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +416 -147
- package/dist/index.d.ts +416 -147
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @sekyro/node
|
|
2
2
|
|
|
3
|
+
## 0.1.0-rc.6
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8bc8320: `subject.reuse` accepts an identity document: `{ document: { type, issuingCountry, number } }` (`SubjectReuseDocument`; `type` is `national_id`, `passport` or `residence_permit`, `issuingCountry` ISO alpha-2 or alpha-3). Still exactly one target per session.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 82c4898: `CreateSessionInput.assistedBy` and `Session.assistedBy`: your own reference for the agent filming the person in an assisted-capture flow.
|
|
12
|
+
|
|
13
|
+
## 0.1.0-rc.5
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 82d0799: Sekyro flows: choose the app's flow at session create, override it per session, and read which flow a session ran.
|
|
18
|
+
|
|
19
|
+
- `sessions.create({ flow })`: the app's flow by slug (e.g. `foreigners`) or `flw_` id; absent = the app's default flow.
|
|
20
|
+
- `sessions.create({ overrides })`: `FlowOverrides` with `settings` (eligibility, decisions, data consistency, returning users, documents) and `services` (per-service settings keyed by `ServiceId`, or `'off'` to remove the service). New types `FlowOverrides`, `FlowOverridableSettings`, `FlowServiceSettings`, `ServiceId`.
|
|
21
|
+
- Document substitution is a setting of the `document` and `nfc` services (`documentSubstitution: { mode: 'off' | 'signal' | 'review', failBelow? }`, new type `DocumentSubstitutionSettings`), not of `device_network`. `population_registry.sources` and `national_eid.providers` also accept `'org'` (every registry or national eID your organisation can use).
|
|
22
|
+
- `sekyro.listFlows()` (`GET /api/v2/flows`): the published flows of the key's app as `FlowSummary` (`id`, `slug`, `name`, `version`, `isDefault`).
|
|
23
|
+
- `flow` (`SessionFlowRef`: `id`, `slug`, `version`) on `Session` and on the `session.*` webhook data; `nodeId` and `service` on session steps, webhook step entries and `session.step.completed`; `handled` on session steps (a fallback took over). Checks of a lane the person did not take are not listed.
|
|
24
|
+
- `flow` on `CreatedSession` (the flow version the new session runs); its steps omit the lanes not taken.
|
|
25
|
+
- Errors: `SekyroFlowErrorCode` (`flow_not_found`, `flow_not_published`, `flow_archived`, `override_invalid`, `override_widens_flow`, `reuse_disabled`, and `choice_not_found` / `choice_locked` / `choice_option_unavailable` from `sessions.selectMethod`); `SekyroApiError.code` is typed `SekyroApiErrorCode` (still any string) and `SekyroApiError.details` carries the API's `error.details`. `isFlowError(err, ...codes)` narrows `details` to the code's documented shape (`SekyroFlowErrorDetailsMap`): `issues[]` (`FlowErrorIssue`: `code`, `message`, `path?`, `nodeId?`, `service?`, `field?`), plus `fields` on `override_widens_flow` and `flow { id, slug }` on `flow_not_published`, `flow_archived` and `reuse_disabled`. Missing or unexpected details are not narrowed, never thrown on.
|
|
26
|
+
- `Subject.returningReady` follows the returning-user settings of the app's default flow.
|
|
27
|
+
- **Breaking:** the `profile` override on `sessions.create` is removed, with the `EkycProfileSettings`, `EkycProfileOverride` and `ProfileEmail` types. Use `overrides`.
|
|
28
|
+
|
|
3
29
|
## 0.1.0-rc.4
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@ dependencies, Node.js ≥ 20, ESM + CommonJS, typed.
|
|
|
5
5
|
|
|
6
6
|
Two jobs, nothing else:
|
|
7
7
|
|
|
8
|
-
1. **Sessions
|
|
8
|
+
1. **Sessions**: create a verification session from your backend and read its
|
|
9
9
|
result. The API key never leaves your server.
|
|
10
|
-
2. **Webhooks
|
|
10
|
+
2. **Webhooks**: verify the `x-sekyro-signature` header on deliveries (the
|
|
11
11
|
legacy `x-ibtikar-signature` name is still sent) and get a typed envelope
|
|
12
12
|
back.
|
|
13
13
|
|
|
@@ -22,9 +22,12 @@ Next.js and Express, billing). The hosted-flow path is in the
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
npm install @sekyro/node
|
|
25
|
+
npm install @sekyro/node@next
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
The SDK is licensed under the
|
|
29
|
+
[Sekyro AI SDK License](https://sekyro.ai/legal/sdk-license).
|
|
30
|
+
|
|
28
31
|
## Sessions
|
|
29
32
|
|
|
30
33
|
```ts
|
|
@@ -32,47 +35,96 @@ import { Sekyro } from "@sekyro/node";
|
|
|
32
35
|
|
|
33
36
|
const sekyro = new Sekyro({
|
|
34
37
|
apiKey: process.env.SEKYRO_API_KEY!,
|
|
35
|
-
// baseUrl: "https://api.sekyro.ai/api/v2", // default; use
|
|
38
|
+
// baseUrl: "https://api.sekyro.ai/api/v2", // default; use https://api-staging.sekyro.ai/api/v2 for sandbox work
|
|
36
39
|
// timeoutMs: 30_000, // default
|
|
37
40
|
});
|
|
38
41
|
// Throws `SekyroConfigError` (a TypeError; `code` = 'missing_api_key' | 'invalid_base_url')
|
|
39
|
-
// when apiKey is missing or baseUrl is not https:
|
|
40
|
-
// localhost / 127.0.0.1 / [::1], since the API key rides on every request.
|
|
42
|
+
// when apiKey is missing or baseUrl is not https: (plain http: is accepted only for
|
|
43
|
+
// localhost / 127.0.0.1 / [::1], since the API key rides on every request).
|
|
41
44
|
|
|
42
|
-
// Create
|
|
45
|
+
// Create: returns the one-time client token your front end hands to @sekyro/web.
|
|
46
|
+
// `flow` picks one of the app's flows (omit it for the default flow).
|
|
43
47
|
// `input.nni` BINDS the session to that identity: the browser never sends the
|
|
44
48
|
// NNI (a different one is refused with 409 nni_locked).
|
|
45
49
|
const created = await sekyro.sessions.create(
|
|
46
|
-
{ locale: "ar", input: { nni: "
|
|
50
|
+
{ flow: "default", locale: "ar", input: { nni: "0000000098" } },
|
|
47
51
|
{ idempotencyKey: `signup:${userId}` },
|
|
48
52
|
);
|
|
49
53
|
created.id; // persist against your user before returning to the page
|
|
50
54
|
created.clientToken; // give this to the browser; it is returned ONCE
|
|
51
55
|
created.hostedUrl; // the hosted-flow link, if you use that path instead
|
|
52
56
|
created.expiresAt; // ISO 8601
|
|
57
|
+
created.flow; // { id, slug, version }: the flow version the session runs
|
|
53
58
|
|
|
54
|
-
// Read
|
|
59
|
+
// Read: the whitelisted session view (never contains the client token).
|
|
55
60
|
const session = await sekyro.sessions.get(created.id);
|
|
56
61
|
session.status; // 'created' | 'in_progress' | 'review_pending' | 'completed' | 'expired' | 'failed'
|
|
57
62
|
session.outcome; // 'approved' | 'review' | 'rejected' | null
|
|
58
|
-
session.
|
|
63
|
+
session.flow; // { id: 'flw_…', slug: 'default', version: 3 }: the flow version the session runs
|
|
64
|
+
session.steps; // [{ checkType, state, nodeId?, service?, handled?, reason?, calibratedScore? }]
|
|
59
65
|
```
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
Your API key belongs to an **app**. The app's **flows**, designed in the Sekyro AI
|
|
68
|
+
dashboard (app page → **Flows**), decide which steps run; a session runs the
|
|
69
|
+
flow you name in `flow`, or the app's default flow. To list the published flows
|
|
70
|
+
of the key's app, call `sekyro.listFlows()`: it resolves to
|
|
71
|
+
`FlowSummary[]` (`[{ id, slug, name, version, isDefault }]`), the default flow
|
|
72
|
+
first (`GET /api/v2/flows` under the hood, scope `sessions:create`;
|
|
73
|
+
see the [quickstart](https://docs.sekyro.ai/en/getting-started/quickstart#choose-a-flow)).
|
|
74
|
+
Every field is optional:
|
|
64
75
|
|
|
65
76
|
| field | meaning |
|
|
66
77
|
|---|---|
|
|
78
|
+
| `flow` | the flow's identifier (slug, e.g. `foreigners`) or id (`flw_…`); omit for the app's default flow. `404 flow_not_found`, `409 flow_not_published`, `409 flow_archived` |
|
|
79
|
+
| `overrides` | `{ settings?, services? }`: changes the flow for this session only (see below; `403 override_widens_flow` for a looser change unless the flow allows it, `400 override_invalid` for an invalid one) |
|
|
67
80
|
| `locale` | `ar`, `fr` or `en`: pins the hosted-flow language |
|
|
68
|
-
| `ttlMinutes` | session lifetime, within the
|
|
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
|
-
| `
|
|
81
|
+
| `ttlMinutes` | session lifetime, within the flow's link range (`400 ttl_out_of_policy` otherwise); omit for the flow default |
|
|
82
|
+
| `input` | the step anchors: `nni`, `phone`, `email`, `fullName`, `birthDate`, all **strings** (an NNI is a 10-digit string, never a number), plus any further keys (flow branches can read them); `input.nni` binds the session to that identity |
|
|
83
|
+
| `subject` | what you know about the person (`nni`, `phone`, `fullName`, `birthDate`, `nationality`, `documentNumber`), your own id for them (`externalRef`), or `reuse` for a [returning user](#subjects) |
|
|
84
|
+
| `identityMethod` | `national_number`, `document_photo`, `document_chip` or `national_eid`: pre-chooses the option of the flow's identity choice |
|
|
85
|
+
| `deliver` | `{ channel: "sms" \| "whatsapp", to: "+1555…" }`: also send the hosted link to the person |
|
|
71
86
|
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
```ts
|
|
88
|
+
// The app's flows (published only, default first): pick one at runtime.
|
|
89
|
+
const flows = await sekyro.listFlows(); // [{ id, slug, name, version, isDefault }]
|
|
74
90
|
|
|
75
|
-
|
|
91
|
+
await sekyro.sessions.create({
|
|
92
|
+
flow: "foreigners",
|
|
93
|
+
overrides: {
|
|
94
|
+
settings: { eligibility: { minAge: 21 } }, // general settings
|
|
95
|
+
services: { liveness: { attempts: 2 } }, // per-service settings
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`overrides.settings` changes the flow's general settings (eligibility,
|
|
101
|
+
decisions, data consistency, returning users, documents); `overrides.services`
|
|
102
|
+
changes a service's settings, keyed by service id (`liveness`, `phone_otp`,
|
|
103
|
+
`device_network`, …), or removes it with `'off'`. An override may only make
|
|
104
|
+
the flow stricter unless the flow allows widening; turning a service `'off'`
|
|
105
|
+
always widens. The changed paths come back on `session.overriddenFields`.
|
|
106
|
+
|
|
107
|
+
> **Sekyro flows breaking change.** The `profile` override is gone: use
|
|
108
|
+
> `overrides`. `flow` is accepted again, now as the app's flow slug or id.
|
|
109
|
+
|
|
110
|
+
The session view also carries `failureReason` (a policy stop such as
|
|
111
|
+
`location_not_allowed` or `underage`), `overriddenFields`, `riskSignals`,
|
|
112
|
+
`subjectId` / `externalRef`, `verifiedContacts`, `eid`, and the end user's
|
|
113
|
+
`network` and `device`. A step with `handled: true` failed, but the flow's
|
|
114
|
+
fallback took over.
|
|
115
|
+
|
|
116
|
+
Two more session calls:
|
|
117
|
+
|
|
118
|
+
- `sekyro.sessions.selectMethod(id, method)` chooses the identity method of a
|
|
119
|
+
session on the person's behalf (`POST /sessions/:id/method`): `409 choice_locked`
|
|
120
|
+
once the flow's identity choice can no longer change, `409 choice_option_unavailable`
|
|
121
|
+
for a method the flow does not offer this session, `409 choice_not_found` when the
|
|
122
|
+
flow has no identity choice.
|
|
123
|
+
- `sekyro.sessions.portrait(id)` returns a 5-minute link to the ICAO portrait of
|
|
124
|
+
a session whose `portrait_icao` check passed (`portraitAvailable`; scope
|
|
125
|
+
`portrait:read`).
|
|
126
|
+
|
|
127
|
+
`sekyro.sessions` is a `SessionsApi`, exported as a type only: you get the
|
|
76
128
|
instance from the client, never by constructing one.
|
|
77
129
|
|
|
78
130
|
### Partner-supplied identity data (`supplied` mode)
|
|
@@ -81,17 +133,18 @@ If Sekyro AI set your environment to the `supplied` mode, you send the
|
|
|
81
133
|
person's identity data (biographic record and photo) with the session in
|
|
82
134
|
`input.suppliedIdentity`. Sekyro AI treats it as identity data supplied by
|
|
83
135
|
your organization: it makes no registry call, and it never receives or stores
|
|
84
|
-
any registry key. If your data comes from your own
|
|
85
|
-
`registryFromAnrpts()` converts that answer locally into
|
|
136
|
+
any registry key. If your data comes from your own call to the national
|
|
137
|
+
population registry, `registryFromAnrpts()` converts that answer locally into
|
|
138
|
+
the record shape:
|
|
86
139
|
|
|
87
140
|
```ts
|
|
88
141
|
import { registryFromAnrpts } from "@sekyro/node";
|
|
89
142
|
|
|
90
|
-
const
|
|
143
|
+
const answer = await myRegistryClient.lookup(nni); // your call, your key
|
|
91
144
|
const receivedAt = new Date();
|
|
92
145
|
await sekyro.sessions.create({
|
|
93
146
|
// fetchedAt: when YOUR system received this answer (required; > 24 h old is refused)
|
|
94
|
-
input: { nni, suppliedIdentity: registryFromAnrpts(
|
|
147
|
+
input: { nni, suppliedIdentity: registryFromAnrpts(answer, { fetchedAt: receivedAt }) },
|
|
95
148
|
});
|
|
96
149
|
```
|
|
97
150
|
|
|
@@ -121,6 +174,7 @@ Every non-2xx response, network failure and timeout throws `SekyroApiError`:
|
|
|
121
174
|
| `code` | API error code, or `network` / `timeout` / `http_error` / `malformed_response` |
|
|
122
175
|
| `message` | human-readable, from the API when available |
|
|
123
176
|
| `requestId` | the API's `request_id`, quote it to support |
|
|
177
|
+
| `details` | the API's `error.details` when sent, else `undefined`; typed for the flow codes through `isFlowError()` |
|
|
124
178
|
| `retryable` | `true` for `network`, `timeout`, `429`, `5xx` and `idempotency_in_progress` |
|
|
125
179
|
|
|
126
180
|
```ts
|
|
@@ -135,11 +189,42 @@ try {
|
|
|
135
189
|
}
|
|
136
190
|
```
|
|
137
191
|
|
|
192
|
+
Flow codes (`SekyroFlowErrorCode`) carry machine-readable `details`:
|
|
193
|
+
`details.issues[]` (`{ code, message, path?, nodeId?, service?, field? }`,
|
|
194
|
+
never a value you sent), plus `fields` on `override_widens_flow` and `flow`
|
|
195
|
+
(`{ id, slug }`) on `flow_not_published`, `flow_archived` and `reuse_disabled`.
|
|
196
|
+
`isFlowError(err, ...codes)` narrows `err.details` to the code's shape; it
|
|
197
|
+
returns `false` when the details are missing or not in that shape, so read
|
|
198
|
+
`err.code` as the fallback.
|
|
199
|
+
|
|
200
|
+
| code | from | `details` |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `404 flow_not_found` | `sessions.create` | `issues` (`path: "flow"`) |
|
|
203
|
+
| `409 flow_not_published`, `409 flow_archived` | `sessions.create` | `issues`, `flow` |
|
|
204
|
+
| `400 override_invalid` | `sessions.create` | `issues`, one per problem (`path: "overrides.services.liveness.attempts"`, …) |
|
|
205
|
+
| `403 override_widens_flow` | `sessions.create` | `issues` (`field_widens_flow`), `fields` |
|
|
206
|
+
| `409 reuse_disabled` | `sessions.create` with `subject.reuse` | `issues`, `flow` |
|
|
207
|
+
| `409 choice_not_found`, `choice_locked`, `choice_option_unavailable` | `sessions.selectMethod` | `issues` |
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { isFlowError } from "@sekyro/node";
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await sekyro.sessions.create({ flow: "foreigners", overrides });
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (isFlowError(err, "override_widens_flow")) {
|
|
216
|
+
console.warn("widening refused", err.details.fields);
|
|
217
|
+
} else if (isFlowError(err, "override_invalid")) {
|
|
218
|
+
for (const issue of err.details.issues) console.warn(issue.path, issue.message);
|
|
219
|
+
} else throw err;
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
138
223
|
## Subjects
|
|
139
224
|
|
|
140
225
|
A **subject** is the same person across all of your apps. Reads need the
|
|
141
226
|
`subjects:read` scope; `revoke` and `delete` need `subjects:write` (keys minted
|
|
142
|
-
from the dashboard carry both
|
|
227
|
+
from the dashboard carry both; mint a new key if an older one answers
|
|
143
228
|
`403 insufficient_scope`).
|
|
144
229
|
|
|
145
230
|
```ts
|
|
@@ -184,13 +269,13 @@ app.post("/webhooks/sekyro", express.raw({ type: "application/json" }), (req, re
|
|
|
184
269
|
switch (envelope.event) {
|
|
185
270
|
case "session.completed":
|
|
186
271
|
case "session.failed":
|
|
187
|
-
envelope.data; // SessionLifecycleEventData
|
|
272
|
+
envelope.data; // SessionLifecycleEventData: { sessionId, status, outcome, flow, steps, … }
|
|
188
273
|
break;
|
|
189
274
|
case "session.review_decided":
|
|
190
|
-
envelope.data; // SessionReviewDecidedEventData
|
|
275
|
+
envelope.data; // SessionReviewDecidedEventData: { sessionId, outcome: 'pass' | 'fail', reason, flow }
|
|
191
276
|
break;
|
|
192
277
|
case "session.step.completed":
|
|
193
|
-
envelope.data; // SessionStepCompletedEventData
|
|
278
|
+
envelope.data; // SessionStepCompletedEventData: { sessionId, checkType, state, nodeId, service, flow }
|
|
194
279
|
break;
|
|
195
280
|
}
|
|
196
281
|
res.sendStatus(200);
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
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,registryFromAnrpts:()=>registryFromAnrpts,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])",invalid_registry_record:"Sekyro AI: the ANRPTS answer can't be used as a registry record"},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.4";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 text2=await res.text(),parsed,parseError;try{parsed=text2.length>0?JSON.parse(text2):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)}`)}async portrait(id){if(typeof id!="string"||id.length===0)throw new TypeError("sessions.portrait: `id` must be a non-empty string");return this.call("GET",`/sessions/${encodeURIComponent(id)}/portrait?format=url`)}};function assertId(method,id){if(typeof id!="string"||id.length===0)throw new TypeError(`subjects.${method}: \`id\` must be a non-empty string`)}var SubjectsApi=class{constructor(call){this.call=call}call;async get(id){return assertId("get",id),this.call("GET",`/subjects/${encodeURIComponent(id)}`)}async list(query){if(typeof query?.externalRef!="string"||query.externalRef.length===0)throw new TypeError("subjects.list: `externalRef` must be a non-empty string");let qs=new URLSearchParams({externalRef:query.externalRef});return this.call("GET",`/subjects?${qs.toString()}`)}async revoke(id,input={},opts={}){return assertId("revoke",id),this.call("POST",`/subjects/${encodeURIComponent(id)}/revoke`,input,opts.idempotencyKey)}async delete(id){return assertId("delete",id),this.call("DELETE",`/subjects/${encodeURIComponent(id)}`)}},Sekyro=class{sessions;subjects;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)),this.subjects=new SubjectsApi(transport.call.bind(transport))}};function registryFromAnrpts(response,options){let p=response?.personne??{},required=(field,value)=>{let s=text(value);if(s===void 0)throw new SekyroConfigError("invalid_registry_record",`Sekyro AI: the ANRPTS answer has no usable \`${field}\``);return s},birthDate=isoDay(p.dateNaissance);if(birthDate===void 0)throw new SekyroConfigError("invalid_registry_record","Sekyro AI: the ANRPTS answer has no usable `personne.dateNaissance`");let photo=required("photo",response?.photo).replace(/\s+/g,""),fetchedAt=new Date(options?.fetchedAt??Number.NaN);if(Number.isNaN(fetchedAt.getTime()))throw new SekyroConfigError("invalid_registry_record","Sekyro AI: `fetchedAt` (when your system received the ANRPTS answer) is missing or not a valid date");let record={nni:required("personne.nni",p.nni),firstNameFr:required("personne.prenomFr",p.prenomFr),lastNameFr:required("personne.patronymeFr",p.patronymeFr),firstNameAr:required("personne.prenomAr",p.prenomAr),lastNameAr:required("personne.patronymeAr",p.patronymeAr),birthDate,photo,fetchedAt:fetchedAt.toISOString()},optional=[["fatherNameFr",p.perePrenomFr],["fatherNameAr",p.perePrenomAr],["birthPlaceFr",p.lieuNaissanceFr],["birthPlaceAr",p.lieuNaissanceAr]];for(let[key,value]of optional){let s=text(value);s!==void 0&&(record[key]=s)}let sex=text(p.sexeCode)?.toUpperCase();(sex==="M"||sex==="F")&&(record.sex=sex);let nationalities=Array.isArray(p.nationalities)?p.nationalities.map(text).filter(n=>n!==void 0).slice(0,5):[];nationalities.length>0&&(record.nationalities=nationalities);let photoDate=isoDay(response?.datePhoto);return photoDate!==void 0&&(record.photoDate=photoDate),record}function text(value){if(typeof value!="string")return;let s=value.trim();return s.length>0?s:void 0}function isoDay(value){let s=text(value);if(s===void 0)return;let iso=/^(\d{4})-(\d{2})-(\d{2})(?:$|T)/.exec(s),fr=/^(\d{2})\/(\d{2})\/(\d{4})$/.exec(s),[y,m,d]=iso?[iso[1],iso[2],iso[3]]:fr?[fr[3],fr[2],fr[1]]:[];if(y===void 0||m===void 0||d===void 0)return;let t=new Date(Date.UTC(Number(y),Number(m)-1,Number(d)));return t.getUTCFullYear()===Number(y)&&t.getUTCMonth()===Number(m)-1&&t.getUTCDate()===Number(d)?`${y}-${m}-${d}`:void 0}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,registryFromAnrpts,signatureFromHeaders,verifyWebhook});
|
|
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,isFlowError:()=>isFlowError,registryFromAnrpts:()=>registryFromAnrpts,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])",invalid_registry_record:"Sekyro AI: the ANRPTS answer can't be used as a registry record"},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;details;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.details=init.details,this.retryable=isRetryable(init.status,init.code)}},FLOW_ERROR_CODES=new Set(["flow_not_found","flow_not_published","flow_archived","override_invalid","override_widens_flow","reuse_disabled","choice_not_found","choice_locked","choice_option_unavailable"]),FLOW_REF_CODES=new Set(["flow_not_published","flow_archived","reuse_disabled"]),isRecord=x=>typeof x=="object"&&x!==null&&!Array.isArray(x),isStringArray=x=>Array.isArray(x)&&x.every(s=>typeof s=="string");function isIssue(x){return isRecord(x)&&typeof x.code=="string"&&typeof x.message=="string"}function hasFlowDetails(code,details){if(!isRecord(details)||!Array.isArray(details.issues)||!details.issues.every(isIssue))return!1;if(code==="override_widens_flow")return isStringArray(details.fields);if(FLOW_REF_CODES.has(code)){let flow=details.flow;return isRecord(flow)&&typeof flow.id=="string"&&typeof flow.slug=="string"}return!0}function isFlowError(err,...codes){return!(err instanceof SekyroApiError)||!FLOW_ERROR_CODES.has(err.code)||codes.length>0&&!codes.includes(err.code)?!1:hasFlowDetails(err.code,err.details)}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.6";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 text2=await res.text(),parsed,parseError;try{parsed=text2.length>0?JSON.parse(text2):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),parsed.error.details!==void 0&&(init2.details=parsed.error.details),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,...wire.flow!==void 0?{flow:wire.flow}:{},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)}`)}async portrait(id){if(typeof id!="string"||id.length===0)throw new TypeError("sessions.portrait: `id` must be a non-empty string");return this.call("GET",`/sessions/${encodeURIComponent(id)}/portrait?format=url`)}};function assertId(method,id){if(typeof id!="string"||id.length===0)throw new TypeError(`subjects.${method}: \`id\` must be a non-empty string`)}var SubjectsApi=class{constructor(call){this.call=call}call;async get(id){return assertId("get",id),this.call("GET",`/subjects/${encodeURIComponent(id)}`)}async list(query){if(typeof query?.externalRef!="string"||query.externalRef.length===0)throw new TypeError("subjects.list: `externalRef` must be a non-empty string");let qs=new URLSearchParams({externalRef:query.externalRef});return this.call("GET",`/subjects?${qs.toString()}`)}async revoke(id,input={},opts={}){return assertId("revoke",id),this.call("POST",`/subjects/${encodeURIComponent(id)}/revoke`,input,opts.idempotencyKey)}async delete(id){return assertId("delete",id),this.call("DELETE",`/subjects/${encodeURIComponent(id)}`)}},Sekyro=class{sessions;subjects;#transport;constructor(opts){if(typeof opts?.apiKey!="string"||opts.apiKey.length===0)throw new SekyroConfigError("missing_api_key");let transport=new Transport(opts);this.#transport=transport,this.sessions=new SessionsApi(transport.call.bind(transport)),this.subjects=new SubjectsApi(transport.call.bind(transport))}async listFlows(){return this.#transport.call("GET","/flows")}};function registryFromAnrpts(response,options){let p=response?.personne??{},required=(field,value)=>{let s=text(value);if(s===void 0)throw new SekyroConfigError("invalid_registry_record",`Sekyro AI: the ANRPTS answer has no usable \`${field}\``);return s},birthDate=isoDay(p.dateNaissance);if(birthDate===void 0)throw new SekyroConfigError("invalid_registry_record","Sekyro AI: the ANRPTS answer has no usable `personne.dateNaissance`");let photo=required("photo",response?.photo).replace(/\s+/g,""),fetchedAt=new Date(options?.fetchedAt??Number.NaN);if(Number.isNaN(fetchedAt.getTime()))throw new SekyroConfigError("invalid_registry_record","Sekyro AI: `fetchedAt` (when your system received the ANRPTS answer) is missing or not a valid date");let record={nni:required("personne.nni",p.nni),firstNameFr:required("personne.prenomFr",p.prenomFr),lastNameFr:required("personne.patronymeFr",p.patronymeFr),firstNameAr:required("personne.prenomAr",p.prenomAr),lastNameAr:required("personne.patronymeAr",p.patronymeAr),birthDate,photo,fetchedAt:fetchedAt.toISOString()},optional=[["fatherNameFr",p.perePrenomFr],["fatherNameAr",p.perePrenomAr],["birthPlaceFr",p.lieuNaissanceFr],["birthPlaceAr",p.lieuNaissanceAr]];for(let[key,value]of optional){let s=text(value);s!==void 0&&(record[key]=s)}let sex=text(p.sexeCode)?.toUpperCase();(sex==="M"||sex==="F")&&(record.sex=sex);let nationalities=Array.isArray(p.nationalities)?p.nationalities.map(text).filter(n=>n!==void 0).slice(0,5):[];nationalities.length>0&&(record.nationalities=nationalities);let photoDate=isoDay(response?.datePhoto);return photoDate!==void 0&&(record.photoDate=photoDate),record}function text(value){if(typeof value!="string")return;let s=value.trim();return s.length>0?s:void 0}function isoDay(value){let s=text(value);if(s===void 0)return;let iso=/^(\d{4})-(\d{2})-(\d{2})(?:$|T)/.exec(s),fr=/^(\d{2})\/(\d{2})\/(\d{4})$/.exec(s),[y,m,d]=iso?[iso[1],iso[2],iso[3]]:fr?[fr[3],fr[2],fr[1]]:[];if(y===void 0||m===void 0||d===void 0)return;let t=new Date(Date.UTC(Number(y),Number(m)-1,Number(d)));return t.getUTCFullYear()===Number(y)&&t.getUTCMonth()===Number(m)-1&&t.getUTCDate()===Number(d)?`${y}-${m}-${d}`:void 0}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,isFlowError,registryFromAnrpts,signatureFromHeaders,verifyWebhook});
|