Keyring

Policy feed

What the SDK polls: the snapshot and the delta.

Generated from packages/api/src/policy/policy.controller.ts by packages/docs/tools/generate-reference.mjs. Every example is a real exchange recorded against the control plane by packages/api/src/testing/reference-examples.spec.ts, with ids from that run. Credentials in the formats this product issues (kr_, krsk_, krses_, JWTs) and values under credential-named fields (password, *_secret, *_token, *_key, lookup_hash) are elided at record time; the recorded file and every page of this site are swept for credential shapes (kr_, krsk_, krses_, JWTs, URLs carrying a password, password fields) before either is accepted. Do not edit by hand: src/reference.spec.ts regenerates it and fails on a difference.

The policy distribution feed: the endpoints that make "zero network requests on the hot path" true rather than aspirational (report section 2.7).

Everything here is a read. No handler writes an audit row, and none takes the per-workspace advisory lock that keyring.audit_log_link does, because a vendor's own control-plane write traffic must never be able to stall the feed that keeps their fleet verifying -- that is the 22.7 s stall the week 2 review reproduced, and the reason config.ts grew timeouts.

The one write is the node watermark, and it is conditional: a node that is up to date and was seen recently writes nothing (see recordNodeAck).

GET /v1/policy/snapshot

Answers 200 on success.

Authentication.

  • A krsk_ secret key only. A person's session is refused on this plane.

Query. Validated by this schema, from the control plane's own source:

const SnapshotQuery = z.object({
  project_id: z.uuid(),
  env: Env.optional(),
  tenant_id: TenantIds,
  node: NodeId.optional(),
  max_keys: MaxKeys,
  cursor: z.string().min(1).max(512).optional(),
});

Report section 6.1's first and largest fix: a host node needs the keys of the tenants that call it, not every key in the project. Narrowing is what keeps a 1 M-key project from being a 267 MB response and 555 MiB of someone else's heap.

On the delta route too, and that is the PR 4 review's finding 8a. The fallback there hardcoded null and the SDK never sent the parameter, so the one request where narrowing matters most -- a node far enough behind to need a re-seed -- was the one request that received the whole project.

const TenantIds = z
  .union([z.uuid(), z.array(z.uuid()).min(1).max(50)])
  .optional()
  .transform((value) =>
    value === undefined ? null : Array.isArray(value) ? value : [value],
  );

Matches policy_node_id_shape in migration 0005. The node id is chosen by the SDK and is therefore untrusted input: it is constrained here so that a malformed one is a 400 rather than a constraint violation, and it is only ever a bound parameter.

const NodeId = z.string().regex(/^[A-Za-z0-9._:-]{1,128}$/);

The caller's statement that it understands a paged answer, and how large a page it wants. Clamped to policySnapshotMaxKeys server-side, so asking for more is not a way around the ceiling.

Its presence is the protocol version. Absent, an oversized snapshot is a 413 that names the two ways out rather than a first page the caller would take for the whole set -- see snapshotTooLarge.

const MaxKeys = z.coerce.number().int().min(1).optional();

Example.

Request
curl https://api.keyringdev.com/v1/policy/snapshot?project_id=01a0fac2-a3b2-7769-a0aa-c32c811aa6ab&node=api-eu-1&max_keys=1000 \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY"
Response
HTTP/1.1 200 OK
etag: W/"snap:01a0fac2-a3b2-7769-a0aa-c32c811aa6ab:all:all:5"
Content-Type: application/json

{
  "object": "policy_snapshot",
  "workspace_id": "01a0fac2-a367-76e5-9af4-ead368f3c163",
  "project_id": "01a0fac2-a3b2-7769-a0aa-c32c811aa6ab",
  "env": null,
  "version": 5,
  "project_deleted": false,
  "generated_at": "2026-10-02T03:57:35.150Z",
  "keys": [
    {
      "key_id": "01a0fac2-a40c-7807-8eba-2e00fcb20ba4",
      "tenant_id": "01a0fac2-a3d8-74da-a017-c3c8c3dbe8b3",
      "env": "live",
      "lookup_hash": "…",
      "display_prefix": "kr_live_7zX5YK",
      "scopes": [
        "orders:read",
        "refunds:read"
      ],
      "expires_at": "2027-01-01T00:00:00.000Z",
      "rate_limits": [
        {
          "id": "per-second",
          "limit": 20,
          "scope": "key",
          "algorithm": "sliding",
          "window_ms": 1000
        }
      ]
    },
    {
      "key_id": "01a0fac2-a3fb-71b0-851e-9a5c0f367d34",
      "tenant_id": "01a0fac2-a3d8-74da-a017-c3c8c3dbe8b3",
      "env": "test",
      "lookup_hash": "…",
      "display_prefix": "kr_test_D9a01n",
      "scopes": [
        "orders:read",
        "orders:write"
      ],
      "expires_at": null,
      "rate_limits": [
        {
          "id": "per-second",
          "limit": 100,
          "scope": "key",
          "algorithm": "sliding",
          "window_ms": 1000
        },
        {
          "id": "daily",
          "limit": 100000,
          "scope": "tenant",
          "algorithm": "fixed",
          "window_ms": 86400000
        }
      ]
    }
  ],
  "next_cursor": null
}

GET /v1/policy/delta

Answers 200 on success.

Authentication.

  • A krsk_ secret key only. A person's session is refused on this plane.

Query. Validated by this schema, from the control plane's own source:

const DeltaQuery = z.object({
  project_id: z.uuid(),
  since: z.coerce.number().int().min(0),
  env: Env.optional(),
  tenant_id: TenantIds,
  node: NodeId.optional(),
  max_keys: MaxKeys,
});

Report section 6.1's first and largest fix: a host node needs the keys of the tenants that call it, not every key in the project. Narrowing is what keeps a 1 M-key project from being a 267 MB response and 555 MiB of someone else's heap.

On the delta route too, and that is the PR 4 review's finding 8a. The fallback there hardcoded null and the SDK never sent the parameter, so the one request where narrowing matters most -- a node far enough behind to need a re-seed -- was the one request that received the whole project.

const TenantIds = z
  .union([z.uuid(), z.array(z.uuid()).min(1).max(50)])
  .optional()
  .transform((value) =>
    value === undefined ? null : Array.isArray(value) ? value : [value],
  );

Matches policy_node_id_shape in migration 0005. The node id is chosen by the SDK and is therefore untrusted input: it is constrained here so that a malformed one is a 400 rather than a constraint violation, and it is only ever a bound parameter.

const NodeId = z.string().regex(/^[A-Za-z0-9._:-]{1,128}$/);

The caller's statement that it understands a paged answer, and how large a page it wants. Clamped to policySnapshotMaxKeys server-side, so asking for more is not a way around the ceiling.

Its presence is the protocol version. Absent, an oversized snapshot is a 413 that names the two ways out rather than a first page the caller would take for the whole set -- see snapshotTooLarge.

const MaxKeys = z.coerce.number().int().min(1).optional();

Example: Changes since a version the node holds.

Request
curl https://api.keyringdev.com/v1/policy/delta?project_id=01a0fac2-a3b2-7769-a0aa-c32c811aa6ab&since=5&node=api-eu-1 \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY"
Response
HTTP/1.1 200 OK
etag: W/"delta:01a0fac2-a3b2-7769-a0aa-c32c811aa6ab:all:all:7"
Content-Type: application/json

{
  "object": "policy_delta",
  "workspace_id": "01a0fac2-a367-76e5-9af4-ead368f3c163",
  "project_id": "01a0fac2-a3b2-7769-a0aa-c32c811aa6ab",
  "env": null,
  "since": 5,
  "version": 7,
  "project_deleted": false,
  "generated_at": "2026-10-02T03:57:35.177Z",
  "changes": [
    {
      "change": "upsert",
      "key_id": "01a0fac2-a40c-7807-8eba-2e00fcb20ba4",
      "tenant_id": "01a0fac2-a3d8-74da-a017-c3c8c3dbe8b3",
      "env": "live",
      "lookup_hash": "…",
      "display_prefix": "kr_live_7zX5YK",
      "scopes": [
        "orders:read",
        "refunds:read"
      ],
      "expires_at": "2026-10-03T03:57:35.160Z",
      "rate_limits": [
        {
          "id": "per-second",
          "limit": 20,
          "scope": "key",
          "algorithm": "sliding",
          "window_ms": 1000
        }
      ]
    },
    {
      "change": "upsert",
      "key_id": "01a0fac2-a43b-7c5e-803d-db48d46fd517",
      "tenant_id": "01a0fac2-a3d8-74da-a017-c3c8c3dbe8b3",
      "env": "live",
      "lookup_hash": "…",
      "display_prefix": "kr_live_t04EIa",
      "scopes": [
        "orders:read",
        "refunds:read"
      ],
      "expires_at": null,
      "rate_limits": [
        {
          "id": "per-second",
          "limit": 20,
          "scope": "key",
          "algorithm": "sliding",
          "window_ms": 1000
        }
      ]
    }
  ]
}

Example: Nothing changed.

Request
curl https://api.keyringdev.com/v1/policy/delta?project_id=01a0fac2-a3b2-7769-a0aa-c32c811aa6ab&since=7&node=api-eu-1 \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY"
Response
HTTP/1.1 304 Not Modified
etag: W/"delta:01a0fac2-a3b2-7769-a0aa-c32c811aa6ab:all:all:7"

Shared validators

Defined once in packages/api/src/validation.ts and used by the schemas above.

const Env = z.enum(['live', 'test']);

On this page