placemark
v0.1.3
Published
Opaque, tamper-evident pagination cursors — seal state into a URL-safe token, open it with typed failures instead of silent corruption
Maintainers
Readme
placemark
Opaque, tamper-evident pagination cursors — seal state into a URL-safe token, open it with typed failures instead of silent corruption.
The problem
Building pagination that's both secure and user-friendly is hard:
- Server-side sessions → Complex state management, doesn't scale horizontally
- Opaque cursors → Clients can't bookmark or share URLs
- JSON in URLs → Exposes internal structure, encourages tampering
- Database offsets → Break when data changes, inconsistent results
- Custom encoding → Easy to get wrong, security vulnerabilities
You get either complex state management or insecure, unfriendly URLs.
The solution
placemark encodes pagination state into tamper-evident URL-safe tokens:
import placemark from "placemark";
// Create instance with secret for tamper protection
const cursor = placemark({ secret: "my-app-secret-key" });
// Seal pagination state into URL-safe token
const token = cursor.seal({
page: 2,
pageSize: 10,
sort: { field: "created", order: "desc" },
filters: { status: "active" }
});
// → "WzEsbnVsbCwie1wicGFnZVwiOjIsXCJwYWdlU2l6ZVwiOjEw..."
// Open token with automatic validation
const state = cursor.open<{ page: number; pageSize: number }>(token);
// → { page: 2, pageSize: 10, ... }Benefits:
- Zero server-side storage — All state in the token
- Tamper-evident — HMAC prevents modification
- URL-safe — Perfect for query parameters
- Type-safe — Full TypeScript support
- Optional expiration — TTL for time-sensitive tokens
Install
npm install placemark
# or
pnpm add placemark
# or
yarn add placemarkUse
Basic pagination
import placemark from "placemark";
const cursor = placemark({ secret: "my-app-secret-key" });
// Server creates initial token
const token = cursor.seal({ page: 1, pageSize: 10 });
// Client requests next page with token
const state = cursor.open(token);
state.page += 1;
const nextToken = cursor.seal(state);
// Use in URLs
const url = `/api/items?cursor=${encodeURIComponent(nextToken)}`;Advanced pagination with filters
import placemark from "placemark";
const cursor = placemark({
secret: "secret",
ttl: 300000 // 5-minute expiration
});
const complexState = {
page: 1,
pageSize: 25,
sort: { field: "created", order: "desc" },
filters: {
status: ["active", "pending"],
category: "electronics",
priceRange: { min: 0, max: 1000 }
}
};
const token = cursor.seal(complexState);
const restored = cursor.open(token); // Perfect restorationRESTful API example
import placemark from "placemark";
const apiCursor = placemark({ secret: "api-secret" });
app.get("/api/items", (req, res) => {
let state = { page: 1, pageSize: 10 };
if (req.query.cursor) {
try {
state = apiCursor.open(req.query.cursor);
} catch (error) {
return res.status(400).json({ error: "Invalid cursor" });
}
}
const items = await fetchItems(state);
const nextState = { ...state, page: state.page + 1 };
const nextCursor = apiCursor.seal(nextState);
res.json({
items,
nextCursor
});
});Error handling
import placemark from "placemark";
import {
MalformedCursor,
TamperedCursor,
ExpiredCursor,
VersionMismatch
} from "placemark";
const cursor = placemark({ secret: "secret" });
try {
const state = cursor.open(token);
} catch (error) {
if (error instanceof MalformedCursor) {
// Invalid token format
} else if (error instanceof TamperedCursor) {
// Token was tampered with
} else if (error instanceof ExpiredCursor) {
// Token has expired (TTL)
} else if (error instanceof VersionMismatch) {
// Version incompatibility
}
}Encoded-only mode (no tamper protection)
import placemark from "placemark";
// No secret = encoded-only mode
const cursor = placemark();
const token = cursor.seal({ page: 1, pageSize: 10 });
const state = cursor.open(token); // Works, but no tamper protectionAPI
Core functions
placemark(options?: PlacemarkOptions): Placemark
Creates a placemark instance for encoding/decoding tokens.
Options:
secret?: string | Uint8Array— Secret key for HMAC validation (omit for encoded-only mode)version?: number— Version number for format changes (default: 1)ttl?: number— Time-to-live in milliseconds (requires issuedAt stamping)clock?: () => number— Clock function for time injection (default: Date.now)
Placemark.seal<T>(data: T): string
Encodes JSON-serializable data into a URL-safe token string.
- With secret:
payload.macformat with HMAC protection - Without secret:
payloadformat (encoded-only)
Placemark.open<T>(cursor: string): T
Decodes a token and returns the original data.
Throws:
MalformedCursor— Invalid token formatTamperedCursor— HMAC validation failed (secret mode only)ExpiredCursor— Token expired (TTL mode only)VersionMismatch— Version mismatch
Error classes
MalformedCursor
Invalid token format or structure.
TamperedCursor
Token failed HMAC validation (indicates tampering).
ExpiredCursor
Token exceeded its time-to-live.
VersionMismatch
Token version doesn't match expected version.
Usage patterns
URL parameter integration
import placemark from "placemark";
const cursor = placemark({ secret: "secret" });
// Server sends URL to client
const pageToken = cursor.seal({ page: 1, pageSize: 10 });
const url = `https://api.example.com/items?cursor=${encodeURIComponent(pageToken)}`;
// Client requests next page
const params = new URLSearchParams(url.split("?")[1]);
const token = params.get("cursor");
const state = cursor.open(token);Clock manipulation for testing
let currentTime = 1000000;
const clock = () => currentTime;
const cursor = placemark({
secret: "secret",
ttl: 60000, // 1 minute
clock
});
const token = cursor.seal({ page: 1 });
currentTime += 120000; // Advance 2 minutes
cursor.open(token); // Throws ExpiredCursorVersion migration strategy
// V1 tokens
const v1Cursor = placemark({ secret: "secret", version: 1 });
// V2 tokens (new format)
const v2Cursor = placemark({ secret: "secret", version: 2 });
// Migration function
function migrateToken(oldToken: string) {
try {
return v2Cursor.open(oldToken);
} catch (error) {
if (error instanceof VersionMismatch) {
const oldState = v1Cursor.open(oldToken);
return v2Cursor.seal(oldState);
}
throw error;
}
}Performance
placemark is optimized for high-traffic applications:
- Encoding: ~0.006ms per token
- Decoding: ~0.006ms per token
- Token size: 50-500 chars depending on data size
- Memory: Zero retained state per token
Suitable for production APIs with thousands of requests per second.
Non-goals
By design, placemark focuses on one thing: encoding state into tamper-evident tokens. These features are explicitly out of scope:
- Encryption — Tokens are encoded, not encrypted. Use encryption libraries for sensitive data.
- Compression — Large payloads produce large tokens. Compress data before sealing if needed.
- Signature algorithms — Only HMAC-SHA256 for tamper evidence.
- Token storage — No server-side token storage or tracking.
- Session management — Use session libraries for complex session handling.
- Rate limiting — Use rate limiting libraries for API protection.
If you need these features, compose placemark with specialized libraries.
Related Packages
Caching & Concurrency:
- @azghr/filterkit — Framework-agnostic, type-safe filtering for TypeScript
- @azghr/singlet — Deduplicate concurrent async calls
- staleness — Stale-while-revalidate caching for async functions
Text Processing:
- @azghr/shorn — Truncate strings by byte budget without breaking graphemes
- seriatim — Sequential processing utilities
HTTP & Network:
- forbear — Read server rate-limit instructions from HTTP responses
- forestall — Delay execution until a condition is met
- obviate — Render operations unnecessary through caching
System & Process:
- quiesce — Ordered, timeboxed graceful shutdown for Node
- sortition — Deterministic percentage rollouts and A/B bucketing
- stanch — Stop flows or operations based on conditions
Utilities:
- expunge — Remove or exclude items from collections
- occlude — Hide or mask data and functionality
- specie — Currency and financial calculations
License
MIT
