Keyring

Projects

One project per API you protect; the unit a policy snapshot is served for.

Generated from packages/api/src/resources/projects.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.

POST /v1/projects

Answers 201 on success.

Authentication.

  • A krsk_ secret key or a krses_ dashboard session, as Authorization: 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(),
});

Example.

Request
curl https://keyring-api.belghalem.fr/v1/projects \
  -X POST \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Payments API",
    "slug": "payments-api",
    "default_rate_limits": [
      {
        "id": "per-second",
        "limit": 50,
        "window_ms": 1000,
        "algorithm": "sliding",
        "scope": "key"
      }
    ]
  }'
Response
HTTP/1.1 201 Created
Content-Type: application/json

{
  "object": "project",
  "id": "01a0ade7-b8fb-7711-bb1a-7d1bd5962995",
  "name": "Payments API",
  "slug": "payments-api",
  "policy_version": 1,
  "default_rate_limits": [
    {
      "id": "per-second",
      "limit": 50,
      "scope": "key",
      "algorithm": "sliding",
      "window_ms": 1000
    }
  ],
  "created_at": "2026-09-17T05:47:19.674Z",
  "updated_at": "2026-09-17T05:47:19.674Z"
}

GET /v1/projects

Answers 200 on success.

Authentication.

  • A krsk_ secret key or a krses_ dashboard session, as Authorization: Bearer.

Example.

Request
curl https://keyring-api.belghalem.fr/v1/projects \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY"
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "object": "list",
  "data": [
    {
      "object": "project",
      "id": "01a0ade7-b8fb-7711-bb1a-7d1bd5962995",
      "name": "Payments API",
      "slug": "payments-api",
      "policy_version": 1,
      "default_rate_limits": [
        {
          "id": "per-second",
          "limit": 50,
          "scope": "key",
          "algorithm": "sliding",
          "window_ms": 1000
        }
      ],
      "created_at": "2026-09-17T05:47:19.674Z",
      "updated_at": "2026-09-17T05:47:19.674Z"
    }
  ]
}

GET /v1/projects/:id

Answers 200 on success.

Authentication.

  • A krsk_ secret key or a krses_ dashboard session, as Authorization: Bearer.

Path parameters. id.

Example.

Request
curl https://keyring-api.belghalem.fr/v1/projects/01a0ade7-b8fb-7711-bb1a-7d1bd5962995 \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY"
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "object": "project",
  "id": "01a0ade7-b8fb-7711-bb1a-7d1bd5962995",
  "name": "Payments API",
  "slug": "payments-api",
  "policy_version": 1,
  "default_rate_limits": [
    {
      "id": "per-second",
      "limit": 50,
      "scope": "key",
      "algorithm": "sliding",
      "window_ms": 1000
    }
  ],
  "created_at": "2026-09-17T05:47:19.674Z",
  "updated_at": "2026-09-17T05:47:19.674Z"
}

PATCH /v1/projects/:id

Answers 200 on success.

Authentication.

  • A krsk_ secret key or a krses_ dashboard session, as Authorization: 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(),
});

Example.

Request
curl https://keyring-api.belghalem.fr/v1/projects/01a0ade7-b8fb-7711-bb1a-7d1bd5962995 \
  -X PATCH \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "default_rate_limits": [
      {
        "id": "per-second",
        "limit": 100,
        "window_ms": 1000,
        "algorithm": "sliding",
        "scope": "key"
      },
      {
        "id": "daily",
        "limit": 100000,
        "window_ms": 86400000,
        "algorithm": "fixed",
        "scope": "tenant"
      }
    ]
  }'
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "object": "project",
  "id": "01a0ade7-b8fb-7711-bb1a-7d1bd5962995",
  "name": "Payments API",
  "slug": "payments-api",
  "policy_version": 2,
  "default_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
    }
  ],
  "created_at": "2026-09-17T05:47:19.674Z",
  "updated_at": "2026-09-17T05:47:19.683Z"
}

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 a krses_ dashboard session, as Authorization: Bearer.

Path parameters. id.

Example.

Request
curl https://keyring-api.belghalem.fr/v1/projects/01a0ade7-bbc4-7234-bfac-d5e3b32a51c8 \
  -X DELETE \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY"
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "object": "project",
  "id": "01a0ade7-bbc4-7234-bfac-d5e3b32a51c8",
  "deleted": true,
  "revoked_keys": 0
}

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',
);

On this page