@anchrd/gate-sdk
v0.13.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.- Runtime-neutral — standard
fetchonly; runs in Cloudflare Workers, Node, Bun, and the browser.
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.
Rechte werden nicht mehr über die CLI gesetzt (der frühere gate grant entfiel mit dem Ausbau des grants-Slice, GATE-65/66) — sie gehören der Rolle und werden in der Admin-UI (Rollen-Matrix) geschaltet.
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 (@anchrd/gate-ui) and builds it; the output is the static asset directory
your gate worker serves (see the api 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 gate-ui already present locally — node_modules/@anchrd/gate-ui
(install it: it ships as source precisely so that this build can apply your config), or
packages/ui in this workspace. The output lands where it belongs to you:
| gate-ui found in | output |
|---|---|
| node_modules/@anchrd/gate-ui | .gate/ui in your project root — point assets.directory there |
| packages/ui (this workspace) | packages/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 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-api— gate itself (the Cloudflare Worker).@anchrd/gate-contract— the shared wire formats.
