@sekyro/node 0.1.0-rc.2 → 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 +40 -0
- package/LICENSE +25 -16
- package/README.md +69 -5
- package/dist/index.cjs +1 -308
- package/dist/index.d.cts +864 -13
- package/dist/index.d.ts +864 -13
- package/dist/index.js +1 -272
- package/package.json +7 -10
- package/sbom.cdx.json +153 -0
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
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
|
+
|
|
31
|
+
## 0.1.0-rc.3
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- 4e7a4a3: Brand: the product is now **Sekyro AI** — package descriptions, READMEs, LICENSE text and the `@sekyro/node` configuration error messages say "Sekyro AI". No API change: package names, the `Sekyro` class, exports and header names are unchanged.
|
|
36
|
+
- c3699fd: Type the end user's `network` (approximate location, operator, VPN/Tor/hosting flags, start IP) and `device` (platform, OS, browser, per-org device keys, signals) on `sessions.get()`, and the same context (without the IP) on `session.completed` / `session.failed` webhook data.
|
|
37
|
+
- 1b6bef2: Profile v2 policy reasons, round 2. New eligibility reason `age_unverifiable` (the app sets a minimum age but the birth date could not be read — `doc_ocr`: retryable with a clearer photo; `population_registry`: final). `failureReason` is widened to every policy reason a session can end on: the session-level reasons plus `nationality_not_allowed`, `underage`, `document_type_not_accepted`, `document_nni_mismatch` and `age_unverifiable`. `@sekyro/web`: `PolicyFailureReason` widened, `age_unverifiable` in `DocReason` / `PopulationReason` / `RETRYABLE_REASONS`. `@sekyro/react`: ar/fr/en copy for `age_unverifiable`. `@sekyro/node`: `EligibilityReason` gains `age_unverifiable`; new `PolicyFailureReason` type used by `Session.failureReason` and the `session.failed` webhook data.
|
|
38
|
+
- 1b6bef2: Profile v2 policy reasons. `@sekyro/web`: the eligibility check reasons `document_type_not_accepted` (retryable), `nationality_not_allowed` and `underage`; `SessionFailureReason` (`location_not_allowed`, `anonymizer_blocked`); `HostedSessionView.failureReason` and `acceptedDocumentTypes`. `@sekyro/react`: ar/fr/en copy for the five reasons. `@sekyro/node`: `SessionFailureReason`, `EligibilityReason`, and `failureReason` on `Session` and the `session.failed` webhook data.
|
|
39
|
+
- 13d512f: Profile v2 step 3: identity-method choice, document-NNI registry submit, matching reasons, `subject` input. `@sekyro/web`: `IdentityMethod`, `MethodChoiceView` and `HostedSessionView.methodChoice`; `HostedStepView.attemptsExhausted` / `nniFromDocument` (and `SessionStepView.attemptsExhausted`); `HostedClient.selectMethod()` (`POST /hosted/method`); error codes `method_required` / `method_locked` / `document_nni_mismatch` (409 when a typed NNI differs from the document's); matching reasons `name_mismatch`, `birth_date_mismatch`, `nationality_mismatch` (doc_ocr + population_registry) and `document_number_mismatch` (doc_ocr) as `MatchReason`. The headless runner stops on a pending choice (`unavailable` / `method_required`, `on('method')`), adds `verification.methodChoice()` / `selectMethod()`, and submits the registry with an empty input when the document gave the NNI. `@sekyro/react`: ar/fr/en copy for the four reasons and the method screen; `<LivenessCapture>` handles the method step while the session waits on the choice: it offers only the methods it can run (the national number — it has no document capture) and selects it without a screen when it is the only one left. `@sekyro/node`: `CreateSessionInput.subject` (`SessionSubjectInput`), `CreateSessionInput.identityMethod` and `sessions.selectMethod(id, method)` (`POST /sessions/:id/method`, `IdentityMethod`) so partners can choose the identity method of an optional-document session, `subjectId` / `externalRef` / `subjectConflict` on `Session` and session webhooks, `SessionStep.attemptsExhausted`, `MatchReason`, and the `subject.updated` webhook data (`{ subjectId, sessionId, status, outcome }`).
|
|
40
|
+
- acbf409: Profile v2 step 4: decision mode, desktop handoff, device stops, per-session profile overrides. `@sekyro/web`: decision-mode reasons `liveness_inconclusive`, `mobile_required`, `match_inconclusive`, `document_authenticity` (`DecisionReason`; the inconclusive three are retryable) and the registered `uncalibrated_capture_channel`; session failure reasons `capture_tampered`, `device_velocity_exceeded`, `device_cooldown`; `HostedStepView.requiresMobile` and `HostedSessionView.retryAfter`; the error code `inference_unavailable` (503, request-level, with `retryAfterSeconds` from `Retry-After` or the body). The headless runner retries `inference_unavailable` up to 3 times after `retryAfterSeconds` (default 10 s) under a fresh idempotency key and a fresh attestation, emitting `on('busy')` for each retry, then surfaces `unavailable / inference_unavailable`; it never captures a `requiresMobile` liveness step on desktop (`unavailable / mobile_required`, nothing uploaded) and adds `verification.mobileRequired()`; a `device_cooldown` result carries `retryAfter`. `@sekyro/react`: ar/fr/en coaching for the inconclusive reasons, "Continue on your phone" instead of the camera when liveness needs a phone, a "busy, trying again" notice, the device stop copy (the cooldown says when to try again — `retryAfterTime()`), no retry button for stops or `mobile_required`; bundle budget 23 → 24 KiB. `@sekyro/node`: `CreateSessionInput.profile` (`EkycProfileOverride`, a deep-partial of `EkycProfileSettings`; unknown keys are rejected server-side, widening answers `403 override_widens_profile` unless the app allows it), `Session.overriddenFields` / `riskSignals` / `retryAfter` (`overriddenFields` and `riskSignals` also on the session lifecycle webhooks), `DecisionReason`, the three device stops in `SessionFailureReason`, and the `subject.suspicious_activity` webhook data (`{ subjectId, sessionId, count }`).
|
|
41
|
+
- 58e220c: Published bundles are now minified and no longer include source maps. The public API and types are unchanged.
|
|
42
|
+
|
|
3
43
|
## 0.1.0-rc.2
|
|
4
44
|
|
|
5
45
|
### Minor Changes
|
package/LICENSE
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
Sekyro SDK
|
|
1
|
+
Sekyro AI SDK License (proprietary)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Ibtikar Technologies. All rights reserved.
|
|
3
|
+
Copyright (c) 2026 Ibtikar Technologies, Inc. All rights reserved.
|
|
4
4
|
|
|
5
|
-
This
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
This SDK is proprietary software of Ibtikar Technologies, Inc., licensed
|
|
6
|
+
only under the Sekyro AI SDK License:
|
|
7
|
+
https://sekyro.ai/en/legal/sdk-license
|
|
8
|
+
(in French: https://sekyro.ai/fr/legal/sdk-license,
|
|
9
|
+
in Arabic: https://sekyro.ai/ar/legal/sdk-license).
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
modify, reverse-engineer, redistribute, sublicense, resell or create
|
|
13
|
-
derivative works of the SDK, nor remove or alter this notice. The source
|
|
14
|
-
code is published for transparency (build provenance, security review); its
|
|
15
|
-
publication grants no additional rights.
|
|
11
|
+
In short, and subject to the full license:
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
- You may use the SDK only with a Sekyro AI account, a customer agreement
|
|
14
|
+
with Ibtikar Technologies, Inc. and valid API keys, to integrate the
|
|
15
|
+
Sekyro AI services into your own applications. You may distribute it to
|
|
16
|
+
your end users only as embedded in those applications.
|
|
17
|
+
- You may not use it with another identity verification provider, modify it
|
|
18
|
+
beyond the configuration and theming it exposes, reverse engineer,
|
|
19
|
+
decompile or disassemble it (except where the law expressly allows it),
|
|
20
|
+
circumvent its security, attestation, license checks or usage metering,
|
|
21
|
+
or distribute or sublicense it on its own.
|
|
22
|
+
- Open-source components keep their own licenses. See the third-party
|
|
23
|
+
notices shipped with this SDK.
|
|
24
|
+
- The SDK is provided "as is", without warranty, except as your customer
|
|
25
|
+
agreement provides. The license is governed by the laws of the State of
|
|
26
|
+
Delaware, USA. If this license and your customer agreement conflict,
|
|
27
|
+
the customer agreement prevails.
|
|
28
|
+
|
|
29
|
+
Questions: contact@sekyro.ai
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @sekyro/node
|
|
2
2
|
|
|
3
|
-
Server SDK for [Sekyro](https://sekyro.ai) identity verification. Zero runtime
|
|
3
|
+
Server SDK for [Sekyro AI](https://sekyro.ai) identity verification. Zero runtime
|
|
4
4
|
dependencies, Node.js ≥ 20, ESM + CommonJS, typed.
|
|
5
5
|
|
|
6
6
|
Two jobs, nothing else:
|
|
@@ -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:** [
|
|
17
|
+
**Integration guide:** [Web SDK guide](https://docs.sekyro.ai/en/web/embedded-sdk)
|
|
18
18
|
(server-bound NNI, display vs authoritative results, webhook receivers for
|
|
19
19
|
Next.js and Express, billing). The hosted-flow path is in the
|
|
20
|
-
[quickstart](
|
|
20
|
+
[quickstart](https://docs.sekyro.ai/en/getting-started/quickstart).
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
@@ -59,7 +59,7 @@ session.steps; // [{ checkType, state, reason?, calibratedScore? }]
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
There is no recipe argument. Your API key belongs to an **app**, and the app's
|
|
62
|
-
eKYC profile (set in the Sekyro dashboard) decides which checks run. Every
|
|
62
|
+
eKYC profile (set in the Sekyro AI dashboard) decides which checks run. Every
|
|
63
63
|
field is optional:
|
|
64
64
|
|
|
65
65
|
| field | meaning |
|
|
@@ -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
|
|
@@ -138,7 +202,7 @@ app.post("/webhooks/sekyro", express.raw({ type: "application/json" }), (req, re
|
|
|
138
202
|
```
|
|
139
203
|
|
|
140
204
|
- The header is `t=<unix seconds>,v1=<hex>[,v1=<hex>]`. During a secret
|
|
141
|
-
rotation Sekyro signs with both the current and the previous secret, so the
|
|
205
|
+
rotation Sekyro AI signs with both the current and the previous secret, so the
|
|
142
206
|
delivery is accepted when **any** `v1` matches the secret you hold.
|
|
143
207
|
- **Header names.** Deliveries carry the signature in `x-sekyro-signature`
|
|
144
208
|
(`SEKYRO_SIGNATURE_HEADER`) and, for the transition, the identical legacy
|
package/dist/index.cjs
CHANGED
|
@@ -1,308 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var src_exports = {};
|
|
22
|
-
__export(src_exports, {
|
|
23
|
-
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
24
|
-
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
25
|
-
LEGACY_SIGNATURE_HEADER: () => LEGACY_SIGNATURE_HEADER,
|
|
26
|
-
SEKYRO_SIGNATURE_HEADER: () => SEKYRO_SIGNATURE_HEADER,
|
|
27
|
-
Sekyro: () => Sekyro,
|
|
28
|
-
SekyroApiError: () => SekyroApiError,
|
|
29
|
-
SekyroConfigError: () => SekyroConfigError,
|
|
30
|
-
SekyroWebhookError: () => SekyroWebhookError,
|
|
31
|
-
signatureFromHeaders: () => signatureFromHeaders,
|
|
32
|
-
verifyWebhook: () => verifyWebhook
|
|
33
|
-
});
|
|
34
|
-
module.exports = __toCommonJS(src_exports);
|
|
35
|
-
|
|
36
|
-
// src/errors.ts
|
|
37
|
-
var WEBHOOK_MESSAGES = {
|
|
38
|
-
invalid_signature: "No v1 signature in the header matches this body and secret",
|
|
39
|
-
stale: "Delivery timestamp is outside the tolerance window",
|
|
40
|
-
malformed: "Signature header or body is not in the expected format"
|
|
41
|
-
};
|
|
42
|
-
var SekyroWebhookError = class extends Error {
|
|
43
|
-
name = "SekyroWebhookError";
|
|
44
|
-
code;
|
|
45
|
-
constructor(code, message = WEBHOOK_MESSAGES[code]) {
|
|
46
|
-
super(message);
|
|
47
|
-
this.code = code;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
var CONFIG_MESSAGES = {
|
|
51
|
-
missing_api_key: "Sekyro: `apiKey` is required",
|
|
52
|
-
invalid_base_url: "Sekyro: `baseUrl` must be an https: URL (http: is allowed only for localhost, 127.0.0.1 or [::1])"
|
|
53
|
-
};
|
|
54
|
-
var SekyroConfigError = class extends TypeError {
|
|
55
|
-
name = "SekyroConfigError";
|
|
56
|
-
code;
|
|
57
|
-
constructor(code, message = CONFIG_MESSAGES[code]) {
|
|
58
|
-
super(message);
|
|
59
|
-
this.code = code;
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
var SekyroApiError = class extends Error {
|
|
63
|
-
name = "SekyroApiError";
|
|
64
|
-
status;
|
|
65
|
-
code;
|
|
66
|
-
requestId;
|
|
67
|
-
retryable;
|
|
68
|
-
constructor(init) {
|
|
69
|
-
super(init.message, init.cause === void 0 ? void 0 : { cause: init.cause });
|
|
70
|
-
this.status = init.status;
|
|
71
|
-
this.code = init.code;
|
|
72
|
-
this.requestId = init.requestId;
|
|
73
|
-
this.retryable = isRetryable(init.status, init.code);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
function isRetryable(status, code) {
|
|
77
|
-
if (code === "network" || code === "timeout") return true;
|
|
78
|
-
if (code === "idempotency_in_progress") return true;
|
|
79
|
-
if (status === 429) return true;
|
|
80
|
-
return status >= 500 && status <= 599;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// src/version.ts
|
|
84
|
-
var SDK_VERSION = true ? "0.1.0-rc.2" : "0.0.0-dev";
|
|
85
|
-
|
|
86
|
-
// src/client.ts
|
|
87
|
-
var USER_AGENT = `@sekyro/node/${SDK_VERSION}`;
|
|
88
|
-
var DEFAULT_BASE_URL = "https://api.sekyro.ai/api/v2";
|
|
89
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
90
|
-
function isSuccessEnvelope(value) {
|
|
91
|
-
return typeof value === "object" && value !== null && value.success === true && "data" in value;
|
|
92
|
-
}
|
|
93
|
-
function isErrorEnvelope(value) {
|
|
94
|
-
if (typeof value !== "object" || value === null) return false;
|
|
95
|
-
const v = value;
|
|
96
|
-
if (v.status !== "error" || typeof v.error !== "object" || v.error === null) return false;
|
|
97
|
-
const e = v.error;
|
|
98
|
-
return typeof e.code === "string" && typeof e.message === "string";
|
|
99
|
-
}
|
|
100
|
-
function isAbortLike(err) {
|
|
101
|
-
if (typeof err !== "object" || err === null) return false;
|
|
102
|
-
const name = err.name;
|
|
103
|
-
return name === "TimeoutError" || name === "AbortError";
|
|
104
|
-
}
|
|
105
|
-
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
106
|
-
function assertSafeBaseUrl(baseUrl) {
|
|
107
|
-
let url;
|
|
108
|
-
try {
|
|
109
|
-
url = new URL(baseUrl);
|
|
110
|
-
} catch {
|
|
111
|
-
throw new SekyroConfigError("invalid_base_url");
|
|
112
|
-
}
|
|
113
|
-
if (url.protocol === "https:") return;
|
|
114
|
-
if (url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname)) return;
|
|
115
|
-
throw new SekyroConfigError("invalid_base_url");
|
|
116
|
-
}
|
|
117
|
-
var Transport = class {
|
|
118
|
-
apiKey;
|
|
119
|
-
baseUrl;
|
|
120
|
-
timeoutMs;
|
|
121
|
-
fetchImpl;
|
|
122
|
-
constructor(opts) {
|
|
123
|
-
const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
124
|
-
assertSafeBaseUrl(baseUrl);
|
|
125
|
-
this.apiKey = opts.apiKey;
|
|
126
|
-
this.baseUrl = baseUrl;
|
|
127
|
-
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
128
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
129
|
-
}
|
|
130
|
-
async call(method, path, body, idempotencyKey) {
|
|
131
|
-
const headers = {
|
|
132
|
-
authorization: `Bearer ${this.apiKey}`,
|
|
133
|
-
accept: "application/json",
|
|
134
|
-
"user-agent": USER_AGENT
|
|
135
|
-
};
|
|
136
|
-
if (body !== void 0) headers["content-type"] = "application/json";
|
|
137
|
-
if (idempotencyKey !== void 0) headers["x-idempotency-key"] = idempotencyKey;
|
|
138
|
-
const init = {
|
|
139
|
-
method,
|
|
140
|
-
headers,
|
|
141
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
142
|
-
};
|
|
143
|
-
if (body !== void 0) init.body = JSON.stringify(body);
|
|
144
|
-
let res;
|
|
145
|
-
try {
|
|
146
|
-
res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
|
|
147
|
-
} catch (err) {
|
|
148
|
-
if (isAbortLike(err)) {
|
|
149
|
-
throw new SekyroApiError({
|
|
150
|
-
status: 0,
|
|
151
|
-
code: "timeout",
|
|
152
|
-
message: `Request timed out after ${this.timeoutMs}ms`,
|
|
153
|
-
cause: err
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
throw new SekyroApiError({
|
|
157
|
-
status: 0,
|
|
158
|
-
code: "network",
|
|
159
|
-
message: `Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
160
|
-
cause: err
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
const text = await res.text();
|
|
164
|
-
let parsed;
|
|
165
|
-
let parseError;
|
|
166
|
-
try {
|
|
167
|
-
parsed = text.length > 0 ? JSON.parse(text) : void 0;
|
|
168
|
-
} catch (err) {
|
|
169
|
-
parseError = err;
|
|
170
|
-
}
|
|
171
|
-
if (!res.ok) {
|
|
172
|
-
if (isErrorEnvelope(parsed)) {
|
|
173
|
-
const init2 = {
|
|
174
|
-
status: res.status,
|
|
175
|
-
code: parsed.error.code,
|
|
176
|
-
message: parsed.error.message
|
|
177
|
-
};
|
|
178
|
-
if (parsed.error.request_id !== void 0) init2.requestId = parsed.error.request_id;
|
|
179
|
-
throw new SekyroApiError(init2);
|
|
180
|
-
}
|
|
181
|
-
throw new SekyroApiError({
|
|
182
|
-
status: res.status,
|
|
183
|
-
code: "http_error",
|
|
184
|
-
message: `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
if (!isSuccessEnvelope(parsed)) {
|
|
188
|
-
throw new SekyroApiError({
|
|
189
|
-
status: res.status,
|
|
190
|
-
code: "malformed_response",
|
|
191
|
-
message: "Response is not a `{ success: true, data }` envelope",
|
|
192
|
-
cause: parseError
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
return parsed.data;
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
var SessionsApi = class {
|
|
199
|
-
/**
|
|
200
|
-
* Constructed by {@link Sekyro}; the transport is kept out of the public
|
|
201
|
-
* surface on purpose (only its bound `call` crosses this boundary).
|
|
202
|
-
* @internal
|
|
203
|
-
*/
|
|
204
|
-
constructor(call) {
|
|
205
|
-
this.call = call;
|
|
206
|
-
}
|
|
207
|
-
call;
|
|
208
|
-
/**
|
|
209
|
-
* `POST /sessions`. Returns the one-time `clientToken` — hand it to the
|
|
210
|
-
* browser, never persist it. Pass an `idempotencyKey` so a retried call
|
|
211
|
-
* cannot create a second session.
|
|
212
|
-
*/
|
|
213
|
-
async create(input = {}, opts = {}) {
|
|
214
|
-
const wire = await this.call("POST", "/sessions", input, opts.idempotencyKey);
|
|
215
|
-
return {
|
|
216
|
-
id: wire.id,
|
|
217
|
-
status: wire.status,
|
|
218
|
-
clientToken: wire.client_token,
|
|
219
|
-
hostedUrl: wire.hosted_url,
|
|
220
|
-
steps: wire.steps,
|
|
221
|
-
expiresAt: wire.expiresAt
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
/** `GET /sessions/:id` — the whitelisted view (no client token). */
|
|
225
|
-
async get(id) {
|
|
226
|
-
if (typeof id !== "string" || id.length === 0) {
|
|
227
|
-
throw new TypeError("sessions.get: `id` must be a non-empty string");
|
|
228
|
-
}
|
|
229
|
-
return this.call("GET", `/sessions/${encodeURIComponent(id)}`);
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
var Sekyro = class {
|
|
233
|
-
sessions;
|
|
234
|
-
constructor(opts) {
|
|
235
|
-
if (typeof opts?.apiKey !== "string" || opts.apiKey.length === 0) {
|
|
236
|
-
throw new SekyroConfigError("missing_api_key");
|
|
237
|
-
}
|
|
238
|
-
const transport = new Transport(opts);
|
|
239
|
-
this.sessions = new SessionsApi(transport.call.bind(transport));
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// src/webhooks.ts
|
|
244
|
-
var import_node_crypto = require("crypto");
|
|
245
|
-
var SEKYRO_SIGNATURE_HEADER = "x-sekyro-signature";
|
|
246
|
-
var LEGACY_SIGNATURE_HEADER = "x-ibtikar-signature";
|
|
247
|
-
function signatureFromHeaders(headers) {
|
|
248
|
-
return readHeader(headers, SEKYRO_SIGNATURE_HEADER) ?? readHeader(headers, LEGACY_SIGNATURE_HEADER);
|
|
249
|
-
}
|
|
250
|
-
function readHeader(headers, name) {
|
|
251
|
-
if (typeof headers.get === "function") {
|
|
252
|
-
const v2 = headers.get(name);
|
|
253
|
-
return v2 === null || v2 === "" ? void 0 : v2;
|
|
254
|
-
}
|
|
255
|
-
const record = headers;
|
|
256
|
-
const key = Object.keys(record).find((k) => k.toLowerCase() === name);
|
|
257
|
-
if (key === void 0) return void 0;
|
|
258
|
-
const raw = record[key];
|
|
259
|
-
const v = Array.isArray(raw) ? raw[0] : raw;
|
|
260
|
-
return v === void 0 || v === "" ? void 0 : v;
|
|
261
|
-
}
|
|
262
|
-
var HEX64 = /^[0-9a-f]{64}$/i;
|
|
263
|
-
function verifyWebhook(rawBody, header, secret, opts = {}) {
|
|
264
|
-
if (!header || !secret) throw new SekyroWebhookError("invalid_signature");
|
|
265
|
-
const parts = header.split(",").map((part) => {
|
|
266
|
-
const trimmed = part.trim();
|
|
267
|
-
const eq = trimmed.indexOf("=");
|
|
268
|
-
return eq === -1 ? [trimmed, ""] : [trimmed.slice(0, eq).trim(), trimmed.slice(eq + 1).trim()];
|
|
269
|
-
});
|
|
270
|
-
const tRaw = parts.find(([k]) => k === "t")?.[1];
|
|
271
|
-
const t = tRaw !== void 0 && /^\d+$/.test(tRaw) ? Number(tRaw) : Number.NaN;
|
|
272
|
-
const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
|
|
273
|
-
if (!Number.isSafeInteger(t) || sigs.length === 0) throw new SekyroWebhookError("malformed");
|
|
274
|
-
const nowSec = Math.floor((opts.now?.() ?? Date.now()) / 1e3);
|
|
275
|
-
const tolerance = opts.toleranceSec ?? 300;
|
|
276
|
-
if (Math.abs(nowSec - t) > tolerance) throw new SekyroWebhookError("stale");
|
|
277
|
-
const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(`${t}.${rawBody}`).digest();
|
|
278
|
-
const ok = sigs.some((hex) => {
|
|
279
|
-
if (!HEX64.test(hex)) return false;
|
|
280
|
-
const got = Buffer.from(hex, "hex");
|
|
281
|
-
return got.length === expected.length && (0, import_node_crypto.timingSafeEqual)(got, expected);
|
|
282
|
-
});
|
|
283
|
-
if (!ok) throw new SekyroWebhookError("invalid_signature");
|
|
284
|
-
let parsed;
|
|
285
|
-
try {
|
|
286
|
-
parsed = JSON.parse(rawBody);
|
|
287
|
-
} catch {
|
|
288
|
-
throw new SekyroWebhookError("malformed", "Signed body is not valid JSON");
|
|
289
|
-
}
|
|
290
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
291
|
-
throw new SekyroWebhookError("malformed", "Signed body is not a JSON object");
|
|
292
|
-
}
|
|
293
|
-
return parsed;
|
|
294
|
-
}
|
|
295
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
296
|
-
0 && (module.exports = {
|
|
297
|
-
DEFAULT_BASE_URL,
|
|
298
|
-
DEFAULT_TIMEOUT_MS,
|
|
299
|
-
LEGACY_SIGNATURE_HEADER,
|
|
300
|
-
SEKYRO_SIGNATURE_HEADER,
|
|
301
|
-
Sekyro,
|
|
302
|
-
SekyroApiError,
|
|
303
|
-
SekyroConfigError,
|
|
304
|
-
SekyroWebhookError,
|
|
305
|
-
signatureFromHeaders,
|
|
306
|
-
verifyWebhook
|
|
307
|
-
});
|
|
308
|
-
//# sourceMappingURL=index.cjs.map
|
|
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});
|