@anchrd/gate-sdk
v0.39.0
Published
The typed client and CLI for **gate** — login and permissions as a finished product. The only package a customer edge installs.
Readme
@anchrd/gate-sdk
The typed client and CLI for gate — login and permissions as a finished product. The only package a customer edge installs.
authorize(bearer)→can(interface, function)— one remote call per request, then local permission checks.gateCLI — register your service and generate typed code.- The shared wire formats as Zod schemas, under
@anchrd/gate-sdk/contract. - Runtime-neutral — standard
fetchonly; runs in Cloudflare Workers, Node, Bun, and the browser.
Where this package stops. It needs a reachable gate (GATE_URL) and the right key for the door
it knocks on — see the two-token rule below. It administers nothing: it
cannot create users, assign roles or grant permissions, and it only ever sees the interfaces of the
one service its key belongs to. It holds no session and caches no permissions — authorize() is one
call per request, and everything after it is local.
Install
npm i @anchrd/gate-sdkQuick start
1. Register your service (once, as an admin — the app key lives in GATE_ADMIN_KEY):
export GATE_URL=https://gate.your-customer.dev
export GATE_ADMIN_KEY=gate_ak_… # admin application key (x-api-key)
gate register dashboard --fn read,export # → service key is written to .env (GATE_KEY)This creates a service named dashboard — the unit that owns exactly one key — together with
its interface of the same name. Further interfaces use that same key and can be managed in the admin
UI or declared by the service itself with gate.interfaces.set().
If
.envalready carries aGATE_KEY,gate registerstops before talking to the gate. The key is handed out exactly once, so creating a second service here would leave one behind whose key nobody ever sees.
2. Generate types for your service:
gate init # writes gate.gen.ts (deterministic, never edit by hand)/schema is scoped to the service your key belongs to: gate.gen.ts contains exactly your own
interfaces. Another service's handle is not in there — and therefore a compile error at your call
site, not a rule that silently never matches.
3. Authorize in your edge (~5 lines):
import { createGateClient } from "@anchrd/gate-sdk";
const gate = createGateClient({
url: process.env.GATE_URL!,
serviceKey: process.env.GATE_KEY!, // from the .env written by `gate register`
});
// per request:
const result = await gate.authorize(userBearer);
if (!result.ok) return new Response(null, { status: result.status }); // 401 / 403 / 5xx
if (!result.can("dashboard", "read")) return new Response(null, { status: 403 });
// … result.identity is the user (id, email)Client
createGateClient({ url, serviceKey, fetch?, timeoutMs? }) returns a client. Service-key requests
time out after 10 seconds by default; timeoutMs overrides that limit.
| | |
|---|---|
| authorize(bearer) | One call to /api/v1/authorization. The response is validated. An invalid or expired bearer is a structured result ({ ok: false, status, problem }), not an exception — the edge can tell 401 from 500. |
| result.can(handle, fn) | Local, no further network call (rules[handle]?.includes(fn)). |
| result.identity · result.rules | Who the user is · their flattened permission set. |
| gate.interfaces.list() | List this service's own interfaces. |
| gate.interfaces.set(handle, functions) | Idempotently create or replace one own interface. This never grants a user access. Gate's internal interfaces cannot be managed this way. |
| gate.interfaces.remove(handle) | Remove one own interface and its grants. |
The gate.interfaces methods throw GateInterfaceError ({ status, problem }) on errors, including
401/403 responses and 502 when gate breaks the response contract. Unlike authorize, they do not
return a structured failure result.
After gate init, can is typed: can("dashboard", "read") — an unknown interface or function is a compile error, with no string escape hatch.
fetch is injectable (mocking in tests means passing a fetch, not an HTTP framework). The service key never appears in logs, errors, or results.
CLI
| Command | |
|---|---|
| gate register <name> --fn a,b,c | Create a service plus its interface of the same name (admin). The service key is written to .env exactly once (GATE_KEY) — never to stdout. <name> is a lowercase slug (a-z, 0-9, -). |
| gate init | Generate types from /api/v1/schema — the interfaces of your service (gate.gen.ts). |
| gate build | Apply gate.json to the local admin UI and build it. Pure file work — no key, no network. |
Env, per command: register needs GATE_URL + GATE_ADMIN_KEY (app key); init needs GATE_URL + GATE_KEY (service key); build needs none — it never talks to a gate.
The CLI does not grant permissions. The former gate grant went away with the grants slice
(GATE-65/66): a permission belongs to a role, and roles are edited in the admin UI (the role matrix)
or through the API. A CLI command would have been a second place where access is handed out.
Branding the admin UI — gate.json
Put a gate.json next to your package.json. gate build reads it, writes two files into the
local admin UI (it travels inside @anchrd/gate) and
builds it; the output is the static asset directory your gate worker serves (see the
@anchrd/gate README).
{
"ui": {
"theme": "./branding/theme.css",
"defaultLanguage": "en",
"languages": { "de": "./i18n/de.json" }
}
}| Field | |
|---|---|
| ui.theme | A CSS file with your design tokens (:root { --primary: … }). It is imported after the UI's own stylesheet, so your tokens win — no component is touched. |
| ui.languages | Language code → catalog file. Every listed file is checked against the UI's en.json (the key source). |
| ui.defaultLanguage | Which of them is built in. Defaults to "en" — the UI's own catalog. |
Three things are deliberate:
- Unknown fields are rejected, not ignored. A typo (
langauges) aborts the build naming the field, instead of silently doing nothing. - An incomplete translation aborts the build, naming the file and the missing keys. There is no silent fallback — you would not see it until a user did, mid-screen, in the wrong language.
- No
gate.jsonis fine.gate buildthen builds the defaults (English, stock tokens) — the same output as before this feature existed.
Everything happens at build time. The running UI loads no config, picks no language and switches no theme: it finds exactly one stylesheet and exactly one catalog.
gate build works against the admin UI already present locally, which travels inside
@anchrd/gate (install it: the UI ships as source precisely so that this build can apply your
config). The output lands where it belongs to you:
| the UI found in | output |
|---|---|
| node_modules/@anchrd/gate/ui | .gate/ui in your project root — point assets.directory there |
| packages/gate/ui (this workspace) | packages/gate/ui/dist, unchanged since GATE-45 |
⚠️ The installed case never builds into
node_modules: the nextnpm ciwould wipe the output while yourwrangler.jsoncstill pointed at it — a worker serving nothing, with nobody having touched a thing. The two mount points (src/theme/custom.css,src/i18n/custom.json) do live innode_modules, and may:gate buildrewrites both every run.
The wire formats: @anchrd/gate-sdk/contract
Until 0.26.0 these lived in a package of their own, @anchrd/gate-contract. Since 0.28.0 they are a
subpath of this one, so a consumer installs one package instead of two — that is the whole
reason for the move. The schemas are unchanged.
import { AuthorizationResponse, type Identity, type Rules } from "@anchrd/gate-sdk/contract";
const { identity, rules } = AuthorizationResponse.parse(await res.json());
// ^ Identity ^ Rules = Record<interface, function[]>| Schema | |
|---|---|
| Identity | Who the authenticated user is: { id, email, name? }. |
| Rules | Record<interface, function[]> — the flattened can() set. |
| AuthorizationResponse | Response of POST /api/v1/authorization: { identity, rules, resource? }. |
| SchemaResponse | Response of GET /api/v1/schema — the basis for codegen (gate init). |
| SchemaInterface · ServiceInterfaceRemoveResponse | Service-scoped interface self-management. |
| ProblemDetails | The single outward error shape (RFC 9457, application/problem+json). |
Formats only, no behavior. Semver-sacred: a breaking change here hits every customer edge at once.
Where the contract stops. It covers the wire formats a customer edge meets — authorization,
schema, interface self-management, and the one error shape. It is deliberately not a complete
type mirror of gate's administration surface: the admin routes belong to the admin UI and to /mcp,
and pinning their shapes here would make every screen change a semver event for every customer.
⚠️ rules are trimmed to the service whose key asked. A service sees its own interfaces and
nothing about what the same person may do elsewhere.
The two-token rule
Never confuse them:
- Service key (
GATE_KEY, in the edge's.env) — only says "this code may talk to gate". - User bearer (per request from the client) — the only thing
can()checks. Permissions always come from the resolved token, never from an argument.
Related
@anchrd/gate— gate itself: the Cloudflare Worker and the admin UI. That is what an operator installs; this package is what a consumer of a running gate installs.
