Skip to content

Agent-Pattern-Labs/geometra-auth

Repository files navigation

@geometra/auth

Token-based WebSocket authentication for Geometra server/client apps.

Live Demo | Source | Geometra auth contract | Token registry

Related packages:

  • geometracreateServer / createClient; see PLATFORM_AUTH.md for close codes 4001 (handshake rejected) and 4003 (forbidden message), refresh, and what stays out of core.
  • geometra-token-registry — HTTP POST /verify for production remoteVerifier(); mint/revoke tokens via admin API.

The demo client is hosted on GitHub Pages. To see it in action, run the example server locally with npm run example and open the page.

Because Geometra's server computes all layout and streams pre-computed geometry to clients, auth lives at the protocol layer — the server controls what each connection can see and do at the frame level.

Install

bun add @geometra/auth

Peer dependencies: @geometra/server (required), @geometra/client (optional — only needed for the client helper).

Server Usage

createAuth() returns onConnection, onMessage, and onDisconnect hooks that spread directly into createServer() options.

import { createAuth, staticTokens } from '@geometra/auth'
import { createServer } from '@geometra/server'

const auth = createAuth({
  verify: staticTokens({
    'admin-secret': 'admin',
    'viewer-secret': 'viewer',
  }),
  policies: {
    viewer: { allow: ['resize'] }, // read-only — no input events
  },
})

const server = await createServer(view, {
  port: 3100,
  ...auth,
})

Verifiers

Verifier Use case
staticTokens(map) Dev / demos — maps token strings to roles
remoteVerifier(url) Production — POSTs Bearer token to your auth endpoint, expects { role, claims? }
chainVerifiers(...fns) Tries multiple verifiers in order, first match wins

Lifecycle Hooks

createAuth({
  verify: staticTokens({ ... }),
  onAccept: (ctx) => console.log(`connected: ${ctx.role}`),
  onReject: (reason, req) => console.log(`rejected: ${reason}`),
  onBlock: (msgType, ctx) => console.log(`blocked ${msgType} from ${ctx.role}`),
  onLeave: (ctx) => console.log(`disconnected: ${ctx.role}`),
})

Custom Token Extraction

By default, the token is read from the ?token= query parameter. Override with extractToken:

createAuth({
  verify: myVerifier,
  extractToken: (req) => req.headers['x-api-key'] as string ?? null,
})

Client Usage

import { connectWithAuth } from '@geometra/auth/client'

const client = await connectWithAuth({
  token: 'admin-secret',
  url: 'ws://localhost:3100',
  renderer,
  canvas,
  onAuthRejected: () => console.log('invalid token'), // WebSocket closed with 4001
  onForbidden: (err) => console.log('action blocked by server'),
})

onAuthRejected runs when the server rejects the upgrade (4001). @geometra/client does not auto-reconnect after 4001. Token refresh: obtain a new token, then open a new connection (see Geometra PLATFORM_AUTH.md).

Role Policies

Policies control which message types each role can send:

policies: {
  viewer: { allow: ['resize'] },          // whitelist
  restricted: { deny: ['dangerous'] },    // blacklist
  admin: {},                               // no restrictions (default)
}
  • allow — only these message types are permitted (whitelist)
  • deny — these message types are blocked (blacklist, applied after allow)
  • No policy entry = all messages allowed

Development

bun install
bun run check    # type check
bun test         # run tests
bun run build    # compile to dist/

License

MIT