@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 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 — Proprietary License
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 software (the "SDK") is proprietary to Ibtikar Technologies. Use is
6
- permitted only under a Sekyro AI customer agreement with Ibtikar Technologies, and
7
- only to integrate the customer's own applications with the Sekyro AI
8
- identity-verification service. For that purpose you may install the SDK
9
- from the public npm registry and run it unmodified.
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
- No other right or license, express or implied, is granted. You may not
12
- modify, reverse-engineer, redistribute, sublicense, resell or create
13
- derivative works of the SDK, nor remove or alter this notice. The source
14
- code is published for transparency (build provenance, security review); its
15
- publication grants no additional rights.
11
+ In short, and subject to the full license:
16
12
 
17
- THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- IMPLIED. TO THE EXTENT PERMITTED BY LAW, IBTIKAR TECHNOLOGIES SHALL NOT BE
19
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE
20
- SDK, EXCEPT AS PROVIDED IN THE APPLICABLE CUSTOMER AGREEMENT.
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** — create a verification session from your backend and read its
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** — verify the `x-sekyro-signature` header on deliveries (the
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:** [docs/partner/web-sdk.md](../../docs/partner/web-sdk.md)
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](../../docs/partner/quickstart.md).
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 the staging origin for sandbox work
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: — plain http: is accepted only for
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 — returns the one-time client token your front end hands to @sekyro/web.
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: "1357924632" } },
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 — the whitelisted session view (never contains the client token).
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.steps; // [{ checkType, state, reason?, calibratedScore? }]
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
- There is no recipe argument. Your API key belongs to an **app**, and the app's
62
- eKYC profile (set in the Sekyro AI dashboard) decides which checks run. Every
63
- field is optional:
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 profile's link range (`400 ttl_out_of_policy` otherwise); omit for the profile default |
69
- | `input` | the step anchors: `nni`, `phone`, `fullName`, `birthDate`, all **strings** (an NNI is a 10-digit string, never a number), plus any further keys; `input.nni` binds the session to that identity |
70
- | `deliver` | `{ channel: "sms" \| "whatsapp", to: "+222…" }`: also send the hosted link to the person |
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
- > **0.2 breaking change.** `flow` and `templateId` are gone. Create an app in
73
- > the dashboard, mint an app API key, and drop `flow` from your calls.
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` — exported as a type only; you get the
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 — { sessionId, status, outcome, steps }
272
+ envelope.data; // SessionLifecycleEventData: { sessionId, status, outcome, flow, steps, … }
124
273
  break;
125
274
  case "session.review_decided":
126
- envelope.data; // SessionReviewDecidedEventData — { sessionId, outcome: 'pass' | 'fail', reason }
275
+ envelope.data; // SessionReviewDecidedEventData: { sessionId, outcome: 'pass' | 'fail', reason, flow }
127
276
  break;
128
277
  case "session.step.completed":
129
- envelope.data; // SessionStepCompletedEventData — { sessionId, checkType, state }
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});