@rebasepro/server 0.23.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 (181) hide show
  1. package/README.md +1 -1
  2. package/bin/rebase-server.js +4 -2
  3. package/dist/{GCSStorageController-CjrA4PMo.js → GCSStorageController-BSiP1c-f.js} +22 -8
  4. package/dist/GCSStorageController-BSiP1c-f.js.map +1 -0
  5. package/dist/{S3StorageController-B6pKDNVj.js → S3StorageController-CAwFRgjV.js} +19 -7
  6. package/dist/S3StorageController-CAwFRgjV.js.map +1 -0
  7. package/dist/api/ast-schema-editor.d.ts +92 -1
  8. package/dist/api/errors.d.ts +9 -0
  9. package/dist/api/live-schema-routes.d.ts +38 -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 +44 -10
  13. package/dist/api/rest/write-validation.d.ts +2 -2
  14. package/dist/api/types.d.ts +17 -1
  15. package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CWqS_sLJ.js} +309 -11
  16. package/dist/ast-schema-editor-CWqS_sLJ.js.map +1 -0
  17. package/dist/auth/access.d.ts +105 -0
  18. package/dist/auth/adapter-middleware.d.ts +2 -1
  19. package/dist/auth/address-ownership.d.ts +16 -1
  20. package/dist/auth/admin-roles-route.d.ts +4 -2
  21. package/dist/auth/admin-roles.d.ts +17 -20
  22. package/dist/auth/admin-users-route.d.ts +1 -0
  23. package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
  24. package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
  25. package/dist/auth/api-keys/api-key-store.d.ts +31 -8
  26. package/dist/auth/api-keys/api-key-types.d.ts +14 -16
  27. package/dist/auth/api-keys/http-operation.d.ts +19 -0
  28. package/dist/auth/api-keys/index.d.ts +11 -11
  29. package/dist/auth/api-keys/key-grant.d.ts +41 -0
  30. package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
  31. package/dist/auth/auth-hooks.d.ts +46 -7
  32. package/dist/auth/builtin-auth-adapter.d.ts +8 -0
  33. package/dist/auth/cookie-utils.d.ts +7 -0
  34. package/dist/auth/deliverable-address.d.ts +6 -0
  35. package/dist/auth/email-change-routes.d.ts +41 -0
  36. package/dist/auth/expired-token-sweep.d.ts +67 -0
  37. package/dist/auth/impersonation.d.ts +110 -0
  38. package/dist/auth/index.d.ts +4 -2
  39. package/dist/auth/interfaces.d.ts +110 -59
  40. package/dist/auth/jwt.d.ts +49 -3
  41. package/dist/auth/magic-link-routes.d.ts +2 -6
  42. package/dist/auth/mfa-routes.d.ts +2 -9
  43. package/dist/auth/middleware.d.ts +17 -5
  44. package/dist/auth/otp-routes.d.ts +2 -6
  45. package/dist/auth/passwordless-signup.d.ts +27 -0
  46. package/dist/auth/platform-token.d.ts +122 -0
  47. package/dist/auth/rate-limiter.d.ts +41 -0
  48. package/dist/auth/routes.d.ts +45 -0
  49. package/dist/auth/scope-routes.d.ts +22 -0
  50. package/dist/auth/session-routes.d.ts +11 -6
  51. package/dist/auth/token-revocation.d.ts +50 -1
  52. package/dist/auth/verify-credential.d.ts +28 -0
  53. package/dist/{auth-B-GIMpDG.js → auth-DMLngxn_.js} +2159 -569
  54. package/dist/auth-DMLngxn_.js.map +1 -0
  55. package/dist/backend-DTAOsLQc.js.map +1 -1
  56. package/dist/backup/backup-common.d.ts +10 -0
  57. package/dist/backup/backup-routes.d.ts +24 -4
  58. package/dist/backup/backup-schedule.d.ts +33 -0
  59. package/dist/backup/backup-storage.d.ts +14 -0
  60. package/dist/backup/index.d.ts +2 -0
  61. package/dist/backup-CN0s50D2.js +444 -0
  62. package/dist/backup-CN0s50D2.js.map +1 -0
  63. package/dist/boot/bundle.d.ts +19 -0
  64. package/dist/boot/env.d.ts +49 -4
  65. package/dist/boot/security-headers.d.ts +26 -0
  66. package/dist/boot/static-routing.d.ts +56 -0
  67. package/dist/collection_patch-BRu-BvDv.js +472 -0
  68. package/dist/collection_patch-BRu-BvDv.js.map +1 -0
  69. package/dist/{contract-routes-CbFjuBwa.js → contract-routes-fz8i4pxs.js} +17 -4
  70. package/dist/contract-routes-fz8i4pxs.js.map +1 -0
  71. package/dist/cron/cron-scheduler.d.ts +25 -20
  72. package/dist/cron/cron-store.d.ts +6 -2
  73. package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-CwaANlOG.js} +4 -4
  74. package/dist/cron-loader-CwaANlOG.js.map +1 -0
  75. package/dist/{cron-routes-eE8nif_b.js → cron-routes-Bc-SB0Se.js} +10 -7
  76. package/dist/cron-routes-Bc-SB0Se.js.map +1 -0
  77. package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-CYQgco86.js} +52 -34
  78. package/dist/cron-scheduler-CYQgco86.js.map +1 -0
  79. package/dist/{cron-store-TcoGz-xS.js → cron-store-D2Q9-Aco.js} +10 -15
  80. package/dist/cron-store-D2Q9-Aco.js.map +1 -0
  81. package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
  82. package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
  83. package/dist/email/index.d.ts +2 -2
  84. package/dist/email/templates.d.ts +22 -0
  85. package/dist/email/types.d.ts +26 -0
  86. package/dist/env.d.ts +1 -2
  87. package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
  88. package/dist/errors-D6_y86c5.js.map +1 -0
  89. package/dist/{function-loader-xnbDAPfa.js → function-loader-D7o5Epjj.js} +2 -2
  90. package/dist/{function-loader-xnbDAPfa.js.map → function-loader-D7o5Epjj.js.map} +1 -1
  91. package/dist/{function-routes-Chet4-lB.js → function-routes-CaNG4waN.js} +24 -12
  92. package/dist/function-routes-CaNG4waN.js.map +1 -0
  93. package/dist/functions/context.d.ts +17 -6
  94. package/dist/functions/guards.d.ts +22 -5
  95. package/dist/functions/index.d.ts +2 -2
  96. package/dist/functions/index.js +90 -36
  97. package/dist/functions/index.js.map +1 -1
  98. package/dist/{history-recorder-B4MpJfJK.js → history-recorder-Nr8zLvoU.js} +4 -4
  99. package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-Nr8zLvoU.js.map} +1 -1
  100. package/dist/{history-store-BhxWOuz9.js → history-store-rcAm_xFR.js} +2 -2
  101. package/dist/{history-store-BhxWOuz9.js.map → history-store-rcAm_xFR.js.map} +1 -1
  102. package/dist/index.d.ts +8 -2
  103. package/dist/index.es.js +3084 -753
  104. package/dist/index.es.js.map +1 -1
  105. package/dist/init/health.d.ts +17 -2
  106. package/dist/init/shutdown.d.ts +10 -0
  107. package/dist/init.d.ts +54 -0
  108. package/dist/{jobs-CazMYhyy.js → jobs-DqYNfquG.js} +5 -5
  109. package/dist/{jobs-CazMYhyy.js.map → jobs-DqYNfquG.js.map} +1 -1
  110. package/dist/{jwt-DnQHNFCl.js → jwt-R6bSPMjk.js} +39 -15
  111. package/dist/{jwt-DnQHNFCl.js.map → jwt-R6bSPMjk.js.map} +1 -1
  112. package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
  113. package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
  114. package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
  115. package/dist/logger-D-S-hO5e.js.map +1 -0
  116. package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
  117. package/dist/logs-routes-DAdv37GI.js.map +1 -0
  118. package/dist/mcp/consent-page.d.ts +1 -1
  119. package/dist/mcp/mcp-routes.d.ts +7 -0
  120. package/dist/mcp/mcp-tools.d.ts +15 -9
  121. package/dist/mcp/oauth-metadata.d.ts +21 -16
  122. package/dist/mcp/oauth-routes.d.ts +7 -1
  123. package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-DAq_XVDu.js} +104 -13
  124. package/dist/openapi-generator-DAq_XVDu.js.map +1 -0
  125. package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
  126. package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
  127. package/dist/{query-parser-DGRVFNM3.js → query-parser-BgiKJKvc.js} +6 -56
  128. package/dist/query-parser-BgiKJKvc.js.map +1 -0
  129. package/dist/{request-timeout-C_4C2BeR.js → request-timeout-DgH7j8qO.js} +3 -3
  130. package/dist/{request-timeout-C_4C2BeR.js.map → request-timeout-DgH7j8qO.js.map} +1 -1
  131. package/dist/schema-edit/apply-schema-change.d.ts +63 -3
  132. package/dist/schema-edit/project-root.d.ts +3 -2
  133. package/dist/schema-edit/remote-source.d.ts +9 -4
  134. package/dist/{schema-editor-routes-C5-lh_jO.js → schema-editor-routes-oIyuWl3L.js} +12 -7
  135. package/dist/schema-editor-routes-oIyuWl3L.js.map +1 -0
  136. package/dist/serve-spa.d.ts +58 -0
  137. package/dist/services/routed-realtime-service.d.ts +11 -0
  138. package/dist/soft-delete-params-BWPilMPF.js +59 -0
  139. package/dist/soft-delete-params-BWPilMPF.js.map +1 -0
  140. package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
  141. package/dist/src-CatHFUym.js.map +1 -0
  142. package/dist/{src-pmvW7BFx.js → src-I3aG1PcY.js} +252 -70
  143. package/dist/src-I3aG1PcY.js.map +1 -0
  144. package/dist/storage/GCSStorageController.d.ts +2 -0
  145. package/dist/storage/LocalStorageController.d.ts +2 -0
  146. package/dist/storage/S3StorageController.d.ts +2 -0
  147. package/dist/storage/index.d.ts +2 -2
  148. package/dist/storage/property-limits.d.ts +41 -6
  149. package/dist/storage/request-keys.d.ts +15 -0
  150. package/dist/storage/requested-object.d.ts +74 -0
  151. package/dist/storage/routes.d.ts +36 -18
  152. package/dist/storage/tus-handler.d.ts +30 -5
  153. package/dist/storage/types.d.ts +19 -0
  154. package/dist/types-BfKcm9do.js.map +1 -1
  155. package/dist/utils/logger.d.ts +12 -0
  156. package/package.json +5 -5
  157. package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
  158. package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
  159. package/dist/admin-roles-vYdp_Pil.js +0 -36
  160. package/dist/admin-roles-vYdp_Pil.js.map +0 -1
  161. package/dist/admin_block-DxKLmdiv.js +0 -206
  162. package/dist/admin_block-DxKLmdiv.js.map +0 -1
  163. package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
  164. package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
  165. package/dist/auth-B-GIMpDG.js.map +0 -1
  166. package/dist/backup-D7YR94N3.js +0 -253
  167. package/dist/backup-D7YR94N3.js.map +0 -1
  168. package/dist/contract-routes-CbFjuBwa.js.map +0 -1
  169. package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
  170. package/dist/cron-routes-eE8nif_b.js.map +0 -1
  171. package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
  172. package/dist/cron-store-TcoGz-xS.js.map +0 -1
  173. package/dist/errors-DWsX4yTd.js.map +0 -1
  174. package/dist/function-routes-Chet4-lB.js.map +0 -1
  175. package/dist/logger-DO2PZc4i.js.map +0 -1
  176. package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
  177. package/dist/openapi-generator-O_O24MAT.js.map +0 -1
  178. package/dist/query-parser-DGRVFNM3.js.map +0 -1
  179. package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
  180. package/dist/src-pmvW7BFx.js.map +0 -1
  181. package/dist/src-vkcwKXbT.js.map +0 -1
@@ -1,32 +1,62 @@
1
1
  /**
2
- * Admin routes for managing Service API Keys.
2
+ * Routes for managing API keys.
3
3
  *
4
- * Mounted under `/api/admin/api-keys` with `requireAuth` + `requireAdmin`.
5
- * All routes return masked keys (never the hash). The full plaintext key
6
- * is returned exactly once in the POST response.
4
+ * - {@link createApiKeyRoutes} — the project's service keys, under
5
+ * `/api/admin/api-keys`, for holders of `keys:read` / `keys:write`.
6
+ * - {@link createPersonalKeyRoutes} — the caller's own personal keys, under
7
+ * `/api/auth/keys`, when the app enables `auth.personalKeys`.
8
+ *
9
+ * All routes return masked keys (never the hash). The full plaintext key is
10
+ * returned exactly once, in the response that creates it. No API key may call
11
+ * either router.
7
12
  *
8
13
  * @module
9
14
  */
10
15
  import { Hono } from "hono";
16
+ import type { AccessModel } from "@rebasepro/types";
11
17
  import type { HonoEnv } from "../../api/types.js";
12
18
  import type { ApiKeyStore } from "./api-key-store.js";
19
+ import { type KeyTargets } from "./key-grant.js";
13
20
  export interface ApiKeyRouteOptions {
14
21
  store: ApiKeyStore;
15
22
  serviceKey?: string;
16
23
  /**
17
24
  * Read the caller's roles from the database rather than from their token.
18
25
  *
19
- * Without it this router trusted the `roles` claim, and a key minted here
20
- * never expires and may carry `admin: true` — so an administrator demoted
21
- * an hour ago could still mint themselves permanent admin access, and the
22
- * demotion would not take effect until a token nobody can see had run out.
23
- * See `createRequireAuth`.
26
+ * A key minted here may outlive the session that minted it, so the roles
27
+ * that bound what it may hold must be the caller's roles now — not the
28
+ * ones a token issued before a demotion still claims. See
29
+ * `createRequireAuth`.
24
30
  */
25
31
  resolveRoles?: (uid: string) => Promise<string[]>;
26
32
  /** Repository for the token-revocation watermark. See `createRequireAuth`. */
27
- revocationRepo?: Pick<import("../interfaces.js").AuthRepository, "getTokensValidAfter">;
33
+ revocationRepo?: import("../token-revocation.js").AccessJudgeRepository;
34
+ /** What a scope's target may name. Unset, targets are not checked for existence. */
35
+ targets?: KeyTargets;
36
+ /** The access model to validate scopes against. Defaults to the configured one. */
37
+ accessModel?: () => AccessModel;
28
38
  }
29
39
  /**
30
- * Create admin routes for API key management.
40
+ * The project's service keys: `GET` needs `keys:read`, everything else
41
+ * `keys:write`.
31
42
  */
32
43
  export declare function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv>;
44
+ export interface PersonalKeyRouteOptions {
45
+ store: ApiKeyStore;
46
+ /** Whether the app enabled `auth.personalKeys`. Off, every route explains how to turn it on. */
47
+ enabled: boolean;
48
+ /** Recognised only to be refused with a reason: the service key has no account. */
49
+ serviceKey?: string;
50
+ resolveRoles?: (uid: string) => Promise<string[]>;
51
+ revocationRepo?: import("../token-revocation.js").AccessJudgeRepository;
52
+ targets?: KeyTargets;
53
+ accessModel?: () => AccessModel;
54
+ }
55
+ /**
56
+ * The caller's own keys. Each acts as the caller — their account, their roles
57
+ * as they are when the key is used — and holds no scope the caller does not.
58
+ *
59
+ * For a signed-in account only: not a key, not the service key (it has no
60
+ * account to act as) and not a guest, whose account is one sign-out from gone.
61
+ */
62
+ export declare function createPersonalKeyRoutes(options: PersonalKeyRouteOptions): Hono<HonoEnv>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Database operations for Service API Keys.
2
+ * Database operations for API keys.
3
3
  *
4
4
  * Uses the DataDriver's `admin.executeSql` capability (same pattern as
5
5
  * the cron-store and ensure-tables modules). All data lives in the
@@ -8,22 +8,45 @@
8
8
  * @module
9
9
  */
10
10
  import type { DataDriver } from "@rebasepro/types";
11
- import type { ApiKey, ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest } from "./api-key-types.js";
11
+ import type { ApiKey, ApiKeyKind, ApiKeyMasked, ApiKeyWithSecret, UpdateApiKeyRequest } from "./api-key-types.js";
12
+ /** What a new key row is made of, already validated by the route that asked. */
13
+ export interface NewApiKey {
14
+ name: string;
15
+ kind: ApiKeyKind;
16
+ scopes: string[];
17
+ /** RLS roles beside `service`. Always empty for a personal key. */
18
+ roles: string[];
19
+ /** The account a personal key acts as. */
20
+ owner_uid: string | null;
21
+ rate_limit: number | null;
22
+ expires_at: string | null;
23
+ }
24
+ /** Which keys a listing returns. */
25
+ export type ApiKeyFilter = {
26
+ kind: "service";
27
+ } | {
28
+ kind: "personal";
29
+ owner_uid: string;
30
+ };
12
31
  export interface ApiKeyStore {
13
32
  /** Ensure the `rebase.api_keys` table exists. Called once on startup. */
14
33
  ensureTable(): Promise<void>;
15
34
  /** Create a new API key. Returns the full plaintext key exactly once. */
16
- createApiKey(request: CreateApiKeyRequest, createdBy: string): Promise<ApiKeyWithSecret>;
35
+ createApiKey(key: NewApiKey, createdBy: string): Promise<ApiKeyWithSecret>;
17
36
  /** Look up an API key by its SHA-256 hash. Returns `null` if not found. */
18
37
  findByKeyHash(hash: string): Promise<ApiKey | null>;
19
- /** List all API keys (masked, never includes hash). */
20
- listApiKeys(): Promise<ApiKeyMasked[]>;
38
+ /** List keys (masked, never includes hash), newest first. */
39
+ listApiKeys(filter: ApiKeyFilter): Promise<ApiKeyMasked[]>;
21
40
  /** Get a single API key by ID (masked). */
22
41
  getApiKeyById(id: string): Promise<ApiKeyMasked | null>;
23
- /** Update name, permissions, rate_limit, or expires_at. */
42
+ /** Update name, scopes, roles, rate_limit, or expires_at. */
24
43
  updateApiKey(id: string, updates: UpdateApiKeyRequest): Promise<ApiKeyMasked | null>;
25
- /** Soft-delete: set `revoked_at` to now. */
26
- revokeApiKey(id: string): Promise<boolean>;
44
+ /**
45
+ * Soft-delete: set `revoked_at` to now. With `owner_uid`, only that
46
+ * account's personal key is revoked — the answer is false for anyone
47
+ * else's, so a route can 404 without saying whether the id exists.
48
+ */
49
+ revokeApiKey(id: string, owner_uid?: string): Promise<boolean>;
27
50
  /** Touch `last_used_at` to the current timestamp. */
28
51
  updateLastUsed(id: string): Promise<void>;
29
52
  }
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Type definitions for Service API Keys.
2
+ * Type definitions for API keys.
3
3
  *
4
- * The wire contract — permissions, the masked key, the create/update payloads —
5
- * lives in `@rebasepro/types`, because the client SDK needs the same shapes and
6
- * the two declarations had already drifted apart. Only {@link ApiKey}, the
7
- * database row carrying `key_hash`, is server-side and stays here.
4
+ * The wire contract — scopes, the masked key, the create/update payloads —
5
+ * lives in `@rebasepro/types`, because the client SDK needs the same shapes.
6
+ * Only {@link ApiKey}, the database row carrying `key_hash`, is server-side
7
+ * and stays here.
8
8
  *
9
9
  * @module
10
10
  */
11
- import type { ApiKeyPermission } from "@rebasepro/types";
12
- export type { ApiKeyPermission, ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest } from "@rebasepro/types";
11
+ import type { ApiKeyKind } from "@rebasepro/types";
12
+ export type { ApiKeyKind, ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequest, CreatePersonalKeyRequest, UpdateApiKeyRequest } from "@rebasepro/types";
13
13
  /**
14
14
  * Full database row for an API key.
15
15
  * The `key_hash` is never exposed via the API — only stored for lookup.
@@ -17,19 +17,17 @@ export type { ApiKeyPermission, ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequ
17
17
  export interface ApiKey {
18
18
  id: string;
19
19
  name: string;
20
+ kind: ApiKeyKind;
20
21
  /** First 12 characters of the plaintext key, for display only. */
21
22
  key_prefix: string;
22
23
  /** SHA-256 hash of the full plaintext key. */
23
24
  key_hash: string;
24
- permissions: ApiKeyPermission[];
25
- /**
26
- * When true, the key is granted the `admin` role: it passes the
27
- * admin-gated routes (users, roles, cron, backups, logs, API keys) and
28
- * the RLS `default_admin` policies. Non-admin keys carry only the
29
- * `service` role — RLS grants them nothing unless a collection policy
30
- * explicitly names that role.
31
- */
32
- admin: boolean;
25
+ /** `resource:action[:target]` scope strings. */
26
+ scopes: string[];
27
+ /** RLS roles a service key runs as, beside `service`. Empty on a personal key. */
28
+ roles: string[];
29
+ /** The account a personal key acts as. Null on a service key. */
30
+ owner_uid: string | null;
33
31
  /**
34
32
  * Requests per 15-minute window. `null` means "no per-key override" —
35
33
  * the data rate limiter then applies its default API-key limit
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Which data-plane operation an HTTP request performs, for the scope it needs:
3
+ * `data:read`, `data:write` or `data:delete` (and the same for storage).
4
+ *
5
+ * @module
6
+ */
7
+ /** The operation part of a data-plane scope. */
8
+ export type DataOperation = "read" | "write" | "delete";
9
+ /**
10
+ * Map an HTTP method to the operation it performs.
11
+ *
12
+ * - `GET`, `HEAD`, `OPTIONS` → `"read"`
13
+ * - `POST`, `PUT`, `PATCH` → `"write"`
14
+ * - `DELETE` → `"delete"`
15
+ *
16
+ * Any other method is a `"write"`: a verb this does not know is not assumed
17
+ * to be harmless.
18
+ */
19
+ export declare function httpMethodToOperation(method: string): DataOperation;
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Service API Keys module.
2
+ * API keys module.
3
3
  *
4
- * Re-exports types, store, middleware, permission guard, and routes
5
- * for the API key authentication system.
4
+ * Re-exports types, store, middleware and routes for API key authentication.
6
5
  *
7
6
  * @module
8
7
  */
9
- export type { ApiKey, ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest } from "./api-key-types.js";
8
+ export type { ApiKey, ApiKeyKind, ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequest, CreatePersonalKeyRequest, UpdateApiKeyRequest } from "./api-key-types.js";
10
9
  export { createApiKeyStore } from "./api-key-store.js";
11
- export type { ApiKeyStore } from "./api-key-store.js";
12
- export { isApiKeyToken, validateApiKey, createApiKeyPreAuth, createFunctionApiKeyGuard, createStorageApiKeyGuard } from "./api-key-middleware.js";
13
- export type { ApiKeyAuthOptions } from "./api-key-middleware.js";
14
- export { httpMethodToOperation, isOperationAllowed, isFunctionAllowed, isStorageAllowed } from "./api-key-permission-guard.js";
15
- export type { ApiKeyOperation } from "./api-key-permission-guard.js";
16
- export { createApiKeyRoutes } from "./api-key-routes.js";
17
- export type { ApiKeyRouteOptions } from "./api-key-routes.js";
10
+ export type { ApiKeyStore, ApiKeyFilter, NewApiKey } from "./api-key-store.js";
11
+ export { isApiKeyToken, resolveApiKey, validateApiKey, createApiKeyPreAuth, createFunctionScopeGuard, createTusScopeGuard } from "./api-key-middleware.js";
12
+ export type { ApiKeyAuthOptions, ApiKeyIdentity, ApiKeyRefusal } from "./api-key-middleware.js";
13
+ export { httpMethodToOperation } from "./http-operation.js";
14
+ export type { DataOperation } from "./http-operation.js";
15
+ export { createApiKeyRoutes, createPersonalKeyRoutes } from "./api-key-routes.js";
16
+ export type { ApiKeyRouteOptions, PersonalKeyRouteOptions } from "./api-key-routes.js";
17
+ export type { KeyTargets } from "./key-grant.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * What a new or changed key may hold, decided against whoever asked for it.
3
+ *
4
+ * One rule for every door that mints a credential: **nothing is minted with
5
+ * more than its minter holds.** A key's scopes must be within the creator's
6
+ * own; a service key's RLS roles must be roles the creator holds, unless the
7
+ * creator is an admin; and `keys:*` never goes on a key at all.
8
+ *
9
+ * @module
10
+ */
11
+ import { type AccessModel } from "@rebasepro/types";
12
+ /**
13
+ * The targets this backend serves, for refusing a key narrowed to something
14
+ * that does not exist — a typo there is a key that silently reaches nothing.
15
+ * Each list is read when a key is minted, so it reflects what is loaded then.
16
+ */
17
+ export interface KeyTargets {
18
+ collections(): readonly string[];
19
+ functions(): readonly string[];
20
+ buckets(): readonly string[];
21
+ }
22
+ /** Read a `scopes` body field: a non-empty array of strings, deduplicated. */
23
+ export declare function readScopesField(value: unknown): string[];
24
+ /** Read a `roles` body field: an array of non-empty strings, deduplicated, without `service`. */
25
+ export declare function readRolesField(value: unknown): string[];
26
+ /**
27
+ * Refuse scopes that are malformed, unknown, aimed at a target that does not
28
+ * exist, for key management, or beyond what `minterScopes` covers.
29
+ */
30
+ export declare function assertScopesGrantable(requested: readonly string[], minterScopes: readonly string[], model: AccessModel, targets?: KeyTargets): void;
31
+ /**
32
+ * Refuse RLS roles the minter does not hold. An admin may give any role — they
33
+ * already read every row — and anyone else only their own.
34
+ */
35
+ export declare function assertRolesGrantable(requested: readonly string[], minterRoles: readonly string[]): void;
36
+ /** Read an optional `expires_at`: absent, null, or a future ISO-8601 instant. */
37
+ export declare function readExpiresAt(value: unknown, requireFuture: boolean): string | null | undefined;
38
+ /** Read an optional `rate_limit`: absent, null, or a positive integer. */
39
+ export declare function readRateLimit(value: unknown): number | null | undefined;
40
+ /** Read a key name: a non-empty string, trimmed. */
41
+ export declare function readName(value: unknown): string;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The scopes a key stored before scopes existed is read as.
3
+ *
4
+ * Such a row carries `permissions` — `[{ collection, operations }]`, with
5
+ * `"*"`, `"storage"`, `"functions"` and `"functions/<name>"` overloading the
6
+ * collection field — and an `admin` flag. This turns it into the scopes and
7
+ * RLS roles it now holds, once, when the store backfills the row.
8
+ *
9
+ * The rule is that nothing widens. Where a stored grant has no exact
10
+ * equivalent, it narrows:
11
+ *
12
+ * - A function grant without `write` becomes nothing. Functions were checked
13
+ * by HTTP method, so a read-only grant let a key call GET functions; a
14
+ * function is code, and `functions:invoke` is not a read.
15
+ * - `admin: true` becomes the `admin` RLS role and the admin surfaces such a
16
+ * key could reach (users, schema, backups, cron, logs) — not `database:*`,
17
+ * which lived on the realtime socket where no key could authenticate, and
18
+ * not `keys:*`, which no key may hold.
19
+ *
20
+ * @module
21
+ */
22
+ /** One stored permission entry, as the old column held it. */
23
+ export interface StoredPermission {
24
+ collection: string;
25
+ operations: string[];
26
+ }
27
+ /** Read a stored `permissions` value — JSON text or already-parsed — into entries. */
28
+ export declare function parseStoredPermissions(value: unknown): StoredPermission[];
29
+ /** The scopes and RLS roles a stored key now holds. Never more than it did. */
30
+ export declare function scopesFromStoredPermissions(permissions: readonly StoredPermission[], admin: boolean): {
31
+ scopes: string[];
32
+ roles: string[];
33
+ };
@@ -37,6 +37,7 @@
37
37
  * ```
38
38
  */
39
39
  import type { PasswordValidationResult } from "./password.js";
40
+ import { ApiError } from "../api/errors.js";
40
41
  import type { AuthRepository, UserData, CreateUserData } from "./interfaces.js";
41
42
  import type { EmailService, EmailConfig } from "../email/index.js";
42
43
  import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
@@ -93,8 +94,12 @@ export interface AuthHooks {
93
94
  */
94
95
  verifyCredentials?(email: string, password: string, repo: AuthRepository): Promise<UserData | null>;
95
96
  /**
96
- * Called after any successful authentication event (login, register,
97
- * OAuth, token refresh, password reset).
97
+ * Called after any successful authentication event: password login
98
+ * (`login`), registration (`register`), an OAuth sign-in (`oauth`), a
99
+ * token refresh (`refresh`), a password reset (`password-reset`), a
100
+ * guest session (`anonymous`), a magic link (`magic-link`), an email code
101
+ * (`otp`) and a second factor (`mfa`). Every value of {@link AuthMethod}
102
+ * is passed by some route.
98
103
  *
99
104
  * Use for audit logging, syncing external state, updating
100
105
  * last-login timestamps, etc.
@@ -103,7 +108,11 @@ export interface AuthHooks {
103
108
  */
104
109
  onAuthenticated?(user: UserData, method: AuthMethod): Promise<void>;
105
110
  /**
106
- * Called before a new user is created (registration or admin creation).
111
+ * Called before a new user is created: registration, an OAuth sign-in
112
+ * that creates the account, a guest, and admin creation.
113
+ *
114
+ * Throw to refuse: the caller gets 400 `HOOK_REJECTED` with your message,
115
+ * or the status your error carries (`ApiError`, or a 4xx `status`).
107
116
  *
108
117
  * Also called when a guest becomes an account through
109
118
  * `POST /auth/anonymous/link`, with the email and password hash it is
@@ -125,10 +134,14 @@ export interface AuthHooks {
125
134
  */
126
135
  afterUserCreate?(user: UserData): Promise<void>;
127
136
  /**
128
- * Pre-login validation. Called before credential verification.
129
- *
130
- * Throw an error to reject the login attempt (e.g. for account lockout,
131
- * IP-based restrictions, etc.).
137
+ * Pre-login validation. Called before credential verification on every
138
+ * sign-in: password (`login`), OAuth (`oauth`, with the provider's
139
+ * address), and the requests for a magic link (`magic-link`) or an email
140
+ * code (`otp`). Not on a token refresh, which is not a sign-in: to stop a
141
+ * signed-in account, disable it (`PUT /admin/users/:uid { disabled: true }`).
142
+ *
143
+ * Throw to refuse: 400 `HOOK_REJECTED` with your message, or the status
144
+ * your error carries.
132
145
  */
133
146
  beforeLogin?(email: string, method: AuthMethod): Promise<void>;
134
147
  /**
@@ -209,6 +222,19 @@ export interface AuthHooks {
209
222
  * This is fire-and-forget — errors are logged but do not fail the request.
210
223
  */
211
224
  afterUserDelete?(uid: string): Promise<void>;
225
+ /**
226
+ * Called when a signed-in user asks to move their account to `newEmail`
227
+ * (`POST /auth/change-email`), before anything is mailed. The address is
228
+ * normalized, free and deliverable by then.
229
+ *
230
+ * The address rule `beforeUserCreate` enforces at sign-up — only your own
231
+ * domain, say — belongs here too, or a member can sign up with an allowed
232
+ * address and then move to any other.
233
+ *
234
+ * Throw to refuse: 400 `HOOK_REJECTED` with your message, or the status
235
+ * your error carries.
236
+ */
237
+ beforeEmailChange?(user: UserData, newEmail: string): Promise<void>;
212
238
  /**
213
239
  * Optional hook to customize or override the default user creation flow via the admin panel/REST API.
214
240
  * When provided, this replaces the built-in password generation, hashing, and invitation email logic.
@@ -258,4 +284,17 @@ export type ResolvedAuthHooks = Required<Pick<AuthHooks, "hashPassword" | "verif
258
284
  * This is the single point where defaults are applied — all consumers
259
285
  * call this once and use the resolved hooks throughout.
260
286
  */
287
+ /**
288
+ * What a hook's thrown error answers.
289
+ *
290
+ * The hooks that refuse (`beforeUserCreate`, `beforeLogin`,
291
+ * `beforeUserDelete`, `beforeEmailChange`) are documented as "throw to reject", and a plain
292
+ * `Error` is what people throw. It reached the error handler as a 500
293
+ * "Internal Server Error", so a deployment that limits sign-ups to its own
294
+ * domain answered an outsider with a server fault. A refusal is the caller's
295
+ * answer: 400 `HOOK_REJECTED` with the hook's message — or the status an
296
+ * error carries, when the hook chose one (`ApiError`, or any error with a 4xx
297
+ * `status`/`statusCode`).
298
+ */
299
+ export declare function hookRefusal(error: unknown, hook: string): ApiError;
261
300
  export declare function resolveAuthHooks(hooks?: AuthHooks): ResolvedAuthHooks;
@@ -70,6 +70,14 @@ export interface BuiltinAuthAdapterConfig {
70
70
  enableEmailOtp?: boolean;
71
71
  /** Opt-in httpOnly cookie mode for refresh tokens. */
72
72
  cookieAuth?: import("./routes.js").CookieAuthConfig;
73
+ /** Refuse password sign-in until the address is verified; confirm-first registration. */
74
+ requireEmailVerification?: boolean;
75
+ /** Seconds a rotated-away refresh token still mints a sibling. Default 10. */
76
+ refreshTokenReuseIntervalSeconds?: number;
77
+ /** What a refresh token replayed after that window does to its session. Default `"reject"`. */
78
+ refreshTokenReuse?: import("./routes.js").RefreshTokenReusePolicy;
79
+ /** A magic-link or email-code request for an unknown address creates its account, while registration is open. */
80
+ magicLinkCreatesUsers?: boolean;
73
81
  }
74
82
  /**
75
83
  * Create the built-in Rebase auth adapter.
@@ -22,6 +22,13 @@ export declare function setRefreshCookie(c: Context<HonoEnv>, refreshToken: stri
22
22
  export declare function clearRefreshCookie(c: Context<HonoEnv>, config: CookieAuthConfig | undefined): void;
23
23
  /**
24
24
  * Read the refresh token from the request — cookie first, then body fallback.
25
+ *
26
+ * An empty string is no token, wherever it comes from. In cookie mode every
27
+ * response this server sends carries `refreshToken: ""` (see
28
+ * `redactRefreshToken`), and a client that echoes it back — `@rebasepro/client`
29
+ * did, on every refresh from a live tab — is presenting nothing. Taking the
30
+ * body's `""` as the token, or refusing it at the schema, kept the cookie
31
+ * beside it from ever being read.
25
32
  */
26
33
  export declare function readRefreshToken(c: Context<HonoEnv>, body: {
27
34
  refreshToken?: string;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Addresses no mail can reach: the synthetic ones a guest and an X (Twitter)
3
+ * account are given, because `email` is NOT NULL. Nothing is mailed to them,
4
+ * and no account may move onto one.
5
+ */
6
+ export declare function isDeliverableAddress(email: string): boolean;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A signed-in user changes their own email address.
3
+ *
4
+ * `POST /auth/change-email { newEmail }` records the change and mails a link to
5
+ * the new address, and a notice without a link to the old one.
6
+ * `POST /auth/confirm-email-change { token }` — the link's landing screen calls
7
+ * it — moves the account onto the new address.
8
+ *
9
+ * The address moves only when its inbox answers, so the account is never
10
+ * pointed at an address nobody proved, and nothing is reserved meanwhile: a
11
+ * pending change that held the address would let any account keep a stranger
12
+ * from signing up with their own address. Whoever has the address when the link
13
+ * is followed keeps it, and the link answers 409.
14
+ *
15
+ * On confirmation the address is the account's, verified. The identities whose
16
+ * provider vouched for the old address are detached: the old address may be
17
+ * the reason for the change — a job left, an inbox lost — and whoever controls
18
+ * it now would otherwise sign in through that provider. The sessions stay: the
19
+ * change was asked for from one, and the link proves the new inbox, not
20
+ * anything about the others.
21
+ *
22
+ * @module
23
+ */
24
+ import { Hono } from "hono";
25
+ import { z } from "zod";
26
+ import type { HonoEnv } from "../api/types.js";
27
+ import type { MiddlewareHandler } from "hono";
28
+ import type { resolveAuthHooks } from "./auth-hooks.js";
29
+ import type { AuthModuleConfig } from "./routes.js";
30
+ /** How long an address-change link stays usable. */
31
+ export declare const EMAIL_CHANGE_TTL_MS: number;
32
+ interface EmailChangeRoutesConfig {
33
+ router: Hono<HonoEnv>;
34
+ config: AuthModuleConfig;
35
+ ops: ReturnType<typeof resolveAuthHooks>;
36
+ parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;
37
+ /** A signed-in user, with the revocation watermark consulted. */
38
+ requireLiveSession: MiddlewareHandler<HonoEnv>;
39
+ }
40
+ export declare function mountEmailChangeRoutes(opts: EmailChangeRoutesConfig): void;
41
+ export {};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Deleting the auth tokens nobody can use any more, on a schedule.
3
+ *
4
+ * `TokenRepository.deleteExpiredTokens` was declared, implemented and never
5
+ * called. Every expired reset link, magic link, email code, abandoned refresh
6
+ * token and stale MFA challenge stayed in the database for good: the tables
7
+ * grew by a row per sign-in attempt and nothing took one away.
8
+ *
9
+ * Runs on the process that owns the timers (`ownership.cronScheduler`), once
10
+ * an hour, and across instances once per hour in all: each tick claims its
11
+ * hour's slot in `rebase.cron_claims` — the claim the cron scheduler takes for
12
+ * a job's run — and only the instance that wins it sweeps. A store that cannot
13
+ * answer the claim means no sweep this hour rather than every instance at
14
+ * once; the tokens are already refused when presented, so a late sweep costs
15
+ * space, not safety.
16
+ *
17
+ * @module
18
+ */
19
+ import type { DataDriver } from "@rebasepro/types";
20
+ import type { TokenRepository } from "./interfaces.js";
21
+ /** The slot key in `rebase.cron_claims`. Namespaced so no job file can take it. */
22
+ export declare const EXPIRED_TOKEN_SWEEP_JOB_ID = "rebase:auth:expired-tokens";
23
+ /** One sweep an hour, fleet-wide. */
24
+ export declare const EXPIRED_TOKEN_SWEEP_INTERVAL_MS: number;
25
+ export interface ExpiredTokenSweepOptions {
26
+ authRepo: Pick<TokenRepository, "deleteExpiredTokens">;
27
+ /**
28
+ * Claim `slot` for `jobId` across every instance: `true` for the one that
29
+ * wins it. Throws when the store cannot tell. Without it — a driver with
30
+ * no SQL, cron persistence switched off — every instance sweeps, which is
31
+ * wasted work but harmless: deleting what has expired is idempotent.
32
+ */
33
+ claimSlot?: (jobId: string, slot: string) => Promise<boolean>;
34
+ intervalMs?: number;
35
+ /** Clock, for tests. */
36
+ now?: () => number;
37
+ }
38
+ /** What one tick did. */
39
+ export type ExpiredTokenSweepOutcome = "swept" | "claimed-elsewhere" | "unclaimed" | "failed";
40
+ export interface ExpiredTokenSweep {
41
+ /** Sweep now, then once per interval. */
42
+ start(): void;
43
+ stop(): void;
44
+ /** One tick: claim this interval's slot and, if it is ours, sweep. */
45
+ runSlot(): Promise<ExpiredTokenSweepOutcome>;
46
+ }
47
+ export declare function createExpiredTokenSweep(options: ExpiredTokenSweepOptions): ExpiredTokenSweep;
48
+ /** What {@link startExpiredTokenSweep} is given by the boot. */
49
+ export interface ExpiredTokenSweepWiring {
50
+ /** The auth repository, when the backend has one. */
51
+ authRepo?: Partial<Pick<TokenRepository, "deleteExpiredTokens">>;
52
+ /** The default data driver: its SQL admin backs the slot claim. */
53
+ driver: DataDriver;
54
+ /** Whether this process owns the timers (`ownership.cronScheduler`). */
55
+ ownsTimers: boolean;
56
+ /** `cronPersistence: false` keeps the claims table out of the database, here too. */
57
+ persistClaims?: boolean;
58
+ }
59
+ /**
60
+ * Start the sweep where it belongs, or answer `undefined` where it does not:
61
+ * a process that does not own the timers, or a backend whose auth repository
62
+ * cannot delete expired tokens.
63
+ *
64
+ * The claim is the cron store's, made on the first tick rather than at boot so
65
+ * the boot does not wait on its tables.
66
+ */
67
+ export declare function startExpiredTokenSweep(wiring: ExpiredTokenSweepWiring): ExpiredTokenSweep | undefined;