Skip to main content

http adapter

Delegates storage operations to a remote duck-iam API. Useful for client-server splits, microservices, and shared policy stores.

Install

import { IamHttpAdapter } from '@gentleduck/iam/adapters/http'

Delegates all storage operations to a remote API via fetch. Zero dependencies.


When to use

  • Your access engine runs on a dedicated service
  • Multiple applications share a single policy store
  • Client-side code needs to evaluate permissions against a server (without exposing the database)

Usage

import { IamHttpAdapter } from '@gentleduck/iam/adapters/http'
import { IamEngine } from '@gentleduck/iam'

const adapter = new IamHttpAdapter({
  baseUrl: 'https://api.example.com/access',
  headers: {
    Authorization: 'Bearer ' + serviceToken,
  },
})

const engine = new IamEngine({ adapter })

Dynamic headers

Pass a function to compute headers per-request. Useful for rotating tokens or per-request context.

const adapter = new IamHttpAdapter({
  baseUrl: 'https://api.example.com/access',
  headers: async () => ({
    Authorization: 'Bearer ' + (await getServiceToken()),
    'X-Request-Id': crypto.randomUUID(),
  }),
})

Custom fetch

Provide your own fetch for environments without a global fetch, or to add middleware (logging, retries, tracing).

import { IamHttpAdapter } from '@gentleduck/iam/adapters/http'

const adapter = new IamHttpAdapter({
  baseUrl: 'https://api.example.com/access',
  fetch: async (url, init) => {
    console.log('Access API request:', url)
    const res = await globalThis.fetch(url, init)
    console.log('Access API response:', res.status)
    return res
  },
})

API endpoints

The HTTP adapter expects these REST endpoints on your server:

MethodPathDescription
GET/policiesList all policies
GET/policies/:idGet a single policy
PUT/policiesCreate or update a policy
DELETE/policies/:idDelete a policy
GET/rolesList all roles
GET/roles/:idGet a single role
PUT/rolesCreate or update a role
DELETE/roles/:idDelete a role
GET/subjects/:id/rolesGet a subject's roles
GET/subjects/:id/scoped-rolesGet a subject's scoped roles
POST/subjects/:id/rolesAssign a role (body: { roleId, scope? })
DELETE/subjects/:id/roles/:roleIdRevoke a role (query: ?scope=...)
GET/subjects/:id/attributesGet subject attributes
PATCH/subjects/:id/attributesUpdate subject attributes (body: attribute object)

The built-in Express adminRouter covers part of this admin surface, but it does not implement the full HTTP adapter contract. To use IamHttpAdapter, either implement the complete endpoint set manually or extend the admin router with the missing item lookups, scoped-role reads, and subject-attribute endpoints.


Error handling

The HTTP adapter throws an Error with the message "@gentleduck/iam HTTP {status}: {responseText}" for any 4xx response. 5xx responses, AbortError, and Node connection errors (ECONNRESET, ECONNREFUSED, ETIMEDOUT, ENOTFOUND) are treated as transient and retried with exponential backoff + jitter. 4xx is never retried - it's treated as a definitive answer.

getPolicy(id) and getRole(id) return null on a 404, matching the IamAdapter.IAdapter contract.

A Content-Type: application/json header is sent on every request. If headers is a function, it is awaited before each request.


Retry, timeout, and circuit breaker

IamHttpAdapter ships full SRE plumbing built-in. All four knobs are optional:

new IamHttpAdapter({
  baseUrl: process.env.IAM_API!,
  timeoutMs: 2_000,                // per-request abort (default 5_000; 0 disables)
  retries: 2,                      // 3 total attempts on transient failure (default 2)
  backoffMs: 100,                  // expo base: 100 -> 200 -> 400 + jitter (default 100)
  circuitBreakerThreshold: 5,      // open after N consecutive transient failures (default 5; 0 disables)
  circuitBreakerCooldownMs: 30_000, // cooldown before half-open probe (default 30s)
})

Circuit breaker:

  • Closed -> traffic flows.
  • Open -> reject immediately for circuitBreakerCooldownMs (no fetch attempt).
  • Half-open -> one probe allowed; success closes, failure re-opens. Concurrent callers reject fast so a recovering upstream doesn't get flooded.

Layered with the engine's own IConfig.adapterTimeoutMs - whichever fires first wins.


Constructor config

OptionTypeDefaultDescription
baseUrlstring--Base URL for the access API (trailing slash stripped)
headersRecord or () -> Record or Promise{}Static or dynamic headers
fetchtypeof fetchglobalThis.fetchCustom fetch implementation
timeoutMsnumber5000Per-request abort timeout. 0 disables.
retriesnumber2Retries on transient failure (3 total attempts).
backoffMsnumber100Base exponential backoff in ms.
circuitBreakerThresholdnumber5Consecutive transient failures before opening. 0 disables.
circuitBreakerCooldownMsnumber30000Cooldown before half-open probe.

Notes

  • IamHttpAdapter fetches authorization data over HTTP, but the consuming engine still evaluates permissions locally. It is not an outsourcing-evaluation adapter - it just moves storage behind a service boundary.
  • For server-side evaluation across services, expose engine.permissions() as an endpoint and consume the resulting PermissionMap on clients via the client integrations.

Types

All types live under the Http namespace at @gentleduck/iam/adapters/http. Type-only - zero bundle cost.

  • Http.IConfig - constructor config for IamHttpAdapter (baseUrl, headers, fetch, retry / timeout / circuit breaker knobs).
import type { Http } from '@gentleduck/iam/adapters/http'

const config: Http.IConfig = {
  baseUrl: process.env.IAM_API!,
  timeoutMs: 2_000,
  retries: 2,
}

The deprecated bare alias IHttpAdapterConfig remains for back-compat and will be removed in 3.0.