@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
@@ -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;
@@ -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,28 @@ 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;
200
+ /**
201
+ * The hash of the token this one replaces, when it is minted by rotating
202
+ * one. A repository that honours it writes the new token only while that
203
+ * token is still live — present and not revoked — decided in the same
204
+ * transaction as the write, and answers `false` from
205
+ * {@link TokenRepository.createRefreshToken} when it is not.
206
+ *
207
+ * That is the sign-out that landed while the rotation was in flight: the
208
+ * refresh read a live token, the logout revoked every row of the session,
209
+ * and the refresh then wrote a new, unrevoked row into it — in cookie mode
210
+ * re-setting the cookie, so the sign-out silently did not happen. A
211
+ * repository that ignores this keeps that race.
212
+ */
213
+ rotatedFrom?: string;
182
214
  }
183
215
  /**
184
216
  * Password reset token info
@@ -202,7 +234,11 @@ export interface ListUsersOptions {
202
234
  limit?: number;
203
235
  /** Number of results to skip (default 0) */
204
236
  offset?: number;
205
- /** 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
+ */
206
242
  search?: string;
207
243
  /** Field to sort by (default "createdAt") */
208
244
  orderBy?: string;
@@ -263,6 +299,15 @@ export interface UserRepository {
263
299
  * Link a new OAuth identity to a user
264
300
  */
265
301
  linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
302
+ /**
303
+ * Detach one OAuth identity from a user.
304
+ *
305
+ * Used when the owner of an address proves it for the first time, to
306
+ * remove the identities someone attached to the account before anyone had
307
+ * proven the address. Optional: a repository without it cannot do that,
308
+ * and such a proof is refused rather than leaving the identities in place.
309
+ */
310
+ unlinkUserIdentity?(uid: string, provider: string, providerId: string): Promise<void>;
266
311
  /**
267
312
  * Update a user
268
313
  */
@@ -280,9 +325,10 @@ export interface UserRepository {
280
325
  */
281
326
  listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult>;
282
327
  /**
283
- * Update user's password hash
328
+ * Update user's password hash. `null` removes the password: the account
329
+ * then signs in only by link, code or provider.
284
330
  */
285
- updatePassword(id: string, passwordHash: string): Promise<void>;
331
+ updatePassword(id: string, passwordHash: string | null): Promise<void>;
286
332
  /**
287
333
  * Set email verification status
288
334
  */
@@ -295,10 +341,6 @@ export interface UserRepository {
295
341
  * Find user by email verification token
296
342
  */
297
343
  getUserByVerificationToken(token: string): Promise<UserData | null>;
298
- /**
299
- * Get roles for a user
300
- */
301
- getUserRoles(uid: string): Promise<RoleData[]>;
302
344
  /**
303
345
  * Get role IDs for a user
304
346
  */
@@ -316,34 +358,45 @@ export interface UserRepository {
316
358
  */
317
359
  getUserWithRoles(uid: string): Promise<{
318
360
  user: UserData;
319
- roles: RoleData[];
361
+ roles: string[];
320
362
  } | null>;
321
- }
322
- /**
323
- * Abstract role repository interface.
324
- * Handles all role-related database operations.
325
- */
326
- export interface RoleRepository {
327
- /**
328
- * Get a role by ID
329
- */
330
- getRoleById(id: string): Promise<RoleData | null>;
331
363
  /**
332
- * List all roles
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.
333
368
  */
334
- listRoles(): Promise<RoleData[]>;
369
+ setUserDisabled?(uid: string, disabled: boolean): Promise<void>;
335
370
  /**
336
- * Create a new role
337
- */
338
- createRole(data: CreateRoleData): Promise<RoleData>;
339
- /**
340
- * Update a role
341
- */
342
- 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>;
343
391
  /**
344
- * 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.
345
398
  */
346
- deleteRole(id: string): Promise<void>;
399
+ applyPendingEmailChange?(uid: string, tokenHash: string): Promise<UserData | null>;
347
400
  }
348
401
  /**
349
402
  * Abstract token repository interface.
@@ -357,8 +410,12 @@ export interface TokenRepository {
357
410
  * optional so that repositories written against an older release keep
358
411
  * satisfying this interface; implementations that ignore it degrade to one
359
412
  * session per token.
413
+ *
414
+ * Resolves `false` when `session.rotatedFrom` names a token that is no
415
+ * longer live, in which case nothing was written. Anything else means the
416
+ * token was written.
360
417
  */
361
- createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
418
+ createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<boolean | void>;
362
419
  /**
363
420
  * Mark a token as superseded by a rotation, WITHOUT making it unusable.
364
421
  *
@@ -390,6 +447,24 @@ export interface TokenRepository {
390
447
  * user's tokens so a rotation racing the delete cannot survive it.
391
448
  */
392
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>;
393
468
  /**
394
469
  * Find a refresh token by hash
395
470
  */
@@ -427,7 +502,11 @@ export interface TokenRepository {
427
502
  */
428
503
  deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
429
504
  /**
430
- * 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`).
431
510
  */
432
511
  deleteExpiredTokens(): Promise<void>;
433
512
  /**
@@ -571,9 +650,11 @@ export interface MfaRepository {
571
650
  */
572
651
  claimMfaFactorCounter?(factorId: string, counter: number): Promise<boolean>;
573
652
  /**
574
- * Record a failed verification against a challenge and return the new
575
- * total. Atomic (`UPDATE … SET attempts = attempts + 1 RETURNING attempts`)
576
- * so concurrent guesses cannot share one increment.
653
+ * Record a verification attempt against a challenge and return the new
654
+ * total. The route claims it before judging the code and refuses a claim
655
+ * past the cap, so this count is the cap. Atomic (`UPDATE … SET attempts =
656
+ * attempts + 1 RETURNING attempts`) so concurrent guesses cannot share
657
+ * one increment.
577
658
  *
578
659
  * Optional; where it is absent the route falls back to the rate limiters
579
660
  * alone.
@@ -583,5 +664,5 @@ export interface MfaRepository {
583
664
  /**
584
665
  * Combined auth repository interface for convenience
585
666
  */
586
- export interface AuthRepository extends UserRepository, RoleRepository, TokenRepository, MfaRepository {
667
+ export interface AuthRepository extends UserRepository, TokenRepository, MfaRepository {
587
668
  }