first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
# Page Tokens
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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, unknown>): 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.