@sekyro/node 0.1.0-rc.6 → 0.1.0-rc.7

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.
@@ -0,0 +1,384 @@
1
+ import { W as WebhookEventType, L as Locale, C as CreatedSession, S as Sekyro, F as FetchLike } from './client-DUMp_I-1.cjs';
2
+
3
+ /**
4
+ * The Sekyro claim contract and its only state machine, `mergeClaim` (spec
5
+ * 2026-10-29 §4). Pure: no I/O, no `jose`, no `node:` built-ins, so it runs in
6
+ * an edge runtime as well as on Node.
7
+ */
8
+ /** The claim contract version this SDK writes and reads. @public */
9
+ declare const CLAIM_VERSION = 1;
10
+ /**
11
+ * Security facts a claim can carry: `face_mismatch` (a step-up failed because
12
+ * the face belongs to another subject), `duplicate` (the face matched another
13
+ * subject), `revoked` (the subject was revoked or deleted).
14
+ * @public
15
+ */
16
+ type ClaimFlag = "face_mismatch" | "duplicate" | "revoked";
17
+ /**
18
+ * The small object a connector stores on the auth provider's user and the
19
+ * session token carries (spec §4.1). Timestamps are unix seconds. It never
20
+ * carries a name, a document number, a photo, a score or the risk block.
21
+ * @public
22
+ */
23
+ interface SekyroClaim {
24
+ /** Contract version; a reader refuses a higher one. */
25
+ v: 1;
26
+ /** The highest level the person proved for this app; `0` after a revocation. */
27
+ level: 0 | 1 | 2 | 3;
28
+ /** The assurance of the session that proved `level` (`none` when it had none). */
29
+ assurance: string;
30
+ /** Unix seconds: when `level` was proved. */
31
+ verified_at: number;
32
+ /** Unix seconds: the latest approved re-check; absent until one ran. */
33
+ rechecked_at?: number;
34
+ /** The session that set `level`. */
35
+ session_id: string;
36
+ /** The Sekyro subject, when known (needed to apply a revocation). */
37
+ subject_id?: string;
38
+ /** The environment the proof comes from. */
39
+ env: "live" | "sandbox";
40
+ /** Sorted and deduplicated; absent when empty. */
41
+ flags?: ClaimFlag[];
42
+ }
43
+ /**
44
+ * A terminal session result in the shape {@link mergeClaim} reads, built from
45
+ * a verified result token, a webhook or a session view.
46
+ * @public
47
+ */
48
+ interface VerificationResultInput {
49
+ sessionId: string;
50
+ outcome: "approved" | "rejected" | "review" | null;
51
+ level: 0 | 1 | 2 | 3 | null;
52
+ assurance: string | null;
53
+ /** ISO 8601. */
54
+ verifiedAt: string;
55
+ env: "live" | "sandbox";
56
+ subjectId?: string;
57
+ reuseMode?: "recheck" | "step_up";
58
+ returning?: boolean;
59
+ subjectConflict?: boolean;
60
+ /** `true` when the session reported at least one duplicate subject. */
61
+ duplicates?: boolean;
62
+ }
63
+ /**
64
+ * What happened, as {@link mergeClaim} sees it. On a revocation or deletion,
65
+ * `userInExternalRefs` is `true` when the event's external references (or the
66
+ * subject's, read with the API key) include this user's id: a stored claim
67
+ * without `subject_id` is revoked only then, otherwise it is `subject_mismatch`.
68
+ * @public
69
+ */
70
+ type MergeEvent = {
71
+ kind: "result";
72
+ result: VerificationResultInput;
73
+ } | {
74
+ kind: "revoked";
75
+ subjectId: string;
76
+ userInExternalRefs?: boolean;
77
+ } | {
78
+ kind: "deleted";
79
+ subjectId: string;
80
+ userInExternalRefs?: boolean;
81
+ };
82
+ /** The rule {@link mergeClaim} applied. @public */
83
+ type MergeRule = "created" | "raised" | "renewed" | "rechecked" | "face_mismatch" | "revoked" | "revoked_ignored" | "not_approved" | "stale_ignored" | "env_mismatch" | "unsupported_version" | "subject_mismatch" | "no_change";
84
+ /** Result of {@link mergeClaim}: write `claim` only when `changed`. @public */
85
+ interface MergeOutcome {
86
+ claim: SekyroClaim | null;
87
+ changed: boolean;
88
+ rule: MergeRule;
89
+ }
90
+ /**
91
+ * Read a stored or token-carried claim: an object or its JSON string.
92
+ * Returns `null` when the value is not a claim. A claim with a higher
93
+ * version than {@link CLAIM_VERSION} is returned as-is (the caller refuses
94
+ * it); unknown keys and unknown flags of a current claim are dropped.
95
+ * @public
96
+ */
97
+ declare function parseClaim(value: unknown): SekyroClaim | null;
98
+ /**
99
+ * Apply one event to the stored claim (spec §4.2) and say whether to write.
100
+ * Pure and idempotent: the webhook and the return-URL path converge on one
101
+ * write, a retry is a no-op, an older result never overwrites a newer one, a
102
+ * failed attempt never erases a proof, a weaker proof never lowers the level,
103
+ * a result from another environment is refused, and a revoked claim stays
104
+ * revoked (`revoked_ignored`) until a session of another, known subject
105
+ * proves a level again.
106
+ *
107
+ * @param previous - The stored value (object, JSON string or nothing).
108
+ * @param event - A session result, or a revocation or deletion of a subject.
109
+ * @param opts - `env`: the connector's environment; `now`: epoch milliseconds
110
+ * (used only when a result's `verifiedAt` is not a date).
111
+ * @public
112
+ */
113
+ declare function mergeClaim(previous: unknown, event: MergeEvent, opts: {
114
+ env: "live" | "sandbox";
115
+ now?: () => number;
116
+ }): MergeOutcome;
117
+
118
+ /**
119
+ * The gate: a claim and a policy in, `{ ok }` or a reason and an action out
120
+ * (spec 2026-10-29 §5.3). Pure and framework-free; edge-runtime safe.
121
+ */
122
+
123
+ /**
124
+ * What a route requires of the person's claim.
125
+ * @public
126
+ */
127
+ interface GatePolicy {
128
+ /** The lowest level that passes. */
129
+ level: 1 | 2 | 3;
130
+ /** Maximum age of `verified_at`, in days. */
131
+ maxAgeDays?: number;
132
+ /** Maximum age of `rechecked_at` (falling back to `verified_at` when absent), in minutes. */
133
+ maxRecheckAgeMinutes?: number;
134
+ /** The assurances that pass; any when omitted. */
135
+ assurance?: string[];
136
+ /** The environment that passes; any when omitted. */
137
+ env?: "live" | "sandbox";
138
+ /** Flags that block. Default `["revoked", "face_mismatch"]`. */
139
+ denyFlags?: ClaimFlag[];
140
+ }
141
+ /** Why a gate failed. @public */
142
+ type GateReason = "no_claim" | "unsupported_version" | "wrong_env" | "flagged" | "level_too_low" | "stale" | "recheck_stale" | "assurance_not_allowed";
143
+ /**
144
+ * What the app should do about a failed gate: start a verification
145
+ * (`verify`), a step-up (`step_up`), a re-check (`recheck`), or refuse
146
+ * (`block`).
147
+ * @public
148
+ */
149
+ type GateAction = "verify" | "step_up" | "recheck" | "block";
150
+ /** Result of {@link evaluateGate}. @public */
151
+ type GateResult = {
152
+ ok: true;
153
+ claim: SekyroClaim;
154
+ } | {
155
+ ok: false;
156
+ reason: GateReason;
157
+ action: GateAction;
158
+ claim: SekyroClaim | null;
159
+ };
160
+ /**
161
+ * Check a claim (as read from the session token, an object or its JSON
162
+ * string) against a policy. The checks run in this order: present, version,
163
+ * environment, flags, level, age, re-check age, assurance.
164
+ *
165
+ * @param claim - The claim value, or anything else (`no_claim`).
166
+ * @param policy - What the route requires.
167
+ * @param nowSec - Unix seconds. Default the current time.
168
+ * @public
169
+ */
170
+ declare function evaluateGate(claim: unknown, policy: GatePolicy, nowSec?: number): GateResult;
171
+
172
+ /**
173
+ * The connector's small state: delivery and token ids already handled (a
174
+ * retry is skipped) and, optionally, which user a subject belongs to (to
175
+ * resolve a `subject.deleted` that carries no external references).
176
+ * @public
177
+ */
178
+ interface ConnectorStore {
179
+ /** Record `id` for `ttlSec`; resolves `true` when it was already recorded (a retry). */
180
+ seen(id: string, ttlSec: number): Promise<boolean>;
181
+ /** Forget a recorded `id` so a retry is handled again; called when handling failed. Optional. */
182
+ forget?(id: string): Promise<void>;
183
+ setSubjectUser?(subjectId: string, userId: string): Promise<void>;
184
+ getSubjectUser?(subjectId: string): Promise<string | undefined>;
185
+ }
186
+ /**
187
+ * The in-process store of {@link memoryStore}. Its content is lost on restart
188
+ * and not shared between processes; use your own {@link ConnectorStore} (a
189
+ * cache or a table) when you run several instances.
190
+ * @public
191
+ */
192
+ interface MemoryStore extends ConnectorStore {
193
+ forget(id: string): Promise<void>;
194
+ setSubjectUser(subjectId: string, userId: string): Promise<void>;
195
+ getSubjectUser(subjectId: string): Promise<string | undefined>;
196
+ /** The number of recorded ids not yet expired. */
197
+ size(): number;
198
+ }
199
+ /**
200
+ * An in-memory {@link ConnectorStore}, the connector's default. Expired ids
201
+ * are evicted on every call, so it never grows past the ids of one TTL window.
202
+ * @param opts - `now`: a clock returning epoch milliseconds (default `Date.now`).
203
+ * @public
204
+ */
205
+ declare function memoryStore(opts?: {
206
+ now?: () => number;
207
+ }): MemoryStore;
208
+
209
+ /**
210
+ * Where a provider keeps the claim. A provider package (`@sekyro/connect-zitadel`,
211
+ * `@sekyro/connect-supabase`, ...) ships one; any object of this shape works.
212
+ * @public
213
+ */
214
+ interface ProviderAdapter {
215
+ /** The provider's name, for example `zitadel` or `supabase`. */
216
+ readonly name: string;
217
+ /** The value stored at the provider's claim key for this user, or `null`. */
218
+ readUser(userId: string): Promise<unknown>;
219
+ /** Store the claim at the provider's claim key for this user. */
220
+ writeUser(userId: string, claim: SekyroClaim): Promise<void>;
221
+ /** The value at the provider's claim path inside a decoded session token. */
222
+ readClaimFromToken(tokenClaims: Record<string, unknown>): unknown;
223
+ /**
224
+ * Optional read-merge-write for one user. Read the stored value, call `fn`
225
+ * with it, store `claim` when the outcome is `changed` and `claim` is not
226
+ * `null`, and resolve to the outcome. Make it atomic for the user against
227
+ * every other `update` the adapter can see (a per-user lock, a transaction
228
+ * or a compare-and-set retry; `fn` is pure and may be called again).
229
+ * Without it, the connector serialises `readUser` then `writeUser` per user
230
+ * in this process only.
231
+ */
232
+ update?(userId: string, fn: (previous: unknown) => MergeOutcome): Promise<MergeOutcome>;
233
+ }
234
+ /** Why a webhook was not written. @public */
235
+ type WebhookSkipReason = "duplicate_delivery" | "ignored_event" | "other_app" | "no_external_ref" | "result_token_required" | "revocation_unresolved" | "env_mismatch" | "unsupported_version" | "stale_ignored" | "not_approved" | "no_change";
236
+ /**
237
+ * Structured events of the connector, for your logger. Ids, enums and rules
238
+ * only: never a claim body, a token, a secret or a return URL.
239
+ * @public
240
+ */
241
+ type ConnectorLogEvent = {
242
+ type: "write";
243
+ userId: string;
244
+ sessionId?: string;
245
+ rule: MergeRule;
246
+ } | {
247
+ type: "skip";
248
+ reason: WebhookSkipReason;
249
+ sessionId?: string;
250
+ subjectId?: string;
251
+ } | {
252
+ type: "result_token_absent";
253
+ sessionId: string;
254
+ } | {
255
+ type: "revocation_unresolved";
256
+ subjectId: string;
257
+ } | {
258
+ type: "env_mismatch";
259
+ sessionId: string;
260
+ } | {
261
+ type: "flag_cleared";
262
+ userId: string;
263
+ flag: ClaimFlag;
264
+ };
265
+ /** Options of {@link createConnector}. @public */
266
+ interface ConnectorOptions {
267
+ /** A client with this environment's API key. */
268
+ sekyro: Sekyro;
269
+ /** Your app id, or every app id this connector serves. */
270
+ appId: string | string[];
271
+ /** The environment of the API key and webhook secret; a claim from the other one is never written. */
272
+ env: "live" | "sandbox";
273
+ /** The webhook endpoint secret. */
274
+ webhookSecret: string;
275
+ /** Where the claim is stored. */
276
+ provider: ProviderAdapter;
277
+ /** What {@link Connector.userDeleted} does with the person's subjects. Default `ignore`. */
278
+ onUserDeleted?: "ignore" | "revoke" | "delete";
279
+ /** Delivery de-duplication and the subject-to-user map. Default {@link memoryStore}. */
280
+ store?: ConnectorStore;
281
+ /** Refuse to write a terminal webhook without a result token. Default `false`. */
282
+ requireResultToken?: boolean;
283
+ /** Passed to `verifyResultToken`. */
284
+ issuer?: string;
285
+ /** Passed to `verifyResultToken`. */
286
+ jwksUrl?: string;
287
+ /** `fetch` for the result-token key set. Default `globalThis.fetch`. */
288
+ fetch?: FetchLike;
289
+ /** Clock returning epoch milliseconds. Default `Date.now`. */
290
+ now?: () => number;
291
+ /** Receives every {@link ConnectorLogEvent}; the connector never writes to the console. */
292
+ logger?: (event: ConnectorLogEvent) => void;
293
+ }
294
+ /** Result of {@link Connector.webhook}. @public */
295
+ type WebhookOutcome = {
296
+ handled: true;
297
+ event: WebhookEventType;
298
+ userId: string;
299
+ claim: SekyroClaim | null;
300
+ rule: MergeRule;
301
+ } | {
302
+ handled: false;
303
+ event: WebhookEventType | null;
304
+ reason: WebhookSkipReason;
305
+ };
306
+ /** Options of {@link Connector.start}, {@link Connector.recheck} and {@link Connector.stepUp}. @public */
307
+ interface StartOptions {
308
+ /** A flow slug or id; the app's default flow when omitted. */
309
+ flow?: string;
310
+ locale?: Locale;
311
+ /** Where the hosted flow sends the person at the end. */
312
+ returnUrl?: string;
313
+ /** A value you store against the user and check on the return. */
314
+ state?: string;
315
+ ttlMinutes?: number;
316
+ }
317
+ /** The object {@link createConnector} returns. @public */
318
+ interface Connector {
319
+ /** The environment the connector writes claims for. */
320
+ readonly env: "live" | "sandbox";
321
+ /** The app ids whose webhooks and result tokens the connector accepts. */
322
+ readonly appIds: readonly string[];
323
+ /**
324
+ * Verify and apply a Sekyro webhook. Pass the raw body exactly as received.
325
+ * Throws `SekyroWebhookError` (bad signature), `SekyroResultTokenError` (bad
326
+ * token) or {@link SekyroConnectError} (token for another session or user):
327
+ * answer those with a 4xx, except `SekyroConnectError` with code
328
+ * `unavailable` (the key set could not be fetched): answer it with a 503 so
329
+ * Sekyro retries. Answer 2xx once it resolves.
330
+ */
331
+ webhook(rawBody: string, headers: Headers | Record<string, string | string[] | undefined>): Promise<WebhookOutcome>;
332
+ /**
333
+ * The return-URL path: read the session and its signed result token with
334
+ * the API key, verify the token (audience = your app ids, environment) and
335
+ * apply it. A session created without an app or for another app is refused
336
+ * (`SekyroResultTokenError` `wrong_audience`), as is a session of another
337
+ * user ({@link SekyroConnectError} `external_ref_mismatch`). Check your
338
+ * `state` first: store the session id with the state when you create the
339
+ * session, and compare both on the return.
340
+ */
341
+ sync(userId: string, sessionId: string): Promise<{
342
+ claim: SekyroClaim | null;
343
+ rule: MergeRule;
344
+ }>;
345
+ /** Create a verification session for your user (`subject.externalRef` = `userId`). */
346
+ start(userId: string, opts?: StartOptions): Promise<CreatedSession>;
347
+ /** Create a re-check session for your user's subject. */
348
+ recheck(userId: string, opts?: Omit<StartOptions, "flow">): Promise<CreatedSession>;
349
+ /** Create a step-up session for your user's subject with an identity flow. */
350
+ stepUp(userId: string, opts: StartOptions & {
351
+ flow: string;
352
+ }): Promise<CreatedSession>;
353
+ /** Your app deleted the user: apply the `onUserDeleted` policy to the person's subjects. */
354
+ userDeleted(userId: string): Promise<{
355
+ policy: "ignore" | "revoke" | "delete";
356
+ subjects: number;
357
+ }>;
358
+ /** The stored claim, read from the provider (no token lag). */
359
+ claimOf(userId: string): Promise<SekyroClaim | null>;
360
+ /** Remove a flag from the stored claim (for example after a manual review of a `face_mismatch`). */
361
+ clearFlag(userId: string, flag: ClaimFlag): Promise<SekyroClaim | null>;
362
+ /** Check a decoded session token against a policy; `env` defaults to the connector's. */
363
+ gate(tokenClaims: Record<string, unknown>, policy: GatePolicy): GateResult;
364
+ }
365
+ /**
366
+ * Tie Sekyro to an auth provider: verified webhooks and return-URL reads
367
+ * become one idempotent claim write on the provider's user, and the gate
368
+ * reads that claim from the session token.
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * const connector = createConnector({
373
+ * sekyro: new Sekyro({ apiKey: process.env.SEKYRO_API_KEY! }),
374
+ * appId: process.env.SEKYRO_APP_ID!,
375
+ * env: "live",
376
+ * webhookSecret: process.env.SEKYRO_WEBHOOK_SECRET!,
377
+ * provider,
378
+ * });
379
+ * ```
380
+ * @public
381
+ */
382
+ declare function createConnector(opts: ConnectorOptions): Connector;
383
+
384
+ export { type Connector as C, type GatePolicy as G, type MemoryStore as M, type ProviderAdapter as P, type SekyroClaim as S, type VerificationResultInput as V, type WebhookOutcome as W, type GateResult as a, CLAIM_VERSION as b, type ClaimFlag as c, type ConnectorLogEvent as d, type ConnectorOptions as e, type ConnectorStore as f, type GateAction as g, type GateReason as h, type MergeEvent as i, type MergeOutcome as j, type MergeRule as k, type StartOptions as l, type WebhookSkipReason as m, createConnector as n, evaluateGate as o, memoryStore as p, mergeClaim as q, parseClaim as r };
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,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.6";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});
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_RESULT_TOKEN_ISSUER:()=>DEFAULT_RESULT_TOKEN_ISSUER,DEFAULT_TIMEOUT_MS:()=>DEFAULT_TIMEOUT_MS,LEGACY_SIGNATURE_HEADER:()=>LEGACY_SIGNATURE_HEADER,RESULT_TOKEN_JWKS_PATH:()=>RESULT_TOKEN_JWKS_PATH,SEKYRO_SIGNATURE_HEADER:()=>SEKYRO_SIGNATURE_HEADER,Sekyro:()=>Sekyro,SekyroApiError:()=>SekyroApiError,SekyroConfigError:()=>SekyroConfigError,SekyroResultTokenError:()=>SekyroResultTokenError,SekyroWebhookError:()=>SekyroWebhookError,isFlowError:()=>isFlowError,registryFromAnrpts:()=>registryFromAnrpts,signatureFromHeaders:()=>signatureFromHeaders,verifyResultToken:()=>verifyResultToken,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}},RESULT_TOKEN_MESSAGES={invalid_token:"The result token is not a valid Sekyro AI token",expired:"The result token has expired",wrong_audience:"The result token was issued for another app",wrong_issuer:"The result token was not issued by the expected issuer",wrong_env:"The result token is for another environment",key_env_mismatch:"The result token environment does not match its signing key",jwks_unavailable:"The Sekyro AI public key set could not be fetched",invalid_options:"Invalid options for verifyResultToken"},SekyroResultTokenError=class extends Error{name="SekyroResultTokenError";code;constructor(code,message=RESULT_TOKEN_MESSAGES[code],cause){super(message,cause===void 0?void 0:{cause}),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.7";var USER_AGENT=`@sekyro/node/${SDK_VERSION}`,DEFAULT_BASE_URL="https://api.sekyro.ai/api/v2",DEFAULT_TIMEOUT_MS=3e4;function toCreatedSession(wire){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,...wire.level_target!==void 0?{levelTarget:wire.level_target}:{}}}var SUBJECT_ID_RE=/^[0-9a-f]{24}$/i;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 toCreatedSession(wire)}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 list(query={}){let qs=new URLSearchParams;if(query.externalRef!==void 0){if(typeof query.externalRef!="string"||query.externalRef.length===0)throw new TypeError("sessions.list: `externalRef` must be a non-empty string");qs.set("externalRef",query.externalRef)}query.status!==void 0&&qs.set("status",query.status);let suffix=qs.size>0?`?${qs.toString()}`:"";return(await this.call("GET",`/sessions${suffix}`)).filter(s=>(query.externalRef===void 0||s.externalRef===query.externalRef)&&(query.status===void 0||s.status===query.status))}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`)}async resultToken(id){if(typeof id!="string"||id.length===0)throw new TypeError("sessions.resultToken: `id` must be a non-empty string");return this.call("GET",`/sessions/${encodeURIComponent(id)}/result-token`)}};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)}`)}async createSession(target,input,opts={}){let path;if(typeof target=="string")assertId("createSession",target),path=SUBJECT_ID_RE.test(target)?`/subjects/${encodeURIComponent(target)}/sessions`:`/subjects/by-external-ref/${encodeURIComponent(target)}/sessions`;else if(target!==null&&typeof target=="object"&&"subjectId"in target)assertId("createSession",target.subjectId),path=`/subjects/${encodeURIComponent(target.subjectId)}/sessions`;else{let ref=target?.externalRef;assertId("createSession",ref),path=`/subjects/by-external-ref/${encodeURIComponent(ref)}/sessions`}if(input?.mode!=="recheck"&&input?.mode!=="step_up")throw new TypeError("subjects.createSession: `mode` must be 'recheck' or 'step_up'");let wire=await this.call("POST",path,input,opts.idempotencyKey);return toCreatedSession(wire)}},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}var import_jose=require("jose");var DEFAULT_RESULT_TOKEN_ISSUER="https://api.sekyro.ai",RESULT_TOKEN_JWKS_PATH="/.well-known/sekyro/jwks.json",SANDBOX_KID_PREFIX="sbx-",JWKS_MAX_AGE_MS=600*1e3,JWKS_COOLDOWN_MS=30*1e3,JWKS_TIMEOUT_MS=5e3,jwksCache=new Map,LOOPBACK_HOSTS2=new Set(["localhost","127.0.0.1","[::1]"]);function assertSafeUrl(raw){let url;try{url=new URL(raw)}catch{throw new SekyroResultTokenError("invalid_options","`jwksUrl` (or `issuer`) is not a URL")}if(url.protocol!=="https:"&&!(url.protocol==="http:"&&LOOPBACK_HOSTS2.has(url.hostname)))throw new SekyroResultTokenError("invalid_options","`jwksUrl` must be an https: URL (http: is allowed only for localhost, 127.0.0.1 or [::1])")}async function fetchKeySet(url,fetchImpl){let res;try{res=await fetchImpl(url,{method:"GET",headers:{accept:"application/json"},signal:AbortSignal.timeout(JWKS_TIMEOUT_MS)})}catch(err){throw new SekyroResultTokenError("jwks_unavailable",void 0,err)}if(!res.ok)throw new SekyroResultTokenError("jwks_unavailable",`Key set answered HTTP ${res.status}`);let body;try{body=await res.json()}catch(err){throw new SekyroResultTokenError("jwks_unavailable","Key set is not JSON",err)}try{return(0,import_jose.createLocalJWKSet)(body)}catch(err){throw new SekyroResultTokenError("jwks_unavailable","Key set is not a JWKS document",err)}}async function keySet(url,fetchImpl,nowMs,force){let entry=jwksCache.get(url);if(entry?.pending)return entry.pending;if(entry?.keys&&!force&&nowMs-entry.fetchedAt<JWKS_MAX_AGE_MS)return entry.keys;let pending=fetchKeySet(url,fetchImpl),next={fetchedAt:entry?.fetchedAt??0,pending};entry?.keys&&(next.keys=entry.keys),jwksCache.set(url,next);try{let keys=await pending;return jwksCache.set(url,{keys,fetchedAt:nowMs}),keys}catch(err){throw next.keys?jwksCache.set(url,{keys:next.keys,fetchedAt:next.fetchedAt}):jwksCache.delete(url),err}}function mapJoseError(err){if(err instanceof SekyroResultTokenError)return err;if(err instanceof import_jose.errors.JWTExpired)return new SekyroResultTokenError("expired",void 0,err);if(err instanceof import_jose.errors.JWTClaimValidationFailed){if(err.claim==="aud")return new SekyroResultTokenError("wrong_audience",void 0,err);if(err.claim==="iss")return new SekyroResultTokenError("wrong_issuer",void 0,err)}return new SekyroResultTokenError("invalid_token",void 0,err)}function str(v){return typeof v=="string"&&v.length>0?v:void 0}function appIdsOf(appId){let ids=Array.isArray(appId)?appId:[appId];if(ids.length===0||ids.some(id=>typeof id!="string"||id.length===0))throw new SekyroResultTokenError("invalid_options","`appId` is required (a non-empty string or array of them)");return ids}function matchedAud(aud,accepted){return typeof aud=="string"?aud:(aud??[]).find(a=>accepted.includes(a))??accepted[0]}function toResult(payload,claim,env,appId){let out={sessionId:String(claim.session_id),orgId:String(claim.org_id??""),appId,env,level:typeof claim.level=="number"?claim.level:null,assurance:str(claim.assurance)??null,outcome:str(claim.outcome)??null,unit:str(claim.unit)??null,verifiedAt:String(claim.verified_at??"")},subjectId=str(payload.sub);subjectId!==void 0&&(out.subjectId=subjectId);let externalRef=str(claim.external_ref);externalRef!==void 0&&(out.externalRef=externalRef);let flow=claim.flow;flow&&typeof flow.slug=="string"&&typeof flow.version=="number"&&(out.flow={slug:flow.slug,version:flow.version}),(claim.reuse_mode==="recheck"||claim.reuse_mode==="step_up")&&(out.reuseMode=claim.reuse_mode);let r=claim.risk;r&&typeof r=="object"&&(out.risk={level:r.level==="high"||r.level==="medium"?r.level:"low",vpn:r.vpn===!0,ipDocumentMismatch:r.ip_document_mismatch===!0,deviceTampered:r.device_tampered===!0});let c=claim.checks;return c&&typeof c=="object"&&(out.checks={attestation:c.attestation,captureProof:c.capture_proof,injectionSignals:c.injection_signals}),Array.isArray(claim.duplicates)&&(out.duplicates=claim.duplicates.map(d=>{let dup={subjectId:String(d.subject_id??d.subjectId??"")},ref=str(d.external_ref)??str(d.externalRef);return ref!==void 0&&(dup.externalRef=ref),dup})),out}async function verifyResultToken(token,opts){let appIds=appIdsOf(opts?.appId);if(typeof token!="string"||token.length===0)throw new SekyroResultTokenError("invalid_token","The token is empty");let issuer=(opts.issuer??DEFAULT_RESULT_TOKEN_ISSUER).replace(/\/+$/,""),jwksUrl=opts.jwksUrl??`${issuer}${RESULT_TOKEN_JWKS_PATH}`;assertSafeUrl(jwksUrl);let fetchImpl=opts.fetch??globalThis.fetch,now=opts.now??Date.now,verifyOpts={issuer,audience:appIds,algorithms:["ES256"],currentDate:new Date(now()),clockTolerance:opts.clockToleranceSec??30},verified;try{let keys=await keySet(jwksUrl,fetchImpl,now(),!1);try{verified=await(0,import_jose.jwtVerify)(token,keys,verifyOpts)}catch(err){if(!(err instanceof import_jose.errors.JWKSNoMatchingKey))throw err;let entry=jwksCache.get(jwksUrl);if(entry&&now()-entry.fetchedAt<JWKS_COOLDOWN_MS)throw err;verified=await(0,import_jose.jwtVerify)(token,await keySet(jwksUrl,fetchImpl,now(),!0),verifyOpts)}}catch(err){throw mapJoseError(err)}let{payload,protectedHeader}=verified,claim=payload.sekyro;if(typeof claim!="object"||claim===null||typeof claim.session_id!="string")throw new SekyroResultTokenError("invalid_token","The token has no `sekyro` claim");let c=claim,env=c.env;if(env!=="live"&&env!=="sandbox")throw new SekyroResultTokenError("invalid_token","The token has no valid `sekyro.env` claim");let sandboxKey=String(protectedHeader.kid??"").startsWith(SANDBOX_KID_PREFIX);if(env==="sandbox"!==sandboxKey)throw new SekyroResultTokenError("key_env_mismatch");if(opts.env!==void 0&&env!==opts.env)throw new SekyroResultTokenError("wrong_env",`The token is for ${env}, expected ${opts.env}`);return toResult(payload,c,env,matchedAud(payload.aud,appIds))}0&&(module.exports={DEFAULT_BASE_URL,DEFAULT_RESULT_TOKEN_ISSUER,DEFAULT_TIMEOUT_MS,LEGACY_SIGNATURE_HEADER,RESULT_TOKEN_JWKS_PATH,SEKYRO_SIGNATURE_HEADER,Sekyro,SekyroApiError,SekyroConfigError,SekyroResultTokenError,SekyroWebhookError,isFlowError,registryFromAnrpts,signatureFromHeaders,verifyResultToken,verifyWebhook});