/** * The built-in API-key scope vocabulary. Hosts append entries through * `App.create({ scopes })`; the merged catalog drives discovery, minting, and * API-key validation. */ import * as z from 'zod/mini' import * as Schema from './internal/Schema.js' /** * Wildcard scope. A key whose `scopes` contains this value satisfies every * required scope, so it never needs re-provisioning when new scopes are added. * Use sparingly — it grants full access. */ export const wildcard = '*' /** Catalog entry describing an issuable API-key scope. */ export type Entry = { /** Human-readable explanation of what the scope grants. */ description: string /** Scope identifier used in a key's `scopes` and in route policies. */ scope: string /** Whether an eligible session may self-mint this scope (else super-admin-only). */ selfServe: boolean } /** * The built-in issuable API-key scopes. {@link wildcard} (`'*'`) is an implicit * grant-all and is intentionally not listed as an issuable scope. */ export const catalog = [ { description: 'Read public chain data: tokens, transactions, transfers, blocks, and receipts.', scope: 'data:read', selfServe: true, }, { description: 'Read organization funding transfers, deposit addresses, and deposits.', scope: 'funding:read', selfServe: false, }, { description: 'Create funding transfers and register their source transactions.', scope: 'funding:write', selfServe: false, }, { description: 'Run ad-hoc queries against the raw indexer passthrough.', scope: 'indexer:query', selfServe: true, }, { description: 'Read organization resources across projects and environments.', scope: 'management:read', selfServe: true, }, { description: 'Create, update, and delete organization resources across projects and environments.', scope: 'management:write', selfServe: true, }, { description: 'Validate and broadcast MPP credentials through Tempo.', scope: 'mpp:write', selfServe: true, }, { description: 'Fill and submit transactions through the relay.', scope: 'rpc-relay:read', selfServe: true, }, { description: 'Sponsor transaction fees through the relay as the managed fee payer.', scope: 'rpc-relay:sponsor', selfServe: true, }, { description: 'Read webhook subscriptions.', scope: 'webhooks:read', selfServe: false, }, { description: 'Create, update, and delete webhook subscriptions.', scope: 'webhooks:write', selfServe: false, }, ] as const satisfies readonly Entry[] /** Templated `/v1/scopes` entry for per-zone read scopes. */ export const zoneEntry = { description: 'Read RPC and indexed data for one zone chain id.', scope: 'zone::read', selfServe: false, } as const satisfies Entry /** Templated `/v1/scopes` entry for per-zone transaction broadcast scopes. */ export const zoneWriteEntry = { description: 'Broadcast signed transactions to one zone chain id.', scope: 'zone::write', selfServe: false, } as const satisfies Entry /** Zod schemas owned by the scope vocabulary. */ export namespace schema { /** A concrete per-zone read or write scope. */ export const Zone = z .union([ z.templateLiteral(['zone:', z.int(), ':read']), z.templateLiteral(['zone:', z.int(), ':write']), ]) .check( z.regex(/^zone:[1-9]\d*:(read|write)$/), z.describe('Per-zone read or write scope (`zone::read|write`).'), ) } /** Per-zone read or write scope, granting access to one zone chain id. */ export type Zone = z.output /** Issuable API-key scope identifier, including host-defined scopes. */ export type Id = (typeof catalog)[number]['scope'] | Zone | (string & {}) /** Scope catalog used by API-key issuance and discovery. */ export type Catalog = readonly Entry[] /** * Scopes an eligible session may self-mint through the developer platform, * derived from the catalog's `selfServe` flag. Route-specific ownership rules * may narrow issuance. Webhooks and {@link wildcard} stay super-admin-only. */ export const selfServe: readonly Id[] = catalog .filter((entry) => entry.selfServe) .map((entry) => entry.scope) /** * Extends the built-in catalog with host-defined scopes. * * @param entries - Host-defined scope entries. * @returns The merged scope catalog. */ export function extend(entries: readonly Entry[] = []): Catalog { const identifiers = new Set() const result = [...catalog, zoneEntry, zoneWriteEntry, ...entries] for (const entry of result) { if (!entry.scope.trim()) throw new Error('Scope identifier must not be empty.') if (identifiers.has(entry.scope)) throw new Error(`Duplicate scope \`${entry.scope}\`.`) identifiers.add(entry.scope) } return result } /** Returns whether a catalog contains a scope identifier. */ export function includes(catalog: Catalog, scope: string): scope is Id { return schema.Zone.safeParse(scope).success || catalog.some((entry) => entry.scope === scope) } /** Adds current read scopes granted by legacy Zone read scopes. */ export function expandAliases(scopes: readonly string[]): readonly string[] { const aliases = new Set() for (const scope of scopes) { const match = /^zone:([1-9]\d*):read$/.exec(scope) if (!match) continue const chainId = Number(match[1]) const resolved = Schema.resolveChainId(chainId) if (resolved !== chainId) aliases.add(`zone:${resolved}:read`) } for (const scope of scopes) aliases.delete(scope) return aliases.size ? [...scopes, ...aliases] : scopes } /** Returns the self-issuable identifiers in a scope catalog. */ export function selfServeFrom(catalog: Catalog): readonly Id[] { return catalog .filter((entry) => entry.selfServe && !schema.Zone.safeParse(entry.scope).success) .map((entry) => entry.scope) }