@suluk/keys
v0.1.1
Published
The delegation-chain ALGEBRA for hierarchical API keys: effective-caps (scope ∩, cap/expiry min up the chain), POOLED subtree headroom (a parent cap bounds parent+children TOTAL spend — abuse-proof), the cascade read-checks (expired/disabled ancestor), ch
Readme
CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable to ratify anything on the SIG's behalf.
Install
bun add @suluk/keysWhat it does
When an API key can mint child keys — and each key carries its own scopes, credit cap, rate share,
and expiry — you need one place that decides what a caller may actually do. @suluk/keys is that
place: the pure, portable algebra (extracted verbatim from a real app, C046) for a materialized-
path key tree. The app builds a ChainNode[] (a caller plus its ancestors, root → self) and the
per-path SpendRow[] from its own store — the DB query is the seam — then calls these functions
so the money/abuse rules can never drift:
effectiveCaps— the caller's real grant, walking up the chain: scopes = the intersection of every node's grant; credit cap / rate share / expiry = the min (soonest) declared. A child can't out-scope or out-spend an ancestor.pooledHeadroom— a node's cap bounds its whole subtree's total spend. This is the abuse-proof property: a parent capped at 50 can't mint children that each spend 50, because every child's spend lands in the parent's subtree.expiredAncestor/disabledAncestor— the read-time revocation cascade: a child dies the moment any ancestor expires or is disabled.clampChildGrant— clamp a freshly-minted child to the parent's effective grant.
Plus the materialized-path utilities (inSubtree, childPath, …) and the scope/metadata parsers.
Usage
The pure algebra (no DB)
import { effectiveCaps, pooledHeadroom, clampChildGrant, type ChainNode } from "@suluk/keys";
// A caller's chain: root → parent → self (each with its OWN grant).
const chain: ChainNode[] = [
{ keyId: "root", path: "root", scopes: ["credits:read", "ask"], ownCreditLimit: 100, ownRateSharePct: null, ownExpiresAt: null },
{ keyId: "child", path: "root/child", scopes: ["ask"], ownCreditLimit: 30, ownRateSharePct: null, ownExpiresAt: null },
];
const caps = effectiveCaps(chain);
caps.scopes; // ["ask"] — the intersection
caps.creditLimit; // 30 — the min declared cap
// Pooled headroom: the BINDING constraint a charge must clear across the subtree.
const headroom = pooledHeadroom(chain, [{ path: "root/child", spent: 10 }]);
headroom; // { limit: 30, spent: 10, remaining: 20 } — or null when nothing is cappedWith the injected DB (lineage tree + pooled query)
import { insertLineage, chainHeadroom, subtreeOf, revokeKeyTree } from "@suluk/keys";
// Mint a child under a parent (materialized path is computed for you).
await insertLineage(db, { keyId: "child", parentKeyId: "root", userId: "user_42", parentPath: "root" });
// The pooled headroom, joined against the @suluk/credits ledger in one grouped query.
const room = await chainHeadroom(db, chain); // Headroom | null
// Cascade revoke — the whole subtree (self + every descendant).
await revokeKeyTree(db, "root");What's inside
| Export | What it does |
| --- | --- |
| effectiveCaps(chain) | The caller's real grant: scope ∩, cap/share/expiry min up the chain. |
| pooledHeadroom(chain, spendRows) | The binding subtree constraint → Headroom ({ limit, spent, remaining }), or null. |
| topCappedPath(chain) | The topmost capped node — whose subtree covers all others, so one query suffices. |
| expiredAncestor / disabledAncestor | The read-time expiry / revocation cascade. |
| clampChildGrant(parent, requested) | Clamp a minted child to the parent's effective grant. |
| escapeLike / subtreeLikePattern / inSubtree / childPath / pathDepth / ancestorIdsOf / pathAt / MAX_KEY_DEPTH | The materialized-path utilities. |
| parseScopes / parseKeyMeta | The scope + metadata model. |
| keyLineage / keyLineage DB ops (subtreeOf, parentPathOf, insertLineage, chainHeadroom, revokeKeyTree) | The lineage-tree schema + queries over an injected Drizzle handle. |
Types ChainNode, EffectiveCaps, SpendRow, Headroom, and KeysDB are exported alongside.
Boundary
This package owns the algebra + the table-owned queries; the grant-fetch that builds a
ChainNode[] is app-specific (an apikey table vs an MCP-token table), so it stays in the app and
calls the pure functions. The pooled-headroom query is where @suluk/keys joins
@suluk/credits — the key tree meets the ledger. It also depends on
@suluk/better-auth + drizzle-orm.
License
Apache-2.0
