Projects
One project per API you protect; the unit a policy snapshot is served for.
packages/api/src/resources/projects.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.POST /v1/projects
Answers 201 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Body. Validated by this schema, from the control plane's own source:
const CreateProject = z.object({
name: Name,
slug: Slug,
default_rate_limits: RateLimits.optional(),
});GET /v1/projects
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
GET /v1/projects/:id
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Path parameters. id.
PATCH /v1/projects/:id
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Path parameters. id.
Body. Validated by this schema, from the control plane's own source:
Changing default_rate_limits re-seeds the fleet.
Migration 0008's trigger bumps policy_version and raises
policy_change_floor to it, so every node's next delta poll is answered with
a full snapshot instead. That is the honest answer for a change that is about
every key in the project at once: the alternative is a policy_change row
per key, which is thousands of rows inside one statement saying the same
thing, and a delta larger than the snapshot it replaces.
const UpdateProject = z.object({
name: Name.optional(),
default_rate_limits: RateLimits.optional(),
});DELETE /v1/projects/:id
Soft delete of the row, hard revocation of the keys under it. Removing the
project row itself would take its keys with it by cascade, and a key that
has vanished cannot be shown to a vendor asking what they just turned off;
revoked_at is the state both the API and week 3's snapshot already read.
cascadeRevoke carries the reasoning for the semantics.
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Path parameters. id.
Shared validators
Defined once in packages/api/src/validation.ts and used by the schemas above.
const Name = z.string().min(1).max(200);The same bounds migration 0008's keyring.rate_limits_valid checks, so a bad
set is a 400 with a field path rather than a 500 wrapping a constraint
violation. The database keeps the check regardless: this API is not the only
thing that will ever write that column, and a limit is an authorisation
bound.
window_ms has a floor of one second because these counters are one network
hop away (report section 3.2): a 100 ms window enforced across an RTT is a
number we cannot honestly claim to hold. The ceiling is 31 days, which is the
longest period anyone means by "per month".
const RateLimits = z
.array(
z.object({
id: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/),
limit: z.number().int().min(1).max(1_000_000_000),
window_ms: z.number().int().min(1_000).max(2_678_400_000),
algorithm: z.enum(['sliding', 'fixed']),
scope: z.enum(['key', 'tenant']),
}),
)
.max(8)
.refine(
(rules) => new Set(rules.map((rule) => rule.id)).size === rules.length,
// Two limits of one name share a counter and disagree about its bound, and
// `RateLimit-Policy` is a structured-field dictionary that cannot express
// the duplicate at all.
{ message: 'rate limit ids must be unique' },
);const Slug = z.string().regex(
/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/,
// The length floor is in the pattern and invisible from outside it, so the
// message says it: "p1" is rejected and "must be a lowercase slug" does not
// explain why.
'must be 3 to 63 characters of lowercase letters, digits and hyphens, starting and ending with a letter or digit',
);