import * as _exortek_crypto from '@exortek/crypto'; /** * @typedef {'hex' | 'base64url' | 'base58' | 'crockford' | 'ulid' | 'uuid4' | 'uuid7' | 'alphanumeric' | 'prefixed' | 'structured'} OpaqueTokenFormat */ /** * @typedef {object} GenerateOptions * @property {OpaqueTokenFormat} format * @property {number} [bytes=32] Entropy size for byte-based generators (`hex`, `base64url`, `base58`). * @property {number} [length] Output length for `alphanumeric` — defaults to `bytes` if omitted. * @property {keyof typeof GENERATORS} [generator='hex'] Which generator produces the entropy segment of a `prefixed` / `structured` token. * @property {string} [prefix] Required for `format: 'prefixed'` — e.g. `'usr'`. * @property {string} [version] Required for `format: 'structured'` — e.g. `'v1'`. * @property {string} [type] Required for `format: 'structured'` — a literal label embedded in the token, e.g. `'ref'`. * @property {string} [separator='_'] Joins the segments of `prefixed` / `structured` tokens. */ /** * Generate an opaque token. `format` is either a bare generator name * (`'hex'`, `'uuid4'`, …) or a composite shape (`'prefixed'`, * `'structured'`) that wraps a generator's output with a literal label. * * @param {GenerateOptions} options * @returns {string} * * @example * generate({ format: 'hex', bytes: 32 }) * // → '3f9a...' (64 hex chars) * * generate({ format: 'prefixed', prefix: 'usr' }) * // → 'usr_3f9a...' (generator defaults to 'hex') * * generate({ format: 'structured', version: 'v1', type: 'ref', generator: 'ulid', separator: '.' }) * // → 'v1.ref.01ARZ3NDEKTSV4RRFFQ69G5FAV' */ declare function generate(options: GenerateOptions): string; type OpaqueTokenFormat = "hex" | "base64url" | "base58" | "crockford" | "ulid" | "uuid4" | "uuid7" | "alphanumeric" | "prefixed" | "structured"; type GenerateOptions = { format: OpaqueTokenFormat; /** * Entropy size for byte-based generators (`hex`, `base64url`, `base58`). */ bytes?: number | undefined; /** * Output length for `alphanumeric` — defaults to `bytes` if omitted. */ length?: number | undefined; /** * Which generator produces the entropy segment of a `prefixed` / `structured` token. */ generator?: string | undefined; /** * Required for `format: 'prefixed'` — e.g. `'usr'`. */ prefix?: string | undefined; /** * Required for `format: 'structured'` — e.g. `'v1'`. */ version?: string | undefined; /** * Required for `format: 'structured'` — a literal label embedded in the token, e.g. `'ref'`. */ type?: string | undefined; /** * Joins the segments of `prefixed` / `structured` tokens. */ separator?: string | undefined; }; /** * Shared base error class — the single error structure behind every * `@exortek/*` package's `errors.js`. * * Every package keeps its own class identity with a one-liner subclass; * codes stay per-package frozen maps, status mapping is declared as a * static field: * * import { BaseError } from '@exortek/shared/errors'; * * export const ErrorCode = Object.freeze({ * INVALID_ARGUMENT: 'INVALID_ARGUMENT', * INVALID_TOKEN: 'INVALID_TOKEN', * }); * * export class JwtError extends BaseError { * static statuses = { INVALID_ARGUMENT: 400, INVALID_TOKEN: 401 }; * static defaultStatus = 500; * } * * Instances carry a stable machine-readable `code` (branch on this, * never on the message), an optional HTTP `status`, an optional * `details` object, and the standard `cause` chain. */ declare class BaseError extends Error { /** * Optional `code → HTTP status` map declared on the subclass. When * absent the instance carries no `status` at all — for HTTP-agnostic * packages like `@exortek/crypto`. * * @type {Record | undefined} */ static statuses: Record | undefined; /** * Fallback status for codes missing from `statuses`. * * @type {number} */ static defaultStatus: number; /** * @param {string} code Stable machine-readable code; branch on this. * @param {string} message Human-readable diagnostic. Free-form; may * change across versions. * @param {{ cause?: unknown, status?: number, details?: Record }} [options] */ constructor(code: string, message: string, options?: { cause?: unknown; status?: number; details?: Record; }); /** @type {string} */ code: string; /** @type {number | undefined} */ status: number | undefined; /** @type {Record | undefined} */ details: Record | undefined; } declare const ErrorCode: Readonly<{ INVALID_ARGUMENT: "INVALID_ARGUMENT"; }>; declare class OpaqueError extends BaseError { static statuses: { INVALID_ARGUMENT: number; }; } /** * Mint a new opaque token and persist its metadata under the token's * hash. The wire token is returned once — only its hash lives in the * store, mirroring how a leaked DB row can't be turned back into a * usable token. * * @param {CreateOptions} options * @returns {Promise<{ token: string, hash: string, expiresAt?: Date }>} */ declare function create(options: CreateOptions): Promise<{ token: string; hash: string; expiresAt?: Date; }>; /** * @typedef {object} VerifyOptions * @property {OpaqueStore} store * @property {import('@exortek/crypto').HashAlgorithm} [hashAlgo='sha256'] */ /** * Look up a token by its hash. Never throws for a bad/expired/unknown * token — that's a normal outcome, not a programmer error. * * @param {string} token * @param {VerifyOptions} options * @returns {Promise<{ valid: true, metadata: Record } | { valid: false, reason: 'not_found' }>} */ declare function verify(token: string, options: VerifyOptions): Promise<{ valid: true; metadata: Record; } | { valid: false; reason: "not_found"; }>; /** * Delete a token's store entry — it fails every subsequent `verify`. * Idempotent: revoking twice, or a token that never existed, both * just return `false`. * * @param {string} token * @param {VerifyOptions} options * @returns {Promise} */ declare function revoke(token: string, options: VerifyOptions): Promise; /** * Log-safe representation — first 4 / last 4 characters, everything * else replaced with an ellipsis. * * @param {string} token * @returns {string} */ declare function mask(token: string): string; /** * @typedef {object} HandlerOptions * @property {OpaqueStore} store * @property {import('@exortek/crypto').HashAlgorithm} [hashAlgo='sha256'] * @property {string} [tokenField='token'] Field read from the parsed request body. * @property {(err: unknown) => void} [onError] Called if `store.get`/`store.delete` * throws. The handler still returns the no-oracle default so the endpoint * never leaks store-health signals — this hook exists so the app can still * log the failure. */ /** * RFC 7662 §2.2 token introspection. Returns a `{ status, body }` pair * the caller writes to their framework's response however they want — * add CORS headers, wrap in an envelope, whatever. The status is always * `200`, and `body.active` is `false` for a missing/malformed/unknown/ * revoked token, so the caller can't distinguish "invalid" from "doesn't * exist" by status code alone (RFC 7662 §2.2 anti-oracle guidance). * * `req.body` must already be parsed (Express `json()` middleware, * Fastify's built-in JSON parsing). * * @param {HandlerOptions} options * @returns {(req: any) => Promise} */ declare function introspectionHandler(options: HandlerOptions): (req: any) => Promise; /** * RFC 7009 §2.2 token revocation. Always returns * `{ status: 200, body: {} }` — for a successful revoke, an unknown * token, or a missing token field — so the endpoint can't be used to * probe token validity. RFC 7009 §2.2 is explicit: "invalid tokens * do not cause an error response since the client cannot handle such * an error in a reasonable way." * * @param {HandlerOptions} options * @returns {(req: any) => Promise} */ declare function revocationHandler(options: HandlerOptions): (req: any) => Promise; type OpaqueStore = { set: (key: string, value: Record, options?: { expiresIn?: string | number; }) => Promise; /** * Must return `null` once the entry's TTL has passed — expiry is the * store's responsibility, not this package's. */ get: (key: string) => Promise | null>; delete: (key: string) => Promise; }; type CreateOptions = GenerateOptions & { store: OpaqueStore; expiresIn?: string | number; metadata?: Record; hashAlgo?: _exortek_crypto.HashAlgorithm; now?: number; }; type VerifyOptions = { store: OpaqueStore; hashAlgo?: _exortek_crypto.HashAlgorithm; }; type HandlerOptions = { store: OpaqueStore; hashAlgo?: _exortek_crypto.HashAlgorithm; /** * Field read from the parsed request body. */ tokenField?: string | undefined; /** * Called if `store.get`/`store.delete` * throws. The handler still returns the no-oracle default so the endpoint * never leaks store-health signals — this hook exists so the app can still * log the failure. */ onError?: ((err: unknown) => void) | undefined; }; type HandlerResult = { /** * HTTP status the caller should respond with. */ status: number; /** * JSON body to serialize. */ body: Record; /** * Response headers the * caller should merge onto their response. Includes the RFC-mandated * defaults (`Content-Type: application/json` on JSON responses, * `Cache-Control: no-store`, `Pragma: no-cache` — RFC 6749 §5.1 * applied to sensitive token responses). The caller is free to add * more (CORS, request-id, whatever) before writing them out. */ headers: Record; }; export { ErrorCode, OpaqueError, create, generate, introspectionHandler, mask, revocationHandler, revoke, verify }; export type { CreateOptions, HandlerOptions, HandlerResult, OpaqueStore, VerifyOptions };