@flashyid/sdk
v0.1.1
Published
Verify an AI agent's delegated authority — is the assertion genuine, and does its chain permit this action? Public key only, no account.
Maintainers
Readme
@flashyid/sdk
Verify a Flashy ID assertion and the delegated authority it carries — from one import, holding no secret. A relying party asks two questions of an assertion, and this answers both:
- Is it genuine? — the signature against Flashy ID's published JWKS (EdDSA), the issuer, the audience, the expiry.
- Does its delegation permit this action? — the grant chain the assertion carries, verified and checked against what the action needs.
The verify path fetches only the public JWKS, so a relying party can never mint an assertion — only check one. That is the whole point of the asymmetric design.
Install
npm install @flashyid/sdkNode 18+, ESM. One runtime dependency (jose).
Authorize a request
import { authorize } from '@flashyid/sdk';
const out = await authorize(jws, { scope: 'payment.execute', amount: 1240 }, {
issuer: 'https://id.flashyid.com',
audience: yourClientId,
nowSec: Math.floor(Date.now() / 1000),
});
if (out === null) {
// the assertion is not genuinely from Flashy ID → 401
} else if (out.result.ok) {
// authorized — out.result.scp / .lim / .exp
} else {
// genuine but refused → 403; out.result.code says why
// 'out_of_mandate' | 'approval_required' | 'untrusted_root' | 'expired' | 'revoked' | …
}Sign in with Flashy ID, across the mesh
The example above is single-issuer: it trusts any chain a genuine Flashy ID assertion carries. On the mesh that is not enough. An agent from org A signs in at org B; org B verifies the assertion is real, but a real assertion can carry a chain rooted at any accountable human anywhere — so "verified" would quietly mean "belongs to someone", not "belongs to someone we trust".
Pin the roots you accept. Every AAO charter names the one human all of its
chains root at (accountableTo), so a relying party derives the set from the
charters it federates with:
import { authorize, trustedRootsFromCharters } from '@flashyid/sdk';
const trustedRoots = trustedRootsFromCharters([ourCharter, partnerCharter]);
const out = await authorize(jws, demand, {
issuer: 'https://id.flashyid.com',
audience: yourClientId,
nowSec: Math.floor(Date.now() / 1000),
trustedRoots, // a sound chain rooted elsewhere is refused with `untrusted_root`
});Omit trustedRoots and behaviour is unchanged — any root is accepted, which is
right for a single issuer and wrong for a mesh. The check runs before the
mandate check, so an untrusted root is never masked as a missing scope. The
underlying rootsWithin(effective, roots) is exported for a party that verifies
a chain itself.
What's in it
signAssertion(input)— the issuer half: mint an EdDSA assertion (JWS) carrying a subject and a delegation chain. The private key never leaves the issuer; anything it signs,verifyAssertionaccepts.verifyAssertion(token, opts)— verify signature + iss/aud/exp; returns the identity and any delegation, ornull. Never throws.authorize(token, demand, opts)— verify the assertion, then verify the delegation authorizes a specific action.- The grant kernel —
issueRoot,attenuate,verifyChain,permits. Pure functions over a delegation chain; the one rule is that a chain only ever narrows (three invariants: never widens, effective expiry is the minimum across the chain, revocation walks down). - Charter → grants —
grantsFromCharter,grantFromCharterRole. An AAO chartered role already carries the fields a grant needs (capabilities → scp,worksIn → res,humanApprovalAtOrAbove → lim,accountableTo → root), so no new policy language.
The enforcement gate
The kernel answers "does the grant permit this?"; the gate maps that answer
onto the three outcomes a caller can act on — ALLOW, ESCALATE (permitted,
but at or above the chain's human-approval bar a named human must co-sign), and
DENY (with the kernel's refusal code, so the "why" is auditable).
import { evaluateGrant, enforce, grantAdapter, recordOnly } from '@flashyid/sdk';
// Pure decision — no clock, no I/O. Same answer for issuer, RP, and auditor.
const r = evaluateGrant(chain, { scope: 'seal', impact: 'CRITICAL' }, { nowSec });
// → { action: 'ESCALATE', escalateAtOrAbove: 'HIGH', ... }
// Operational wrapper: adapter within a timeout, record-only fallback + alert.
const decision = await enforce({ chain, demand, nowSec }, grantAdapter(), {
onUnavailable: (err) => alertOps(err),
});evaluateGrant(chain, demand, opts)— the pure core.grantAdapter()— an adapter backed byevaluateGrant(the real gate).recordOnly— the default adapter: alwaysALLOW, never block.enforce(decision, adapter, opts)— run an adapter with a timeout and fall back to record-only (raisingonUnavailable) when it is unreachable.
The approval bar is enforced in permits itself: a demand whose impact is at
or above the chain's effective approval_at_or_above refuses with
approval_required, so authorize never silently allows an action a human was
meant to see. The gate is the one caller that turns that specific refusal into
ESCALATE. Full contract: docs/enforcement-gate.md.
The flashyid init command
Read an organization's charter and issue the root grants its roles hold — the charter is already the grant, so this is a file read and a mapping, not a policy language to learn.
npx flashyid init # reads ./flashyos.roles.json, prints a summary
npx flashyid init my-charter.json # a specific charter file
npx flashyid init --json # emit the grants as JSONFlags: a positional file (or --file/-f), and --json. Exits non-zero when
the file is missing/malformed or any role's capability maps to no registered
scope — an init that would silently drop authority fails loudly instead.
Refusal codes
A refusal always carries a specific reason — it is part of the published contract, for relying parties and auditors alike.
| Code | Meaning |
|---|---|
| chain_widened | a link grants more scope, resource, or limit than its parent |
| broken_chain | a link's issuer is not the holder above it, the chain's leaf holder is not the assertion's subject, or the del claim is malformed |
| expired | a link is past its expiry at the evaluated time |
| revoked | a link (or one above it) has been revoked |
| out_of_mandate | the action exceeds the chain's effective scope, resource, or spend limit |
| approval_required | in mandate, but at/above the human-approval bar — a named human must co-sign (the gate maps this to ESCALATE) |
| scope_unmapped | a charter capability maps to no registered scope |
| untrusted_root | the chain is sound but roots at a principal this relying party has not agreed to accept (only when trustedRoots is set) |
| empty_chain | no links to evaluate |
Design line
contract.sign is deliberately absent from the vocabulary. An agent moves value
within a mandate; a named human signs what binds the company. The kernel
enforces the mandate — it never issues authority to sign.
Standards
OIDC ID tokens (EdDSA / JWS) for the assertion; a delegation-chain profile (Flashy ID spec v0.3) for the authority. Verification is pure and deterministic — the same code runs in a relying party and in a stranger's audit script.
Spec and registries: https://flashyid.com/spec.
