# Page Tokens _(no submodules)_ ## Purpose The page tokens module provides opaque, URL-safe token strings that carry all the state needed to resume a paginated query: the cursor or offset value, sort field and direction, and an optional filter hash for cache invalidation. Tokens are encoded as base64url JSON so they survive URL query-string round-trips without additional percent-encoding. This module sits above the raw cursor encoding in `cursor-pagination.ts`: where that module encodes a single value, page tokens encode a structured payload that includes sort and filter metadata — making them suitable for stateless pagination APIs where the client must carry all context. ## Features - `encodePageToken` / `decodePageToken` — full payload round-trip with type validation. - `createCursorToken` — create a token from a domain value (ID, score, timestamp) and optional sort/filter info. - `createOffsetToken` — shortcut for offset-based tokens. - `extractCursorValue` — pull just the value field from a token. - `extractSortInfo` — pull sort field and direction from a token. - `hashFilters` — stable djb2 hash of a filter map for cache-invalidation. - `validateTokenFilter` — check that a token's filter hash matches an expected value. ## Usage: Basic Example ```typescript import { createCursorToken, decodePageToken } from "@suaveplan/pagination/tokens"; const token = createCursorToken("user_99", "createdAt", "desc"); // → opaque base64url string const payload = decodePageToken(token); // → { v: "user_99", s: "createdAt", d: "desc" } ``` ## Usage: Advanced Example ```typescript import { createCursorToken, hashFilters, validateTokenFilter, extractSortInfo, } from "@suaveplan/pagination/tokens"; const filters = { status: "active", role: "admin" }; const hash = hashFilters(filters); // → stable hash string, e.g. "1q7f9k" const token = createCursorToken("cursor_abc", "name", "asc", hash); // On the next request, verify the filter hasn't changed const valid = validateTokenFilter(token, hash); // → true const invalid = validateTokenFilter(token, "differenthash"); // → false // Extract sort metadata to rebuild the query const sort = extractSortInfo(token); // → { sortBy: "name", sortDirection: "asc" } ``` ## API Reference ### `encodePageToken(payload: PageTokenPayload): string` Serialises the payload to JSON and encodes as base64url. The payload must have a `v` field (string or number); `s`, `d`, `f` are optional. ### `decodePageToken(token: string): PageTokenPayload | null` Decodes and validates a token. Returns `null` for any invalid input, never throws. ### `createCursorToken(value, sortBy?, sortDirection?, filterHash?): string` Builds and encodes a `PageTokenPayload` from a cursor value and optional sort/filter metadata. ### `createOffsetToken(offset, sortBy?, sortDirection?, filterHash?): string` Alias of `createCursorToken` with `offset: number` as the first argument. ### `extractCursorValue(token: string): string | number | null` Returns the `v` field of the decoded token, or `null` if the token is invalid. ### `extractSortInfo(token: string): { sortBy?: string; sortDirection?: SortDirection } | null` Returns the `s` and `d` fields of the decoded token, or `null` if the token is invalid. ### `hashFilters(filters: Record): string` Sorts keys lexicographically, concatenates `key=value` pairs, and applies a djb2 hash. Returns the absolute hash as a base-36 string. Suitable for cache-invalidation only — not cryptographic. ### `validateTokenFilter(token: string, expectedFilterHash: string): boolean` Returns `true` when the token is valid and its `f` field equals `expectedFilterHash`. Returns `true` (unconditional) when the token has no `f` field. Returns `false` for invalid tokens or mismatched hashes. ## Implementation Notes The djb2 hash in `hashFilters` uses bitwise OR with `0` to keep the accumulator within signed 32-bit integer range — preventing floating-point precision loss for long filter strings. The filter hash is NOT a cryptographic MAC; it cannot prevent token forgery. Use HMAC-signed tokens if forgery resistance is required.