@oncely/next 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pages.ts"],"names":["MemoryStorage","hashObject","parseTtl","IdempotencyError","ConflictError","MissingKeyError","MismatchError","oncely"],"mappings":"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","file":"pages.cjs","sourcesContent":["/**\n * @oncely/next/pages - Next.js Pages Router integration for oncely idempotency\n *\n * Provides a wrapper for Next.js Pages Router API routes with idempotency protection.\n *\n * @example\n * ```typescript\n * // pages/api/orders.ts\n * import { pages } from '@oncely/next/pages';\n *\n * export default pages()(async (req, res) => {\n * const order = await createOrder(req.body);\n * res.status(201).json(order);\n * });\n * ```\n */\n\nimport type { NextApiRequest, NextApiResponse } from 'next';\nimport {\n oncely,\n MemoryStorage,\n hashObject,\n parseTtl,\n IdempotencyError,\n ConflictError,\n MissingKeyError,\n MismatchError,\n type StorageAdapter,\n type OncelyOptions,\n type StoredResponse,\n} from '@oncely/core';\n\n// RFC 7807 Problem Details type URL base\nconst PROBLEM_BASE = 'https://oncely.dev/errors';\n\n/**\n * IETF standard header names\n */\nexport const IDEMPOTENCY_KEY_HEADER = 'Idempotency-Key';\nexport const IDEMPOTENCY_REPLAY_HEADER = 'Idempotency-Replay';\n\n/**\n * Options for the Next.js Pages Router wrapper\n */\nexport interface PagesMiddlewareOptions {\n /**\n * Storage adapter for persisting idempotency records.\n * @default MemoryStorage\n */\n storage?: StorageAdapter;\n\n /**\n * TTL for idempotency records in milliseconds or duration string.\n * @default '24h'\n */\n ttl?: number | string;\n\n /**\n * Whether to require an idempotency key.\n * If true, returns 400 when key is missing.\n * @default false\n */\n required?: boolean;\n\n /**\n * Custom function to extract idempotency key from request.\n * @default Reads Idempotency-Key header\n */\n getKey?: (req: NextApiRequest) => string | undefined | null;\n\n /**\n * Custom function to generate hash from request for mismatch detection.\n * @default Hashes request body as JSON\n */\n getHash?: (req: NextApiRequest) => Promise<string | null> | string | null;\n\n /**\n * Whether to add idempotency headers to responses.\n * @default true\n */\n includeHeaders?: boolean;\n\n /**\n * Debug mode for logging.\n * @default false\n */\n debug?: boolean;\n\n /**\n * Callback when a cached response is returned.\n */\n onHit?: OncelyOptions['onHit'];\n\n /**\n * Callback when a new response is generated.\n */\n onMiss?: OncelyOptions['onMiss'];\n\n /**\n * Callback when an error occurs.\n */\n onError?: OncelyOptions['onError'];\n\n /**\n * If true, proceed with the request when storage fails (e.g., Redis down).\n * When false (default), storage errors return 500.\n * Use with non-critical idempotency where availability is more important.\n * @default false\n */\n failOpen?: boolean;\n}\n\n/**\n * Stored response data for replay\n */\ninterface CachedResponse {\n status: number;\n headers: Record<string, string>;\n body: unknown;\n}\n\n/**\n * API route handler function signature\n */\ntype ApiHandler = (\n req: NextApiRequest,\n res: NextApiResponse\n) => Promise<void | NextApiResponse> | void | NextApiResponse;\n\n// Shared default storage instance\nlet defaultStorage: StorageAdapter | null = null;\n\nfunction getStorage(provided?: StorageAdapter): StorageAdapter {\n if (provided) return provided;\n if (!defaultStorage) {\n defaultStorage = new MemoryStorage();\n }\n return defaultStorage;\n}\n\n/**\n * Default hash function that hashes request body\n */\nfunction defaultGetHash(req: NextApiRequest): string | null {\n if (req.body && typeof req.body === 'object' && Object.keys(req.body).length > 0) {\n return hashObject(req.body);\n }\n return null;\n}\n\n/**\n * Create a Next.js Pages Router API handler wrapper with idempotency protection.\n *\n * @example\n * ```typescript\n * // pages/api/orders.ts\n * import { pages } from '@oncely/next/pages';\n *\n * // Basic usage\n * export default pages()(async (req, res) => {\n * const order = await createOrder(req.body);\n * res.status(201).json(order);\n * });\n *\n * // With options\n * export default pages({ required: true, ttl: '1h' })(async (req, res) => {\n * const result = await processPayment(req.body);\n * res.json(result);\n * });\n * ```\n */\nexport function pages(options: PagesMiddlewareOptions = {}): (handler: ApiHandler) => ApiHandler {\n const {\n storage: providedStorage,\n ttl = '24h',\n required = false,\n getKey = (req) => {\n const header = req.headers[IDEMPOTENCY_KEY_HEADER.toLowerCase()];\n return Array.isArray(header) ? header[0] : header;\n },\n getHash = defaultGetHash,\n includeHeaders = true,\n debug = false,\n failOpen = false,\n onHit,\n onMiss,\n onError,\n } = options;\n\n const storage = getStorage(providedStorage);\n const ttlMs = parseTtl(ttl);\n\n const log = (message: string) => {\n if (debug) {\n console.log(`[oncely/next/pages] ${message}`);\n }\n };\n\n return (handler: ApiHandler): ApiHandler => {\n return async (req: NextApiRequest, res: NextApiResponse): Promise<void | NextApiResponse> => {\n // Get idempotency key\n const key = getKey(req);\n\n // Skip if no key and not required\n if (!key && !required) {\n return handler(req, res);\n }\n\n // Return 400 if key required but not provided\n if (!key && required) {\n if (includeHeaders) {\n res.setHeader('Content-Type', 'application/problem+json');\n }\n return res.status(400).json({\n type: `${PROBLEM_BASE}/key-required`,\n title: 'Idempotency Key Required',\n status: 400,\n detail: `The ${IDEMPOTENCY_KEY_HEADER} header is required for this request.`,\n });\n }\n\n // Compute request hash\n const hash = await getHash(req);\n\n try {\n log(`Acquiring lock for key: ${key}`);\n let result;\n try {\n result = await storage.acquire(key!, hash, ttlMs);\n } catch (storageErr) {\n // Storage failed (e.g., Redis down)\n if (failOpen) {\n log(`Storage error for key: ${key}, failing open`);\n onError?.(key!, storageErr as Error);\n return handler(req, res); // Proceed without idempotency protection\n }\n throw storageErr;\n }\n\n if (result.status === 'hit') {\n // Return cached response\n log(`Cache hit for key: ${key}`);\n onHit?.(key!, result.response);\n\n const cached = result.response.data as CachedResponse;\n\n // Set headers\n Object.entries(cached.headers).forEach(([headerKey, value]) => {\n res.setHeader(headerKey, value);\n });\n\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n res.setHeader(IDEMPOTENCY_REPLAY_HEADER, 'true');\n }\n\n return res.status(cached.status).json(cached.body);\n }\n\n if (result.status === 'conflict') {\n log(`Conflict for key: ${key}`);\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n res.setHeader('Retry-After', '1');\n }\n return res.status(409).json({\n type: `${PROBLEM_BASE}/conflict`,\n title: 'Conflict',\n status: 409,\n detail: 'A request with this idempotency key is already being processed.',\n });\n }\n\n if (result.status === 'mismatch') {\n log(`Hash mismatch for key: ${key}`);\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n }\n return res.status(422).json({\n type: `${PROBLEM_BASE}/mismatch`,\n title: 'Idempotency Key Mismatch',\n status: 422,\n detail:\n 'The request body does not match the original request for this idempotency key.',\n existingHash: result.existingHash,\n providedHash: result.providedHash,\n });\n }\n\n // status === 'acquired' - execute handler\n log(`Lock acquired for key: ${key}`);\n onMiss?.(key!);\n\n // Intercept response methods to capture the response\n let capturedStatus = 200;\n const capturedHeaders: Record<string, string> = {};\n let capturedBody: unknown;\n\n const originalStatus = res.status.bind(res);\n const originalSetHeader = res.setHeader.bind(res);\n const originalJson = res.json.bind(res);\n const originalSend = res.send.bind(res);\n\n res.status = (statusCode: number) => {\n capturedStatus = statusCode;\n return originalStatus(statusCode);\n };\n\n res.setHeader = (name: string, value: string | number | readonly string[]) => {\n if (typeof value === 'string') {\n capturedHeaders[name] = value;\n } else if (Array.isArray(value)) {\n capturedHeaders[name] = value.join(', ');\n } else {\n capturedHeaders[name] = String(value);\n }\n return originalSetHeader(name, value);\n };\n\n res.json = (body: unknown) => {\n capturedBody = body;\n\n // Save to storage\n const cached: CachedResponse = {\n status: capturedStatus,\n headers: capturedHeaders,\n body: capturedBody,\n };\n\n const storedResponse: StoredResponse = {\n data: cached,\n createdAt: Date.now(),\n hash,\n };\n\n storage.save(key!, storedResponse).catch((err) => {\n log(`Failed to save response for key: ${key}`);\n onError?.(key!, err as Error);\n });\n\n // Add idempotency headers\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n }\n\n return originalJson(body);\n };\n\n res.send = (body: unknown) => {\n capturedBody = body;\n\n // Save to storage\n const cached: CachedResponse = {\n status: capturedStatus,\n headers: capturedHeaders,\n body: capturedBody,\n };\n\n const storedResponse: StoredResponse = {\n data: cached,\n createdAt: Date.now(),\n hash,\n };\n\n storage.save(key!, storedResponse).catch((err) => {\n log(`Failed to save response for key: ${key}`);\n onError?.(key!, err as Error);\n });\n\n // Add idempotency headers\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n }\n\n return originalSend(body);\n };\n\n try {\n return await handler(req, res);\n } catch (err) {\n // Release lock on error\n log(`Handler error for key: ${key}, releasing lock`);\n await storage.release(key!).catch(() => {});\n throw err;\n }\n } catch (err) {\n // Handle idempotency errors\n if (err instanceof IdempotencyError) {\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n if (err instanceof ConflictError) {\n res.setHeader('Retry-After', String(err.retryAfter ?? 1));\n }\n }\n\n let type = `${PROBLEM_BASE}/error`;\n if (err instanceof MissingKeyError) type = `${PROBLEM_BASE}/key-required`;\n else if (err instanceof ConflictError) type = `${PROBLEM_BASE}/conflict`;\n else if (err instanceof MismatchError) type = `${PROBLEM_BASE}/mismatch`;\n\n return res.status(err.statusCode).json({\n type,\n title: err.name,\n status: err.statusCode,\n detail: err.message,\n });\n }\n\n throw err;\n }\n };\n };\n}\n\n/**\n * Create a pre-configured wrapper factory with default options.\n *\n * @example\n * ```typescript\n * // lib/idempotency.ts\n * import { configure } from '@oncely/next/pages';\n * import { redis } from '@oncely/redis';\n *\n * export const idempotent = configure({\n * storage: redis(),\n * ttl: '1h',\n * });\n *\n * // pages/api/orders.ts\n * import { idempotent } from '@/lib/idempotency';\n *\n * export default idempotent()(async (req, res) => {\n * res.status(201).json(await createOrder(req.body));\n * });\n * ```\n */\nexport function configure(\n defaults: PagesMiddlewareOptions\n): (overrides?: Partial<PagesMiddlewareOptions>) => (handler: ApiHandler) => ApiHandler {\n return (overrides = {}) => pages({ ...defaults, ...overrides });\n}\n\n// Augment oncely namespace\ndeclare module '@oncely/core' {\n interface OncelyNamespace {\n /**\n * Create Next.js Pages Router wrapper with idempotency.\n */\n pages: typeof pages;\n }\n}\n\n// Register on oncely namespace\n(oncely as Record<string, unknown>).pages = pages;\n\n// Re-export types and utilities\nexport {\n oncely,\n MemoryStorage,\n hashObject,\n IdempotencyError,\n MissingKeyError,\n ConflictError,\n MismatchError,\n type StorageAdapter,\n type OncelyOptions,\n} from '@oncely/core';\n"]}
@@ -0,0 +1,145 @@
1
+ import { NextApiRequest, NextApiResponse } from 'next';
2
+ import { StorageAdapter, OncelyOptions } from '@oncely/core';
3
+ export { ConflictError, IdempotencyError, MemoryStorage, MismatchError, MissingKeyError, OncelyOptions, StorageAdapter, hashObject, oncely } from '@oncely/core';
4
+
5
+ /**
6
+ * @oncely/next/pages - Next.js Pages Router integration for oncely idempotency
7
+ *
8
+ * Provides a wrapper for Next.js Pages Router API routes with idempotency protection.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // pages/api/orders.ts
13
+ * import { pages } from '@oncely/next/pages';
14
+ *
15
+ * export default pages()(async (req, res) => {
16
+ * const order = await createOrder(req.body);
17
+ * res.status(201).json(order);
18
+ * });
19
+ * ```
20
+ */
21
+
22
+ /**
23
+ * IETF standard header names
24
+ */
25
+ declare const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
26
+ declare const IDEMPOTENCY_REPLAY_HEADER = "Idempotency-Replay";
27
+ /**
28
+ * Options for the Next.js Pages Router wrapper
29
+ */
30
+ interface PagesMiddlewareOptions {
31
+ /**
32
+ * Storage adapter for persisting idempotency records.
33
+ * @default MemoryStorage
34
+ */
35
+ storage?: StorageAdapter;
36
+ /**
37
+ * TTL for idempotency records in milliseconds or duration string.
38
+ * @default '24h'
39
+ */
40
+ ttl?: number | string;
41
+ /**
42
+ * Whether to require an idempotency key.
43
+ * If true, returns 400 when key is missing.
44
+ * @default false
45
+ */
46
+ required?: boolean;
47
+ /**
48
+ * Custom function to extract idempotency key from request.
49
+ * @default Reads Idempotency-Key header
50
+ */
51
+ getKey?: (req: NextApiRequest) => string | undefined | null;
52
+ /**
53
+ * Custom function to generate hash from request for mismatch detection.
54
+ * @default Hashes request body as JSON
55
+ */
56
+ getHash?: (req: NextApiRequest) => Promise<string | null> | string | null;
57
+ /**
58
+ * Whether to add idempotency headers to responses.
59
+ * @default true
60
+ */
61
+ includeHeaders?: boolean;
62
+ /**
63
+ * Debug mode for logging.
64
+ * @default false
65
+ */
66
+ debug?: boolean;
67
+ /**
68
+ * Callback when a cached response is returned.
69
+ */
70
+ onHit?: OncelyOptions['onHit'];
71
+ /**
72
+ * Callback when a new response is generated.
73
+ */
74
+ onMiss?: OncelyOptions['onMiss'];
75
+ /**
76
+ * Callback when an error occurs.
77
+ */
78
+ onError?: OncelyOptions['onError'];
79
+ /**
80
+ * If true, proceed with the request when storage fails (e.g., Redis down).
81
+ * When false (default), storage errors return 500.
82
+ * Use with non-critical idempotency where availability is more important.
83
+ * @default false
84
+ */
85
+ failOpen?: boolean;
86
+ }
87
+ /**
88
+ * API route handler function signature
89
+ */
90
+ type ApiHandler = (req: NextApiRequest, res: NextApiResponse) => Promise<void | NextApiResponse> | void | NextApiResponse;
91
+ /**
92
+ * Create a Next.js Pages Router API handler wrapper with idempotency protection.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // pages/api/orders.ts
97
+ * import { pages } from '@oncely/next/pages';
98
+ *
99
+ * // Basic usage
100
+ * export default pages()(async (req, res) => {
101
+ * const order = await createOrder(req.body);
102
+ * res.status(201).json(order);
103
+ * });
104
+ *
105
+ * // With options
106
+ * export default pages({ required: true, ttl: '1h' })(async (req, res) => {
107
+ * const result = await processPayment(req.body);
108
+ * res.json(result);
109
+ * });
110
+ * ```
111
+ */
112
+ declare function pages(options?: PagesMiddlewareOptions): (handler: ApiHandler) => ApiHandler;
113
+ /**
114
+ * Create a pre-configured wrapper factory with default options.
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * // lib/idempotency.ts
119
+ * import { configure } from '@oncely/next/pages';
120
+ * import { redis } from '@oncely/redis';
121
+ *
122
+ * export const idempotent = configure({
123
+ * storage: redis(),
124
+ * ttl: '1h',
125
+ * });
126
+ *
127
+ * // pages/api/orders.ts
128
+ * import { idempotent } from '@/lib/idempotency';
129
+ *
130
+ * export default idempotent()(async (req, res) => {
131
+ * res.status(201).json(await createOrder(req.body));
132
+ * });
133
+ * ```
134
+ */
135
+ declare function configure(defaults: PagesMiddlewareOptions): (overrides?: Partial<PagesMiddlewareOptions>) => (handler: ApiHandler) => ApiHandler;
136
+ declare module '@oncely/core' {
137
+ interface OncelyNamespace {
138
+ /**
139
+ * Create Next.js Pages Router wrapper with idempotency.
140
+ */
141
+ pages: typeof pages;
142
+ }
143
+ }
144
+
145
+ export { IDEMPOTENCY_KEY_HEADER, IDEMPOTENCY_REPLAY_HEADER, type PagesMiddlewareOptions, configure, pages };
@@ -0,0 +1,145 @@
1
+ import { NextApiRequest, NextApiResponse } from 'next';
2
+ import { StorageAdapter, OncelyOptions } from '@oncely/core';
3
+ export { ConflictError, IdempotencyError, MemoryStorage, MismatchError, MissingKeyError, OncelyOptions, StorageAdapter, hashObject, oncely } from '@oncely/core';
4
+
5
+ /**
6
+ * @oncely/next/pages - Next.js Pages Router integration for oncely idempotency
7
+ *
8
+ * Provides a wrapper for Next.js Pages Router API routes with idempotency protection.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // pages/api/orders.ts
13
+ * import { pages } from '@oncely/next/pages';
14
+ *
15
+ * export default pages()(async (req, res) => {
16
+ * const order = await createOrder(req.body);
17
+ * res.status(201).json(order);
18
+ * });
19
+ * ```
20
+ */
21
+
22
+ /**
23
+ * IETF standard header names
24
+ */
25
+ declare const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
26
+ declare const IDEMPOTENCY_REPLAY_HEADER = "Idempotency-Replay";
27
+ /**
28
+ * Options for the Next.js Pages Router wrapper
29
+ */
30
+ interface PagesMiddlewareOptions {
31
+ /**
32
+ * Storage adapter for persisting idempotency records.
33
+ * @default MemoryStorage
34
+ */
35
+ storage?: StorageAdapter;
36
+ /**
37
+ * TTL for idempotency records in milliseconds or duration string.
38
+ * @default '24h'
39
+ */
40
+ ttl?: number | string;
41
+ /**
42
+ * Whether to require an idempotency key.
43
+ * If true, returns 400 when key is missing.
44
+ * @default false
45
+ */
46
+ required?: boolean;
47
+ /**
48
+ * Custom function to extract idempotency key from request.
49
+ * @default Reads Idempotency-Key header
50
+ */
51
+ getKey?: (req: NextApiRequest) => string | undefined | null;
52
+ /**
53
+ * Custom function to generate hash from request for mismatch detection.
54
+ * @default Hashes request body as JSON
55
+ */
56
+ getHash?: (req: NextApiRequest) => Promise<string | null> | string | null;
57
+ /**
58
+ * Whether to add idempotency headers to responses.
59
+ * @default true
60
+ */
61
+ includeHeaders?: boolean;
62
+ /**
63
+ * Debug mode for logging.
64
+ * @default false
65
+ */
66
+ debug?: boolean;
67
+ /**
68
+ * Callback when a cached response is returned.
69
+ */
70
+ onHit?: OncelyOptions['onHit'];
71
+ /**
72
+ * Callback when a new response is generated.
73
+ */
74
+ onMiss?: OncelyOptions['onMiss'];
75
+ /**
76
+ * Callback when an error occurs.
77
+ */
78
+ onError?: OncelyOptions['onError'];
79
+ /**
80
+ * If true, proceed with the request when storage fails (e.g., Redis down).
81
+ * When false (default), storage errors return 500.
82
+ * Use with non-critical idempotency where availability is more important.
83
+ * @default false
84
+ */
85
+ failOpen?: boolean;
86
+ }
87
+ /**
88
+ * API route handler function signature
89
+ */
90
+ type ApiHandler = (req: NextApiRequest, res: NextApiResponse) => Promise<void | NextApiResponse> | void | NextApiResponse;
91
+ /**
92
+ * Create a Next.js Pages Router API handler wrapper with idempotency protection.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // pages/api/orders.ts
97
+ * import { pages } from '@oncely/next/pages';
98
+ *
99
+ * // Basic usage
100
+ * export default pages()(async (req, res) => {
101
+ * const order = await createOrder(req.body);
102
+ * res.status(201).json(order);
103
+ * });
104
+ *
105
+ * // With options
106
+ * export default pages({ required: true, ttl: '1h' })(async (req, res) => {
107
+ * const result = await processPayment(req.body);
108
+ * res.json(result);
109
+ * });
110
+ * ```
111
+ */
112
+ declare function pages(options?: PagesMiddlewareOptions): (handler: ApiHandler) => ApiHandler;
113
+ /**
114
+ * Create a pre-configured wrapper factory with default options.
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * // lib/idempotency.ts
119
+ * import { configure } from '@oncely/next/pages';
120
+ * import { redis } from '@oncely/redis';
121
+ *
122
+ * export const idempotent = configure({
123
+ * storage: redis(),
124
+ * ttl: '1h',
125
+ * });
126
+ *
127
+ * // pages/api/orders.ts
128
+ * import { idempotent } from '@/lib/idempotency';
129
+ *
130
+ * export default idempotent()(async (req, res) => {
131
+ * res.status(201).json(await createOrder(req.body));
132
+ * });
133
+ * ```
134
+ */
135
+ declare function configure(defaults: PagesMiddlewareOptions): (overrides?: Partial<PagesMiddlewareOptions>) => (handler: ApiHandler) => ApiHandler;
136
+ declare module '@oncely/core' {
137
+ interface OncelyNamespace {
138
+ /**
139
+ * Create Next.js Pages Router wrapper with idempotency.
140
+ */
141
+ pages: typeof pages;
142
+ }
143
+ }
144
+
145
+ export { IDEMPOTENCY_KEY_HEADER, IDEMPOTENCY_REPLAY_HEADER, type PagesMiddlewareOptions, configure, pages };
package/dist/pages.js ADDED
@@ -0,0 +1,221 @@
1
+ import { oncely, parseTtl, IdempotencyError, ConflictError, hashObject, MemoryStorage, MissingKeyError, MismatchError } from '@oncely/core';
2
+ export { ConflictError, IdempotencyError, MemoryStorage, MismatchError, MissingKeyError, hashObject, oncely } from '@oncely/core';
3
+
4
+ // src/pages.ts
5
+ var PROBLEM_BASE = "https://oncely.dev/errors";
6
+ var IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
7
+ var IDEMPOTENCY_REPLAY_HEADER = "Idempotency-Replay";
8
+ var defaultStorage = null;
9
+ function getStorage(provided) {
10
+ if (provided) return provided;
11
+ if (!defaultStorage) {
12
+ defaultStorage = new MemoryStorage();
13
+ }
14
+ return defaultStorage;
15
+ }
16
+ function defaultGetHash(req) {
17
+ if (req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) {
18
+ return hashObject(req.body);
19
+ }
20
+ return null;
21
+ }
22
+ function pages(options = {}) {
23
+ const {
24
+ storage: providedStorage,
25
+ ttl = "24h",
26
+ required = false,
27
+ getKey = (req) => {
28
+ const header = req.headers[IDEMPOTENCY_KEY_HEADER.toLowerCase()];
29
+ return Array.isArray(header) ? header[0] : header;
30
+ },
31
+ getHash = defaultGetHash,
32
+ includeHeaders = true,
33
+ debug = false,
34
+ failOpen = false,
35
+ onHit,
36
+ onMiss,
37
+ onError
38
+ } = options;
39
+ const storage = getStorage(providedStorage);
40
+ const ttlMs = parseTtl(ttl);
41
+ const log = (message) => {
42
+ if (debug) {
43
+ console.log(`[oncely/next/pages] ${message}`);
44
+ }
45
+ };
46
+ return (handler) => {
47
+ return async (req, res) => {
48
+ const key = getKey(req);
49
+ if (!key && !required) {
50
+ return handler(req, res);
51
+ }
52
+ if (!key && required) {
53
+ if (includeHeaders) {
54
+ res.setHeader("Content-Type", "application/problem+json");
55
+ }
56
+ return res.status(400).json({
57
+ type: `${PROBLEM_BASE}/key-required`,
58
+ title: "Idempotency Key Required",
59
+ status: 400,
60
+ detail: `The ${IDEMPOTENCY_KEY_HEADER} header is required for this request.`
61
+ });
62
+ }
63
+ const hash = await getHash(req);
64
+ try {
65
+ log(`Acquiring lock for key: ${key}`);
66
+ let result;
67
+ try {
68
+ result = await storage.acquire(key, hash, ttlMs);
69
+ } catch (storageErr) {
70
+ if (failOpen) {
71
+ log(`Storage error for key: ${key}, failing open`);
72
+ onError?.(key, storageErr);
73
+ return handler(req, res);
74
+ }
75
+ throw storageErr;
76
+ }
77
+ if (result.status === "hit") {
78
+ log(`Cache hit for key: ${key}`);
79
+ onHit?.(key, result.response);
80
+ const cached = result.response.data;
81
+ Object.entries(cached.headers).forEach(([headerKey, value]) => {
82
+ res.setHeader(headerKey, value);
83
+ });
84
+ if (includeHeaders) {
85
+ res.setHeader(IDEMPOTENCY_KEY_HEADER, key);
86
+ res.setHeader(IDEMPOTENCY_REPLAY_HEADER, "true");
87
+ }
88
+ return res.status(cached.status).json(cached.body);
89
+ }
90
+ if (result.status === "conflict") {
91
+ log(`Conflict for key: ${key}`);
92
+ if (includeHeaders) {
93
+ res.setHeader(IDEMPOTENCY_KEY_HEADER, key);
94
+ res.setHeader("Retry-After", "1");
95
+ }
96
+ return res.status(409).json({
97
+ type: `${PROBLEM_BASE}/conflict`,
98
+ title: "Conflict",
99
+ status: 409,
100
+ detail: "A request with this idempotency key is already being processed."
101
+ });
102
+ }
103
+ if (result.status === "mismatch") {
104
+ log(`Hash mismatch for key: ${key}`);
105
+ if (includeHeaders) {
106
+ res.setHeader(IDEMPOTENCY_KEY_HEADER, key);
107
+ }
108
+ return res.status(422).json({
109
+ type: `${PROBLEM_BASE}/mismatch`,
110
+ title: "Idempotency Key Mismatch",
111
+ status: 422,
112
+ detail: "The request body does not match the original request for this idempotency key.",
113
+ existingHash: result.existingHash,
114
+ providedHash: result.providedHash
115
+ });
116
+ }
117
+ log(`Lock acquired for key: ${key}`);
118
+ onMiss?.(key);
119
+ let capturedStatus = 200;
120
+ const capturedHeaders = {};
121
+ let capturedBody;
122
+ const originalStatus = res.status.bind(res);
123
+ const originalSetHeader = res.setHeader.bind(res);
124
+ const originalJson = res.json.bind(res);
125
+ const originalSend = res.send.bind(res);
126
+ res.status = (statusCode) => {
127
+ capturedStatus = statusCode;
128
+ return originalStatus(statusCode);
129
+ };
130
+ res.setHeader = (name, value) => {
131
+ if (typeof value === "string") {
132
+ capturedHeaders[name] = value;
133
+ } else if (Array.isArray(value)) {
134
+ capturedHeaders[name] = value.join(", ");
135
+ } else {
136
+ capturedHeaders[name] = String(value);
137
+ }
138
+ return originalSetHeader(name, value);
139
+ };
140
+ res.json = (body) => {
141
+ capturedBody = body;
142
+ const cached = {
143
+ status: capturedStatus,
144
+ headers: capturedHeaders,
145
+ body: capturedBody
146
+ };
147
+ const storedResponse = {
148
+ data: cached,
149
+ createdAt: Date.now(),
150
+ hash
151
+ };
152
+ storage.save(key, storedResponse).catch((err) => {
153
+ log(`Failed to save response for key: ${key}`);
154
+ onError?.(key, err);
155
+ });
156
+ if (includeHeaders) {
157
+ res.setHeader(IDEMPOTENCY_KEY_HEADER, key);
158
+ }
159
+ return originalJson(body);
160
+ };
161
+ res.send = (body) => {
162
+ capturedBody = body;
163
+ const cached = {
164
+ status: capturedStatus,
165
+ headers: capturedHeaders,
166
+ body: capturedBody
167
+ };
168
+ const storedResponse = {
169
+ data: cached,
170
+ createdAt: Date.now(),
171
+ hash
172
+ };
173
+ storage.save(key, storedResponse).catch((err) => {
174
+ log(`Failed to save response for key: ${key}`);
175
+ onError?.(key, err);
176
+ });
177
+ if (includeHeaders) {
178
+ res.setHeader(IDEMPOTENCY_KEY_HEADER, key);
179
+ }
180
+ return originalSend(body);
181
+ };
182
+ try {
183
+ return await handler(req, res);
184
+ } catch (err) {
185
+ log(`Handler error for key: ${key}, releasing lock`);
186
+ await storage.release(key).catch(() => {
187
+ });
188
+ throw err;
189
+ }
190
+ } catch (err) {
191
+ if (err instanceof IdempotencyError) {
192
+ if (includeHeaders) {
193
+ res.setHeader(IDEMPOTENCY_KEY_HEADER, key);
194
+ if (err instanceof ConflictError) {
195
+ res.setHeader("Retry-After", String(err.retryAfter ?? 1));
196
+ }
197
+ }
198
+ let type = `${PROBLEM_BASE}/error`;
199
+ if (err instanceof MissingKeyError) type = `${PROBLEM_BASE}/key-required`;
200
+ else if (err instanceof ConflictError) type = `${PROBLEM_BASE}/conflict`;
201
+ else if (err instanceof MismatchError) type = `${PROBLEM_BASE}/mismatch`;
202
+ return res.status(err.statusCode).json({
203
+ type,
204
+ title: err.name,
205
+ status: err.statusCode,
206
+ detail: err.message
207
+ });
208
+ }
209
+ throw err;
210
+ }
211
+ };
212
+ };
213
+ }
214
+ function configure(defaults) {
215
+ return (overrides = {}) => pages({ ...defaults, ...overrides });
216
+ }
217
+ oncely.pages = pages;
218
+
219
+ export { IDEMPOTENCY_KEY_HEADER, IDEMPOTENCY_REPLAY_HEADER, configure, pages };
220
+ //# sourceMappingURL=pages.js.map
221
+ //# sourceMappingURL=pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pages.ts"],"names":[],"mappings":"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","file":"pages.js","sourcesContent":["/**\n * @oncely/next/pages - Next.js Pages Router integration for oncely idempotency\n *\n * Provides a wrapper for Next.js Pages Router API routes with idempotency protection.\n *\n * @example\n * ```typescript\n * // pages/api/orders.ts\n * import { pages } from '@oncely/next/pages';\n *\n * export default pages()(async (req, res) => {\n * const order = await createOrder(req.body);\n * res.status(201).json(order);\n * });\n * ```\n */\n\nimport type { NextApiRequest, NextApiResponse } from 'next';\nimport {\n oncely,\n MemoryStorage,\n hashObject,\n parseTtl,\n IdempotencyError,\n ConflictError,\n MissingKeyError,\n MismatchError,\n type StorageAdapter,\n type OncelyOptions,\n type StoredResponse,\n} from '@oncely/core';\n\n// RFC 7807 Problem Details type URL base\nconst PROBLEM_BASE = 'https://oncely.dev/errors';\n\n/**\n * IETF standard header names\n */\nexport const IDEMPOTENCY_KEY_HEADER = 'Idempotency-Key';\nexport const IDEMPOTENCY_REPLAY_HEADER = 'Idempotency-Replay';\n\n/**\n * Options for the Next.js Pages Router wrapper\n */\nexport interface PagesMiddlewareOptions {\n /**\n * Storage adapter for persisting idempotency records.\n * @default MemoryStorage\n */\n storage?: StorageAdapter;\n\n /**\n * TTL for idempotency records in milliseconds or duration string.\n * @default '24h'\n */\n ttl?: number | string;\n\n /**\n * Whether to require an idempotency key.\n * If true, returns 400 when key is missing.\n * @default false\n */\n required?: boolean;\n\n /**\n * Custom function to extract idempotency key from request.\n * @default Reads Idempotency-Key header\n */\n getKey?: (req: NextApiRequest) => string | undefined | null;\n\n /**\n * Custom function to generate hash from request for mismatch detection.\n * @default Hashes request body as JSON\n */\n getHash?: (req: NextApiRequest) => Promise<string | null> | string | null;\n\n /**\n * Whether to add idempotency headers to responses.\n * @default true\n */\n includeHeaders?: boolean;\n\n /**\n * Debug mode for logging.\n * @default false\n */\n debug?: boolean;\n\n /**\n * Callback when a cached response is returned.\n */\n onHit?: OncelyOptions['onHit'];\n\n /**\n * Callback when a new response is generated.\n */\n onMiss?: OncelyOptions['onMiss'];\n\n /**\n * Callback when an error occurs.\n */\n onError?: OncelyOptions['onError'];\n\n /**\n * If true, proceed with the request when storage fails (e.g., Redis down).\n * When false (default), storage errors return 500.\n * Use with non-critical idempotency where availability is more important.\n * @default false\n */\n failOpen?: boolean;\n}\n\n/**\n * Stored response data for replay\n */\ninterface CachedResponse {\n status: number;\n headers: Record<string, string>;\n body: unknown;\n}\n\n/**\n * API route handler function signature\n */\ntype ApiHandler = (\n req: NextApiRequest,\n res: NextApiResponse\n) => Promise<void | NextApiResponse> | void | NextApiResponse;\n\n// Shared default storage instance\nlet defaultStorage: StorageAdapter | null = null;\n\nfunction getStorage(provided?: StorageAdapter): StorageAdapter {\n if (provided) return provided;\n if (!defaultStorage) {\n defaultStorage = new MemoryStorage();\n }\n return defaultStorage;\n}\n\n/**\n * Default hash function that hashes request body\n */\nfunction defaultGetHash(req: NextApiRequest): string | null {\n if (req.body && typeof req.body === 'object' && Object.keys(req.body).length > 0) {\n return hashObject(req.body);\n }\n return null;\n}\n\n/**\n * Create a Next.js Pages Router API handler wrapper with idempotency protection.\n *\n * @example\n * ```typescript\n * // pages/api/orders.ts\n * import { pages } from '@oncely/next/pages';\n *\n * // Basic usage\n * export default pages()(async (req, res) => {\n * const order = await createOrder(req.body);\n * res.status(201).json(order);\n * });\n *\n * // With options\n * export default pages({ required: true, ttl: '1h' })(async (req, res) => {\n * const result = await processPayment(req.body);\n * res.json(result);\n * });\n * ```\n */\nexport function pages(options: PagesMiddlewareOptions = {}): (handler: ApiHandler) => ApiHandler {\n const {\n storage: providedStorage,\n ttl = '24h',\n required = false,\n getKey = (req) => {\n const header = req.headers[IDEMPOTENCY_KEY_HEADER.toLowerCase()];\n return Array.isArray(header) ? header[0] : header;\n },\n getHash = defaultGetHash,\n includeHeaders = true,\n debug = false,\n failOpen = false,\n onHit,\n onMiss,\n onError,\n } = options;\n\n const storage = getStorage(providedStorage);\n const ttlMs = parseTtl(ttl);\n\n const log = (message: string) => {\n if (debug) {\n console.log(`[oncely/next/pages] ${message}`);\n }\n };\n\n return (handler: ApiHandler): ApiHandler => {\n return async (req: NextApiRequest, res: NextApiResponse): Promise<void | NextApiResponse> => {\n // Get idempotency key\n const key = getKey(req);\n\n // Skip if no key and not required\n if (!key && !required) {\n return handler(req, res);\n }\n\n // Return 400 if key required but not provided\n if (!key && required) {\n if (includeHeaders) {\n res.setHeader('Content-Type', 'application/problem+json');\n }\n return res.status(400).json({\n type: `${PROBLEM_BASE}/key-required`,\n title: 'Idempotency Key Required',\n status: 400,\n detail: `The ${IDEMPOTENCY_KEY_HEADER} header is required for this request.`,\n });\n }\n\n // Compute request hash\n const hash = await getHash(req);\n\n try {\n log(`Acquiring lock for key: ${key}`);\n let result;\n try {\n result = await storage.acquire(key!, hash, ttlMs);\n } catch (storageErr) {\n // Storage failed (e.g., Redis down)\n if (failOpen) {\n log(`Storage error for key: ${key}, failing open`);\n onError?.(key!, storageErr as Error);\n return handler(req, res); // Proceed without idempotency protection\n }\n throw storageErr;\n }\n\n if (result.status === 'hit') {\n // Return cached response\n log(`Cache hit for key: ${key}`);\n onHit?.(key!, result.response);\n\n const cached = result.response.data as CachedResponse;\n\n // Set headers\n Object.entries(cached.headers).forEach(([headerKey, value]) => {\n res.setHeader(headerKey, value);\n });\n\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n res.setHeader(IDEMPOTENCY_REPLAY_HEADER, 'true');\n }\n\n return res.status(cached.status).json(cached.body);\n }\n\n if (result.status === 'conflict') {\n log(`Conflict for key: ${key}`);\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n res.setHeader('Retry-After', '1');\n }\n return res.status(409).json({\n type: `${PROBLEM_BASE}/conflict`,\n title: 'Conflict',\n status: 409,\n detail: 'A request with this idempotency key is already being processed.',\n });\n }\n\n if (result.status === 'mismatch') {\n log(`Hash mismatch for key: ${key}`);\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n }\n return res.status(422).json({\n type: `${PROBLEM_BASE}/mismatch`,\n title: 'Idempotency Key Mismatch',\n status: 422,\n detail:\n 'The request body does not match the original request for this idempotency key.',\n existingHash: result.existingHash,\n providedHash: result.providedHash,\n });\n }\n\n // status === 'acquired' - execute handler\n log(`Lock acquired for key: ${key}`);\n onMiss?.(key!);\n\n // Intercept response methods to capture the response\n let capturedStatus = 200;\n const capturedHeaders: Record<string, string> = {};\n let capturedBody: unknown;\n\n const originalStatus = res.status.bind(res);\n const originalSetHeader = res.setHeader.bind(res);\n const originalJson = res.json.bind(res);\n const originalSend = res.send.bind(res);\n\n res.status = (statusCode: number) => {\n capturedStatus = statusCode;\n return originalStatus(statusCode);\n };\n\n res.setHeader = (name: string, value: string | number | readonly string[]) => {\n if (typeof value === 'string') {\n capturedHeaders[name] = value;\n } else if (Array.isArray(value)) {\n capturedHeaders[name] = value.join(', ');\n } else {\n capturedHeaders[name] = String(value);\n }\n return originalSetHeader(name, value);\n };\n\n res.json = (body: unknown) => {\n capturedBody = body;\n\n // Save to storage\n const cached: CachedResponse = {\n status: capturedStatus,\n headers: capturedHeaders,\n body: capturedBody,\n };\n\n const storedResponse: StoredResponse = {\n data: cached,\n createdAt: Date.now(),\n hash,\n };\n\n storage.save(key!, storedResponse).catch((err) => {\n log(`Failed to save response for key: ${key}`);\n onError?.(key!, err as Error);\n });\n\n // Add idempotency headers\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n }\n\n return originalJson(body);\n };\n\n res.send = (body: unknown) => {\n capturedBody = body;\n\n // Save to storage\n const cached: CachedResponse = {\n status: capturedStatus,\n headers: capturedHeaders,\n body: capturedBody,\n };\n\n const storedResponse: StoredResponse = {\n data: cached,\n createdAt: Date.now(),\n hash,\n };\n\n storage.save(key!, storedResponse).catch((err) => {\n log(`Failed to save response for key: ${key}`);\n onError?.(key!, err as Error);\n });\n\n // Add idempotency headers\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n }\n\n return originalSend(body);\n };\n\n try {\n return await handler(req, res);\n } catch (err) {\n // Release lock on error\n log(`Handler error for key: ${key}, releasing lock`);\n await storage.release(key!).catch(() => {});\n throw err;\n }\n } catch (err) {\n // Handle idempotency errors\n if (err instanceof IdempotencyError) {\n if (includeHeaders) {\n res.setHeader(IDEMPOTENCY_KEY_HEADER, key!);\n if (err instanceof ConflictError) {\n res.setHeader('Retry-After', String(err.retryAfter ?? 1));\n }\n }\n\n let type = `${PROBLEM_BASE}/error`;\n if (err instanceof MissingKeyError) type = `${PROBLEM_BASE}/key-required`;\n else if (err instanceof ConflictError) type = `${PROBLEM_BASE}/conflict`;\n else if (err instanceof MismatchError) type = `${PROBLEM_BASE}/mismatch`;\n\n return res.status(err.statusCode).json({\n type,\n title: err.name,\n status: err.statusCode,\n detail: err.message,\n });\n }\n\n throw err;\n }\n };\n };\n}\n\n/**\n * Create a pre-configured wrapper factory with default options.\n *\n * @example\n * ```typescript\n * // lib/idempotency.ts\n * import { configure } from '@oncely/next/pages';\n * import { redis } from '@oncely/redis';\n *\n * export const idempotent = configure({\n * storage: redis(),\n * ttl: '1h',\n * });\n *\n * // pages/api/orders.ts\n * import { idempotent } from '@/lib/idempotency';\n *\n * export default idempotent()(async (req, res) => {\n * res.status(201).json(await createOrder(req.body));\n * });\n * ```\n */\nexport function configure(\n defaults: PagesMiddlewareOptions\n): (overrides?: Partial<PagesMiddlewareOptions>) => (handler: ApiHandler) => ApiHandler {\n return (overrides = {}) => pages({ ...defaults, ...overrides });\n}\n\n// Augment oncely namespace\ndeclare module '@oncely/core' {\n interface OncelyNamespace {\n /**\n * Create Next.js Pages Router wrapper with idempotency.\n */\n pages: typeof pages;\n }\n}\n\n// Register on oncely namespace\n(oncely as Record<string, unknown>).pages = pages;\n\n// Re-export types and utilities\nexport {\n oncely,\n MemoryStorage,\n hashObject,\n IdempotencyError,\n MissingKeyError,\n ConflictError,\n MismatchError,\n type StorageAdapter,\n type OncelyOptions,\n} from '@oncely/core';\n"]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@oncely/next",
3
+ "version": "1.0.0",
4
+ "description": "Next.js integration for oncely idempotency",
5
+ "author": "stacks0x",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/stacks0x/oncely.git",
10
+ "directory": "packages/next"
11
+ },
12
+ "keywords": [
13
+ "oncely",
14
+ "idempotency",
15
+ "nextjs",
16
+ "next",
17
+ "app-router",
18
+ "pages-router"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs"
26
+ },
27
+ "./pages": {
28
+ "types": "./dist/pages.d.ts",
29
+ "import": "./dist/pages.js",
30
+ "require": "./dist/pages.cjs"
31
+ }
32
+ },
33
+ "main": "./dist/index.cjs",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "dev": "tsup --watch",
44
+ "typecheck": "tsc --noEmit",
45
+ "clean": "rm -rf dist"
46
+ },
47
+ "peerDependencies": {
48
+ "@oncely/core": "workspace:*",
49
+ "next": ">=13.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@oncely/core": "workspace:*",
53
+ "next": "^16.1.4",
54
+ "tsup": "^8.0.1",
55
+ "typescript": "^5.3.3"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "engines": {
61
+ "node": ">=18.0.0"
62
+ }
63
+ }