Embed plane
What an embed token reaches: one tenant's keys and nothing else.
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);