@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc

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 (224) hide show
  1. package/README.md +27 -2
  2. package/dist/GCSStorageController-fv6JAZ87.js +402 -0
  3. package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
  4. package/dist/S3StorageController-BNg-pXiY.js +480 -0
  5. package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
  6. package/dist/api/mount-path.d.ts +30 -0
  7. package/dist/api/mount.d.ts +6 -13
  8. package/dist/api/rest/api-generator.d.ts +53 -1
  9. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  10. package/dist/api/rest/field-access-query.d.ts +28 -8
  11. package/dist/api/rest/field-ops.d.ts +19 -7
  12. package/dist/api/rest/query-body.d.ts +10 -0
  13. package/dist/api/rest/write-validation.d.ts +7 -5
  14. package/dist/api/schema-editor-routes.d.ts +10 -1
  15. package/dist/api/sql-export-routes.d.ts +8 -0
  16. package/dist/api/types.d.ts +35 -0
  17. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
  18. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
  19. package/dist/auth/access.d.ts +57 -8
  20. package/dist/auth/admin-user-ops.d.ts +24 -2
  21. package/dist/auth/admin-users-route.d.ts +1 -0
  22. package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
  23. package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
  24. package/dist/auth/builtin-auth-adapter.d.ts +13 -0
  25. package/dist/auth/email-change-routes.d.ts +3 -0
  26. package/dist/auth/expired-token-sweep.d.ts +5 -1
  27. package/dist/auth/interfaces.d.ts +10 -0
  28. package/dist/auth/jwks-routes.d.ts +5 -1
  29. package/dist/auth/jwt-crypto.d.ts +7 -0
  30. package/dist/auth/jwt-keys.d.ts +4 -0
  31. package/dist/auth/jwt.d.ts +282 -166
  32. package/dist/auth/magic-link-routes.d.ts +3 -0
  33. package/dist/auth/mfa-crypto.d.ts +9 -3
  34. package/dist/auth/mfa-gate.d.ts +4 -2
  35. package/dist/auth/mfa-routes.d.ts +6 -0
  36. package/dist/auth/middleware.d.ts +9 -2
  37. package/dist/auth/otp-routes.d.ts +3 -0
  38. package/dist/auth/rate-limiter.d.ts +61 -34
  39. package/dist/auth/registration-policy.d.ts +17 -0
  40. package/dist/auth/require-auth.d.ts +1 -1
  41. package/dist/auth/routes.d.ts +16 -0
  42. package/dist/auth/session-routes.d.ts +6 -0
  43. package/dist/auth/user-picking.d.ts +37 -0
  44. package/dist/auth/verify-credential.d.ts +25 -4
  45. package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
  46. package/dist/auth-GnONzteu.js.map +1 -0
  47. package/dist/auth_adapter-CIItCTc8.js +21 -0
  48. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  49. package/dist/backend/create-backend.d.ts +281 -0
  50. package/dist/backend/default-backend.d.ts +35 -0
  51. package/dist/backend/options.d.ts +912 -0
  52. package/dist/backend/scope.d.ts +23 -0
  53. package/dist/backend/services.d.ts +54 -0
  54. package/dist/backend-DTAOsLQc.js.map +1 -1
  55. package/dist/backup/backup-common.d.ts +14 -3
  56. package/dist/backup/backup-routes.d.ts +23 -2
  57. package/dist/backup/backup-storage.d.ts +3 -1
  58. package/dist/backup/index.d.ts +3 -1
  59. package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
  60. package/dist/backup-BnvkSLHi.js.map +1 -0
  61. package/dist/boot/boot.d.ts +2 -2
  62. package/dist/boot/driver.d.ts +3 -0
  63. package/dist/boot/env.d.ts +9 -2
  64. package/dist/boot/http-server.d.ts +22 -0
  65. package/dist/boot/options.d.ts +16 -1
  66. package/dist/boot/provision.d.ts +4 -2
  67. package/dist/boot/runtime-cors.d.ts +17 -0
  68. package/dist/boot/sources.d.ts +2 -0
  69. package/dist/boot/static-routing.d.ts +9 -0
  70. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
  71. package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
  72. package/dist/cron/cron-loader.d.ts +77 -18
  73. package/dist/cron/cron-routes.d.ts +1 -1
  74. package/dist/cron/cron-scheduler.d.ts +69 -3
  75. package/dist/cron/cron-store.d.ts +33 -5
  76. package/dist/cron/define-cron.d.ts +6 -2
  77. package/dist/cron/index.d.ts +3 -3
  78. package/dist/cron-loader-B-u0snqq.js +202 -0
  79. package/dist/cron-loader-B-u0snqq.js.map +1 -0
  80. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
  81. package/dist/cron-routes-OD6BstUj.js.map +1 -0
  82. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
  83. package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
  84. package/dist/cron-store-CZ30ZBi-.js +577 -0
  85. package/dist/cron-store-CZ30ZBi-.js.map +1 -0
  86. package/dist/email/smtp-email-service.d.ts +23 -0
  87. package/dist/email/types.d.ts +18 -0
  88. package/dist/env.d.ts +0 -37
  89. package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
  90. package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
  91. package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
  92. package/dist/function-loader-iKhOZatS.js.map +1 -0
  93. package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
  94. package/dist/function-routes-j9TwpvQe.js.map +1 -0
  95. package/dist/functions/context.d.ts +9 -0
  96. package/dist/functions/define-function.d.ts +10 -6
  97. package/dist/functions/function-loader.d.ts +75 -18
  98. package/dist/functions/function-routes.d.ts +2 -1
  99. package/dist/functions/index.d.ts +5 -4
  100. package/dist/functions/index.js +42 -13
  101. package/dist/functions/index.js.map +1 -1
  102. package/dist/functions/internal.d.ts +2 -2
  103. package/dist/functions/proxy.d.ts +6 -2
  104. package/dist/functions/request-timeout.d.ts +7 -5
  105. package/dist/history/history-routes.d.ts +9 -0
  106. package/dist/history/retention-sweep.d.ts +81 -0
  107. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
  108. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
  109. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  110. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  111. package/dist/index.d.ts +23 -10
  112. package/dist/index.es.js +24423 -20038
  113. package/dist/index.es.js.map +1 -1
  114. package/dist/init/docs.d.ts +27 -10
  115. package/dist/init/process-safety.d.ts +7 -2
  116. package/dist/init/shutdown.d.ts +18 -3
  117. package/dist/init.d.ts +49 -865
  118. package/dist/internal-tables-L2t39RdT.js +136 -0
  119. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  120. package/dist/jobs/job-queue.d.ts +8 -0
  121. package/dist/jobs/job-store.d.ts +24 -0
  122. package/dist/jobs/types.d.ts +25 -4
  123. package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
  124. package/dist/jobs-BCbflZj2.js.map +1 -0
  125. package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
  126. package/dist/jwt-BlvuKoR6.js.map +1 -0
  127. package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
  128. package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
  129. package/dist/load-failure-L5hvfDoG.js +80 -0
  130. package/dist/load-failure-L5hvfDoG.js.map +1 -0
  131. package/dist/mcp/mcp-routes.d.ts +6 -0
  132. package/dist/mcp/mcp-tools.d.ts +8 -0
  133. package/dist/mcp/oauth-routes.d.ts +7 -0
  134. package/dist/metrics/index.d.ts +18 -0
  135. package/dist/metrics/pool-wait.d.ts +50 -0
  136. package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
  137. package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
  138. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
  139. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
  140. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
  141. package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
  142. package/dist/policy-PdzX-Opd.js +121 -0
  143. package/dist/policy-PdzX-Opd.js.map +1 -0
  144. package/dist/project_manifest-R6y_KHms.js +59 -0
  145. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  146. package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
  147. package/dist/proxy-CkTlMGmh.js.map +1 -0
  148. package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
  149. package/dist/query-parser-4WkMoA_K.js.map +1 -0
  150. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
  151. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  152. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  153. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  154. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  155. package/dist/schema-edit/local-source.d.ts +29 -0
  156. package/dist/schema-edit/remote-source.d.ts +13 -3
  157. package/dist/schema-edit/source-editor.d.ts +28 -0
  158. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
  159. package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
  160. package/dist/services/outbound-post.d.ts +50 -0
  161. package/dist/services/outbound-url-guard.d.ts +34 -5
  162. package/dist/services/routed-realtime-service.d.ts +5 -0
  163. package/dist/services/webhook-service.d.ts +66 -14
  164. package/dist/singleton.d.ts +9 -5
  165. package/dist/soft-delete-params-DfV3vVN5.js +668 -0
  166. package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
  167. package/dist/sql-export-routes-ClRwzIsC.js +240 -0
  168. package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
  169. package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
  170. package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
  171. package/dist/storage/GCSStorageController.d.ts +46 -2
  172. package/dist/storage/LocalStorageController.d.ts +67 -2
  173. package/dist/storage/S3StorageController.d.ts +37 -3
  174. package/dist/storage/image-transform.d.ts +5 -0
  175. package/dist/storage/maintenance-routes.d.ts +23 -0
  176. package/dist/storage/maintenance.d.ts +126 -0
  177. package/dist/storage/rendition-cache.d.ts +9 -11
  178. package/dist/storage/request-principals.d.ts +30 -0
  179. package/dist/storage/routes.d.ts +20 -0
  180. package/dist/storage/source-variables.d.ts +10 -0
  181. package/dist/storage/streams.d.ts +42 -0
  182. package/dist/storage/tus-handler.d.ts +46 -2
  183. package/dist/storage/types.d.ts +82 -7
  184. package/dist/storage/upload-validation.d.ts +13 -0
  185. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  186. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  187. package/dist/strings.es-BeZWAodn.js +256 -0
  188. package/dist/strings.es-BeZWAodn.js.map +1 -0
  189. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  190. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  191. package/dist/utils/byte-size.d.ts +10 -0
  192. package/dist/utils/compression.d.ts +6 -0
  193. package/dist/utils/load-failure.d.ts +14 -0
  194. package/package.json +5 -5
  195. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  196. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  197. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  198. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  199. package/dist/auth-DMLngxn_.js.map +0 -1
  200. package/dist/backup-CN0s50D2.js.map +0 -1
  201. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  202. package/dist/cron-loader-CwaANlOG.js +0 -118
  203. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  204. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  205. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  206. package/dist/cron-store-D2Q9-Aco.js +0 -300
  207. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  208. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  209. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  210. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  211. package/dist/function-routes-CaNG4waN.js.map +0 -1
  212. package/dist/jobs-DqYNfquG.js.map +0 -1
  213. package/dist/jwt-R6bSPMjk.js.map +0 -1
  214. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  215. package/dist/proxy-qRlqeUmO.js.map +0 -1
  216. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  217. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  218. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  219. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  220. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  221. package/dist/src-I3aG1PcY.js +0 -6870
  222. package/dist/src-I3aG1PcY.js.map +0 -1
  223. package/dist/types-BfKcm9do.js +0 -39
  224. package/dist/types-BfKcm9do.js.map +0 -1
@@ -4,16 +4,48 @@ globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
6
  import { r as logger } from "./logger-D-S-hO5e.js";
7
- import { t as nativeDynamicImport } from "./dynamic-import-X40pTZUQ.js";
7
+ import { i as warnDeprecated, n as describeLoadFailure, r as nativeDynamicImport, t as SYNTAX_ERROR_ADVICE } from "./load-failure-L5hvfDoG.js";
8
8
  import * as fs$1 from "fs";
9
9
  import * as path$1 from "path";
10
10
  import { pathToFileURL } from "url";
11
11
  //#region src/functions/function-loader.ts
12
12
  var function_loader_exports = /* @__PURE__ */ __exportAll({
13
+ functionList: () => functionList,
14
+ loadFunctions: () => loadFunctions,
13
15
  loadFunctionsFromDirectory: () => loadFunctionsFromDirectory,
14
- loadFunctionsWithDiagnostics: () => loadFunctionsWithDiagnostics
16
+ loadFunctionsWithDiagnostics: () => loadFunctionsWithDiagnostics,
17
+ readFunctionsDirectory: () => readFunctionsDirectory
15
18
  });
16
19
  /**
20
+ * Load the custom functions in a directory, as values.
21
+ *
22
+ * Each file default-exports a Hono app — `defineFunction(...)` — or a factory
23
+ * returning one. The filename without its extension is the record key, which
24
+ * is the URL segment it is served at: `functions/send-invoice.ts` →
25
+ * `{ "send-invoice": app }`. Only the top level is read.
26
+ *
27
+ * Never throws for a bad file: it is skipped, logged, and passed to
28
+ * `options.onSkip`.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const functions = await loadFunctions(path.resolve("functions"));
33
+ * const backend = await createRebaseBackend({ database, functions, provision: true });
34
+ * ```
35
+ */
36
+ async function loadFunctions(directory, options = {}) {
37
+ const { functions, problems } = await readFunctionsDirectory(directory, options.importModule);
38
+ if (options.onSkip) for (const problem of problems) options.onSkip(problem);
39
+ return functions;
40
+ }
41
+ /** A record of functions as the list {@link createFunctionRoutes} mounts, in record order. */
42
+ function functionList(functions) {
43
+ return Object.entries(functions).map(([name, app]) => ({
44
+ name,
45
+ app
46
+ }));
47
+ }
48
+ /**
17
49
  * Extensions the bundler compiles (or a developer might reasonably write) that
18
50
  * this loader cannot import. `rebase build` globs `functions/**\/*.ts`, so the
19
51
  * build's idea of "a function file" is strictly wider than the runtime's — the
@@ -30,29 +62,35 @@ var UNSUPPORTED_CODE_EXTENSIONS = [
30
62
  ".cjs"
31
63
  ];
32
64
  /**
33
- * Auto-discover Hono route files from a directory.
34
- *
35
- * Each file should default-export a Hono app (or router).
36
- * The filename (without extension) becomes the mount path:
37
- * `functions/send-invoice.ts` → mounted at `/send-invoice`
38
- *
39
- * This mirrors how `loadCollectionsFromDirectory` works for collections.
40
- *
41
- * Returns only what loaded. Use {@link loadFunctionsWithDiagnostics} when the
42
- * caller also needs to report what did not.
65
+ * @deprecated Use `loadFunctions` instead, which returns the functions as a
66
+ * record keyed by name. Will be removed in a future release.
43
67
  */
44
68
  async function loadFunctionsFromDirectory(directory, importModule = nativeDynamicImport) {
45
- return (await loadFunctionsWithDiagnostics(directory, importModule)).functions;
69
+ warnDeprecated("server.loadFunctionsFromDirectory", "`loadFunctionsFromDirectory` is deprecated; use `loadFunctions(dir)`, which returns `{ [name]: app }`.");
70
+ return functionList((await readFunctionsDirectory(directory, importModule)).functions);
46
71
  }
47
72
  /**
48
- * {@link loadFunctionsFromDirectory}, plus the list of files that were skipped.
73
+ * @deprecated Use `loadFunctions` instead, with `onSkip` to collect the files
74
+ * it skipped. Will be removed in a future release.
75
+ */
76
+ async function loadFunctionsWithDiagnostics(directory, importModule = nativeDynamicImport) {
77
+ warnDeprecated("server.loadFunctionsWithDiagnostics", "`loadFunctionsWithDiagnostics` is deprecated; use `loadFunctions(dir, { onSkip })`, which returns `{ [name]: app }`.");
78
+ const { functions, problems } = await readFunctionsDirectory(directory, importModule);
79
+ return {
80
+ functions: functionList(functions),
81
+ problems
82
+ };
83
+ }
84
+ /**
85
+ * Read a directory of function files: what loaded, keyed by name, and one
86
+ * `<file> (<reason>)` per file that did not.
49
87
  *
50
88
  * Never throws: a single malformed function must not crash server boot. The
51
- * caller decides what to do with `problems` — `init.ts` mounts the router
52
- * regardless and surfaces the count on the listing endpoint.
89
+ * backend mounts the router regardless and surfaces the count of skipped files
90
+ * on the listing endpoint.
53
91
  */
54
- async function loadFunctionsWithDiagnostics(directory, importModule = nativeDynamicImport) {
55
- const functions = [];
92
+ async function readFunctionsDirectory(directory, importModule = nativeDynamicImport) {
93
+ const functions = {};
56
94
  const problems = [];
57
95
  if (!fs$1.existsSync(directory)) return {
58
96
  functions,
@@ -83,26 +121,17 @@ async function loadFunctionsWithDiagnostics(directory, importModule = nativeDyna
83
121
  problems.push(`${file} (no default export)`);
84
122
  continue;
85
123
  }
86
- if (isHonoLike(exported)) {
124
+ const app = isHonoLike(exported) ? exported : typeof exported === "function" ? exported() : void 0;
125
+ if (isHonoLike(app)) {
87
126
  const name = path$1.basename(file, path$1.extname(file));
88
- functions.push({
89
- name,
90
- app: exported
91
- });
92
- logger.debug(`⚡ Loaded function route: ${name}`);
93
- continue;
94
- }
95
- if (typeof exported === "function") {
96
- const result = exported();
97
- if (isHonoLike(result)) {
98
- const name = path$1.basename(file, path$1.extname(file));
99
- functions.push({
100
- name,
101
- app: result
102
- });
103
- logger.debug(`⚡ Loaded function route: ${name}`);
127
+ if (Object.hasOwn(functions, name)) {
128
+ logger.warn(`[functions] ${file}: another file is already served as '${name}'. Skipping.`);
129
+ problems.push(`${file} (another file is already served as '${name}')`);
104
130
  continue;
105
131
  }
132
+ functions[name] = app;
133
+ logger.debug(`⚡ Loaded function route: ${name}`);
134
+ continue;
106
135
  }
107
136
  const exportType = typeof exported;
108
137
  const keys = exported && typeof exported === "object" ? Object.getOwnPropertyNames(Object.getPrototypeOf(exported)).slice(0, 10).join(", ") : "N/A";
@@ -113,12 +142,13 @@ async function loadFunctionsWithDiagnostics(directory, importModule = nativeDyna
113
142
  } catch (err) {
114
143
  const message = err instanceof Error ? err.message : String(err);
115
144
  logger.error(`[functions] Failed to load ${file}: ${message}`);
116
- problems.push(`${file} (threw: ${message})`);
145
+ problems.push(describeLoadFailure(file, err));
117
146
  }
118
147
  }
119
148
  }
120
149
  if (problems.length > 0) {
121
150
  const advice = [];
151
+ if (problems.some((p) => p.includes("(syntax error"))) advice.push(SYNTAX_ERROR_ADVICE);
122
152
  if (problems.some((p) => p.includes("(threw:"))) advice.push(" A file that threw ran at import time. Read configuration *inside* a handler (`requireEnv(c, \"STRIPE_KEY\")`, or a lazily-built client) rather than at module scope, where one undefined variable takes the whole file down before any route exists.");
123
153
  if (problems.some((p) => p.includes("(no default export)"))) advice.push(" A file with no default export exports nothing the loader can mount. `export default defineFunction((app) => { … })`.");
124
154
  if (problems.some((p) => p.includes("(not a Hono app or factory)"))) advice.push(" A default export the loader did not recognise is usually two copies of hono. Author with `defineFunction(...)` from @rebasepro/server/functions, which uses the server's own copy.");
@@ -141,6 +171,6 @@ function isHonoLike(obj) {
141
171
  return typeof record.fetch === "function" && Array.isArray(record.routes);
142
172
  }
143
173
  //#endregion
144
- export { loadFunctionsFromDirectory as n, loadFunctionsWithDiagnostics as r, function_loader_exports as t };
174
+ export { loadFunctionsWithDiagnostics as i, loadFunctions as n, loadFunctionsFromDirectory as r, function_loader_exports as t };
145
175
 
146
- //# sourceMappingURL=function-loader-D7o5Epjj.js.map
176
+ //# sourceMappingURL=function-loader-iKhOZatS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"function-loader-iKhOZatS.js","names":[],"sources":["../src/functions/function-loader.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { Hono } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { logger } from \"../utils/logger.js\";\nimport { nativeDynamicImport, type ModuleImporter } from \"../utils/dynamic-import.js\";\nimport { describeLoadFailure, SYNTAX_ERROR_ADVICE } from \"../utils/load-failure.js\";\nimport { warnDeprecated } from \"@rebasepro/common\";\n\n/**\n * One custom function: a Hono app, served at `<functions mount>/<name>`.\n *\n * What `defineFunction(...)` returns, and what a function file default-exports:\n * a Hono app over {@link HonoEnv}, the environment a function runs in — build\n * one by hand as `new Hono<HonoEnv>()`. Any Hono-compatible app works at\n * runtime — the loader checks for `.fetch()` and `.routes` rather than an\n * `instanceof`, because a project and the server can hold two copies of hono.\n */\nexport type FunctionApp = Hono<HonoEnv>;\n\n/**\n * Functions as values: the URL segment each one answers at, to its app.\n *\n * `functions: { \"send-invoice\": sendInvoice }` serves `sendInvoice` at\n * `/api/functions/send-invoice` — the same thing a file named\n * `functions/send-invoice.ts` does. {@link loadFunctions} returns exactly this\n * shape, so `functions: await loadFunctions(dir)` and `functionsDir: dir` serve\n * the same routes.\n */\nexport type FunctionsRecord = Record<string, FunctionApp>;\n\n/** How a directory of modules is read. Shared by the function and cron loaders. */\nexport interface LoadDirectoryOptions {\n /**\n * How a module file is imported. Defaults to the runtime's own `import()`;\n * a caller with its own loader hooks (tsx, a test runner's sandbox) passes\n * `url => import(url)` so they apply.\n */\n importModule?: ModuleImporter;\n /**\n * Called once for every file the loader saw and did **not** load, as\n * `<file> (<reason>)`. The files are skipped either way — one malformed\n * file must not take the others with it — and logged; this is how a caller\n * reports them anywhere else.\n */\n onSkip?: (problem: string) => void;\n}\n\n/**\n * Load the custom functions in a directory, as values.\n *\n * Each file default-exports a Hono app — `defineFunction(...)` — or a factory\n * returning one. The filename without its extension is the record key, which\n * is the URL segment it is served at: `functions/send-invoice.ts` →\n * `{ \"send-invoice\": app }`. Only the top level is read.\n *\n * Never throws for a bad file: it is skipped, logged, and passed to\n * `options.onSkip`.\n *\n * @example\n * ```ts\n * const functions = await loadFunctions(path.resolve(\"functions\"));\n * const backend = await createRebaseBackend({ database, functions, provision: true });\n * ```\n */\nexport async function loadFunctions(directory: string, options: LoadDirectoryOptions = {}): Promise<FunctionsRecord> {\n const { functions, problems } = await readFunctionsDirectory(directory, options.importModule);\n if (options.onSkip) for (const problem of problems) options.onSkip(problem);\n return functions;\n}\n\n/** A loaded function, by name. The shape {@link createFunctionRoutes} mounts. */\nexport interface LoadedFunction {\n /** Endpoint name derived from filename (e.g., \"send-invoice\") */\n name: string;\n /** The Hono sub-app to mount */\n app: FunctionApp;\n}\n\n/** What a directory of function files produced: what mounted, and what did not. */\nexport interface LoadedFunctions {\n /** The functions that will be served. */\n functions: LoadedFunction[];\n /**\n * One entry per file the loader saw and did **not** mount, each already\n * phrased as `<name> (<reason>)`.\n */\n problems: string[];\n}\n\n/** A record of functions as the list {@link createFunctionRoutes} mounts, in record order. */\nexport function functionList(functions: FunctionsRecord): LoadedFunction[] {\n return Object.entries(functions).map(([name, app]) => ({ name, app }));\n}\n\n/**\n * Extensions the bundler compiles (or a developer might reasonably write) that\n * this loader cannot import. `rebase build` globs `functions/**\\/*.ts`, so the\n * build's idea of \"a function file\" is strictly wider than the runtime's — the\n * worst direction for a mismatch to go. Anything listed here is reported as a\n * problem instead of vanishing; non-code files (`.md`, `.json`, `.txt`) stay\n * silent, because a README next to your functions is not a mistake.\n */\nconst UNSUPPORTED_CODE_EXTENSIONS = [\".mts\", \".cts\", \".tsx\", \".jsx\", \".mjs\", \".cjs\"];\n\n/**\n * @deprecated Use `loadFunctions` instead, which returns the functions as a\n * record keyed by name. Will be removed in a future release.\n */\nexport async function loadFunctionsFromDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedFunction[]> {\n warnDeprecated(\n \"server.loadFunctionsFromDirectory\",\n \"`loadFunctionsFromDirectory` is deprecated; use `loadFunctions(dir)`, which returns `{ [name]: app }`.\"\n );\n return functionList((await readFunctionsDirectory(directory, importModule)).functions);\n}\n\n/**\n * @deprecated Use `loadFunctions` instead, with `onSkip` to collect the files\n * it skipped. Will be removed in a future release.\n */\nexport async function loadFunctionsWithDiagnostics(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedFunctions> {\n warnDeprecated(\n \"server.loadFunctionsWithDiagnostics\",\n \"`loadFunctionsWithDiagnostics` is deprecated; use `loadFunctions(dir, { onSkip })`, which returns `{ [name]: app }`.\"\n );\n const { functions, problems } = await readFunctionsDirectory(directory, importModule);\n return { functions: functionList(functions), problems };\n}\n\n/**\n * Read a directory of function files: what loaded, keyed by name, and one\n * `<file> (<reason>)` per file that did not.\n *\n * Never throws: a single malformed function must not crash server boot. The\n * backend mounts the router regardless and surfaces the count of skipped files\n * on the listing endpoint.\n */\nexport async function readFunctionsDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<{ functions: FunctionsRecord; problems: string[] }> {\n const functions: FunctionsRecord = {};\n // Aggregate problem files so a broken function surfaces as one loud\n // summary line, not just a warning buried per-file. We still don't throw:\n // a single malformed function must not crash server boot.\n const problems: string[] = [];\n\n if (!fs.existsSync(directory)) {\n return { functions, problems };\n }\n\n // `withFileTypes` so a directory entry is a *reported* skip rather than a\n // filter miss. `readdirSync(dir)` returned bare names, and a subdirectory\n // simply failed the `.ts`/`.js` test — so `functions/admin/users.ts` was\n // compiled by `rebase build`, shipped in the bundle, and then dropped at\n // boot without a single log line.\n const entries = fs.readdirSync(directory, { withFileTypes: true });\n for (const entry of entries) {\n const file = entry.name;\n\n if (entry.isDirectory()) {\n // Dot-directories are tooling (`.git`, `.turbo`), not intent.\n if (file.startsWith(\".\") || file === \"node_modules\") continue;\n logger.warn(\n `[functions] ${file}/: subdirectory ignored. Functions are loaded from the top level of ` +\n `${directory} only, so nothing under ${file}/ is served. Move the file up (or flatten the ` +\n \"name: `admin/users.ts` → `admin-users.ts`).\"\n );\n problems.push(`${file}/ (subdirectory — functions are not loaded recursively)`);\n continue;\n }\n\n const extension = path.extname(file);\n if (\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n UNSUPPORTED_CODE_EXTENSIONS.includes(extension)\n ) {\n logger.warn(\n `[functions] ${file}: ${extension} files are not loaded. Rename it to .ts (or .js) to serve it.`\n );\n problems.push(`${file} (unsupported extension ${extension})`);\n continue;\n }\n\n if (\n (file.endsWith(\".ts\") || file.endsWith(\".js\")) &&\n // Dotfiles: notably macOS bsdtar AppleDouble sidecars (`._foo.ts`),\n // binary blobs that cannot be imported.\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n !file.endsWith(\".d.ts\") &&\n file !== \"index.ts\" &&\n file !== \"index.js\"\n ) {\n const filePath = path.join(directory, file);\n try {\n const fileUrl = pathToFileURL(filePath).href;\n\n const mod = await importModule(fileUrl);\n\n const exported = mod.default;\n\n if (!exported) {\n logger.warn(`[functions] ${file}: no default export. Skipping.`);\n problems.push(`${file} (no default export)`);\n continue;\n }\n\n // Accept a Hono instance — use duck-typing to handle different\n // Hono versions which may not share the same prototype.\n const app = isHonoLike(exported)\n ? exported as FunctionApp\n // Also accept a factory function that returns a Hono instance\n : typeof exported === \"function\" ? exported() : undefined;\n if (isHonoLike(app)) {\n const name = path.basename(file, path.extname(file));\n // `send.ts` and `send.js` side by side name one URL. A\n // record holds one app per name, so the second is reported\n // rather than silently replacing the first.\n if (Object.hasOwn(functions, name)) {\n logger.warn(`[functions] ${file}: another file is already served as '${name}'. Skipping.`);\n problems.push(`${file} (another file is already served as '${name}')`);\n continue;\n }\n functions[name] = app as FunctionApp;\n logger.debug(`⚡ Loaded function route: ${name}`);\n continue;\n }\n\n // Provide actionable diagnostics\n const exportType = typeof exported;\n const keys = exported && typeof exported === \"object\"\n ? Object.getOwnPropertyNames(Object.getPrototypeOf(exported)).slice(0, 10).join(\", \")\n : \"N/A\";\n logger.warn(\n `[functions] ${file}: default export is not a Hono app or factory. Skipping.\\n` +\n ` export type: ${exportType}${exported?.constructor?.name ? ` (${exported.constructor.name})` : \"\"}\\n` +\n ` prototype methods: ${keys}\\n` +\n \" Hint: ensure the function exports a Hono app created with the same hono version as the server.\\n\" +\n \" Author with `defineFunction(...)` from @rebasepro/server for a typed, checked contract.\\n\" +\n \" The loader checks for .fetch() and .routes — any Hono-compatible app will work.\"\n );\n problems.push(`${file} (not a Hono app or factory)`);\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : String(err);\n logger.error(`[functions] Failed to load ${file}: ${message}`);\n problems.push(describeLoadFailure(file, err));\n }\n }\n }\n\n if (problems.length > 0) {\n // Advice keyed to what actually went wrong. The old line said\n // \"author them with `defineFunction(...)`\" for every failure — which is\n // the fix for a wrong export shape and no help at all for the commonest\n // one: a file that threw while being imported, usually a module-scope\n // `process.env.X` read that came back undefined. Told to reach for\n // `defineFunction`, an author rewrites a file whose export was already\n // correct and gets the same failure.\n const advice: string[] = [];\n if (problems.some(p => p.includes(\"(syntax error\"))) {\n advice.push(SYNTAX_ERROR_ADVICE);\n }\n if (problems.some(p => p.includes(\"(threw:\"))) {\n advice.push(\n \" A file that threw ran at import time. Read configuration *inside* a handler \" +\n \"(`requireEnv(c, \\\"STRIPE_KEY\\\")`, or a lazily-built client) rather than at module \" +\n \"scope, where one undefined variable takes the whole file down before any route exists.\"\n );\n }\n if (problems.some(p => p.includes(\"(no default export)\"))) {\n advice.push(\n \" A file with no default export exports nothing the loader can mount. \" +\n \"`export default defineFunction((app) => { … })`.\"\n );\n }\n if (problems.some(p => p.includes(\"(not a Hono app or factory)\"))) {\n advice.push(\n \" A default export the loader did not recognise is usually two copies of hono. \" +\n \"Author with `defineFunction(...)` from @rebasepro/server/functions, which uses the \" +\n \"server's own copy.\"\n );\n }\n logger.warn(\n `[functions] ${problems.length} function file(s) were skipped and will NOT be served:\\n` +\n problems.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\" +\n advice.join(\"\\n\")\n );\n }\n\n return { functions, problems };\n}\n\n/**\n * Duck-type check for Hono apps.\n * We avoid `instanceof Hono` because different Hono versions\n * installed in the user's project vs. our dependencies will\n * not share the same prototype, causing false negatives.\n */\nfunction isHonoLike(obj: unknown): boolean {\n if (!obj || typeof obj !== \"object\") return false;\n // Hono instances always have .fetch() and .routes\n const record = obj as Record<string, unknown>;\n return (\n typeof record.fetch === \"function\" &&\n Array.isArray(record.routes)\n );\n}\n"],"mappings":"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"}
@@ -243,10 +243,11 @@ function createFunctionRoutes(functions, problems = [], mountPath = "/functions"
243
243
  const router = new Hono();
244
244
  const skipped = problems.length;
245
245
  router.get("/", requireAuth, (c) => {
246
+ const at = c.get("mountPath") ?? mountPath;
246
247
  return c.json({
247
248
  functions: functions.map((fn) => ({
248
249
  name: fn.name,
249
- endpoint: `${mountPath}/${fn.name}`
250
+ endpoint: `${at}/${fn.name}`
250
251
  })),
251
252
  ...skipped > 0 && {
252
253
  skipped,
@@ -265,10 +266,10 @@ function createFunctionRoutes(functions, problems = [], mountPath = "/functions"
265
266
  const refuse = (message) => {
266
267
  throw new ApiError(404, "FUNCTION_NOT_FOUND", message, { function: name }, true);
267
268
  };
268
- if (!name) refuse(`No function in the request path. Expected ${mountPath}/<function>.`);
269
+ if (!name) refuse(`No function in the request path. Expected ${c.get("mountPath") ?? mountPath}/<function>.`);
269
270
  if (mounted.has(name)) refuse(`The function '${name}' is served, and has no ${c.req.method} route at '/${rest}'. The path after the function name is routed by the function's own Hono app.`);
270
271
  const file = failedToLoad.get(name);
271
- if (file) refuse(`The function '${name}' is not served: '${file}' failed to load. The server log records why, at boot.`);
272
+ if (file && identified) refuse(`The function '${name}' is not served: '${file}' failed to load. The server log records why, at boot.`);
272
273
  return refuse(`No function named '${name}' on this backend.` + (identified ? mounted.size > 0 ? ` This backend serves: ${[...mounted].sort().join(", ")}.` : " This backend serves no functions." : "") + (skipped > 0 && identified ? ` ${skipped} function file(s) failed to load and are not served — see the server log.` : ""));
273
274
  });
274
275
  return router;
@@ -276,4 +277,4 @@ function createFunctionRoutes(functions, problems = [], mountPath = "/functions"
276
277
  //#endregion
277
278
  export { getDriver as a, getUser as c, identityResolved as d, isAdmin as f, getApiKey as i, getUserId as l, requireDriver as m, function_routes_exports as n, getRequestId as o, isAuthenticated as p, requireRole as r, getRoles as s, createFunctionRoutes as t, hasRole as u };
278
279
 
279
- //# sourceMappingURL=function-routes-CaNG4waN.js.map
280
+ //# sourceMappingURL=function-routes-j9TwpvQe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"function-routes-j9TwpvQe.js","names":[],"sources":["../src/auth/admin-roles.ts","../src/functions/context.ts","../src/functions/guards.ts","../src/functions/function-routes.ts"],"sourcesContent":["/**\n * The admin role and scope matching, for code that may not import\n * `@rebasepro/types`.\n *\n * The custom-functions surface (`@rebasepro/server/functions`) must bundle for\n * a runtime with no Node built-ins and imports nothing but `hono`, so its\n * guards cannot reach the canonical definitions in `@rebasepro/types`\n * (`ADMIN_ROLE`, `hasAdminRole`, `scopeGrants`). These are the same rules,\n * restated, and `test/admin-roles.test.ts` holds them equal.\n *\n * Everything else imports the `@rebasepro/types` versions.\n */\n\n/** The one built-in role. It holds every scope. */\nexport const ADMIN_ROLE_NAME = \"admin\";\n\n/** Does this list of roles include the admin role? */\nexport function holdsAdminRole(roles: readonly string[] | null | undefined): boolean {\n return !!roles?.includes(ADMIN_ROLE_NAME);\n}\n\n/**\n * Does a set of held scopes grant `scope`, optionally on one `target`? The\n * unqualified grant covers every target; `scope:target` covers its own.\n */\nexport function heldScopesGrant(held: readonly string[], scope: string, target?: string): boolean {\n for (const entry of held) {\n if (entry === scope) return true;\n if (target !== undefined && entry === `${scope}:${target}`) return true;\n }\n return false;\n}\n","/**\n * Reading the request context from inside a custom function.\n *\n * The functions router resolves the caller's identity before any handler runs\n * and leaves the result on the Hono context. Getting it back out used to be the\n * user's problem, and the shape made that worse than it sounds: `HonoEnv`\n * types `user` as `AuthResult`, a union that includes `boolean`, `null` and an\n * index signature, because the same slot is filled by four different middlewares\n * — JWT, service key, API key, and a user-supplied validator that may return\n * `true`. Every example in the documentation therefore opened with\n *\n * const user = c.get(\"user\") as { uid: string; roles?: string[] } | undefined;\n *\n * and an assertion in a security-relevant position is exactly the kind of line\n * that gets copied once and then never re-examined. It is also wrong in one\n * case that occurs in practice: a custom validator returning `true` stores\n * `{ uid: \"default\", roles: [] }`, which the assertion above types as having a\n * `uid` — true here, but nothing checks it.\n *\n * These accessors do the narrowing once, in the framework, where it can be\n * tested. They are also **runtime-neutral by construction** — no crypto, no\n * token parsing, no I/O, nothing but property reads on an object another\n * middleware already populated. That is what lets them live in\n * `@rebasepro/server/functions` and run unchanged on a host that has no Node\n * built-ins.\n *\n * @module\n */\nimport type { Context } from \"hono\";\nimport type { DataDriver } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { ApiKeyMasked } from \"../auth/api-keys/api-key-types\";\nimport { heldScopesGrant, holdsAdminRole } from \"../auth/admin-roles\";\n\n/**\n * The caller, as a custom function sees them.\n *\n * A narrowed view of whatever the auth middleware resolved: `uid` and `roles`\n * are guaranteed, and the index signature keeps any extra claims the token or\n * the adapter carried (`email`, `org_id`, anything a custom validator added)\n * reachable without a cast.\n */\nexport interface FunctionUser {\n /** Stable id of the caller. `\"service\"` for service-key and API-key callers. */\n uid: string;\n /** Roles as resolved for this request. Never `undefined` — an empty array instead. */\n roles: string[];\n /** Present when the identity carried one. Not every auth method does. */\n email?: string;\n /** Any further claim the token, adapter or validator supplied. */\n [claim: string]: unknown;\n}\n\n/** Anything with a Hono-style `.get`, so these work on any `Context` shape. */\ntype CtxLike = Context<HonoEnv> | Context;\n\nfunction read<K extends keyof HonoEnv[\"Variables\"]>(\n c: CtxLike,\n key: K\n): HonoEnv[\"Variables\"][K] | undefined {\n // `c.get` is typed against the app's own Env, which a handler mounted\n // through `app.route()` may have declared more loosely. The cast is\n // confined to this one function rather than repeated at every call site.\n return (c as Context<HonoEnv>).get(key);\n}\n\n/**\n * The authenticated caller, or `undefined` for an anonymous request.\n *\n * **`undefined` is not a permission decision.** The functions router mounts its\n * auth middleware with `requireAuth: false` on purpose — a webhook receiver has\n * no token to send — so an anonymous caller reaches the handler and reads\n * `undefined` here while the handler runs on regardless. Use {@link requireAuth}\n * (or a `!user` branch that returns 401) to make it a decision.\n *\n * A caller who presented a *bad* token never gets this far: both auth\n * middlewares reject an unverifiable token with 401 before the router is\n * reached, precisely so an expired session cannot be silently downgraded to an\n * anonymous one.\n */\nexport function getUser(c: CtxLike): FunctionUser | undefined {\n const raw = read(c, \"user\");\n if (!raw || typeof raw !== \"object\") return undefined;\n\n const record = raw as Record<string, unknown>;\n const uid = typeof record.uid === \"string\" ? record.uid : undefined;\n if (uid === undefined) return undefined;\n\n const roles = Array.isArray(record.roles)\n ? record.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n\n return { ...record,\n uid,\n roles } as FunctionUser;\n}\n\n/** The caller's id, or `undefined` when nobody is signed in. */\nexport function getUserId(c: CtxLike): string | undefined {\n return getUser(c)?.uid;\n}\n\n/** The caller's roles. Empty for an anonymous request — never `undefined`. */\nexport function getRoles(c: CtxLike): string[] {\n return getUser(c)?.roles ?? [];\n}\n\n/**\n * Whether the caller holds **any** of the named roles.\n *\n * Any rather than all, because that is what a route guard means by a list of\n * roles; require several by calling this more than once.\n */\nexport function hasRole(c: CtxLike, ...roles: string[]): boolean {\n if (roles.length === 0) return false;\n const held = new Set(getRoles(c));\n return roles.some(role => held.has(role));\n}\n\n/** Whether the caller holds the `admin` role, which holds every scope. */\nexport function isAdmin(c: CtxLike): boolean {\n return holdsAdminRole(getRoles(c));\n}\n\n/**\n * Everything the caller may do, as `resource:action[:target]` scope strings —\n * the data plane, the app's own `auth.scopes`, and whatever their roles (or\n * their API key) hold. Empty for an anonymous request.\n *\n * Resolved by the framework before the handler runs; `undefined` only when no\n * Rebase auth middleware ran (see {@link identityResolved}).\n */\nexport function getScopes(c: CtxLike): string[] | undefined {\n return read(c, \"scopes\");\n}\n\n/**\n * Whether the caller holds `scope`, on `target` when one is given.\n *\n * For an app scope declared under `auth.scopes` — `project:deploy` — a\n * signed-in person always holds it, so this narrows only API keys and tokens.\n * Whether the *person* may deploy *this* project is still the handler's to\n * decide.\n */\nexport function hasScope(c: CtxLike, scope: string, target?: string): boolean {\n return heldScopesGrant(getScopes(c) ?? [], scope, target);\n}\n\n/** Whether the request carries an identity at all. */\nexport function isAuthenticated(c: CtxLike): boolean {\n return getUser(c) !== undefined;\n}\n\n/**\n * The request-scoped data driver: reads and writes run as **the caller**, with\n * your row-level security policies evaluated against their identity.\n *\n * This is the accessor to reach for when a function serves user-facing data.\n * `rebase.dataAsAdmin` is the other one, and it is not the same thing — it runs\n * as `{ uid: \"service\", roles: [\"admin\"] }` for every caller alike, which is\n * correct for trusted background work and wrong for a request.\n *\n * `undefined` only when no Rebase auth middleware ran (see\n * {@link identityResolved}); inside a function mounted by the framework it is\n * always present, anonymous requests included — they get an anon-scoped driver\n * so policies still have an identity to evaluate.\n */\nexport function getDriver(c: CtxLike): DataDriver | undefined {\n return read(c, \"driver\");\n}\n\n/**\n * {@link getDriver}, but throws instead of handing back `undefined`.\n *\n * For the common case where a handler cannot proceed without it and would\n * otherwise write `c.get(\"driver\")!` — an assertion that turns a wiring problem\n * into `Cannot read properties of undefined (reading 'fetchCollection')` twenty\n * lines away from the cause.\n */\nexport function requireDriver(c: CtxLike): DataDriver {\n const driver = getDriver(c);\n if (!driver) {\n throw new Error(\n \"No request-scoped driver on this context. A Rebase auth middleware \" +\n \"populates it before any custom function runs, so this means the handler \" +\n \"was mounted outside the functions router — e.g. added to your own Hono \" +\n \"app directly. Mount it from the functions directory, or use \" +\n \"`rebase.dataAsAdmin` if the work is genuinely service-scoped.\"\n );\n }\n return driver;\n}\n\n/**\n * The API key this request authenticated with, masked, or `undefined` when it\n * did not use one.\n *\n * Useful for attribution and for per-key behaviour. The permission check itself\n * has already happened — reaching a handler means the key was allowed to.\n */\nexport function getApiKey(c: CtxLike): ApiKeyMasked | undefined {\n return read(c, \"apiKey\");\n}\n\n/**\n * The correlation id for this request — generated, or taken from an inbound\n * `X-Request-ID`.\n *\n * Log it. It is the only thing that ties a line written inside a function to\n * the framework's own lines for the same request.\n */\nexport function getRequestId(c: CtxLike): string | undefined {\n return read(c, \"requestId\");\n}\n\n/**\n * The signal aborted when this request outlasts the functions timeout and is\n * answered `504 FUNCTION_TIMEOUT`. Pass it to `fetch` and anything else that\n * takes one, so the work stops with the request instead of running on after\n * the caller was told it failed.\n *\n * Never aborts when the timeout is off, or outside the functions router.\n */\nexport function getSignal(c: CtxLike): AbortSignal {\n return read(c, \"functionSignal\") ?? new AbortController().signal;\n}\n\n/**\n * Whether a Rebase auth middleware has run on this request.\n *\n * Both middlewares populate `driver` for *every* outcome, anonymous included,\n * and populate `user` whenever there is one. So \"neither is set\" does not mean\n * \"anonymous\" — it means nothing resolved the identity, and treating that as\n * anonymous is the dangerous reading. The guards use this to tell a genuinely\n * anonymous caller (401) from a misconfigured mount (500), because answering\n * 401 to the second sends whoever is debugging it to look at the token.\n */\nexport function identityResolved(c: CtxLike): boolean {\n return read(c, \"user\") !== undefined || read(c, \"driver\") !== undefined;\n}\n","/**\n * Route guards for custom functions.\n *\n * These decide access from the identity the platform already resolved. They do\n * **not** verify tokens, and that division is the point rather than a\n * limitation:\n *\n * - Verifying a token needs a signing key, constant-time comparison and a\n * revocation lookup. That is host work, it belongs to the process that holds\n * the secret, and it is the part of the stack that cannot be made\n * runtime-neutral without rewriting it against WebCrypto.\n * - Deciding whether *this* caller may call *this* route is application work.\n * It needs nothing but the resolved identity, so it costs nothing to make it\n * portable — and it is the half that lives in user code.\n *\n * Splitting there is what lets a function file compile and run unchanged on a\n * host with no Node built-ins, and it is why these live in\n * `@rebasepro/server/functions` while `verifyAccessToken` does not.\n *\n * **Inside the functions router these are equivalent to the guards exported\n * from the package root.** Both auth middlewares resolve the identity before\n * any handler runs: a valid credential populates `user`, an invalid one is\n * rejected with 401 by the middleware itself, and a missing one leaves `user`\n * unset. So the root `requireAuth`'s token-parsing branch is unreachable from a\n * function, and removing it changes no outcome. The one difference is a handler\n * mounted **outside** the framework's router, where no middleware ran: the root\n * guard would parse the `Authorization` header itself, and these refuse the\n * request with a 500 that names the wiring problem. Fail-closed, and legible.\n *\n * @module\n */\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { getUser, isAdmin, getRoles, identityResolved, hasScope } from \"./context\";\n\n/**\n * The answer to \"a guard ran, but no middleware had resolved anything\".\n *\n * Deliberately a 500 and not a 401. A 401 tells the caller their credential is\n * the problem, and here the caller's credential was never looked at — sending\n * them to check their token is sending them to the one place the answer is not.\n */\nfunction unresolvedIdentity(): { error: { message: string; code: string } } {\n return {\n error: {\n message:\n \"This route's identity was never resolved: no Rebase auth middleware ran \" +\n \"before the guard. A function loaded from the functions directory always \" +\n \"has one. This usually means the Hono app was mounted onto your own \" +\n \"server directly, bypassing the functions router.\",\n code: \"AUTH_MIDDLEWARE_MISSING\"\n }\n };\n}\n\n/**\n * Reject anonymous callers with 401.\n *\n * Put it in the route's own middleware slot rather than `app.use(\"/*\", …)`:\n * `use()` covers only the routes declared *below* it, so a route appended later\n * — by you, months from now, at the bottom of the file — is silently\n * unprotected. The per-route form cannot drift that way.\n *\n * @example\n * ```ts\n * app.post(\"/\", requireAuth, async (c) => {\n * const user = getUser(c)!; // guaranteed by the guard\n * return c.json({ uid: user.uid });\n * });\n * ```\n */\nexport const requireAuth: MiddlewareHandler<HonoEnv> = async (c, next) => {\n if (getUser(c)) return next();\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n};\n\n/**\n * Reject callers without the `admin` role with 403.\n *\n * Must come **after** {@link requireAuth}: on its own it answers 401 for an\n * anonymous caller, which is right, but pairing them keeps the two failures\n * distinguishable — 401 \"who are you\", 403 \"not you\".\n *\n * Prefer {@link requireScope} for anything an app declares a scope for: a\n * scope can be granted to a narrower role and to a key, and `admin` cannot.\n */\nexport const requireAdmin: MiddlewareHandler<HonoEnv> = async (c, next) => {\n const user = getUser(c);\n if (!user) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n\n if (!isAdmin(c)) {\n return c.json({\n error: {\n message: \"Admin privileges required for this operation\",\n code: \"FORBIDDEN\"\n }\n }, 403);\n }\n\n return next();\n};\n\n/**\n * Reject callers holding none of the named roles with 403.\n *\n * Any of them, not all — require several by chaining the guard twice. Naming no\n * role at all is a programming error and throws at module load rather than at\n * request time, because `requireRole()` with an empty list would otherwise read\n * as a guard while admitting everyone.\n *\n * @example\n * ```ts\n * app.post(\"/publish\", requireAuth, requireRole(\"editor\", \"admin\"), handler);\n * ```\n */\nexport function requireRole(...roles: string[]): MiddlewareHandler<HonoEnv> {\n if (roles.length === 0) {\n throw new Error(\n \"requireRole() needs at least one role. An empty list would admit every \" +\n \"signed-in caller while reading as a restriction.\"\n );\n }\n\n const allowed = new Set(roles);\n return async (c, next) => {\n const user = getUser(c);\n if (!user) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n\n if (!getRoles(c).some(role => allowed.has(role))) {\n return c.json({\n error: {\n message: `This operation requires one of these roles: ${roles.join(\", \")}`,\n code: \"FORBIDDEN\"\n }\n }, 403);\n }\n\n return next();\n };\n}\n\n/**\n * Reject callers who do not hold `scope` with 403.\n *\n * `scope` is a built-in scope or one the app declares under `auth.scopes` on\n * the users collection. `target` narrows it to one resource — a key holding\n * `project:deploy:p1` passes `requireScope(\"project:deploy\", c => c.req.param(\"project\"))`\n * for `p1` only — and may be read from the request.\n *\n * A signed-in person holds every app scope, so for a person this is no\n * authorization at all: it narrows API keys and tokens. Decide whether the\n * person may act in the handler, as you would without it.\n *\n * @example\n * ```ts\n * app.post(\"/deploy/:project\", requireAuth, requireScope(\"project:deploy\", c => c.req.param(\"project\")), handler);\n * ```\n */\nexport function requireScope(\n scope: string,\n target?: string | ((c: Context<HonoEnv>) => string | undefined)\n): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n if (!getUser(c)) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n const resolvedTarget = typeof target === \"function\" ? target(c) : target;\n if (!hasScope(c, scope, resolvedTarget)) {\n const wanted = resolvedTarget !== undefined ? `${scope}:${resolvedTarget}` : scope;\n return c.json({\n error: {\n message: `This credential does not hold the \"${wanted}\" scope.`,\n code: \"SCOPE_MISSING\",\n details: { requiredScope: wanted }\n }\n }, 403);\n }\n return next();\n };\n}\n","import { Hono } from \"hono\";\nimport { HonoEnv } from \"../api/types\";\nimport { ApiError } from \"../api/errors\";\nimport { LoadedFunction } from \"./function-loader\";\nimport { requireAuth } from \"./guards\";\n\n/** The file a loader problem names, without its extension: `broken.ts (threw: …)` → `broken`. */\nfunction problemName(problem: string): string {\n return problem.split(\" \")[0].replace(/\\/$/, \"\").replace(/\\.[cm]?[jt]s$/, \"\");\n}\n\n/**\n * Mount all loaded function routes under a single Hono router.\n *\n * Each function is mounted at `/<function-name>`, preserving\n * whatever HTTP methods and middleware the Hono sub-app defines.\n *\n * @param functions What loaded. May be empty — the router still mounts, so\n * \"no functions are served\" answers 200 with an empty list instead of 404.\n * @param problems The files the loader saw and could not serve, as\n * `\"<file> (<reason>)\"`. The listing reports a count and a pointer to the log,\n * not the reasons: those carry import errors, and the listing is one guard\n * away from anyone. The unmatched-route handler uses the *names* to answer the\n * one question a 404 on a function nobody can find should answer — \"there is a\n * file for this and it did not load\" — which is the difference between a typo\n * and a broken deploy, and the loader was the only thing that knew.\n */\nexport function createFunctionRoutes(\n functions: LoadedFunction[],\n problems: string[] = [],\n /**\n * Where this router is mounted, so the listing can report a path a caller\n * can actually request. It used to hardcode `/functions/<name>`, which is\n * wrong under every `basePath` including the default `/api`. The\n * `mountPath` a functions group records for the request wins over it.\n */\n mountPath = \"/functions\"\n): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n const skipped = problems.length;\n\n // Listing endpoint: GET / → list available functions.\n //\n // Functions themselves stay anonymous-callable by default — a webhook\n // receiver has to be — but the index of them does not: it is an inventory\n // of every custom endpoint, for whoever asks. `requireAuth` admits any\n // resolved identity (a signed-in user, an API key, the service key), so\n // `rebase doctor` and `rebase cloud debug` keep their answer; the latter\n // already reads a 401 here as \"mounted\".\n router.get(\"/\", requireAuth, (c) => {\n const at = c.get(\"mountPath\") ?? mountPath;\n return c.json({\n functions: functions.map((fn) => ({\n name: fn.name,\n endpoint: `${at}/${fn.name}`\n })),\n ...(skipped > 0 && {\n skipped,\n note: `${skipped} function file(s) failed to load and are NOT served — see the server log for the reason.`\n })\n });\n });\n\n for (const fn of functions) {\n router.route(`/${fn.name}`, fn.app);\n }\n\n // A name that matches nothing, answered in the envelope.\n //\n // Registered last so every real route wins it. Without it, a typo'd\n // function name — the single most likely 404 a developer meets on this\n // surface — fell through to Hono's default `404 Not Found` as `text/plain`,\n // and through the SDK arrived as `RebaseApiError { code: undefined }`, so\n // the documented `e.code === \"FUNCTION_NOT_FOUND\"` branch never ran.\n //\n // What the message may say depends on who is asking. The mounted names are\n // an inventory of every custom endpoint, which is exactly what the listing\n // above requires an identity to see — so an anonymous caller is told their\n // name is unknown and nothing more, and a resolved caller gets the list\n // that turns the 404 into a fix.\n const mounted = new Set(functions.map(fn => fn.name));\n const failedToLoad = new Map(problems.map(p => [problemName(p), p.split(\" \")[0]]));\n\n router.all(\"/:name{.*}\", (c): never => {\n const requested = (c.req.param(\"name\") ?? \"\").split(\"/\").filter(Boolean);\n const name = requested[0] ?? \"\";\n const rest = requested.slice(1).join(\"/\");\n const identified = Boolean(c.get(\"user\"));\n\n const refuse = (message: string): never => {\n throw new ApiError(404, \"FUNCTION_NOT_FOUND\", message, { function: name }, true);\n };\n\n if (!name) {\n refuse(`No function in the request path. Expected ${c.get(\"mountPath\") ?? mountPath}/<function>.`);\n }\n if (mounted.has(name)) {\n refuse(\n `The function '${name}' is served, and has no ${c.req.method} route at '/${rest}'. ` +\n \"The path after the function name is routed by the function's own Hono app.\"\n );\n }\n // The file name is the same inventory, a file at a time — and it\n // names a source file besides — so it is told to an identified caller\n // only. An anonymous one gets exactly the answer a name that matches\n // nothing gets.\n const file = failedToLoad.get(name);\n if (file && identified) {\n refuse(\n `The function '${name}' is not served: '${file}' failed to load. ` +\n \"The server log records why, at boot.\"\n );\n }\n return refuse(\n `No function named '${name}' on this backend.` +\n (identified\n ? (mounted.size > 0\n ? ` This backend serves: ${[...mounted].sort().join(\", \")}.`\n : \" This backend serves no functions.\")\n : \"\") +\n (skipped > 0 && identified\n ? ` ${skipped} function file(s) failed to load and are not served — see the server log.`\n : \"\")\n );\n });\n\n return router;\n}\n"],"mappings":"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"}
@@ -138,6 +138,15 @@ export declare function getApiKey(c: CtxLike): ApiKeyMasked | undefined;
138
138
  * the framework's own lines for the same request.
139
139
  */
140
140
  export declare function getRequestId(c: CtxLike): string | undefined;
141
+ /**
142
+ * The signal aborted when this request outlasts the functions timeout and is
143
+ * answered `504 FUNCTION_TIMEOUT`. Pass it to `fetch` and anything else that
144
+ * takes one, so the work stops with the request instead of running on after
145
+ * the caller was told it failed.
146
+ *
147
+ * Never aborts when the timeout is off, or outside the functions router.
148
+ */
149
+ export declare function getSignal(c: CtxLike): AbortSignal;
141
150
  /**
142
151
  * Whether a Rebase auth middleware has run on this request.
143
152
  *
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import type { RebaseServerClient } from "@rebasepro/types";
2
+ import type { RebaseServerClient, RegisteredDatabase } from "@rebasepro/types";
3
3
  import type { HonoEnv } from "../api/types.js";
4
4
  /**
5
5
  * Typed context injected into a function authored with {@link defineFunction}.
@@ -10,10 +10,11 @@ import type { HonoEnv } from "../api/types.js";
10
10
  * on the Hono context via {@link HonoEnv} — read them with `c.get("user")`
11
11
  * / `c.var.driver` inside a handler.
12
12
  */
13
- export interface RebaseFunctionContext {
13
+ export interface RebaseFunctionContext<DB = RegisteredDatabase> {
14
14
  /**
15
- * The server-side Rebase singleton (`dataAsAdmin`, `auth`, `storage`,
16
- * `email`, `sql`).
15
+ * The server-side Rebase client (`dataAsAdmin`, `auth`, `storage`,
16
+ * `email`, `sql`) of the backend serving the request — the same object
17
+ * the imported `rebase` is there.
17
18
  *
18
19
  * `rebase.dataAsAdmin` runs as the service identity
19
20
  * `{ uid: "service", roles: ["admin"] }` — **admin-scoped, not an RLS
@@ -36,8 +37,11 @@ export interface RebaseFunctionContext {
36
37
  * (`c.var.driver`), which carries the caller's identity. (`rebase.data` no
37
38
  * longer exists on this type — `dataAsAdmin` is the only name for the
38
39
  * admin-scoped accessor.)
40
+ *
41
+ * Typed from the `Database` the project registered on `Register`, or from
42
+ * the type argument of `defineFunction<Database>(…)`; untyped otherwise.
39
43
  */
40
- rebase: RebaseServerClient;
44
+ rebase: RebaseServerClient<DB>;
41
45
  }
42
46
  /**
43
47
  * Typed authoring contract for a custom backend function.
@@ -74,4 +78,4 @@ export interface RebaseFunctionContext {
74
78
  * own `Hono<HonoEnv>` app to use instead.
75
79
  * @returns The Hono app to default-export from the function file.
76
80
  */
77
- export declare function defineFunction(definition: (app: Hono<HonoEnv>, ctx: RebaseFunctionContext) => void | Hono<HonoEnv>): Hono<HonoEnv>;
81
+ export declare function defineFunction<DB = RegisteredDatabase>(definition: (app: Hono<HonoEnv>, ctx: RebaseFunctionContext<DB>) => void | Hono<HonoEnv>): Hono<HonoEnv>;
@@ -1,10 +1,66 @@
1
1
  import { Hono } from "hono";
2
+ import type { HonoEnv } from "../api/types.js";
2
3
  import { type ModuleImporter } from "../utils/dynamic-import.js";
4
+ /**
5
+ * One custom function: a Hono app, served at `<functions mount>/<name>`.
6
+ *
7
+ * What `defineFunction(...)` returns, and what a function file default-exports:
8
+ * a Hono app over {@link HonoEnv}, the environment a function runs in — build
9
+ * one by hand as `new Hono<HonoEnv>()`. Any Hono-compatible app works at
10
+ * runtime — the loader checks for `.fetch()` and `.routes` rather than an
11
+ * `instanceof`, because a project and the server can hold two copies of hono.
12
+ */
13
+ export type FunctionApp = Hono<HonoEnv>;
14
+ /**
15
+ * Functions as values: the URL segment each one answers at, to its app.
16
+ *
17
+ * `functions: { "send-invoice": sendInvoice }` serves `sendInvoice` at
18
+ * `/api/functions/send-invoice` — the same thing a file named
19
+ * `functions/send-invoice.ts` does. {@link loadFunctions} returns exactly this
20
+ * shape, so `functions: await loadFunctions(dir)` and `functionsDir: dir` serve
21
+ * the same routes.
22
+ */
23
+ export type FunctionsRecord = Record<string, FunctionApp>;
24
+ /** How a directory of modules is read. Shared by the function and cron loaders. */
25
+ export interface LoadDirectoryOptions {
26
+ /**
27
+ * How a module file is imported. Defaults to the runtime's own `import()`;
28
+ * a caller with its own loader hooks (tsx, a test runner's sandbox) passes
29
+ * `url => import(url)` so they apply.
30
+ */
31
+ importModule?: ModuleImporter;
32
+ /**
33
+ * Called once for every file the loader saw and did **not** load, as
34
+ * `<file> (<reason>)`. The files are skipped either way — one malformed
35
+ * file must not take the others with it — and logged; this is how a caller
36
+ * reports them anywhere else.
37
+ */
38
+ onSkip?: (problem: string) => void;
39
+ }
40
+ /**
41
+ * Load the custom functions in a directory, as values.
42
+ *
43
+ * Each file default-exports a Hono app — `defineFunction(...)` — or a factory
44
+ * returning one. The filename without its extension is the record key, which
45
+ * is the URL segment it is served at: `functions/send-invoice.ts` →
46
+ * `{ "send-invoice": app }`. Only the top level is read.
47
+ *
48
+ * Never throws for a bad file: it is skipped, logged, and passed to
49
+ * `options.onSkip`.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const functions = await loadFunctions(path.resolve("functions"));
54
+ * const backend = await createRebaseBackend({ database, functions, provision: true });
55
+ * ```
56
+ */
57
+ export declare function loadFunctions(directory: string, options?: LoadDirectoryOptions): Promise<FunctionsRecord>;
58
+ /** A loaded function, by name. The shape {@link createFunctionRoutes} mounts. */
3
59
  export interface LoadedFunction {
4
60
  /** Endpoint name derived from filename (e.g., "send-invoice") */
5
61
  name: string;
6
62
  /** The Hono sub-app to mount */
7
- app: Hono<import("hono").Env>;
63
+ app: FunctionApp;
8
64
  }
9
65
  /** What a directory of function files produced: what mounted, and what did not. */
10
66
  export interface LoadedFunctions {
@@ -12,30 +68,31 @@ export interface LoadedFunctions {
12
68
  functions: LoadedFunction[];
13
69
  /**
14
70
  * One entry per file the loader saw and did **not** mount, each already
15
- * phrased as `<name> (<reason>)`. Returned rather than only logged so the
16
- * running server can say what is missing — a boot log line is not reachable
17
- * from `GET /api/functions`.
71
+ * phrased as `<name> (<reason>)`.
18
72
  */
19
73
  problems: string[];
20
74
  }
75
+ /** A record of functions as the list {@link createFunctionRoutes} mounts, in record order. */
76
+ export declare function functionList(functions: FunctionsRecord): LoadedFunction[];
21
77
  /**
22
- * Auto-discover Hono route files from a directory.
23
- *
24
- * Each file should default-export a Hono app (or router).
25
- * The filename (without extension) becomes the mount path:
26
- * `functions/send-invoice.ts` → mounted at `/send-invoice`
27
- *
28
- * This mirrors how `loadCollectionsFromDirectory` works for collections.
29
- *
30
- * Returns only what loaded. Use {@link loadFunctionsWithDiagnostics} when the
31
- * caller also needs to report what did not.
78
+ * @deprecated Use `loadFunctions` instead, which returns the functions as a
79
+ * record keyed by name. Will be removed in a future release.
32
80
  */
33
81
  export declare function loadFunctionsFromDirectory(directory: string, importModule?: ModuleImporter): Promise<LoadedFunction[]>;
34
82
  /**
35
- * {@link loadFunctionsFromDirectory}, plus the list of files that were skipped.
83
+ * @deprecated Use `loadFunctions` instead, with `onSkip` to collect the files
84
+ * it skipped. Will be removed in a future release.
85
+ */
86
+ export declare function loadFunctionsWithDiagnostics(directory: string, importModule?: ModuleImporter): Promise<LoadedFunctions>;
87
+ /**
88
+ * Read a directory of function files: what loaded, keyed by name, and one
89
+ * `<file> (<reason>)` per file that did not.
36
90
  *
37
91
  * Never throws: a single malformed function must not crash server boot. The
38
- * caller decides what to do with `problems` — `init.ts` mounts the router
39
- * regardless and surfaces the count on the listing endpoint.
92
+ * backend mounts the router regardless and surfaces the count of skipped files
93
+ * on the listing endpoint.
40
94
  */
41
- export declare function loadFunctionsWithDiagnostics(directory: string, importModule?: ModuleImporter): Promise<LoadedFunctions>;
95
+ export declare function readFunctionsDirectory(directory: string, importModule?: ModuleImporter): Promise<{
96
+ functions: FunctionsRecord;
97
+ problems: string[];
98
+ }>;
@@ -21,6 +21,7 @@ export declare function createFunctionRoutes(functions: LoadedFunction[], proble
21
21
  /**
22
22
  * Where this router is mounted, so the listing can report a path a caller
23
23
  * can actually request. It used to hardcode `/functions/<name>`, which is
24
- * wrong under every `basePath` including the default `/api`.
24
+ * wrong under every `basePath` including the default `/api`. The
25
+ * `mountPath` a functions group records for the request wins over it.
25
26
  */
26
27
  mountPath?: string): Hono<HonoEnv>;
@@ -67,12 +67,13 @@ export type { RebaseFunctionContext } from "./define-function.js";
67
67
  *
68
68
  * It is safe to hold at module scope, unlike a configuration value, because it
69
69
  * is a lazy Proxy: nothing is resolved until a property is read, which happens
70
- * inside a request. That indirection is also the whole of what a future
71
- * isolate-based host has to hook — see `_setRebaseResolver` in
72
- * `../singleton.ts`.
70
+ * inside a request — and it resolves to the backend serving that request, so
71
+ * with two backends in one process each one's functions see their own. That
72
+ * indirection is the whole of what a host hooks — the Node server per request,
73
+ * an isolate host per isolate — see `_setRebaseResolver` in `../singleton.ts`.
73
74
  */
74
75
  export { rebase } from "../singleton.js";
75
- export { getUser, getUserId, getRoles, hasRole, isAdmin, getScopes, hasScope, isAuthenticated, getDriver, requireDriver, getApiKey, getRequestId, identityResolved } from "./context.js";
76
+ export { getUser, getUserId, getRoles, hasRole, isAdmin, getScopes, hasScope, isAuthenticated, getDriver, requireDriver, getApiKey, getRequestId, getSignal, identityResolved } from "./context.js";
76
77
  export type { FunctionUser } from "./context.js";
77
78
  export { requireAuth, requireAdmin, requireRole, requireScope } from "./guards.js";
78
79
  export { getEnv, env, requireEnv, runtimeKey, isNodeRuntime, lazyResource } from "./runtime-env.js";
@@ -96,13 +96,15 @@ var INSTANCE_SLOT = Symbol.for("@rebasepro/server:singleton-instance");
96
96
  * A *function* that answers "which client is this call for", for hosts where
97
97
  * one answer per process is the wrong shape.
98
98
  *
99
- * A Node server boots once and serves every request from the same client, so
100
- * {@link INSTANCE_SLOT} is the whole story. An isolate-based host is different
101
- * in a way that matters: the environment arrives attached to the **request**,
102
- * not to the module, so there is nothing to publish at import time, and an
103
- * isolate can be reused across requests that must not share state. Such a host
104
- * registers a resolver — typically reading an `AsyncLocalStorage` populated per
105
- * request — and it is consulted first.
99
+ * {@link INSTANCE_SLOT} is one answer per process: the default backend's. A
100
+ * Node process can hold several backends, and the code each one runs — a
101
+ * function, a cron job, a hook — must see that one, so the server registers a
102
+ * resolver reading the backend scope it enters per request (an
103
+ * `AsyncLocalStorage`, host-side, in `backend/scope.ts`). An isolate-based
104
+ * host is different in the same way: the environment arrives attached to the
105
+ * **request**, not to the module, so there is nothing to publish at import
106
+ * time, and an isolate can be reused across requests that must not share
107
+ * state. The resolver is consulted first.
106
108
  *
107
109
  * This exists now, before there is a host that needs it, because of what it
108
110
  * protects: `rebase` is a lazy Proxy, so every property access already goes
@@ -123,10 +125,13 @@ function getInstance() {
123
125
  return global[INSTANCE_SLOT] ?? null;
124
126
  }
125
127
  /**
126
- * The server-side Rebase singleton.
128
+ * The server-side Rebase client, as a module-level handle.
127
129
  *
128
- * Initialized automatically during server startup. Provides access to all
129
- * app-scoped services: **data**, **auth**, **storage**, and **email**.
130
+ * Inside a request, cron run or job a backend serves, it is that backend's
131
+ * client (`backend.rebase`) — so with two backends in one process, each one's
132
+ * functions see their own. Anywhere else it is the process default's, which
133
+ * `initializeRebaseBackend` (or `setDefaultBackend`) installs. Provides access
134
+ * to all app-scoped services: **data**, **auth**, **storage**, and **email**.
130
135
  *
131
136
  * **Admin data plane** (`rebase.dataAsAdmin`):
132
137
  * Backed by the native DataDriver — calls go directly to the database without
@@ -185,12 +190,25 @@ var rebase = new Proxy({}, {
185
190
  return instance[prop];
186
191
  },
187
192
  set(_, prop) {
188
- throw new Error(`Cannot set rebase.${String(prop)} directly. The singleton is read-only. Use _initRebase() during server startup.`);
193
+ throw new Error(`Cannot set rebase.${String(prop)} directly. The singleton is read-only. Install a backend with setDefaultBackend(backend), or use backend.rebase.`);
189
194
  }
190
195
  });
191
196
  //#endregion
192
197
  //#region src/functions/define-function.ts
193
198
  /**
199
+ * The `rebase` handle, seen through the `Database` a function names.
200
+ *
201
+ * One object serves every function; `DB` only describes it. It resolves, per
202
+ * call, to the backend serving the request — the same answer the imported
203
+ * `rebase` gives. Each collection accessor on `dataAsAdmin` is resolved by
204
+ * name at call time, so a generated `Database` adds compile-time checks and
205
+ * changes nothing the object does — the same reason the browser client's
206
+ * `createRebaseClient<Database>()` is one implementation for every `Database`.
207
+ */
208
+ function typedSingleton() {
209
+ return rebase;
210
+ }
211
+ /**
194
212
  * Typed authoring contract for a custom backend function.
195
213
  *
196
214
  * A custom function is a file in the `functionsDir` that default-exports a
@@ -227,7 +245,7 @@ var rebase = new Proxy({}, {
227
245
  */
228
246
  function defineFunction(definition) {
229
247
  const app = new Hono();
230
- const returned = definition(app, { rebase });
248
+ const returned = definition(app, { rebase: typedSingleton() });
231
249
  return returned instanceof Hono ? returned : app;
232
250
  }
233
251
  //#endregion
@@ -393,6 +411,17 @@ function getRequestId(c) {
393
411
  return read(c, "requestId");
394
412
  }
395
413
  /**
414
+ * The signal aborted when this request outlasts the functions timeout and is
415
+ * answered `504 FUNCTION_TIMEOUT`. Pass it to `fetch` and anything else that
416
+ * takes one, so the work stops with the request instead of running on after
417
+ * the caller was told it failed.
418
+ *
419
+ * Never aborts when the timeout is off, or outside the functions router.
420
+ */
421
+ function getSignal(c) {
422
+ return read(c, "functionSignal") ?? new AbortController().signal;
423
+ }
424
+ /**
396
425
  * Whether a Rebase auth middleware has run on this request.
397
426
  *
398
427
  * Both middlewares populate `driver` for *every* outcome, anonymous included,
@@ -1078,6 +1107,6 @@ var ApiError = class ApiError extends Error {
1078
1107
  }
1079
1108
  };
1080
1109
  //#endregion
1081
- export { ApiError, defineFunction, env, getApiKey, getDriver, getEnv, getRequestId, getRoles, getScopes, getUser, getUserId, hasRole, hasScope, identityResolved, isAdmin, isAuthenticated, isNodeRuntime, lazyResource, rebase, requireAdmin, requireAuth, requireDriver, requireEnv, requireRole, requireScope, runtimeKey, waitUntil };
1110
+ export { ApiError, defineFunction, env, getApiKey, getDriver, getEnv, getRequestId, getRoles, getScopes, getSignal, getUser, getUserId, hasRole, hasScope, identityResolved, isAdmin, isAuthenticated, isNodeRuntime, lazyResource, rebase, requireAdmin, requireAuth, requireDriver, requireEnv, requireRole, requireScope, runtimeKey, waitUntil };
1082
1111
 
1083
1112
  //# sourceMappingURL=index.js.map