@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
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Running a request as another user: the `x-rebase-impersonate` header, and
3
+ * the `impersonate` field of the realtime socket's `AUTHENTICATE`.
4
+ *
5
+ * It exists for an administrator checking what one user can see and do — "can
6
+ * B read A's rows, can B write them?" — against the real policies, from
7
+ * Studio's API explorer and JS editor. A granted request runs as one of B's
8
+ * own would: B's uid, B's roles as they are now, B's guest flag and the claims
9
+ * a token minted for B now would carry reach `withAuth`, so the statement runs
10
+ * as `rebase_user` with B's identity and every policy is evaluated for B.
11
+ *
12
+ * Who may: a signed-in administrator, on their own session, judged on their
13
+ * roles as the database has them now rather than as their token claims. Never
14
+ * an API key — not a personal key whose owner is an administrator either — and
15
+ * never the service key. Impersonation is a person checking on another person,
16
+ * and the audit line has to name one.
17
+ *
18
+ * Where: the data API, custom functions and the realtime socket. Every other
19
+ * route refuses the header ({@link refuseUnhonouredImpersonation}).
20
+ *
21
+ * Fail closed: a request asking to act as someone either runs as the user it
22
+ * names or is refused. It never runs as its caller, because that is the
23
+ * failure this exists to end — a response showing the administrator's rows
24
+ * under a label saying they were B's.
25
+ *
26
+ * @module
27
+ */
28
+ import type { Context, MiddlewareHandler } from "hono";
29
+ import { type AuthenticatedUser, type DataDriver } from "@rebasepro/types";
30
+ import type { HonoEnv } from "../api/types.js";
31
+ import { ApiError } from "../api/errors.js";
32
+ /** How the caller authenticated, as far as impersonation is concerned. */
33
+ export type ImpersonationCredential = "session" | "api-key" | "service-key" | "none";
34
+ /** How the auth in use turns a uid into a request identity — `AuthAdapter.resolveUser`. */
35
+ export type ImpersonationUserResolver = (uid: string) => Promise<AuthenticatedUser | null>;
36
+ export interface ImpersonationRequest {
37
+ /** The uid the request asked to act as, as sent. */
38
+ requestedUid: string;
39
+ /** What the caller presented. Only a `session` may impersonate. */
40
+ credential: ImpersonationCredential;
41
+ /** The caller, as their credential verified. */
42
+ caller?: {
43
+ uid: string;
44
+ roles: readonly string[];
45
+ };
46
+ /** Absent when the auth in use has no `resolveUser`: then nobody may. */
47
+ resolveUser?: ImpersonationUserResolver;
48
+ /**
49
+ * Where the request came in, for the security audit line — or absent to
50
+ * write none, for a decision that only re-confirms one already logged
51
+ * (the socket re-asks before every frame).
52
+ */
53
+ audit?: Record<string, unknown>;
54
+ }
55
+ /** The answer to a request to act as another user. */
56
+ export type ImpersonationDecision = {
57
+ granted: AuthenticatedUser;
58
+ impersonator: string;
59
+ } | {
60
+ refused: ApiError;
61
+ };
62
+ /**
63
+ * Decide whether this caller may act as the user they named, and who that
64
+ * user is now.
65
+ *
66
+ * The one decision both doors take — the HTTP data plane through
67
+ * {@link applyImpersonation}, the realtime socket at `AUTHENTICATE` and before
68
+ * every frame after it. The caller is judged before the named user is looked
69
+ * up, so the answer to a caller who may not impersonate says nothing about
70
+ * whether the uid exists.
71
+ *
72
+ * Throws when a lookup fails: that is not an answer about anyone, and each
73
+ * door refuses it its own way.
74
+ */
75
+ export declare function decideImpersonation(request: ImpersonationRequest): Promise<ImpersonationDecision>;
76
+ export interface ImpersonationOptions {
77
+ /** What the caller presented. Only a `session` may impersonate. */
78
+ credential: ImpersonationCredential;
79
+ /** The unscoped delegate the request's driver was scoped from. */
80
+ driver: DataDriver;
81
+ /** The auth adapter's `resolveUser`. Absent: the header is refused for everyone. */
82
+ resolveUser?: ImpersonationUserResolver;
83
+ }
84
+ /**
85
+ * Honour or refuse this request's {@link IMPERSONATE_HEADER}.
86
+ *
87
+ * Called by the data-plane auth middlewares once the caller is on the context
88
+ * and its driver is scoped. When the header names a user the caller may act
89
+ * as, the context's `user` and `driver` are replaced with that user's and
90
+ * `impersonator` names the caller.
91
+ *
92
+ * @returns The refusal to send, or `undefined` to carry on — as the caller
93
+ * when the header is absent, as the named user when it was granted.
94
+ */
95
+ export declare function applyImpersonation(c: Context<HonoEnv>, { credential, driver, resolveUser }: ImpersonationOptions): Promise<Response | undefined>;
96
+ /**
97
+ * Refuse {@link IMPERSONATE_HEADER} on every route that does not honour it.
98
+ *
99
+ * Only the data API and custom functions run a request as another user; the
100
+ * rest — storage, auth, the admin surfaces — authenticate the caller and
101
+ * ignore the header. Ignored, a request asking to act as B would be answered
102
+ * as the administrator who sent it, which is the one outcome impersonation
103
+ * must never have. Mounted ahead of every route, so a surface added later is
104
+ * refused until it is listed here as honouring the header.
105
+ *
106
+ * @param honouredMounts - The mount points whose auth middleware applies the
107
+ * header (`/api/data`, `/api/functions`). A path is under one when it
108
+ * is the mount itself or continues it with a `/`.
109
+ */
110
+ export declare function refuseUnhonouredImpersonation(honouredMounts: readonly string[]): MiddlewareHandler<HonoEnv>;
@@ -49,8 +49,10 @@ export { createSqlRateLimitStore } from "./sql-rate-limit-store.js";
49
49
  export type { SqlRateLimitStoreOptions } from "./sql-rate-limit-store.js";
50
50
  export { resolveRateLimitStoreKind, RateLimitStoreConfigurationError } from "./resolve-rate-limit-store.js";
51
51
  export type { RateLimitStoreKind, RateLimitStoreEnv } from "./resolve-rate-limit-store.js";
52
- export { createApiKeyStore, createApiKeyRoutes, isApiKeyToken, validateApiKey, httpMethodToOperation, isOperationAllowed } from "./api-keys/index.js";
53
- export type { ApiKey, ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest, ApiKeyStore, ApiKeyOperation } from "./api-keys/index.js";
52
+ export { createApiKeyStore, createApiKeyRoutes, createPersonalKeyRoutes, isApiKeyToken, resolveApiKey, validateApiKey, httpMethodToOperation } from "./api-keys/index.js";
53
+ export type { ApiKey, ApiKeyKind, ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequest, CreatePersonalKeyRequest, UpdateApiKeyRequest, ApiKeyStore, DataOperation, KeyTargets } from "./api-keys/index.js";
54
+ export { requireScope, requireScopeByMethod, hasScope, callerScopes, getAccessModel, configureAccess, accessModelFromCollections, AccessModelError } from "./access.js";
55
+ export type { KeyOwnerResolver } from "./access.js";
54
56
  export { createBuiltinAuthAdapter } from "./builtin-auth-adapter.js";
55
57
  export type { BuiltinAuthAdapterConfig } from "./builtin-auth-adapter.js";
56
58
  export { createCustomAuthAdapter } from "./custom-auth-adapter.js";
@@ -19,6 +19,11 @@ export interface UserData {
19
19
  emailVerificationToken?: string | null;
20
20
  emailVerificationSentAt?: Date | null;
21
21
  isAnonymous?: boolean;
22
+ /**
23
+ * An administrator switched the account off: no sign-in, no refresh, and
24
+ * no token it already holds is honoured. See `setUserDisabled`.
25
+ */
26
+ disabled?: boolean;
22
27
  metadata?: Record<string, unknown>;
23
28
  createdAt: Date;
24
29
  updatedAt: Date;
@@ -35,6 +40,16 @@ export interface CreateUserData {
35
40
  isAnonymous?: boolean;
36
41
  metadata?: Record<string, unknown>;
37
42
  }
43
+ /**
44
+ * An address change waiting for its confirmation link. See
45
+ * {@link UserRepository.setPendingEmailChange}.
46
+ */
47
+ export interface PendingEmailChange {
48
+ /** The address the account is moving to, normalized. */
49
+ email: string;
50
+ /** When the link was mailed; it is refused 24 hours after this. */
51
+ sentAt: Date;
52
+ }
38
53
  /**
39
54
  * User Identity Data (OAuth accounts linked to user)
40
55
  */
@@ -92,36 +107,6 @@ export interface OAuthProvider<T = unknown> {
92
107
  */
93
108
  verify(payload: T): Promise<OAuthProviderProfile | null>;
94
109
  }
95
- /**
96
- * Role data structure
97
- */
98
- export interface RoleData {
99
- id: string;
100
- name: string;
101
- isAdmin: boolean;
102
- defaultPermissions: {
103
- read?: boolean;
104
- create?: boolean;
105
- edit?: boolean;
106
- delete?: boolean;
107
- } | null;
108
- collectionPermissions: Record<string, {
109
- read?: boolean;
110
- create?: boolean;
111
- edit?: boolean;
112
- delete?: boolean;
113
- }> | null;
114
- }
115
- /**
116
- * Data for creating a new role
117
- */
118
- export interface CreateRoleData {
119
- id: string;
120
- name: string;
121
- isAdmin?: boolean;
122
- defaultPermissions?: RoleData["defaultPermissions"];
123
- collectionPermissions?: RoleData["collectionPermissions"];
124
- }
125
110
  /**
126
111
  * Refresh token info
127
112
  */
@@ -161,6 +146,31 @@ export interface RefreshTokenInfo {
161
146
  * that do not store it; both read as `aal1`, the restrictive value.
162
147
  */
163
148
  aal?: "aal1" | "aal2";
149
+ /**
150
+ * How the session was signed in — `"password"`, `"anonymous"`,
151
+ * `"magic-link"`, `"otp"`, `"mfa"` or a provider id such as `"google"`.
152
+ * See {@link RefreshTokenSession.method}. Absent on rows written before
153
+ * the column existed, which read as `"password"`.
154
+ */
155
+ method?: string;
156
+ }
157
+ /**
158
+ * What {@link TokenRepository.getAccountAccessState} reads about an account
159
+ * that exists.
160
+ */
161
+ export interface AccountAccessState {
162
+ /** The account's roles as the database has them now. */
163
+ roles: string[];
164
+ /** The revocation watermark: sessions that began before it are void. */
165
+ tokensValidAfter: Date | null;
166
+ /**
167
+ * Whether the session asked about has a refresh token that is not
168
+ * revoked. `undefined` when no session was asked about, or the store
169
+ * cannot tell (a refresh-token table without session grouping).
170
+ */
171
+ sessionActive?: boolean;
172
+ /** The account is switched off (`UserData.disabled`). */
173
+ disabled?: boolean;
164
174
  }
165
175
  /**
166
176
  * Identity of the sign-in a refresh token belongs to, threaded through
@@ -179,6 +189,14 @@ export interface RefreshTokenSession {
179
189
  * assurance level is a property of the *sign-in*, not of the account.
180
190
  */
181
191
  aal?: "aal1" | "aal2";
192
+ /**
193
+ * How the session was signed in: what `providerId` says in every auth
194
+ * response for it. Written at sign-in and carried across rotations like
195
+ * {@link aal}, because a refresh is not a sign-in and has nothing else to
196
+ * read it from — answering `"password"` there turned a Google session into
197
+ * a password one an access-token lifetime after it began.
198
+ */
199
+ method?: string;
182
200
  /**
183
201
  * The hash of the token this one replaces, when it is minted by rotating
184
202
  * one. A repository that honours it writes the new token only while that
@@ -216,7 +234,11 @@ export interface ListUsersOptions {
216
234
  limit?: number;
217
235
  /** Number of results to skip (default 0) */
218
236
  offset?: number;
219
- /** Search term — matches against email and displayName (case-insensitive) */
237
+ /**
238
+ * Search term — a case-insensitive substring of the email, the display
239
+ * name, any role the user holds, or the uid. (The Postgres store matches
240
+ * all four; the Mongo store, email and display name.)
241
+ */
220
242
  search?: string;
221
243
  /** Field to sort by (default "createdAt") */
222
244
  orderBy?: string;
@@ -319,10 +341,6 @@ export interface UserRepository {
319
341
  * Find user by email verification token
320
342
  */
321
343
  getUserByVerificationToken(token: string): Promise<UserData | null>;
322
- /**
323
- * Get roles for a user
324
- */
325
- getUserRoles(uid: string): Promise<RoleData[]>;
326
344
  /**
327
345
  * Get role IDs for a user
328
346
  */
@@ -340,34 +358,45 @@ export interface UserRepository {
340
358
  */
341
359
  getUserWithRoles(uid: string): Promise<{
342
360
  user: UserData;
343
- roles: RoleData[];
361
+ roles: string[];
344
362
  } | null>;
345
- }
346
- /**
347
- * Abstract role repository interface.
348
- * Handles all role-related database operations.
349
- */
350
- export interface RoleRepository {
351
- /**
352
- * Get a role by ID
353
- */
354
- getRoleById(id: string): Promise<RoleData | null>;
355
- /**
356
- * List all roles
357
- */
358
- listRoles(): Promise<RoleData[]>;
359
363
  /**
360
- * Create a new role
364
+ * Switch an account off, or back on. Off, it cannot sign in or refresh,
365
+ * and the tokens it holds are refused (`judgeAccessToken`). Optional: a
366
+ * repository without it cannot disable accounts, and the admin route says
367
+ * so rather than pretending.
361
368
  */
362
- createRole(data: CreateRoleData): Promise<RoleData>;
369
+ setUserDisabled?(uid: string, disabled: boolean): Promise<void>;
363
370
  /**
364
- * Update a role
365
- */
366
- updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null>;
371
+ * Record the address an account is moving to and the hash of the token
372
+ * mailed there, stamped now — or with `null`, drop it. One per account: a
373
+ * new request replaces the last, and its link stops working.
374
+ *
375
+ * Nothing is reserved by it. The address stays free for anyone to
376
+ * register until the link is followed, because a pending change that held
377
+ * it would let any account lock a stranger out of signing up with their
378
+ * own address.
379
+ */
380
+ setPendingEmailChange?(uid: string, change: {
381
+ email: string;
382
+ tokenHash: string;
383
+ } | null): Promise<void>;
384
+ /** The account's pending change, if it has one. */
385
+ getPendingEmailChange?(uid: string): Promise<PendingEmailChange | null>;
386
+ /** The account whose pending change `tokenHash` confirms, with the change. */
387
+ findPendingEmailChange?(tokenHash: string): Promise<{
388
+ user: UserData;
389
+ change: PendingEmailChange;
390
+ } | null>;
367
391
  /**
368
- * Delete a role
392
+ * Move the account onto its pending address, in one write that holds only
393
+ * while `tokenHash` is still that change's token: the address becomes the
394
+ * account's, verified, and the pending change and any outstanding
395
+ * verification token are cleared. `null` when the token no longer names
396
+ * the change (confirmed already, replaced, cancelled). An address another
397
+ * account holds by then is a 409 `EMAIL_EXISTS`, as `updateUser` answers.
369
398
  */
370
- deleteRole(id: string): Promise<void>;
399
+ applyPendingEmailChange?(uid: string, tokenHash: string): Promise<UserData | null>;
371
400
  }
372
401
  /**
373
402
  * Abstract token repository interface.
@@ -418,6 +447,24 @@ export interface TokenRepository {
418
447
  * user's tokens so a rotation racing the delete cannot survive it.
419
448
  */
420
449
  setTokensValidAfter?(uid: string, at: Date): Promise<void>;
450
+ /**
451
+ * Everything an access token is judged against, in one read — or `null`
452
+ * when there is no such account.
453
+ *
454
+ * An access token is a bearer credential minted up to an hour ago, and the
455
+ * account it names may since have been deleted, revoked or demoted. Every
456
+ * door that honours one asks this before it does: the data plane, the admin
457
+ * gates, the realtime socket on every frame. See `judgeAccessToken`.
458
+ *
459
+ * `sessionId` is the token's `sid`: when given, the state also says
460
+ * whether that sign-in is still live, which is how signing one device out
461
+ * reaches the access token that device holds.
462
+ *
463
+ * Optional. Without it the judge composes the answer from
464
+ * `getUserWithRoles` and `getTokensValidAfter`, two reads instead of one,
465
+ * and cannot see one revoked session — only every session at once.
466
+ */
467
+ getAccountAccessState?(uid: string, sessionId?: string): Promise<AccountAccessState | null>;
421
468
  /**
422
469
  * Find a refresh token by hash
423
470
  */
@@ -455,7 +502,11 @@ export interface TokenRepository {
455
502
  */
456
503
  deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
457
504
  /**
458
- * Clean up expired tokens
505
+ * Delete every token past its expiry: reset links, magic links and email
506
+ * codes, refresh tokens, and whatever else the repository keeps that
507
+ * expires. A token is refused when presented whether or not this has run,
508
+ * so it is housekeeping, not revocation. The server calls it once an hour
509
+ * on one instance of the fleet (`startExpiredTokenSweep`).
459
510
  */
460
511
  deleteExpiredTokens(): Promise<void>;
461
512
  /**
@@ -613,5 +664,5 @@ export interface MfaRepository {
613
664
  /**
614
665
  * Combined auth repository interface for convenience
615
666
  */
616
- export interface AuthRepository extends UserRepository, RoleRepository, TokenRepository, MfaRepository {
667
+ export interface AuthRepository extends UserRepository, TokenRepository, MfaRepository {
617
668
  }
@@ -61,6 +61,23 @@ export interface AccessTokenPayload {
61
61
  * comparison and the watermark was read on exactly one path — refresh.
62
62
  */
63
63
  iat?: number;
64
+ /**
65
+ * When the token expires, in seconds since the epoch — the standard `exp`
66
+ * claim. Carried through because a socket authenticated with this token
67
+ * has to stop honouring it at this instant, and has no request to refuse.
68
+ */
69
+ exp?: number;
70
+ /**
71
+ * The sign-in this token was minted for: the session id its refresh
72
+ * tokens share, carried across every rotation.
73
+ *
74
+ * What lets one device be signed out — `POST /auth/logout`,
75
+ * `DELETE /auth/sessions/:id` — reach that device's access token rather
76
+ * than only its refresh token, and what lets `GET /auth/sessions` mark the
77
+ * caller's own session. Absent on tokens minted before it existed; those
78
+ * keep working until they expire, judged by the watermark alone.
79
+ */
80
+ sid?: string;
64
81
  /** Email claim from the JWT, if present */
65
82
  email?: string;
66
83
  /** Display name claim from the JWT, if present */
@@ -89,6 +106,14 @@ export interface AccessTokenPayload {
89
106
  */
90
107
  claims?: Record<string, unknown>;
91
108
  }
109
+ /**
110
+ * The custom half of a verified token, or nothing when there is none.
111
+ *
112
+ * Exported for the one other place that builds a request identity's claims
113
+ * without a token to verify — `resolveUser`, which reads what a token minted
114
+ * now would carry — so both drop the same identity claims.
115
+ */
116
+ export declare function customClaimsOf(decoded: Record<string, unknown>): Record<string, unknown> | undefined;
92
117
  /**
93
118
  * Configure JWT settings - call this during initialization.
94
119
  * Validates the secret strength to prevent deployment with default/weak secrets.
@@ -105,6 +130,12 @@ export declare function configureJwt(config: JwtConfig): void;
105
130
  export declare function getJwks(): {
106
131
  keys: PublicJwk[];
107
132
  };
133
+ /**
134
+ * The secret `configureJwt` was given, or undefined before it is called. For
135
+ * `mfa-crypto.ts`, which falls back to it when no MFA key is set — the secret
136
+ * passed in code, not only the one in `JWT_SECRET`.
137
+ */
138
+ export declare function configuredJwtSecret(): string | undefined;
108
139
  /** Is this backend signing access tokens asymmetrically? */
109
140
  export declare function hasAsymmetricSigningKey(): boolean;
110
141
  /**
@@ -128,7 +159,12 @@ export declare function generateAccessToken(uid: string, roles: string[], aal?:
128
159
  * account. Written into the token so the RLS identity and the WebSocket
129
160
  * path can tell the two apart without a database lookup.
130
161
  */
131
- isAnonymous?: boolean): Promise<string>;
162
+ isAnonymous?: boolean,
163
+ /**
164
+ * The sign-in this token belongs to — see {@link AccessTokenPayload.sid}.
165
+ * Every token the auth routes mint carries one.
166
+ */
167
+ sessionId?: string): Promise<string>;
132
168
  /**
133
169
  * Get the expiration time of an access token in milliseconds from now
134
170
  */
@@ -165,7 +201,8 @@ export declare function getAccessTokenExpiry(): number;
165
201
  */
166
202
  export declare function verifyAccessToken(token: string): Promise<AccessTokenPayload | null>;
167
203
  /**
168
- * Generate a random refresh token (long-lived, 30 days by default)
204
+ * Generate a random refresh token. Long-lived: 400 days by default, sliding —
205
+ * see {@link getRefreshTokenTtlMs}.
169
206
  */
170
207
  export declare function generateRefreshToken(): string;
171
208
  /**
@@ -236,6 +273,13 @@ export interface DownloadTokenPayload {
236
273
  * existed are read.
237
274
  */
238
275
  storageId: string;
276
+ /**
277
+ * An opaque mark of the user who minted the token, for rate limiting only:
278
+ * a read that spends the token is charged to that user's allowance rather
279
+ * than to the reader's address. A hash, because the token travels in a URL.
280
+ * Absent for a token minted for nobody.
281
+ */
282
+ rl?: string;
239
283
  }
240
284
  /**
241
285
  * Generate a short-lived download token scoped to a specific file path or prefix
@@ -254,7 +298,9 @@ export interface DownloadTokenPayload {
254
298
  * site that forgets to pass a named source produces a default-scoped token,
255
299
  * which fails closed at `/file/*` rather than over-granting.
256
300
  */
257
- export declare function generateDownloadToken(path: string, expiresInSeconds?: number, storageId?: string | null): Promise<string>;
301
+ export declare function generateDownloadToken(path: string, expiresInSeconds?: number, storageId?: string | null,
302
+ /** The user minting it, so the reads it buys are charged to them. */
303
+ principal?: string | null): Promise<string>;
258
304
  /**
259
305
  * Verify and decode a download token.
260
306
  *
@@ -1,6 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import type { MiddlewareHandler } from "hono";
3
- import type { AuthModuleConfig } from "./routes.js";
3
+ import type { AuthModuleConfig, CreateSessionAndTokens } from "./routes.js";
4
4
  import type { ResolvedAuthHooks } from "./auth-hooks.js";
5
5
  import type { HonoEnv } from "../api/types.js";
6
6
  import { z } from "zod";
@@ -24,11 +24,7 @@ export declare function mountMagicLinkRoutes(deps: {
24
24
  isAnonymous?: boolean;
25
25
  metadata?: Record<string, unknown> | null;
26
26
  }, roleIds: string[], accessToken: string, refreshToken: string, providerId: string) => unknown;
27
- createSessionAndTokens: (uid: string, userAgent: string, ipAddress: string) => Promise<{
28
- roleIds: string[];
29
- accessToken: string;
30
- refreshToken: string;
31
- }>;
27
+ createSessionAndTokens: CreateSessionAndTokens;
32
28
  applyTransformHook: (response: AuthResponsePayload, method: TransformAuthResponseContext["method"], request: Request, uid: string) => Promise<AuthResponsePayload>;
33
29
  /**
34
30
  * Built by the caller so a misconfiguration fails the boot once, rather
@@ -1,7 +1,7 @@
1
1
  import { Hono, type MiddlewareHandler } from "hono";
2
2
  import { z } from "zod";
3
3
  import { HonoEnv } from "../api/types.js";
4
- import type { AuthModuleConfig } from "./routes.js";
4
+ import type { AuthModuleConfig, CreateSessionAndTokens } from "./routes.js";
5
5
  import { resolveAuthHooks } from "./auth-hooks.js";
6
6
  import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
7
7
  interface MfaRoutesConfig {
@@ -27,14 +27,7 @@ interface MfaRoutesConfig {
27
27
  isAnonymous?: boolean;
28
28
  metadata?: Record<string, unknown> | null;
29
29
  }, roleIds: string[], accessToken: string, refreshToken: string, providerId: string) => unknown;
30
- createSessionAndTokens: (uid: string, userAgent: string, ipAddress: string, options?: {
31
- skipMfaGate?: boolean;
32
- aal?: "aal1" | "aal2";
33
- }) => Promise<{
34
- roleIds: string[];
35
- accessToken: string;
36
- refreshToken: string;
37
- }>;
30
+ createSessionAndTokens: CreateSessionAndTokens;
38
31
  applyTransformHook?: (response: AuthResponsePayload, method: TransformAuthResponseContext["method"], request: Request, uid: string) => Promise<AuthResponsePayload>;
39
32
  }
40
33
  export declare function mountMfaRoutes(opts: MfaRoutesConfig): void;
@@ -1,5 +1,5 @@
1
1
  import { MiddlewareHandler, Context } from "hono";
2
- import type { AuthRepository } from "./interfaces.js";
2
+ import { type AccessJudgeRepository } from "./token-revocation.js";
3
3
  import { DataDriver } from "@rebasepro/types";
4
4
  import { AccessTokenPayload } from "./jwt.js";
5
5
  import type { HonoEnv } from "../api/types.js";
@@ -131,11 +131,15 @@ export declare function createRequireAuth(options?: {
131
131
  * invalidated by `logout` or a password reset stops working on admin routes
132
132
  * too, not just on the data plane.
133
133
  */
134
- revocationRepo?: Pick<AuthRepository, "getTokensValidAfter">;
134
+ revocationRepo?: AccessJudgeRepository;
135
135
  }): MiddlewareHandler<HonoEnv>;
136
136
  /**
137
- * Middleware that requires the user to have an admin or schema-admin role.
138
- * Must be used AFTER requireAuth or on a route where user is guaranteed.
137
+ * Middleware that requires the user to hold the `admin` role, which holds
138
+ * every scope. Must be used AFTER requireAuth or on a route where user is
139
+ * guaranteed.
140
+ *
141
+ * Prefer `requireScope` from `./access` for anything a scope names: a scope
142
+ * can be granted to a narrower role and to a key, and `admin` cannot.
139
143
  */
140
144
  export declare const requireAdmin: MiddlewareHandler<HonoEnv>;
141
145
  /**
@@ -197,6 +201,14 @@ export declare const queryTokenAuth: MiddlewareHandler<HonoEnv>;
197
201
  * requested object path is public, it sets a minimal "public" principal so the
198
202
  * downstream `requireAuth` gate lets the read through. Private paths are left
199
203
  * untouched, so they still require a valid token.
204
+ *
205
+ * Public means the object the route will serve is public, so the decision is
206
+ * made on that object's canonical key, derived by the route's own function
207
+ * (`requestedStorageObject`). It used to be made on the raw path, through a
208
+ * check that strips a `scheme://`: `notes://public/secret.txt` read as
209
+ * `public/secret.txt` here while the route served the private
210
+ * `notes:/public/secret.txt`, and the authorize hook — not asked about this
211
+ * principal — never saw it.
200
212
  */
201
213
  export declare const publicObjectAuth: MiddlewareHandler<HonoEnv>;
202
214
  /**
@@ -217,7 +229,7 @@ export declare const publicObjectAuth: MiddlewareHandler<HonoEnv>;
217
229
  * Hono routes the request, so this comparison already runs on a resolved path.
218
230
  * But that is a guarantee of the runtime rather than of this code, and it is
219
231
  * one line to not depend on it. The same rule already guards the public-object
220
- * path — see `isPublicStoragePath`.
232
+ * path — see `isPublicStorageKey`.
221
233
  */
222
234
  export declare function isPathMatch(requested: string, allowed: string): boolean;
223
235
  /**
@@ -42,7 +42,7 @@
42
42
  import { Hono } from "hono";
43
43
  import type { MiddlewareHandler } from "hono";
44
44
  import { z } from "zod";
45
- import type { AuthModuleConfig } from "./routes.js";
45
+ import type { AuthModuleConfig, CreateSessionAndTokens } from "./routes.js";
46
46
  import type { ResolvedAuthHooks } from "./auth-hooks.js";
47
47
  import type { HonoEnv } from "../api/types.js";
48
48
  import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
@@ -78,11 +78,7 @@ export declare function mountOtpRoutes(deps: {
78
78
  isAnonymous?: boolean;
79
79
  metadata?: Record<string, unknown> | null;
80
80
  }, roleIds: string[], accessToken: string, refreshToken: string, providerId: string) => unknown;
81
- createSessionAndTokens: (uid: string, userAgent: string, ipAddress: string) => Promise<{
82
- roleIds: string[];
83
- accessToken: string;
84
- refreshToken: string;
85
- }>;
81
+ createSessionAndTokens: CreateSessionAndTokens;
86
82
  applyTransformHook: (response: AuthResponsePayload, method: TransformAuthResponseContext["method"], request: Request, uid: string) => Promise<AuthResponsePayload>;
87
83
  /** Built by the caller, as for magic link. Absent when captcha is off. */
88
84
  captchaMiddleware?: MiddlewareHandler<HonoEnv>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Sign-up by magic link or email code (`auth.magicLinkCreatesUsers`).
3
+ *
4
+ * Both passwordless doors refuse an address with no account — silently, so
5
+ * they do not say which addresses have one. A passwordless-only app then had
6
+ * no way to create accounts at all. With the option on, and registration open,
7
+ * an unknown address gets an account at request time — no password, unverified
8
+ * — and the link or code mailed to it is what proves the address and signs it
9
+ * in, exactly as for an existing account. Supabase's `shouldCreateUser`.
10
+ *
11
+ * @module
12
+ */
13
+ import type { AuthModuleConfig } from "./routes.js";
14
+ import type { ResolvedAuthHooks } from "./auth-hooks.js";
15
+ import type { UserData } from "./interfaces.js";
16
+ /**
17
+ * The account a passwordless request for `email` should mail, creating it
18
+ * when the deployment lets passwordless requests sign people up. `null` when
19
+ * there is none and none may be made — the caller answers as for any unknown
20
+ * address.
21
+ *
22
+ * Registration's controls hold: the kill switch, `allowRegistration` (with no
23
+ * first-user exception: the first admin is made by registering or by the
24
+ * operator, never by whoever asks for a link first), `beforeUserCreate`,
25
+ * `afterUserCreate` and the default role.
26
+ */
27
+ export declare function accountForPasswordlessRequest(config: AuthModuleConfig, ops: ResolvedAuthHooks, email: string): Promise<UserData | null>;