@sekyro/node 0.1.0-rc.3 → 0.1.0-rc.5
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 +44 -0
- package/LICENSE +25 -16
- package/README.md +173 -24
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +808 -107
- package/dist/index.d.ts +808 -107
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/sbom.cdx.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @sekyro/node
|
|
2
2
|
|
|
3
|
+
## 0.1.0-rc.5
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 82d0799: Sekyro flows: choose the app's flow at session create, override it per session, and read which flow a session ran.
|
|
8
|
+
|
|
9
|
+
- `sessions.create({ flow })`: the app's flow by slug (e.g. `foreigners`) or `flw_` id; absent = the app's default flow.
|
|
10
|
+
- `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`.
|
|
11
|
+
- 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).
|
|
12
|
+
- `sekyro.listFlows()` (`GET /api/v2/flows`): the published flows of the key's app as `FlowSummary` (`id`, `slug`, `name`, `version`, `isDefault`).
|
|
13
|
+
- `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.
|
|
14
|
+
- `flow` on `CreatedSession` (the flow version the new session runs); its steps omit the lanes not taken.
|
|
15
|
+
- 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.
|
|
16
|
+
- `Subject.returningReady` follows the returning-user settings of the app's default flow.
|
|
17
|
+
- **Breaking:** the `profile` override on `sessions.create` is removed, with the `EkycProfileSettings`, `EkycProfileOverride` and `ProfileEmail` types. Use `overrides`.
|
|
18
|
+
|
|
19
|
+
## 0.1.0-rc.4
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- 46905f2: National eID (SDK spec §3.2; Houwiyeti in Mauritania): the `eid_auth` check type, the `national_eid` identity method, and the generic `eid` result (`{ provider, country, verifiedAt }`) on the partner session view and the terminal `session.*` webhook data.
|
|
24
|
+
|
|
25
|
+
**Migration (TypeScript):** `CheckType` gains `eid_auth` and `IdentityMethod` gains `national_eid`; webhook/session handlers that switch over them exhaustively need an extra branch. The `eid` field is optional and additive.
|
|
26
|
+
- 7a3f935: Phone OTP channel choice and the email OTP step.
|
|
27
|
+
|
|
28
|
+
- `@sekyro/web`: the phone OTP step lists the channels the person can choose from (`PhoneOtpSnapshot.channels`, WhatsApp first; `[]` on a session created before channel choice). With more than one, `send({ channel })` needs the person's choice — it is never picked for them — and a channel the step doesn't offer is refused locally with the new `OtpInputError` (`channel`, `required` / `unavailable`). `resendAvailableAt` now follows the server's resend time when the session reports one. New `email_otp` step (`EmailOtpStepHandle`: `send({ email })`, `verify(code)`; a malformed address rejects with `OtpInputError("email", "format")`), shown before the identity-method choice like the phone step. New types: `HostedEmailOtpView`, `EmailOtpInput`, `EmailOtpReason`, the `email_otp` check type and the reasons `otp_channel_unavailable`, `otp_destination_not_allowed`, `otp_destination_unknown`, `otp_rate_limited`, `email_invalid`, `email_disposable`.
|
|
29
|
+
- `@sekyro/react`: `PhoneOtpStep` shows one "Send code by …" button per offered channel (WhatsApp first) and a "Resend by …" button per channel after the server's resend time; sessions without channels keep the single "Send code" button. New `EmailOtpStep` (rendered by `SekyroVerification` for the `email_otp` step); its view and ar/fr/en copy load on demand, so apps without an email check don't download them. New copy keys `otp.chooseChannel`, `otp.sendBy`, `otp.resendBy`, the new `otp.reason_*` keys and `emailOtp.*` (overridable through `strings`).
|
|
30
|
+
- `@sekyro/node`: profile `phone.channels` / `phone.destinations`, `email` (`ProfileEmail`: `emailSource`, `disposable`), `attempts.email`, the `input.email` session anchor and the `email_otp` check type.
|
|
31
|
+
- 3ee9ae7: `EkycProfileSettings.portrait`: the live photo's purpose and the new selfie ICAO policy (`icao: { policy: "alert" | "reject" }`). `reject` asks the person to retake a selfie that does not meet the ICAO photo rules; `alert` lets them continue and records the risk signal `icao_non_compliant` on the session.
|
|
32
|
+
- 757f31d: Type `verifiedContacts` (exported `VerifiedContacts`): the full phone number (E.164, with the channel) and email address the person verified in a session, from OTP checks that passed — on `Session` (`sessions.get`), on `session.completed` / `session.failed` webhook data, and on `session.step.completed` for a passed `phone_otp` / `email_otp` check. Partner-only; absent when nothing was verified or after the session's personal data is purged.
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- 4c1aa5b: International documents (profile v2 §5.3a): the document type `residence_permit` joins `AcceptedDocumentType` / `ProfileDocumentType`, and `carte_identite` now means a national identity card of any issuing country. `acceptedDocumentTypes` keeps listing only `carte_identite` and `passport`.
|
|
37
|
+
- 9acafe1: NFC chip check (M-nfc). New check type `nfc_chip` (read by the native SDKs only), its reasons (`NfcChipReason`: retryable `chip_read_failed`, `chip_access_denied`, `chip_parse_failed`; final `chip_not_present`, `nfc_unavailable`, `chip_document_mismatch`, `chip_integrity_failed`, `registry_mismatch`, `document_nni_mismatch`, `platform_unsupported`), the error codes `method_not_offered` and `nfc_outcome_not_accepted` (`KNOWN_CODES`), the identity method `document_chip`, the session failure reason `platform_unsupported` and the optional `HostedStepView.nfc` hints (`NfcStepView`). Web behaviour is unchanged: the server skips the chip for web clients or stops the session with `platform_unsupported`; `@sekyro/react`'s method screen never renders the `document_chip` option.
|
|
38
|
+
- 5988b08: Verified subjects (M-nfc): assurance `chip_biometric` (a passed document chip read plus a face match) and reference source `chip` (the chip's DG2 portrait).
|
|
39
|
+
- e229ab3: Add partner-supplied identity data to `sessions.create`: `input.suppliedIdentity` (`SuppliedIdentityRecord`, the person's biographic record and photo, for environments on the `supplied` mode). Add `registryFromAnrpts(response, { fetchedAt })`, a local mapping of an ANRPTS `getPersonne` answer your server already received into that record (`fetchedAt`, when your system received it, is required). Sekyro AI never sees or stores any ANRPTS key. A missing required field throws `SekyroConfigError` with the new code `invalid_registry_record`.
|
|
40
|
+
- 8425e95: Add `sekyro.subjects` — `get(id)`, `list({ externalRef })`, `revoke(id, { reason })` and `delete(id)` (reads need the `subjects:read` scope, revoke / delete the new `subjects:write` scope). Type returning-user sessions (`subject.reuse` at create with exactly one of `subjectId`, `externalRef` or `nni`; `returning: true` + `reuseOf` on the session view and on the session webhook data), the `subject.enrolled` and `subject.deleted` webhook data, and the revocation form of `subject.updated` (`status: 'revoked'`, no session).
|
|
41
|
+
- b6df220: Docs: `sessions.create` in a `supplied` environment now documents `422 supplied_identity_required` (identity data not sent), and that the face on the supplied photo is checked after consent, at the face match (`no_face_reference`), not at create.
|
|
42
|
+
- 471d833: ICAO `id_document` portrait mode (SDK spec §4.9), types and partner helper. New check type `portrait_icao` with its reasons (`PortraitIcaoReason`: `icao_pose`, `icao_eyes`, `icao_mouth`, `icao_occlusion`, `icao_lighting`, `icao_background`, `icao_resolution`, `portrait_not_from_liveness`, all retryable), the per-criterion report (`IcaoReportEntry` on `HostedStepView.icao` and `SessionStepView.report`), `HostedSessionView.portrait` and the `PortraitIcaoInput` submission shape. `@sekyro/node`: `sessions.portrait(id)` returns a 5-minute link to the 480×640 portrait (scope `portrait:read`), `Session.portraitAvailable` and the same flag on the terminal and `review_decided` webhook data. Web behaviour is unchanged: the browser ICAO gate ships later, so the web step model renders `portrait_icao` as "update required".
|
|
43
|
+
- 8425e95: Face-match probe binding (SDK spec §4.10): the server matches the face_match probe against face crops of the scored liveness clip. New retryable, billed `face_match` reason `probe_not_from_liveness` (the face image isn't the person in the liveness clip). `@sekyro/web`: in `FaceMatchReason` and the retryable set (the runner re-captures). `@sekyro/react`: ar/fr/en retry copy "Keep your face in the frame the whole time, then try again"; bundle budget 24 → 25 KiB. `@sekyro/node`: `ProbeBindingReason`. No capture or upload change on any platform.
|
|
44
|
+
- 6b9a2f9: `SessionStep.probeQuality` (new `ProbeQuality` type) on `face_match` steps from `sessions.get` / `sessions.list`: the selfie's measured occlusion, head pose, eye openness and sharpness. Informational only, it never changes the step's state or reason, and it is absent until the platform enables the measurement.
|
|
45
|
+
- 45f639f: Licence: the SDKs are now under the Sekyro AI SDK License of Ibtikar Technologies, Inc. (https://sekyro.ai/en/legal/sdk-license, also in French and Arabic). `package.json#license` is `SEE LICENSE IN LICENSE`, and each package's `LICENSE` summarises the licence and links to it. `@sekyro/web` now also ships `THIRD-PARTY-NOTICES.txt` with the MIT notice of the bundled scanic document detector and the MIT/NCSA notice of the Emscripten glue in its OCR worker.
|
|
46
|
+
|
|
3
47
|
## 0.1.0-rc.3
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
package/LICENSE
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
Sekyro AI SDK
|
|
1
|
+
Sekyro AI SDK License (proprietary)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Ibtikar Technologies. All rights reserved.
|
|
3
|
+
Copyright (c) 2026 Ibtikar Technologies, Inc. All rights reserved.
|
|
4
4
|
|
|
5
|
-
This
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
This SDK is proprietary software of Ibtikar Technologies, Inc., licensed
|
|
6
|
+
only under the Sekyro AI SDK License:
|
|
7
|
+
https://sekyro.ai/en/legal/sdk-license
|
|
8
|
+
(in French: https://sekyro.ai/fr/legal/sdk-license,
|
|
9
|
+
in Arabic: https://sekyro.ai/ar/legal/sdk-license).
|
|
10
10
|
|
|
11
|
-
|
|
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.
|
|
11
|
+
In short, and subject to the full license:
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
- You may use the SDK only with a Sekyro AI account, a customer agreement
|
|
14
|
+
with Ibtikar Technologies, Inc. and valid API keys, to integrate the
|
|
15
|
+
Sekyro AI services into your own applications. You may distribute it to
|
|
16
|
+
your end users only as embedded in those applications.
|
|
17
|
+
- You may not use it with another identity verification provider, modify it
|
|
18
|
+
beyond the configuration and theming it exposes, reverse engineer,
|
|
19
|
+
decompile or disassemble it (except where the law expressly allows it),
|
|
20
|
+
circumvent its security, attestation, license checks or usage metering,
|
|
21
|
+
or distribute or sublicense it on its own.
|
|
22
|
+
- Open-source components keep their own licenses. See the third-party
|
|
23
|
+
notices shipped with this SDK.
|
|
24
|
+
- The SDK is provided "as is", without warranty, except as your customer
|
|
25
|
+
agreement provides. The license is governed by the laws of the State of
|
|
26
|
+
Delaware, USA. If this license and your customer agreement conflict,
|
|
27
|
+
the customer agreement prevails.
|
|
28
|
+
|
|
29
|
+
Questions: contact@sekyro.ai
|
package/README.md
CHANGED
|
@@ -5,26 +5,29 @@ 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
|
|
|
14
14
|
The SDK does **not** orchestrate checks or retry for you; the partner owns the
|
|
15
15
|
retry policy (`SekyroApiError.retryable` tells you when a retry is sensible).
|
|
16
16
|
|
|
17
|
-
**Integration guide:** [
|
|
17
|
+
**Integration guide:** [Web SDK guide](https://docs.sekyro.ai/en/web/embedded-sdk)
|
|
18
18
|
(server-bound NNI, display vs authoritative results, webhook receivers for
|
|
19
19
|
Next.js and Express, billing). The hosted-flow path is in the
|
|
20
|
-
[quickstart](
|
|
20
|
+
[quickstart](https://docs.sekyro.ai/en/getting-started/quickstart).
|
|
21
21
|
|
|
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,49 +35,135 @@ 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 |
|
|
86
|
+
|
|
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 }]
|
|
90
|
+
|
|
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:
|
|
71
117
|
|
|
72
|
-
|
|
73
|
-
|
|
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`).
|
|
74
126
|
|
|
75
|
-
`sekyro.sessions` is a `SessionsApi
|
|
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
|
|
|
130
|
+
### Partner-supplied identity data (`supplied` mode)
|
|
131
|
+
|
|
132
|
+
If Sekyro AI set your environment to the `supplied` mode, you send the
|
|
133
|
+
person's identity data (biographic record and photo) with the session in
|
|
134
|
+
`input.suppliedIdentity`. Sekyro AI treats it as identity data supplied by
|
|
135
|
+
your organization: it makes no registry call, and it never receives or stores
|
|
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:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { registryFromAnrpts } from "@sekyro/node";
|
|
142
|
+
|
|
143
|
+
const answer = await myRegistryClient.lookup(nni); // your call, your key
|
|
144
|
+
const receivedAt = new Date();
|
|
145
|
+
await sekyro.sessions.create({
|
|
146
|
+
// fetchedAt: when YOUR system received this answer (required; > 24 h old is refused)
|
|
147
|
+
input: { nni, suppliedIdentity: registryFromAnrpts(answer, { fetchedAt: receivedAt }) },
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`suppliedIdentity.nni` must equal `input.nni`. Refusals:
|
|
152
|
+
`403 supplied_identity_not_entitled` (the environment is not in `supplied` mode),
|
|
153
|
+
`422 supplied_identity_required` (the environment is in `supplied` mode and the app
|
|
154
|
+
verifies identity, but `input.suppliedIdentity` was not sent),
|
|
155
|
+
`400 supplied_identity_nni_mismatch`, `422 supplied_identity_stale` (obtained more
|
|
156
|
+
than 24 h ago) and `422 supplied_identity_photo_unusable` (not a JPEG or PNG of at
|
|
157
|
+
most 1 MB within the size bounds).
|
|
158
|
+
|
|
159
|
+
No face processing runs at create. The face on the supplied photo is checked after
|
|
160
|
+
the person consents, at the face match: a photo without one usable face fails that
|
|
161
|
+
step with `no_face_reference` (or `low_quality_reference`). That step runs after liveness, so check
|
|
162
|
+
that the photo shows exactly one clear face before you send it.
|
|
163
|
+
|
|
164
|
+
Partner-supplied identity data is not billed: the identity step in `supplied` mode
|
|
165
|
+
records no usage event (other steps, such as the face match, bill as usual).
|
|
166
|
+
|
|
78
167
|
### Errors
|
|
79
168
|
|
|
80
169
|
Every non-2xx response, network failure and timeout throws `SekyroApiError`:
|
|
@@ -85,6 +174,7 @@ Every non-2xx response, network failure and timeout throws `SekyroApiError`:
|
|
|
85
174
|
| `code` | API error code, or `network` / `timeout` / `http_error` / `malformed_response` |
|
|
86
175
|
| `message` | human-readable, from the API when available |
|
|
87
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()` |
|
|
88
178
|
| `retryable` | `true` for `network`, `timeout`, `429`, `5xx` and `idempotency_in_progress` |
|
|
89
179
|
|
|
90
180
|
```ts
|
|
@@ -99,6 +189,65 @@ try {
|
|
|
99
189
|
}
|
|
100
190
|
```
|
|
101
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
|
+
|
|
223
|
+
## Subjects
|
|
224
|
+
|
|
225
|
+
A **subject** is the same person across all of your apps. Reads need the
|
|
226
|
+
`subjects:read` scope; `revoke` and `delete` need `subjects:write` (keys minted
|
|
227
|
+
from the dashboard carry both; mint a new key if an older one answers
|
|
228
|
+
`403 insufficient_scope`).
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const subject = await sekyro.subjects.get(subjectId);
|
|
232
|
+
subject.status; // 'active' | 'revoked'
|
|
233
|
+
subject.reference; // { source: 'registry' | 'doc_registry' | 'chip' | 'document', capturedAt } | null
|
|
234
|
+
subject.assurance; // 'registry_biometric' | 'chip_biometric' | 'document_biometric' | 'registry_only' | 'document_only' | null
|
|
235
|
+
subject.returningReady; // a returning-user session can be created now
|
|
236
|
+
|
|
237
|
+
const [byRef] = await sekyro.subjects.list({ externalRef: userId });
|
|
238
|
+
|
|
239
|
+
// Returning user: liveness + face match against the stored reference photo.
|
|
240
|
+
// Name exactly one of subjectId, externalRef or nni.
|
|
241
|
+
await sekyro.sessions.create({ subject: { reuse: { subjectId } } });
|
|
242
|
+
|
|
243
|
+
await sekyro.subjects.revoke(subjectId, { reason: "account closed" }); // idempotent
|
|
244
|
+
await sekyro.subjects.delete(subjectId); // photos, face templates and identifiers; cannot be undone
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`subject.enrolled` (first reference photo stored), `subject.updated` (a linked
|
|
248
|
+
session finished, or `status: 'revoked'`) and `subject.deleted` webhooks are
|
|
249
|
+
typed on the envelope.
|
|
250
|
+
|
|
102
251
|
## Webhooks
|
|
103
252
|
|
|
104
253
|
Verify with the **raw** request body (bytes as received, before any JSON
|
|
@@ -120,13 +269,13 @@ app.post("/webhooks/sekyro", express.raw({ type: "application/json" }), (req, re
|
|
|
120
269
|
switch (envelope.event) {
|
|
121
270
|
case "session.completed":
|
|
122
271
|
case "session.failed":
|
|
123
|
-
envelope.data; // SessionLifecycleEventData
|
|
272
|
+
envelope.data; // SessionLifecycleEventData: { sessionId, status, outcome, flow, steps, … }
|
|
124
273
|
break;
|
|
125
274
|
case "session.review_decided":
|
|
126
|
-
envelope.data; // SessionReviewDecidedEventData
|
|
275
|
+
envelope.data; // SessionReviewDecidedEventData: { sessionId, outcome: 'pass' | 'fail', reason, flow }
|
|
127
276
|
break;
|
|
128
277
|
case "session.step.completed":
|
|
129
|
-
envelope.data; // SessionStepCompletedEventData
|
|
278
|
+
envelope.data; // SessionStepCompletedEventData: { sessionId, checkType, state, nodeId, service, flow }
|
|
130
279
|
break;
|
|
131
280
|
}
|
|
132
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,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});
|
|
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.5";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});
|