@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
@@ -16,36 +16,48 @@
16
16
  * - A write can fail with a permission error the tool cannot explain in
17
17
  * detail, because the policy that refused it is not visible from here.
18
18
  *
19
- * `mcp:write` gates whether the mutating tools are *offered* at all. It is a
20
- * second lock, not the main one — a `mcp:write` token still cannot write a row
21
- * the user could not write themselves.
19
+ * The scopes gate which tools are *offered* and which collections each
20
+ * reaches — `data:write:posts` offers the write tools for `posts` alone. That
21
+ * is a second lock, not the main one: a `data:write` token still cannot write
22
+ * a row the user could not write themselves.
22
23
  */
23
- import type { CollectionConfig, DataDriver } from "@rebasepro/types";
24
+ import type { AuthAdapter, CollectionConfig, DataDriver } from "@rebasepro/types";
25
+ import type { McpScope } from "./oauth-metadata.js";
24
26
  /** The identity a tool call runs as. Comes from the verified access token. */
25
27
  export interface McpCaller {
26
28
  uid: string;
27
29
  roles: string[];
28
- scope: string;
30
+ /** The scopes granted to this connection — `data:read`, `data:write:posts`, … */
31
+ scopes: string[];
29
32
  clientId: string;
30
33
  }
31
34
  export interface McpToolContext {
32
35
  driver: DataDriver;
33
36
  collections: CollectionConfig[];
34
37
  caller: McpCaller;
38
+ /**
39
+ * The deployment's auth adapter. The auth collection's rows are the users,
40
+ * so the write tools hand a write to one to it, as REST and `/admin/users`
41
+ * do. See `api/rest/auth-collection-writes.ts`.
42
+ */
43
+ authAdapter?: AuthAdapter;
35
44
  }
36
45
  export interface McpToolDefinition {
37
46
  name: string;
38
47
  description: string;
39
48
  inputSchema: Record<string, unknown>;
40
- /** The scope a caller must hold for this tool to be listed or callable. */
41
- requiredScope: "mcp:read" | "mcp:write";
49
+ /**
50
+ * The scope a caller must hold — on at least one collection for the tool
51
+ * to be listed, and on the collection a call names for it to run.
52
+ */
53
+ requiredScope: McpScope;
42
54
  run(args: Record<string, unknown>, ctx: McpToolContext): Promise<unknown>;
43
55
  }
44
56
  /** An error whose message is safe to hand back to the model. */
45
57
  export declare class McpToolError extends Error {
46
58
  }
47
59
  export declare const MCP_TOOLS: McpToolDefinition[];
48
- /** The tools a caller holding `scope` may see and call. */
49
- export declare function toolsForScope(scope: string): McpToolDefinition[];
60
+ /** The tools a caller holding `scopes` may see and call: each needs its scope on at least one collection. */
61
+ export declare function toolsForScopes(scopes: readonly string[]): McpToolDefinition[];
50
62
  /** Look up a tool by name, honouring scope — an unlisted tool is not callable. */
51
- export declare function findTool(name: string, scope: string): McpToolDefinition | undefined;
63
+ export declare function findTool(name: string, scopes: readonly string[]): McpToolDefinition | undefined;
@@ -1,20 +1,24 @@
1
1
  /**
2
- * What a token may do at `/mcp`.
2
+ * What a token may do at `/mcp`: the data-plane scopes its tools need, in the
3
+ * vocabulary every other credential uses — `data:read`, `data:write`,
4
+ * `data:delete` — each narrowable to one collection (`data:read:posts`).
3
5
  *
4
- * Two, and no more. RFC 9728 asks `scopes_supported` to be the minimal set that
5
- * makes the resource usable, and the MCP specification's scope-minimization
6
- * guidance points the same way: a client that only needs to read should be able
7
- * to ask for exactly that, and a consent screen listing eleven fine-grained
8
- * permissions is one nobody reads.
9
- *
10
- * They are not a substitute for RLS. A `mcp:read` token still reads through the
11
- * user's own policies — the scope decides whether the tool exists for this
12
- * session, the database decides which rows come back.
6
+ * They are not a substitute for RLS. A `data:read` token still reads through
7
+ * the user's own policies — the scope decides whether a tool exists for this
8
+ * session and which collections it reaches; the database decides which rows
9
+ * come back.
13
10
  */
14
- export declare const MCP_SCOPES: readonly ["mcp:read", "mcp:write"];
11
+ export declare const MCP_SCOPES: readonly ["data:read", "data:write", "data:delete"];
15
12
  export type McpScope = (typeof MCP_SCOPES)[number];
16
13
  /** The scope a client gets when it asks for nothing in particular. */
17
- export declare const DEFAULT_MCP_SCOPE = "mcp:read";
14
+ export declare const DEFAULT_MCP_SCOPE = "data:read";
15
+ /**
16
+ * A scope string as granted before `/mcp` spoke the shared vocabulary, read as
17
+ * what it granted: `mcp:read` was every read, `mcp:write` every create, change
18
+ * and delete. Applied to stored refresh grants and to access tokens still in
19
+ * flight, so a connection made before the change keeps exactly its reach.
20
+ */
21
+ export declare function upgradeStoredMcpScope(scope: string): string;
18
22
  /**
19
23
  * The canonical resource identifier for this server's MCP endpoint.
20
24
  *
@@ -93,12 +97,13 @@ export declare function redirectUriAllowed(candidate: string, registered: string
93
97
  *
94
98
  * An unknown scope is dropped rather than refused. RFC 6749 §3.3 allows either,
95
99
  * and dropping is what keeps a client that asks for `openid profile email` out
96
- * of habit from failing to connect at all — it gets the MCP scopes it is
97
- * entitled to and none of the ones it invented.
100
+ * of habit from failing to connect at all — it gets the data scopes it is
101
+ * entitled to and none of the ones it invented. A collection target is kept:
102
+ * `data:read:posts` is a narrower grant, never a wider one.
98
103
  */
99
104
  export declare function narrowScope(requested: string | undefined | null): string;
100
- /** Does a granted scope string carry this permission? */
101
- export declare function scopeAllows(scope: string, needed: McpScope): boolean;
105
+ /** A granted scope string as the list of scopes it holds. */
106
+ export declare function scopeList(scope: string): string[];
102
107
  /**
103
108
  * The `WWW-Authenticate` value for an unauthenticated request.
104
109
  *
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import { Hono } from "hono";
27
27
  import type { HonoEnv } from "../api/types.js";
28
+ import type { AuthRepository } from "../auth/interfaces.js";
28
29
  import type { OAuthStore } from "./oauth-store.js";
29
30
  export interface OAuthRoutesConfig {
30
31
  store: OAuthStore;
@@ -36,7 +37,33 @@ export interface OAuthRoutesConfig {
36
37
  authBasePath: string;
37
38
  /** Whether open registration is permitted. */
38
39
  allowDynamicRegistration: boolean;
40
+ /**
41
+ * Who a grant's user is now. Absent when the auth adapter exposes no user
42
+ * repository, and then a grant carries the roles it was consented with.
43
+ */
44
+ identity?: McpGrantIdentity;
39
45
  }
46
+ /**
47
+ * The account a grant acts for, read where a grant is issued or renewed.
48
+ *
49
+ * A refresh has no session to re-read anything from, so without this a grant
50
+ * is a snapshot of the moment of consent: the roles it was given, for an
51
+ * account that may since have been demoted, signed out everywhere, or deleted.
52
+ */
53
+ export interface McpGrantIdentity {
54
+ /** The account's roles as they are now, or null when it no longer exists. */
55
+ currentRoles(uid: string): Promise<string[] | null>;
56
+ /** Where "sign out everywhere" and every password change leave their mark. */
57
+ revocation?: Pick<AuthRepository, "getTokensValidAfter">;
58
+ }
59
+ /**
60
+ * The grant identity the auth repository can answer, or undefined when the
61
+ * adapter exposes no repository that can say whether an account exists.
62
+ *
63
+ * The same repository the admin gate re-reads roles and the revocation
64
+ * watermark from, so a grant is held to what a session is held to.
65
+ */
66
+ export declare function grantIdentityFromRepository(repo: Partial<Pick<AuthRepository, "getUserById" | "getUserRoleIds" | "getTokensValidAfter">> | undefined): McpGrantIdentity | undefined;
40
67
  export declare function createOAuthRoutes(config: OAuthRoutesConfig): Hono<HonoEnv>;
41
68
  /**
42
69
  * Why a redirect URI cannot be registered, or null if it can.
@@ -45,7 +72,13 @@ export declare function createOAuthRoutes(config: OAuthRoutesConfig): Hono<HonoE
45
72
  * means a stored row is never itself the hazard.
46
73
  */
47
74
  export declare function redirectUriProblem(uri: string): string | null;
48
- /** Human-readable consent lines, one per granted scope. */
75
+ /**
76
+ * Human-readable consent lines, one per granted scope.
77
+ *
78
+ * Worded for the person consenting, not taken from the scope catalogue: what
79
+ * they need to read here is that the client gets *their own* access and no
80
+ * more, and which collections it reaches when the grant names some.
81
+ */
49
82
  export declare function describeScopes(scope: string): {
50
83
  scope: string;
51
84
  description: string;
@@ -53,26 +53,29 @@ export interface RefreshTokenRecord {
53
53
  clientId: string;
54
54
  uid: string;
55
55
  /**
56
- * The roles the grant was made with.
56
+ * The roles the token was minted with.
57
57
  *
58
- * Carried here because a refresh has no session to re-read them from, and
59
- * an access token minted with an empty `roles` is not a smaller grant — it
60
- * is a DIFFERENT identity to the database. Any policy written as "a row this
61
- * user's role may see" evaluates against an empty list and returns nothing,
62
- * so dropping them turns the first token refresh into an integration that
63
- * silently stops seeing data.
64
- *
65
- * The consequence of storing them is that a role change does not reach an
66
- * existing grant until the refresh token expires or the user revokes the
67
- * client. That is stated in the docs, and it is the trade this design makes
68
- * knowingly: the alternative is a user lookup on every refresh, which puts
69
- * the auth adapter on a path that currently has no dependency on it.
58
+ * A refresh re-reads the account's roles when the routes are given an
59
+ * identity to ask (`McpGrantIdentity`), so these are what a grant falls
60
+ * back on when there is none — an auth adapter that exposes no user
61
+ * repository. The fallback is these rather than an empty list because an
62
+ * access token minted with no `roles` is not a smaller grant — it is a
63
+ * DIFFERENT identity to the database: any policy written as "a row this
64
+ * user's role may see" evaluates against an empty list and returns nothing.
70
65
  */
71
66
  roles: string[];
72
67
  scope: string;
73
68
  resource: string;
74
69
  family: string;
75
70
  }
71
+ /** A refresh token that could be spent right now, and when it was minted. */
72
+ export interface LiveRefreshToken extends RefreshTokenRecord {
73
+ /**
74
+ * When this token — not its family — was issued. Compared with the user's
75
+ * revocation watermark: a token minted before "sign out everywhere" is void.
76
+ */
77
+ issuedAt: Date;
78
+ }
76
79
  export interface OAuthStore {
77
80
  ensureTables(): Promise<void>;
78
81
  registerClient(client: OAuthClient): Promise<void>;
@@ -91,6 +94,19 @@ export interface OAuthStore {
91
94
  * the same family before returning null.
92
95
  */
93
96
  consumeRefreshToken(token: string): Promise<RefreshTokenRecord | null>;
97
+ /**
98
+ * A live refresh token's grant, WITHOUT spending it.
99
+ *
100
+ * What the token endpoint checks before it rotates: whose client it is,
101
+ * the scope asked for, and whether the account behind it still stands.
102
+ * Spending first and checking after means a refusal — or a database blip
103
+ * in the account lookup — leaves the holder with a spent token, and their
104
+ * retry reads as a replay that kills the family.
105
+ *
106
+ * Returns null for anything {@link consumeRefreshToken} would refuse, and
107
+ * writes nothing: a replayed token is detected when it is spent.
108
+ */
109
+ peekRefreshToken(token: string): Promise<LiveRefreshToken | null>;
94
110
  revokeFamily(family: string): Promise<void>;
95
111
  /**
96
112
  * Revoke the family a token belongs to, for RFC 7009 — without spending it.
@@ -1,4 +1,4 @@
1
- import type { DataDriver } from "@rebasepro/types";
1
+ import { type DataDriver } from "@rebasepro/types";
2
2
  import { type MetricSeries, type SeriesPoint } from "./history-store.js";
3
3
  export interface MetricsHistory {
4
4
  /** Create the table and sweep what has aged out. */
@@ -2,9 +2,44 @@ import { createRequire as __rebaseCreateRequire } from "module";
2
2
  import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
- import { B as fieldKeyForColumn, E as effectiveAccess, R as getTenantConfig, U as isRelationRequired, V as findRelation, W as resolveCollectionRelations, ut as isToMany } from "./src-1vL-I1Po.js";
6
- import "./src-Br6ARbs6.js";
5
+ import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
+ import { B as findRelation, H as isRelationRequired, L as getTenantConfig, T as effectiveAccess, W as resolveCollectionRelations, ft as isToMany, z as fieldKeyForColumn } from "./src-I3aG1PcY.js";
7
+ import "./src-CatHFUym.js";
8
+ import { n as HARD_DELETE_QUERY_PARAM, t as DELETED_QUERY_PARAM } from "./soft-delete-params-BWPilMPF.js";
9
+ /**
10
+ * The property a `softDelete` collection records a deletion in — its wire
11
+ * name, what a delete stamps and what a restore (an update to `null`) clears —
12
+ * or `undefined` for a collection without soft delete.
13
+ *
14
+ * One reading of the declaration for every layer that needs it: the driver
15
+ * that stamps and filters on it, and the REST route that has to recognise a
16
+ * restore before it looks the row up.
17
+ */
18
+ function softDeleteFieldOf(collection) {
19
+ const declared = collection && "softDelete" in collection ? collection.softDelete : void 0;
20
+ if (!declared) return void 0;
21
+ return typeof declared === "object" && declared.field || "deletedAt";
22
+ }
23
+ /**
24
+ * Whether an update's values restore a soft-deleted row: they set the
25
+ * soft-delete field back to `null`, the documented restore.
26
+ *
27
+ * A restore is an ordinary update of a row every default read hides, so a door
28
+ * that looks the row up before updating it has to include the trashed rows for
29
+ * this update — and only for this one: any other edit of a trashed row is a
30
+ * 404.
31
+ */
32
+ function restoresSoftDeletedRow(collection, values) {
33
+ const field = softDeleteFieldOf(collection);
34
+ return field !== void 0 && values?.[field] === null;
35
+ }
36
+ //#endregion
7
37
  //#region src/api/openapi-generator.ts
38
+ var openapi_generator_exports = /* @__PURE__ */ __exportAll({
39
+ buildCollectionInputSchema: () => buildCollectionInputSchema,
40
+ buildCollectionSchema: () => buildCollectionSchema,
41
+ generateOpenApiSpec: () => generateOpenApiSpec
42
+ });
8
43
  function generateOpenApiSpec(collections, options = {}) {
9
44
  const basePath = options.basePath ?? "/api";
10
45
  const requireAuth = options.requireAuth ?? true;
@@ -108,7 +143,7 @@ function generateOpenApiSpec(collections, options = {}) {
108
143
  name: "distinct",
109
144
  in: "query",
110
145
  schema: { type: "boolean" },
111
- description: "`SELECT DISTINCT` over the returned columns. Only meaningful alongside `fields`: the primary key is always in the projection, so without narrowing it every row is already distinct. `meta.total` counts distinct rows too. Refused (400) alongside `searchString` or a vector search, which attach a per-row score that makes every row distinct by construction, and (400 DISTINCT_ORDER_BY_NOT_SELECTED) when `orderBy` names a column `fields` does not return.",
146
+ description: "`SELECT DISTINCT` over the returned columns. Only meaningful alongside `fields`: the primary key is always in the projection, so without narrowing it every row is already distinct. The response has no `meta.total` — the rows before deduplication are a different set, so their count would not describe the page — and `meta.hasMore` is true when the page came back full. Refused (400) alongside `searchString` or a vector search, which attach a per-row score that makes every row distinct by construction, and (400 DISTINCT_ORDER_BY_NOT_SELECTED) when `orderBy` names a column `fields` does not return.",
112
147
  example: "true"
113
148
  },
114
149
  {
@@ -183,7 +218,7 @@ function generateOpenApiSpec(collections, options = {}) {
183
218
  properties: {
184
219
  total: {
185
220
  type: "integer",
186
- description: "Total number of matching records"
221
+ description: "Total number of matching records. Absent on a `distinct` read, which has no count of the rows it returns."
187
222
  },
188
223
  limit: {
189
224
  type: "integer",
@@ -220,7 +255,50 @@ function generateOpenApiSpec(collections, options = {}) {
220
255
  const paths = spec.paths;
221
256
  const schemas = spec.components.schemas;
222
257
  const tags = spec.tags;
223
- const reservedParameterNames = new Set(listQueryParameters().map((p) => p.name));
258
+ const reservedParameterNames = /* @__PURE__ */ new Set([...listQueryParameters().map((p) => p.name), DELETED_QUERY_PARAM]);
259
+ /**
260
+ * `?deleted=` on the reads of a `softDelete` collection. A delete there
261
+ * moves the row to the trash, and every read hides trashed rows unless it
262
+ * asks for them; the list, the single row, the count and the aggregate all
263
+ * honour this. Empty for any other collection.
264
+ */
265
+ const deletedParameters = (collection) => {
266
+ const field = softDeleteFieldOf(collection);
267
+ if (!field) return [];
268
+ return [{
269
+ name: DELETED_QUERY_PARAM,
270
+ in: "query",
271
+ required: false,
272
+ schema: {
273
+ type: "string",
274
+ enum: ["include", "only"]
275
+ },
276
+ description: `Rows a delete moved to the trash (\`${field}\` set) are left out unless this asks for them: \`include\` reads them with the live rows, \`only\` reads them alone. Any other value is a 400 INVALID_DELETED_PARAM.`
277
+ }];
278
+ };
279
+ /**
280
+ * `?hard=` on the deletes of a `softDelete` collection: delete for good
281
+ * rather than move to the trash. Empty for any other collection.
282
+ */
283
+ const hardDeleteParameters = (collection) => {
284
+ if (!softDeleteFieldOf(collection)) return [];
285
+ return [{
286
+ name: HARD_DELETE_QUERY_PARAM,
287
+ in: "query",
288
+ required: false,
289
+ schema: {
290
+ type: "boolean",
291
+ default: false
292
+ },
293
+ description: "`true` deletes for good instead of moving to the trash — a row already in the trash included, which is how the trash is emptied. Only `true`/`1` and `false`/`0` are accepted; anything else is a 400 INVALID_HARD_PARAM."
294
+ }];
295
+ };
296
+ /** What a delete does to a `softDelete` collection's row, for the delete operations' descriptions. */
297
+ const softDeleteBehaviour = (collection) => {
298
+ const field = softDeleteFieldOf(collection);
299
+ if (!field) return void 0;
300
+ return `This collection soft-deletes: a delete moves the row to the trash by stamping \`${field}\`, and every read leaves it out unless it passes \`?deleted=include\` or \`?deleted=only\`. A PATCH setting \`${field}\` back to \`null\` restores it. Pass \`?hard=true\` to delete for good.`;
301
+ };
224
302
  const registeredSchemas = new Set((collections || []).map(schemaNameFor));
225
303
  /**
226
304
  * `Prefer: return=minimal`, on every route that would otherwise send a row
@@ -387,12 +465,17 @@ function generateOpenApiSpec(collections, options = {}) {
387
465
  schemas[`${schemaName}Input`] = buildCollectionInputSchema(collection);
388
466
  schemas[`${schemaName}Update`] = buildCollectionUpdateSchema(collection);
389
467
  const dataPath = `/data/${slug}`;
468
+ const softDeleteNote = softDeleteBehaviour(collection);
390
469
  paths[`${dataPath}/count`] = { get: {
391
470
  tags: [collection.name],
392
471
  summary: `Count ${collection.name}`,
393
472
  description: "The number of rows the same filters would return, without returning them. Takes the filter and search parameters of the list endpoint; `limit`, `offset` and `orderBy` are not part of the question and are ignored.",
394
473
  operationId: `count${schemaName}`,
395
- parameters: [...listQueryParameters().filter((p) => p.name === "searchString"), ...buildFilterParameters(collection, reservedParameterNames)],
474
+ parameters: [
475
+ ...listQueryParameters().filter((p) => p.name === "searchString"),
476
+ ...deletedParameters(collection),
477
+ ...buildFilterParameters(collection, reservedParameterNames)
478
+ ],
396
479
  responses: {
397
480
  200: {
398
481
  description: "The number of matching rows",
@@ -430,7 +513,16 @@ function generateOpenApiSpec(collections, options = {}) {
430
513
  schema: { type: "string" },
431
514
  example: "status"
432
515
  },
433
- ...listQueryParameters().filter((p) => p.name === "searchString" || p.name === "limit"),
516
+ ...listQueryParameters().filter((p) => p.name === "searchString" || p.name === "limit" || p.name === "offset" || p.name === "page"),
517
+ {
518
+ name: "orderBy",
519
+ in: "query",
520
+ required: false,
521
+ description: "Sort for the groups, by a `groupBy` field or an aggregate's result key (`count`, `sum_total`): `key:asc`, `key:desc` or `key:desc:last`. Groups it leaves tied are ordered by the `groupBy` fields, so every page boundary falls in the same place. `offset`, `page` and `orderBy` need `groupBy` — without it the result is one row — and are otherwise a 400 INVALID_AGGREGATE_WINDOW.",
522
+ schema: { type: "string" },
523
+ example: "count:desc"
524
+ },
525
+ ...deletedParameters(collection),
434
526
  ...buildFilterParameters(collection, reservedParameterNames)
435
527
  ],
436
528
  responses: {
@@ -439,13 +531,27 @@ function generateOpenApiSpec(collections, options = {}) {
439
531
  content: { "application/json": { schema: {
440
532
  type: "object",
441
533
  required: ["data"],
442
- properties: { data: {
443
- type: "array",
444
- items: {
534
+ properties: {
535
+ data: {
536
+ type: "array",
537
+ items: {
538
+ type: "object",
539
+ additionalProperties: true
540
+ }
541
+ },
542
+ meta: {
445
543
  type: "object",
446
- additionalProperties: true
544
+ description: "The page of groups. Present when `groupBy` is.",
545
+ properties: {
546
+ limit: { type: "integer" },
547
+ offset: { type: "integer" },
548
+ hasMore: {
549
+ type: "boolean",
550
+ description: "Whether a group follows this page."
551
+ }
552
+ }
447
553
  }
448
- } }
554
+ }
449
555
  } } }
450
556
  },
451
557
  501: { description: "This backend's data driver does not implement aggregates" },
@@ -457,7 +563,11 @@ function generateOpenApiSpec(collections, options = {}) {
457
563
  tags: [collection.name],
458
564
  summary: `List ${collection.name}`,
459
565
  operationId: `list${schemaName}`,
460
- parameters: [...listQueryParameters(), ...buildFilterParameters(collection, reservedParameterNames)],
566
+ parameters: [
567
+ ...listQueryParameters(),
568
+ ...deletedParameters(collection),
569
+ ...buildFilterParameters(collection, reservedParameterNames)
570
+ ],
461
571
  responses: {
462
572
  200: {
463
573
  description: "Paginated list of entities",
@@ -627,9 +737,9 @@ function generateOpenApiSpec(collections, options = {}) {
627
737
  paths[`/data/${slug}/bulk/delete`] = { post: {
628
738
  tags: [collection.name],
629
739
  summary: `Delete many ${collection.name} in one transaction`,
630
- description: "A POST, not `DELETE /bulk` with a body. Bodies on DELETE are permitted but widely dropped by proxies and CDNs, and several generators ignore `requestBody` on a DELETE operation — a generated client would send the request with no ids at all. Takes ids rather than a filter: a mistyped condition that empties a table cannot be reviewed at the call site the way an explicit list can. `beforeDelete`/`afterDelete` fire per row.",
740
+ description: "A POST, not `DELETE /bulk` with a body. Bodies on DELETE are permitted but widely dropped by proxies and CDNs, and several generators ignore `requestBody` on a DELETE operation — a generated client would send the request with no ids at all. Takes ids rather than a filter: a mistyped condition that empties a table cannot be reviewed at the call site the way an explicit list can. `beforeDelete`/`afterDelete` fire per row." + (softDeleteNote ? `\n\n${softDeleteNote}` : ""),
631
741
  operationId: `deleteMany${schemaName}`,
632
- parameters: [idempotencyHeader],
742
+ parameters: [idempotencyHeader, ...hardDeleteParameters(collection)],
633
743
  requestBody: {
634
744
  required: true,
635
745
  content: { "application/json": { schema: {
@@ -643,7 +753,7 @@ function generateOpenApiSpec(collections, options = {}) {
643
753
  },
644
754
  responses: {
645
755
  200: {
646
- description: "How many rows were deleted",
756
+ description: softDeleteNote ? "How many rows were moved to the trash, or deleted for good with `?hard=true`" : "How many rows were deleted",
647
757
  content: { "application/json": { schema: {
648
758
  type: "object",
649
759
  properties: { meta: {
@@ -686,7 +796,8 @@ function generateOpenApiSpec(collections, options = {}) {
686
796
  schema: { type: "string" },
687
797
  description: "Comma-separated columns to return, as a SELECT projection. The primary key always survives and `excludeFromApi` columns stay hidden.",
688
798
  example: "id,title"
689
- }
799
+ },
800
+ ...deletedParameters(collection)
690
801
  ],
691
802
  responses: {
692
803
  200: {
@@ -713,6 +824,7 @@ function generateOpenApiSpec(collections, options = {}) {
713
824
  delete: {
714
825
  tags: [collection.name],
715
826
  summary: `Delete ${collection.singularName || collection.name}`,
827
+ ...softDeleteNote ? { description: softDeleteNote } : {},
716
828
  operationId: `delete${schemaName}`,
717
829
  parameters: [
718
830
  {
@@ -729,10 +841,11 @@ function generateOpenApiSpec(collections, options = {}) {
729
841
  required: false,
730
842
  schema: { type: "string" },
731
843
  description: "Names this delete so a retry replays its answer. A delete replayed after the first attempt committed would otherwise answer 404 — which an offline queue reads as a permanent failure for a delete that in fact succeeded."
732
- }
844
+ },
845
+ ...hardDeleteParameters(collection)
733
846
  ],
734
847
  responses: {
735
- 204: { description: "Deleted successfully" },
848
+ 204: { description: softDeleteNote ? "Moved to the trash: the row still exists and can be restored. With `?hard=true`, deleted for good" : "Deleted successfully" },
736
849
  404: {
737
850
  description: "Entity not found",
738
851
  content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
@@ -1055,7 +1168,7 @@ function updateOperation(collection, schemaName, requireAuth, shared) {
1055
1168
  return {
1056
1169
  tags: [collection.name],
1057
1170
  summary: `Update ${collection.singularName || collection.name}`,
1058
- description: "Partial update: only the properties present in the body are written; the rest are left unchanged.\n\nA property's value may instead be a field operation — `{ \"views\": { \"$inc\": 1 } }`, `{ \"tags\": { \"$push\": \"new\" } }`, `{ \"tags\": { \"$pull\": \"old\" } }`, `{ \"meta\": { \"$merge\": { \"seen\": true } } }` — which is applied inside the statement holding the row lock. That is the difference between a counter that is correct under concurrency and one that silently loses increments, because expressing the same change as a value means reading it first. `$inc` needs a `number` property, `$push`/`$pull` an `array`, `$merge` a `map`; anything else is a 400. See the `FieldOperation` schema.",
1171
+ description: "Partial update: only the properties present in the body are written; the rest are left unchanged.\n\nA property's value may instead be a field operation — `{ \"views\": { \"$inc\": 1 } }`, `{ \"tags\": { \"$push\": \"new\" } }`, `{ \"tags\": { \"$pull\": \"old\" } }`, `{ \"meta\": { \"$merge\": { \"seen\": true } } }` — which is applied inside the statement holding the row lock. That is the difference between a counter that is correct under concurrency and one that silently loses increments, because expressing the same change as a value means reading it first. `$inc` needs a `number` property, `$push`/`$pull` an `array`, `$merge` a `map`; anything else is a 400. Pushed elements and merged keys answer to the property's own rules, as a value would. See the `FieldOperation` schema.",
1059
1172
  operationId: `update${schemaName}`,
1060
1173
  parameters: [
1061
1174
  {
@@ -1424,6 +1537,6 @@ function toPascalCase(str) {
1424
1537
  return str.replace(/[^a-zA-Z0-9]+/g, " ").split(" ").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
1425
1538
  }
1426
1539
  //#endregion
1427
- export { generateOpenApiSpec };
1540
+ export { softDeleteFieldOf as a, restoresSoftDeletedRow as i, buildCollectionSchema as n, openapi_generator_exports as r, buildCollectionInputSchema as t };
1428
1541
 
1429
- //# sourceMappingURL=openapi-generator-D8uFz-LW.js.map
1542
+ //# sourceMappingURL=openapi-generator-DAq_XVDu.js.map