import { type Context, Hono } from 'hono' import * as z from 'zod/mini' import * as Analytics from '../../../analytics/Analytics.js' import * as RequestEvents from '../../../analytics/tables/requestEvents.js' import * as ApiKey from '../../../ApiKey.js' import * as ApiKeys from '../../../ApiKeys.js' import type * as App from '../../../App.js' import * as Db from '../../../db/Db.js' import * as Auth from '../../../internal/Auth.js' import * as Billing from '../Billing.js' import * as OpenApi from '../../../internal/OpenApi.js' import * as Response from '../../../internal/Response.js' import * as Schema from '../../../internal/Schema.js' import * as Store from '../../../internal/Store.js' import * as Scope from '../../../Scope.js' import * as ManagementError from '../Error.js' /** Zod schemas owned by the API-keys resource. */ export namespace schema { /** One API key: metadata only — the token appears once, at mint. */ export const Key = OpenApi.component( Schema.describe( z.object({ allowedIps: ApiKey.schema.AllowedIps.check( z.describe( 'Client IP addresses and CIDR ranges allowed to use this key. An empty list means unrestricted.', ), z.meta({ examples: [['203.0.113.0/24', '2001:db8::1']] }), ), createdAt: z.iso .datetime() .check( z.describe('When the key was minted (ISO 8601).'), z.meta({ examples: ['2026-01-01T00:00:00.000Z'] }), ), createdBy: z .optional(z.string()) .check( z.describe('Identity that minted the key (`usr_…`, or `super_admin`).'), z.meta({ examples: ['usr_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), environment: z .enum(['production', 'sandbox']) .check(z.describe('Key environment.'), z.meta({ examples: ['sandbox'] })), expiresAt: z .optional(z.iso.datetime()) .check( z.describe('When the key expires (ISO 8601). Absent for a non-expiring key.'), z.meta({ examples: ['2027-01-01T00:00:00.000Z'] }), ), id: z .string() .check( z.describe('Opaque key id (`key_…`).'), z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), lastUsedAt: z .optional(z.iso.datetime()) .check( z.describe('When the key last authenticated a request (ISO 8601).'), z.meta({ examples: ['2026-07-21T12:34:56.000Z'] }), ), name: z .optional(z.string()) .check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })), orgId: z .string() .check( z.describe('Owning organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), projectId: z.optional(z.string()).check( z.describe('Attributed project id (`prj_…`).'), // prettier-ignore z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), scopes: z .array(z.string().check(z.minLength(1))) .check( z.describe( "Granted scopes. '*' and non-self-serve scopes appear only on keys minted by the super admin.", ), z.meta({ examples: [['data:read']] }), ), tokenLast4: z .string() .check( z.describe('Last 4 characters of the plaintext token.'), z.meta({ examples: ['a1b2'] }), ), }), 'An API key: metadata only — the token appears once, at mint.', ), 'ApiKey', ) /** Path parameters addressing a project's key collection. */ export const Params = z .object({ orgId: z .string() .check( z.describe('The organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), projectId: z .string() .check( z.describe('The project id (`prj_…`).'), z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), }) .check(z.describe("Path parameters for a project's API keys.")) /** Path parameters addressing an organization's key collection. */ export const OrgParams = z .object({ orgId: z .string() .check( z.describe('The organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), }) .check(z.describe("Path parameters for an organization's API keys.")) /** Path parameters addressing one API key. */ export const KeyParams = z .object({ keyId: z .string() .check( z.describe('The key id (`key_…`).'), z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), orgId: z .string() .check( z.describe('The organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), projectId: z .string() .check( z.describe('The project id (`prj_…`).'), z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), }) .check(z.describe('Path parameters for one API key.')) /** Path parameters addressing one organization-level API key. */ export const OrgKeyParams = z .object({ keyId: z .string() .check( z.describe('The key id (`key_…`).'), z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), orgId: z .string() .check( z.describe('The organization id (`org_…`).'), z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), }) .check(z.describe('Path parameters for one organization-level API key.')) /** Request body minting a key. The wildcard scope is not issuable here. */ export const CreateApiKeyRequest = OpenApi.component( z .object({ allowedIps: z .optional(ApiKey.schema.AllowedIps) .check( z.describe( 'Client IP addresses and CIDR ranges allowed to use the key. Omit or use an empty list for unrestricted access.', ), z.meta({ examples: [['203.0.113.0/24']] }), ), environment: z ._default(z.enum(['production', 'sandbox']), 'production') .check(z.describe('Key environment.'), z.meta({ examples: ['sandbox'] })), name: z .optional(z.string().check(z.minLength(1), z.maxLength(100))) .check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })), scopes: z ._default( z.array( z.string().check( z.minLength(1), z.refine((scope) => scope !== Scope.wildcard), ), ), [], ) .check(z.describe('Scopes to grant.'), z.meta({ examples: [['data:read']] })), }) .check(z.describe('Fields for minting an API key.')), 'CreateApiKeyRequest', ) /** Schemas for the createApiKey operation. */ export namespace createApiKey { /** The minted key plus its one-time token. */ export const Response = OpenApi.component( Schema.describe( z.extend(Key, { token: z .string() .check( z.describe('The plaintext token — shown once, unrecoverable afterward.'), z.meta({ examples: ['tempo_sandbox:sk:…'] }), ), }), 'The minted key, including its one-time token.', ), 'CreateApiKeyResponse', ) } /** Non-paginated list of API key metadata. */ export const ApiKeyList = OpenApi.component( Schema.describe( z.object({ data: z.array(Key).check(z.describe('The API keys, newest first.')), }), 'A non-paginated list of API key metadata.', ), 'ApiKeyList', ) /** Schemas for the listApiKeys operation. */ export namespace listApiKeys { /** Query parameters filtering a project's keys. */ export const Query = z .strictObject({ environment: Schema.Environment }) .check(z.describe("Query parameters for a project's API keys.")) /** Non-paginated list of a project's keys. */ export const Response = ApiKeyList } /** Schemas for the listOrgApiKeys operation. */ export namespace listOrgApiKeys { /** Query parameters filtering the organization's keys. */ export const Query = z .strictObject({ environment: Schema.Environment }) .check(z.describe("Query parameters for an organization's API keys.")) /** Non-paginated list of every key in the organization. */ export const Response = ApiKeyList } /** Schemas for the revokeApiKey operation. */ export namespace revokeApiKey { /** Confirmation that the key was revoked. */ export const Response = OpenApi.component( z .object({ id: z .string() .check( z.describe('ID of the key that was revoked.'), z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }), ), }) .check(z.describe('Confirmation that the key was revoked.')), 'RevokeApiKeyResponse', ) } /** Schemas for the rotateApiKey operation. */ export namespace rotateApiKey { /** The replacement key plus its one-time token. */ export const Response = OpenApi.component( Schema.describe( z.extend(Key, { token: z .string() .check( z.describe('The plaintext token — shown once, unrecoverable afterward.'), z.meta({ examples: ['tempo_sandbox:sk:…'] }), ), }), 'The replacement key, including its one-time token.', ), 'RotateApiKeyResponse', ) } /** Schemas for the updateApiKey operation. */ export namespace updateApiKey { /** Request body replacing or clearing a key's IP allowlist. */ export const Body = OpenApi.component( z .object({ allowedIps: ApiKey.schema.AllowedIps.check( z.describe( 'Replacement client IP/CIDR allowlist. Use an empty list for unrestricted access.', ), z.meta({ examples: [['203.0.113.0/24']] }), ), }) .check(z.describe("Fields for updating an API key's network access.")), 'UpdateApiKeyRequest', ) } } /** * Mounts project-attributed API key mint, list, update, and revoke routes plus the org-wide list. Project routes enforce caller organization and project attribution. */ export function apiKeys(options: apiKeys.Options = {}) { const hidden = !options.enabled return new Hono() .post( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'admin' }), Auth.ensureProject(), OpenApi.validate('param', schema.Params, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.validate('json', schema.CreateApiKeyRequest, { code: 'body_invalid', message: 'Check the request body and try again.', }), OpenApi.describeRoute({ description: 'Mint a project-attributed API key. The token appears once and is never shown again.', // prettier-ignore hide: hidden, operationId: 'createApiKey', responses: OpenApi.responses({ errors: { 400: { codes: ['body_invalid', 'param_invalid'], description: 'Malformed API key, invalid path, or invalid body.', }, 403: { codes: ['scope_not_issuable'], description: 'The requested scopes are not issuable.', }, 404: { codes: ['api_keys_not_enabled', 'organization_not_found', 'project_not_found'], description: 'No accessible project or API key surface was found.', }, }, success: { description: 'The minted key, including its one-time token.', schema: schema.createApiKey.Response, }, }), summary: 'Create API key', tags: ['API Keys'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowScope) return Auth.ensureProjectError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'body_invalid', message: 'Check the request body and try again.', }) const kv = c.get('kv') if (!kv) return notEnabled(c) const createdBy = mintedBy(c) if (!createdBy) return ManagementError.sessionRequired(c) const body = c.req.valid('json') if (!body.scopes.every((scope) => Scope.includes(c.get('scopeCatalog'), scope))) return Response.error(c, { code: 'body_invalid', message: 'Check the request body and try again.', status: 400, }) if (!canIssue(c, body.scopes)) return Response.error(c, { code: 'scope_not_issuable', message: 'The requested scopes are not issuable.', status: 403, }) try { const orgId = Auth.org(c).id // Snapshot billing onto the record so the auth path throttles a // sandbox key to the public quota without a per-request billing read. // Only sandbox is gated; production keys carry no snapshot. const billingActive = body.environment === 'sandbox' ? await Billing.active(Db.get(c.get('db')), orgId, 'sandbox') : undefined const { record, token } = await ApiKeys.mint( kv.store, { createdBy, environment: body.environment, ...(body.allowedIps === undefined ? {} : { allowedIps: body.allowedIps }), ...(billingActive === undefined ? {} : { billingActive }), ...(body.name === undefined ? {} : { name: body.name }), orgId, projectId: Auth.project(c).id, scopes: body.scopes, }, { scopeCatalog: c.get('scopeCatalog') }, ) return c.json( Response.validated(schema.createApiKey.Response, { ...serializeKey(record), token }), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) .get( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys', Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }), Auth.ensureOrg(), Auth.ensureProject(), OpenApi.validate('param', schema.Params, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.validate('query', schema.listApiKeys.Query, { code: 'query_invalid', message: 'Check the query parameters and try again.', }), OpenApi.describeRoute({ hide: hidden, operationId: 'listApiKeys', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid', 'query_invalid'], description: 'Malformed API key, invalid path, or invalid query.', }, 404: { codes: ['api_keys_not_enabled', 'organization_not_found', 'project_not_found'], description: 'No accessible project or API key surface was found.', }, }, success: { description: "The project's keys, newest first (metadata only).", schema: schema.listApiKeys.Response, }, }), summary: 'List API keys', tags: ['API Keys'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (Auth.narrowScope) return Auth.ensureProjectError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'query_invalid', message: 'Check the query parameters and try again.', }) const kv = c.get('kv') if (!kv) return notEnabled(c) try { const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, { environment: c.req.valid('query').environment, projectId: Auth.project(c).id, scopeCatalog: c.get('scopeCatalog'), }) return c.json( Response.validated(schema.listApiKeys.Response, { data: await serializeKeys(c, records), }), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) .get( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/api-keys', Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }), Auth.ensureOrg(), OpenApi.validate('param', schema.OrgParams, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.validate('query', schema.listOrgApiKeys.Query, { code: 'query_invalid', message: 'Check the query parameters and try again.', }), OpenApi.describeRoute({ description: 'List every API key in the organization, across all of its projects.', hide: hidden, operationId: 'listOrgApiKeys', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid', 'query_invalid'], description: 'Malformed API key, invalid path, or invalid query.', }, 404: { codes: ['api_keys_not_enabled', 'organization_not_found'], description: 'No accessible organization or API key surface was found.', }, }, success: { description: "The organization's keys across all projects, newest first (metadata only).", // prettier-ignore schema: schema.listOrgApiKeys.Response, }, }), summary: 'List organization API keys', tags: ['API Keys'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (Auth.narrowScope) return Auth.ensureOrgError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'query_invalid', message: 'Check the query parameters and try again.', }) const kv = c.get('kv') if (!kv) return notEnabled(c) try { const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, { environment: c.req.valid('query').environment, scopeCatalog: c.get('scopeCatalog'), }) return c.json( Response.validated(schema.listOrgApiKeys.Response, { data: await serializeKeys(c, records), }), 200, ) } catch (cause) { return Response.upstream(c, cause) } }, ) .post( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}/rotate', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'admin' }), OpenApi.validate('param', schema.OrgKeyParams, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.describeRoute({ description: 'Create a replacement organization-level API key with the same access as an existing key. The existing key remains active until revoked.', hide: hidden, operationId: 'rotateOrgApiKey', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path.' }, 403: { codes: ['scope_not_issuable'], description: 'The source key contains scopes the caller cannot rotate.', }, 404: { codes: ['api_key_not_found', 'api_keys_not_enabled', 'organization_not_found'], description: 'No accessible API key or API key surface was found.', }, }, success: { description: 'The replacement key, including its one-time token.', schema: schema.rotateApiKey.Response, }, }), summary: 'Rotate organization API key', tags: ['API Keys'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) const kv = c.get('kv') if (!kv) return notEnabled(c) const createdBy = mintedBy(c) if (!createdBy) return ManagementError.sessionRequired(c) const { keyId } = c.req.valid('param') try { const source = await ApiKeys.get(kv.store, keyId, { scopeCatalog: c.get('scopeCatalog'), }) if (!source || source.orgId !== Auth.org(c).id || source.projectId !== undefined) return keyNotFound(c) if (!canRotate(c, source.scopes)) return Response.error(c, { code: 'scope_not_issuable', message: 'The source key contains scopes the caller cannot rotate.', status: 403, }) return rotate(c, kv.store, source, createdBy) } catch (cause) { return Response.upstream(c, cause) } }, ) .delete( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}', Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }), Auth.ensureOrg({ role: 'admin' }), OpenApi.validate('param', schema.OrgKeyParams, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.describeRoute({ description: 'Revoke an organization-level API key. Requests presenting its token stop resolving.', hide: hidden, operationId: 'revokeOrgApiKey', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path.' }, 404: { codes: ['api_key_not_found', 'api_keys_not_enabled', 'organization_not_found'], description: 'No accessible API key or API key surface was found.', }, }, success: { description: 'Confirmation that the key was revoked.', schema: schema.revokeApiKey.Response, }, }), summary: 'Revoke organization API key', tags: ['API Keys'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) const kv = c.get('kv') if (!kv) return notEnabled(c) const { keyId } = c.req.valid('param') try { const record = await ApiKeys.get(kv.store, keyId, { scopeCatalog: c.get('scopeCatalog'), }) if (!record || record.orgId !== Auth.org(c).id || record.projectId !== undefined) return keyNotFound(c) return revoke(c, kv.store, keyId) } catch (cause) { return Response.upstream(c, cause) } }, ) .patch( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}', Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }), Auth.ensureOrg({ role: 'admin' }), Auth.ensureProject(), OpenApi.validate('param', schema.KeyParams, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.validate('json', schema.updateApiKey.Body, { code: 'body_invalid', message: 'Check the request body and try again.', }), OpenApi.describeRoute({ description: "Replace an API key's client IP/CIDR allowlist. An empty list removes the restriction.", hide: hidden, operationId: 'updateApiKey', responses: OpenApi.responses({ errors: { 400: { codes: ['body_invalid', 'param_invalid'], description: 'Malformed API key, invalid path, or invalid body.', }, 404: { codes: [ 'api_key_not_found', 'api_keys_not_enabled', 'organization_not_found', 'project_not_found', ], description: 'No accessible API key or API key surface was found.', }, }, success: { description: 'The updated API key metadata.', schema: schema.Key, }, }), summary: 'Update API key', tags: ['API Keys'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (Auth.narrowScope) return Auth.ensureProjectError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'body_invalid', message: 'Check the request body and try again.', }) const kv = c.get('kv') if (!kv) return notEnabled(c) const { keyId } = c.req.valid('param') try { const record = await ApiKeys.get(kv.store, keyId, { scopeCatalog: c.get('scopeCatalog'), }) if (!record || record.orgId !== Auth.org(c).id || record.projectId !== Auth.project(c).id) return keyNotFound(c) const updated = await ApiKeys.update(kv.store, keyId, c.req.valid('json'), { scopeCatalog: c.get('scopeCatalog'), }) if (!updated) return keyNotFound(c) return c.json(Response.validated(schema.Key, serializeKey(updated)), 200) } catch (cause) { return Response.upstream(c, cause) } }, ) .post( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}/rotate', Auth.policy({ session: true }), Auth.ensureOrg({ role: 'admin' }), Auth.ensureProject(), OpenApi.validate('param', schema.KeyParams, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.describeRoute({ description: 'Create a replacement API key with the same access as an existing key. The existing key remains active until revoked.', hide: hidden, operationId: 'rotateApiKey', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path.' }, 403: { codes: ['scope_not_issuable'], description: 'The source key contains scopes the caller cannot rotate.', }, 404: { codes: [ 'api_key_not_found', 'api_keys_not_enabled', 'organization_not_found', 'project_not_found', ], description: 'No accessible API key or API key surface was found.', }, }, success: { description: 'The replacement key, including its one-time token.', schema: schema.rotateApiKey.Response, }, }), summary: 'Rotate API key', tags: ['API Keys'], }), async (c) => { if (Auth.narrowAccess) return Auth.superAdminAccessError(c) if (Auth.narrowScope) return Auth.ensureProjectError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) const kv = c.get('kv') if (!kv) return notEnabled(c) const createdBy = mintedBy(c) if (!createdBy) return ManagementError.sessionRequired(c) const { keyId } = c.req.valid('param') try { const source = await ApiKeys.get(kv.store, keyId, { scopeCatalog: c.get('scopeCatalog'), }) if (!source || source.orgId !== Auth.org(c).id || source.projectId !== Auth.project(c).id) return keyNotFound(c) if (!canRotate(c, source.scopes)) return Response.error(c, { code: 'scope_not_issuable', message: 'The source key contains scopes the caller cannot rotate.', status: 403, }) return rotate(c, kv.store, source, createdBy) } catch (cause) { return Response.upstream(c, cause) } }, ) .delete( '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}', Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }), Auth.ensureOrg({ role: 'admin' }), Auth.ensureProject(), OpenApi.validate('param', schema.KeyParams, { code: 'param_invalid', message: 'Check the path parameters and try again.', }), OpenApi.describeRoute({ description: 'Revoke an API key. Requests presenting its token stop resolving.', hide: hidden, operationId: 'revokeApiKey', responses: OpenApi.responses({ errors: { 400: { codes: ['param_invalid'], description: 'Malformed API key or invalid path.' }, 404: { codes: [ 'api_key_not_found', 'api_keys_not_enabled', 'organization_not_found', 'project_not_found', ], description: 'No accessible API key or API key surface was found.', }, }, success: { description: 'Confirmation that the key was revoked.', schema: schema.revokeApiKey.Response, }, }), summary: 'Revoke API key', tags: ['API Keys'], }), async (c) => { if (Auth.narrowAccess) return Auth.accessError(c) if (Auth.narrowScope) return Auth.ensureProjectError(c) if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'param_invalid', message: 'Check the path parameters and try again.', }) const kv = c.get('kv') if (!kv) return notEnabled(c) const { keyId } = c.req.valid('param') try { // Bind the key to the scoped project before revoking: a key id from // another org or project must be indistinguishable from a missing one. const record = await ApiKeys.get(kv.store, keyId, { scopeCatalog: c.get('scopeCatalog'), }) if (!record || record.orgId !== Auth.org(c).id || record.projectId !== Auth.project(c).id) return keyNotFound(c) return revoke(c, kv.store, keyId) } catch (cause) { return Response.upstream(c, cause) } }, ) } export declare namespace apiKeys { /** Options for creating the API-keys resource. */ type Options = { /** Whether the surface is enabled (a KV state store is configured); disabled routes are hidden and return `404`. */ enabled?: boolean | undefined } } /** Enriches key records with their latest authenticated request timestamp. */ async function serializeKeys(c: Context, records: readonly ApiKeys.Record[]) { const source = c.get('analytics') if (!source || records.length === 0) return records.map((record) => serializeKey(record)) const lastUsedAt = new Map( ( await RequestEvents.readLastUsedAt( Analytics.get(source), records.map(({ id }) => id), ) ).map((usage) => [usage.apiKeyId, usage.lastUsedAt]), ) return records.map((record) => serializeKey(record, lastUsedAt.get(record.id))) } /** Projects a persisted record onto the management key shape (no rate limits, no token). */ function serializeKey(record: ApiKeys.Record, lastUsedAt?: string) { return { allowedIps: [...record.allowedIps], createdAt: record.createdAt, ...(record.createdBy === undefined ? {} : { createdBy: record.createdBy }), environment: record.environment, ...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }), id: record.id, ...(lastUsedAt === undefined ? {} : { lastUsedAt }), ...(record.name === undefined ? {} : { name: record.name }), orgId: record.orgId, ...(record.projectId === undefined ? {} : { projectId: record.projectId }), scopes: [...record.scopes], tokenLast4: record.tokenLast4, } } /** The caller identity recorded as `createdBy`, or `undefined` for non-minting principals. */ function mintedBy(c: Context) { const principal = Auth.getPrincipal(c) if (principal?.type === 'super_admin') return 'super_admin' if (principal?.type === 'session') return principal.id return undefined } /** Checks whether the caller may grant every requested scope. */ function canIssue(c: Context, scopes: readonly Scope.Id[]) { const principal = Auth.getPrincipal(c) if (principal?.type === 'super_admin') return true if (principal?.type === 'session') { const selfServe = Scope.selfServeFrom(c.get('scopeCatalog')) return ( scopes.every((scope) => selfServe.includes(scope)) && (!scopes.includes('management:write') || Auth.membership(c)?.role === 'owner') ) } return false } /** Checks caller-specific restrictions that still apply when copying existing grants. */ function canRotate(c: Context, scopes: readonly Scope.Id[]) { const principal = Auth.getPrincipal(c) if (principal?.type === 'super_admin') return true if (principal?.type !== 'session') return false return ( (!scopes.includes('management:write') && !scopes.includes(Scope.wildcard)) || Auth.membership(c)?.role === 'owner' ) } /** Mints a replacement that preserves the source key's access and attribution. */ async function rotate( c: Context, store: Store.State, source: ApiKeys.Record, createdBy: string, ) { const billingActive = source.environment === 'sandbox' ? await Billing.active(Db.get(c.get('db')), source.orgId, 'sandbox') : undefined const { record, token } = await ApiKeys.mint( store, { allowedIps: source.allowedIps, createdBy, environment: source.environment, orgId: source.orgId, ...(source.projectId === undefined ? {} : { projectId: source.projectId }), scopes: source.scopes, ...(billingActive === undefined ? {} : { billingActive }), ...(source.expiresAt === undefined ? {} : { expiresAt: source.expiresAt }), ...(source.name === undefined ? {} : { name: source.name }), ...(source.rateLimits === undefined ? {} : { rateLimits: source.rateLimits }), }, { scopeCatalog: c.get('scopeCatalog') }, ) return c.json( Response.validated(schema.rotateApiKey.Response, { ...serializeKey(record), token }), 200, ) } /** Revokes an API key after its route has verified ownership. */ async function revoke(c: Context, store: Store.State, keyId: string) { const revoked = await ApiKeys.revoke(store, keyId) if (!revoked) return keyNotFound(c) return c.json(Response.validated(schema.revokeApiKey.Response, { id: keyId }), 200) } function keyNotFound(c: Context) { return Response.error(c, { code: 'api_key_not_found', message: 'API key not found', status: 404, }) } function notEnabled(c: Context) { return Response.error(c, { code: 'api_keys_not_enabled', message: 'API keys are not enabled for this Tempo API deployment.', status: 404, }) }