@sekyro/node 0.1.0-rc.5 → 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.
- package/CHANGELOG.md +20 -0
- package/README.md +132 -5
- package/dist/chunk-O5GJ3YJV.js +1 -0
- package/dist/chunk-TAPXGI3Q.js +1 -0
- package/dist/client-DUMp_I-1.d.cts +1485 -0
- package/dist/client-DUMp_I-1.d.ts +1485 -0
- package/dist/connect/express/index.cjs +1 -0
- package/dist/connect/express/index.d.cts +35 -0
- package/dist/connect/express/index.d.ts +35 -0
- package/dist/connect/express/index.js +1 -0
- package/dist/connect/index.cjs +1 -0
- package/dist/connect/index.d.cts +29 -0
- package/dist/connect/index.d.ts +29 -0
- package/dist/connect/index.js +1 -0
- package/dist/connect/next/index.cjs +1 -0
- package/dist/connect/next/index.d.cts +43 -0
- package/dist/connect/next/index.d.ts +43 -0
- package/dist/connect/next/index.js +1 -0
- package/dist/connector-CMHuC-CU.d.ts +384 -0
- package/dist/connector-D1bhSLBl.d.cts +384 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +107 -1209
- package/dist/index.d.ts +107 -1209
- package/dist/index.js +1 -1
- package/package.json +37 -4
- package/sbom.cdx.json +47 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @sekyro/node
|
|
2
2
|
|
|
3
|
+
## 0.1.0-rc.7
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1e450dc: Connect: the `@sekyro/node/connect` subpath (claim contract, `mergeClaim`, `evaluateGate`, `createConnector`) with Express and Next.js gates; `verifyResultToken` accepts several app ids and returns `appId` and `reuseMode`; `sessions.list({ externalRef, status })`; `Subject.level`, `levelAt`, `levelAssurance`, `sessions[].level`; `externalRefs` on subject events; `reuseMode` and `link.state` on session events; the optional `ProviderAdapter.update` for an atomic read-merge-write, and writes for one user serialised in process.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 3ea3d33: Docs: `listFlows()` now documents that an invalid API key answers `401 unauthorized` (every key belongs to an app; the `key_not_bound` error is gone).
|
|
12
|
+
|
|
13
|
+
## 0.1.0-rc.6
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 8bc8320: `subject.reuse` accepts an identity document: `{ document: { type, issuingCountry, number } }` (`SubjectReuseDocument`; `type` is `national_id`, `passport` or `residence_permit`, `issuingCountry` ISO alpha-2 or alpha-3). Still exactly one target per session.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 82c4898: `CreateSessionInput.assistedBy` and `Session.assistedBy`: your own reference for the agent filming the person in an assisted-capture flow.
|
|
22
|
+
|
|
3
23
|
## 0.1.0-rc.5
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# @sekyro/node
|
|
2
2
|
|
|
3
|
-
Server SDK for [Sekyro AI](https://sekyro.ai) identity verification.
|
|
4
|
-
|
|
3
|
+
Server SDK for [Sekyro AI](https://sekyro.ai) identity verification. One runtime
|
|
4
|
+
dependency (`jose`, for result tokens), Node.js ≥ 20, ESM + CommonJS, typed.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Three jobs, nothing else:
|
|
7
7
|
|
|
8
8
|
1. **Sessions**: create a verification session from your backend and read its
|
|
9
9
|
result. The API key never leaves your server.
|
|
10
10
|
2. **Webhooks**: verify the `x-sekyro-signature` header on deliveries (the
|
|
11
11
|
legacy `x-ibtikar-signature` name is still sent) and get a typed envelope
|
|
12
12
|
back.
|
|
13
|
+
3. **Result tokens**: verify the signed result token of a finished session
|
|
14
|
+
(`verifyResultToken`) and read its level, assurance and outcome.
|
|
13
15
|
|
|
14
16
|
The SDK does **not** orchestrate checks or retry for you; the partner owns the
|
|
15
17
|
retry policy (`SekyroApiError.retryable` tells you when a retry is sensible).
|
|
@@ -108,7 +110,8 @@ always widens. The changed paths come back on `session.overriddenFields`.
|
|
|
108
110
|
> `overrides`. `flow` is accepted again, now as the app's flow slug or id.
|
|
109
111
|
|
|
110
112
|
The session view also carries `failureReason` (a policy stop such as
|
|
111
|
-
`location_not_allowed` or `underage`), `
|
|
113
|
+
`location_not_allowed` or `underage`), `failureDetail` (why, when that reason
|
|
114
|
+
is `camera_unavailable`: `denied` or `no_camera`), `overriddenFields`, `riskSignals`,
|
|
112
115
|
`subjectId` / `externalRef`, `verifiedContacts`, `eid`, and the end user's
|
|
113
116
|
`network` and `device`. A step with `handled: true` failed, but the flow's
|
|
114
117
|
fallback took over.
|
|
@@ -233,6 +236,9 @@ subject.status; // 'active' | 'revoked'
|
|
|
233
236
|
subject.reference; // { source: 'registry' | 'doc_registry' | 'chip' | 'document', capturedAt } | null
|
|
234
237
|
subject.assurance; // 'registry_biometric' | 'chip_biometric' | 'document_biometric' | 'registry_only' | 'document_only' | null
|
|
235
238
|
subject.returningReady; // a returning-user session can be created now
|
|
239
|
+
subject.level; // the highest level proved: 0 | 1 | 2 | 3 (0 once revoked), null before any, absent on an older API
|
|
240
|
+
|
|
241
|
+
const recent = await sekyro.sessions.list({ externalRef: userId, status: "completed" }); // newest first, at most 100; filters re-applied client-side
|
|
236
242
|
|
|
237
243
|
const [byRef] = await sekyro.subjects.list({ externalRef: userId });
|
|
238
244
|
|
|
@@ -240,6 +246,11 @@ const [byRef] = await sekyro.subjects.list({ externalRef: userId });
|
|
|
240
246
|
// Name exactly one of subjectId, externalRef or nni.
|
|
241
247
|
await sekyro.sessions.create({ subject: { reuse: { subjectId } } });
|
|
242
248
|
|
|
249
|
+
// The same in one call, with a mode: `recheck` (verify the same person again)
|
|
250
|
+
// or `step_up` (raise a level 1 subject to identity level). A 24-hex string is
|
|
251
|
+
// a subject id; anything else is your external reference.
|
|
252
|
+
await sekyro.subjects.createSession(userId, { mode: "step_up", flow: "identity" });
|
|
253
|
+
|
|
243
254
|
await sekyro.subjects.revoke(subjectId, { reason: "account closed" }); // idempotent
|
|
244
255
|
await sekyro.subjects.delete(subjectId); // photos, face templates and identifiers; cannot be undone
|
|
245
256
|
```
|
|
@@ -301,7 +312,123 @@ app.post("/webhooks/sekyro", express.raw({ type: "application/json" }), (req, re
|
|
|
301
312
|
`stale`; pass `{ now }` to inject a clock in tests.
|
|
302
313
|
- Retried deliveries carry the same envelope `id`; de-duplicate on it.
|
|
303
314
|
|
|
315
|
+
## Result tokens
|
|
316
|
+
|
|
317
|
+
A finished session carries a short-lived ES256 JWT: `resultToken` on the
|
|
318
|
+
`session.completed` and `session.failed` webhooks, or on demand with
|
|
319
|
+
`sekyro.sessions.resultToken(id)`. Verify it against the public key set before
|
|
320
|
+
trusting it:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { verifyResultToken, SekyroResultTokenError } from "@sekyro/node";
|
|
324
|
+
|
|
325
|
+
const result = await verifyResultToken(token, { appId: process.env.SEKYRO_APP_ID!, env: "live" });
|
|
326
|
+
result.level; // 0 | 1 | 2 | 3 | null (1 human, 2 identity, 3 corroborated)
|
|
327
|
+
result.assurance; // 'live_only' | 'document_biometric' | 'registry_biometric' | ...
|
|
328
|
+
result.outcome; // 'approved' | 'rejected' | ...
|
|
329
|
+
result.subjectId; // the person, when known
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
- Keys come from `https://api.sekyro.ai/.well-known/sekyro/jwks.json` and are
|
|
333
|
+
cached for 10 minutes; an unknown key id fetches the set again.
|
|
334
|
+
- `aud` must be your app id (pass an array to accept several; `result.appId`
|
|
335
|
+
says which), and the token expires 10 minutes after signing.
|
|
336
|
+
- Sandbox tokens are signed with sandbox keys (`sbx-` key ids); pass
|
|
337
|
+
`env: "live"` to refuse them in production.
|
|
338
|
+
- Failures throw `SekyroResultTokenError` with a `code`: `invalid_token`,
|
|
339
|
+
`expired`, `wrong_audience`, `wrong_issuer`, `wrong_env`,
|
|
340
|
+
`key_env_mismatch`, `jwks_unavailable` or `invalid_options`.
|
|
341
|
+
|
|
342
|
+
## Connect
|
|
343
|
+
|
|
344
|
+
`@sekyro/node/connect` carries the Sekyro proof into your auth provider as one
|
|
345
|
+
small claim, `sekyro`, on the provider's user, and gates routes on it. A
|
|
346
|
+
provider package supplies the adapter that reads and writes the claim
|
|
347
|
+
(`@sekyro/connect-zitadel`, `@sekyro/connect-supabase`, `@sekyro/connect-clerk`,
|
|
348
|
+
`@sekyro/connect-auth0`, `@sekyro/connect-firebase`); any object with the
|
|
349
|
+
`ProviderAdapter` shape works too. Nothing else to install: the subpaths ship
|
|
350
|
+
inside `@sekyro/node`.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { Sekyro } from "@sekyro/node";
|
|
354
|
+
import { createConnector } from "@sekyro/node/connect";
|
|
355
|
+
|
|
356
|
+
const connector = createConnector({
|
|
357
|
+
sekyro: new Sekyro({ apiKey: process.env.SEKYRO_API_KEY! }),
|
|
358
|
+
appId: process.env.SEKYRO_APP_ID!, // or an array of app ids
|
|
359
|
+
env: "live",
|
|
360
|
+
webhookSecret: process.env.SEKYRO_WEBHOOK_SECRET!,
|
|
361
|
+
provider, // from a provider package
|
|
362
|
+
onUserDeleted: "ignore", // 'ignore' | 'revoke' | 'delete'
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Start a verification for your user: their id becomes the session's externalRef.
|
|
366
|
+
const session = await connector.start(userId, { returnUrl, state });
|
|
367
|
+
// Store session.id with the state: the return must name this session.
|
|
368
|
+
// Later: connector.recheck(userId) or connector.stepUp(userId, { flow: "identity" }).
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The webhook route writes the claim. Mount it on the raw body:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import express from "express";
|
|
375
|
+
import { SekyroResultTokenError, SekyroWebhookError } from "@sekyro/node";
|
|
376
|
+
import { SekyroConnectError } from "@sekyro/node/connect";
|
|
377
|
+
|
|
378
|
+
app.post("/webhooks/sekyro", express.raw({ type: "application/json" }), async (req, res) => {
|
|
379
|
+
try {
|
|
380
|
+
await connector.webhook((req.body as Buffer).toString("utf8"), req.headers);
|
|
381
|
+
res.sendStatus(200);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (err instanceof SekyroWebhookError || err instanceof SekyroResultTokenError || err instanceof SekyroConnectError) {
|
|
384
|
+
// `unavailable`: the key set is unreachable, a 503 makes Sekyro retry. Anything else is refused: 400.
|
|
385
|
+
res.status(err instanceof SekyroConnectError && err.code === "unavailable" ? 503 : 400).send(err.code);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
throw err;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// The return URL: check `state` and the session id against the values you stored, then
|
|
393
|
+
// sync reads the session and its signed result token server-side (a session of another
|
|
394
|
+
// app, or created without one, is refused).
|
|
395
|
+
await connector.sync(userId, sessionIdFromQuery);
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Gate a route on the claim in the session token (no network call):
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { requireLevel } from "@sekyro/node/connect/express";
|
|
402
|
+
|
|
403
|
+
app.get(
|
|
404
|
+
"/payouts",
|
|
405
|
+
requireLevel(connector, { level: 2, maxAgeDays: 365 }, { claims: (req) => (req as { auth?: Record<string, unknown> }).auth }),
|
|
406
|
+
handler,
|
|
407
|
+
);
|
|
408
|
+
// Fails with 403 { error: "sekyro_gate", reason, action }; action is verify | step_up | recheck | block.
|
|
409
|
+
// Next.js: sekyroGate() and gateRoute() from "@sekyro/node/connect/next".
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The claim (unix seconds; never a name, a document number, a photo or a score):
|
|
413
|
+
|
|
414
|
+
```json
|
|
415
|
+
{ "v": 1, "level": 2, "assurance": "document_biometric", "verified_at": 1790000000, "rechecked_at": 1790003600,
|
|
416
|
+
"session_id": "<session id>", "subject_id": "<subject id>", "env": "live", "flags": ["face_mismatch"] }
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Trust rules:
|
|
420
|
+
|
|
421
|
+
- A webhook writes only after its HMAC signature verified; retries are de-duplicated on the envelope id, which is never left recorded for a delivery whose handling failed (see the `store` contract in the guide).
|
|
422
|
+
- A key set outage while checking a result token throws `SekyroConnectError` with code `unavailable`: answer `503` so Sekyro retries, and `400` for every other refusal.
|
|
423
|
+
- When the webhook carries a result token it must verify too (audience, environment, same session, same user), and it is the source of the level, the assurance and the time.
|
|
424
|
+
- The return URL is a hint: store the session id with your `state` at start and compare both on return; `sync` then reads the session and its signed result token with your API key, verifies the token (your app ids, your environment) and refuses a session of another user, of another app or created without one.
|
|
425
|
+
- Every input goes through one merge: a failed attempt never erases a proof, a weaker proof never lowers the level, an older result never overwrites a newer one, and a result from the other environment is refused.
|
|
426
|
+
- Writes for one user are serialised: each one reads the stored claim, merges and writes it back through the adapter's optional `update` when it has one (the Firebase adapter does), otherwise under a lock per user inside the connector. That lock is per process: two server instances can still interleave for one user, and a re-check result read just before a revocation could write over it. When that matters, send the webhook route to one instance or queue its deliveries per user id.
|
|
427
|
+
- A revocation or deletion of the subject sets the level to 0 with the `revoked` flag (a claim stored without `subject_id` too, when the event's `externalRefs` include the user's id); the gate blocks `revoked` and `face_mismatch` by default. A revoked claim stays revoked: a later result of the same subject, or one without a subject id, is `revoked_ignored`; only an approved session of another subject writes a level again.
|
|
428
|
+
|
|
429
|
+
The full guide, with the Zitadel and Supabase setups, is in [Auth connectors](https://docs.sekyro.ai/en/connectors/overview).
|
|
430
|
+
|
|
304
431
|
## Compatibility
|
|
305
432
|
|
|
306
|
-
Only `node:crypto
|
|
433
|
+
Only `node:crypto`, the global `fetch` and `jose` (result tokens) are used. You can inject a `fetch`
|
|
307
434
|
implementation via `new Sekyro({ fetch })` for tests or custom agents.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var CLAIM_VERSION=1,KNOWN_FLAGS=["face_mismatch","duplicate","revoked"];function isRecord(v){return typeof v=="object"&&v!==null&&!Array.isArray(v)}function isFiniteNumber(v){return typeof v=="number"&&Number.isFinite(v)}function build(c){let out={v:1,level:c.level,assurance:c.assurance,verified_at:c.verified_at};c.rechecked_at!==void 0&&(out.rechecked_at=c.rechecked_at),out.session_id=c.session_id,c.subject_id!==void 0&&(out.subject_id=c.subject_id),out.env=c.env;let flags=new Set;for(let f of c.flags??[])KNOWN_FLAGS.includes(f)&&flags.add(f);return flags.size>0&&(out.flags=[...flags].sort()),out}function parseClaim(value){let v=value;if(typeof v=="string")try{v=JSON.parse(v)}catch{return null}if(!isRecord(v)||!isFiniteNumber(v.v))return null;if(v.v>1)return v;if(v.v!==1)return null;let level=v.level;return level!==0&&level!==1&&level!==2&&level!==3||typeof v.assurance!="string"||!isFiniteNumber(v.verified_at)||typeof v.session_id!="string"||v.env!=="live"&&v.env!=="sandbox"?null:build({level,assurance:v.assurance,verified_at:v.verified_at,rechecked_at:isFiniteNumber(v.rechecked_at)?v.rechecked_at:void 0,session_id:v.session_id,subject_id:typeof v.subject_id=="string"?v.subject_id:void 0,env:v.env,flags:Array.isArray(v.flags)?v.flags:void 0})}function toUnix(isoTime,now){let ms=Date.parse(isoTime);return Math.floor((Number.isFinite(ms)?ms:now())/1e3)}var same=(claim,rule)=>({claim,changed:!1,rule}),wrote=(claim,rule)=>({claim,changed:!0,rule});function mergeClaim(previous,event,opts){let now=opts.now??Date.now,prev=parseClaim(previous);if(prev&&prev.v>1)return same(prev,"unsupported_version");if(event.kind==="revoked"||event.kind==="deleted"){if(!prev)return same(null,"no_change");if(!(prev.subject_id===void 0?event.userInExternalRefs===!0:prev.subject_id===event.subjectId))return same(prev,"subject_mismatch");if(prev.level===0&&prev.flags?.includes("revoked"))return same(prev,"no_change");let{rechecked_at:_cleared,...kept}=prev;return wrote(build({...kept,level:0,subject_id:event.subjectId,flags:["revoked"]}),"revoked")}let r=event.result;if(r.env!==opts.env)return same(prev,"env_mismatch");let t=toUnix(r.verifiedAt,now);if(prev&&t<prev.verified_at)return same(prev,"stale_ignored");let revokedPrev=prev?.flags?.includes("revoked")===!0;if(prev&&revokedPrev&&(r.subjectId===void 0||r.subjectId===prev.subject_id))return same(prev,"revoked_ignored");if(r.outcome==="rejected"&&r.subjectConflict===!0)return!prev||prev.flags?.includes("face_mismatch")?same(prev,"no_change"):wrote(build({...prev,flags:[...prev.flags??[],"face_mismatch"]}),"face_mismatch");if(r.outcome!=="approved"||r.level===null||r.level===0)return same(prev,"not_approved");let level=r.level,assurance=r.assurance??"none",duplicate=r.duplicates===!0?["duplicate"]:[];if(!prev)return wrote(build({level,assurance,verified_at:t,session_id:r.sessionId,subject_id:r.subjectId,env:r.env,flags:duplicate}),"created");if(!revokedPrev&&(r.reuseMode==="recheck"||r.returning===!0&&level<=prev.level))return prev.rechecked_at!==void 0&&t<=prev.rechecked_at?same(prev,t<prev.rechecked_at?"stale_ignored":"no_change"):wrote(build({...prev,rechecked_at:t,subject_id:prev.subject_id??r.subjectId}),"rechecked");if(level>prev.level){let kept=(prev.flags??[]).filter(f=>f!=="face_mismatch"&&f!=="revoked");return wrote(build({...prev,level,assurance,verified_at:t,session_id:r.sessionId,subject_id:r.subjectId??prev.subject_id,flags:[...kept,...duplicate]}),"raised")}return level===prev.level&&t>prev.verified_at?wrote(build({...prev,assurance,verified_at:t,session_id:r.sessionId,subject_id:prev.subject_id??r.subjectId,flags:[...prev.flags??[],...duplicate]}),"renewed"):same(prev,"no_change")}var DEFAULT_DENY_FLAGS=["revoked","face_mismatch"],fail=(reason,action,claim)=>({ok:!1,reason,action,claim});function evaluateGate(claim,policy,nowSec=Math.floor(Date.now()/1e3)){let parsed=parseClaim(claim);if(!parsed)return fail("no_claim","verify",null);if(parsed.v>1)return fail("unsupported_version","verify",parsed);if(policy.env!==void 0&&parsed.env!==policy.env)return fail("wrong_env","verify",parsed);let deny=policy.denyFlags??DEFAULT_DENY_FLAGS;return parsed.flags?.some(f=>deny.includes(f))?fail("flagged","block",parsed):parsed.level<policy.level?fail("level_too_low",parsed.level>=1?"step_up":"verify",parsed):policy.maxAgeDays!==void 0&&nowSec-parsed.verified_at>policy.maxAgeDays*86400?fail("stale","recheck",parsed):policy.maxRecheckAgeMinutes!==void 0&&nowSec-(parsed.rechecked_at??parsed.verified_at)>policy.maxRecheckAgeMinutes*60?fail("recheck_stale","recheck",parsed):policy.assurance&&!policy.assurance.includes(parsed.assurance)?fail("assurance_not_allowed","step_up",parsed):{ok:!0,claim:parsed}}export{CLAIM_VERSION,parseClaim,mergeClaim,evaluateGate};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{evaluateGate}from"./chunk-O5GJ3YJV.js";function sekyroGate(source,tokenClaims,policy){let claims=tokenClaims??{};if("gate"in source)return source.gate(claims,policy);let effective=source.env!==void 0&&policy.env===void 0?{...policy,env:source.env}:policy;return evaluateGate(source.provider.readClaimFromToken(claims),effective)}function gateRoute(result,routes){if(result.ok)return null;switch(result.action){case"verify":return routes.verify;case"step_up":return routes.stepUp;case"recheck":return routes.recheck;case"block":return routes.block}}export{sekyroGate,gateRoute};
|