@spfn/core 0.2.0-beta.56 → 0.2.0-beta.59

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,34 @@
1
+ /**
2
+ * @spfn/core/authz - Authorization primitives
3
+ *
4
+ * Small, domain-agnostic guards for resource ownership. The point is to make the
5
+ * "load, then check it belongs to the requester" step a single call so a handler
6
+ * can't forget it and leak another user's data (IDOR).
7
+ */
8
+ interface RequireOwnerOptions<T> {
9
+ /**
10
+ * Property on the resource holding the owner id. Defaults to `'ownerId'`.
11
+ * SPFN entities commonly use `userId` / `createdBy` — pass it explicitly:
12
+ * `requireOwner(doc, userId, { ownerKey: 'userId' })`.
13
+ */
14
+ ownerKey?: keyof T;
15
+ /** Message for the thrown NotFoundError. @default 'not found' */
16
+ message?: string;
17
+ }
18
+ /**
19
+ * Return the resource only if it exists and belongs to `ownerId`; otherwise throw
20
+ * `NotFoundError`. Not-found and not-owned are deliberately the same response, so
21
+ * the endpoint never reveals that a resource exists to a non-owner.
22
+ *
23
+ * Ids are compared as strings, so number/string/bigint ids interoperate. A
24
+ * missing or null owner value never matches, so it throws.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const chat = requireOwner(await chats.findById(id), userId, { ownerKey: 'userId' });
29
+ * // chat is non-null here, narrowed from T | null
30
+ * ```
31
+ */
32
+ declare function requireOwner<T extends object>(resource: T | null | undefined, ownerId: string | number | bigint, options?: RequireOwnerOptions<T>): T;
33
+
34
+ export { type RequireOwnerOptions, requireOwner };
@@ -0,0 +1,415 @@
1
+ // src/errors/error-registry.ts
2
+ var ErrorRegistry = class _ErrorRegistry {
3
+ errors = /* @__PURE__ */ new Map();
4
+ constructor(initialErrors) {
5
+ if (initialErrors) {
6
+ for (const input of initialErrors) {
7
+ if (input instanceof _ErrorRegistry) {
8
+ this.concat(input);
9
+ } else if (Array.isArray(input)) {
10
+ this.append(input);
11
+ } else {
12
+ this.append(input);
13
+ }
14
+ }
15
+ }
16
+ }
17
+ /**
18
+ * Append error class(es) to the registry
19
+ *
20
+ * @param input - Error constructor or array of constructors
21
+ * @returns This registry for chaining
22
+ */
23
+ append(input) {
24
+ if (Array.isArray(input)) {
25
+ for (const ErrorClass of input) {
26
+ this.errors.set(ErrorClass.name, ErrorClass);
27
+ }
28
+ } else {
29
+ this.errors.set(input.name, input);
30
+ }
31
+ return this;
32
+ }
33
+ /**
34
+ * Concatenate another ErrorRegistry into this one
35
+ *
36
+ * @param registry - Another ErrorRegistry to merge
37
+ * @returns This registry for chaining
38
+ */
39
+ concat(registry) {
40
+ for (const [name, ErrorClass] of registry.errors) {
41
+ this.errors.set(name, ErrorClass);
42
+ }
43
+ return this;
44
+ }
45
+ /**
46
+ * Check if error type is registered
47
+ *
48
+ * @param name - Error class name
49
+ */
50
+ has(name) {
51
+ return this.errors.has(name);
52
+ }
53
+ /**
54
+ * Deserialize error from JSON data
55
+ *
56
+ * @param data - Serialized error data with __type field
57
+ * @returns Deserialized error instance
58
+ * @throws Error if error type is not registered
59
+ */
60
+ deserialize(data) {
61
+ const ErrorClass = this.errors.get(data.__type);
62
+ if (!ErrorClass) {
63
+ throw new Error(`Unknown error type: ${data.__type}`);
64
+ }
65
+ return new ErrorClass(data);
66
+ }
67
+ /**
68
+ * Try to deserialize error, return null if type unknown
69
+ *
70
+ * @param data - Serialized error data
71
+ * @returns Deserialized error or null
72
+ */
73
+ tryDeserialize(data) {
74
+ if (!data.__type || !this.has(data.__type)) {
75
+ return null;
76
+ }
77
+ try {
78
+ return this.deserialize(data);
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ /**
84
+ * Get all registered error types
85
+ */
86
+ getRegisteredTypes() {
87
+ return Array.from(this.errors.keys());
88
+ }
89
+ };
90
+
91
+ // src/errors/serializable-error.ts
92
+ var SerializableError = class extends Error {
93
+ /**
94
+ * Serialize error to JSON-compatible object
95
+ *
96
+ * Automatically includes:
97
+ * - __type: Constructor name for deserialization
98
+ * - message: Error message
99
+ * - All public instance properties (except name, stack)
100
+ */
101
+ toJSON() {
102
+ const json = {
103
+ __type: this.constructor.name,
104
+ message: this.message
105
+ };
106
+ for (const key of Object.keys(this)) {
107
+ if (key !== "name" && key !== "message" && key !== "stack" && key !== "statusCode") {
108
+ json[key] = this[key];
109
+ }
110
+ }
111
+ return json;
112
+ }
113
+ };
114
+
115
+ // src/errors/http-errors.ts
116
+ var HttpError = class extends SerializableError {
117
+ statusCode;
118
+ details;
119
+ constructor(data) {
120
+ super(data.message);
121
+ this.name = "HttpError";
122
+ this.statusCode = data.statusCode;
123
+ if (data.details) {
124
+ this.details = data.details;
125
+ }
126
+ Error.captureStackTrace(this, this.constructor);
127
+ }
128
+ };
129
+ var BadRequestError = class extends HttpError {
130
+ constructor(data = {}) {
131
+ super({
132
+ message: data.message || "Bad request",
133
+ statusCode: 400,
134
+ details: data.details
135
+ });
136
+ this.name = "BadRequestError";
137
+ }
138
+ };
139
+ var ValidationError = class extends HttpError {
140
+ fields;
141
+ constructor(data) {
142
+ super({
143
+ message: data.message,
144
+ statusCode: 400,
145
+ details: data.details
146
+ });
147
+ this.name = "ValidationError";
148
+ if (data.fields) {
149
+ this.fields = data.fields;
150
+ }
151
+ }
152
+ };
153
+ var UnauthorizedError = class extends HttpError {
154
+ constructor(data = {}) {
155
+ super({
156
+ message: data.message || "Authentication required",
157
+ statusCode: 401,
158
+ details: data.details
159
+ });
160
+ this.name = "UnauthorizedError";
161
+ }
162
+ };
163
+ var ForbiddenError = class extends HttpError {
164
+ constructor(data = {}) {
165
+ super({
166
+ message: data.message || "Access forbidden",
167
+ statusCode: 403,
168
+ details: data.details
169
+ });
170
+ this.name = "ForbiddenError";
171
+ }
172
+ };
173
+ var NotFoundError = class extends HttpError {
174
+ resource;
175
+ constructor(data = {}) {
176
+ super({
177
+ message: data.message || "Resource not found",
178
+ statusCode: 404,
179
+ details: data.details
180
+ });
181
+ this.name = "NotFoundError";
182
+ if (data.resource) {
183
+ this.resource = data.resource;
184
+ }
185
+ }
186
+ };
187
+ var ConflictError = class extends HttpError {
188
+ constructor(data = {}) {
189
+ super({
190
+ message: data.message || "Resource conflict",
191
+ statusCode: 409,
192
+ details: data.details
193
+ });
194
+ this.name = "ConflictError";
195
+ }
196
+ };
197
+ var GoneError = class extends HttpError {
198
+ resource;
199
+ constructor(data = {}) {
200
+ super({
201
+ message: data.message || "Resource permanently deleted",
202
+ statusCode: 410,
203
+ details: data.details
204
+ });
205
+ this.name = "GoneError";
206
+ if (data.resource) {
207
+ this.resource = data.resource;
208
+ }
209
+ }
210
+ };
211
+ var TooManyRequestsError = class extends HttpError {
212
+ retryAfter;
213
+ constructor(data = {}) {
214
+ super({
215
+ message: data.message || "Too many requests",
216
+ statusCode: 429,
217
+ details: data.details
218
+ });
219
+ this.name = "TooManyRequestsError";
220
+ if (data.retryAfter) {
221
+ this.retryAfter = data.retryAfter;
222
+ }
223
+ }
224
+ };
225
+ var InternalServerError = class extends HttpError {
226
+ constructor(data = {}) {
227
+ super({
228
+ message: data.message || "Internal server error",
229
+ statusCode: 500,
230
+ details: data.details
231
+ });
232
+ this.name = "InternalServerError";
233
+ }
234
+ };
235
+ var UnsupportedMediaTypeError = class extends HttpError {
236
+ mediaType;
237
+ supportedTypes;
238
+ constructor(data = {}) {
239
+ super({
240
+ message: data.message || "Unsupported media type",
241
+ statusCode: 415,
242
+ details: data.details
243
+ });
244
+ this.name = "UnsupportedMediaTypeError";
245
+ if (data.mediaType) {
246
+ this.mediaType = data.mediaType;
247
+ }
248
+ if (data.supportedTypes) {
249
+ this.supportedTypes = data.supportedTypes;
250
+ }
251
+ }
252
+ };
253
+ var UnprocessableEntityError = class extends HttpError {
254
+ constructor(data = {}) {
255
+ super({
256
+ message: data.message || "Unprocessable entity",
257
+ statusCode: 422,
258
+ details: data.details
259
+ });
260
+ this.name = "UnprocessableEntityError";
261
+ }
262
+ };
263
+ var ServiceUnavailableError = class extends HttpError {
264
+ retryAfter;
265
+ constructor(data = {}) {
266
+ super({
267
+ message: data.message || "Service unavailable",
268
+ statusCode: 503,
269
+ details: data.details
270
+ });
271
+ this.name = "ServiceUnavailableError";
272
+ if (data.retryAfter) {
273
+ this.retryAfter = data.retryAfter;
274
+ }
275
+ }
276
+ };
277
+
278
+ // src/errors/database-errors.ts
279
+ var DatabaseError = class extends SerializableError {
280
+ statusCode;
281
+ details;
282
+ constructor(data) {
283
+ super(data.message);
284
+ this.name = "DatabaseError";
285
+ this.statusCode = data.statusCode ?? 500;
286
+ this.details = data.details;
287
+ Error.captureStackTrace(this, this.constructor);
288
+ }
289
+ /**
290
+ * Whether this error's message/details are derived from the raw database
291
+ * driver (SQL text, table/column/constraint names, parameter values) and
292
+ * therefore must NOT be exposed to clients in production. Defined as a
293
+ * prototype getter so it is never serialized into the response by toJSON().
294
+ * Subclasses with a safe, constructed message override this to `false`.
295
+ */
296
+ get internal() {
297
+ return true;
298
+ }
299
+ };
300
+ var ConnectionError = class extends DatabaseError {
301
+ constructor(data) {
302
+ super({ message: data.message, statusCode: 503, details: data.details });
303
+ this.name = "ConnectionError";
304
+ }
305
+ };
306
+ var QueryError = class extends DatabaseError {
307
+ constructor(data) {
308
+ super({
309
+ message: data.message,
310
+ statusCode: data.statusCode ?? 500,
311
+ details: data.details
312
+ });
313
+ this.name = "QueryError";
314
+ }
315
+ };
316
+ var EntityNotFoundError = class extends QueryError {
317
+ resource;
318
+ id;
319
+ constructor(data) {
320
+ super({
321
+ message: `${data.resource} with id ${data.id} not found`,
322
+ statusCode: 404,
323
+ details: { resource: data.resource, id: data.id }
324
+ });
325
+ this.name = "EntityNotFoundError";
326
+ this.resource = data.resource;
327
+ this.id = data.id;
328
+ }
329
+ // Message/details are constructed from the resource + id, not driver text
330
+ get internal() {
331
+ return false;
332
+ }
333
+ };
334
+ var ConstraintViolationError = class extends QueryError {
335
+ constructor(data) {
336
+ super({ message: data.message, statusCode: 400, details: data.details });
337
+ this.name = "ConstraintViolationError";
338
+ }
339
+ };
340
+ var TransactionError = class extends DatabaseError {
341
+ constructor(data) {
342
+ super({
343
+ message: data.message,
344
+ statusCode: data.statusCode ?? 500,
345
+ details: data.details
346
+ });
347
+ this.name = "TransactionError";
348
+ }
349
+ };
350
+ var DeadlockError = class extends TransactionError {
351
+ constructor(data) {
352
+ super({ message: data.message, statusCode: 409, details: data.details });
353
+ this.name = "DeadlockError";
354
+ }
355
+ };
356
+ var DuplicateEntryError = class extends QueryError {
357
+ field;
358
+ value;
359
+ constructor(data) {
360
+ super({
361
+ message: `${data.field} already exists`,
362
+ statusCode: 409,
363
+ details: { field: data.field }
364
+ });
365
+ this.name = "DuplicateEntryError";
366
+ this.field = data.field;
367
+ this.value = data.value;
368
+ }
369
+ // Field-only message/details — safe to surface to the client (not internal)
370
+ get internal() {
371
+ return false;
372
+ }
373
+ };
374
+
375
+ // src/errors/index.ts
376
+ var errorRegistry = new ErrorRegistry();
377
+ errorRegistry.append([
378
+ HttpError,
379
+ BadRequestError,
380
+ ValidationError,
381
+ UnauthorizedError,
382
+ ForbiddenError,
383
+ NotFoundError,
384
+ ConflictError,
385
+ GoneError,
386
+ TooManyRequestsError,
387
+ UnsupportedMediaTypeError,
388
+ UnprocessableEntityError,
389
+ InternalServerError,
390
+ ServiceUnavailableError
391
+ ]);
392
+ errorRegistry.append([
393
+ DatabaseError,
394
+ ConnectionError,
395
+ QueryError,
396
+ EntityNotFoundError,
397
+ ConstraintViolationError,
398
+ TransactionError,
399
+ DeadlockError,
400
+ DuplicateEntryError
401
+ ]);
402
+
403
+ // src/authz/index.ts
404
+ function requireOwner(resource, ownerId, options = {}) {
405
+ const ownerKey = options.ownerKey ?? "ownerId";
406
+ const value = resource ? resource[ownerKey] : void 0;
407
+ if (!resource || value === null || value === void 0 || String(value) !== String(ownerId)) {
408
+ throw new NotFoundError({ message: options.message ?? "not found" });
409
+ }
410
+ return resource;
411
+ }
412
+
413
+ export { requireOwner };
414
+ //# sourceMappingURL=index.js.map
415
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/errors/error-registry.ts","../../src/errors/serializable-error.ts","../../src/errors/http-errors.ts","../../src/errors/database-errors.ts","../../src/errors/index.ts","../../src/authz/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Error Registry\n *\n * Central registry for serializable error types.\n * Enables automatic error deserialization on the client side.\n */\n\nimport type { SerializableError } from './serializable-error';\n\n/**\n * Error constructor type\n */\nexport type SerializableErrorConstructor = new (data: any) => SerializableError;\n\n/**\n * Input type for ErrorRegistry constructor\n */\nexport type ErrorRegistryInput =\n | SerializableErrorConstructor\n | SerializableErrorConstructor[]\n | ErrorRegistry;\n\n/**\n * Error registry for serialization/deserialization\n *\n * @example\n * ```typescript\n * // Create registry with initial errors\n * const registry = new ErrorRegistry([\n * errorRegistry, // Another ErrorRegistry\n * PaymentFailedError, // Single error class\n * [RefundError, OrderError], // Array of error classes\n * ]);\n *\n * // Or create empty and append\n * const registry = new ErrorRegistry();\n * registry\n * .append(ValidationError)\n * .append([PaymentFailedError, RefundError]);\n *\n * // Deserialize from JSON\n * const error = registry.deserialize({\n * __type: 'PaymentFailedError',\n * message: 'Payment failed',\n * transactionId: 'tx_123',\n * reason: 'insufficient_funds'\n * });\n *\n * // error instanceof PaymentFailedError === true\n * ```\n */\nexport class ErrorRegistry\n{\n private errors = new Map<string, SerializableErrorConstructor>();\n\n constructor(initialErrors?: ErrorRegistryInput[])\n {\n if (initialErrors)\n {\n for (const input of initialErrors)\n {\n if (input instanceof ErrorRegistry)\n {\n this.concat(input);\n }\n else if (Array.isArray(input))\n {\n this.append(input);\n }\n else\n {\n this.append(input);\n }\n }\n }\n }\n\n /**\n * Append error class(es) to the registry\n *\n * @param ErrorClass - Single error constructor\n * @returns This registry for chaining\n */\n append(ErrorClass: SerializableErrorConstructor): this;\n\n /**\n * Append error class(es) to the registry\n *\n * @param ErrorClasses - Array of error constructors\n * @returns This registry for chaining\n */\n append(ErrorClasses: SerializableErrorConstructor[]): this;\n\n /**\n * Append error class(es) to the registry\n *\n * @param input - Error constructor or array of constructors\n * @returns This registry for chaining\n */\n append(input: SerializableErrorConstructor | SerializableErrorConstructor[]): this\n {\n if (Array.isArray(input))\n {\n for (const ErrorClass of input)\n {\n this.errors.set(ErrorClass.name, ErrorClass);\n }\n }\n else\n {\n this.errors.set(input.name, input);\n }\n\n return this;\n }\n\n /**\n * Concatenate another ErrorRegistry into this one\n *\n * @param registry - Another ErrorRegistry to merge\n * @returns This registry for chaining\n */\n concat(registry: ErrorRegistry): this\n {\n for (const [name, ErrorClass] of registry.errors)\n {\n this.errors.set(name, ErrorClass);\n }\n\n return this;\n }\n\n /**\n * Check if error type is registered\n *\n * @param name - Error class name\n */\n has(name: string): boolean\n {\n return this.errors.has(name);\n }\n\n /**\n * Deserialize error from JSON data\n *\n * @param data - Serialized error data with __type field\n * @returns Deserialized error instance\n * @throws Error if error type is not registered\n */\n deserialize(data: { __type: string; [key: string]: any }): SerializableError\n {\n const ErrorClass = this.errors.get(data.__type);\n\n if (!ErrorClass)\n {\n throw new Error(`Unknown error type: ${data.__type}`);\n }\n\n // Pass entire data object to constructor\n return new ErrorClass(data);\n }\n\n /**\n * Try to deserialize error, return null if type unknown\n *\n * @param data - Serialized error data\n * @returns Deserialized error or null\n */\n tryDeserialize(data: { __type?: string; [key: string]: any }): SerializableError | null\n {\n if (!data.__type || !this.has(data.__type))\n {\n return null;\n }\n\n try\n {\n return this.deserialize(data as any);\n }\n catch\n {\n return null;\n }\n }\n\n /**\n * Get all registered error types\n */\n getRegisteredTypes(): string[]\n {\n return Array.from(this.errors.keys());\n }\n}\n","/**\n * Serializable Error Base Class\n *\n * Base class for errors that can be serialized/deserialized across HTTP boundary.\n * Automatically serializes public fields for transmission to client.\n */\n\n/**\n * Serialized error format for JSON transmission\n */\nexport interface SerializedError\n{\n __type: string;\n message: string;\n [key: string]: unknown;\n}\n\n/**\n * Base class for all serializable errors\n *\n * Features:\n * - Auto-serialization of public fields via toJSON()\n * - Constructor accepts object with all fields\n * - Type-safe error handling with instanceof\n *\n * @example\n * ```typescript\n * class PaymentFailedError extends SerializableError\n * {\n * readonly statusCode = 402;\n * transactionId!: string;\n * reason!: 'insufficient_funds' | 'card_declined';\n *\n * constructor(data: {\n * message: string;\n * transactionId: string;\n * reason: 'insufficient_funds' | 'card_declined';\n * })\n * {\n * super(data.message);\n * this.name = 'PaymentFailedError';\n * Object.assign(this, data);\n * }\n * }\n * ```\n */\nexport abstract class SerializableError extends Error\n{\n /**\n * HTTP status code for this error type\n */\n abstract readonly statusCode: number;\n\n /**\n * Serialize error to JSON-compatible object\n *\n * Automatically includes:\n * - __type: Constructor name for deserialization\n * - message: Error message\n * - All public instance properties (except name, stack)\n */\n toJSON(): SerializedError\n {\n const json: SerializedError = {\n __type: this.constructor.name,\n message: this.message,\n };\n\n // Extract all public instance properties\n for (const key of Object.keys(this))\n {\n // Skip Error built-ins and statusCode (inferred from type)\n if (key !== 'name' && key !== 'message' && key !== 'stack' && key !== 'statusCode')\n {\n json[key] = (this as unknown as Record<string, unknown>)[key];\n }\n }\n\n return json;\n }\n}\n","/**\n * HTTP Error Classes\n *\n * Standard HTTP error classes for API responses\n * All errors are serializable for type-safe client-side error handling\n */\n\nimport { SerializableError } from './serializable-error';\n\n/**\n * Base HTTP Error\n *\n * Base class for all HTTP-related errors\n */\nexport class HttpError extends SerializableError\n{\n public readonly statusCode: number;\n public details?: Record<string, unknown>;\n\n constructor(data: {\n message: string;\n statusCode: number;\n details?: Record<string, unknown>;\n })\n {\n super(data.message);\n\n this.name = 'HttpError';\n this.statusCode = data.statusCode;\n\n if (data.details)\n {\n this.details = data.details;\n }\n\n Error.captureStackTrace(this, this.constructor);\n }\n}\n\n/**\n * Bad Request Error (400)\n *\n * Generic bad request - malformed syntax, invalid parameters, etc.\n */\nexport class BadRequestError extends HttpError\n{\n constructor(data: { message?: string; details?: Record<string, any> } = {})\n {\n super({\n message: data.message || 'Bad request',\n statusCode: 400,\n details: data.details,\n });\n\n this.name = 'BadRequestError';\n }\n}\n\n/**\n * Validation Error (400)\n *\n * Input validation failure (request params, query, body)\n * Used by define-route system for automatic validation\n */\nexport class ValidationError extends HttpError\n{\n fields?: Array<{ path: string; message: string; value?: any }>;\n\n constructor(data: {\n message: string;\n fields?: Array<{ path: string; message: string; value?: any }>;\n details?: Record<string, any>;\n })\n {\n super({\n message: data.message,\n statusCode: 400,\n details: data.details,\n });\n\n this.name = 'ValidationError';\n\n if (data.fields)\n {\n this.fields = data.fields;\n }\n }\n}\n\n/**\n * Unauthorized Error (401)\n *\n * Authentication required or authentication failed\n */\nexport class UnauthorizedError extends HttpError\n{\n constructor(data: { message?: string; details?: Record<string, any> } = {})\n {\n super({\n message: data.message || 'Authentication required',\n statusCode: 401,\n details: data.details,\n });\n\n this.name = 'UnauthorizedError';\n }\n}\n\n/**\n * Forbidden Error (403)\n *\n * Authenticated but lacks permission to access resource\n */\nexport class ForbiddenError extends HttpError\n{\n constructor(data: { message?: string; details?: Record<string, any> } = {})\n {\n super({\n message: data.message || 'Access forbidden',\n statusCode: 403,\n details: data.details,\n });\n\n this.name = 'ForbiddenError';\n }\n}\n\n/**\n * Not Found Error (404)\n *\n * Requested resource does not exist\n */\nexport class NotFoundError extends HttpError\n{\n resource?: string;\n\n constructor(data: { message?: string; resource?: string; details?: Record<string, any> } = {})\n {\n super({\n message: data.message || 'Resource not found',\n statusCode: 404,\n details: data.details,\n });\n\n this.name = 'NotFoundError';\n\n if (data.resource)\n {\n this.resource = data.resource;\n }\n }\n}\n\n/**\n * Conflict Error (409)\n *\n * Generic conflict - resource state conflict, concurrent modification, etc.\n */\nexport class ConflictError extends HttpError\n{\n constructor(data: { message?: string; details?: Record<string, any> } = {})\n {\n super({\n message: data.message || 'Resource conflict',\n statusCode: 409,\n details: data.details,\n });\n\n this.name = 'ConflictError';\n }\n}\n\n/**\n * Gone Error (410)\n *\n * Resource permanently deleted and no longer available\n */\nexport class GoneError extends HttpError\n{\n resource?: string;\n\n constructor(data: { message?: string; resource?: string; details?: Record<string, any> } = {})\n {\n super({\n message: data.message || 'Resource permanently deleted',\n statusCode: 410,\n details: data.details,\n });\n\n this.name = 'GoneError';\n\n if (data.resource)\n {\n this.resource = data.resource;\n }\n }\n}\n\n/**\n * Too Many Requests Error (429)\n *\n * Rate limit exceeded\n */\nexport class TooManyRequestsError extends HttpError\n{\n retryAfter?: number;\n\n constructor(data: {\n message?: string;\n retryAfter?: number;\n details?: Record<string, any>;\n } = {})\n {\n super({\n message: data.message || 'Too many requests',\n statusCode: 429,\n details: data.details,\n });\n\n this.name = 'TooManyRequestsError';\n\n if (data.retryAfter)\n {\n this.retryAfter = data.retryAfter;\n }\n }\n}\n\n/**\n * Internal Server Error (500)\n *\n * Generic server error when no specific error type applies\n */\nexport class InternalServerError extends HttpError\n{\n constructor(data: { message?: string; details?: Record<string, any> } = {})\n {\n super({\n message: data.message || 'Internal server error',\n statusCode: 500,\n details: data.details,\n });\n\n this.name = 'InternalServerError';\n }\n}\n\n/**\n * Unsupported Media Type Error (415)\n *\n * Media type not supported - invalid file type, content type, etc.\n */\nexport class UnsupportedMediaTypeError extends HttpError\n{\n mediaType?: string;\n supportedTypes?: string[];\n\n constructor(data: {\n message?: string;\n mediaType?: string;\n supportedTypes?: string[];\n details?: Record<string, any>;\n } = {})\n {\n super({\n message: data.message || 'Unsupported media type',\n statusCode: 415,\n details: data.details,\n });\n\n this.name = 'UnsupportedMediaTypeError';\n\n if (data.mediaType)\n {\n this.mediaType = data.mediaType;\n }\n\n if (data.supportedTypes)\n {\n this.supportedTypes = data.supportedTypes;\n }\n }\n}\n\n/**\n * Unprocessable Entity Error (422)\n *\n * Request is well-formed but contains semantic errors\n */\nexport class UnprocessableEntityError extends HttpError\n{\n constructor(data: { message?: string; details?: Record<string, any> } = {})\n {\n super({\n message: data.message || 'Unprocessable entity',\n statusCode: 422,\n details: data.details,\n });\n\n this.name = 'UnprocessableEntityError';\n }\n}\n\n/**\n * Service Unavailable Error (503)\n *\n * Service temporarily unavailable (maintenance, overload, etc.)\n */\nexport class ServiceUnavailableError extends HttpError\n{\n retryAfter?: number;\n\n constructor(data: {\n message?: string;\n retryAfter?: number;\n details?: Record<string, any>;\n } = {})\n {\n super({\n message: data.message || 'Service unavailable',\n statusCode: 503,\n details: data.details,\n });\n\n this.name = 'ServiceUnavailableError';\n\n if (data.retryAfter)\n {\n this.retryAfter = data.retryAfter;\n }\n }\n}\n","/**\n * Database Error Classes\n *\n * Type-safe error handling with custom error class hierarchy\n * Mapped to HTTP status codes for API responses\n * All errors extend SerializableError for consistent JSON serialization\n */\n\nimport { SerializableError } from './serializable-error';\n\n/**\n * Base Database Error\n *\n * Base class for all database-related errors\n */\nexport class DatabaseError<TDetails extends Record<string, unknown> = Record<string, unknown>>\n extends SerializableError\n{\n public readonly statusCode: number;\n public readonly details?: TDetails;\n\n constructor(data: {\n message: string;\n statusCode?: number;\n details?: TDetails;\n })\n {\n super(data.message);\n this.name = 'DatabaseError';\n this.statusCode = data.statusCode ?? 500;\n this.details = data.details;\n Error.captureStackTrace(this, this.constructor);\n }\n\n /**\n * Whether this error's message/details are derived from the raw database\n * driver (SQL text, table/column/constraint names, parameter values) and\n * therefore must NOT be exposed to clients in production. Defined as a\n * prototype getter so it is never serialized into the response by toJSON().\n * Subclasses with a safe, constructed message override this to `false`.\n */\n get internal(): boolean\n {\n return true;\n }\n}\n\n/**\n * Connection Error (503 Service Unavailable)\n *\n * Database connection failure, connection pool exhaustion, etc.\n */\nexport class ConnectionError extends DatabaseError\n{\n constructor(data: { message: string; details?: Record<string, unknown> })\n {\n super({ message: data.message, statusCode: 503, details: data.details });\n this.name = 'ConnectionError';\n }\n}\n\n/**\n * Query Error (500 Internal Server Error)\n *\n * SQL query execution failure, syntax errors, etc.\n */\nexport class QueryError extends DatabaseError\n{\n constructor(data: {\n message: string;\n statusCode?: number;\n details?: Record<string, unknown>;\n })\n {\n super({\n message: data.message,\n statusCode: data.statusCode ?? 500,\n details: data.details,\n });\n this.name = 'QueryError';\n }\n}\n\n/**\n * Entity Not Found Error (404 Not Found)\n *\n * Database entity does not exist\n */\nexport class EntityNotFoundError extends QueryError\n{\n public readonly resource: string;\n public readonly id: string | number;\n\n constructor(data: { resource: string; id: string | number })\n {\n super({\n message: `${data.resource} with id ${data.id} not found`,\n statusCode: 404,\n details: { resource: data.resource, id: data.id },\n });\n this.name = 'EntityNotFoundError';\n this.resource = data.resource;\n this.id = data.id;\n }\n\n // Message/details are constructed from the resource + id, not driver text\n override get internal(): boolean\n {\n return false;\n }\n}\n\n/**\n * Constraint Violation Error (400 Bad Request)\n *\n * Database constraint violation (NOT NULL, CHECK, FOREIGN KEY, etc.)\n * This is different from HTTP ValidationError which validates request input\n */\nexport class ConstraintViolationError extends QueryError\n{\n constructor(data: { message: string; details?: Record<string, unknown> })\n {\n super({ message: data.message, statusCode: 400, details: data.details });\n this.name = 'ConstraintViolationError';\n }\n}\n\n/**\n * Transaction Error (500 Internal Server Error)\n *\n * Transaction start/commit/rollback failure\n */\nexport class TransactionError extends DatabaseError\n{\n constructor(data: {\n message: string;\n statusCode?: number;\n details?: Record<string, unknown>;\n })\n {\n super({\n message: data.message,\n statusCode: data.statusCode ?? 500,\n details: data.details,\n });\n this.name = 'TransactionError';\n }\n}\n\n/**\n * Deadlock Error (409 Conflict)\n *\n * Database deadlock detected\n */\nexport class DeadlockError extends TransactionError\n{\n constructor(data: { message: string; details?: Record<string, unknown> })\n {\n super({ message: data.message, statusCode: 409, details: data.details });\n this.name = 'DeadlockError';\n }\n}\n\n/**\n * Duplicate Entry Error (409 Conflict)\n *\n * Unique constraint violation (e.g., duplicate email)\n */\nexport class DuplicateEntryError extends QueryError\n{\n public readonly field: string;\n public readonly value: string | number;\n\n constructor(data: { field: string; value: string | number })\n {\n // The client-facing message/details name the field but NOT the value: echoing\n // the value back (e.g. an email) confirms it exists, an enumeration aid. The\n // raw value stays on the instance for server-side handling/logging.\n super({\n message: `${data.field} already exists`,\n statusCode: 409,\n details: { field: data.field },\n });\n this.name = 'DuplicateEntryError';\n this.field = data.field;\n this.value = data.value;\n }\n\n // Field-only message/details — safe to surface to the client (not internal)\n override get internal(): boolean\n {\n return false;\n }\n}\n","/**\n * Error Module Exports\n *\n * Entry point for error handling module with serialization support\n */\n\n// Core Error Registry\n// Pre-configured registry with all built-in HTTP and Database errors\nimport { ErrorRegistry } from './error-registry';\nimport {\n HttpError,\n BadRequestError,\n ValidationError,\n UnauthorizedError,\n ForbiddenError,\n NotFoundError,\n ConflictError,\n GoneError,\n TooManyRequestsError,\n UnsupportedMediaTypeError,\n UnprocessableEntityError,\n InternalServerError,\n ServiceUnavailableError,\n} from './http-errors';\nimport {\n DatabaseError,\n ConnectionError,\n QueryError,\n EntityNotFoundError,\n ConstraintViolationError,\n TransactionError,\n DeadlockError,\n DuplicateEntryError,\n} from './database-errors';\n\n// Base Classes\nexport { SerializableError } from './serializable-error';\nexport type { SerializedError } from './serializable-error';\nexport { ErrorRegistry } from './error-registry';\nexport type { SerializableErrorConstructor, ErrorRegistryInput } from './error-registry';\n\n// Database Error Classes\nexport {\n DatabaseError,\n ConnectionError,\n QueryError,\n EntityNotFoundError,\n ConstraintViolationError,\n TransactionError,\n DeadlockError,\n DuplicateEntryError,\n} from './database-errors';\n\n// HTTP Error Classes\nexport {\n HttpError,\n BadRequestError,\n ValidationError,\n UnauthorizedError,\n ForbiddenError,\n NotFoundError,\n ConflictError,\n GoneError,\n TooManyRequestsError,\n UnsupportedMediaTypeError,\n UnprocessableEntityError,\n InternalServerError,\n ServiceUnavailableError,\n} from './http-errors';\n\n// Error Utilities\nexport {\n isDatabaseError,\n isHttpError,\n hasStatusCode,\n} from './error-utils';\n\nexport const errorRegistry = new ErrorRegistry();\n\n// HTTP Errors\nerrorRegistry.append([\n HttpError,\n BadRequestError,\n ValidationError,\n UnauthorizedError,\n ForbiddenError,\n NotFoundError,\n ConflictError,\n GoneError,\n TooManyRequestsError,\n UnsupportedMediaTypeError,\n UnprocessableEntityError,\n InternalServerError,\n ServiceUnavailableError,\n]);\n\n// Database Errors\nerrorRegistry.append([\n DatabaseError,\n ConnectionError,\n QueryError,\n EntityNotFoundError,\n ConstraintViolationError,\n TransactionError,\n DeadlockError,\n DuplicateEntryError,\n]);\n\nexport * as HttpErrors from './http-errors';\nexport * as DatabaseErrors from './database-errors';\n","/**\n * @spfn/core/authz - Authorization primitives\n *\n * Small, domain-agnostic guards for resource ownership. The point is to make the\n * \"load, then check it belongs to the requester\" step a single call so a handler\n * can't forget it and leak another user's data (IDOR).\n */\n\nimport { NotFoundError } from '../errors';\n\nexport interface RequireOwnerOptions<T>\n{\n /**\n * Property on the resource holding the owner id. Defaults to `'ownerId'`.\n * SPFN entities commonly use `userId` / `createdBy` — pass it explicitly:\n * `requireOwner(doc, userId, { ownerKey: 'userId' })`.\n */\n ownerKey?: keyof T;\n\n /** Message for the thrown NotFoundError. @default 'not found' */\n message?: string;\n}\n\n/**\n * Return the resource only if it exists and belongs to `ownerId`; otherwise throw\n * `NotFoundError`. Not-found and not-owned are deliberately the same response, so\n * the endpoint never reveals that a resource exists to a non-owner.\n *\n * Ids are compared as strings, so number/string/bigint ids interoperate. A\n * missing or null owner value never matches, so it throws.\n *\n * @example\n * ```ts\n * const chat = requireOwner(await chats.findById(id), userId, { ownerKey: 'userId' });\n * // chat is non-null here, narrowed from T | null\n * ```\n */\nexport function requireOwner<T extends object>(\n resource: T | null | undefined,\n ownerId: string | number | bigint,\n options: RequireOwnerOptions<T> = {},\n): T\n{\n const ownerKey = (options.ownerKey ?? 'ownerId') as keyof T;\n const value = resource ? (resource as Record<PropertyKey, unknown>)[ownerKey] : undefined;\n\n if (!resource || value === null || value === undefined || String(value) !== String(ownerId))\n {\n throw new NotFoundError({ message: options.message ?? 'not found' });\n }\n\n return resource;\n}\n"]}
@@ -21,6 +21,13 @@ interface JobOptions {
21
21
  * @default 1000
22
22
  */
23
23
  retryDelay?: number;
24
+ /**
25
+ * Exponential backoff between retries (retryDelay * 2^attempt). Defaults to
26
+ * true so a failed cohort (e.g. on a provider 429) doesn't all retry at the
27
+ * same fixed offset — a thundering herd. Set false for fixed retryDelay.
28
+ * @default true
29
+ */
30
+ retryBackoff?: boolean;
24
31
  /**
25
32
  * Job expiration in seconds
26
33
  * @default 300 (5 minutes)
@@ -564,6 +564,50 @@ declare const coreEnvSchema: {
564
564
  } & {
565
565
  key: "TRUSTED_PROXY_HOPS";
566
566
  };
567
+ RATE_LIMIT_MODE: Omit<_spfn_core_env.EnvVarSchema<"off" | "on">, "key" | "type" | "validator"> & {
568
+ type: "enum";
569
+ validator: (val: string) => "off" | "on";
570
+ } & {
571
+ key: "RATE_LIMIT_MODE";
572
+ };
573
+ RATE_LIMIT_DEFAULT_LIMIT: {
574
+ description: string;
575
+ default: number;
576
+ examples: number[];
577
+ } & {
578
+ type: "number";
579
+ validator: (value: string) => number;
580
+ } & {
581
+ key: "RATE_LIMIT_DEFAULT_LIMIT";
582
+ };
583
+ RATE_LIMIT_DEFAULT_WINDOW_MS: {
584
+ description: string;
585
+ default: number;
586
+ examples: number[];
587
+ } & {
588
+ type: "number";
589
+ validator: (value: string) => number;
590
+ } & {
591
+ key: "RATE_LIMIT_DEFAULT_WINDOW_MS";
592
+ };
593
+ RATE_LIMIT_FAIL_CLOSED: {
594
+ description: string;
595
+ default: boolean;
596
+ } & {
597
+ type: "boolean";
598
+ validator: (value: string) => boolean;
599
+ } & {
600
+ key: "RATE_LIMIT_FAIL_CLOSED";
601
+ };
602
+ SAFE_FETCH_BLOCK_PRIVATE_IPS: {
603
+ description: string;
604
+ default: boolean;
605
+ } & {
606
+ type: "boolean";
607
+ validator: (value: string) => boolean;
608
+ } & {
609
+ key: "SAFE_FETCH_BLOCK_PRIVATE_IPS";
610
+ };
567
611
  };
568
612
 
569
613
  /**
@@ -1120,6 +1164,50 @@ declare const registry: _spfn_core_env.EnvRegistry<{
1120
1164
  } & {
1121
1165
  key: "TRUSTED_PROXY_HOPS";
1122
1166
  };
1167
+ RATE_LIMIT_MODE: Omit<_spfn_core_env.EnvVarSchema<"off" | "on">, "key" | "type" | "validator"> & {
1168
+ type: "enum";
1169
+ validator: (val: string) => "off" | "on";
1170
+ } & {
1171
+ key: "RATE_LIMIT_MODE";
1172
+ };
1173
+ RATE_LIMIT_DEFAULT_LIMIT: {
1174
+ description: string;
1175
+ default: number;
1176
+ examples: number[];
1177
+ } & {
1178
+ type: "number";
1179
+ validator: (value: string) => number;
1180
+ } & {
1181
+ key: "RATE_LIMIT_DEFAULT_LIMIT";
1182
+ };
1183
+ RATE_LIMIT_DEFAULT_WINDOW_MS: {
1184
+ description: string;
1185
+ default: number;
1186
+ examples: number[];
1187
+ } & {
1188
+ type: "number";
1189
+ validator: (value: string) => number;
1190
+ } & {
1191
+ key: "RATE_LIMIT_DEFAULT_WINDOW_MS";
1192
+ };
1193
+ RATE_LIMIT_FAIL_CLOSED: {
1194
+ description: string;
1195
+ default: boolean;
1196
+ } & {
1197
+ type: "boolean";
1198
+ validator: (value: string) => boolean;
1199
+ } & {
1200
+ key: "RATE_LIMIT_FAIL_CLOSED";
1201
+ };
1202
+ SAFE_FETCH_BLOCK_PRIVATE_IPS: {
1203
+ description: string;
1204
+ default: boolean;
1205
+ } & {
1206
+ type: "boolean";
1207
+ validator: (value: string) => boolean;
1208
+ } & {
1209
+ key: "SAFE_FETCH_BLOCK_PRIVATE_IPS";
1210
+ };
1123
1211
  }>;
1124
1212
  /**
1125
1213
  * Validated environment configuration
@@ -1669,6 +1757,50 @@ declare const env: _spfn_core_env.InferEnvType<{
1669
1757
  } & {
1670
1758
  key: "TRUSTED_PROXY_HOPS";
1671
1759
  };
1760
+ RATE_LIMIT_MODE: Omit<_spfn_core_env.EnvVarSchema<"off" | "on">, "key" | "type" | "validator"> & {
1761
+ type: "enum";
1762
+ validator: (val: string) => "off" | "on";
1763
+ } & {
1764
+ key: "RATE_LIMIT_MODE";
1765
+ };
1766
+ RATE_LIMIT_DEFAULT_LIMIT: {
1767
+ description: string;
1768
+ default: number;
1769
+ examples: number[];
1770
+ } & {
1771
+ type: "number";
1772
+ validator: (value: string) => number;
1773
+ } & {
1774
+ key: "RATE_LIMIT_DEFAULT_LIMIT";
1775
+ };
1776
+ RATE_LIMIT_DEFAULT_WINDOW_MS: {
1777
+ description: string;
1778
+ default: number;
1779
+ examples: number[];
1780
+ } & {
1781
+ type: "number";
1782
+ validator: (value: string) => number;
1783
+ } & {
1784
+ key: "RATE_LIMIT_DEFAULT_WINDOW_MS";
1785
+ };
1786
+ RATE_LIMIT_FAIL_CLOSED: {
1787
+ description: string;
1788
+ default: boolean;
1789
+ } & {
1790
+ type: "boolean";
1791
+ validator: (value: string) => boolean;
1792
+ } & {
1793
+ key: "RATE_LIMIT_FAIL_CLOSED";
1794
+ };
1795
+ SAFE_FETCH_BLOCK_PRIVATE_IPS: {
1796
+ description: string;
1797
+ default: boolean;
1798
+ } & {
1799
+ type: "boolean";
1800
+ validator: (value: string) => boolean;
1801
+ } & {
1802
+ key: "SAFE_FETCH_BLOCK_PRIVATE_IPS";
1803
+ };
1672
1804
  }>;
1673
1805
 
1674
1806
  export { env, coreEnvSchema as envSchema, registry };