@volter/twin-clerk
v0.1.17
Published
Local Clerk twin — a faithful, stateful local Clerk Backend API your real `@clerk/backend` SDK talks to unmodified. Users, sessions, orgs, real signed JWTs + a JWKS endpoint. Mirror, simulate, and fork. Built on @volter/world-core.
Readme
@volter/twin-clerk
A faithful, stateful local Clerk Backend API — your real @clerk/backend SDK (pointed
at the twin's base URL) talks to it unmodified. It models **users, sessions, organizations
- memberships + invitations, allowlist/blocklist, and JWT templates**, issues real
RS256-signed JWTs from a persisted local keypair, and serves a real JWKS endpoint so
an app verifying a twin-issued token against the twin's JWKS succeeds with zero changes.
Built on the shared
@volter/world-corekernel; see the model for storage and branching — no parallel store.
world-clerk serve # the Clerk Backend API twin (+ /.well-known/jwks.json)
world-clerk mirror # the React admin dashboard mirror (users / sessions / orgs)
world-clerk conformance # validate served resources vs the per-object schemasWhat it models
- Users — create / get / list / update / delete, ban·unban, lock·unlock, metadata merge, list filters (email/username/external_id), count, duplicate/empty-identifier validation.
- Sessions — create / get / list / revoke / verify, and token issuance (a real signed session JWT, plus per-JWT-template tokens with custom claims).
- Organizations — full CRUD, slug lookup, metadata, slug-uniqueness + name validation.
- Organization memberships — add / update-role / remove / list (nested user + org),
members_counttracking, duplicate-member rejection; plus a user's memberships. - Organization roles and permissions — what a person sets on the Dashboard (Roles &
Permissions) and what
/v1/organization_permissionsand/v1/organization_rolesdo to the same instance: the nineorg:sys_*system permissions and the system rolesorg:admin(the creator role) andorg:member; custom permissions (org:<feature>:<action>, CRUD); custom roles holding permissions by id (CRUD, assign / remove one permission, a key rename cascading to members and the creator role, delete refused while the role is in use). A membership'spermissionsandrole_nameare its role's, read when the membership is, so a change to a role reaches every member at once. The pre-org:keysadmin/basic_memberresolve to the system roles. Deleting a permission takes it off every role; a membership or invitation naming a role the instance does not define is refused (422), and one naming none gets the Member role under its current key; the creator role keeps manage-members and manage-organization. - Organization invitations & application Invitations — create / list / revoke.
- Allowlist / Blocklist identifiers — create / list / delete.
- JWT templates — CRUD, and token issuance that merges the template's claims.
- Sign-in tokens — issue a real signed token for a user.
- Webhooks — svix-style signed emission on user/session/org events (a real
svixconsumer verifies thesvix-signatureheader unchanged), via an injected delivery sink.
The JWT / JWKS story (the highest-value capability)
On first use the twin generates a 2048-bit RSA keypair and persists it in kernel
state (so signing and the served JWKS share one key, stable across restarts).
node:crypto is lazily required inside the signing helpers (never a top-level import),
so the helper module is safe to pull into the browser bundle of the UI mirror.
POST /v1/sessions/:id/tokens[/:template]→ a genuine compact RS256 JWT whose headerkidmatches the JWKS, withsub/sid/iss(+ any template claims) in the payload.GET /.well-known/jwks.json→ a genuine JWKS{ keys: [{ kty:'RSA', alg:'RS256', kid, n, e }] }exported from the same key.GET /.well-known/openid-configuration→ the OIDC discovery document:issueris exactly theissevery token carries,jwks_uriis this request's own origin +/.well-known/jwks.json.- A session token's
azpis theOriginof the browser request that minted it through the Frontend API (clerk-js); a Backend API mint has no browser origin and carrieshttp://localhost. - A session token is Clerk's v2 shape (
v: 2). While the session has an active organization the user belongs to,ocarriesid,rol(the role key withoutorg:) andslg, and the role's CUSTOM permissions aso.per(the distinct actions) ando.fpm(per feature offea, a bitmask overper) — what@clerk/backend'sauth().has({ permission })decodes. System permissions are left out of the token, as Clerk leaves them out. - An app verifies:
verifyJwtWithJwks(token, jwks)(or a genericjwt.verifywith the JWKS) succeeds for a valid token, and fails for a tampered or expired one. The capabilityverify()proves this offline: sign → fetch JWKS → verify → assert claims.
Auth
A twin fakes auth — it accepts any Backend API key (no real 401), like every other
twin. The real vendor's 401-on-bad-key is filed as the manifest todo clerk.auth.bad_key_401
(a real key check could be added later as its own capability).
Coverage
This pack enumerates the real Clerk Backend API surface as a capability manifest
(clerk-capabilities.ts, ≥50 capabilities). Most start as todo (the honest
denominator) and a solid core is proven done with failable, offline verify()
predicates. Clerk's hosted <SignIn/> / <UserProfile/> / <OrganizationSwitcher/> components are
a separate frontend product; this twin serves the Backend API + JWKS those components call. The
Emails API accepts and records a message and returns it (the OTP / magic-link is available via the
API/log), and a local twin accepts any key, so there is no Turnstile challenge to enforce.
The proxy-checks endpoint is modeled at the API/validation level (the domain must exist
and the proxy_url must be a well-formed https URL → successful; unknown domain → 404,
malformed URL → 422), but the real endpoint's live DNS/HTTP reachability probe of the
proxy is a genuine network op the twin doesn't perform (the no-network rule), so successful
is the deterministic result of the offline validation rather than a real round-trip.
The Backend API surface now proven done includes full-text user search, password
(real scrypt hash + verify), TOTP/2FA secret + backup codes, profile-image upload/delete,
web3-wallet identifiers, add/verify email & phone identifiers, external-OAuth-account
management, networkless authenticateRequest token verification, session touch, bulk
session revoke, organization domains + logo + custom roles/permissions, organization-invitation
accept + bulk create, application invitation notify/redirect_url handling, sign-in-token +
actor-token (impersonation) + testing-token mint/revoke, client list + token verification,
instance settings (restrictions/MFA/attack protection), redirect-URL / OAuth-application /
SAML-connection / instance-domain (primary + satellite) CRUD, a JWT template with a
bring-your-own HS256 custom signing key (real HMAC, verifiable with the shared secret),
webhook-endpoint management via the Backend API + a per-endpoint delivery-attempts log,
waitlist entries (idempotent on email), proxy-check validation, beta-features instance
settings, and a sessions/memberships connector pull. The mirror UI also renders the dashboard
create-a-user form and the impersonate (sign-in-as) action, both driving the same
Backend API endpoints (API↔UI parity). Everything else not yet built is a todo in the
manifest (machine-auth M2M tokens + machines, sign-up attempts, role sets, and the role/permission
webhook events) — a worklist, never a silent gap.
Frontend API (clerk-js)
The twin also serves the Frontend API surface the real @clerk/clerk-js browser bundle
drives — GET/PATCH /v1/environment, lazy client minting (cookie __client), password
sign-in (/v1/client/sign_ins + attempt_first_factor over the Backend scrypt verify),
session touch/tokens (delegated to the Backend mint), and sign-out (single session and
collection-level). Organizations follow the instance's organization_settings (enabled is what
clerk-js checks before its organization hooks work); the user carries its organization memberships
with their permissions (what clerk-js's has({ permission }) reads for the active organization),
setActive({ organization }) touches the session with active_organization_id (a membership is
required), a token is minted for the organization_id clerk-js asks for, and
GET /v1/me/organization_memberships lists the memberships. clerk-js's organization components
(<OrganizationSwitcher/>, <CreateOrganization/>, <OrganizationProfile/>) drive Frontend API
organization, membership, invitation and suggestion routes the twin does not serve yet (404); they
are filed as clerk.fapi.* todos. The pinned real bundle is vendored at client/clerk.browser.js and served
at the /npm/@clerk/clerk-js loader path. Both surfaces project the same rows: a user created
through the Backend API signs in through the Frontend one.
Personas are world state, not defaults: pass --personas FILE (a JSON array of
{email, password, first_name?, last_name?}) and the server seeds them as ordinary user rows
at start. Nothing is pre-signed-in; sign-in is a protocol, and the world can be signed out of.
