@sekyro/node 0.1.0-rc.4 → 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,21 @@
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
+
3
19
  ## 0.1.0-rc.4
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -5,9 +5,9 @@ dependencies, Node.js ≥ 20, ESM + CommonJS, typed.
5
5
 
6
6
  Two jobs, nothing else:
7
7
 
8
- 1. **Sessions** — 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
 
@@ -22,9 +22,12 @@ Next.js and Express, billing). The hosted-flow path is in the
22
22
  ## Install
23
23
 
24
24
  ```sh
25
- npm install @sekyro/node
25
+ npm install @sekyro/node@next
26
26
  ```
27
27
 
28
+ The SDK is licensed under the
29
+ [Sekyro AI SDK License](https://sekyro.ai/legal/sdk-license).
30
+
28
31
  ## Sessions
29
32
 
30
33
  ```ts
@@ -32,47 +35,96 @@ import { Sekyro } from "@sekyro/node";
32
35
 
33
36
  const sekyro = new Sekyro({
34
37
  apiKey: process.env.SEKYRO_API_KEY!,
35
- // baseUrl: "https://api.sekyro.ai/api/v2", // default; use 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 |
71
86
 
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.
87
+ ```ts
88
+ // The app's flows (published only, default first): pick one at runtime.
89
+ const flows = await sekyro.listFlows(); // [{ id, slug, name, version, isDefault }]
74
90
 
75
- `sekyro.sessions` is a `SessionsApi` — exported as a type only; you get the
91
+ await sekyro.sessions.create({
92
+ flow: "foreigners",
93
+ overrides: {
94
+ settings: { eligibility: { minAge: 21 } }, // general settings
95
+ services: { liveness: { attempts: 2 } }, // per-service settings
96
+ },
97
+ });
98
+ ```
99
+
100
+ `overrides.settings` changes the flow's general settings (eligibility,
101
+ decisions, data consistency, returning users, documents); `overrides.services`
102
+ changes a service's settings, keyed by service id (`liveness`, `phone_otp`,
103
+ `device_network`, …), or removes it with `'off'`. An override may only make
104
+ the flow stricter unless the flow allows widening; turning a service `'off'`
105
+ always widens. The changed paths come back on `session.overriddenFields`.
106
+
107
+ > **Sekyro flows breaking change.** The `profile` override is gone: use
108
+ > `overrides`. `flow` is accepted again, now as the app's flow slug or id.
109
+
110
+ The session view also carries `failureReason` (a policy stop such as
111
+ `location_not_allowed` or `underage`), `overriddenFields`, `riskSignals`,
112
+ `subjectId` / `externalRef`, `verifiedContacts`, `eid`, and the end user's
113
+ `network` and `device`. A step with `handled: true` failed, but the flow's
114
+ fallback took over.
115
+
116
+ Two more session calls:
117
+
118
+ - `sekyro.sessions.selectMethod(id, method)` chooses the identity method of a
119
+ session on the person's behalf (`POST /sessions/:id/method`): `409 choice_locked`
120
+ once the flow's identity choice can no longer change, `409 choice_option_unavailable`
121
+ for a method the flow does not offer this session, `409 choice_not_found` when the
122
+ flow has no identity choice.
123
+ - `sekyro.sessions.portrait(id)` returns a 5-minute link to the ICAO portrait of
124
+ a session whose `portrait_icao` check passed (`portraitAvailable`; scope
125
+ `portrait:read`).
126
+
127
+ `sekyro.sessions` is a `SessionsApi`, exported as a type only: you get the
76
128
  instance from the client, never by constructing one.
77
129
 
78
130
  ### Partner-supplied identity data (`supplied` mode)
@@ -81,17 +133,18 @@ If Sekyro AI set your environment to the `supplied` mode, you send the
81
133
  person's identity data (biographic record and photo) with the session in
82
134
  `input.suppliedIdentity`. Sekyro AI treats it as identity data supplied by
83
135
  your organization: it makes no registry call, and it never receives or stores
84
- any registry key. If your data comes from your own ANRPTS call,
85
- `registryFromAnrpts()` converts that answer locally into the record shape:
136
+ any registry key. If your data comes from your own call to the national
137
+ population registry, `registryFromAnrpts()` converts that answer locally into
138
+ the record shape:
86
139
 
87
140
  ```ts
88
141
  import { registryFromAnrpts } from "@sekyro/node";
89
142
 
90
- const anrpts = await myAnrptsClient.getPersonne(nni); // your call, your key
143
+ const answer = await myRegistryClient.lookup(nni); // your call, your key
91
144
  const receivedAt = new Date();
92
145
  await sekyro.sessions.create({
93
146
  // fetchedAt: when YOUR system received this answer (required; > 24 h old is refused)
94
- input: { nni, suppliedIdentity: registryFromAnrpts(anrpts, { fetchedAt: receivedAt }) },
147
+ input: { nni, suppliedIdentity: registryFromAnrpts(answer, { fetchedAt: receivedAt }) },
95
148
  });
96
149
  ```
97
150
 
@@ -121,6 +174,7 @@ Every non-2xx response, network failure and timeout throws `SekyroApiError`:
121
174
  | `code` | API error code, or `network` / `timeout` / `http_error` / `malformed_response` |
122
175
  | `message` | human-readable, from the API when available |
123
176
  | `requestId` | the API's `request_id`, quote it to support |
177
+ | `details` | the API's `error.details` when sent, else `undefined`; typed for the flow codes through `isFlowError()` |
124
178
  | `retryable` | `true` for `network`, `timeout`, `429`, `5xx` and `idempotency_in_progress` |
125
179
 
126
180
  ```ts
@@ -135,11 +189,42 @@ try {
135
189
  }
136
190
  ```
137
191
 
192
+ Flow codes (`SekyroFlowErrorCode`) carry machine-readable `details`:
193
+ `details.issues[]` (`{ code, message, path?, nodeId?, service?, field? }`,
194
+ never a value you sent), plus `fields` on `override_widens_flow` and `flow`
195
+ (`{ id, slug }`) on `flow_not_published`, `flow_archived` and `reuse_disabled`.
196
+ `isFlowError(err, ...codes)` narrows `err.details` to the code's shape; it
197
+ returns `false` when the details are missing or not in that shape, so read
198
+ `err.code` as the fallback.
199
+
200
+ | code | from | `details` |
201
+ |---|---|---|
202
+ | `404 flow_not_found` | `sessions.create` | `issues` (`path: "flow"`) |
203
+ | `409 flow_not_published`, `409 flow_archived` | `sessions.create` | `issues`, `flow` |
204
+ | `400 override_invalid` | `sessions.create` | `issues`, one per problem (`path: "overrides.services.liveness.attempts"`, …) |
205
+ | `403 override_widens_flow` | `sessions.create` | `issues` (`field_widens_flow`), `fields` |
206
+ | `409 reuse_disabled` | `sessions.create` with `subject.reuse` | `issues`, `flow` |
207
+ | `409 choice_not_found`, `choice_locked`, `choice_option_unavailable` | `sessions.selectMethod` | `issues` |
208
+
209
+ ```ts
210
+ import { isFlowError } from "@sekyro/node";
211
+
212
+ try {
213
+ await sekyro.sessions.create({ flow: "foreigners", overrides });
214
+ } catch (err) {
215
+ if (isFlowError(err, "override_widens_flow")) {
216
+ console.warn("widening refused", err.details.fields);
217
+ } else if (isFlowError(err, "override_invalid")) {
218
+ for (const issue of err.details.issues) console.warn(issue.path, issue.message);
219
+ } else throw err;
220
+ }
221
+ ```
222
+
138
223
  ## Subjects
139
224
 
140
225
  A **subject** is the same person across all of your apps. Reads need the
141
226
  `subjects:read` scope; `revoke` and `delete` need `subjects:write` (keys minted
142
- from the dashboard carry both — mint a new key if an older one answers
227
+ from the dashboard carry both; mint a new key if an older one answers
143
228
  `403 insufficient_scope`).
144
229
 
145
230
  ```ts
@@ -184,13 +269,13 @@ app.post("/webhooks/sekyro", express.raw({ type: "application/json" }), (req, re
184
269
  switch (envelope.event) {
185
270
  case "session.completed":
186
271
  case "session.failed":
187
- envelope.data; // SessionLifecycleEventData — { sessionId, status, outcome, steps }
272
+ envelope.data; // SessionLifecycleEventData: { sessionId, status, outcome, flow, steps, … }
188
273
  break;
189
274
  case "session.review_decided":
190
- envelope.data; // SessionReviewDecidedEventData — { sessionId, outcome: 'pass' | 'fail', reason }
275
+ envelope.data; // SessionReviewDecidedEventData: { sessionId, outcome: 'pass' | 'fail', reason, flow }
191
276
  break;
192
277
  case "session.step.completed":
193
- envelope.data; // SessionStepCompletedEventData — { sessionId, checkType, state }
278
+ envelope.data; // SessionStepCompletedEventData: { sessionId, checkType, state, nodeId, service, flow }
194
279
  break;
195
280
  }
196
281
  res.sendStatus(200);
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var src_exports={};__export(src_exports,{DEFAULT_BASE_URL:()=>DEFAULT_BASE_URL,DEFAULT_TIMEOUT_MS:()=>DEFAULT_TIMEOUT_MS,LEGACY_SIGNATURE_HEADER:()=>LEGACY_SIGNATURE_HEADER,SEKYRO_SIGNATURE_HEADER:()=>SEKYRO_SIGNATURE_HEADER,Sekyro:()=>Sekyro,SekyroApiError:()=>SekyroApiError,SekyroConfigError:()=>SekyroConfigError,SekyroWebhookError:()=>SekyroWebhookError,registryFromAnrpts:()=>registryFromAnrpts,signatureFromHeaders:()=>signatureFromHeaders,verifyWebhook:()=>verifyWebhook});module.exports=__toCommonJS(src_exports);var WEBHOOK_MESSAGES={invalid_signature:"No v1 signature in the header matches this body and secret",stale:"Delivery timestamp is outside the tolerance window",malformed:"Signature header or body is not in the expected format"},SekyroWebhookError=class extends Error{name="SekyroWebhookError";code;constructor(code,message=WEBHOOK_MESSAGES[code]){super(message),this.code=code}},CONFIG_MESSAGES={missing_api_key:"Sekyro AI: `apiKey` is required",invalid_base_url:"Sekyro AI: `baseUrl` must be an https: URL (http: is allowed only for localhost, 127.0.0.1 or [::1])",invalid_registry_record:"Sekyro AI: the ANRPTS answer can't be used as a registry record"},SekyroConfigError=class extends TypeError{name="SekyroConfigError";code;constructor(code,message=CONFIG_MESSAGES[code]){super(message),this.code=code}},SekyroApiError=class extends Error{name="SekyroApiError";status;code;requestId;retryable;constructor(init){super(init.message,init.cause===void 0?void 0:{cause:init.cause}),this.status=init.status,this.code=init.code,this.requestId=init.requestId,this.retryable=isRetryable(init.status,init.code)}};function isRetryable(status,code){return code==="network"||code==="timeout"||code==="idempotency_in_progress"||status===429?!0:status>=500&&status<=599}var SDK_VERSION="0.1.0-rc.4";var USER_AGENT=`@sekyro/node/${SDK_VERSION}`,DEFAULT_BASE_URL="https://api.sekyro.ai/api/v2",DEFAULT_TIMEOUT_MS=3e4;function isSuccessEnvelope(value){return typeof value=="object"&&value!==null&&value.success===!0&&"data"in value}function isErrorEnvelope(value){if(typeof value!="object"||value===null)return!1;let v=value;if(v.status!=="error"||typeof v.error!="object"||v.error===null)return!1;let e=v.error;return typeof e.code=="string"&&typeof e.message=="string"}function isAbortLike(err){if(typeof err!="object"||err===null)return!1;let name=err.name;return name==="TimeoutError"||name==="AbortError"}var LOOPBACK_HOSTS=new Set(["localhost","127.0.0.1","[::1]"]);function assertSafeBaseUrl(baseUrl){let url;try{url=new URL(baseUrl)}catch{throw new SekyroConfigError("invalid_base_url")}if(url.protocol!=="https:"&&!(url.protocol==="http:"&&LOOPBACK_HOSTS.has(url.hostname)))throw new SekyroConfigError("invalid_base_url")}var Transport=class{apiKey;baseUrl;timeoutMs;fetchImpl;constructor(opts){let baseUrl=(opts.baseUrl??DEFAULT_BASE_URL).replace(/\/+$/,"");assertSafeBaseUrl(baseUrl),this.apiKey=opts.apiKey,this.baseUrl=baseUrl,this.timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS,this.fetchImpl=opts.fetch??globalThis.fetch}async call(method,path,body,idempotencyKey){let headers={authorization:`Bearer ${this.apiKey}`,accept:"application/json","user-agent":USER_AGENT};body!==void 0&&(headers["content-type"]="application/json"),idempotencyKey!==void 0&&(headers["x-idempotency-key"]=idempotencyKey);let init={method,headers,signal:AbortSignal.timeout(this.timeoutMs)};body!==void 0&&(init.body=JSON.stringify(body));let res;try{res=await this.fetchImpl(`${this.baseUrl}${path}`,init)}catch(err){throw isAbortLike(err)?new SekyroApiError({status:0,code:"timeout",message:`Request timed out after ${this.timeoutMs}ms`,cause:err}):new SekyroApiError({status:0,code:"network",message:`Network error: ${err instanceof Error?err.message:String(err)}`,cause:err})}let text2=await res.text(),parsed,parseError;try{parsed=text2.length>0?JSON.parse(text2):void 0}catch(err){parseError=err}if(!res.ok){if(isErrorEnvelope(parsed)){let init2={status:res.status,code:parsed.error.code,message:parsed.error.message};throw parsed.error.request_id!==void 0&&(init2.requestId=parsed.error.request_id),new SekyroApiError(init2)}throw new SekyroApiError({status:res.status,code:"http_error",message:`HTTP ${res.status}${res.statusText?` ${res.statusText}`:""}`})}if(!isSuccessEnvelope(parsed))throw new SekyroApiError({status:res.status,code:"malformed_response",message:"Response is not a `{ success: true, data }` envelope",cause:parseError});return parsed.data}},SessionsApi=class{constructor(call){this.call=call}call;async create(input={},opts={}){let wire=await this.call("POST","/sessions",input,opts.idempotencyKey);return{id:wire.id,status:wire.status,clientToken:wire.client_token,hostedUrl:wire.hosted_url,steps:wire.steps,expiresAt:wire.expiresAt}}async selectMethod(id,method){if(typeof id!="string"||id.length===0)throw new TypeError("sessions.selectMethod: `id` must be a non-empty string");return this.call("POST",`/sessions/${encodeURIComponent(id)}/method`,{method})}async get(id){if(typeof id!="string"||id.length===0)throw new TypeError("sessions.get: `id` must be a non-empty string");return this.call("GET",`/sessions/${encodeURIComponent(id)}`)}async portrait(id){if(typeof id!="string"||id.length===0)throw new TypeError("sessions.portrait: `id` must be a non-empty string");return this.call("GET",`/sessions/${encodeURIComponent(id)}/portrait?format=url`)}};function assertId(method,id){if(typeof id!="string"||id.length===0)throw new TypeError(`subjects.${method}: \`id\` must be a non-empty string`)}var SubjectsApi=class{constructor(call){this.call=call}call;async get(id){return assertId("get",id),this.call("GET",`/subjects/${encodeURIComponent(id)}`)}async list(query){if(typeof query?.externalRef!="string"||query.externalRef.length===0)throw new TypeError("subjects.list: `externalRef` must be a non-empty string");let qs=new URLSearchParams({externalRef:query.externalRef});return this.call("GET",`/subjects?${qs.toString()}`)}async revoke(id,input={},opts={}){return assertId("revoke",id),this.call("POST",`/subjects/${encodeURIComponent(id)}/revoke`,input,opts.idempotencyKey)}async delete(id){return assertId("delete",id),this.call("DELETE",`/subjects/${encodeURIComponent(id)}`)}},Sekyro=class{sessions;subjects;constructor(opts){if(typeof opts?.apiKey!="string"||opts.apiKey.length===0)throw new SekyroConfigError("missing_api_key");let transport=new Transport(opts);this.sessions=new SessionsApi(transport.call.bind(transport)),this.subjects=new SubjectsApi(transport.call.bind(transport))}};function registryFromAnrpts(response,options){let p=response?.personne??{},required=(field,value)=>{let s=text(value);if(s===void 0)throw new SekyroConfigError("invalid_registry_record",`Sekyro AI: the ANRPTS answer has no usable \`${field}\``);return s},birthDate=isoDay(p.dateNaissance);if(birthDate===void 0)throw new SekyroConfigError("invalid_registry_record","Sekyro AI: the ANRPTS answer has no usable `personne.dateNaissance`");let photo=required("photo",response?.photo).replace(/\s+/g,""),fetchedAt=new Date(options?.fetchedAt??Number.NaN);if(Number.isNaN(fetchedAt.getTime()))throw new SekyroConfigError("invalid_registry_record","Sekyro AI: `fetchedAt` (when your system received the ANRPTS answer) is missing or not a valid date");let record={nni:required("personne.nni",p.nni),firstNameFr:required("personne.prenomFr",p.prenomFr),lastNameFr:required("personne.patronymeFr",p.patronymeFr),firstNameAr:required("personne.prenomAr",p.prenomAr),lastNameAr:required("personne.patronymeAr",p.patronymeAr),birthDate,photo,fetchedAt:fetchedAt.toISOString()},optional=[["fatherNameFr",p.perePrenomFr],["fatherNameAr",p.perePrenomAr],["birthPlaceFr",p.lieuNaissanceFr],["birthPlaceAr",p.lieuNaissanceAr]];for(let[key,value]of optional){let s=text(value);s!==void 0&&(record[key]=s)}let sex=text(p.sexeCode)?.toUpperCase();(sex==="M"||sex==="F")&&(record.sex=sex);let nationalities=Array.isArray(p.nationalities)?p.nationalities.map(text).filter(n=>n!==void 0).slice(0,5):[];nationalities.length>0&&(record.nationalities=nationalities);let photoDate=isoDay(response?.datePhoto);return photoDate!==void 0&&(record.photoDate=photoDate),record}function text(value){if(typeof value!="string")return;let s=value.trim();return s.length>0?s:void 0}function isoDay(value){let s=text(value);if(s===void 0)return;let iso=/^(\d{4})-(\d{2})-(\d{2})(?:$|T)/.exec(s),fr=/^(\d{2})\/(\d{2})\/(\d{4})$/.exec(s),[y,m,d]=iso?[iso[1],iso[2],iso[3]]:fr?[fr[3],fr[2],fr[1]]:[];if(y===void 0||m===void 0||d===void 0)return;let t=new Date(Date.UTC(Number(y),Number(m)-1,Number(d)));return t.getUTCFullYear()===Number(y)&&t.getUTCMonth()===Number(m)-1&&t.getUTCDate()===Number(d)?`${y}-${m}-${d}`:void 0}var import_node_crypto=require("crypto");var SEKYRO_SIGNATURE_HEADER="x-sekyro-signature",LEGACY_SIGNATURE_HEADER="x-ibtikar-signature";function signatureFromHeaders(headers){return readHeader(headers,SEKYRO_SIGNATURE_HEADER)??readHeader(headers,LEGACY_SIGNATURE_HEADER)}function readHeader(headers,name){if(typeof headers.get=="function"){let v2=headers.get(name);return v2===null||v2===""?void 0:v2}let record=headers,key=Object.keys(record).find(k=>k.toLowerCase()===name);if(key===void 0)return;let raw=record[key],v=Array.isArray(raw)?raw[0]:raw;return v===void 0||v===""?void 0:v}var HEX64=/^[0-9a-f]{64}$/i;function verifyWebhook(rawBody,header,secret,opts={}){if(!header||!secret)throw new SekyroWebhookError("invalid_signature");let parts=header.split(",").map(part=>{let trimmed=part.trim(),eq=trimmed.indexOf("=");return eq===-1?[trimmed,""]:[trimmed.slice(0,eq).trim(),trimmed.slice(eq+1).trim()]}),tRaw=parts.find(([k])=>k==="t")?.[1],t=tRaw!==void 0&&/^\d+$/.test(tRaw)?Number(tRaw):Number.NaN,sigs=parts.filter(([k])=>k==="v1").map(([,v])=>v);if(!Number.isSafeInteger(t)||sigs.length===0)throw new SekyroWebhookError("malformed");let nowSec=Math.floor((opts.now?.()??Date.now())/1e3),tolerance=opts.toleranceSec??300;if(Math.abs(nowSec-t)>tolerance)throw new SekyroWebhookError("stale");let expected=(0,import_node_crypto.createHmac)("sha256",secret).update(`${t}.${rawBody}`).digest();if(!sigs.some(hex=>{if(!HEX64.test(hex))return!1;let got=Buffer.from(hex,"hex");return got.length===expected.length&&(0,import_node_crypto.timingSafeEqual)(got,expected)}))throw new SekyroWebhookError("invalid_signature");let parsed;try{parsed=JSON.parse(rawBody)}catch{throw new SekyroWebhookError("malformed","Signed body is not valid JSON")}if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))throw new SekyroWebhookError("malformed","Signed body is not a JSON object");return parsed}0&&(module.exports={DEFAULT_BASE_URL,DEFAULT_TIMEOUT_MS,LEGACY_SIGNATURE_HEADER,SEKYRO_SIGNATURE_HEADER,Sekyro,SekyroApiError,SekyroConfigError,SekyroWebhookError,registryFromAnrpts,signatureFromHeaders,verifyWebhook});
1
+ "use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var src_exports={};__export(src_exports,{DEFAULT_BASE_URL:()=>DEFAULT_BASE_URL,DEFAULT_TIMEOUT_MS:()=>DEFAULT_TIMEOUT_MS,LEGACY_SIGNATURE_HEADER:()=>LEGACY_SIGNATURE_HEADER,SEKYRO_SIGNATURE_HEADER:()=>SEKYRO_SIGNATURE_HEADER,Sekyro:()=>Sekyro,SekyroApiError:()=>SekyroApiError,SekyroConfigError:()=>SekyroConfigError,SekyroWebhookError:()=>SekyroWebhookError,isFlowError:()=>isFlowError,registryFromAnrpts:()=>registryFromAnrpts,signatureFromHeaders:()=>signatureFromHeaders,verifyWebhook:()=>verifyWebhook});module.exports=__toCommonJS(src_exports);var WEBHOOK_MESSAGES={invalid_signature:"No v1 signature in the header matches this body and secret",stale:"Delivery timestamp is outside the tolerance window",malformed:"Signature header or body is not in the expected format"},SekyroWebhookError=class extends Error{name="SekyroWebhookError";code;constructor(code,message=WEBHOOK_MESSAGES[code]){super(message),this.code=code}},CONFIG_MESSAGES={missing_api_key:"Sekyro AI: `apiKey` is required",invalid_base_url:"Sekyro AI: `baseUrl` must be an https: URL (http: is allowed only for localhost, 127.0.0.1 or [::1])",invalid_registry_record:"Sekyro AI: the ANRPTS answer can't be used as a registry record"},SekyroConfigError=class extends TypeError{name="SekyroConfigError";code;constructor(code,message=CONFIG_MESSAGES[code]){super(message),this.code=code}},SekyroApiError=class extends Error{name="SekyroApiError";status;code;requestId;details;retryable;constructor(init){super(init.message,init.cause===void 0?void 0:{cause:init.cause}),this.status=init.status,this.code=init.code,this.requestId=init.requestId,this.details=init.details,this.retryable=isRetryable(init.status,init.code)}},FLOW_ERROR_CODES=new Set(["flow_not_found","flow_not_published","flow_archived","override_invalid","override_widens_flow","reuse_disabled","choice_not_found","choice_locked","choice_option_unavailable"]),FLOW_REF_CODES=new Set(["flow_not_published","flow_archived","reuse_disabled"]),isRecord=x=>typeof x=="object"&&x!==null&&!Array.isArray(x),isStringArray=x=>Array.isArray(x)&&x.every(s=>typeof s=="string");function isIssue(x){return isRecord(x)&&typeof x.code=="string"&&typeof x.message=="string"}function hasFlowDetails(code,details){if(!isRecord(details)||!Array.isArray(details.issues)||!details.issues.every(isIssue))return!1;if(code==="override_widens_flow")return isStringArray(details.fields);if(FLOW_REF_CODES.has(code)){let flow=details.flow;return isRecord(flow)&&typeof flow.id=="string"&&typeof flow.slug=="string"}return!0}function isFlowError(err,...codes){return!(err instanceof SekyroApiError)||!FLOW_ERROR_CODES.has(err.code)||codes.length>0&&!codes.includes(err.code)?!1:hasFlowDetails(err.code,err.details)}function isRetryable(status,code){return code==="network"||code==="timeout"||code==="idempotency_in_progress"||status===429?!0:status>=500&&status<=599}var SDK_VERSION="0.1.0-rc.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});