@rebasepro/server 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/{GCSStorageController-CRd9bFrc.js → GCSStorageController-40FyFeSx.js} +6 -2
  2. package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
  3. package/dist/{S3StorageController-D00EFa5P.js → S3StorageController-Bl5shp94.js} +6 -2
  4. package/dist/S3StorageController-Bl5shp94.js.map +1 -0
  5. package/dist/access-BFAUvsSs.js +1897 -0
  6. package/dist/access-BFAUvsSs.js.map +1 -0
  7. package/dist/api/branch-routes.d.ts +25 -0
  8. package/dist/api/live-schema-routes.d.ts +10 -0
  9. package/dist/api/rest/api-generator.d.ts +9 -1
  10. package/dist/api/rest/idempotency.d.ts +15 -1
  11. package/dist/api/root-error-handler.d.ts +6 -5
  12. package/dist/api/types.d.ts +12 -0
  13. package/dist/{ast-schema-editor-mCeSC03x.js → ast-schema-editor-madTa9Hh.js} +2 -2
  14. package/dist/{ast-schema-editor-mCeSC03x.js.map → ast-schema-editor-madTa9Hh.js.map} +1 -1
  15. package/dist/auth/access.d.ts +10 -0
  16. package/dist/auth/adapter-middleware.d.ts +2 -1
  17. package/dist/auth/adapter-service-key.d.ts +24 -0
  18. package/dist/auth/api-keys/api-key-middleware.d.ts +21 -1
  19. package/dist/auth/api-keys/api-key-store.d.ts +8 -1
  20. package/dist/auth/api-keys/key-grant.d.ts +18 -1
  21. package/dist/auth/expired-token-sweep.d.ts +5 -0
  22. package/dist/auth/impersonation.d.ts +3 -1
  23. package/dist/auth/jwt.d.ts +8 -2
  24. package/dist/auth/oauth-signin-policy.d.ts +21 -35
  25. package/dist/auth/rls-scope.d.ts +17 -7
  26. package/dist/auth/rls-simulator-routes.d.ts +51 -0
  27. package/dist/auth/simulation-read-only.d.ts +28 -0
  28. package/dist/auth/simulation.d.ts +141 -0
  29. package/dist/auth/sql-rate-limit-store.d.ts +15 -1
  30. package/dist/{auth-Dkvo2tTJ.js → auth-izI7AEzC.js} +370 -1880
  31. package/dist/auth-izI7AEzC.js.map +1 -0
  32. package/dist/auth_adapter-B7RUMk6e.js +89 -0
  33. package/dist/auth_adapter-B7RUMk6e.js.map +1 -0
  34. package/dist/backend/create-backend.d.ts +6 -0
  35. package/dist/backend/options.d.ts +9 -4
  36. package/dist/{backend-DTAOsLQc.js → backend-CaG_Jyso.js} +9 -2
  37. package/dist/backend-CaG_Jyso.js.map +1 -0
  38. package/dist/{backup-B9m3cb3X.js → backup-BVbHoKo8.js} +6 -6
  39. package/dist/{backup-B9m3cb3X.js.map → backup-BVbHoKo8.js.map} +1 -1
  40. package/dist/boot/boot.d.ts +2 -15
  41. package/dist/boot/ddl-bootstrap.d.ts +34 -0
  42. package/dist/boot/env.d.ts +6 -0
  43. package/dist/boot/fetch-bundle.d.ts +5 -1
  44. package/dist/boot/provision-only.d.ts +23 -0
  45. package/dist/boot/provision.d.ts +53 -2
  46. package/dist/boot/role.d.ts +2 -0
  47. package/dist/boot/schema-change-recorder.d.ts +45 -0
  48. package/dist/boot/security-headers.d.ts +22 -0
  49. package/dist/branch-routes-B2vvEx53.js +84 -0
  50. package/dist/branch-routes-B2vvEx53.js.map +1 -0
  51. package/dist/{contract-routes-CsFXW7j4.js → contract-routes-CU2A3JnF.js} +2 -2
  52. package/dist/{contract-routes-CsFXW7j4.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
  53. package/dist/cron/cron-store.d.ts +8 -1
  54. package/dist/{cron-loader-Dj6lA4HQ.js → cron-loader-mMhqbVsl.js} +2 -2
  55. package/dist/{cron-loader-Dj6lA4HQ.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
  56. package/dist/{cron-routes-CwbLZguO.js → cron-routes-Cs33j_Kr.js} +3 -3
  57. package/dist/{cron-routes-CwbLZguO.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
  58. package/dist/{cron-scheduler-B04ACoUE.js → cron-scheduler-Be7CNghL.js} +2 -2
  59. package/dist/{cron-scheduler-B04ACoUE.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
  60. package/dist/{cron-store-DkyfuLOH.js → cron-store-DLn11M8H.js} +10 -9
  61. package/dist/cron-store-DLn11M8H.js.map +1 -0
  62. package/dist/{strings.es-BeZWAodn.js → data_driver-D0mg1Y_x.js} +2 -90
  63. package/dist/data_driver-D0mg1Y_x.js.map +1 -0
  64. package/dist/{ddl-bootstrap-Ck20GSIX.js → ddl-bootstrap-CjRrn12l.js} +91 -2
  65. package/dist/ddl-bootstrap-CjRrn12l.js.map +1 -0
  66. package/dist/deploy/pod-contract.d.ts +1 -1
  67. package/dist/email/dev-sink.d.ts +6 -0
  68. package/dist/email/index.d.ts +1 -1
  69. package/dist/{soft-delete-params-BmlVwecC.js → field-access-DYsvjI_P.js} +27 -57
  70. package/dist/field-access-DYsvjI_P.js.map +1 -0
  71. package/dist/{filter-dialect-O1HK6Nvr.js → filter-dialect-TrKFWLqT.js} +3 -3
  72. package/dist/{filter-dialect-O1HK6Nvr.js.map → filter-dialect-TrKFWLqT.js.map} +1 -1
  73. package/dist/{function-loader-KKDqTcIr.js → function-loader-B8YzXEsQ.js} +21 -14
  74. package/dist/function-loader-B8YzXEsQ.js.map +1 -0
  75. package/dist/functions/hono-like.d.ts +12 -0
  76. package/dist/functions/index.js +19 -1
  77. package/dist/functions/index.js.map +1 -1
  78. package/dist/functions/request-timeout.d.ts +3 -3
  79. package/dist/history/history-routes.d.ts +6 -0
  80. package/dist/history/retention-sweep.d.ts +5 -0
  81. package/dist/{history-recorder-B2rkuhuy.js → history-recorder-Bsau7h4X.js} +6 -6
  82. package/dist/history-recorder-Bsau7h4X.js.map +1 -0
  83. package/dist/{history-store-DPofGVDp.js → history-store-BQz5YCkW.js} +26 -2
  84. package/dist/history-store-BQz5YCkW.js.map +1 -0
  85. package/dist/index.d.ts +3 -1
  86. package/dist/index.es.js +2449 -543
  87. package/dist/index.es.js.map +1 -1
  88. package/dist/init/process-safety.d.ts +4 -3
  89. package/dist/init/shutdown.d.ts +8 -0
  90. package/dist/init.d.ts +6 -1
  91. package/dist/jobs/job-store.d.ts +8 -1
  92. package/dist/{jobs-h1WRq3cp.js → jobs-Co6Ni5HP.js} +6 -6
  93. package/dist/jobs-Co6Ni5HP.js.map +1 -0
  94. package/dist/{jwt-D3oFauZI.js → jwt-rDS0884_.js} +4 -3
  95. package/dist/{jwt-D3oFauZI.js.map → jwt-rDS0884_.js.map} +1 -1
  96. package/dist/{keys-COzQ2aE8.js → keys-BXcdRNhJ.js} +2 -2
  97. package/dist/{keys-COzQ2aE8.js.map → keys-BXcdRNhJ.js.map} +1 -1
  98. package/dist/mcp/consent-page.d.ts +38 -1
  99. package/dist/mcp/mcp-routes.d.ts +9 -0
  100. package/dist/mcp/mcp-tools.d.ts +7 -1
  101. package/dist/mcp/oauth-store.d.ts +12 -5
  102. package/dist/metrics/history-recorder.d.ts +4 -1
  103. package/dist/metrics/history-store.d.ts +8 -0
  104. package/dist/{offline-BrqCRgih-BtDB1ecy.js → offline-AvuEcg7u-DFHL98OT.js} +127 -40
  105. package/dist/offline-AvuEcg7u-DFHL98OT.js.map +1 -0
  106. package/dist/{offline-store-BhYXViha-BW5j1We_.js → offline-store-BhYXViha-YTmjBpo2.js} +5 -5
  107. package/dist/{offline-store-BhYXViha-BW5j1We_.js.map → offline-store-BhYXViha-YTmjBpo2.js.map} +1 -1
  108. package/dist/{openapi-generator-BjHr6upl.js → openapi-generator-CyxEtYEX.js} +61 -22
  109. package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
  110. package/dist/{proxy-CH8Jsovg.js → proxy-BYir8tWK.js} +4 -6
  111. package/dist/proxy-BYir8tWK.js.map +1 -0
  112. package/dist/{query-parser-otFZqJsC.js → query-parser-CDXWf2hH.js} +6 -5
  113. package/dist/{query-parser-otFZqJsC.js.map → query-parser-CDXWf2hH.js.map} +1 -1
  114. package/dist/{request-timeout-59crS-9u.js → request-timeout-DVjo-c_8.js} +4 -4
  115. package/dist/request-timeout-DVjo-c_8.js.map +1 -0
  116. package/dist/rls-simulator-routes-bEY7-5-u.js +190 -0
  117. package/dist/rls-simulator-routes-bEY7-5-u.js.map +1 -0
  118. package/dist/schema-edit/deployed-schema-guard.d.ts +47 -0
  119. package/dist/{schema-editor-routes-Bsut0bCC.js → schema-editor-routes-Dc3DU_jm.js} +3 -3
  120. package/dist/{schema-editor-routes-Bsut0bCC.js.map → schema-editor-routes-Dc3DU_jm.js.map} +1 -1
  121. package/dist/soft-delete-params-BRONCOA2.js +59 -0
  122. package/dist/soft-delete-params-BRONCOA2.js.map +1 -0
  123. package/dist/{sql-export-routes-DmB_uZ4l.js → sql-export-routes-QycX8W8S.js} +38 -4
  124. package/dist/sql-export-routes-QycX8W8S.js.map +1 -0
  125. package/dist/{src-D5Y3N-S4.js → src-Bh9LRPb9.js} +21 -2
  126. package/dist/{src-D5Y3N-S4.js.map → src-Bh9LRPb9.js.map} +1 -1
  127. package/dist/storage/GCSStorageController.d.ts +2 -0
  128. package/dist/storage/LocalStorageController.d.ts +8 -3
  129. package/dist/storage/S3StorageController.d.ts +2 -0
  130. package/dist/storage/maintenance-keys.d.ts +17 -0
  131. package/dist/storage/maintenance-routes.d.ts +28 -5
  132. package/dist/storage/maintenance.d.ts +136 -4
  133. package/dist/storage/orphan-scans.d.ts +120 -0
  134. package/dist/storage/types.d.ts +11 -0
  135. package/dist/strings.es-B7QLnHh2.js +96 -0
  136. package/dist/strings.es-B7QLnHh2.js.map +1 -0
  137. package/dist/upload-validation-Bgo626uY.js.map +1 -1
  138. package/package.json +5 -5
  139. package/dist/GCSStorageController-CRd9bFrc.js.map +0 -1
  140. package/dist/S3StorageController-D00EFa5P.js.map +0 -1
  141. package/dist/auth-Dkvo2tTJ.js.map +0 -1
  142. package/dist/auth_adapter-CIItCTc8.js +0 -21
  143. package/dist/auth_adapter-CIItCTc8.js.map +0 -1
  144. package/dist/backend-DTAOsLQc.js.map +0 -1
  145. package/dist/cron-store-DkyfuLOH.js.map +0 -1
  146. package/dist/ddl-bootstrap-Ck20GSIX.js.map +0 -1
  147. package/dist/function-loader-KKDqTcIr.js.map +0 -1
  148. package/dist/history-recorder-B2rkuhuy.js.map +0 -1
  149. package/dist/history-store-DPofGVDp.js.map +0 -1
  150. package/dist/jobs-h1WRq3cp.js.map +0 -1
  151. package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
  152. package/dist/openapi-generator-BjHr6upl.js.map +0 -1
  153. package/dist/proxy-CH8Jsovg.js.map +0 -1
  154. package/dist/request-timeout-59crS-9u.js.map +0 -1
  155. package/dist/soft-delete-params-BmlVwecC.js.map +0 -1
  156. package/dist/sql-export-routes-DmB_uZ4l.js.map +0 -1
  157. package/dist/strings.es-BeZWAodn.js.map +0 -1
@@ -0,0 +1,1897 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ import { _ as scopesBeyond, c as intersectScopes, d as isDataPlaneScope, g as scopeGrantsAny, h as scopeGrants, i as EMPTY_ACCESS_MODEL, m as roleNameProblem, n as ADMIN_SCOPES, p as parseScope, r as BUILT_IN_SCOPES, s as hasAdminRole, t as ADMIN_ROLE, v as scopesForRoles, x as validateScopes } from "./src-Bh9LRPb9.js";
6
+ import { t as ANONYMOUS_USER_ID } from "./policy-PdzX-Opd.js";
7
+ import { a as SIMULATE_USER_ID_HEADER, i as SIMULATE_ROLE_HEADER, n as SIMULATED_CONTEXT_HEADER, o as formatSimulatedContext, r as SIMULATE_API_KEY_ID_HEADER, t as IMPERSONATE_HEADER } from "./auth_adapter-B7RUMk6e.js";
8
+ import { a as logger } from "./logger-CW8pmTS6.js";
9
+ import { r as restrictedFieldNames, t as apiHiddenFieldNames } from "./field-access-DYsvjI_P.js";
10
+ import { E as sha256Hex, S as constantTimeEqual, h as processJwtService, m as jwtOf } from "./jwt-rDS0884_.js";
11
+ import { a as isDataException, c as warnIfDatabaseClosed, i as errorHandler, o as isDatabaseUnreachable, t as ApiError } from "./errors-OMWkKSH-.js";
12
+ import { a as canonicalStorageId, o as canonicalStorageKey, r as InvalidStorageKeyError } from "./keys-BXcdRNhJ.js";
13
+ import { n as scopeGuardProblem } from "./admin-roles-DI9RsgN3.js";
14
+ //#region ../types/src/controllers/storage.ts
15
+ /**
16
+ * Path prefix that marks an object as **public**. Files stored under this
17
+ * prefix are served without any auth token via a stable, permanent,
18
+ * CDN-cacheable URL (see {@link StorageSource.getSignedUrl}). Shared by the
19
+ * client SDK and the backend so both agree on which objects are public.
20
+ *
21
+ * @group Models
22
+ */
23
+ var PUBLIC_STORAGE_PREFIX = "public/";
24
+ /**
25
+ * True when a storage **key** is public: it lies under
26
+ * {@link PUBLIC_STORAGE_PREFIX}.
27
+ *
28
+ * The key is read exactly as it is — the canonical key within its bucket, the
29
+ * one the server stores, serves, and hands a `storageAuthorize` hook as
30
+ * `key` — and nothing is parsed out of it. That is the difference from
31
+ * {@link isPublicStoragePath}, which reads a client-side *path* and so strips
32
+ * a `scheme://` and a leading `default/` bucket segment first. On a key both are
33
+ * wrong: `default/public/x` is a key in a folder called `default`, and a
34
+ * request path `notes://public/x` names the key `notes:/public/x`. Neither is
35
+ * public, and the server decides on this function so that it never says they
36
+ * are.
37
+ *
38
+ * @group Models
39
+ */
40
+ function isPublicStorageKey(key) {
41
+ if (!key || !key.startsWith("public/")) return false;
42
+ return !key.split(/[\\/]/).includes("..");
43
+ }
44
+ /**
45
+ * True when a storage key or client-side path points at a public object
46
+ * (lives under {@link PUBLIC_STORAGE_PREFIX}).
47
+ *
48
+ * For a path as a client holds it: a `scheme://` prefix and a single leading
49
+ * `default/` bucket segment are stripped first. The server and a
50
+ * `storageAuthorize` hook, which hold the canonical key, use
51
+ * {@link isPublicStorageKey}.
52
+ *
53
+ * @group Models
54
+ */
55
+ function isPublicStoragePath(path) {
56
+ if (!path) return false;
57
+ let p = path;
58
+ const scheme = p.indexOf("://");
59
+ if (scheme !== -1) p = p.substring(scheme + 3);
60
+ p = p.replace(/^\/+/, "");
61
+ if (p.split("/").some((seg) => seg === "..")) return false;
62
+ return p.startsWith("public/") || p.startsWith(`default/public/`);
63
+ }
64
+ /**
65
+ * The path the storage routes address an object by, from a key or from the
66
+ * `storageUrl` an upload returned (`<scheme>://<bucket>/<key>`, which is what a
67
+ * property with `includeBucketUrl: true` keeps in its column).
68
+ *
69
+ * The routes address a source's objects by key. On S3 and GCS the bucket in a
70
+ * `storageUrl` is always the source's own — the server refuses any other — so
71
+ * it is dropped: sent along, `s3://acme-media/products/a.png` reached the
72
+ * server as the key `acme-media/products/a.png`, the preview found nothing and
73
+ * a delete reported success while the object stayed. It worked on local disk
74
+ * only, because `local://default/<key>` happens to carry the one bucket
75
+ * segment the routes recognise — which is kept, as is any other local bucket.
76
+ *
77
+ * A bare key whose first folder is `default` is sent behind that bucket
78
+ * segment. The routes read one leading `default/` (in any case) as the bucket,
79
+ * so the key `default/photo.png` sent as it is names the root `photo.png`:
80
+ * {@link defaultBucketPath} is the spelling that names the key itself.
81
+ *
82
+ * Shared by every storage source that talks to those routes, so they address
83
+ * an object alike.
84
+ *
85
+ * @group Models
86
+ */
87
+ function storageObjectPath(keyOrUrl, bucket) {
88
+ let filePath = keyOrUrl;
89
+ let bucketQualified = false;
90
+ const scheme = /^(local|s3|gs):\/\//.exec(filePath);
91
+ if (scheme) {
92
+ filePath = filePath.substring(scheme[0].length);
93
+ if (scheme[1] !== "local") {
94
+ const slash = filePath.indexOf("/");
95
+ if (slash > 0) filePath = filePath.substring(slash + 1);
96
+ } else bucketQualified = true;
97
+ }
98
+ if (bucket && filePath) {
99
+ if (!filePath.startsWith(`${bucket}/`)) filePath = `${bucket}/${filePath}`;
100
+ bucketQualified = true;
101
+ }
102
+ return bucketQualified ? filePath : defaultBucketPath(filePath);
103
+ }
104
+ /**
105
+ * The request path of a key in the default bucket: the key itself, unless the
106
+ * routes would read its first folder as the bucket — a first folder named
107
+ * `default`, matched as the routes match it (case-insensitively, and only when
108
+ * something follows it). Then the bucket segment goes in front, so exactly the
109
+ * one segment the routes strip is the one added.
110
+ */
111
+ function defaultBucketPath(key) {
112
+ const segments = key.split("/");
113
+ return segments.length > 1 && segments[0].toLowerCase() === "default" ? `default/${key}` : key;
114
+ }
115
+ //#endregion
116
+ //#region src/auth/api-keys/http-operation.ts
117
+ /**
118
+ * Map an HTTP method to the operation it performs.
119
+ *
120
+ * - `GET`, `HEAD`, `OPTIONS` → `"read"`
121
+ * - `POST`, `PUT`, `PATCH` → `"write"`
122
+ * - `DELETE` → `"delete"`
123
+ *
124
+ * Any other method is a `"write"`: a verb this does not know is not assumed
125
+ * to be harmless.
126
+ */
127
+ function httpMethodToOperation(method) {
128
+ switch (method.toUpperCase()) {
129
+ case "GET":
130
+ case "HEAD":
131
+ case "OPTIONS": return "read";
132
+ case "DELETE": return "delete";
133
+ default: return "write";
134
+ }
135
+ }
136
+ //#endregion
137
+ //#region src/auth/simulation-read-only.ts
138
+ /**
139
+ * "Simulate access" is read-only.
140
+ *
141
+ * The simulator answers "what does this role, user or key see". A write made
142
+ * while simulating ran as the simulated identity for real: it committed as
143
+ * `rebase_user` with `app.uid` set to the simulated uid, `entity_history`
144
+ * recorded that uid as the author, callbacks saw `context.user` holding a role
145
+ * the account may not even have, and a role-only simulation stamped owner
146
+ * columns with the nobody uid. Only the request log named the administrator.
147
+ * So every door that honours the simulation headers refuses anything but a
148
+ * read, naming the simulation, rather than performing it as someone else.
149
+ *
150
+ * Its own module so the REST generator, history routes and the functions
151
+ * guard can share it without importing the simulator itself.
152
+ *
153
+ * @module
154
+ */
155
+ /**
156
+ * Refuse a write or delete on a request that simulates access.
157
+ *
158
+ * `c` is anything with the request context's `get` — the REST generator calls
159
+ * this with a per-operation stand-in for each operation of a `_batch`.
160
+ * `operation` is what the request does, not its verb: `POST …/query` is a read.
161
+ */
162
+ function refuseSimulatedWrite(c, operation) {
163
+ if (operation === "read" || !c.get("simulation")) return;
164
+ throw new ApiError(403, "SIMULATION_READ_ONLY", `This request simulates access (x-rebase-simulate-*), and simulated access is read-only: it would ${operation === "delete" ? "delete" : "write"} as the simulated identity, not as you. Stop simulating to make changes.`);
165
+ }
166
+ //#endregion
167
+ //#region src/auth/token-revocation.ts
168
+ /**
169
+ * Has this access token been revoked?
170
+ *
171
+ * Everything that ends every session a user holds — a password change or reset
172
+ * of any kind, and `DELETE /auth/sessions` — goes through
173
+ * {@link revokeAllSessions}, which stamps a `tokensValidAfter` watermark on the
174
+ * user. It also deletes refresh-token rows, which is what made the gap easy to
175
+ * miss: the session really is gone, and the *refresh* path really does check
176
+ * the watermark — so signing out looked like it worked.
177
+ *
178
+ * The access token was untouched. It is a bearer credential that nothing
179
+ * consulted a database about, so it stayed valid for its full lifetime after
180
+ * every one of those actions. "Sign out everywhere" left the stolen token
181
+ * working; so did changing your password because you thought it had leaked.
182
+ *
183
+ * The watermark had exactly one read in the repository, on refresh. This adds
184
+ * the other one.
185
+ *
186
+ * ## Cost
187
+ *
188
+ * One indexed lookup per request, on the paths that already make one. The
189
+ * adapter re-reads roles for every authenticated request; the admin routes now
190
+ * do too. Both call this with the row they were already fetching in mind — a
191
+ * repository that wants to serve both from one query is free to cache
192
+ * internally, but nothing here assumes it.
193
+ *
194
+ * ## Failure
195
+ *
196
+ * Fails **open**, deliberately, and this is the one place in the auth stack
197
+ * where that is right. The watermark is an extra revocation signal layered over
198
+ * a token that has already been verified — signature, expiry and purpose all
199
+ * checked. Refusing every request when the database is unreachable would turn a
200
+ * transient outage into a total sign-out of an entire deployment, and the
201
+ * attacker this protects against needs to have already stolen a live token. The
202
+ * failure is logged at warn so it is visible rather than silent.
203
+ */
204
+ async function isAccessTokenRevoked(authRepo, payload) {
205
+ if (typeof authRepo.getTokensValidAfter !== "function") return false;
206
+ if (typeof payload.iat !== "number") return false;
207
+ let validAfter;
208
+ try {
209
+ validAfter = await authRepo.getTokensValidAfter(payload.uid);
210
+ } catch (error) {
211
+ logger.warn("[Auth] Could not read the token revocation watermark; allowing the request", {
212
+ uid: payload.uid,
213
+ error
214
+ });
215
+ return false;
216
+ }
217
+ return issuedBefore(payload, validAfter);
218
+ }
219
+ /**
220
+ * Is the account behind this verified access token still the one that may use
221
+ * it — and with which roles?
222
+ *
223
+ * A verified signature says who the token was minted for, an hour ago at
224
+ * most. This asks the database what has happened to that account since. Every
225
+ * door that honours an access token — the data plane, the admin gates, the
226
+ * realtime socket — asks it here, so they cannot disagree about it.
227
+ *
228
+ * They did. A deleted account read as "not revoked": the watermark lives on
229
+ * the user row, so once the row was gone there was no watermark, and the
230
+ * roles lookup answered `[]` rather than "nobody". A token its owner had
231
+ * revoked came back to life when an administrator deleted the account, as an
232
+ * authenticated principal with that uid, for the rest of its lifetime. MCP
233
+ * refresh and personal API keys already treated a missing account as revoked;
234
+ * the main door did not.
235
+ *
236
+ * Throws when the repository does. Unlike the watermark read on its own, this
237
+ * one decides the roles a request runs with, so a failure is a refusal (the
238
+ * callers answer 503) rather than a guess.
239
+ *
240
+ * Except one failure that is an answer: the store refusing the uid as a value
241
+ * its id column cannot hold — `u1` against a UUID key. That names no account,
242
+ * exactly as a well-formed id nobody has does, so it is `account-deleted` and
243
+ * the caller answers 401. Answered as a failure it was a 503 asking the
244
+ * client to try again, which it would do forever.
245
+ */
246
+ async function judgeAccessToken(authRepo, payload) {
247
+ try {
248
+ return await readAccessVerdict(authRepo, payload);
249
+ } catch (error) {
250
+ if (isDataException(error)) return {
251
+ live: false,
252
+ refusal: "account-deleted"
253
+ };
254
+ throw error;
255
+ }
256
+ }
257
+ async function readAccessVerdict(authRepo, payload) {
258
+ let roles;
259
+ let validAfter = null;
260
+ if (typeof authRepo.getAccountAccessState === "function") {
261
+ const state = await authRepo.getAccountAccessState(payload.uid, payload.sid);
262
+ if (!state) return {
263
+ live: false,
264
+ refusal: "account-deleted"
265
+ };
266
+ if (state.disabled) return {
267
+ live: false,
268
+ refusal: "account-disabled"
269
+ };
270
+ roles = state.roles;
271
+ validAfter = state.tokensValidAfter;
272
+ if (payload.sid && state.sessionActive === false) return {
273
+ live: false,
274
+ refusal: "session-revoked"
275
+ };
276
+ } else {
277
+ if (typeof authRepo.getUserWithRoles === "function") {
278
+ const account = await authRepo.getUserWithRoles(payload.uid);
279
+ if (!account) return {
280
+ live: false,
281
+ refusal: "account-deleted"
282
+ };
283
+ if (account.user.disabled) return {
284
+ live: false,
285
+ refusal: "account-disabled"
286
+ };
287
+ roles = account.roles;
288
+ }
289
+ if (typeof authRepo.getTokensValidAfter === "function") validAfter = await authRepo.getTokensValidAfter(payload.uid);
290
+ }
291
+ if (issuedBefore(payload, validAfter)) return {
292
+ live: false,
293
+ refusal: "revoked"
294
+ };
295
+ return {
296
+ live: true,
297
+ roles
298
+ };
299
+ }
300
+ /**
301
+ * The 401 a door answers an access token with when {@link judgeAccessToken}
302
+ * refuses it — one wording and one code for each reason, wherever the token
303
+ * was presented.
304
+ *
305
+ * `SESSION_REVOKED` for a token whose session or account is gone: the client
306
+ * SDK answers it with a refresh, which the refresh route refuses for the same
307
+ * reason, and the client signs out. `ACCOUNT_DISABLED` for an account an
308
+ * administrator switched off, which no refresh can mend.
309
+ */
310
+ function accessTokenRefusalError(refusal) {
311
+ switch (refusal) {
312
+ case "account-disabled": return ApiError.unauthorized("This account has been disabled", "ACCOUNT_DISABLED");
313
+ case "account-deleted": return ApiError.unauthorized("This account no longer exists", "SESSION_REVOKED");
314
+ case "revoked":
315
+ case "session-revoked": return ApiError.unauthorized("Session has been revoked", "SESSION_REVOKED");
316
+ }
317
+ }
318
+ /** The watermark comparison {@link isAccessTokenRevoked} and the judge share. */
319
+ function issuedBefore(payload, validAfter) {
320
+ if (!validAfter || typeof payload.iat !== "number") return false;
321
+ return payload.iat < Math.floor(validAfter.getTime() / 1e3);
322
+ }
323
+ /**
324
+ * End every session this user holds, on every device.
325
+ *
326
+ * Two writes, because each covers what the other cannot. Deleting the refresh
327
+ * rows ends the sessions that exist at this instant, and on a repository
328
+ * without the watermark it is the only revocation there is. The watermark
329
+ * voids what the delete cannot see: a refresh already in flight that inserts
330
+ * its rotated token a moment after the delete ran, and every access token
331
+ * already handed out, which {@link isAccessTokenRevoked} refuses from here on.
332
+ *
333
+ * The watermark write does not fail the caller. By the time it runs the
334
+ * credential has usually already changed and the rows are gone, so a 500 would
335
+ * tell someone their password did not change when it did. It is logged, because
336
+ * a failure here leaves access tokens working until they expire.
337
+ */
338
+ async function revokeAllSessions(authRepo, uid) {
339
+ await authRepo.deleteAllRefreshTokensForUser(uid);
340
+ try {
341
+ await authRepo.setTokensValidAfter?.(uid, /* @__PURE__ */ new Date());
342
+ } catch (error) {
343
+ logger.warn("[Auth] Could not write the token revocation watermark; access tokens issued before now stay valid until they expire", {
344
+ uid,
345
+ error
346
+ });
347
+ }
348
+ }
349
+ /**
350
+ * Replace a user's password and end every session they hold.
351
+ *
352
+ * The one way a password hash is written over an existing account. A new
353
+ * password is what someone sets when they believe the old one — or a session
354
+ * signed in with it — is in someone else's hands, so a password change that
355
+ * leaves those sessions alive does not do the thing it was done for.
356
+ *
357
+ * It used to be a pair of lines repeated after each `updatePassword`, and it
358
+ * reached the two self-service routes and none of the admin ones: an
359
+ * administrator resetting a phished account left the attacker's refresh token
360
+ * minting access tokens for the rest of its lifetime.
361
+ * `test/password-change-revokes-sessions.test.ts` holds every route that sets a
362
+ * password to this, and fails if anything but this function calls
363
+ * `updatePassword`.
364
+ *
365
+ * `null` removes the password instead, for the same reason and with the same
366
+ * revocation: see `confirmAddressOwnership`.
367
+ */
368
+ async function replaceUserPassword(authRepo, uid, passwordHash) {
369
+ await authRepo.updatePassword(uid, passwordHash);
370
+ await revokeAllSessions(authRepo, uid);
371
+ await authRepo.deleteAllPasswordResetTokensForUser?.(uid);
372
+ }
373
+ //#endregion
374
+ //#region src/auth/rls-scope.ts
375
+ /**
376
+ * The identity the trusted server plane runs as: `rebase.dataAsAdmin`, and any
377
+ * caller presenting the service key.
378
+ *
379
+ * Naming it makes the privilege legible, because it is narrower than it reads.
380
+ * This is **not** an RLS bypass — a driver scoped with it is an
381
+ * `AuthenticatedPostgresBackendDriver`, so every statement runs in a
382
+ * transaction that has done `SET LOCAL ROLE rebase_user` with
383
+ * `app.uid = 'service'`, and policies are evaluated. Two consequences worth
384
+ * knowing before you write a policy:
385
+ *
386
+ * - It passes the default policies through their `rolesOverlap(['admin'])`
387
+ * arm — the same arm an application user holding the `admin` role passes.
388
+ * - `policy.serverContext()` compiles to `rebase.uid() IS NULL` and is therefore
389
+ * **false** for it. A collection with `disableDefaultPolicies: true` whose
390
+ * only rule is `serverContext()` denies these writes (42501) and returns
391
+ * zero rows for these reads.
392
+ *
393
+ * The true bypass is `rebase.sql()`, which runs on the owner connection and
394
+ * never goes near `withAuth`.
395
+ */
396
+ var SERVICE_IDENTITY = {
397
+ uid: "service",
398
+ roles: ["admin"]
399
+ };
400
+ /**
401
+ * Returns true if the driver supports RLS scoping via `withAuth()`.
402
+ */
403
+ function isRLSScopedDriver(driver) {
404
+ return "withAuth" in driver && typeof driver.withAuth === "function";
405
+ }
406
+ /**
407
+ * Can this driver run a request as a specific user?
408
+ *
409
+ * The same question `scopeDataDriver` asks internally, exported because one
410
+ * caller must act on the answer BEFORE it has a request to scope.
411
+ *
412
+ * `scopeDataDriver` returns the driver unscoped when the answer is no, which is
413
+ * the right behaviour for `/api/data` — Mongo has no row-level security and
414
+ * never claimed to, and the route's own checks still apply. It is the wrong
415
+ * behaviour for a surface whose entire promise is "this runs as you": there,
416
+ * an unscoped driver silently means "runs as the database owner". So the MCP
417
+ * surface asks this at boot and declines to mount rather than degrade.
418
+ */
419
+ function supportsRlsScoping(driver) {
420
+ return isRLSScopedDriver(driver);
421
+ }
422
+ /**
423
+ * Scope a DataDriver via `withAuth()` for RLS.
424
+ *
425
+ * SECURITY: If `withAuth()` is available but fails, the error is re-thrown
426
+ * so the request is **denied** rather than proceeding with unscoped access
427
+ * (fail-closed behavior).
428
+ *
429
+ * If the driver does not support RLS, the original driver is returned.
430
+ *
431
+ * @param driver - The DataDriver to scope.
432
+ * @param user - The authenticated user identity for RLS.
433
+ * @returns The RLS-scoped DataDriver (or the original if RLS is unsupported).
434
+ */
435
+ async function scopeDataDriver(driver, user) {
436
+ if (isRLSScopedDriver(driver)) return await driver.withAuth(user);
437
+ return driver;
438
+ }
439
+ //#endregion
440
+ //#region src/auth/crypto-utils.ts
441
+ /**
442
+ * Cryptographic utility functions for auth.
443
+ *
444
+ * @module
445
+ */
446
+ /**
447
+ * Constant-time string comparison to prevent timing attacks.
448
+ *
449
+ * Used for comparing service keys, tokens, and other secrets where
450
+ * timing side-channels could leak information about the expected value.
451
+ *
452
+ * The implementation lives in `utils/portable-crypto.ts`, which does this over
453
+ * UTF-8 bytes without `node:crypto` — this function is on the request path, and
454
+ * the byte-level reasoning it depends on is documented there.
455
+ *
456
+ * @param a - First string to compare.
457
+ * @param b - Second string to compare.
458
+ * @returns `true` if the strings are identical, `false` otherwise.
459
+ */
460
+ function safeCompare(a, b) {
461
+ return constantTimeEqual(a, b);
462
+ }
463
+ //#endregion
464
+ //#region src/auth/bearer-token.ts
465
+ /**
466
+ * Parsing of the `Authorization: Bearer <token>` header.
467
+ *
468
+ * Its own module rather than a helper inside `middleware.ts`, because the
469
+ * API-key middleware needs it too and `middleware.ts` already imports from
470
+ * there — putting it in either one would close an import cycle.
471
+ *
472
+ * @module
473
+ */
474
+ /**
475
+ * Pull the token out of an `Authorization: Bearer <token>` header.
476
+ *
477
+ * RFC 7235 §2.1 makes the auth-scheme name case-insensitive, and plenty of
478
+ * clients send `bearer` — HTTP libraries that lower-case what they emit,
479
+ * hand-written curl, proxies that normalise headers. Matching `"Bearer "`
480
+ * exactly turned those into a 401 that, from the caller's side, is
481
+ * indistinguishable from a rejected token; people spend an afternoon
482
+ * regenerating credentials that were fine. Only the scheme is folded — the
483
+ * token itself is returned byte-for-byte.
484
+ *
485
+ * Returns `undefined` when there is no header or it names another scheme.
486
+ * That is deliberately distinct from an empty token (`Bearer ` with nothing
487
+ * after it), which callers still route into verification so it fails as a bad
488
+ * token rather than as a missing header.
489
+ */
490
+ function extractBearerToken(authHeader) {
491
+ if (!authHeader) return void 0;
492
+ const separator = authHeader.indexOf(" ");
493
+ if (separator < 0) return void 0;
494
+ if (authHeader.slice(0, separator).toLowerCase() !== "bearer") return void 0;
495
+ return authHeader.slice(separator + 1);
496
+ }
497
+ //#endregion
498
+ //#region src/auth/api-keys/api-key-middleware.ts
499
+ /**
500
+ * Check whether a token looks like a Rebase API key.
501
+ */
502
+ function isApiKeyToken(token) {
503
+ return token.startsWith("rk_");
504
+ }
505
+ /**
506
+ * Verify a presented key: it exists, is live, and — for a personal key —
507
+ * personal keys are on and its owner still exists. Records the use.
508
+ *
509
+ * `access` is the backend the key belongs to: whether it takes personal keys,
510
+ * how it finds their owners, and what their roles hold. The process default
511
+ * when omitted.
512
+ */
513
+ async function resolveApiKey(store, token, access = processAccessControl) {
514
+ const apiKey = await store.findByKeyHash(await sha256Hex(token));
515
+ if (!apiKey) return { message: "Invalid API key" };
516
+ const identity = await identityOfKey(apiKey, access);
517
+ if (!("uid" in identity)) return identity;
518
+ const lastTouch = lastUsedTouchedAt.get(apiKey.id);
519
+ const now = Date.now();
520
+ if (!lastTouch || now - lastTouch >= LAST_USED_DEBOUNCE_MS) {
521
+ lastUsedTouchedAt.set(apiKey.id, now);
522
+ store.updateLastUsed(apiKey.id).catch(() => {});
523
+ }
524
+ const masked = {
525
+ id: apiKey.id,
526
+ name: apiKey.name,
527
+ kind: apiKey.kind,
528
+ key_prefix: apiKey.key_prefix,
529
+ scopes: apiKey.scopes,
530
+ roles: apiKey.roles,
531
+ owner_uid: apiKey.owner_uid,
532
+ rate_limit: apiKey.rate_limit,
533
+ created_by: apiKey.created_by,
534
+ created_at: apiKey.created_at,
535
+ updated_at: apiKey.updated_at,
536
+ last_used_at: apiKey.last_used_at,
537
+ expires_at: apiKey.expires_at,
538
+ revoked_at: apiKey.revoked_at
539
+ };
540
+ return {
541
+ ...identity,
542
+ apiKey: masked
543
+ };
544
+ }
545
+ /**
546
+ * What a collection callback is told about the key a request authenticated
547
+ * with (`context.credential`): its kind, id and name, never the secret.
548
+ */
549
+ function credentialOf(apiKey) {
550
+ return {
551
+ kind: apiKey.kind,
552
+ id: apiKey.id,
553
+ name: apiKey.name
554
+ };
555
+ }
556
+ /**
557
+ * Who a stored key acts as and what it holds — the half of
558
+ * {@link resolveApiKey} that does not depend on how the key was found. The RLS
559
+ * simulator asks it of a key named by id, so "view as this key" and a request
560
+ * presenting the key cannot drift apart.
561
+ */
562
+ async function identityOfKey(apiKey, access = processAccessControl) {
563
+ if (apiKey.revoked_at) return { message: "API key has been revoked" };
564
+ if (apiKey.expires_at && new Date(apiKey.expires_at) < /* @__PURE__ */ new Date()) return { message: "API key has expired" };
565
+ if (apiKey.kind === "personal") {
566
+ const resolveOwner = access.resolveKeyOwner;
567
+ if (!resolveOwner || !apiKey.owner_uid) return { message: "Personal API keys are switched off on this backend" };
568
+ const owner = await resolveOwner(apiKey.owner_uid);
569
+ if (!owner) return { message: "The account this API key acts as no longer exists" };
570
+ return {
571
+ uid: apiKey.owner_uid,
572
+ roles: owner.roles,
573
+ scopes: intersectScopes(apiKey.scopes, scopesForRoles(owner.roles, access.model))
574
+ };
575
+ }
576
+ return {
577
+ uid: `api-key:${apiKey.id}`,
578
+ roles: ["service", ...apiKey.roles.filter((role) => role !== "service")],
579
+ scopes: apiKey.scopes
580
+ };
581
+ }
582
+ /** Per-process debounce state for last_used_at touches. */
583
+ var lastUsedTouchedAt = /* @__PURE__ */ new Map();
584
+ var LAST_USED_DEBOUNCE_MS = 6e4;
585
+ /**
586
+ * Validate an API key token and populate the Hono context: `user`, `apiKey`,
587
+ * `scopes` and the RLS-scoped `driver`.
588
+ *
589
+ * Returns `true` when the context is populated, or the error Response.
590
+ */
591
+ async function validateApiKey(c, token, options) {
592
+ const resolved = await resolveApiKey(options.store, token, accessOf(c));
593
+ if (!("uid" in resolved)) return errorHandler(ApiError.unauthenticated(resolved.message), c);
594
+ c.set("user", {
595
+ uid: resolved.uid,
596
+ roles: resolved.roles
597
+ });
598
+ c.set("apiKey", resolved.apiKey);
599
+ c.set("scopes", resolved.scopes);
600
+ try {
601
+ c.set("driver", await scopeDataDriver(options.driver, {
602
+ uid: resolved.uid,
603
+ roles: resolved.roles,
604
+ credential: credentialOf(resolved.apiKey)
605
+ }));
606
+ } catch (error) {
607
+ logger.error("[AUTH] RLS scoping failed for API key", { error });
608
+ return errorHandler(ApiError.internal("Internal authentication error"), c);
609
+ }
610
+ return true;
611
+ }
612
+ /**
613
+ * The 403 for a credential that lacks a data-plane scope outside the REST
614
+ * generator (storage and functions). Names the scope that would grant it.
615
+ */
616
+ function forbidScope(c, scope, target) {
617
+ const wanted = target !== void 0 && target !== "" ? `${scope}:${target}` : scope;
618
+ return errorHandler(new ApiError(403, "SCOPE_MISSING", `This credential does not hold the "${wanted}" scope.`, { requiredScope: wanted }), c);
619
+ }
620
+ /** Whether this request's caller holds `scope` — on `target` when given. */
621
+ function callerHolds(c, scope, target) {
622
+ const narrowed = c.get("scopes");
623
+ if (!narrowed) return true;
624
+ return target === void 0 ? scopeGrantsAny(narrowed, scope) : scopeGrants(narrowed, scope, target);
625
+ }
626
+ /**
627
+ * Scope guard for the resumable-upload routes (`/tus/:id`).
628
+ *
629
+ * Every step of an upload — the offset check (HEAD), the chunks (PATCH), the
630
+ * cancel (DELETE) — is part of writing it, so all of them need
631
+ * `storage:write`. Which source the upload writes to was checked when it was
632
+ * created; these steps only reach an upload the same caller owns.
633
+ */
634
+ function createTusScopeGuard() {
635
+ return async (c, next) => {
636
+ if (/\/tus\/[^/]+$/.test(c.req.path) && c.get("user") && !callerHolds(c, "storage:write")) return forbidScope(c, "storage:write");
637
+ return next();
638
+ };
639
+ }
640
+ /**
641
+ * Scope guard for the custom-functions router: a narrowed credential needs
642
+ * `functions:invoke` on the function it calls. The functions index — the
643
+ * listing at the mount point itself — needs the unqualified scope.
644
+ *
645
+ * People pass: what a person may do inside a function is the function's own
646
+ * business, as it always was.
647
+ *
648
+ * @param mountPrefix - The path the functions router is mounted at
649
+ * (e.g. `/api/functions`), used to extract the function
650
+ * name from the request path. Omitted, it is the
651
+ * `mountPath` the functions group recorded for this
652
+ * request — wherever the application mounted it.
653
+ *
654
+ * It also keeps "Simulate access" read-only here: a function called with a
655
+ * simulation header by any verb but GET, HEAD or OPTIONS is refused
656
+ * (`SIMULATION_READ_ONLY`), since it would run as the simulated identity.
657
+ */
658
+ function createFunctionScopeGuard(mountPrefix) {
659
+ return async (c, next) => {
660
+ try {
661
+ refuseSimulatedWrite(c, httpMethodToOperation(c.req.method));
662
+ } catch (error) {
663
+ return errorHandler(error instanceof Error ? error : new Error(String(error)), c);
664
+ }
665
+ const narrowed = c.get("scopes");
666
+ if (!narrowed) return next();
667
+ const path = c.req.path;
668
+ const prefix = mountPrefix ?? c.get("mountPath") ?? "";
669
+ const idx = path.indexOf(prefix);
670
+ const rawName = (idx < 0 ? "" : path.slice(idx + prefix.length)).split("/").filter(Boolean)[0] ?? "";
671
+ let functionName;
672
+ try {
673
+ functionName = decodeURIComponent(rawName);
674
+ } catch {
675
+ functionName = rawName;
676
+ }
677
+ if (!(functionName === "" ? scopeGrants(narrowed, "functions:invoke") : scopeGrants(narrowed, "functions:invoke", functionName))) return forbidScope(c, "functions:invoke", functionName);
678
+ return next();
679
+ };
680
+ }
681
+ /**
682
+ * Pre-auth middleware for `rk_` bearer tokens.
683
+ *
684
+ * Routers whose auth gate is JWT-based (`createRequireAuth` — the admin
685
+ * surfaces, the key routes) don't know about API keys. Mounted in front of
686
+ * them, this authenticates `rk_` tokens and populates the request context; the
687
+ * downstream gates then see the already-resolved caller and check its scopes.
688
+ *
689
+ * Requests without an `rk_` bearer token pass through untouched. An invalid,
690
+ * revoked, or expired `rk_` token is rejected here (401) rather than falling
691
+ * through to be misparsed as a JWT.
692
+ */
693
+ function createApiKeyPreAuth(options) {
694
+ return async (c, next) => {
695
+ if (c.get("apiKey")) return next();
696
+ const token = extractBearerToken(c.req.header("authorization")) ?? "";
697
+ if (!token || !isApiKeyToken(token)) return next();
698
+ const result = await validateApiKey(c, token, options);
699
+ if (result !== true) return result;
700
+ return next();
701
+ };
702
+ }
703
+ /** Roles that mean "no session at all". */
704
+ var ANONYMOUS_ROLES = /* @__PURE__ */ new Set(["anon", "anonymous"]);
705
+ var ADMIN_ONLY$1 = "Only a signed-in administrator can simulate another role, user or API key.";
706
+ /** Read the three simulation headers, or `undefined` when none was sent. */
707
+ function simulationOf(header) {
708
+ const role = header(SIMULATE_ROLE_HEADER);
709
+ const userId = header(SIMULATE_USER_ID_HEADER);
710
+ const apiKeyId = header(SIMULATE_API_KEY_ID_HEADER);
711
+ if (role === void 0 && userId === void 0 && apiKeyId === void 0) return void 0;
712
+ return {
713
+ ...role !== void 0 ? { role } : {},
714
+ ...userId !== void 0 ? { userId } : {},
715
+ ...apiKeyId !== void 0 ? { apiKeyId } : {}
716
+ };
717
+ }
718
+ /** Whether a request carries any simulation header. */
719
+ function carriesSimulation(header) {
720
+ return simulationOf(header) !== void 0;
721
+ }
722
+ /**
723
+ * Decide whether this caller may simulate what they asked for, and who the
724
+ * request then runs as.
725
+ *
726
+ * The caller is judged before anything they named is looked up, so a refusal
727
+ * says nothing about whether a user or key exists. Throws when a lookup fails.
728
+ */
729
+ async function decideSimulation(request) {
730
+ const { credential, caller, resolveUser, apiKeyStore, audit } = request;
731
+ const role = request.simulation.role?.trim();
732
+ const userId = request.simulation.userId?.trim();
733
+ const apiKeyId = request.simulation.apiKeyId?.trim();
734
+ const refuse = (reason, error) => {
735
+ if (audit) logger.warn("[Security Audit] Refused a request to simulate access", {
736
+ eventType: "auth.simulation.refused",
737
+ reason,
738
+ callerUid: caller?.uid,
739
+ simulation: request.simulation,
740
+ ...audit
741
+ });
742
+ return { refused: error };
743
+ };
744
+ if (role === "" || userId === "" || apiKeyId === "") return refuse("malformed", ApiError.badRequest("A simulation header was sent empty. Name a role, a user id or an API key id, or leave the header out.", "SIMULATION_INVALID"));
745
+ if (apiKeyId !== void 0 && (role !== void 0 || userId !== void 0)) return refuse("malformed", ApiError.badRequest(`${SIMULATE_API_KEY_ID_HEADER} runs as the key's own identity and roles, so it cannot be combined with ${SIMULATE_ROLE_HEADER} or ${SIMULATE_USER_ID_HEADER}.`, "SIMULATION_INVALID"));
746
+ if (credential !== "session" || !caller || !hasAdminRole(caller.roles)) return refuse(credential === "session" ? "not-admin" : credential, ApiError.forbidden(ADMIN_ONLY$1, "SIMULATION_FORBIDDEN"));
747
+ if (!resolveUser) return refuse("unavailable", new ApiError(501, "SIMULATION_UNAVAILABLE", "This backend's auth cannot confirm an administrator's roles: its auth adapter does not implement resolveUser."));
748
+ const self = await resolveUser(caller.uid);
749
+ if (!self || !hasAdminRole(self.roles)) return refuse("not-admin", ApiError.forbidden(ADMIN_ONLY$1, "SIMULATION_FORBIDDEN"));
750
+ let identity;
751
+ if (apiKeyId !== void 0) {
752
+ if (!apiKeyStore) return refuse("unavailable", new ApiError(501, "SIMULATION_UNAVAILABLE", "This backend has no API keys to simulate."));
753
+ const key = await apiKeyStore.getApiKeyById(apiKeyId);
754
+ const keyIdentity = key ? await identityOfKey(key, request.access) : void 0;
755
+ if (!key || !keyIdentity || !("uid" in keyIdentity)) return refuse("unknown-target", ApiError.notFound(`No active API key has the id "${apiKeyId}", so there is nothing to simulate.`, "SIMULATION_TARGET_NOT_FOUND"));
756
+ identity = {
757
+ uid: keyIdentity.uid,
758
+ roles: keyIdentity.roles,
759
+ isAnonymous: false,
760
+ scopes: keyIdentity.scopes,
761
+ apiKey: key,
762
+ role: keyIdentity.roles.join(",")
763
+ };
764
+ } else if (role !== void 0) identity = ANONYMOUS_ROLES.has(role) && userId === void 0 ? {
765
+ uid: ANONYMOUS_USER_ID,
766
+ roles: ["anon"],
767
+ isAnonymous: false,
768
+ role
769
+ } : {
770
+ uid: userId ?? "00000000-0000-0000-0000-000000000000",
771
+ roles: [role],
772
+ isAnonymous: false,
773
+ role
774
+ };
775
+ else {
776
+ const target = userId === void 0 ? null : await resolveUser(userId);
777
+ if (!target) return refuse("unknown-target", ApiError.notFound(`No active user has the id "${userId ?? ""}". Send ${SIMULATE_ROLE_HEADER} as well to simulate a uid that has no account.`, "SIMULATION_TARGET_NOT_FOUND"));
778
+ identity = {
779
+ uid: target.uid,
780
+ roles: target.roles,
781
+ isAnonymous: target.isAnonymous === true,
782
+ ...target.claims ? { claims: target.claims } : {},
783
+ role: target.roles.join(",")
784
+ };
785
+ }
786
+ if (audit) logger.info("[Security Audit] Ran a request with simulated access", {
787
+ eventType: "auth.simulation",
788
+ simulatorUid: caller.uid,
789
+ targetUid: identity.uid,
790
+ targetRoles: identity.roles,
791
+ ...identity.apiKey ? { apiKeyId: identity.apiKey.id } : {},
792
+ ...audit
793
+ });
794
+ return {
795
+ granted: identity,
796
+ simulator: caller.uid
797
+ };
798
+ }
799
+ /** The caller the auth middleware put on the context, narrowed. */
800
+ function callerOf$1(c) {
801
+ const user = c.get("user");
802
+ if (typeof user !== "object" || user === null) return void 0;
803
+ const uid = "uid" in user && typeof user.uid === "string" && user.uid !== "" ? user.uid : void 0;
804
+ if (uid === void 0) return void 0;
805
+ return {
806
+ uid,
807
+ roles: "roles" in user && Array.isArray(user.roles) ? user.roles.filter((entry) => typeof entry === "string") : []
808
+ };
809
+ }
810
+ /**
811
+ * Honour or refuse this request's simulation headers.
812
+ *
813
+ * Called by the data-plane auth middlewares once the caller is on the context
814
+ * and before impersonation, which it excludes. On a grant the context's
815
+ * `user`, `driver`, `scopes` and `apiKey` become the simulated identity's,
816
+ * `impersonator` names the administrator for the request log, and
817
+ * `simulation` records what to report on the response.
818
+ *
819
+ * @returns The refusal to send, or `undefined` to carry on.
820
+ */
821
+ async function applySimulation(c, { credential, driver, resolveUser, apiKeyStore }) {
822
+ const simulation = simulationOf((name) => c.req.header(name));
823
+ if (simulation === void 0) return void 0;
824
+ if (c.req.header("x-rebase-impersonate") !== void 0) return errorHandler(ApiError.badRequest(`A request either impersonates a user (${IMPERSONATE_HEADER}) or simulates access, not both.`, "SIMULATION_INVALID"), c);
825
+ let decision;
826
+ try {
827
+ decision = await decideSimulation({
828
+ simulation,
829
+ credential,
830
+ caller: callerOf$1(c),
831
+ resolveUser,
832
+ apiKeyStore,
833
+ access: accessOf(c),
834
+ audit: {
835
+ door: "http",
836
+ method: c.req.method,
837
+ path: c.req.path,
838
+ requestId: c.get("requestId")
839
+ }
840
+ });
841
+ } catch (error) {
842
+ return errorHandler(error instanceof Error ? error : new Error(String(error)), c);
843
+ }
844
+ if ("refused" in decision) return errorHandler(decision.refused, c);
845
+ const identity = decision.granted;
846
+ c.set("user", identity.uid === "anonymous" ? void 0 : {
847
+ uid: identity.uid,
848
+ roles: identity.roles,
849
+ isAnonymous: identity.isAnonymous
850
+ });
851
+ c.set("impersonator", { uid: decision.simulator });
852
+ c.set("scopes", identity.scopes);
853
+ c.set("apiKey", identity.apiKey);
854
+ c.set("simulation", {
855
+ role: identity.role,
856
+ sub: identity.uid,
857
+ roles: identity.roles,
858
+ ...identity.apiKey ? { apiKeyId: identity.apiKey.id } : {}
859
+ });
860
+ try {
861
+ c.set("driver", await scopeDataDriver(driver, {
862
+ uid: identity.uid,
863
+ roles: identity.roles,
864
+ isAnonymous: identity.isAnonymous,
865
+ ...identity.claims ? { claims: identity.claims } : {}
866
+ }));
867
+ } catch (error) {
868
+ logger.error("[AUTH] RLS scoping failed for a simulated request", { error });
869
+ return errorHandler(ApiError.internal("Internal authentication error"), c);
870
+ }
871
+ }
872
+ /**
873
+ * The fields of `collection` the roles cannot read: what the driver strips
874
+ * from every row it hands them. Fields hidden from everyone (`excludeFromApi`)
875
+ * are left out — no role sees those, so they say nothing about this one.
876
+ */
877
+ function maskedFieldsFor(collection, roles) {
878
+ const hiddenFromAll = apiHiddenFieldNames(collection);
879
+ return restrictedFieldNames(collection, { roles }, "read").declared.filter((name) => !hiddenFromAll.has(name));
880
+ }
881
+ /**
882
+ * Name what a simulated request ran as on its response
883
+ * ({@link SIMULATED_CONTEXT_HEADER}), refusals included.
884
+ *
885
+ * Mounted ahead of the data-plane groups; it reads, after the request is
886
+ * answered, what {@link applySimulation} and the REST generator recorded on
887
+ * the context — the identity, and which collection the request was about.
888
+ */
889
+ function reportSimulatedContext() {
890
+ return async (c, next) => {
891
+ await next();
892
+ const simulation = c.get("simulation");
893
+ if (!simulation) return;
894
+ const slug = c.get("collection");
895
+ const collection = slug ? c.get("services")?.collections().find((entry) => entry.slug === slug) : void 0;
896
+ const value = formatSimulatedContext({
897
+ role: simulation.role,
898
+ sub: simulation.sub,
899
+ maskedColumns: collection ? maskedFieldsFor(collection, simulation.roles) : [],
900
+ ...simulation.apiKeyId ? { apiKeyId: simulation.apiKeyId } : {},
901
+ ...slug ? { collection: slug } : {}
902
+ });
903
+ try {
904
+ c.res.headers.set(SIMULATED_CONTEXT_HEADER, value);
905
+ } catch {}
906
+ };
907
+ }
908
+ //#endregion
909
+ //#region src/auth/impersonation.ts
910
+ var ADMIN_ONLY = "Only a signed-in administrator can run a request as another user.";
911
+ var NOT_A_PERSON = {
912
+ "api-key": {
913
+ reason: "api-key",
914
+ message: "An API key cannot run a request as another user. Send it with an administrator's session instead."
915
+ },
916
+ "service-key": {
917
+ reason: "service-key",
918
+ message: "The service key cannot run a request as another user. Send it with an administrator's session instead."
919
+ },
920
+ none: {
921
+ reason: "anonymous",
922
+ message: ADMIN_ONLY
923
+ }
924
+ };
925
+ /**
926
+ * Decide whether this caller may act as the user they named, and who that
927
+ * user is now.
928
+ *
929
+ * The one decision both doors take — the HTTP data plane through
930
+ * {@link applyImpersonation}, the realtime socket at `AUTHENTICATE` and before
931
+ * every frame after it. The caller is judged before the named user is looked
932
+ * up, so the answer to a caller who may not impersonate says nothing about
933
+ * whether the uid exists.
934
+ *
935
+ * Throws when a lookup fails: that is not an answer about anyone, and each
936
+ * door refuses it its own way.
937
+ */
938
+ async function decideImpersonation(request) {
939
+ const { credential, caller, resolveUser, audit } = request;
940
+ const targetUid = request.requestedUid.trim();
941
+ const refuse = (reason, error) => {
942
+ if (audit) logger.warn("[Security Audit] Refused a request to act as another user", {
943
+ eventType: "auth.impersonation.refused",
944
+ reason,
945
+ callerUid: caller?.uid,
946
+ requestedUid: targetUid,
947
+ ...audit
948
+ });
949
+ return { refused: error };
950
+ };
951
+ if (targetUid === "") return refuse("malformed", ApiError.badRequest(`The ${IMPERSONATE_HEADER} header names no user. Send the uid of the user to act as, or leave the header out.`, "IMPERSONATION_INVALID"));
952
+ if (credential !== "session") {
953
+ const { reason, message } = NOT_A_PERSON[credential];
954
+ return refuse(reason, ApiError.forbidden(message, "IMPERSONATION_FORBIDDEN"));
955
+ }
956
+ if (!caller || !hasAdminRole(caller.roles)) return refuse("not-admin", ApiError.forbidden(ADMIN_ONLY, "IMPERSONATION_FORBIDDEN"));
957
+ if (!resolveUser) return refuse("unavailable", new ApiError(501, "IMPERSONATION_UNAVAILABLE", "This backend's auth cannot run a request as another user: its auth adapter does not implement resolveUser."));
958
+ const self = await resolveUser(caller.uid);
959
+ if (!self || !hasAdminRole(self.roles)) return refuse("not-admin", ApiError.forbidden(ADMIN_ONLY, "IMPERSONATION_FORBIDDEN"));
960
+ const target = await resolveUser(targetUid);
961
+ if (!target) return refuse("unknown-target", ApiError.notFound(`No active user has the id "${targetUid}", so there is nobody to run this request as.`, "IMPERSONATION_TARGET_NOT_FOUND"));
962
+ if (audit) logger.info("[Security Audit] Ran a request as another user", {
963
+ eventType: "auth.impersonation",
964
+ impersonatorUid: caller.uid,
965
+ targetUid: target.uid,
966
+ targetRoles: target.roles,
967
+ ...audit
968
+ });
969
+ return {
970
+ granted: target,
971
+ impersonator: caller.uid
972
+ };
973
+ }
974
+ /** The caller the auth middleware put on the context, narrowed. */
975
+ function callerOf(c) {
976
+ const user = c.get("user");
977
+ if (typeof user !== "object" || user === null) return void 0;
978
+ const uid = "uid" in user && typeof user.uid === "string" && user.uid !== "" ? user.uid : void 0;
979
+ if (uid === void 0) return void 0;
980
+ return {
981
+ uid,
982
+ roles: "roles" in user && Array.isArray(user.roles) ? user.roles.filter((role) => typeof role === "string") : []
983
+ };
984
+ }
985
+ /**
986
+ * Honour or refuse this request's {@link IMPERSONATE_HEADER}.
987
+ *
988
+ * Called by the data-plane auth middlewares once the caller is on the context
989
+ * and its driver is scoped. When the header names a user the caller may act
990
+ * as, the context's `user` and `driver` are replaced with that user's and
991
+ * `impersonator` names the caller.
992
+ *
993
+ * @returns The refusal to send, or `undefined` to carry on — as the caller
994
+ * when the header is absent, as the named user when it was granted.
995
+ */
996
+ async function applyImpersonation(c, { credential, driver, resolveUser }) {
997
+ const requested = c.req.header(IMPERSONATE_HEADER);
998
+ if (requested === void 0) return void 0;
999
+ const caller = callerOf(c);
1000
+ let decision;
1001
+ try {
1002
+ decision = await decideImpersonation({
1003
+ requestedUid: requested,
1004
+ credential,
1005
+ caller,
1006
+ resolveUser,
1007
+ audit: {
1008
+ door: "http",
1009
+ method: c.req.method,
1010
+ path: c.req.path,
1011
+ requestId: c.get("requestId")
1012
+ }
1013
+ });
1014
+ } catch (error) {
1015
+ return errorHandler(error instanceof Error ? error : new Error(String(error)), c);
1016
+ }
1017
+ if ("refused" in decision) return errorHandler(decision.refused, c);
1018
+ const target = decision.granted;
1019
+ const isAnonymous = target.isAnonymous === true;
1020
+ c.set("user", {
1021
+ uid: target.uid,
1022
+ email: target.email,
1023
+ roles: target.roles,
1024
+ isAnonymous
1025
+ });
1026
+ c.set("impersonator", { uid: decision.impersonator });
1027
+ try {
1028
+ c.set("driver", await scopeDataDriver(driver, {
1029
+ uid: target.uid,
1030
+ roles: target.roles,
1031
+ isAnonymous,
1032
+ ...target.claims ? { claims: target.claims } : {}
1033
+ }));
1034
+ } catch (error) {
1035
+ logger.error("[AUTH] RLS scoping failed for an impersonated request", { error });
1036
+ return errorHandler(ApiError.internal("Internal authentication error"), c);
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Refuse {@link IMPERSONATE_HEADER} — and the RLS simulator's
1041
+ * `x-rebase-simulate-*` headers (`auth/simulation.ts`) — on every route that
1042
+ * does not honour it.
1043
+ *
1044
+ * Only the data API and custom functions run a request as another user; the
1045
+ * rest — storage, auth, the admin surfaces — authenticate the caller and
1046
+ * ignore the header. Ignored, a request asking to act as B would be answered
1047
+ * as the administrator who sent it, which is the one outcome impersonation
1048
+ * must never have. Mounted ahead of every route, so a surface added later is
1049
+ * refused until it is listed here as honouring the header.
1050
+ *
1051
+ * @param honouredMounts - The mount points whose auth middleware applies the
1052
+ * header (`/api/data`, `/api/functions`). A path is under one when it
1053
+ * is the mount itself or continues it with a `/`.
1054
+ */
1055
+ function refuseUnhonouredImpersonation(honouredMounts) {
1056
+ return async (c, next) => {
1057
+ const impersonating = c.req.header(IMPERSONATE_HEADER) !== void 0;
1058
+ const simulating = carriesSimulation((name) => c.req.header(name));
1059
+ if (!impersonating && !simulating) return next();
1060
+ const path = c.req.path;
1061
+ if (honouredMounts.some((mount) => path === mount || path.startsWith(`${mount}/`))) return next();
1062
+ logger.warn(impersonating ? "[Security Audit] Refused a request to act as another user" : "[Security Audit] Refused a request to simulate access", {
1063
+ eventType: impersonating ? "auth.impersonation.refused" : "auth.simulation.refused",
1064
+ reason: "unsupported-route",
1065
+ method: c.req.method,
1066
+ path,
1067
+ requestId: c.get("requestId")
1068
+ });
1069
+ return errorHandler(impersonating ? ApiError.badRequest(`This route cannot run as another user, so it refuses the ${IMPERSONATE_HEADER} header. Only the data API and custom functions honour it.`, "IMPERSONATION_UNSUPPORTED") : ApiError.badRequest("This route cannot run with simulated access, so it refuses the x-rebase-simulate-* headers. Only the data API and custom functions honour them.", "SIMULATION_UNSUPPORTED"), c);
1070
+ };
1071
+ }
1072
+ //#endregion
1073
+ //#region src/storage/requested-object.ts
1074
+ /**
1075
+ * Which object a storage request's URL path names.
1076
+ *
1077
+ * Three parties read the path of a `GET /file/*`, `GET /metadata/*` or
1078
+ * `DELETE /file/*`: the route, which serves or deletes an object;
1079
+ * `publicObjectAuth`, which lets an anonymous caller through when that object
1080
+ * is public; and `fileTokenAuth`, which compares it with the path a download
1081
+ * token grants. A decision is only about the object acted on if it is made on
1082
+ * the same key, so all three derive it here — the wildcard, the decoding, the
1083
+ * bucket segment and the canonical key — and none of them spells a step of its
1084
+ * own.
1085
+ *
1086
+ * They used to. `publicObjectAuth` judged the raw path with a check that strips
1087
+ * everything up to a `://`, while the route canonicalized it, folding `//` to
1088
+ * `/`: `GET /file/notes://public/secret.txt` was public to the one and served
1089
+ * the private key `notes:/public/secret.txt` from the other, with the authorize
1090
+ * hook never asked.
1091
+ *
1092
+ * Nothing here parses a scheme. A request path is a key, optionally behind the
1093
+ * one bucket segment the routes recognise; a `://` in it is a `:` followed by
1094
+ * an empty segment, and canonicalization folds the empty segment away.
1095
+ */
1096
+ /**
1097
+ * The wildcard part of a storage route's path, still percent-encoded.
1098
+ *
1099
+ * Hono's `c.req.param('*')` does not work reliably in sub-routers mounted
1100
+ * via `app.route(prefix, subRouter)`. Instead it is derived from the
1101
+ * fully-resolved `c.req.path` and `c.req.routePath`.
1102
+ *
1103
+ * For a route `/metadata/*` mounted at `/api/storage`, a request to
1104
+ * `/api/storage/metadata/default/file.jpg` yields routePath
1105
+ * `/api/storage/metadata/*`. The prefix (everything before `/*`) is stripped,
1106
+ * plus one character for the trailing `/`, to obtain `default/file.jpg`.
1107
+ */
1108
+ function storageRequestWildcard(c) {
1109
+ const prefix = c.req.routePath.replace("/*", "");
1110
+ const fullPath = c.req.path;
1111
+ const idx = fullPath.indexOf(prefix);
1112
+ if (idx < 0) return "";
1113
+ return fullPath.substring(idx + prefix.length + 1);
1114
+ }
1115
+ /**
1116
+ * The object a decoded path names: a key, behind an optional `default/` bucket
1117
+ * segment (matched case-insensitively). Throws {@link InvalidStorageKeyError}
1118
+ * when the key cannot be canonicalized.
1119
+ *
1120
+ * For a path that is already text — the folder route's JSON body. A URL path
1121
+ * goes through {@link requestedStorageObject}, which decodes it first.
1122
+ */
1123
+ function storageObjectOfPath(decodedPath) {
1124
+ const parts = decodedPath.split("/");
1125
+ return {
1126
+ bucket: "default",
1127
+ key: canonicalStorageKey(parts.length > 1 && parts[0].toLowerCase() === "default" ? parts.slice(1).join("/") : decodedPath)
1128
+ };
1129
+ }
1130
+ /**
1131
+ * The object a request's wildcard path names, or {@link InvalidStorageKeyError}.
1132
+ *
1133
+ * The wildcard arrives as Hono leaves it — `decodeURI`d, with the reserved
1134
+ * characters (`/`, `:`, `%25` among them) still escaped — so one
1135
+ * `decodeURIComponent` yields the path the client encoded. A `%` that does not
1136
+ * begin an escape names no key, and is refused like any other key that cannot be
1137
+ * canonicalized rather than thrown as a `URIError`.
1138
+ */
1139
+ function requestedStorageObject(wildcard) {
1140
+ let decoded;
1141
+ try {
1142
+ decoded = decodeURIComponent(wildcard);
1143
+ } catch {
1144
+ throw new InvalidStorageKeyError("Storage path is not valid percent-encoding: a '%' must begin an escape such as '%25' (a literal '%').");
1145
+ }
1146
+ return storageObjectOfPath(decoded);
1147
+ }
1148
+ /**
1149
+ * {@link requestedStorageObject}, or `null` when the path names no object.
1150
+ *
1151
+ * For the middleware, which must fail closed without an exception: a path that
1152
+ * names no object is neither public nor covered by any download token, and the
1153
+ * route answers it with a 400 of its own.
1154
+ */
1155
+ function tryRequestedStorageObject(wildcard) {
1156
+ try {
1157
+ return requestedStorageObject(wildcard);
1158
+ } catch (err) {
1159
+ if (err instanceof InvalidStorageKeyError) return null;
1160
+ throw err;
1161
+ }
1162
+ }
1163
+ //#endregion
1164
+ //#region src/storage/request-principals.ts
1165
+ /**
1166
+ * The two principals a storage read can be granted without a user: the reader
1167
+ * of a declared-public object, and the bearer of a download token.
1168
+ *
1169
+ * `checkAuthorized` does not ask the `storageAuthorize` hook about either — the
1170
+ * public prefix and the token are the authorization. That skip used to be
1171
+ * decided on the principal's `uid` string (`"public"`, `"download-token"`), so
1172
+ * any identity carrying that uid skipped the hook: a custom `AuthAdapter` whose
1173
+ * `verifyRequest` returns `{ uid: "public" }` for a guest could then upload,
1174
+ * delete and list anywhere, with the hook never asked.
1175
+ *
1176
+ * Each principal is now one object, set only by the middleware that granted
1177
+ * it (`publicObjectAuth`, `fileTokenAuth`) and recognised by identity. An
1178
+ * adapter builds a new object per request, so it can name the uid but never be
1179
+ * the grant.
1180
+ */
1181
+ /** The principal `publicObjectAuth` grants an anonymous read of a public object. */
1182
+ var PUBLIC_OBJECT_PRINCIPAL = Object.freeze({
1183
+ uid: "public",
1184
+ roles: ["public"]
1185
+ });
1186
+ /** The principal `fileTokenAuth` grants a read covered by a valid download token. */
1187
+ var DOWNLOAD_TOKEN_PRINCIPAL = Object.freeze({
1188
+ uid: "download-token",
1189
+ roles: ["reader"]
1190
+ });
1191
+ /** Whether `user` is the grant `publicObjectAuth` made — the object, not the uid. */
1192
+ function isPublicObjectGrant(user) {
1193
+ return user === PUBLIC_OBJECT_PRINCIPAL;
1194
+ }
1195
+ /** Whether `user` is the grant `fileTokenAuth` made — the object, not the uid. */
1196
+ function isDownloadTokenGrant(user) {
1197
+ return user === DOWNLOAD_TOKEN_PRINCIPAL;
1198
+ }
1199
+ //#endregion
1200
+ //#region src/auth/middleware.ts
1201
+ /**
1202
+ * Answer a refusal with the canonical envelope, through the same formatter
1203
+ * every route uses.
1204
+ *
1205
+ * These middlewares used to hand-build `c.json({ error: { message, code } },
1206
+ * 401)` — seven near-copies of it, which is how they came to omit `requestId`
1207
+ * while every other error on the server carried it: the one string a bug report
1208
+ * quotes was missing from precisely the errors an app hits most. It also meant
1209
+ * a routine 401 (a client refreshing before it knows whether a session exists)
1210
+ * logged like an incident, because `ApiError.expected` never reached the
1211
+ * logger.
1212
+ *
1213
+ * `errorHandler` is *called* rather than thrown to. A throw only becomes an
1214
+ * envelope where an `onError` is registered, and these middlewares are mounted
1215
+ * by callers this package does not control — `defineFunction` hands users a
1216
+ * Hono app and invites them to put `requireAuth` on a route of it. Making the
1217
+ * status depend on the mount point would turn a 401 into a 500 for exactly
1218
+ * those callers. Calling the formatter directly gets the envelope with none of
1219
+ * that.
1220
+ */
1221
+ function refuse(c, error) {
1222
+ return errorHandler(error, c);
1223
+ }
1224
+ /**
1225
+ * Hono middleware that requires a valid JWT token via Authorization header.
1226
+ * Returns 401 if token is missing or invalid.
1227
+ *
1228
+ * **Security:** Tokens are only accepted via the `Authorization: Bearer`
1229
+ * header. Query-string tokens (`?token=`) are intentionally NOT accepted
1230
+ * here because URLs leak into access logs, proxies, Referer headers, and
1231
+ * browser history. Use {@link queryTokenAuth} on routes that legitimately
1232
+ * need query-string tokens (e.g. storage file serving for `<img src>`).
1233
+ */
1234
+ var requireAuth = async (c, next) => {
1235
+ if (c.get("user")) return next();
1236
+ const token = extractBearerToken(c.req.header("authorization"));
1237
+ if (token === void 0) return refuse(c, ApiError.unauthenticated("Authorization header missing or invalid"));
1238
+ const payload = await jwtOf(c).verifyAccessToken(token);
1239
+ if (!payload) return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
1240
+ c.set("user", payload);
1241
+ return next();
1242
+ };
1243
+ /**
1244
+ * Factory that creates a requireAuth middleware with optional service key support.
1245
+ *
1246
+ * When `serviceKey` is provided, the middleware will check if the Bearer token
1247
+ * matches the service key using constant-time comparison. If it matches, the
1248
+ * request is authenticated as a service user with admin privileges.
1249
+ *
1250
+ * This allows admin routes (which use standalone requireAuth + requireAdmin)
1251
+ * to be accessed via service keys for scripts and server-to-server calls.
1252
+ */
1253
+ function createRequireAuth(options) {
1254
+ if (!options?.serviceKey && !options?.resolveRoles && !options?.revocationRepo) return requireAuth;
1255
+ const key = options.serviceKey;
1256
+ const resolveRoles = options.resolveRoles;
1257
+ const revocationRepo = options.revocationRepo;
1258
+ return async (c, next) => {
1259
+ if (c.get("user")) return next();
1260
+ const token = extractBearerToken(c.req.header("authorization"));
1261
+ if (token === void 0) return refuse(c, ApiError.unauthenticated("Authorization header missing or invalid"));
1262
+ if (key !== void 0 && safeCompare(token, key)) {
1263
+ c.set("user", {
1264
+ uid: "service",
1265
+ roles: ["admin"]
1266
+ });
1267
+ return next();
1268
+ }
1269
+ const payload = await jwtOf(c).verifyAccessToken(token);
1270
+ if (!payload) return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
1271
+ if (resolveRoles || revocationRepo) try {
1272
+ let judgedRoles;
1273
+ if (revocationRepo) {
1274
+ const verdict = await judgeAccessToken(revocationRepo, payload);
1275
+ if (!verdict.live) return refuse(c, accessTokenRefusalError(verdict.refusal));
1276
+ judgedRoles = verdict.roles;
1277
+ }
1278
+ c.set("user", resolveRoles ? {
1279
+ ...payload,
1280
+ roles: judgedRoles ?? await resolveRoles(payload.uid)
1281
+ } : payload);
1282
+ return next();
1283
+ } catch (error) {
1284
+ if (isDatabaseUnreachable(error)) {
1285
+ if (!warnIfDatabaseClosed(error)) logger.warn("[Auth] Could not resolve roles for an admin-gated request: the database is unreachable", { uid: payload.uid });
1286
+ return refuse(c, ApiError.databaseUnavailable());
1287
+ }
1288
+ logger.warn("[Auth] Could not resolve roles for an admin-gated request", {
1289
+ uid: payload.uid,
1290
+ error
1291
+ });
1292
+ return refuse(c, ApiError.serviceUnavailable("Could not verify your permissions. Please try again.", "ROLE_LOOKUP_FAILED"));
1293
+ }
1294
+ c.set("user", payload);
1295
+ return next();
1296
+ };
1297
+ }
1298
+ /**
1299
+ * Middleware that requires the user to hold the `admin` role, which holds
1300
+ * every scope. Must be used AFTER requireAuth or on a route where user is
1301
+ * guaranteed.
1302
+ *
1303
+ * Prefer `requireScope` from `./access` for anything a scope names: a scope
1304
+ * can be granted to a narrower role and to a key, and `admin` cannot.
1305
+ */
1306
+ var requireAdmin = async (c, next) => {
1307
+ const user = c.get("user");
1308
+ if (!user) return refuse(c, ApiError.unauthorized("User not authenticated. requireAuth middleware is missing?"));
1309
+ if (!hasAdminRole(typeof user === "object" && user !== null && "roles" in user && Array.isArray(user.roles) ? user.roles.filter((role) => typeof role === "string") : [])) return refuse(c, ApiError.forbidden("Admin privileges required for this operation"));
1310
+ return next();
1311
+ };
1312
+ /**
1313
+ * Middleware that optionally extracts user from JWT via Authorization header.
1314
+ * Does not return 401 if token is missing — allows anonymous access.
1315
+ *
1316
+ * Query-string tokens are NOT accepted here. Use {@link queryTokenAuth}
1317
+ * on routes that need them.
1318
+ */
1319
+ var optionalAuth = async (c, next) => {
1320
+ if (c.get("user")) return next();
1321
+ const token = extractBearerToken(c.req.header("authorization"));
1322
+ const jwt = jwtOf(c);
1323
+ if (token !== void 0 && jwt.isConfigured()) {
1324
+ const payload = await jwt.verifyAccessToken(token);
1325
+ if (payload) c.set("user", payload);
1326
+ }
1327
+ return next();
1328
+ };
1329
+ /**
1330
+ * Who a session token names, as `jwt` verifies it — or "nobody" rather than a
1331
+ * throw on a backend with no JWT of its own, so a socket closes as
1332
+ * unauthenticated, which is the outcome this describes.
1333
+ */
1334
+ async function userFromToken(jwt, token) {
1335
+ if (!jwt.isConfigured()) return null;
1336
+ return jwt.verifyAccessToken(token);
1337
+ }
1338
+ /**
1339
+ * Extract user from token - for WebSocket authentication. Answers for the
1340
+ * process default; a backend's own socket verifies with its own keys.
1341
+ */
1342
+ async function extractUserFromToken(token) {
1343
+ return userFromToken(processJwtService, token);
1344
+ }
1345
+ /**
1346
+ * Create a configurable auth middleware that handles:
1347
+ * 1. Token extraction (via custom validator or JWT Bearer token)
1348
+ * 2. RLS-scoped DataDriver via withAuth()
1349
+ * 3. Enforcement (401 when requireAuth is true and no user)
1350
+ *
1351
+ * **Secure by default:** `requireAuth` defaults to `true`. Anonymous
1352
+ * access is only allowed when the developer explicitly opts out by
1353
+ * setting `requireAuth: false`, indicating that Postgres RLS policies
1354
+ * fully control access.
1355
+ *
1356
+ * **Fail-closed:** The raw unscoped driver is never placed in the
1357
+ * request context. Every code path either scopes via `withAuth()` or
1358
+ * rejects the request. This prevents silent RLS bypass.
1359
+ *
1360
+ * This is the single source of truth for HTTP auth in Rebase.
1361
+ * Use this instead of manually parsing tokens in route handlers.
1362
+ */
1363
+ function createAuthMiddleware(options) {
1364
+ const { driver: baseDriver, resolveDriver, requireAuth: enforceAuth = true, validator, serviceKey, apiKeyStore } = options;
1365
+ return async (c, next) => {
1366
+ const driver = resolveDriver ? resolveDriver(c) : baseDriver;
1367
+ let credential = "none";
1368
+ if (validator) try {
1369
+ const authResult = await validator(c);
1370
+ if (authResult && typeof authResult === "object") {
1371
+ const id = "uid" in authResult ? authResult.uid : void 0;
1372
+ if (id) {
1373
+ const roles = authResult.roles || [];
1374
+ credential = "session";
1375
+ c.set("user", {
1376
+ uid: id,
1377
+ roles
1378
+ });
1379
+ const user = {
1380
+ uid: id,
1381
+ roles,
1382
+ ...authResult
1383
+ };
1384
+ c.set("driver", await scopeDataDriver(driver, user));
1385
+ } else c.set("driver", await scopeDataDriver(driver, {
1386
+ uid: ANONYMOUS_USER_ID,
1387
+ roles: ["anon"]
1388
+ }));
1389
+ } else if (authResult === true) {
1390
+ credential = "session";
1391
+ c.set("user", {
1392
+ uid: "default",
1393
+ roles: []
1394
+ });
1395
+ c.set("driver", await scopeDataDriver(driver, {
1396
+ uid: "default",
1397
+ roles: []
1398
+ }));
1399
+ } else c.set("driver", await scopeDataDriver(driver, {
1400
+ uid: ANONYMOUS_USER_ID,
1401
+ roles: ["anon"]
1402
+ }));
1403
+ } catch (error) {
1404
+ return refuse(c, ApiError.unauthenticated("Unauthorized"));
1405
+ }
1406
+ else {
1407
+ const token = extractBearerToken(c.req.header("authorization"));
1408
+ if (token !== void 0) if (serviceKey && safeCompare(token, serviceKey)) {
1409
+ const serviceUser = {
1410
+ uid: "service",
1411
+ roles: ["admin"]
1412
+ };
1413
+ credential = "service-key";
1414
+ c.set("user", serviceUser);
1415
+ try {
1416
+ c.set("driver", await scopeDataDriver(driver, {
1417
+ uid: "service",
1418
+ roles: ["admin"]
1419
+ }));
1420
+ } catch (error) {
1421
+ logger.error("[AUTH] RLS scoping failed for service key", { error });
1422
+ return c.json({ error: {
1423
+ message: "Internal authentication error",
1424
+ code: "INTERNAL_ERROR"
1425
+ } }, 500);
1426
+ }
1427
+ } else if (apiKeyStore && isApiKeyToken(token)) {
1428
+ const result = await validateApiKey(c, token, {
1429
+ store: apiKeyStore,
1430
+ driver
1431
+ });
1432
+ if (result !== true) return result;
1433
+ credential = "api-key";
1434
+ } else {
1435
+ const payload = await userFromToken(jwtOf(c), token);
1436
+ if (payload) {
1437
+ credential = "session";
1438
+ c.set("user", payload);
1439
+ try {
1440
+ const user = {
1441
+ uid: payload.uid,
1442
+ roles: payload.roles,
1443
+ isAnonymous: payload.isAnonymous === true,
1444
+ claims: payload.claims
1445
+ };
1446
+ c.set("driver", await scopeDataDriver(driver, user));
1447
+ } catch (error) {
1448
+ logger.error("[AUTH] RLS scoping failed for authenticated user", { error });
1449
+ return c.json({ error: {
1450
+ message: "Internal authentication error",
1451
+ code: "INTERNAL_ERROR"
1452
+ } }, 500);
1453
+ }
1454
+ } else return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
1455
+ }
1456
+ else try {
1457
+ c.set("driver", await scopeDataDriver(driver, {
1458
+ uid: ANONYMOUS_USER_ID,
1459
+ roles: ["anon"]
1460
+ }));
1461
+ } catch (error) {
1462
+ logger.error("[AUTH] Failed to create anon-scoped driver", { error });
1463
+ return c.json({ error: {
1464
+ message: "Server configuration error",
1465
+ code: "INTERNAL_ERROR"
1466
+ } }, 500);
1467
+ }
1468
+ }
1469
+ const refusedSimulation = await applySimulation(c, {
1470
+ credential,
1471
+ driver,
1472
+ apiKeyStore
1473
+ });
1474
+ if (refusedSimulation) return refusedSimulation;
1475
+ const refused = await applyImpersonation(c, {
1476
+ credential,
1477
+ driver
1478
+ });
1479
+ if (refused) return refused;
1480
+ if (enforceAuth && !c.get("user") && !c.get("simulation")) return refuse(c, ApiError.unauthenticated("Unauthorized: Authentication required. This is the API-level gate, not a row rule — a collection granting public reads still needs a caller. Set AUTH_REQUIRE=false (or `auth.requireAuth: false`) to let RLS alone decide."));
1481
+ return next();
1482
+ };
1483
+ }
1484
+ /**
1485
+ * Middleware that authenticates via a `?token=` query parameter.
1486
+ *
1487
+ * **Use sparingly.** Tokens in URLs leak into access logs, proxy logs,
1488
+ * Referer headers, and browser history. This middleware exists solely for
1489
+ * routes where the consumer cannot set HTTP headers — e.g. `<img src>`,
1490
+ * `<a href>` for file downloads, or similar browser-native requests.
1491
+ *
1492
+ * Apply it **before** `requireAuth` or `optionalAuth` on the specific
1493
+ * route that needs it. Those middlewares will see the user context this
1494
+ * middleware sets and skip their own 401 check.
1495
+ *
1496
+ * @example
1497
+ * ```ts
1498
+ * router.get("/file/*", queryTokenAuth, readAuthMiddleware, handler);
1499
+ * ```
1500
+ */
1501
+ var queryTokenAuth = async (c, next) => {
1502
+ if (extractBearerToken(c.req.header("authorization")) !== void 0) return next();
1503
+ const queryToken = c.req.query("token");
1504
+ const jwt = jwtOf(c);
1505
+ if (queryToken && jwt.isConfigured()) {
1506
+ const payload = await jwt.verifyAccessToken(queryToken);
1507
+ if (payload) c.set("user", payload);
1508
+ }
1509
+ return next();
1510
+ };
1511
+ /**
1512
+ * Authorizes anonymous access to **public** storage objects (those under the
1513
+ * public prefix), which are served token-less via stable, permanent URLs.
1514
+ *
1515
+ * Runs on the storage `/file/*` and `/metadata/*` routes, *after* the token
1516
+ * middleware and *before* the read-auth gate. If the request is already
1517
+ * authenticated (Bearer or scoped token), it does nothing. Otherwise, when the
1518
+ * requested object path is public, it sets a minimal "public" principal so the
1519
+ * downstream `requireAuth` gate lets the read through. Private paths are left
1520
+ * untouched, so they still require a valid token.
1521
+ *
1522
+ * Public means the object the route will serve is public, so the decision is
1523
+ * made on that object's canonical key, derived by the route's own function
1524
+ * (`requestedStorageObject`). It used to be made on the raw path, through a
1525
+ * check that strips a `scheme://`: `notes://public/secret.txt` read as
1526
+ * `public/secret.txt` here while the route served the private
1527
+ * `notes:/public/secret.txt`, and the authorize hook — not asked about this
1528
+ * principal — never saw it.
1529
+ */
1530
+ var publicObjectAuth = async (c, next) => {
1531
+ if (c.get("user")) return next();
1532
+ const requested = tryRequestedStorageObject(storageRequestWildcard(c));
1533
+ if (requested && isPublicStorageKey(requested.key)) c.set("user", PUBLIC_OBJECT_PRINCIPAL);
1534
+ return next();
1535
+ };
1536
+ /**
1537
+ * Helper to match paths for scoped file tokens.
1538
+ * Matches exact paths or prefixes (exact folder prefixes or ending with /).
1539
+ *
1540
+ * The traversal check is defence in depth, not the load-bearing defence.
1541
+ *
1542
+ * A prefix comparison is a lie for any path containing `..`: `user1/../user2/x`
1543
+ * starts with `user1/`, so a token scoped to `user1/` would authorize reading
1544
+ * user2's file — and the storage controller would not object, because its own
1545
+ * guard stops a path escaping the *bucket*, and that one resolves to `user2/x`,
1546
+ * comfortably inside it. The grant is what would be escaped, and this is the
1547
+ * only layer that knows what was granted.
1548
+ *
1549
+ * Nothing arrives here in that shape today: the URL parser resolves `..` (and
1550
+ * `%2e%2e`, which the WHATWG spec treats as a dot for normalization) before
1551
+ * Hono routes the request, so this comparison already runs on a resolved path.
1552
+ * But that is a guarantee of the runtime rather than of this code, and it is
1553
+ * one line to not depend on it. The same rule already guards the public-object
1554
+ * path — see `isPublicStorageKey`.
1555
+ */
1556
+ function isPathMatch(requested, allowed) {
1557
+ if (requested.split("/").some((seg) => seg === "..")) return false;
1558
+ if (requested === allowed) return true;
1559
+ if (allowed.endsWith("/")) return requested.startsWith(allowed);
1560
+ return requested.startsWith(allowed + "/");
1561
+ }
1562
+ /**
1563
+ * Middleware that authenticates file-serving routes using scoped download tokens.
1564
+ * It enforces that only scoped "file-read" tokens can access "/file/*" and "?token=" query params.
1565
+ * Full access JWTs are explicitly rejected on "/file/*" routes, and in the "?token=" query param.
1566
+ */
1567
+ var fileTokenAuth = async (c, next) => {
1568
+ const isFileRoute = c.req.path.includes("/storage/file/");
1569
+ const authHeader = c.req.header("authorization");
1570
+ const queryToken = c.req.query("token");
1571
+ /**
1572
+ * Decide what a valid download token entitles this request to.
1573
+ *
1574
+ * One function rather than a copy per token location: a Bearer token and a
1575
+ * `?token=` are the same grant presented two ways, and the whole failure
1576
+ * this guards against is two places disagreeing about what was granted.
1577
+ *
1578
+ * `"no-path"` is distinct from the two denials because the caller treats it
1579
+ * differently — a request with no object path at all is not a denied read,
1580
+ * it is a request this middleware has nothing to say about, and on
1581
+ * `/metadata/*` it must still reach the downstream auth gate.
1582
+ */
1583
+ const evaluateGrant = (payload) => {
1584
+ if (canonicalStorageId(c.req.query("storageId")) !== payload.storageId) return "deny-storage";
1585
+ const rawPath = storageRequestWildcard(c);
1586
+ if (!rawPath) return "no-path";
1587
+ const requested = tryRequestedStorageObject(rawPath);
1588
+ if (requested === null) return "deny-path";
1589
+ return isPathMatch(`${requested.bucket}/${requested.key}`, payload.path) ? "grant" : "deny-path";
1590
+ };
1591
+ /**
1592
+ * Which half of the scope failed is named in the message. The holder of the
1593
+ * token already knows both values, so this reveals nothing to an attacker —
1594
+ * and without it, a client that forgets to forward `?storageId=` on the file
1595
+ * URL reports a "path mismatch" for a path that matches perfectly.
1596
+ */
1597
+ const denyMismatch = (outcome) => refuse(c, ApiError.forbidden(outcome === "deny-storage" ? "Forbidden: Scoped token storage mismatch" : "Forbidden: Scoped token path mismatch"));
1598
+ const bearerToken = extractBearerToken(authHeader);
1599
+ if (bearerToken !== void 0) {
1600
+ const token = bearerToken;
1601
+ const payload = await jwtOf(c).verifyDownloadToken(token);
1602
+ if (payload) {
1603
+ const outcome = evaluateGrant(payload);
1604
+ if (outcome === "grant") {
1605
+ c.set("user", DOWNLOAD_TOKEN_PRINCIPAL);
1606
+ return next();
1607
+ }
1608
+ if (outcome !== "no-path") return denyMismatch(outcome);
1609
+ }
1610
+ if (isFileRoute) return refuse(c, ApiError.unauthenticated("Unauthorized: Access JWT not allowed on file routes"));
1611
+ return next();
1612
+ }
1613
+ if (queryToken) {
1614
+ const payload = await jwtOf(c).verifyDownloadToken(queryToken);
1615
+ if (payload) {
1616
+ const outcome = evaluateGrant(payload);
1617
+ if (outcome === "grant") {
1618
+ c.set("user", DOWNLOAD_TOKEN_PRINCIPAL);
1619
+ return next();
1620
+ }
1621
+ if (outcome !== "no-path") return denyMismatch(outcome);
1622
+ }
1623
+ return refuse(c, ApiError.unauthenticated("Unauthorized: Invalid or unauthorized token"));
1624
+ }
1625
+ return next();
1626
+ };
1627
+ //#endregion
1628
+ //#region src/auth/access.ts
1629
+ /**
1630
+ * How a personal key finds its owner in an account store: the account as it
1631
+ * is now — a missing or disabled one is no owner — and its roles.
1632
+ *
1633
+ * One read where the store answers both at once: `getAccountAccessState`, the
1634
+ * statement every access token is already judged against, which reads roles
1635
+ * and `disabled_at` together. Otherwise the account, then its roles — the two
1636
+ * reads this always made, one round trip more per request.
1637
+ */
1638
+ function keyOwnerResolverFor(repo) {
1639
+ if (typeof repo.getAccountAccessState === "function") {
1640
+ const readAccount = repo.getAccountAccessState.bind(repo);
1641
+ return async (uid) => {
1642
+ const account = await readAccount(uid);
1643
+ if (!account || account.disabled) return null;
1644
+ return { roles: account.roles };
1645
+ };
1646
+ }
1647
+ return async (uid) => {
1648
+ const user = await repo.getUserById(uid);
1649
+ if (!user || user.disabled) return null;
1650
+ return { roles: await repo.getUserRoleIds(uid) };
1651
+ };
1652
+ }
1653
+ /**
1654
+ * What the module-level API answers for: {@link getAccessModel},
1655
+ * {@link getKeyOwnerResolver}, and every scope check on a route mounted
1656
+ * outside any backend. Built-ins only until {@link configureAccess} or a
1657
+ * backend installed as the process default sets it.
1658
+ */
1659
+ var processAccess = { model: EMPTY_ACCESS_MODEL };
1660
+ /** The process default's access model. Built-ins only until one is configured. */
1661
+ function getAccessModel() {
1662
+ return processAccess.model;
1663
+ }
1664
+ /**
1665
+ * Set the process default's access model, and — when the app enables
1666
+ * `auth.personalKeys` and has an account store to read owners from — how
1667
+ * personal keys find their owner. For a test, or a server wired by hand that
1668
+ * uses the module-level API; a backend built by `createRebaseBackend` carries
1669
+ * its own.
1670
+ */
1671
+ function configureAccess(options) {
1672
+ processAccess = {
1673
+ model: options.model,
1674
+ resolveKeyOwner: options.resolveKeyOwner
1675
+ };
1676
+ }
1677
+ /** @internal See {@link defaultAccessControl}. */
1678
+ function setDefaultAccessControl(access) {
1679
+ processAccess = access;
1680
+ }
1681
+ /**
1682
+ * The process default as an {@link AccessControl} that follows it — what a
1683
+ * route or a key check built outside any backend falls back to.
1684
+ */
1685
+ var processAccessControl = {
1686
+ get model() {
1687
+ return processAccess.model;
1688
+ },
1689
+ get resolveKeyOwner() {
1690
+ return processAccess.resolveKeyOwner;
1691
+ }
1692
+ };
1693
+ /**
1694
+ * The access control serving this request: the backend's whose route group is
1695
+ * answering it, or the process default for a route mounted outside any.
1696
+ */
1697
+ function accessOf(c) {
1698
+ return c.get("services")?.access ?? processAccessControl;
1699
+ }
1700
+ /** The resource names Rebase's own scopes use; an app scope may not reuse one. */
1701
+ var BUILT_IN_RESOURCES = new Set(BUILT_IN_SCOPES.map((scope) => scope.slice(0, scope.indexOf(":"))));
1702
+ /** A declaration the access model cannot be built from. Fails the boot. */
1703
+ var AccessModelError = class extends Error {
1704
+ constructor(message) {
1705
+ super(message);
1706
+ this.name = "AccessModelError";
1707
+ }
1708
+ };
1709
+ /**
1710
+ * Build the access model from the users collection's `auth` block, refusing
1711
+ * anything that would read as a grant it is not.
1712
+ *
1713
+ * - `admin` may not be declared: it is built in and holds everything, and a
1714
+ * declaration would either repeat that or quietly narrow it.
1715
+ * - An app scope may not reuse a built-in resource name (`logs:export`): the
1716
+ * built-in resources are Rebase's, and a reader of `logs:*` should not have
1717
+ * to know which half is whose.
1718
+ * - A role may not list a data-plane scope: a person's data access is the
1719
+ * collection's `securityRules`, so `data:write` on a role would grant nothing
1720
+ * and look like it granted something.
1721
+ */
1722
+ function accessModelFromCollections(collections) {
1723
+ const authCollection = collections.find((collection) => {
1724
+ const auth = collection.auth;
1725
+ return auth === true || !!auth && typeof auth === "object" && auth.enabled === true;
1726
+ });
1727
+ const auth = authCollection && typeof authCollection.auth === "object" ? authCollection.auth : void 0;
1728
+ const declaredScopes = { ...auth?.scopes ?? {} };
1729
+ const declaredRoles = { ...auth?.roles ?? {} };
1730
+ const where = `the "${authCollection?.slug ?? "users"}" collection's auth block`;
1731
+ for (const [name, definition] of Object.entries(declaredScopes)) {
1732
+ const parsed = parseScope(name);
1733
+ if (!parsed || parsed.target !== void 0) throw new AccessModelError(`auth.scopes: "${name}" in ${where} is not a scope name. Name scopes resource:action, lower-case, without a target — for example "project:deploy".`);
1734
+ const resource = name.slice(0, name.indexOf(":"));
1735
+ if (BUILT_IN_RESOURCES.has(resource)) throw new AccessModelError(`auth.scopes: "${name}" in ${where} uses the built-in resource "${resource}". App scopes need their own resource name; the built-in ones are ${[...BUILT_IN_RESOURCES].join(", ")}.`);
1736
+ if (typeof definition?.label !== "string" || definition.label.trim() === "") throw new AccessModelError(`auth.scopes: "${name}" in ${where} needs a label — it is what a person granting it reads.`);
1737
+ }
1738
+ const model = {
1739
+ roles: declaredRoles,
1740
+ scopes: declaredScopes
1741
+ };
1742
+ for (const [role, definition] of Object.entries(declaredRoles)) {
1743
+ if (role === "admin") throw new AccessModelError(`auth.roles: "admin" in ${where} is built in — it holds every scope — and cannot be declared. Declare a role with another name for a narrower set.`);
1744
+ if (!Array.isArray(definition?.scopes)) throw new AccessModelError(`auth.roles: "${role}" in ${where} needs a scopes list.`);
1745
+ const dataPlane = definition.scopes.filter((scope) => isDataPlaneScope(parseScope(scope)?.scope ?? ""));
1746
+ if (dataPlane.length > 0) throw new AccessModelError(`auth.roles: "${role}" in ${where} lists ${dataPlane.join(", ")}. Every signed-in person already holds the data plane; what they may do with rows is the collection's securityRules. List admin-plane and app scopes only.`);
1747
+ const problems = validateScopes(definition.scopes, model);
1748
+ if (problems.length > 0) throw new AccessModelError(`auth.roles: "${role}" in ${where} lists ${problems.map((p) => `"${p.scope}" (${p.reason})`).join(", ")}. Role scopes are ${ADMIN_SCOPES.join(", ")} and the app's own auth.scopes.`);
1749
+ }
1750
+ return model;
1751
+ }
1752
+ /** Narrow a context's user to the roles it carries. */
1753
+ function rolesOf(user) {
1754
+ if (typeof user !== "object" || user === null || !("roles" in user)) return [];
1755
+ const roles = user.roles;
1756
+ return Array.isArray(roles) ? roles.filter((role) => typeof role === "string") : [];
1757
+ }
1758
+ /**
1759
+ * Everything the caller of this request may do. Empty for a request with no
1760
+ * caller.
1761
+ */
1762
+ function callerScopes(c) {
1763
+ const narrowed = c.get("scopes");
1764
+ if (narrowed) return narrowed;
1765
+ const user = c.get("user");
1766
+ if (!user) return [];
1767
+ return scopesForRoles(rolesOf(user), accessOf(c).model);
1768
+ }
1769
+ /**
1770
+ * Whether this request's caller holds the `admin` role. The service key signs
1771
+ * in as one.
1772
+ */
1773
+ function callerIsAdmin(c) {
1774
+ return hasAdminRole(rolesOf(c.get("user")));
1775
+ }
1776
+ /** Does this request's caller hold `scope`, on `target` when one is given? */
1777
+ function hasScope(c, scope, target) {
1778
+ return scopeGrants(callerScopes(c), scope, target);
1779
+ }
1780
+ /**
1781
+ * The refusal for a missing scope, worded for whoever is holding the
1782
+ * credential. `details.requiredScope` names it for clients that branch.
1783
+ */
1784
+ function scopeMissing(c, scope, target) {
1785
+ const wanted = target !== void 0 ? `${scope}:${target}` : scope;
1786
+ return new ApiError(403, "SCOPE_MISSING", c.get("apiKey") !== void 0 ? `This API key does not hold the "${wanted}" scope. Create a key that includes it.` : c.get("scopes") ? `This token does not hold the "${wanted}" scope. Reconnect and grant it.` : `Your account does not hold the "${wanted}" scope. A role that lists it (auth.roles on the users collection) grants it; the admin role holds every scope.`, { requiredScope: wanted });
1787
+ }
1788
+ /**
1789
+ * Refuse a request whose caller does not hold `scope`.
1790
+ *
1791
+ * `scope` is a built-in scope or one this backend declares under
1792
+ * `auth.scopes`. A name no caller could ever hold — not a scope name, a typo
1793
+ * of a built-in one (`data:raed`) — throws as the route is registered; an app
1794
+ * scope the backend answering does not declare is answered
1795
+ * `500 SCOPE_NOT_DECLARED`, naming the declaration to add.
1796
+ *
1797
+ * `target` narrows the check to one collection, bucket, function or app
1798
+ * resource, and may be read from the request. A guard, not an authenticator:
1799
+ * it judges the caller an auth middleware identified, so mount it after one —
1800
+ * `requireAuth`, or a route group's own. A request with no caller is refused
1801
+ * as unauthenticated, not as unscoped.
1802
+ *
1803
+ * @example
1804
+ * ```ts
1805
+ * app.post("/deploy/:project", requireAuth, requireScope("project:deploy", c => c.req.param("project")), handler);
1806
+ * ```
1807
+ */
1808
+ function requireScope(scope, target) {
1809
+ const problem = scopeGuardProblem(scope, typeof target === "string" ? target : void 0);
1810
+ if (problem) throw new Error(problem);
1811
+ return async (c, next) => {
1812
+ if (!c.get("user")) return refuse(c, noCallerRefusal(c));
1813
+ const undeclared = scopeGuardProblem(scope, void 0, Object.keys(accessOf(c).model.scopes));
1814
+ if (undeclared) return refuse(c, new ApiError(500, "SCOPE_NOT_DECLARED", undeclared));
1815
+ const resolvedTarget = typeof target === "function" ? target(c) : target;
1816
+ if (!hasScope(c, scope, resolvedTarget)) return refuse(c, scopeMissing(c, scope, resolvedTarget));
1817
+ return next();
1818
+ };
1819
+ }
1820
+ /**
1821
+ * The 401 for a guard that found no caller.
1822
+ *
1823
+ * A request that sent a credential, on a route where no auth middleware ran —
1824
+ * it sets `driver` for every caller, anonymous included — never had that
1825
+ * credential read. "Authentication required" sends whoever holds a valid
1826
+ * session to check it, so this names the wiring instead.
1827
+ */
1828
+ function noCallerRefusal(c) {
1829
+ if (c.req.header("authorization") !== void 0 && c.get("driver") === void 0) return ApiError.unauthenticated("Authentication required: this request carries a credential, but nothing on this route verified it. requireScope judges the caller an auth middleware identified; put requireAuth in front of it — app.post(path, requireAuth, requireScope(scope), handler).");
1830
+ return ApiError.unauthenticated("Authentication required");
1831
+ }
1832
+ /**
1833
+ * Refuse a request unless its caller holds the scope for its HTTP method:
1834
+ * `read` for GET and HEAD, `write` for everything else. For an admin surface
1835
+ * whose reads and writes split exactly along the method.
1836
+ */
1837
+ function requireScopeByMethod(scopes) {
1838
+ const read = requireScope(scopes.read);
1839
+ const write = requireScope(scopes.write);
1840
+ return async (c, next) => {
1841
+ const method = c.req.method.toUpperCase();
1842
+ return (method === "GET" || method === "HEAD" ? read : write)(c, next);
1843
+ };
1844
+ }
1845
+ /**
1846
+ * What a set of roles holds beyond this request's caller — the scopes, plus
1847
+ * `admin` itself when the roles include it and the caller does not hold it.
1848
+ * Empty when the caller covers every one.
1849
+ *
1850
+ * `admin` is named separately because it is more than its scopes: it is the
1851
+ * RLS role that reads every row, so a role listing every admin-plane scope
1852
+ * still cannot hand it out.
1853
+ */
1854
+ function beyondCaller(c, roles) {
1855
+ const beyond = scopesBeyond(scopesForRoles(roles, accessOf(c).model), callerScopes(c));
1856
+ if (hasAdminRole(roles) && !hasAdminRole(rolesOf(c.get("user")))) beyond.unshift(ADMIN_ROLE);
1857
+ return beyond;
1858
+ }
1859
+ /**
1860
+ * Refuse to let the caller act on an account — edit it, reset its password,
1861
+ * delete it — when that account holds anything the caller does not.
1862
+ *
1863
+ * `users:write` is the scope that lets a support role manage accounts, and
1864
+ * without this it would also let that role reset an administrator's password
1865
+ * and sign in as them.
1866
+ */
1867
+ function assertMayManageAccount(c, targetRoles) {
1868
+ const beyond = beyondCaller(c, targetRoles);
1869
+ if (beyond.length > 0) throw new ApiError(403, "ACCOUNT_OUTRANKS_CALLER", `This account holds ${beyond.join(", ")}, which you do not, so you cannot change it.`, { beyond });
1870
+ }
1871
+ /**
1872
+ * Read a `roles` body field as the exact list that is then judged and written.
1873
+ *
1874
+ * Every entry must be a string `roleNameProblem` accepts. Filtering out the bad
1875
+ * entries instead is how the ceiling once judged one list and the write stored
1876
+ * another: `["support", ["admin"]]` was judged as `["support"]` and written as
1877
+ * `{support,admin}`, and `"support,admin"` was one undeclared role to the check
1878
+ * and two roles to Postgres.
1879
+ */
1880
+ function readRoleNames(value) {
1881
+ if (!Array.isArray(value)) throw new ApiError(400, "INVALID_ROLE", "roles must be an array of role names, e.g. [\"editor\"]");
1882
+ const problems = value.flatMap((entry) => {
1883
+ const problem = roleNameProblem(entry);
1884
+ return problem ? [`${JSON.stringify(entry)} ${problem}`] : [];
1885
+ });
1886
+ if (problems.length > 0) throw new ApiError(400, "INVALID_ROLE", `Not valid role names: ${problems.join("; ")}. A role name is a non-empty string without , { } " \\ or control characters.`, { problems });
1887
+ return [...new Set(value)];
1888
+ }
1889
+ /** Refuse to let the caller grant roles that hold anything the caller does not. */
1890
+ function assertMayGrantRoles(c, roles) {
1891
+ const beyond = beyondCaller(c, roles);
1892
+ if (beyond.length > 0) throw new ApiError(403, "ROLE_EXCEEDS_CALLER", `These roles hold ${beyond.join(", ")}, which you do not, so you cannot grant them.`, { beyond });
1893
+ }
1894
+ //#endregion
1895
+ export { accessTokenRefusalError as $, isPublicObjectGrant as A, maskedFieldsFor as B, queryTokenAuth as C, userFromToken as D, requireAuth as E, decideImpersonation as F, isApiKeyToken as G, createApiKeyPreAuth as H, refuseUnhonouredImpersonation as I, extractBearerToken as J, resolveApiKey as K, applySimulation as L, storageObjectOfPath as M, storageRequestWildcard as N, PUBLIC_OBJECT_PRINCIPAL as O, applyImpersonation as P, supportsRlsScoping as Q, callerOf$1 as R, publicObjectAuth as S, requireAdmin as T, createFunctionScopeGuard as U, reportSimulatedContext as V, createTusScopeGuard as W, SERVICE_IDENTITY as X, safeCompare as Y, scopeDataDriver as Z, createAuthMiddleware as _, assertMayManageAccount as a, httpMethodToOperation as at, fileTokenAuth as b, configureAccess as c, isPublicStoragePath as ct, keyOwnerResolverFor as d, isAccessTokenRevoked as et, processAccessControl as f, setDefaultAccessControl as g, requireScopeByMethod as h, assertMayGrantRoles as i, refuseSimulatedWrite as it, requestedStorageObject as j, isDownloadTokenGrant as k, getAccessModel as l, storageObjectPath as lt, requireScope as m, accessModelFromCollections as n, replaceUserPassword as nt, callerIsAdmin as o, PUBLIC_STORAGE_PREFIX as ot, readRoleNames as p, validateApiKey as q, accessOf as r, revokeAllSessions as rt, callerScopes as s, isPublicStorageKey as st, AccessModelError as t, judgeAccessToken as tt, hasScope as u, createRequireAuth as v, refuse as w, optionalAuth as x, extractUserFromToken as y, decideSimulation as z };
1896
+
1897
+ //# sourceMappingURL=access-BFAUvsSs.js.map