@sekyro/node 0.1.0-rc.3 → 0.1.0-rc.4

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,33 @@
1
1
  # @sekyro/node
2
2
 
3
+ ## 0.1.0-rc.4
4
+
5
+ ### Minor Changes
6
+
7
+ - 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.
8
+
9
+ **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.
10
+ - 7a3f935: Phone OTP channel choice and the email OTP step.
11
+
12
+ - `@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`.
13
+ - `@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`).
14
+ - `@sekyro/node`: profile `phone.channels` / `phone.destinations`, `email` (`ProfileEmail`: `emailSource`, `disposable`), `attempts.email`, the `input.email` session anchor and the `email_otp` check type.
15
+ - 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.
16
+ - 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.
17
+
18
+ ### Patch Changes
19
+
20
+ - 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`.
21
+ - 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.
22
+ - 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).
23
+ - 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`.
24
+ - 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).
25
+ - 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.
26
+ - 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".
27
+ - 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.
28
+ - 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.
29
+ - 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.
30
+
3
31
  ## 0.1.0-rc.3
4
32
 
5
33
  ### 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
@@ -14,10 +14,10 @@ Two jobs, nothing else:
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
 
@@ -75,6 +75,42 @@ field is optional:
75
75
  `sekyro.sessions` is a `SessionsApi` — exported as a type only; you get the
76
76
  instance from the client, never by constructing one.
77
77
 
78
+ ### Partner-supplied identity data (`supplied` mode)
79
+
80
+ If Sekyro AI set your environment to the `supplied` mode, you send the
81
+ person's identity data (biographic record and photo) with the session in
82
+ `input.suppliedIdentity`. Sekyro AI treats it as identity data supplied by
83
+ 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:
86
+
87
+ ```ts
88
+ import { registryFromAnrpts } from "@sekyro/node";
89
+
90
+ const anrpts = await myAnrptsClient.getPersonne(nni); // your call, your key
91
+ const receivedAt = new Date();
92
+ await sekyro.sessions.create({
93
+ // fetchedAt: when YOUR system received this answer (required; > 24 h old is refused)
94
+ input: { nni, suppliedIdentity: registryFromAnrpts(anrpts, { fetchedAt: receivedAt }) },
95
+ });
96
+ ```
97
+
98
+ `suppliedIdentity.nni` must equal `input.nni`. Refusals:
99
+ `403 supplied_identity_not_entitled` (the environment is not in `supplied` mode),
100
+ `422 supplied_identity_required` (the environment is in `supplied` mode and the app
101
+ verifies identity, but `input.suppliedIdentity` was not sent),
102
+ `400 supplied_identity_nni_mismatch`, `422 supplied_identity_stale` (obtained more
103
+ than 24 h ago) and `422 supplied_identity_photo_unusable` (not a JPEG or PNG of at
104
+ most 1 MB within the size bounds).
105
+
106
+ No face processing runs at create. The face on the supplied photo is checked after
107
+ the person consents, at the face match: a photo without one usable face fails that
108
+ step with `no_face_reference` (or `low_quality_reference`). That step runs after liveness, so check
109
+ that the photo shows exactly one clear face before you send it.
110
+
111
+ Partner-supplied identity data is not billed: the identity step in `supplied` mode
112
+ records no usage event (other steps, such as the face match, bill as usual).
113
+
78
114
  ### Errors
79
115
 
80
116
  Every non-2xx response, network failure and timeout throws `SekyroApiError`:
@@ -99,6 +135,34 @@ try {
99
135
  }
100
136
  ```
101
137
 
138
+ ## Subjects
139
+
140
+ A **subject** is the same person across all of your apps. Reads need the
141
+ `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
143
+ `403 insufficient_scope`).
144
+
145
+ ```ts
146
+ const subject = await sekyro.subjects.get(subjectId);
147
+ subject.status; // 'active' | 'revoked'
148
+ subject.reference; // { source: 'registry' | 'doc_registry' | 'chip' | 'document', capturedAt } | null
149
+ subject.assurance; // 'registry_biometric' | 'chip_biometric' | 'document_biometric' | 'registry_only' | 'document_only' | null
150
+ subject.returningReady; // a returning-user session can be created now
151
+
152
+ const [byRef] = await sekyro.subjects.list({ externalRef: userId });
153
+
154
+ // Returning user: liveness + face match against the stored reference photo.
155
+ // Name exactly one of subjectId, externalRef or nni.
156
+ await sekyro.sessions.create({ subject: { reuse: { subjectId } } });
157
+
158
+ await sekyro.subjects.revoke(subjectId, { reason: "account closed" }); // idempotent
159
+ await sekyro.subjects.delete(subjectId); // photos, face templates and identifiers; cannot be undone
160
+ ```
161
+
162
+ `subject.enrolled` (first reference photo stored), `subject.updated` (a linked
163
+ session finished, or `status: 'revoked'`) and `subject.deleted` webhooks are
164
+ typed on the envelope.
165
+
102
166
  ## Webhooks
103
167
 
104
168
  Verify with the **raw** request body (bytes as received, before any JSON
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,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});