@rebasepro/server 0.22.0 → 0.24.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 (211) hide show
  1. package/README.md +1 -1
  2. package/bin/rebase-server.js +4 -2
  3. package/dist/{GCSStorageController-CLIJXwGS.js → GCSStorageController-BSiP1c-f.js} +57 -29
  4. package/dist/GCSStorageController-BSiP1c-f.js.map +1 -0
  5. package/dist/{S3StorageController-Dcuf8lMA.js → S3StorageController-CAwFRgjV.js} +19 -7
  6. package/dist/S3StorageController-CAwFRgjV.js.map +1 -0
  7. package/dist/api/ast-schema-editor.d.ts +127 -1
  8. package/dist/api/errors.d.ts +9 -0
  9. package/dist/api/live-schema-routes.d.ts +52 -8
  10. package/dist/api/logs-routes.d.ts +39 -1
  11. package/dist/api/openapi-generator.d.ts +17 -0
  12. package/dist/api/rest/api-generator.d.ts +158 -42
  13. package/dist/api/rest/auth-collection-writes.d.ts +85 -0
  14. package/dist/api/rest/field-access-query.d.ts +6 -2
  15. package/dist/api/rest/idempotency.d.ts +7 -1
  16. package/dist/api/rest/nested-write-access.d.ts +46 -0
  17. package/dist/api/rest/write-validation.d.ts +34 -2
  18. package/dist/api/types.d.ts +17 -1
  19. package/dist/{ast-schema-editor-CslO8Oje.js → ast-schema-editor-CWqS_sLJ.js} +411 -13
  20. package/dist/ast-schema-editor-CWqS_sLJ.js.map +1 -0
  21. package/dist/auth/access.d.ts +105 -0
  22. package/dist/auth/adapter-middleware.d.ts +2 -1
  23. package/dist/auth/address-ownership.d.ts +68 -0
  24. package/dist/auth/admin-roles-route.d.ts +4 -2
  25. package/dist/auth/admin-roles.d.ts +17 -20
  26. package/dist/auth/admin-user-ops.d.ts +35 -2
  27. package/dist/auth/admin-users-route.d.ts +1 -0
  28. package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
  29. package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
  30. package/dist/auth/api-keys/api-key-store.d.ts +31 -8
  31. package/dist/auth/api-keys/api-key-types.d.ts +14 -16
  32. package/dist/auth/api-keys/http-operation.d.ts +19 -0
  33. package/dist/auth/api-keys/index.d.ts +11 -11
  34. package/dist/auth/api-keys/key-grant.d.ts +41 -0
  35. package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
  36. package/dist/auth/auth-hooks.d.ts +50 -7
  37. package/dist/auth/builtin-auth-adapter.d.ts +8 -0
  38. package/dist/auth/captcha.d.ts +5 -0
  39. package/dist/auth/cookie-utils.d.ts +7 -0
  40. package/dist/auth/deliverable-address.d.ts +6 -0
  41. package/dist/auth/email-change-routes.d.ts +41 -0
  42. package/dist/auth/expired-token-sweep.d.ts +67 -0
  43. package/dist/auth/impersonation.d.ts +110 -0
  44. package/dist/auth/index.d.ts +4 -2
  45. package/dist/auth/interfaces.d.ts +146 -65
  46. package/dist/auth/jwt.d.ts +66 -3
  47. package/dist/auth/magic-link-routes.d.ts +2 -6
  48. package/dist/auth/mfa-routes.d.ts +2 -9
  49. package/dist/auth/middleware.d.ts +17 -5
  50. package/dist/auth/oauth-signin-policy.d.ts +25 -8
  51. package/dist/auth/otp-routes.d.ts +2 -6
  52. package/dist/auth/passwordless-signup.d.ts +27 -0
  53. package/dist/auth/platform-token.d.ts +122 -0
  54. package/dist/auth/rate-limiter.d.ts +72 -1
  55. package/dist/auth/routes.d.ts +45 -0
  56. package/dist/auth/scope-routes.d.ts +22 -0
  57. package/dist/auth/session-routes.d.ts +18 -6
  58. package/dist/auth/token-revocation.d.ts +53 -1
  59. package/dist/auth/verify-credential.d.ts +28 -0
  60. package/dist/{auth-CCDpk2rn.js → auth-DMLngxn_.js} +2712 -711
  61. package/dist/auth-DMLngxn_.js.map +1 -0
  62. package/dist/backend-DTAOsLQc.js +30 -0
  63. package/dist/backend-DTAOsLQc.js.map +1 -0
  64. package/dist/backup/backup-common.d.ts +29 -0
  65. package/dist/backup/backup-routes.d.ts +24 -4
  66. package/dist/backup/backup-schedule.d.ts +33 -0
  67. package/dist/backup/backup-storage.d.ts +14 -0
  68. package/dist/backup/index.d.ts +2 -0
  69. package/dist/backup-CN0s50D2.js +444 -0
  70. package/dist/backup-CN0s50D2.js.map +1 -0
  71. package/dist/boot/bundle.d.ts +19 -0
  72. package/dist/boot/driver.d.ts +10 -0
  73. package/dist/boot/env.d.ts +51 -6
  74. package/dist/boot/fetch-bundle.d.ts +18 -1
  75. package/dist/boot/rls-audit-option.d.ts +26 -0
  76. package/dist/boot/security-headers.d.ts +26 -0
  77. package/dist/boot/sources.d.ts +1 -0
  78. package/dist/boot/static-routing.d.ts +56 -0
  79. package/dist/collection_patch-BRu-BvDv.js +472 -0
  80. package/dist/collection_patch-BRu-BvDv.js.map +1 -0
  81. package/dist/{contract-routes-eLxV0le1.js → contract-routes-fz8i4pxs.js} +17 -4
  82. package/dist/contract-routes-fz8i4pxs.js.map +1 -0
  83. package/dist/cron/cron-routes.d.ts +7 -2
  84. package/dist/cron/cron-scheduler.d.ts +146 -21
  85. package/dist/cron/cron-store.d.ts +76 -8
  86. package/dist/cron/index.d.ts +1 -1
  87. package/dist/{cron-loader-CQjvjpEw.js → cron-loader-CwaANlOG.js} +4 -4
  88. package/dist/cron-loader-CwaANlOG.js.map +1 -0
  89. package/dist/cron-routes-Bc-SB0Se.js +96 -0
  90. package/dist/cron-routes-Bc-SB0Se.js.map +1 -0
  91. package/dist/{cron-scheduler-COPQxlEq.js → cron-scheduler-CYQgco86.js} +427 -83
  92. package/dist/cron-scheduler-CYQgco86.js.map +1 -0
  93. package/dist/{cron-store-BYGZFNWk.js → cron-store-D2Q9-Aco.js} +139 -23
  94. package/dist/cron-store-D2Q9-Aco.js.map +1 -0
  95. package/dist/{ddl-bootstrap-CfNvxMuK.js → ddl-bootstrap-BaqMSa4Y.js} +3 -26
  96. package/dist/ddl-bootstrap-BaqMSa4Y.js.map +1 -0
  97. package/dist/email/index.d.ts +2 -2
  98. package/dist/email/link-base.d.ts +5 -4
  99. package/dist/email/smtp-email-service.d.ts +13 -1
  100. package/dist/email/templates.d.ts +31 -0
  101. package/dist/email/types.d.ts +29 -2
  102. package/dist/env.d.ts +25 -7
  103. package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
  104. package/dist/errors-D6_y86c5.js.map +1 -0
  105. package/dist/{function-loader-xnbDAPfa.js → function-loader-D7o5Epjj.js} +2 -2
  106. package/dist/{function-loader-xnbDAPfa.js.map → function-loader-D7o5Epjj.js.map} +1 -1
  107. package/dist/{function-routes-Chet4-lB.js → function-routes-CaNG4waN.js} +24 -12
  108. package/dist/function-routes-CaNG4waN.js.map +1 -0
  109. package/dist/functions/context.d.ts +17 -6
  110. package/dist/functions/guards.d.ts +22 -5
  111. package/dist/functions/index.d.ts +2 -2
  112. package/dist/functions/index.js +90 -36
  113. package/dist/functions/index.js.map +1 -1
  114. package/dist/{history-recorder-BQmB0P_j.js → history-recorder-Nr8zLvoU.js} +9 -7
  115. package/dist/history-recorder-Nr8zLvoU.js.map +1 -0
  116. package/dist/{history-store-CetkrBBD.js → history-store-rcAm_xFR.js} +2 -2
  117. package/dist/{history-store-CetkrBBD.js.map → history-store-rcAm_xFR.js.map} +1 -1
  118. package/dist/index.d.ts +14 -4
  119. package/dist/index.es.js +5733 -1551
  120. package/dist/index.es.js.map +1 -1
  121. package/dist/init/docs.d.ts +5 -2
  122. package/dist/init/health.d.ts +17 -2
  123. package/dist/init/shutdown.d.ts +18 -3
  124. package/dist/init.d.ts +54 -0
  125. package/dist/jobs/index.d.ts +2 -2
  126. package/dist/jobs/job-queue.d.ts +23 -2
  127. package/dist/jobs/job-store.d.ts +37 -5
  128. package/dist/jobs/types.d.ts +8 -6
  129. package/dist/{jobs-Bjr8DZAi.js → jobs-DqYNfquG.js} +306 -167
  130. package/dist/jobs-DqYNfquG.js.map +1 -0
  131. package/dist/{jwt-C4OW-DNq.js → jwt-R6bSPMjk.js} +114 -38
  132. package/dist/{jwt-C4OW-DNq.js.map → jwt-R6bSPMjk.js.map} +1 -1
  133. package/dist/{keys-Qfc4XieN.js → keys-GAVZqbqx.js} +18 -17
  134. package/dist/{keys-Qfc4XieN.js.map → keys-GAVZqbqx.js.map} +1 -1
  135. package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
  136. package/dist/logger-D-S-hO5e.js.map +1 -0
  137. package/dist/{logs-routes-3EEzPjhl.js → logs-routes-DAdv37GI.js} +54 -11
  138. package/dist/logs-routes-DAdv37GI.js.map +1 -0
  139. package/dist/mcp/consent-page.d.ts +1 -1
  140. package/dist/mcp/mcp-routes.d.ts +45 -2
  141. package/dist/mcp/mcp-tools.d.ts +22 -10
  142. package/dist/mcp/oauth-metadata.d.ts +21 -16
  143. package/dist/mcp/oauth-routes.d.ts +34 -1
  144. package/dist/mcp/oauth-store.d.ts +29 -13
  145. package/dist/metrics/history-recorder.d.ts +1 -1
  146. package/dist/{openapi-generator-D8uFz-LW.js → openapi-generator-DAq_XVDu.js} +135 -22
  147. package/dist/openapi-generator-DAq_XVDu.js.map +1 -0
  148. package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
  149. package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
  150. package/dist/{query-parser-BleZmY18.js → query-parser-BgiKJKvc.js} +41 -82
  151. package/dist/query-parser-BgiKJKvc.js.map +1 -0
  152. package/dist/{request-timeout-C_4C2BeR.js → request-timeout-DgH7j8qO.js} +3 -3
  153. package/dist/{request-timeout-C_4C2BeR.js.map → request-timeout-DgH7j8qO.js.map} +1 -1
  154. package/dist/rls-audit/index.d.ts +4 -0
  155. package/dist/schema-edit/apply-schema-change.d.ts +63 -3
  156. package/dist/schema-edit/project-root.d.ts +3 -2
  157. package/dist/schema-edit/remote-source.d.ts +9 -4
  158. package/dist/{schema-editor-routes-DdLihzp0.js → schema-editor-routes-oIyuWl3L.js} +12 -7
  159. package/dist/schema-editor-routes-oIyuWl3L.js.map +1 -0
  160. package/dist/serve-spa.d.ts +58 -0
  161. package/dist/services/routed-realtime-service.d.ts +11 -0
  162. package/dist/soft-delete-params-BWPilMPF.js +59 -0
  163. package/dist/soft-delete-params-BWPilMPF.js.map +1 -0
  164. package/dist/{src-Br6ARbs6.js → src-CatHFUym.js} +439 -45
  165. package/dist/src-CatHFUym.js.map +1 -0
  166. package/dist/{src-1vL-I1Po.js → src-I3aG1PcY.js} +371 -81
  167. package/dist/src-I3aG1PcY.js.map +1 -0
  168. package/dist/storage/GCSStorageController.d.ts +13 -1
  169. package/dist/storage/LocalStorageController.d.ts +2 -0
  170. package/dist/storage/S3StorageController.d.ts +2 -0
  171. package/dist/storage/index.d.ts +2 -2
  172. package/dist/storage/keys.d.ts +12 -0
  173. package/dist/storage/property-limits.d.ts +41 -6
  174. package/dist/storage/rendition-cache.d.ts +11 -1
  175. package/dist/storage/request-keys.d.ts +82 -0
  176. package/dist/storage/requested-object.d.ts +74 -0
  177. package/dist/storage/routes.d.ts +36 -18
  178. package/dist/storage/tus-handler.d.ts +30 -5
  179. package/dist/storage/types.d.ts +36 -1
  180. package/dist/types-BfKcm9do.js.map +1 -1
  181. package/dist/utils/logger.d.ts +12 -0
  182. package/package.json +9 -9
  183. package/dist/GCSStorageController-CLIJXwGS.js.map +0 -1
  184. package/dist/S3StorageController-Dcuf8lMA.js.map +0 -1
  185. package/dist/admin-roles-vYdp_Pil.js +0 -36
  186. package/dist/admin-roles-vYdp_Pil.js.map +0 -1
  187. package/dist/admin_block-DxKLmdiv.js +0 -206
  188. package/dist/admin_block-DxKLmdiv.js.map +0 -1
  189. package/dist/ast-schema-editor-CslO8Oje.js.map +0 -1
  190. package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
  191. package/dist/auth-CCDpk2rn.js.map +0 -1
  192. package/dist/backup-DzI9jLwc.js +0 -192
  193. package/dist/backup-DzI9jLwc.js.map +0 -1
  194. package/dist/contract-routes-eLxV0le1.js.map +0 -1
  195. package/dist/cron-loader-CQjvjpEw.js.map +0 -1
  196. package/dist/cron-routes-B7CRGfiq.js +0 -72
  197. package/dist/cron-routes-B7CRGfiq.js.map +0 -1
  198. package/dist/cron-scheduler-COPQxlEq.js.map +0 -1
  199. package/dist/cron-store-BYGZFNWk.js.map +0 -1
  200. package/dist/ddl-bootstrap-CfNvxMuK.js.map +0 -1
  201. package/dist/errors-DWsX4yTd.js.map +0 -1
  202. package/dist/function-routes-Chet4-lB.js.map +0 -1
  203. package/dist/history-recorder-BQmB0P_j.js.map +0 -1
  204. package/dist/jobs-Bjr8DZAi.js.map +0 -1
  205. package/dist/logger-DO2PZc4i.js.map +0 -1
  206. package/dist/logs-routes-3EEzPjhl.js.map +0 -1
  207. package/dist/openapi-generator-D8uFz-LW.js.map +0 -1
  208. package/dist/query-parser-BleZmY18.js.map +0 -1
  209. package/dist/schema-editor-routes-DdLihzp0.js.map +0 -1
  210. package/dist/src-1vL-I1Po.js.map +0 -1
  211. package/dist/src-Br6ARbs6.js.map +0 -1
@@ -44,6 +44,15 @@ declare class LogRingBuffer {
44
44
  getLatest(count?: number): LogEntry[];
45
45
  }
46
46
  export declare const logBuffer: LogRingBuffer;
47
+ /**
48
+ * Which process this log belongs to.
49
+ *
50
+ * The ring is this process's memory, and nothing gathers the rings of a
51
+ * deployment's other replicas or of its split runtime roles. So a reader is
52
+ * told whose log it is: `HOSTNAME` is the pod name on Kubernetes and the
53
+ * container id under Docker; the pid tells two local processes apart.
54
+ */
55
+ export declare function logInstanceName(): string;
47
56
  /** Add a log entry */
48
57
  export declare function addLog(level: LogEntry["level"], source: LogEntry["source"], message: string, metadata?: Record<string, unknown>): void;
49
58
  export interface LogMiddlewareOptions {
@@ -55,7 +64,25 @@ export interface LogMiddlewareOptions {
55
64
  * server it is also the thing evicting real entries out of the ring.
56
65
  */
57
66
  ignorePaths?: string[];
67
+ /**
68
+ * Where the API is mounted (`/api`). The segment after it names the
69
+ * subsystem a request is filed under — see {@link sourceForRequestPath}.
70
+ * Unset, the first segment of the path is read.
71
+ */
72
+ basePath?: string;
58
73
  }
74
+ /**
75
+ * The `source` of a request entry: the subsystem its path addresses.
76
+ *
77
+ * `/api/auth/*` and the MCP authorization server under `/api/oauth/*` are
78
+ * `auth`, `/api/storage/*` is `storage`, and everything else is `api`. The
79
+ * Logs Explorer's Source filter matches this field, so a sign-in filed as
80
+ * `api` was invisible under Auth. The realtime socket is not an HTTP request
81
+ * and is not recorded here.
82
+ *
83
+ * Exported for its test.
84
+ */
85
+ export declare function sourceForRequestPath(path: string, basePath?: string): LogEntry["source"];
59
86
  /** Hono middleware to log API requests */
60
87
  export declare function logMiddleware(options?: LogMiddlewareOptions): MiddlewareHandler<HonoEnv>;
61
88
  /**
@@ -79,6 +106,17 @@ export interface LogStreamTiming {
79
106
  heartbeatMs?: number;
80
107
  maxPending?: number;
81
108
  }
82
- export declare function createLogsRoutes(timing?: LogStreamTiming): Hono<HonoEnv>;
109
+ /** What ends the stream from the server's side. */
110
+ export interface LogsRoutesOptions {
111
+ /**
112
+ * Aborted when the backend shuts down. Every open stream ends then, rather
113
+ * than when its client happens to leave: `server.close()` waits on every
114
+ * open connection, so one Studio → Logs tab held a SIGTERM for the whole
115
+ * force timeout — longer than Cloud Run's or `docker stop`'s grace period,
116
+ * which killed the process before the pool was closed.
117
+ */
118
+ closeSignal?: AbortSignal;
119
+ }
120
+ export declare function createLogsRoutes(timing?: LogStreamTiming, options?: LogsRoutesOptions): Hono<HonoEnv>;
83
121
  declare const _default: Hono<HonoEnv, import("hono/types").BlankSchema, "/">;
84
122
  export default _default;
@@ -28,3 +28,20 @@ export interface OpenApiGeneratorOptions {
28
28
  };
29
29
  }
30
30
  export declare function generateOpenApiSpec(collections: CollectionConfig[], options?: OpenApiGeneratorOptions): Record<string, unknown>;
31
+ /**
32
+ * Build the component schema for a collection (output / read shape).
33
+ *
34
+ * Every declared property except the ones {@link isDocumentedProperty} rules
35
+ * out, plus the foreign keys and relations {@link emitRelationProperties} adds.
36
+ */
37
+ export declare function buildCollectionSchema(collection: CollectionConfig, registeredSchemas: ReadonlySet<string>): Record<string, unknown>;
38
+ /**
39
+ * Build an input schema (for POST/PUT) — excludes auto-generated fields.
40
+ *
41
+ * `excludeFromApi` columns are left out too, and the server now agrees: a write
42
+ * naming one is refused (`write-validation.ts`), which is what the flag's name
43
+ * and the generated SDK's own documentation always said. This comment used to
44
+ * record that such a write was "still accepted" — the document was right and
45
+ * the server was the thing that had not caught up.
46
+ */
47
+ export declare function buildCollectionInputSchema(collection: CollectionConfig): Record<string, unknown>;
@@ -1,7 +1,32 @@
1
1
  import { Hono } from "hono";
2
2
  import { AuthAdapter, DataDriver, CollectionConfig } from "@rebasepro/types";
3
- import { HonoEnv } from "../types.js";
3
+ import { QueryOptions, HonoEnv } from "../types.js";
4
4
  import { type ListLimitOptions } from "./query-parser.js";
5
+ /**
6
+ * The row a single-row write addresses, as its route found it.
7
+ *
8
+ * `/comments/7` and `/posts/1/comments/7` address the same kind of row — a row
9
+ * of `comments` — two ways, and a write to it is one operation whichever way
10
+ * it arrived: the same body checks, the same `If-Match`, the same
11
+ * `Idempotency-Key`, the same answer. It was two pipelines, and the nested one
12
+ * re-listed two of the root's checks by hand and skipped the rest.
13
+ */
14
+ export interface RowAddress {
15
+ /** The collection the row belongs to: what a body is checked against and an `ETag` computed for. */
16
+ collection: CollectionConfig;
17
+ /**
18
+ * The path the driver reads and writes at: the collection's own, or a
19
+ * nested one, which the driver resolves and scopes to its parent row.
20
+ */
21
+ path: string;
22
+ /**
23
+ * Handed to the driver beside `path`. Unset for a nested path, whose
24
+ * collection the driver resolves from the path itself.
25
+ */
26
+ driverCollection?: CollectionConfig;
27
+ /** What a 404 calls the address. */
28
+ name: string;
29
+ }
5
30
  /**
6
31
  * Lightweight REST API generator that leverages existing Rebase DataDriver.
7
32
  * Supports `include` query parameter for eager-loading relations via Drizzle.
@@ -113,34 +138,64 @@ export declare class RestApiGenerator {
113
138
  */
114
139
  private createUnmatchedRoute;
115
140
  /**
116
- * Check API key permissions for a collection operation.
117
- * Throws 403 if the key doesn't have the required permission.
118
- * No-ops if the request is not authenticated via an API key.
141
+ * Check a narrowed credential's data scope for a collection operation:
142
+ * `data:<operation>` on the collection, or unqualified.
143
+ *
144
+ * Throws 403 when it is missing. A person's own session holds the whole
145
+ * data plane — their rows are RLS's to decide — so this only ever refuses
146
+ * an API key or a token.
119
147
  */
120
148
  private enforceApiKeyPermission;
121
149
  /**
122
- * API key permission check for nested paths. The operation targets the
123
- * LAST collection in the path (e.g. "posts" for /authors/1/posts), so
124
- * that is the slug the key must hold permission for — checking the
125
- * parent instead would let a key scoped to "authors" write "posts".
126
- * `parseSubPath` always yields a collectionPath ending in a collection
127
- * slug, never an id.
150
+ * Data-scope check for nested paths.
151
+ *
152
+ * The operation lands on the collection the path addresses — the target
153
+ * of its last relation — so that is the collection the key must hold the
154
+ * route's operation for; checking only the parent would let a key scoped
155
+ * to `authors` write `posts`. The *collection*, not the path segment: a
156
+ * relation's name is not its target's slug, and a key for `notes` read
157
+ * `internal_notes` through `projects/1/notes` because the check read the
158
+ * name off the URL.
159
+ *
160
+ * Every collection the path passes through is addressed as well — "the
161
+ * posts of author 1" goes through a row of `authors` — so the key must be
162
+ * able to read each of them.
128
163
  */
129
164
  private enforceSubcollectionApiKeyPermission;
130
165
  /**
131
- * The collection a nested path writes into — the target of the relation its
132
- * last segment names.
166
+ * The collection a path's first segment names.
167
+ *
168
+ * By the rule the driver's registry applies (`CollectionRegistry.get`),
169
+ * because the driver is what a nested path is forwarded to: the slug, then
170
+ * the slug written in kebab case — URLs are — then the table name. Matching
171
+ * the slug alone left the other two spellings resolving in the driver and
172
+ * in nothing here, so a nested request spelled either way was checked
173
+ * against no collection at all.
174
+ */
175
+ private collectionNamed;
176
+ /**
177
+ * Walk a nested path to the collection it addresses — the target of the
178
+ * relation its last segment names.
133
179
  *
134
- * Needed so a nested write can be checked against a schema at all. Without
135
- * it these routes skipped `assertKnownWriteFields` entirely, which is why a
136
- * typo `POST /posts` rejected with a 400 while the same typo on
137
- * `POST /authors/1/posts` was dropped from the statement and answered 201.
180
+ * Needed so a nested request can be checked against a schema at all: the
181
+ * fields a body may write and a query may read are the target's. A path
182
+ * that names nothing is a 404 here, before anything else happens. This
183
+ * used to return `undefined` instead and leave the refusal to the driver,
184
+ * and every check on these routes was written `if (target) …` — so a path
185
+ * the driver could resolve and this walk could not was a request with no
186
+ * checks on it, rather than a refused one.
138
187
  *
139
- * Returns `undefined` rather than throwing when the path cannot be walked:
140
- * the driver raises the authoritative error a moment later, and duplicating
141
- * it here would report a resolution failure as a validation failure.
188
+ * The path handed back is spelled with the root's slug, which is what
189
+ * makes the two agree: whatever spelling arrived, the driver is asked about
190
+ * the collection this route checked the request against.
142
191
  */
143
- private resolveNestedWriteCollection;
192
+ private resolveNestedPath;
193
+ /**
194
+ * Refuse a write through `nested` that sets a relation this caller may not
195
+ * write — the parent's relation, or the parent's key on the child. See
196
+ * {@link assertNestedWriteAllowed}; the socket asks it the same question.
197
+ */
198
+ private assertNestedRelationWritable;
144
199
  /**
145
200
  * `PATCH <collection>/<id>/<relation>/<targetId>` with a `_pivot` body: set
146
201
  * the columns that one many-to-many link carries.
@@ -160,15 +215,30 @@ export declare class RestApiGenerator {
160
215
  */
161
216
  private updateRelationPivotFromBody;
162
217
  /**
163
- * The synthetic collection standing in for the junction a nested path's last
164
- * hop reaches through, or `undefined` when that hop is not a `manyToMany`
165
- * carrying `through.properties`.
218
+ * Updates to rows of the auth collection, checked and put in stored form
219
+ * by the auth adapter — `AuthAdapter.prepareUserUpdates`. Those rows are
220
+ * the users, so an update is user administration and holds to what
221
+ * `/admin/users` holds to: the last administrator stays one, an email is
222
+ * stored the way sign-in looks it up. Any other collection's values, and
223
+ * an adapter with no such step, pass through unchanged.
224
+ */
225
+ private prepareUserUpdates;
226
+ /**
227
+ * Delete rows of the auth collection as user administration: the adapter's
228
+ * checks and `beforeUserDelete` before (either may refuse), its session
229
+ * cleanup and `afterUserDelete` after. See {@link prepareUserUpdates}.
230
+ */
231
+ private deletingUsers;
232
+ /**
233
+ * `POST /users/bulk`: every row a user creation, the rows written as one
234
+ * transaction, the invitations sent once it has committed.
166
235
  *
167
- * Built from the relation rather than by walking every collection: the same
168
- * `getJunctionConfigForRelation` the schema planner and the driver use, so
169
- * the shape validated here is the shape the columns were emitted from.
236
+ * Answered in full whatever `Prefer` says, and never replayed from the
237
+ * idempotency store, as `POST /users` is: the answer can carry temporary
238
+ * passwords, which are shown once. Dropped from a minimal answer they are
239
+ * gone, and handed out again on a replayed key they are disclosed.
170
240
  */
171
- private resolveJunctionCollection;
241
+ private createUsers;
172
242
  /**
173
243
  * Get the request-scoped driver. Throws if none is set — never falls
174
244
  * back to the unscoped `this.driver` to avoid bypassing RLS/auth.
@@ -199,16 +269,41 @@ export declare class RestApiGenerator {
199
269
  */
200
270
  private runIdempotent;
201
271
  /**
202
- * The row as the read routes serve it, for hashing into an `ETag`.
272
+ * The row an `ETag` is hashed from: the REST read of it, with no
273
+ * `?fields=` projection and no `?include=`.
274
+ *
275
+ * The read routes and the write routes both take their tag from here, so a
276
+ * tag a `GET` handed out and the tag a write compares it against are
277
+ * hashes of one rendering of the row. The write routes' own existence read
278
+ * is not that rendering, not even for its version column: `driver.fetchOne`
279
+ * returns the admin view model, where a date is a `{ __type: "date" }`
280
+ * envelope rather than the ISO string the REST row carries. Hashing it
281
+ * matched no tag any read ever handed out, so every conditional write to a
282
+ * collection with an `on_update` date was a 412.
283
+ *
284
+ * A driver without a REST read serves `GET /:id` from `fetchOne` too, so
285
+ * there the row already read is the rendering to hash.
286
+ *
287
+ * @param withDeleted how the row was read — a purge reads a trashed row,
288
+ * and a tag compared against a read that hides it compares against
289
+ * nothing.
290
+ */
291
+ static rowForETag(driver: DataDriver, address: RowAddress, id: string, alreadyRead: Record<string, unknown> | undefined, withDeleted?: QueryOptions["withDeleted"]): Promise<Record<string, unknown> | undefined>;
292
+ /**
293
+ * `GET /:id`, at a row's own address or through a parent: the row as the
294
+ * query asked for it, and the `ETag` naming its version.
203
295
  *
204
- * A tag derived from a version column is the same whichever read produced
205
- * the row, so nothing extra is fetched for it. The fallback hashes the row
206
- * itself, and there the *shape* matters: `GET /:id` serves the REST walk
207
- * while the write routes read through `driver.fetchOne` (the admin view
208
- * model), so hashing whichever one happened to be in hand would give the
209
- * same row two different tags and make every `If-Match` a coin toss.
296
+ * The tag names the row, not the read. `?fields=` and `?include=` change
297
+ * what the response carries, and hashing what they produced gave one row a
298
+ * different tag for every way of reading it — none of them the tag a write
299
+ * computes, so the `If-Match` of any caller that read a projection or a
300
+ * relation was a 412. A narrowed read therefore takes its tag from
301
+ * {@link rowForETag}, the read the write routes make.
210
302
  */
211
- private rowForETag;
303
+ static readRow(driver: DataDriver, address: RowAddress, id: string, queryOptions: QueryOptions): Promise<{
304
+ row: Record<string, unknown>;
305
+ etag: string | undefined;
306
+ } | undefined>;
212
307
  /**
213
308
  * Answer a bulk write, honouring `Prefer: return=minimal`.
214
309
  *
@@ -218,6 +313,28 @@ export declare class RestApiGenerator {
218
313
  * the count never has to ask for the rows.
219
314
  */
220
315
  private respondBulk;
316
+ /**
317
+ * Create one row: `POST /<collection>`, and the same create reached through
318
+ * a parent, `POST /<parent>/<id>/<relation>`.
319
+ *
320
+ * Errors from here are deliberately not re-classified. This layer cannot
321
+ * tell a constraint violation from an unreachable database, and it used to
322
+ * call both `BAD_REQUEST` — a claim that the caller sent something wrong and
323
+ * should not retry. The driver holds the SQLSTATE and raises an `ApiError`
324
+ * for what is genuinely the request's fault; anything still unclassified
325
+ * here is ours, and that is a 500.
326
+ */
327
+ private createRow;
328
+ /**
329
+ * Update one row: `PATCH /<collection>/<id>`, and the same row reached
330
+ * through a parent. Errors are not re-classified; see {@link createRow}.
331
+ */
332
+ private updateRow;
333
+ /**
334
+ * Delete one row: `DELETE /<collection>/<id>`, and the same row reached
335
+ * through a parent.
336
+ */
337
+ private deleteRow;
221
338
  /**
222
339
  * Create REST routes for a collection using existing Rebase patterns
223
340
  */
@@ -258,17 +375,16 @@ export declare class RestApiGenerator {
258
375
  * @param collectionPath the path to read — a nested listing passes its own
259
376
  * `parent/:id/child` path, which the driver resolves.
260
377
  */
261
- private readPage;
378
+ static readPage(driver: DataDriver, collection: CollectionConfig, queryOptions: QueryOptions, searchString?: string, searchExplain?: boolean, collectionPath?: string): Promise<{
379
+ rows: Record<string, unknown>[];
380
+ meta: Record<string, unknown>;
381
+ }>;
262
382
  /**
263
383
  * Fetch raw collection data without Entity wrapper (fallback for non-Postgres)
264
384
  */
265
- private fetchRawCollection;
385
+ static fetchRawCollection(driver: DataDriver, collection: CollectionConfig, queryOptions: QueryOptions, searchString?: string, searchExplain?: boolean, collectionPath?: string, startAfter?: Record<string, unknown>): Promise<Record<string, unknown>[]>;
266
386
  /**
267
387
  * Count raw entities for a collection
268
388
  */
269
- private countRawEntities;
270
- /**
271
- * Fetch single entity raw data without Entity wrapper (fallback)
272
- */
273
- private fetchRawEntity;
389
+ static countRawEntities(driver: DataDriver, collection: CollectionConfig, queryOptions: QueryOptions, searchString?: string, collectionPath?: string): Promise<number>;
274
390
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * A write to the auth collection is user administration.
3
+ *
4
+ * The auth collection's rows are the users. `/admin/users` holds a change to
5
+ * one to the auth adapter's rules: a create goes through the adapter's user
6
+ * creation (the password hashed, the email normalized, the collection's
7
+ * `onCreateUser`, the invitation), an update may not leave the project without
8
+ * an administrator and stores an email the way sign-in looks it up, and a
9
+ * deletion runs `beforeUserDelete` (a veto), ends the sessions and runs
10
+ * `afterUserDelete`. The data doors that write the same rows — the REST routes
11
+ * and MCP's write tools — call these, so that no door writes a user by rules
12
+ * of its own. (The Postgres socket calls the adapter's methods directly.)
13
+ *
14
+ * Every function here leaves any other collection, and an adapter without the
15
+ * step, exactly as it was: the rows are then written as any table's.
16
+ */
17
+ import type { AuthAdapter, CollectionConfig, UserCreationFinalizeResult, UserCreationPrepareResult } from "@rebasepro/types";
18
+ import type { FieldViewer } from "@rebasepro/common";
19
+ /** Whether `collection` is the auth collection — its rows are the users. */
20
+ export declare function isAuthCollection(collection: CollectionConfig): boolean;
21
+ /** An adapter that performs user creation (`prepareUserCreation`). */
22
+ export type UserCreatingAdapter = AuthAdapter & Required<Pick<AuthAdapter, "prepareUserCreation">>;
23
+ /**
24
+ * Whether a create on `collection` is a user creation `adapter` performs: the
25
+ * collection is the auth collection and the adapter has the step. When not,
26
+ * the door writes the row as any other.
27
+ */
28
+ export declare function createsUsers(adapter: AuthAdapter | undefined, collection: CollectionConfig): adapter is UserCreatingAdapter;
29
+ /**
30
+ * Check a create body for the auth collection against what the adapter says a
31
+ * create consumes (`describeUserCreationContract`).
32
+ *
33
+ * Signups carry credential fields (`password`) the collection does not declare
34
+ * as columns, so the body is checked for everything else. A custom
35
+ * `onCreateUser` owns the body's shape, and then the adapter asks for no check:
36
+ * the collection's fields do not describe what arrived.
37
+ *
38
+ * `rowIndex` names the row in a bulk write's message.
39
+ */
40
+ export declare function assertUserCreationBodyValid(adapter: AuthAdapter | undefined, collection: CollectionConfig, body: Record<string, unknown>, viewer: FieldViewer, rowIndex?: number): void;
41
+ /**
42
+ * The first half of creating users, before anything is written: each body
43
+ * through the adapter's `prepareUserCreation` (the password hashed or
44
+ * generated, the email normalized, the collection's `onCreateUser`), in order.
45
+ * The `values` of each result are what the door writes.
46
+ */
47
+ export declare function prepareUserCreations(adapter: UserCreatingAdapter, collection: CollectionConfig, bodies: readonly Record<string, unknown>[]): Promise<UserCreationPrepareResult[]>;
48
+ /**
49
+ * The second half, once the rows are written — and only then, so a write that
50
+ * fails invites nobody: the credentials' delivery through
51
+ * {@link completeUserCreation}, which is also what `POST /admin/users` calls.
52
+ *
53
+ * @returns For each row, the delivery fields a create response carries
54
+ * (`invitationSent`, and `temporaryPassword` when nobody was sent one).
55
+ */
56
+ export declare function completeUserCreations(adapter: UserCreatingAdapter, prepared: readonly UserCreationPrepareResult[], rows: readonly Record<string, unknown>[]): Promise<UserCreationFinalizeResult[]>;
57
+ /**
58
+ * Create one user through the auth collection: {@link prepareUserCreations},
59
+ * then `save` (the door's own driver write), then
60
+ * {@link completeUserCreations}.
61
+ *
62
+ * `undefined` when the create is not a user creation ({@link createsUsers}) —
63
+ * the door then writes the row as any other.
64
+ */
65
+ export declare function createUserThroughAuthCollection(adapter: AuthAdapter | undefined, collection: CollectionConfig, body: Record<string, unknown>, save: (values: Record<string, unknown>) => Promise<Record<string, unknown>>): Promise<{
66
+ row: Record<string, unknown>;
67
+ delivery: UserCreationFinalizeResult;
68
+ } | undefined>;
69
+ /**
70
+ * Updates to rows of the auth collection, checked and put in stored form by
71
+ * the adapter's `prepareUserUpdates`: judged together, so a bulk demotion of
72
+ * every administrator is refused although each row alone would pass.
73
+ *
74
+ * @returns Each update's values, as they should be written, in order.
75
+ */
76
+ export declare function prepareAuthCollectionUpdates(adapter: AuthAdapter | undefined, collection: CollectionConfig, updates: {
77
+ uid: string;
78
+ values: Record<string, unknown>;
79
+ }[]): Promise<Record<string, unknown>[]>;
80
+ /**
81
+ * Delete rows of the auth collection as user administration: the adapter's
82
+ * checks and `beforeUserDelete` before `remove` runs (either may refuse), its
83
+ * session cleanup and `afterUserDelete` after.
84
+ */
85
+ export declare function deletingAuthCollectionUsers<T>(adapter: AuthAdapter | undefined, collection: CollectionConfig | undefined, uids: string[], remove: () => Promise<T>): Promise<T>;
@@ -1,4 +1,4 @@
1
- import type { CollectionConfig } from "@rebasepro/types";
1
+ import type { CollectionConfig, IncludeSpec } from "@rebasepro/types";
2
2
  import type { LogicalCondition } from "@rebasepro/types";
3
3
  import { type FieldViewer } from "@rebasepro/common";
4
4
  /**
@@ -39,7 +39,7 @@ export declare function requestViewer(c: {
39
39
  get: (key: never) => unknown;
40
40
  }): FieldViewer;
41
41
  /** Which query parameter a refused field arrived in, for the message. */
42
- type Where = "filter" | "orderBy" | "fields" | "select" | "groupBy";
42
+ type Where = "filter" | "orderBy" | "fields" | "select" | "groupBy" | "vector_search";
43
43
  /**
44
44
  * Refuse the request when any of `names` is a field this caller cannot read.
45
45
  *
@@ -62,5 +62,9 @@ export declare function assertQueryFieldsReadable(options: {
62
62
  field: string;
63
63
  }[];
64
64
  fields?: string[];
65
+ include?: IncludeSpec;
66
+ vectorSearch?: {
67
+ property: string;
68
+ };
65
69
  }, collection: CollectionConfig, viewer: FieldViewer | undefined): void;
66
70
  export {};
@@ -10,8 +10,14 @@ import { DataDriver } from "@rebasepro/types";
10
10
  * carrying corrected rows from the retry of an unanswered one, which is the
11
11
  * difference between "here is your earlier answer" and silently discarding a
12
12
  * correction.
13
+ *
14
+ * The query string separates instructions sent to one path: `?hard=true` purges
15
+ * where the same `DELETE` without it soft-deletes, and `?on_conflict=` makes a
16
+ * create an upsert. Left out, a purge sent under the soft delete's key was
17
+ * answered with that delete's `204` and never ran. A request with no query
18
+ * string fingerprints exactly as it did before one could enter.
13
19
  */
14
- export declare function requestFingerprint(method: string, path: string, body: unknown): Promise<string>;
20
+ export declare function requestFingerprint(method: string, path: string, body: unknown, query?: URLSearchParams): Promise<string>;
15
21
  /**
16
22
  * What a caller may do with a key.
17
23
  *
@@ -0,0 +1,46 @@
1
+ import { type CollectionConfig, type ResolvedRelation } from "@rebasepro/types";
2
+ import { type FieldViewer } from "@rebasepro/common";
3
+ /** The last hop of a nested path: the parent, the relation it names, and that relation's target. */
4
+ export interface NestedWriteHop {
5
+ parentCollection: CollectionConfig;
6
+ relation: ResolvedRelation;
7
+ targetCollection: CollectionConfig;
8
+ }
9
+ /**
10
+ * What a write through a nested path does to the row the path addresses.
11
+ *
12
+ * - `create`: a new row under the parent (`POST /bands/7/members`);
13
+ * - `update`: an existing row reached through the parent (`PATCH …/members/3`);
14
+ * - `pivot`: the columns of one many-to-many link (`PATCH …/tags/5 { _pivot }`);
15
+ * - `unlink`: a `DELETE` through a nested path. Through a many-to-many one it
16
+ * drops the link; through an owning one it deletes the row, which writes
17
+ * no relation and is the row's own delete rules' to answer.
18
+ */
19
+ export type NestedWriteKind = "create" | "update" | "pivot" | "unlink";
20
+ /**
21
+ * Refuse a write through a parent that sets a relation the caller may not write.
22
+ *
23
+ * A body is checked against the target's field rules, and that is all the
24
+ * nested routes used to check. But the address writes too, and what it writes
25
+ * is not in the body:
26
+ *
27
+ * - A create under a parent whose target carries the parent's key
28
+ * (`hasMany`/`hasOne`) has that key stamped from the URL — `POST
29
+ * /bands/7/members` sets `members.band_id = 7`, which `POST /members
30
+ * { bandId: 7 }` answers `FIELD_NOT_WRITABLE` for when `members.band` is
31
+ * closed to the caller.
32
+ * - A create is also a new member of the parent's relation, and every write
33
+ * through a many-to-many path is set membership: it links on create and on
34
+ * update, edits the link with `_pivot`, and drops it on delete. That is a
35
+ * write of the parent's relation property — `PATCH /posts/1 { tags: [5] }`
36
+ * when `posts.tags` is closed — and its rules hold here.
37
+ *
38
+ * An update through an owning parent changes neither: the driver refuses it
39
+ * unless the row is already the parent's, and does not write the key.
40
+ *
41
+ * One rule for every door that takes a nested path — the REST routes and the
42
+ * realtime socket — asked of the same fields `assertNoClosedFields` judges a
43
+ * body by, so the answer (`FIELD_NOT_WRITABLE`, or `VALIDATION_EXCLUDED_FIELDS`
44
+ * for an `excludeFromApi` relation) is the one the root route gives.
45
+ */
46
+ export declare function assertNestedWriteAllowed(hop: NestedWriteHop, kind: NestedWriteKind, path: string, viewer: FieldViewer | undefined): void;
@@ -1,5 +1,37 @@
1
- import { CollectionConfig } from "@rebasepro/types";
1
+ import { CollectionConfig, type IncludeSpec } from "@rebasepro/types";
2
2
  import { type FieldViewer } from "@rebasepro/common";
3
+ /**
4
+ * Refuse a write naming a field this caller may not set.
5
+ *
6
+ * `excludeFromApi` means what the generated SDK already says it means: "absent
7
+ * from Row, Insert and Update — the API surface does not mention it, in either
8
+ * direction". The server only ever enforced the read half, so the column a
9
+ * collection had declared unservable was still writable by anyone whose
10
+ * policies let them write the row.
11
+ *
12
+ * On the user store that is the sharp one. `users_write_own` lets you update
13
+ * your own row, `password_hash` is a column on it, and setting it directly is a
14
+ * password change that does not need the old password — so a stolen access
15
+ * token, which expires within the hour, becomes a password the attacker chose.
16
+ * Everything else the flag protects is the same shape: a verification token, a
17
+ * rotation counter, a secret the server owns.
18
+ *
19
+ * `access.write: ["editor"]` is the same rule with a role list instead of an
20
+ * empty one, and answers `FIELD_NOT_WRITABLE` rather than
21
+ * `VALIDATION_EXCLUDED_FIELDS`. Both refuse rather than dropping the key: a
22
+ * write that silently discards a field reports success for an edit that did not
23
+ * happen, which is the one outcome a form cannot recover from.
24
+ *
25
+ * The empty-list half is refused for every caller, not only unprivileged ones,
26
+ * and regardless of `strictWrites`. The framework's own auth paths do not come
27
+ * through here — `prepareUserCreation` builds the row itself — so what is left
28
+ * is callers the rule was written to exclude.
29
+ *
30
+ * Exported for the history revert, which writes a stored version rather than a
31
+ * body and so asks this question of the fields the revert would change, not of
32
+ * every key the snapshot holds. `where` prefixes the message.
33
+ */
34
+ export declare function assertNoClosedFields(values: Record<string, unknown>, collection: CollectionConfig, where: string, viewer: FieldViewer | undefined): void;
3
35
  /**
4
36
  * Reject a write naming a field the collection does not have.
5
37
  *
@@ -119,7 +151,7 @@ export declare function assertWriteValuesValid(values: Record<string, unknown>,
119
151
  * assuming one.
120
152
  */
121
153
  export declare function projectResponseFields<T extends Record<string, unknown>>(rows: T[], fields: readonly string[] | undefined, collection: CollectionConfig, options?: {
122
- include?: readonly string[];
154
+ include?: IncludeSpec;
123
155
  }): T[];
124
156
  /**
125
157
  * Both write checks, as one call, for a transport that is not the REST router.
@@ -31,8 +31,24 @@ export type HonoEnv = {
31
31
  */
32
32
  recipientEmail?: string;
33
33
  driver?: DataDriver;
34
- /** Set when the request is authenticated via a Service API Key. */
34
+ /** Set when the request is authenticated with an API key (`rk_`). */
35
35
  apiKey?: ApiKeyMasked;
36
+ /**
37
+ * What the caller may do, when its credential is narrower than its
38
+ * person — an API key or an MCP token. Absent for a person's own
39
+ * session, who holds what their roles hold. Read it through
40
+ * `callerScopes` / `hasScope`, which fill in that second case.
41
+ */
42
+ scopes?: string[];
43
+ /**
44
+ * Who asked, when an administrator's request runs as another user
45
+ * (`x-rebase-impersonate`). `user` and `driver` are then the
46
+ * impersonated user's; this is the only place the administrator
47
+ * remains. See `auth/impersonation.ts`.
48
+ */
49
+ impersonator?: {
50
+ uid: string;
51
+ };
36
52
  /** Unique request correlation ID (generated or propagated from X-Request-ID header). */
37
53
  requestId?: string;
38
54
  /**