Keyring
ReferenceControl-plane API

Embed plane

What an embed token reaches: one tenant's keys and nothing else.

Generated from packages/api/src/embed/embed.controller.ts by packages/docs/tools/generate-reference.mjs. Do not edit by hand: src/reference.spec.ts regenerates it and fails on a difference.

The embed plane (report section 7.1's second column): our customer's customer, in a browser, managing their own keys.

There is no tenant_id in this file, and there must never be one. Not in a path, not in a query, not in a body, not in a header. The tenant comes from the signed token and reaches the database through TenantScopedRepository, which has no method that takes one — that is what makes horizontal tenant escalation structurally impossible rather than a WHERE clause somebody has to remember. embed-isolation.spec.ts asserts it over the live route table, so a route added later is covered by construction rather than by review.

The same argument applies one level up and is why nothing here reads project_id or env either: all four scope columns come from the token.

GET /v1/embed/session

What the component renders its chrome from: whose keys these are, which environment, what this session may do, and when the token dies so the customer's getToken() can be called before it does.

Answers 200 on success.

Authentication.

  • An embed token minted by POST /v1/embed_tokens, bound to one tenant.

GET /v1/embed/keys

Answers 200 on success.

Authentication.

  • An embed token minted by POST /v1/embed_tokens, bound to one tenant.

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

const ListKeys = z.object({
  include_revoked: z
    .enum(['true', 'false'])
    .default('false')
    .transform((value) => value === 'true'),
  limit: z.coerce.number().int().min(1).max(100).default(50),
});

POST /v1/embed/keys

Answers 201 on success.

Authentication.

  • An embed token minted by POST /v1/embed_tokens, bound to one tenant.

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

const CreateKey = z.object({
  name: Name.optional(),
  scopes: Scopes.optional(),
});

POST /v1/embed/keys/:id/rotate

Answers 200 on success.

Authentication.

  • An embed token minted by POST /v1/embed_tokens, bound to one tenant.

Path parameters. id.

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

const RotateKey = z.object({
  overlap_hours: z.number().min(0).max(168).default(24),
});

POST /v1/embed/keys/:id/revoke

Idempotent, for the same reason the control plane's revoke is.

Answers 200 on success.

Authentication.

  • An embed token minted by POST /v1/embed_tokens, bound to one tenant.

Path parameters. id.

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

const RevokeKey = z.object({ reason: z.string().min(1).max(500).optional() });

GET /v1/embed/keys/:id/usage

Answers 200 on success.

Authentication.

  • An embed token minted by POST /v1/embed_tokens, bound to one tenant.

Path parameters. id.

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

const Usage = z.object({
  days: z.coerce.number().int().min(1).max(90).default(30),
});

Shared validators

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

const Name = z.string().min(1).max(200);
const Scopes = z.array(z.string().min(1).max(200)).max(64);

On this page