@srcabc/notabot-server
v0.3.0
Published
Official Notabot server-side validation SDK (proof validation, unlock cookies, widget passkey subject tokens)
Readme
@srcabc/notabot-server
Official server-side Notabot validation SDK.
Install
npm install @srcabc/notabot-serverValidate a Protected Action
import { NotabotValidator } from "@srcabc/notabot-server"
const notabot = new NotabotValidator({
siteKey: process.env.NOTABOT_SITE_KEY!,
signingSecret: process.env.NOTABOT_SIGNING_SECRET!,
apiBase: process.env.NOTABOT_API_BASE ?? "https://notabot.srcabc.com/api/v1",
timeoutMs: 5000,
})
const result = await notabot.validateToken({
proofToken: req.body.proof_token,
action: "article_unlock",
origin: req.headers.origin,
correlationId: req.body.correlation_id,
})
if (!result.ok) {
// Fail closed. Never allow the protected action on validation failure.
throw new Error("verification_failed")
}Contract
The SDK sends this body to POST /api/v1/verification/validate:
{
"proof_token": "nbp_...",
"action": "article_unlock",
"origin": "https://customer.example",
"correlation_id": "customer_req_..."
}It signs the exact JSON body with the SVK v1 canonical request:
POST
/api/v1/verification/validate
sha256(raw_body)
site_key
timestamp
nonceDo not send session_id or challenge_id; those are internal proof context fields.
The SDK fails closed on timeout, network errors, invalid JSON, replay, expiry, origin mismatch, and denied decisions. It never logs raw proof tokens.
Use validationReasonToStatus(reason) when you need the SDK's canonical
validation-failure status mapping in a framework wrapper.
Unlock Cookies
import { createSignedUnlockCookie, verifySignedUnlockCookie } from "@srcabc/notabot-server"
const setCookie = createSignedUnlockCookie({
secret: process.env.NOTABOT_UNLOCK_COOKIE_SECRET!,
scope: "example.com/article",
aud: "example.com",
path: "/article",
ttlSeconds: 3600,
cookieName: "article_unlock",
})
const result = verifySignedUnlockCookie({
secret: process.env.NOTABOT_UNLOCK_COOKIE_SECRET!,
scope: "example.com/article",
cookieValue,
})For frameworks that set cookies with an object API, use buildUnlockCookie
instead of parsing a Set-Cookie header string:
import { buildUnlockCookie } from "@srcabc/notabot-server"
const cookie = buildUnlockCookie({
secret: process.env.NOTABOT_UNLOCK_COOKIE_SECRET!,
scope: "example.com/article",
aud: "example.com",
path: "/article",
ttlSeconds: 3600,
cookieName: "article_unlock",
})
response.cookies.set({
name: cookie.name,
value: cookie.value,
...cookie.options,
})Widget Passkey Subject Tokens
Mint a subject-identity token (nbs_...) so the widget can run durable passkey
step-up. The token binds a stable, server-controlled subject to the widget's
session and origin; the Notabot backend verifies it with the site signing
secret. Issue it from your subject-token endpoint and return { subject_token }.
import { createWidgetSubjectToken } from "@srcabc/notabot-server"
const subject_token = createWidgetSubjectToken({
siteKey: process.env.NOTABOT_SITE_KEY!,
signingSecret: process.env.NOTABOT_SIGNING_SECRET!,
subjectId, // stable, server-controlled (e.g. a first-party cookie)
sessionId, // the widget's session id from the request body
origin: req.headers.origin,
})verifyWidgetSubjectToken checks the signature, structural claims, time window,
and optional session/origin/audience/scope bindings, and never throws:
import { verifyWidgetSubjectToken } from "@srcabc/notabot-server"
const result = verifyWidgetSubjectToken({
siteKey,
token,
signingSecret,
expectedSessionId,
expectedOrigin,
requiredScope: "challenge",
})
if (!result.ok) {
// result.reason: "invalid_subject_token_signature" | "subject_token_expired" | ...
}The subject id is server-authoritative — never accept it from the client, or a caller could bind to another visitor's credentials.
