@spfn/core 0.2.0-beta.9 → 0.3.0-beta.10

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.
Files changed (119) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +693 -279
  3. package/dist/app-config/index.d.ts +96 -0
  4. package/dist/app-config/index.js +58 -0
  5. package/dist/app-config/index.js.map +1 -0
  6. package/dist/authz/index.d.ts +34 -0
  7. package/dist/authz/index.js +430 -0
  8. package/dist/authz/index.js.map +1 -0
  9. package/dist/boss-BiDCbbAQ.d.ts +337 -0
  10. package/dist/cache/index.js +42 -30
  11. package/dist/cache/index.js.map +1 -1
  12. package/dist/codegen/index.d.ts +121 -13
  13. package/dist/codegen/index.js +212 -15
  14. package/dist/codegen/index.js.map +1 -1
  15. package/dist/config/index.d.ts +576 -30
  16. package/dist/config/index.js +136 -17
  17. package/dist/config/index.js.map +1 -1
  18. package/dist/contract/index.d.ts +221 -0
  19. package/dist/contract/index.js +558 -0
  20. package/dist/contract/index.js.map +1 -0
  21. package/dist/db/index.d.ts +704 -94
  22. package/dist/db/index.js +1105 -260
  23. package/dist/db/index.js.map +1 -1
  24. package/dist/define-middleware-CVKgqo8S.d.ts +167 -0
  25. package/dist/env/index.d.ts +26 -2
  26. package/dist/env/index.js +15 -5
  27. package/dist/env/index.js.map +1 -1
  28. package/dist/env/loader.d.ts +26 -19
  29. package/dist/env/loader.js +55 -25
  30. package/dist/env/loader.js.map +1 -1
  31. package/dist/errors/index.d.ts +10 -0
  32. package/dist/errors/index.js +40 -7
  33. package/dist/errors/index.js.map +1 -1
  34. package/dist/event/index.d.ts +33 -3
  35. package/dist/event/index.js +24 -3
  36. package/dist/event/index.js.map +1 -1
  37. package/dist/event/sse/client.d.ts +55 -3
  38. package/dist/event/sse/client.js +164 -45
  39. package/dist/event/sse/client.js.map +1 -1
  40. package/dist/event/sse/index.d.ts +12 -5
  41. package/dist/event/sse/index.js +280 -32
  42. package/dist/event/sse/index.js.map +1 -1
  43. package/dist/event/ws/client.d.ts +59 -0
  44. package/dist/event/ws/client.js +273 -0
  45. package/dist/event/ws/client.js.map +1 -0
  46. package/dist/event/ws/index.d.ts +94 -0
  47. package/dist/event/ws/index.js +272 -0
  48. package/dist/event/ws/index.js.map +1 -0
  49. package/dist/job/index.d.ts +48 -3
  50. package/dist/job/index.js +265 -74
  51. package/dist/job/index.js.map +1 -1
  52. package/dist/logger/index.d.ts +5 -0
  53. package/dist/logger/index.js +14 -12
  54. package/dist/logger/index.js.map +1 -1
  55. package/dist/middleware/index.d.ts +352 -10
  56. package/dist/middleware/index.js +1079 -13
  57. package/dist/middleware/index.js.map +1 -1
  58. package/dist/nextjs/index.d.ts +39 -3
  59. package/dist/nextjs/index.js +82 -29
  60. package/dist/nextjs/index.js.map +1 -1
  61. package/dist/nextjs/server.d.ts +69 -52
  62. package/dist/nextjs/server.js +140 -60
  63. package/dist/nextjs/server.js.map +1 -1
  64. package/dist/ops/index.d.ts +208 -0
  65. package/dist/ops/index.js +800 -0
  66. package/dist/ops/index.js.map +1 -0
  67. package/dist/route/index.d.ts +10 -694
  68. package/dist/route/index.js +112 -23
  69. package/dist/route/index.js.map +1 -1
  70. package/dist/route-builder-2ani2jEI.d.ts +553 -0
  71. package/dist/router-DJdpwuB6.d.ts +127 -0
  72. package/dist/security/index.d.ts +83 -0
  73. package/dist/security/index.js +173 -0
  74. package/dist/security/index.js.map +1 -0
  75. package/dist/server/index.d.ts +619 -31
  76. package/dist/server/index.js +2583 -360
  77. package/dist/server/index.js.map +1 -1
  78. package/dist/token-manager-BT5EnUAR.d.ts +278 -0
  79. package/dist/types-2AbaW4Ie.d.ts +205 -0
  80. package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
  81. package/dist/types-ClQVomgV.d.ts +115 -0
  82. package/dist/types-ZQODsBft.d.ts +282 -0
  83. package/docs/file-upload.md +195 -333
  84. package/package.json +250 -208
  85. package/src/cache/README.md +330 -0
  86. package/src/codegen/README.md +516 -0
  87. package/src/config/README.md +326 -0
  88. package/src/contract/README.md +326 -0
  89. package/src/db/README.md +589 -0
  90. package/src/db/manager/README.md +500 -0
  91. package/src/db/schema/README.md +354 -0
  92. package/src/db/transaction/README.md +822 -0
  93. package/src/env/README.md +651 -0
  94. package/src/errors/README.md +429 -0
  95. package/src/event/README.md +741 -0
  96. package/src/job/README.md +613 -0
  97. package/src/logger/README.md +321 -0
  98. package/src/middleware/README.md +634 -0
  99. package/src/nextjs/README.md +608 -0
  100. package/src/route/README.md +738 -0
  101. package/src/security/README.md +100 -0
  102. package/src/server/README.md +704 -0
  103. package/dist/boss-DI1r4kTS.d.ts +0 -244
  104. package/dist/router-Di7ENoah.d.ts +0 -151
  105. package/dist/types-B-e_f2dQ.d.ts +0 -121
  106. package/docs/cache.md +0 -133
  107. package/docs/codegen.md +0 -74
  108. package/docs/database.md +0 -346
  109. package/docs/entity.md +0 -539
  110. package/docs/env.md +0 -477
  111. package/docs/errors.md +0 -319
  112. package/docs/event.md +0 -116
  113. package/docs/job.md +0 -131
  114. package/docs/logger.md +0 -108
  115. package/docs/middleware.md +0 -337
  116. package/docs/nextjs.md +0 -241
  117. package/docs/repository.md +0 -496
  118. package/docs/route.md +0 -497
  119. package/docs/server.md +0 -307
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `spfn.config.js` — the app's serving shape, read by both the CLI and the server.
3
+ *
4
+ * The file has existed since `spfn init` started writing it, and until now
5
+ * nothing read it back. It is the one committed place that describes how an app
6
+ * is served, which makes it the right home for the ports: an app that wants a
7
+ * different port says so once here instead of repeating the number across its
8
+ * `server.config.ts`, Dockerfile, compose file, `next.config.ts` and env
9
+ * example, where nothing checks that the copies still agree.
10
+ *
11
+ * This module is deliberately free of side effects. `@spfn/core/config`
12
+ * validates the whole environment schema the moment it is imported, which is
13
+ * fine inside a server and wrong inside a CLI that has not been handed an
14
+ * app's environment yet.
15
+ *
16
+ * @module app-config
17
+ */
18
+ /**
19
+ * Ports the app is served on.
20
+ *
21
+ * Two names because two processes are started. Next.js reads `PORT` as its own,
22
+ * so the SPFN server cannot claim that name without the two colliding whenever
23
+ * they run together.
24
+ */
25
+ interface SpfnPorts {
26
+ /** Next.js frontend. Passed to `next` as `-p`. */
27
+ next?: number;
28
+ /** SPFN API server. */
29
+ server?: number;
30
+ }
31
+ interface SpfnAppConfig {
32
+ ports?: SpfnPorts;
33
+ /** Host the SPFN API server binds. */
34
+ host?: string;
35
+ /** Deployment settings — untouched by this module, kept so the type is whole. */
36
+ [key: string]: unknown;
37
+ }
38
+ /**
39
+ * The single place each default exists.
40
+ *
41
+ * Defaults live at the bottom of the chain and nowhere else. A default declared
42
+ * at an input layer — a commander option, a generated entry file, an env schema
43
+ * — cannot be told apart from a value someone supplied, so it silently outranks
44
+ * whatever is below it. Three separate defects in this repository came from
45
+ * exactly that, including an env schema default that forced the environment to
46
+ * be consulted last and made an injected port unreachable.
47
+ */
48
+ declare const PORT_DEFAULTS: {
49
+ readonly next: 3790;
50
+ readonly server: 8790;
51
+ };
52
+ declare const HOST_DEFAULT = "localhost";
53
+ /**
54
+ * Read `spfn.config.js` from an app directory.
55
+ *
56
+ * Returns an empty object when there is no file, and also when the file cannot
57
+ * be imported — a malformed deployment config must not stop a server from
58
+ * booting. The caller decides whether the absence is worth reporting.
59
+ */
60
+ declare function loadAppConfig(cwd?: string): Promise<SpfnAppConfig>;
61
+ /**
62
+ * The port each process binds, given a loaded config and the environment.
63
+ *
64
+ * Three layers, in this order, and no more: environment variable, then
65
+ * `spfn.config.js`, then the default.
66
+ */
67
+ declare function resolvePorts(config: SpfnAppConfig, env?: NodeJS.ProcessEnv): {
68
+ next: number;
69
+ server: number;
70
+ };
71
+ /**
72
+ * The host the SPFN server binds, on the same three layers.
73
+ *
74
+ * `localhost` by default: a container states `SPFN_HOST=0.0.0.0` in its own
75
+ * files, where that decision belongs, rather than every developer machine
76
+ * publishing a dev server to its network.
77
+ */
78
+ declare function resolveHost(config: SpfnAppConfig, env?: NodeJS.ProcessEnv): string;
79
+ /**
80
+ * Where the SPFN server binds, including the address a server config may still
81
+ * carry through `.port()` / `.host()`.
82
+ *
83
+ * Those two are deprecated and sit between `spfn.config.js` and the default for
84
+ * one release, so an app that has not moved yet keeps the address it had. Once
85
+ * they are removed this collapses back to {@link resolvePorts} and
86
+ * {@link resolveHost}.
87
+ */
88
+ declare function resolveServerAddress(config: SpfnAppConfig, deprecated?: {
89
+ port?: number;
90
+ host?: string;
91
+ }, env?: NodeJS.ProcessEnv): {
92
+ port: number;
93
+ host: string;
94
+ };
95
+
96
+ export { HOST_DEFAULT, PORT_DEFAULTS, type SpfnAppConfig, type SpfnPorts, loadAppConfig, resolveHost, resolvePorts, resolveServerAddress };
@@ -0,0 +1,58 @@
1
+ import { existsSync } from 'fs';
2
+ import { pathToFileURL } from 'url';
3
+ import { join } from 'path';
4
+
5
+ // src/app-config/index.ts
6
+ var PORT_DEFAULTS = {
7
+ next: 3790,
8
+ server: 8790
9
+ };
10
+ var HOST_DEFAULT = "localhost";
11
+ var CONFIG_FILE_NAMES = [
12
+ "spfn.config.js",
13
+ "spfn.config.mjs"
14
+ ];
15
+ async function loadAppConfig(cwd = process.cwd()) {
16
+ for (const fileName of CONFIG_FILE_NAMES) {
17
+ const fullPath = join(cwd, fileName);
18
+ if (!existsSync(fullPath)) {
19
+ continue;
20
+ }
21
+ try {
22
+ const module = await import(pathToFileURL(fullPath).href);
23
+ return module.default ?? {};
24
+ } catch (error) {
25
+ console.warn(
26
+ `\u26A0\uFE0F ${fileName} could not be imported \u2014 falling back to defaults for ports and host. ${error instanceof Error ? error.message : String(error)}`
27
+ );
28
+ return {};
29
+ }
30
+ }
31
+ return {};
32
+ }
33
+ function resolvePorts(config, env = process.env) {
34
+ return {
35
+ next: readPort(env.NEXT_PORT) ?? config.ports?.next ?? PORT_DEFAULTS.next,
36
+ server: readPort(env.SPFN_PORT) ?? config.ports?.server ?? PORT_DEFAULTS.server
37
+ };
38
+ }
39
+ function resolveHost(config, env = process.env) {
40
+ return env.SPFN_HOST || config.host || HOST_DEFAULT;
41
+ }
42
+ function resolveServerAddress(config, deprecated = {}, env = process.env) {
43
+ return {
44
+ port: readPort(env.SPFN_PORT) ?? config.ports?.server ?? deprecated.port ?? PORT_DEFAULTS.server,
45
+ host: env.SPFN_HOST || config.host || deprecated.host || HOST_DEFAULT
46
+ };
47
+ }
48
+ function readPort(value) {
49
+ if (!value) {
50
+ return void 0;
51
+ }
52
+ const port = Number(value);
53
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : void 0;
54
+ }
55
+
56
+ export { HOST_DEFAULT, PORT_DEFAULTS, loadAppConfig, resolveHost, resolvePorts, resolveServerAddress };
57
+ //# sourceMappingURL=index.js.map
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/app-config/index.ts"],"names":[],"mappings":";;;;;AA2DO,IAAM,aAAA,GAAgB;AAAA,EACzB,IAAA,EAAM,IAAA;AAAA,EACN,MAAA,EAAQ;AACZ;AAEO,IAAM,YAAA,GAAe;AAE5B,IAAM,iBAAA,GAAoB;AAAA,EACtB,gBAAA;AAAA,EACA;AACJ,CAAA;AASA,eAAsB,aAAA,CAAc,GAAA,GAAc,OAAA,CAAQ,GAAA,EAAI,EAC9D;AACI,EAAA,KAAA,MAAW,YAAY,iBAAA,EACvB;AACI,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAK,QAAQ,CAAA;AAEnC,IAAA,IAAI,CAAC,UAAA,CAAW,QAAQ,CAAA,EACxB;AACI,MAAA;AAAA,IACJ;AAEA,IAAA,IACA;AAGI,MAAA,MAAM,MAAA,GAAS,MAAM,OAAO,aAAA,CAAc,QAAQ,CAAA,CAAE,IAAA,CAAA;AAEpD,MAAA,OAAQ,MAAA,CAAO,WAAW,EAAC;AAAA,IAC/B,SACO,KAAA,EACP;AAQI,MAAA,OAAA,CAAQ,IAAA;AAAA,QACJ,CAAA,cAAA,EAAO,QAAQ,CAAA,2EAAA,EACU,KAAA,YAAiB,QAAQ,KAAA,CAAM,OAAA,GAAU,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,OACnF;AAEA,MAAA,OAAO,EAAC;AAAA,IACZ;AAAA,EACJ;AAEA,EAAA,OAAO,EAAC;AACZ;AAQO,SAAS,YAAA,CACZ,MAAA,EACA,GAAA,GAAyB,OAAA,CAAQ,GAAA,EAErC;AACI,EAAA,OAAO;AAAA,IACH,IAAA,EAAM,SAAS,GAAA,CAAI,SAAS,KAAK,MAAA,CAAO,KAAA,EAAO,QAAQ,aAAA,CAAc,IAAA;AAAA,IACrE,MAAA,EAAQ,SAAS,GAAA,CAAI,SAAS,KAAK,MAAA,CAAO,KAAA,EAAO,UAAU,aAAA,CAAc;AAAA,GAC7E;AACJ;AASO,SAAS,WAAA,CACZ,MAAA,EACA,GAAA,GAAyB,OAAA,CAAQ,GAAA,EAErC;AACI,EAAA,OAAO,GAAA,CAAI,SAAA,IAAa,MAAA,CAAO,IAAA,IAAQ,YAAA;AAC3C;AAWO,SAAS,qBACZ,MAAA,EACA,UAAA,GAA+C,EAAC,EAChD,GAAA,GAAyB,QAAQ,GAAA,EAErC;AACI,EAAA,OAAO;AAAA,IACH,IAAA,EAAM,QAAA,CAAS,GAAA,CAAI,SAAS,CAAA,IACrB,OAAO,KAAA,EAAO,MAAA,IACd,UAAA,CAAW,IAAA,IACX,aAAA,CAAc,MAAA;AAAA,IACrB,MAAM,GAAA,CAAI,SAAA,IACH,MAAA,CAAO,IAAA,IACP,WAAW,IAAA,IACX;AAAA,GACX;AACJ;AAEA,SAAS,SAAS,KAAA,EAClB;AACI,EAAA,IAAI,CAAC,KAAA,EACL;AACI,IAAA,OAAO,MAAA;AAAA,EACX;AAEA,EAAA,MAAM,IAAA,GAAO,OAAO,KAAK,CAAA;AAEzB,EAAA,OAAO,MAAA,CAAO,UAAU,IAAI,CAAA,IAAK,OAAO,CAAA,IAAK,IAAA,GAAO,QAAQ,IAAA,GAAO,MAAA;AACvE","file":"index.js","sourcesContent":["/**\n * `spfn.config.js` — the app's serving shape, read by both the CLI and the server.\n *\n * The file has existed since `spfn init` started writing it, and until now\n * nothing read it back. It is the one committed place that describes how an app\n * is served, which makes it the right home for the ports: an app that wants a\n * different port says so once here instead of repeating the number across its\n * `server.config.ts`, Dockerfile, compose file, `next.config.ts` and env\n * example, where nothing checks that the copies still agree.\n *\n * This module is deliberately free of side effects. `@spfn/core/config`\n * validates the whole environment schema the moment it is imported, which is\n * fine inside a server and wrong inside a CLI that has not been handed an\n * app's environment yet.\n *\n * @module app-config\n */\n\nimport { existsSync } from 'fs';\nimport { pathToFileURL } from 'url';\nimport { join } from 'path';\n\n/**\n * Ports the app is served on.\n *\n * Two names because two processes are started. Next.js reads `PORT` as its own,\n * so the SPFN server cannot claim that name without the two colliding whenever\n * they run together.\n */\nexport interface SpfnPorts\n{\n /** Next.js frontend. Passed to `next` as `-p`. */\n next?: number;\n\n /** SPFN API server. */\n server?: number;\n}\n\nexport interface SpfnAppConfig\n{\n ports?: SpfnPorts;\n\n /** Host the SPFN API server binds. */\n host?: string;\n\n /** Deployment settings — untouched by this module, kept so the type is whole. */\n [key: string]: unknown;\n}\n\n/**\n * The single place each default exists.\n *\n * Defaults live at the bottom of the chain and nowhere else. A default declared\n * at an input layer — a commander option, a generated entry file, an env schema\n * — cannot be told apart from a value someone supplied, so it silently outranks\n * whatever is below it. Three separate defects in this repository came from\n * exactly that, including an env schema default that forced the environment to\n * be consulted last and made an injected port unreachable.\n */\nexport const PORT_DEFAULTS = {\n next: 3790,\n server: 8790,\n} as const;\n\nexport const HOST_DEFAULT = 'localhost';\n\nconst CONFIG_FILE_NAMES = [\n 'spfn.config.js',\n 'spfn.config.mjs',\n] as const;\n\n/**\n * Read `spfn.config.js` from an app directory.\n *\n * Returns an empty object when there is no file, and also when the file cannot\n * be imported — a malformed deployment config must not stop a server from\n * booting. The caller decides whether the absence is worth reporting.\n */\nexport async function loadAppConfig(cwd: string = process.cwd()): Promise<SpfnAppConfig>\n{\n for (const fileName of CONFIG_FILE_NAMES)\n {\n const fullPath = join(cwd, fileName);\n\n if (!existsSync(fullPath))\n {\n continue;\n }\n\n try\n {\n // A file URL, not a path: on Windows an absolute path is not a\n // valid ESM specifier, and this file is imported at runtime.\n const module = await import(pathToFileURL(fullPath).href);\n\n return (module.default ?? {}) as SpfnAppConfig;\n }\n catch (error)\n {\n // Falling back to defaults, and saying so. A file that exists and\n // cannot be imported is a typo, not an absence — silence would send\n // the server to port 8790 while its own config named another, and\n // the only symptom would be a port nobody chose.\n //\n // console, not the server logger: this runs inside the CLI too,\n // before any logger is configured.\n console.warn(\n `⚠️ ${fileName} could not be imported — falling back to defaults `\n + `for ports and host. ${error instanceof Error ? error.message : String(error)}`,\n );\n\n return {};\n }\n }\n\n return {};\n}\n\n/**\n * The port each process binds, given a loaded config and the environment.\n *\n * Three layers, in this order, and no more: environment variable, then\n * `spfn.config.js`, then the default.\n */\nexport function resolvePorts(\n config: SpfnAppConfig,\n env: NodeJS.ProcessEnv = process.env,\n): { next: number; server: number }\n{\n return {\n next: readPort(env.NEXT_PORT) ?? config.ports?.next ?? PORT_DEFAULTS.next,\n server: readPort(env.SPFN_PORT) ?? config.ports?.server ?? PORT_DEFAULTS.server,\n };\n}\n\n/**\n * The host the SPFN server binds, on the same three layers.\n *\n * `localhost` by default: a container states `SPFN_HOST=0.0.0.0` in its own\n * files, where that decision belongs, rather than every developer machine\n * publishing a dev server to its network.\n */\nexport function resolveHost(\n config: SpfnAppConfig,\n env: NodeJS.ProcessEnv = process.env,\n): string\n{\n return env.SPFN_HOST || config.host || HOST_DEFAULT;\n}\n\n/**\n * Where the SPFN server binds, including the address a server config may still\n * carry through `.port()` / `.host()`.\n *\n * Those two are deprecated and sit between `spfn.config.js` and the default for\n * one release, so an app that has not moved yet keeps the address it had. Once\n * they are removed this collapses back to {@link resolvePorts} and\n * {@link resolveHost}.\n */\nexport function resolveServerAddress(\n config: SpfnAppConfig,\n deprecated: { port?: number; host?: string } = {},\n env: NodeJS.ProcessEnv = process.env,\n): { port: number; host: string }\n{\n return {\n port: readPort(env.SPFN_PORT)\n ?? config.ports?.server\n ?? deprecated.port\n ?? PORT_DEFAULTS.server,\n host: env.SPFN_HOST\n || config.host\n || deprecated.host\n || HOST_DEFAULT,\n };\n}\n\nfunction readPort(value: string | undefined): number | undefined\n{\n if (!value)\n {\n return undefined;\n }\n\n const port = Number(value);\n\n return Number.isInteger(port) && port > 0 && port < 65536 ? port : undefined;\n}\n"]}
@@ -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,430 @@
1
+ import { logger } from '@spfn/core/logger';
2
+
3
+ // src/errors/error-registry.ts
4
+ var ErrorRegistry = class _ErrorRegistry {
5
+ errors = /* @__PURE__ */ new Map();
6
+ constructor(initialErrors) {
7
+ if (initialErrors) {
8
+ for (const input of initialErrors) {
9
+ if (input instanceof _ErrorRegistry) {
10
+ this.concat(input);
11
+ } else if (Array.isArray(input)) {
12
+ this.append(input);
13
+ } else {
14
+ this.append(input);
15
+ }
16
+ }
17
+ }
18
+ }
19
+ /**
20
+ * Append error class(es) to the registry
21
+ *
22
+ * @param input - Error constructor or array of constructors
23
+ * @returns This registry for chaining
24
+ */
25
+ append(input) {
26
+ if (Array.isArray(input)) {
27
+ for (const ErrorClass of input) {
28
+ this.errors.set(ErrorClass.name, ErrorClass);
29
+ }
30
+ } else {
31
+ this.errors.set(input.name, input);
32
+ }
33
+ return this;
34
+ }
35
+ /**
36
+ * Concatenate another ErrorRegistry into this one
37
+ *
38
+ * @param registry - Another ErrorRegistry to merge
39
+ * @returns This registry for chaining
40
+ */
41
+ concat(registry) {
42
+ for (const [name, ErrorClass] of registry.errors) {
43
+ this.errors.set(name, ErrorClass);
44
+ }
45
+ return this;
46
+ }
47
+ /**
48
+ * Check if error type is registered
49
+ *
50
+ * @param name - Error class name
51
+ */
52
+ has(name) {
53
+ return this.errors.has(name);
54
+ }
55
+ /**
56
+ * Deserialize error from JSON data
57
+ *
58
+ * @param data - Serialized error data with __type field
59
+ * @returns Deserialized error instance
60
+ * @throws Error if error type is not registered
61
+ */
62
+ deserialize(data) {
63
+ const ErrorClass = this.errors.get(data.__type);
64
+ if (!ErrorClass) {
65
+ throw new Error(`Unknown error type: ${data.__type}`);
66
+ }
67
+ const { __type: _type, error: _envelope, ...fields } = data;
68
+ return new ErrorClass(fields);
69
+ }
70
+ /**
71
+ * Try to deserialize error, return null if type unknown
72
+ *
73
+ * @param data - Serialized error data
74
+ * @returns Deserialized error or null
75
+ */
76
+ tryDeserialize(data) {
77
+ if (!data.__type || !this.has(data.__type)) {
78
+ return null;
79
+ }
80
+ try {
81
+ return this.deserialize(data);
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Get all registered error types
88
+ */
89
+ getRegisteredTypes() {
90
+ return Array.from(this.errors.keys());
91
+ }
92
+ };
93
+ var RESERVED_RESPONSE_KEYS = /* @__PURE__ */ new Set(["__type", "message", "error"]);
94
+ var AUTHORING_ENVIRONMENTS = /* @__PURE__ */ new Set(["local", "development", "test"]);
95
+ function refuseReservedKey(className, key) {
96
+ const detail = `${className} declares the reserved field "${key}". Reserved response keys: ${[...RESERVED_RESPONSE_KEYS].join(", ")}. Rename the field.`;
97
+ if (AUTHORING_ENVIRONMENTS.has(process.env.NODE_ENV ?? "local")) {
98
+ throw new Error(detail);
99
+ }
100
+ logger.error(`[SerializableError] ${detail} The field is omitted from the response.`);
101
+ }
102
+ var SerializableError = class extends Error {
103
+ /**
104
+ * Serialize error to JSON-compatible object
105
+ *
106
+ * Automatically includes:
107
+ * - __type: Constructor name for deserialization
108
+ * - message: Error message
109
+ * - All public instance properties (except name, stack)
110
+ */
111
+ toJSON() {
112
+ const json = {
113
+ __type: this.constructor.name,
114
+ message: this.message
115
+ };
116
+ for (const key of Object.keys(this)) {
117
+ if (key === "name" || key === "stack" || key === "statusCode") {
118
+ continue;
119
+ }
120
+ if (RESERVED_RESPONSE_KEYS.has(key)) {
121
+ refuseReservedKey(this.constructor.name, key);
122
+ continue;
123
+ }
124
+ json[key] = this[key];
125
+ }
126
+ return json;
127
+ }
128
+ };
129
+
130
+ // src/errors/http-errors.ts
131
+ var HttpError = class extends SerializableError {
132
+ statusCode;
133
+ details;
134
+ constructor(data) {
135
+ super(data.message);
136
+ this.name = "HttpError";
137
+ this.statusCode = data.statusCode;
138
+ if (data.details) {
139
+ this.details = data.details;
140
+ }
141
+ Error.captureStackTrace(this, this.constructor);
142
+ }
143
+ };
144
+ var BadRequestError = class extends HttpError {
145
+ constructor(data = {}) {
146
+ super({
147
+ message: data.message || "Bad request",
148
+ statusCode: 400,
149
+ details: data.details
150
+ });
151
+ this.name = "BadRequestError";
152
+ }
153
+ };
154
+ var ValidationError = class extends HttpError {
155
+ fields;
156
+ constructor(data) {
157
+ super({
158
+ message: data.message,
159
+ statusCode: 400,
160
+ details: data.details
161
+ });
162
+ this.name = "ValidationError";
163
+ if (data.fields) {
164
+ this.fields = data.fields;
165
+ }
166
+ }
167
+ };
168
+ var UnauthorizedError = class extends HttpError {
169
+ constructor(data = {}) {
170
+ super({
171
+ message: data.message || "Authentication required",
172
+ statusCode: 401,
173
+ details: data.details
174
+ });
175
+ this.name = "UnauthorizedError";
176
+ }
177
+ };
178
+ var ForbiddenError = class extends HttpError {
179
+ constructor(data = {}) {
180
+ super({
181
+ message: data.message || "Access forbidden",
182
+ statusCode: 403,
183
+ details: data.details
184
+ });
185
+ this.name = "ForbiddenError";
186
+ }
187
+ };
188
+ var NotFoundError = class extends HttpError {
189
+ resource;
190
+ constructor(data = {}) {
191
+ super({
192
+ message: data.message || "Resource not found",
193
+ statusCode: 404,
194
+ details: data.details
195
+ });
196
+ this.name = "NotFoundError";
197
+ if (data.resource) {
198
+ this.resource = data.resource;
199
+ }
200
+ }
201
+ };
202
+ var ConflictError = class extends HttpError {
203
+ constructor(data = {}) {
204
+ super({
205
+ message: data.message || "Resource conflict",
206
+ statusCode: 409,
207
+ details: data.details
208
+ });
209
+ this.name = "ConflictError";
210
+ }
211
+ };
212
+ var GoneError = class extends HttpError {
213
+ resource;
214
+ constructor(data = {}) {
215
+ super({
216
+ message: data.message || "Resource permanently deleted",
217
+ statusCode: 410,
218
+ details: data.details
219
+ });
220
+ this.name = "GoneError";
221
+ if (data.resource) {
222
+ this.resource = data.resource;
223
+ }
224
+ }
225
+ };
226
+ var TooManyRequestsError = class extends HttpError {
227
+ retryAfter;
228
+ constructor(data = {}) {
229
+ super({
230
+ message: data.message || "Too many requests",
231
+ statusCode: 429,
232
+ details: data.details
233
+ });
234
+ this.name = "TooManyRequestsError";
235
+ if (data.retryAfter) {
236
+ this.retryAfter = data.retryAfter;
237
+ }
238
+ }
239
+ };
240
+ var InternalServerError = class extends HttpError {
241
+ constructor(data = {}) {
242
+ super({
243
+ message: data.message || "Internal server error",
244
+ statusCode: 500,
245
+ details: data.details
246
+ });
247
+ this.name = "InternalServerError";
248
+ }
249
+ };
250
+ var UnsupportedMediaTypeError = class extends HttpError {
251
+ mediaType;
252
+ supportedTypes;
253
+ constructor(data = {}) {
254
+ super({
255
+ message: data.message || "Unsupported media type",
256
+ statusCode: 415,
257
+ details: data.details
258
+ });
259
+ this.name = "UnsupportedMediaTypeError";
260
+ if (data.mediaType) {
261
+ this.mediaType = data.mediaType;
262
+ }
263
+ if (data.supportedTypes) {
264
+ this.supportedTypes = data.supportedTypes;
265
+ }
266
+ }
267
+ };
268
+ var UnprocessableEntityError = class extends HttpError {
269
+ constructor(data = {}) {
270
+ super({
271
+ message: data.message || "Unprocessable entity",
272
+ statusCode: 422,
273
+ details: data.details
274
+ });
275
+ this.name = "UnprocessableEntityError";
276
+ }
277
+ };
278
+ var ServiceUnavailableError = class extends HttpError {
279
+ retryAfter;
280
+ constructor(data = {}) {
281
+ super({
282
+ message: data.message || "Service unavailable",
283
+ statusCode: 503,
284
+ details: data.details
285
+ });
286
+ this.name = "ServiceUnavailableError";
287
+ if (data.retryAfter) {
288
+ this.retryAfter = data.retryAfter;
289
+ }
290
+ }
291
+ };
292
+
293
+ // src/errors/database-errors.ts
294
+ var DatabaseError = class extends SerializableError {
295
+ statusCode;
296
+ details;
297
+ constructor(data) {
298
+ super(data.message);
299
+ this.name = "DatabaseError";
300
+ this.statusCode = data.statusCode ?? 500;
301
+ this.details = data.details;
302
+ Error.captureStackTrace(this, this.constructor);
303
+ }
304
+ /**
305
+ * Whether this error's message/details are derived from the raw database
306
+ * driver (SQL text, table/column/constraint names, parameter values) and
307
+ * therefore must NOT be exposed to clients in production. Defined as a
308
+ * prototype getter so it is never serialized into the response by toJSON().
309
+ * Subclasses with a safe, constructed message override this to `false`.
310
+ */
311
+ get internal() {
312
+ return true;
313
+ }
314
+ };
315
+ var ConnectionError = class extends DatabaseError {
316
+ constructor(data) {
317
+ super({ message: data.message, statusCode: 503, details: data.details });
318
+ this.name = "ConnectionError";
319
+ }
320
+ };
321
+ var QueryError = class extends DatabaseError {
322
+ constructor(data) {
323
+ super({
324
+ message: data.message,
325
+ statusCode: data.statusCode ?? 500,
326
+ details: data.details
327
+ });
328
+ this.name = "QueryError";
329
+ }
330
+ };
331
+ var EntityNotFoundError = class extends QueryError {
332
+ resource;
333
+ id;
334
+ constructor(data) {
335
+ super({
336
+ message: `${data.resource} with id ${data.id} not found`,
337
+ statusCode: 404,
338
+ details: { resource: data.resource, id: data.id }
339
+ });
340
+ this.name = "EntityNotFoundError";
341
+ this.resource = data.resource;
342
+ this.id = data.id;
343
+ }
344
+ // Message/details are constructed from the resource + id, not driver text
345
+ get internal() {
346
+ return false;
347
+ }
348
+ };
349
+ var ConstraintViolationError = class extends QueryError {
350
+ constructor(data) {
351
+ super({ message: data.message, statusCode: 400, details: data.details });
352
+ this.name = "ConstraintViolationError";
353
+ }
354
+ };
355
+ var TransactionError = class extends DatabaseError {
356
+ constructor(data) {
357
+ super({
358
+ message: data.message,
359
+ statusCode: data.statusCode ?? 500,
360
+ details: data.details
361
+ });
362
+ this.name = "TransactionError";
363
+ }
364
+ };
365
+ var DeadlockError = class extends TransactionError {
366
+ constructor(data) {
367
+ super({ message: data.message, statusCode: 409, details: data.details });
368
+ this.name = "DeadlockError";
369
+ }
370
+ };
371
+ var DuplicateEntryError = class extends QueryError {
372
+ field;
373
+ value;
374
+ constructor(data) {
375
+ super({
376
+ message: `${data.field} already exists`,
377
+ statusCode: 409,
378
+ details: { field: data.field }
379
+ });
380
+ this.name = "DuplicateEntryError";
381
+ this.field = data.field;
382
+ this.value = data.value;
383
+ }
384
+ // Field-only message/details — safe to surface to the client (not internal)
385
+ get internal() {
386
+ return false;
387
+ }
388
+ };
389
+
390
+ // src/errors/index.ts
391
+ var errorRegistry = new ErrorRegistry();
392
+ errorRegistry.append([
393
+ HttpError,
394
+ BadRequestError,
395
+ ValidationError,
396
+ UnauthorizedError,
397
+ ForbiddenError,
398
+ NotFoundError,
399
+ ConflictError,
400
+ GoneError,
401
+ TooManyRequestsError,
402
+ UnsupportedMediaTypeError,
403
+ UnprocessableEntityError,
404
+ InternalServerError,
405
+ ServiceUnavailableError
406
+ ]);
407
+ errorRegistry.append([
408
+ DatabaseError,
409
+ ConnectionError,
410
+ QueryError,
411
+ EntityNotFoundError,
412
+ ConstraintViolationError,
413
+ TransactionError,
414
+ DeadlockError,
415
+ DuplicateEntryError
416
+ ]);
417
+
418
+ // src/authz/index.ts
419
+ function requireOwner(resource, ownerId, options = {}) {
420
+ const ownerKey = options.ownerKey ?? "ownerId";
421
+ const value = resource ? resource[ownerKey] : void 0;
422
+ if (!resource || value === null || value === void 0 || String(value) !== String(ownerId)) {
423
+ throw new NotFoundError({ message: options.message ?? "not found" });
424
+ }
425
+ return resource;
426
+ }
427
+
428
+ export { requireOwner };
429
+ //# sourceMappingURL=index.js.map
430
+ //# sourceMappingURL=index.js.map