/** * Accept signatures made with the pre-1.2.0 canonical form, so links issued * before the upgrade keep working during a migration. Those signatures remain * forgeable as described above; turn this off once old links have expired. */ interface LegacySignatureOption { acceptLegacySignatures?: boolean; } /** * @lacspace/signed-url — secure layer (additive, new in 1.1.0) * * A superset of the classic {@link sign}/{@link verify} and * {@link signUrl}/{@link verifyUrl} helpers that adds, all optional and fully * backward compatible with the same on-the-wire `"."` * token format and the same `?exp=…&sig=…` URL scheme: * * 1. Key rotation — sign with one of several named secrets (a key id is * stamped in the signature) and verify against a key *set*, so secrets can * rotate without invalidating live tokens / URLs. * 2. Binding constraints — optionally bind a signature to an HTTP method, a * client IP and/or a required path prefix; verify enforces whatever was * bound (unbound behaviour is identical to the classic helpers). * 3. One-time / max-uses nonce — embed a random nonce in the signature and a * pure {@link consumeNonce} checker so callers can enforce single-use / * max-N-use against their own store (storage stays the caller's). * 4. Clock tolerance — an optional `clockTolerance` (leeway seconds) on expiry. * 5. Signed claims — attach arbitrary small, tamper-proof metadata that * verify returns. * * Built on the same @lacspace/crypto primitives — no new dependencies. */ /** A named set of secrets for key rotation: `{ "2024": secretA, "2025": secretB }`. */ type KeySet = Record; /** * Optional constraints bound into the signature. Whatever is set here must be * satisfied by the {@link RequestContext} passed to verify, or verification * fails with reason `"binding"`. Anything left unset is not enforced. */ interface BindConstraints { /** Required HTTP method (case-insensitive), e.g. `"POST"`. */ method?: string; /** Required exact client IP, e.g. `"203.0.113.7"`. */ ip?: string; /** Required path prefix the request path must start with, e.g. `"/files/"`. */ pathPrefix?: string; } /** Runtime request context supplied to verify, checked against {@link BindConstraints}. */ interface RequestContext { method?: string; ip?: string; path?: string; } /** Reasons a secure verify can fail — the classic reasons plus rotation/binding. */ type SecureFailReason = FailReason | "unknown-key" | "binding"; interface SecureSignOptions extends ExpiryOptions { /** Single secret (classic mode). Provide this OR `keys`. */ secret?: Secret; /** Named secret set for rotation. When set, `keyId` picks which to sign with. */ keys?: KeySet; /** Which id in `keys` to sign with. Defaults to the first key in `keys`. */ keyId?: string; /** HMAC hash. Default "SHA-256". */ algorithm?: SignAlgorithm; /** Bind the signature to a method / IP / path prefix that verify must match. */ bind?: BindConstraints; /** Embed a random single-use nonce: `true` → 16 bytes, or a byte length. */ nonce?: boolean | number; /** Arbitrary small, tamper-proof metadata returned verbatim by verify. */ claims?: Record; } interface SecureVerifyOptions { /** Single secret (classic mode). Provide this OR `keys`. */ secret?: Secret; /** Named secret set for rotation. The stamped key id is looked up here. */ keys?: KeySet; algorithm?: SignAlgorithm; /** Override "now" (Unix seconds) — for deterministic tests. */ now?: number; /** Allowed clock skew in seconds. Default 0. */ clockTolerance?: number; /** Runtime request context enforced against any bound constraints. */ context?: RequestContext; } interface SecureVerifyResult { valid: boolean; /** The original data, present when `valid`. */ data?: T; /** The signed claims, present when `valid` and claims were attached. */ claims?: Record; /** The embedded nonce (hex), present when one was attached. */ nonce?: string; /** The key id that signed this, present when key rotation was used. */ keyId?: string; /** Why verification failed. */ reason?: SecureFailReason; /** Expiry timestamp (Unix seconds), if the token/URL had one. */ expiresAt?: number; } /** * Sign data into a compact, tamper-proof token like {@link sign}, with optional * key rotation, binding, a single-use nonce, and signed claims. The on-the-wire * format is identical (`"."`), so a token signed with none * of the new options is byte-for-byte what {@link sign} would produce and * verifies with plain {@link verify} too. * * @example * const token = await signSecure({ userId: 42 }, { * keys: { "2025": SECRET_A }, keyId: "2025", * bind: { method: "POST", pathPrefix: "/admin/" }, * nonce: true, claims: { role: "owner" }, expiresIn: 900, * }); */ declare function signSecure(data: TokenData, opts: SecureSignOptions): Promise; /** * Verify a token from {@link signSecure} (or a plain {@link sign} token). * Never throws — returns `{ valid, data?, claims?, nonce?, keyId?, reason?, expiresAt? }`. * Enforces expiry (with optional `clockTolerance`), key rotation (looks the * stamped key id up in `keys`) and any bound constraints against `context`. * * @example * const r = await verifySecure(token, { * keys: { "2024": OLD, "2025": SECRET_A }, * context: { method: req.method, ip: clientIp, path: url.pathname }, * clockTolerance: 30, * }); * if (r.valid) grant(r.data, r.claims); */ declare function verifySecure(token: string, opts: SecureVerifyOptions): Promise>; interface SecureSignUrlOptions extends SecureSignOptions { /** Query param that holds the signature. Default "sig". */ sigParam?: string; /** Query param that holds the expiry. Default "exp". */ expParam?: string; } interface SecureVerifyUrlOptions extends SecureVerifyOptions, LegacySignatureOption { sigParam?: string; expParam?: string; } /** * Like {@link signUrl}, with optional key rotation, binding, a nonce and signed * claims stamped into the query and covered by the signature. A URL signed with * none of the new options is exactly what {@link signUrl} produces. * * @example * const link = await signSecureUrl("https://cdn.me/files/a.pdf?uid=42", { * keys: { "2025": SECRET_A }, keyId: "2025", * bind: { pathPrefix: "/files/" }, claims: { uid: 42 }, expiresIn: 300, * }); */ declare function signSecureUrl(url: string, opts: SecureSignUrlOptions): Promise; /** * Verify a URL from {@link signSecureUrl} (or a plain {@link signUrl} URL). * Never throws. Enforces signature, expiry (with optional `clockTolerance`), * key rotation and any bound constraints against `context`. */ declare function verifySecureUrl(url: string, opts: SecureVerifyUrlOptions): Promise>; /** Generate a random nonce (hex). Default 16 bytes. */ declare function generateNonce(bytes?: number): string; interface ConsumeNonceOptions { /** Maximum allowed uses of the nonce. Default 1 (one-time). */ maxUses?: number; } interface ConsumeNonceResult { /** Whether this use is allowed (i.e. the nonce was not already exhausted). */ ok: boolean; /** The new total use count to persist back to your store. */ uses: number; /** How many uses remain after this one. */ remaining: number; } /** * Pure single-use / max-N-use checker. The library owns no storage: you read the * current use count for a nonce from your own store, call this, and — if `ok` — * write the returned `uses` back. `previousUses` is the count you have stored so * far (undefined / 0 for a never-seen nonce). * * @example * const prior = await store.get(nonce); // number | undefined * const c = consumeNonce(prior, { maxUses: 1 }); * if (!c.ok) return reject("already used"); * await store.set(nonce, c.uses); // persist */ declare function consumeNonce(previousUses: number | undefined, opts?: ConsumeNonceOptions): ConsumeNonceResult; /** * @lacspace/signed-url * * HMAC-signed, expiring URLs & tokens over Web Crypto. Two things developers * re-implement badly on every project: * * 1. Signed *tokens* — compact, tamper-proof strings for magic-login links, * email unsubscribe links, one-time actions, email confirmations. * 2. Signed *URLs* — protect a download / image-proxy / CDN link with an * expiring `?exp=…&sig=…` signature (S3-presigned-style, self-hosted). * * Tamper-proof (any change breaks the signature), timing-safe verification, * optional expiry, zero config. Isomorphic: Node, edge runtimes and browsers. * * Built on @lacspace/crypto (Web Crypto) — never hand-rolled cryptography. */ type SignAlgorithm = "SHA-256" | "SHA-384" | "SHA-512"; type Secret = string | Uint8Array; /** JSON-serialisable token payload. */ type TokenData = string | number | boolean | null | { [key: string]: unknown; } | unknown[]; interface ExpiryOptions { /** Seconds until expiry (relative to now). Ignored when `expiresAt` is set. */ expiresIn?: number; /** Absolute expiry as a Unix timestamp (seconds). */ expiresAt?: number; /** Override "now" (Unix seconds) — for deterministic tests. */ now?: number; } interface SignOptions extends ExpiryOptions { secret: Secret; /** HMAC hash. Default "SHA-256". */ algorithm?: SignAlgorithm; } type FailReason = "malformed" | "bad-signature" | "expired"; interface VerifyResult { valid: boolean; /** The original data (tokens only), present when `valid`. */ data?: T; /** Why verification failed. */ reason?: FailReason; /** Expiry timestamp (Unix seconds), if the token/URL had one. */ expiresAt?: number; } interface VerifyOptions { secret: Secret; algorithm?: SignAlgorithm; /** Override "now" (Unix seconds) — for deterministic tests. */ now?: number; /** Allowed clock skew in seconds. Default 0. */ clockToleranceSec?: number; } /** * Sign arbitrary JSON data into a compact, URL-safe, tamper-proof token: * `"."`. Optionally expiring. * * @example * const token = await sign({ userId: 42, action: "reset-password" }, { * secret: process.env.LINK_SECRET!, expiresIn: 3600, * }); */ declare function sign(data: TokenData, opts: SignOptions): Promise; /** * Verify and decode a token from {@link sign}. Never throws — returns a result * with `valid` and, on failure, a `reason` ("malformed" | "bad-signature" | "expired"). * * @example * const r = await verify<{ userId: number }>(token, { secret }); * if (r.valid) grantAccess(r.data.userId); */ declare function verify(token: string, opts: VerifyOptions): Promise>; /** Convenience: `true` iff the token is valid (signature + not expired). */ declare function isValid(token: string, opts: VerifyOptions): Promise; interface SignUrlOptions extends SignOptions { /** Query param that holds the signature. Default "sig". */ sigParam?: string; /** Query param that holds the expiry. Default "exp". */ expParam?: string; } interface VerifyUrlOptions extends VerifyOptions, LegacySignatureOption { sigParam?: string; expParam?: string; } /** * Append an expiring HMAC signature to a URL — self-hosted presigned URLs for * downloads, image proxies or any link you want to protect from tampering. * Query-param order is normalised, so the link verifies regardless of ordering. * * @example * const link = await signUrl("https://cdn.me/files/report.pdf?uid=42", { * secret, expiresIn: 300, * }); // → …?uid=42&exp=1699999999&sig=AbC… */ declare function signUrl(url: string, opts: SignUrlOptions): Promise; /** * Verify a URL produced by {@link signUrl}: checks the signature (timing-safe) * and expiry. Never throws. * * @example * const r = await verifyUrl(request.url, { secret }); * if (!r.valid) return new Response("Link expired or invalid", { status: 403 }); */ declare function verifyUrl(url: string, opts: VerifyUrlOptions): Promise>; interface MagicLinkOptions extends SignOptions { /** Query param that carries the token. Default "token". */ param?: string; } /** * Build a magic link — a base URL with a signed token embedded as a query param. * Perfect for passwordless login, email verification and one-time actions. * * @example * const link = await magicLink("https://app.me/auth/callback", { email }, { * secret, expiresIn: 900, * }); */ declare function magicLink(baseUrl: string, data: TokenData, opts: MagicLinkOptions): Promise; /** Read & verify the token embedded in a magic link by {@link magicLink}. */ declare function readMagicLink(url: string, opts: VerifyOptions & { param?: string; }): Promise>; export { type BindConstraints, type ConsumeNonceOptions, type ConsumeNonceResult, type ExpiryOptions, type FailReason, type KeySet, type LegacySignatureOption, type MagicLinkOptions, type RequestContext, type Secret, type SecureFailReason, type SecureSignOptions, type SecureSignUrlOptions, type SecureVerifyOptions, type SecureVerifyResult, type SecureVerifyUrlOptions, type SignAlgorithm, type SignOptions, type SignUrlOptions, type TokenData, type VerifyOptions, type VerifyResult, type VerifyUrlOptions, consumeNonce, generateNonce, isValid, magicLink, readMagicLink, sign, signSecure, signSecureUrl, signUrl, verify, verifySecure, verifySecureUrl, verifyUrl };