@rebasepro/server 0.14.1 → 0.14.2-canary.g27a129e

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 (37) hide show
  1. package/dist/{admin_block-H86dCPsj.js → admin_block-DLILvzle.js} +2 -1
  2. package/dist/admin_block-DLILvzle.js.map +1 -0
  3. package/dist/api/logs-routes.d.ts +47 -8
  4. package/dist/auth/index.d.ts +4 -0
  5. package/dist/auth/resolve-rate-limit-store.d.ts +51 -0
  6. package/dist/auth/sql-rate-limit-store.d.ts +63 -0
  7. package/dist/{auth-BobZVd0j.js → auth-DIKS1rsI.js} +205 -9
  8. package/dist/auth-DIKS1rsI.js.map +1 -0
  9. package/dist/boot/boot.d.ts +15 -0
  10. package/dist/{contract-routes-Bet-eCNJ.js → contract-routes-DDNj4_J1.js} +2 -2
  11. package/dist/{contract-routes-Bet-eCNJ.js.map → contract-routes-DDNj4_J1.js.map} +1 -1
  12. package/dist/{cron-store-CB1x-Ken.js → cron-store-BBGvOA-9.js} +3 -3
  13. package/dist/{cron-store-CB1x-Ken.js.map → cron-store-BBGvOA-9.js.map} +1 -1
  14. package/dist/ddl-bootstrap-Cywoj8Ta.js.map +1 -1
  15. package/dist/index.es.js +128 -20
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/{jobs-DR4SjGrD.js → jobs-BOEOIGAm.js} +3 -3
  18. package/dist/{jobs-DR4SjGrD.js.map → jobs-BOEOIGAm.js.map} +1 -1
  19. package/dist/{jwt-VJyXTdQQ.js → jwt-DxH9fLPt.js} +2 -2
  20. package/dist/{jwt-VJyXTdQQ.js.map → jwt-DxH9fLPt.js.map} +1 -1
  21. package/dist/logs-routes-CWBLQj2l.js +245 -0
  22. package/dist/logs-routes-CWBLQj2l.js.map +1 -0
  23. package/dist/{openapi-generator-DQeQ_q2f.js → openapi-generator-DLiiGD9X.js} +3 -3
  24. package/dist/{openapi-generator-DQeQ_q2f.js.map → openapi-generator-DLiiGD9X.js.map} +1 -1
  25. package/dist/{schema-editor-routes-CRcS3ArS.js → schema-editor-routes-CV9k0w3G.js} +3 -3
  26. package/dist/{schema-editor-routes-CRcS3ArS.js.map → schema-editor-routes-CV9k0w3G.js.map} +1 -1
  27. package/dist/{src-8XDWyDfR.js → src-B-E7RjdN.js} +54 -14
  28. package/dist/src-B-E7RjdN.js.map +1 -0
  29. package/dist/{src-Cz9nMgUR.js → src-CrCxd8km.js} +60 -2
  30. package/dist/src-CrCxd8km.js.map +1 -0
  31. package/package.json +5 -5
  32. package/dist/admin_block-H86dCPsj.js.map +0 -1
  33. package/dist/auth-BobZVd0j.js.map +0 -1
  34. package/dist/logs-routes-BYA72C_C.js +0 -100
  35. package/dist/logs-routes-BYA72C_C.js.map +0 -1
  36. package/dist/src-8XDWyDfR.js.map +0 -1
  37. package/dist/src-Cz9nMgUR.js.map +0 -1
@@ -38,6 +38,7 @@ var ADMIN_COLLECTION_KEYS = [
38
38
  "additionalFields",
39
39
  "alwaysApplyDefaultValues",
40
40
  "components",
41
+ "customViews",
41
42
  "defaultEntityAction",
42
43
  "defaultFilter",
43
44
  "defaultSelectedView",
@@ -181,4 +182,4 @@ function nestAdminPropertyKeys(property) {
181
182
  //#endregion
182
183
  export { nestAdminPropertyKeys as i, ADMIN_PROPERTY_KEYS as n, nestAdminCollectionKeys as r, ADMIN_COLLECTION_KEYS as t };
183
184
 
184
- //# sourceMappingURL=admin_block-H86dCPsj.js.map
185
+ //# sourceMappingURL=admin_block-DLILvzle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin_block-DLILvzle.js","names":[],"sources":["../../types/src/types/admin_block.ts"],"sourcesContent":["/**\n * The keys of a collection's admin block, as data.\n *\n * There is no *type* for the block in this package any more, and that is the point:\n * `admin` is not declared on `BaseCollectionConfig` or on any property here, so a\n * BaaS install cannot even write one. `@rebasepro/admin-types` adds the field back by\n * declaration merging, which is why installing it is what makes the admin surface\n * appear.\n *\n * The *list* still has to live here, because three runtime consumers need it and two\n * of them are core — see below.\n */\n\n/**\n * Every key that belongs inside a collection's `admin` block, as data.\n *\n * The type that describes these fields is `AdminCollectionOptions` in\n * `@rebasepro/admin-types`, and it is erased at build time — but three runtime\n * consumers need the list, and two of them are core:\n *\n * - `serializeCollections`, to drop the block from the contract\n * - the ts-morph schema editor in `@rebasepro/server`, which rewrites collection\n * files on disk from the admin panel and has to know where each key goes. A key\n * missing from this list gets written to the *top level* of the file, where the\n * backend ignores it and the panel never finds it again.\n * - the `collections-admin-block` codemod\n *\n * `@rebasepro/admin-types` re-exports this and asserts it names only real option\n * keys; the count is pinned by a test there.\n *\n * @group Models\n */\nexport const ADMIN_COLLECTION_KEYS = [\n \"Actions\",\n \"additionalFields\",\n \"alwaysApplyDefaultValues\",\n \"components\",\n \"customViews\",\n \"defaultEntityAction\",\n \"defaultFilter\",\n \"defaultSelectedView\",\n \"defaultSize\",\n \"defaultViewMode\",\n \"disableDefaultActions\",\n \"display\",\n \"enabledViews\",\n \"entityActions\",\n \"entityViews\",\n \"exportable\",\n \"filterPresets\",\n \"fixedFilter\",\n \"form\",\n \"formAutoSave\",\n \"formView\",\n \"group\",\n \"hideFromEntityViews\",\n \"hideFromNavigation\",\n \"hideIdFromCollection\",\n \"hideIdFromForm\",\n \"icon\",\n \"includeJsonView\",\n \"inlineEditing\",\n \"kanban\",\n \"listProperties\",\n \"localChangesBackup\",\n \"openEntityMode\",\n \"orderProperty\",\n \"pagination\",\n \"previewProperties\",\n \"propertiesOrder\",\n \"selectionController\",\n \"selectionEnabled\",\n \"sideDialogWidth\",\n \"sort\",\n \"titleProperty\"\n] as const;\n\n/** A key of a collection's `admin` block. @group Models */\nexport type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];\n\n/**\n * Every key that belongs inside a *property's* `admin` block, as data.\n *\n * The union of `AdminPropertyOptions` and its per-type extensions\n * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/admin-types`.\n * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the\n * runtime consumers are core packages that the BaaS guard forbids from\n * importing `@rebasepro/admin-types`. Here it is the boot-time collection\n * validator in `@rebasepro/server`, which has to tell \"you left `readOnly` at\n * the top of the property, where nothing reads it\" apart from \"you invented a\n * key we have never heard of\".\n *\n * `@rebasepro/admin-types` re-exports this and asserts it names only real\n * option keys.\n *\n * @group Models\n */\nexport const ADMIN_PROPERTY_KEYS = [\n \"canAddElements\",\n \"clearable\",\n \"columnWidth\",\n \"customProps\",\n \"disabled\",\n \"expanded\",\n \"Field\",\n \"Filter\",\n \"filterOperators\",\n \"fixedFilter\",\n \"hideFromCollection\",\n \"includeEntityLink\",\n \"includeId\",\n \"markdown\",\n \"minimalistView\",\n \"multiline\",\n \"Preview\",\n \"previewAsTag\",\n \"previewProperties\",\n \"readOnly\",\n \"sortable\",\n \"span\",\n \"spreadChildren\",\n \"urlPreview\",\n \"widget\",\n] as const;\n\n/** A key of a property's `admin` block. @group Models */\nexport type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];\n\n/**\n * Move flattened admin keys back down into the `admin` block.\n *\n * The admin panel works with a *flat* view model — the block merged onto the\n * collection — so what comes back from a form has `icon` and `defaultViewMode`\n * at the top level while `admin` still holds whatever the file was loaded with.\n * This is the way back.\n *\n * **The top-level value wins.** It is the one the form just wrote; the block is\n * the copy the collection was loaded with, and preferring it resolves every edit\n * in favour of the value the user changed away from.\n *\n * This lives here, next to the key lists, because it had two implementations —\n * `toAdminCollectionConfig` in `@rebasepro/admin-types` and `nestAdminKeys` in\n * `@rebasepro/server`'s schema editor — that agreed on everything except that\n * precedence, which is the only part that decides whether a save is visible.\n *\n * @group Models\n */\nexport function nestAdminKeysOf(\n source: Record<string, unknown>,\n adminKeys: readonly string[]\n): Record<string, unknown> {\n const keys = new Set<string>(adminKeys);\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = { ...((source.admin as Record<string, unknown> | undefined) ?? {}) };\n\n for (const [key, value] of Object.entries(source)) {\n if (key === \"admin\") continue;\n if (keys.has(key)) block[key] = value;\n else top[key] = value;\n }\n\n if (Object.keys(block).length > 0) top.admin = block;\n return top;\n}\n\n/**\n * {@link nestAdminKeysOf} for a collection.\n *\n * @group Models\n */\nexport function nestAdminCollectionKeys(collection: Record<string, unknown>): Record<string, unknown> {\n return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);\n}\n\n/**\n * {@link nestAdminKeysOf} for a property, applied to its children too.\n *\n * A map property carries `properties`, an array property carries `of`, and both\n * hold properties with `admin` blocks of their own. A flat `readOnly` left on a\n * child is as dead — and as fatal at the next boot — as one left on the parent,\n * so the walk goes all the way down.\n *\n * @group Models\n */\nexport function nestAdminPropertyKeys(property: Record<string, unknown>): Record<string, unknown> {\n const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);\n\n const children = nested.properties;\n if (children && typeof children === \"object\" && !Array.isArray(children)) {\n nested.properties = Object.fromEntries(\n Object.entries(children as Record<string, unknown>).map(([key, child]) => [\n key,\n child && typeof child === \"object\" && !Array.isArray(child)\n ? nestAdminPropertyKeys(child as Record<string, unknown>)\n : child\n ])\n );\n }\n\n const of = nested.of;\n if (Array.isArray(of)) {\n nested.of = of.map(entry => entry && typeof entry === \"object\" && !Array.isArray(entry)\n ? nestAdminPropertyKeys(entry as Record<string, unknown>)\n : entry);\n } else if (of && typeof of === \"object\") {\n nested.of = nestAdminPropertyKeys(of as Record<string, unknown>);\n }\n\n return nested;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,wBAAwB;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;AAsBA,IAAa,sBAAsB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,gBACZ,QACA,WACuB;CACvB,MAAM,OAAO,IAAI,IAAY,SAAS;CACtC,MAAM,MAA+B,CAAC;CACtC,MAAM,QAAiC,EAAE,GAAK,OAAO,SAAiD,CAAC,EAAG;CAE1G,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EAC/C,IAAI,QAAQ,SAAS;EACrB,IAAI,KAAK,IAAI,GAAG,GAAG,MAAM,OAAO;OAC3B,IAAI,OAAO;CACpB;CAEA,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,QAAQ;CAC/C,OAAO;AACX;;;;;;AAOA,SAAgB,wBAAwB,YAA8D;CAClG,OAAO,gBAAgB,YAAY,qBAAqB;AAC5D;;;;;;;;;;;AAYA,SAAgB,sBAAsB,UAA4D;CAC9F,MAAM,SAAS,gBAAgB,UAAU,mBAAmB;CAE5D,MAAM,WAAW,OAAO;CACxB,IAAI,YAAY,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,GACnE,OAAO,aAAa,OAAO,YACvB,OAAO,QAAQ,QAAmC,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CACtE,KACA,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IACpD,sBAAsB,KAAgC,IACtD,KACV,CAAC,CACL;CAGJ,MAAM,KAAK,OAAO;CAClB,IAAI,MAAM,QAAQ,EAAE,GAChB,OAAO,KAAK,GAAG,KAAI,UAAS,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAChF,sBAAsB,KAAgC,IACtD,KAAK;MACR,IAAI,MAAM,OAAO,OAAO,UAC3B,OAAO,KAAK,sBAAsB,EAA6B;CAGnE,OAAO;AACX"}
@@ -9,19 +9,34 @@ export interface LogEntry {
9
9
  message: string;
10
10
  metadata?: Record<string, unknown>;
11
11
  }
12
+ /** What a caller can narrow the log by, in either direction (query or stream). */
13
+ export interface LogFilterOptions {
14
+ level?: string;
15
+ source?: string;
16
+ search?: string;
17
+ since?: string;
18
+ }
19
+ /** Notified for every entry pushed, in push order. */
20
+ export type LogListener = (entry: LogEntry) => void;
12
21
  declare class LogRingBuffer {
13
22
  private buffer;
14
23
  private maxSize;
15
24
  private idCounter;
25
+ private listeners;
16
26
  constructor(maxSize?: number);
17
27
  push(entry: Omit<LogEntry, "id">): void;
18
- query(options: {
19
- level?: string;
20
- source?: string;
21
- search?: string;
28
+ /**
29
+ * Follow the buffer. Returns the unsubscribe — call it, always: a listener
30
+ * left behind holds its whole closure, and on this class that closure is a
31
+ * pending-entry array.
32
+ *
33
+ * A listener must not log. It is called from inside `push`, so anything that
34
+ * reaches `addLog` from here recurses until the stack gives out.
35
+ */
36
+ subscribe(listener: LogListener): () => void;
37
+ query(options: LogFilterOptions & {
22
38
  limit?: number;
23
39
  offset?: number;
24
- since?: string;
25
40
  }): {
26
41
  entries: LogEntry[];
27
42
  total: number;
@@ -31,7 +46,31 @@ declare class LogRingBuffer {
31
46
  export declare const logBuffer: LogRingBuffer;
32
47
  /** Add a log entry */
33
48
  export declare function addLog(level: LogEntry["level"], source: LogEntry["source"], message: string, metadata?: Record<string, unknown>): void;
49
+ export interface LogMiddlewareOptions {
50
+ /**
51
+ * Paths this sink ignores, matched exactly against `c.req.path`.
52
+ *
53
+ * For requests whose only reason to exist is to read the log. Recording
54
+ * those makes the reader the loudest thing in its own output, and on a quiet
55
+ * server it is also the thing evicting real entries out of the ring.
56
+ */
57
+ ignorePaths?: string[];
58
+ }
34
59
  /** Hono middleware to log API requests */
35
- export declare function logMiddleware(): MiddlewareHandler<HonoEnv>;
36
- declare const app: Hono<HonoEnv, import("hono/types").BlankSchema, "/">;
37
- export default app;
60
+ export declare function logMiddleware(options?: LogMiddlewareOptions): MiddlewareHandler<HonoEnv>;
61
+ /**
62
+ * The stream's timings, injectable only so they can be tested.
63
+ *
64
+ * The defaults above are the contract and nothing in production passes this. A
65
+ * heartbeat is a 25-second wait to observe, and a suite that cannot observe it is
66
+ * a suite where the keepalive can rot — which surfaces as "the tail dies after a
67
+ * few minutes behind the load balancer", months later, on someone else's cluster.
68
+ */
69
+ export interface LogStreamTiming {
70
+ flushMs?: number;
71
+ heartbeatMs?: number;
72
+ maxPending?: number;
73
+ }
74
+ export declare function createLogsRoutes(timing?: LogStreamTiming): Hono<HonoEnv>;
75
+ declare const _default: Hono<HonoEnv, import("hono/types").BlankSchema, "/">;
76
+ export default _default;
@@ -41,6 +41,10 @@ export { createRateLimiter, defaultAuthLimiter, strictAuthLimiter, createDataRat
41
41
  export type { DataRateLimitConfig } from "./rate-limiter";
42
42
  export { MemoryRateLimitStore } from "./rate-limit-store";
43
43
  export type { RateLimitStore, RateLimitDecision } from "./rate-limit-store";
44
+ export { createSqlRateLimitStore } from "./sql-rate-limit-store";
45
+ export type { SqlRateLimitStoreOptions } from "./sql-rate-limit-store";
46
+ export { resolveRateLimitStoreKind, RateLimitStoreConfigurationError } from "./resolve-rate-limit-store";
47
+ export type { RateLimitStoreKind, RateLimitStoreEnv } from "./resolve-rate-limit-store";
44
48
  export { createApiKeyStore, createApiKeyRoutes, isApiKeyToken, validateApiKey, httpMethodToOperation, isOperationAllowed } from "./api-keys";
45
49
  export type { ApiKey, ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest, ApiKeyStore, ApiKeyOperation } from "./api-keys";
46
50
  export { createBuiltinAuthAdapter } from "./builtin-auth-adapter";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Choosing where rate-limit counts live, from configuration and environment.
3
+ *
4
+ * Pure, and separate from both stores, because the decision is the risk. Getting
5
+ * it wrong in either direction is silent: a deployment that wanted shared counts
6
+ * and got per-process ones enforces N times its limit, and a single-container
7
+ * deployment that got shared ones pays a database round trip on every request
8
+ * for a count nobody is sharing with.
9
+ *
10
+ * ## Why memory is still the default
11
+ *
12
+ * The same reason `role.ts` gives for having no channel-bus refusal: **a process
13
+ * cannot read its own replica count.** A single container scaled to three needs
14
+ * a shared store exactly as much as a split deployment does, and neither is
15
+ * visible from inside one process. So this does not guess. It stays on the
16
+ * default that costs nothing, and the deployment that knows it has peers says
17
+ * so — with one environment variable, which is the same sentence whether it is
18
+ * written in a compose file, a Helm value, or a platform-pinned pod spec.
19
+ */
20
+ /** Where a deployment keeps its rate-limit counts. */
21
+ export type RateLimitStoreKind = "memory" | "sql";
22
+ /** The environment this module reads. Narrowed so it can be called with a literal. */
23
+ export interface RateLimitStoreEnv {
24
+ /** `memory` (default) or `sql`. */
25
+ REBASE_RATE_LIMIT_STORE?: string;
26
+ }
27
+ /**
28
+ * A refusal to boot, phrased as the variable to change.
29
+ *
30
+ * Its own class so `bootFromBundle` can present it as a configuration problem
31
+ * rather than a crash, matching `RoleConfigurationError`.
32
+ */
33
+ export declare class RateLimitStoreConfigurationError extends Error {
34
+ readonly hint?: string | undefined;
35
+ constructor(message: string, hint?: string | undefined);
36
+ }
37
+ /**
38
+ * Which store this process should build.
39
+ *
40
+ * A misspelled value **refuses to boot** rather than falling back to memory.
41
+ * Every other outcome here is silent — the whole failure mode of this setting is
42
+ * that nothing looks wrong — so the one moment it can be caught is the moment it
43
+ * is read. `REBASE_RATE_LIMIT_STORE=postgres` is the obvious thing to type and
44
+ * would otherwise be a per-replica limit wearing the shape of a shared one.
45
+ *
46
+ * A blank or whitespace-only value is *unset*, not invalid:
47
+ * `REBASE_RATE_LIMIT_STORE=${SOMETHING}` with `SOMETHING` undefined is the
48
+ * ordinary way to write a compose file, and it is how the platform neutralises a
49
+ * tenant's own variable.
50
+ */
51
+ export declare function resolveRateLimitStoreKind(env: RateLimitStoreEnv): RateLimitStoreKind;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * A rate-limit store every process in a deployment shares.
3
+ *
4
+ * {@link MemoryRateLimitStore} counts in one process's memory, so N replicas
5
+ * enforce N times the limit between them. That is fine for the single container
6
+ * almost every deployment runs, and wrong the moment a deployment splits — an
7
+ * `api` pod and a `functions` pod each grant a caller the full allowance, and
8
+ * nothing anywhere reports it. A limit that silently lets through 3× the traffic
9
+ * is worse than no limit, because it is believed.
10
+ *
11
+ * So this keeps the counts in Postgres, on the same seam the cron store already
12
+ * uses: `DataDriver.admin.executeSql`. No new dependency, no new service, and it
13
+ * degrades on a driver with no SQL (Mongo) rather than failing to boot.
14
+ *
15
+ * ## Why two buckets rather than one row per hit
16
+ *
17
+ * The memory store keeps a timestamp per hit, which is exact and costs a row per
18
+ * request here. This keeps **one row per key per window** and approximates the
19
+ * same sliding behaviour by weighting the previous window by how much of it is
20
+ * still in view:
21
+ *
22
+ * estimate = current + previous × (1 − elapsed ÷ window)
23
+ *
24
+ * At the moment a window rolls over the previous bucket counts fully and decays
25
+ * to nothing by the end, so the boundary burst a fixed window allows — spend the
26
+ * whole allowance in the last second, spend it again in the first second of the
27
+ * next — is bounded rather than free. This is the standard approximation and its
28
+ * error is at most one window's distribution skew, which for a floor against
29
+ * runaway clients is not a number worth a row per request.
30
+ *
31
+ * ## Why a denied hit is given back
32
+ *
33
+ * `INSERT … ON CONFLICT DO UPDATE … RETURNING` is atomic per row, which is what
34
+ * the interface demands: two concurrent hits cannot both read `limit - 1` and
35
+ * both be allowed. But it means the count is incremented *before* the decision,
36
+ * so a request that turns out to be over the limit has already been charged for.
37
+ *
38
+ * The memory store does not charge denied hits, and a store whose semantics
39
+ * differ from the default is a store nobody can safely switch to — the limit
40
+ * would tighten for reasons an operator would attribute to their traffic. So a
41
+ * denial issues a compensating decrement. That is a second statement, on the
42
+ * path that is by definition the rare one; the allowed path stays at one.
43
+ */
44
+ import type { DataDriver } from "@rebasepro/types";
45
+ import type { RateLimitStore } from "./rate-limit-store.js";
46
+ export interface SqlRateLimitStoreOptions {
47
+ /** How often to delete expired buckets. Defaults to 5 minutes. */
48
+ sweepMs?: number;
49
+ /**
50
+ * Clock, for tests. Driving the window directly beats sleeping through half
51
+ * of one — a real sleep overruns on a loaded machine and takes the hits it
52
+ * expected to still be in the window with it.
53
+ */
54
+ now?: () => number;
55
+ }
56
+ /**
57
+ * Build the store, or `undefined` if this driver cannot back it.
58
+ *
59
+ * Returning `undefined` rather than throwing matches `createCronStore`: a
60
+ * deployment on a driver with no SQL admin should degrade to per-process counts
61
+ * with a warning, not refuse to serve.
62
+ */
63
+ export declare function createSqlRateLimitStore(driver: DataDriver, options?: SqlRateLimitStoreOptions): RateLimitStore | undefined;
@@ -2,10 +2,10 @@ import { createRequire as __createRequire } from "module";
2
2
  import process from "process";
3
3
  __createRequire(import.meta.url);
4
4
  import { i as __toESM, n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
5
- import { C as ListLimitError, D as isAnonymousUid, E as ANONYMOUS_USER_ID, T as resolveClientListLimit, a as deserializeLogicalCondition, i as deserializeFilter, r as UnknownFilterOperatorError } from "./src-8XDWyDfR.js";
6
- import "./src-Cz9nMgUR.js";
5
+ import { C as ListLimitError, D as isAnonymousUid, E as ANONYMOUS_USER_ID, T as resolveClientListLimit, a as deserializeLogicalCondition, i as deserializeFilter, r as UnknownFilterOperatorError } from "./src-B-E7RjdN.js";
6
+ import "./src-CrCxd8km.js";
7
7
  import { n as createDdlBootstrapper, o as revokeInternalTableSql, s as isSQLAdmin } from "./ddl-bootstrap-Cywoj8Ta.js";
8
- import { S as canonicalStorageId, T as require_jsonwebtoken, _ as verifyMfaPendingToken, a as generateMfaPendingToken, c as getJwks, d as hasAsymmetricSigningKey, f as hashRefreshToken, g as verifyDownloadToken, h as verifyAccessToken, i as generateDownloadToken, l as getRefreshTokenExpiry, n as configureJwt, o as generateRefreshToken, p as isJwtConfigured, r as generateAccessToken, s as getAccessTokenExpiry, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenTtlMs, w as tryCanonicalStorageKey } from "./jwt-VJyXTdQQ.js";
8
+ import { S as canonicalStorageId, T as require_jsonwebtoken, _ as verifyMfaPendingToken, a as generateMfaPendingToken, c as getJwks, d as hasAsymmetricSigningKey, f as hashRefreshToken, g as verifyDownloadToken, h as verifyAccessToken, i as generateDownloadToken, l as getRefreshTokenExpiry, n as configureJwt, o as generateRefreshToken, p as isJwtConfigured, r as generateAccessToken, s as getAccessTokenExpiry, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenTtlMs, w as tryCanonicalStorageKey } from "./jwt-DxH9fLPt.js";
9
9
  import { t as logger } from "./logger-DfvF_8r-.js";
10
10
  import { n as errorHandler, t as ApiError } from "./errors-EBYiaJ2E.js";
11
11
  import { createHash, randomBytes, randomInt } from "node:crypto";
@@ -7366,7 +7366,7 @@ function mountSessionRoutes(opts) {
7366
7366
  clearRefreshCookie(c, config.cookieAuth);
7367
7367
  const accessToken = extractBearerToken(c.req.header("authorization"));
7368
7368
  if (ops.afterLogout && accessToken !== void 0) {
7369
- const { verifyAccessToken } = await import("./jwt-VJyXTdQQ.js").then((n) => n.m);
7369
+ const { verifyAccessToken } = await import("./jwt-DxH9fLPt.js").then((n) => n.m);
7370
7370
  const payload = verifyAccessToken(accessToken);
7371
7371
  if (payload) ops.afterLogout(payload.uid).catch((err) => {
7372
7372
  logger.error("[AuthHooks] afterLogout error", { error: err instanceof Error ? err.message : err });
@@ -8022,12 +8022,13 @@ function createAuthRoutes(config) {
8022
8022
  photoUrl: user.photoUrl || externalUser.photoUrl || void 0
8023
8023
  });
8024
8024
  } else {
8025
- if (config.disableSelfRegistration) throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
8025
+ const noSignupsMessage = `No account exists for this ${provider.id} identity, and new sign-ups are disabled on this backend. Ask an administrator to create your account.`;
8026
+ if (config.disableSelfRegistration) throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
8026
8027
  let bootstrapRegistration = false;
8027
8028
  if (!isRegistrationAllowed()) {
8028
8029
  const { total } = await authRepo.listUsersPaginated({ limit: 1 });
8029
8030
  bootstrapRegistration = total === 0;
8030
- if (!bootstrapRegistration) throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
8031
+ if (!bootstrapRegistration) throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
8031
8032
  }
8032
8033
  user = await authRepo.createUser({
8033
8034
  email: normalizeEmail(externalUser.email),
@@ -8045,7 +8046,7 @@ function createAuthRoutes(config) {
8045
8046
  const isFirstUser = allUsers.length === 1 && allUsers[0].id === user.id;
8046
8047
  if (bootstrapRegistration && !isFirstUser) {
8047
8048
  await authRepo.deleteUser(user.id);
8048
- throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
8049
+ throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
8049
8050
  }
8050
8051
  if (isFirstUser) await authRepo.setUserRoles(user.id, ["admin"]);
8051
8052
  else if (config.defaultRole) await authRepo.assignDefaultRole(user.id, config.defaultRole);
@@ -10036,6 +10037,198 @@ function createSpotifyProvider(config) {
10036
10037
  };
10037
10038
  }
10038
10039
  //#endregion
10040
+ //#region src/auth/sql-rate-limit-store.ts
10041
+ var SCHEMA = "rebase";
10042
+ var TABLE_NAME = "rate_limit_hits";
10043
+ var TABLE$1 = `${SCHEMA}.${TABLE_NAME}`;
10044
+ /** How often expired buckets are deleted. */
10045
+ var DEFAULT_SWEEP_MS = 300 * 1e3;
10046
+ /**
10047
+ * Build the store, or `undefined` if this driver cannot back it.
10048
+ *
10049
+ * Returning `undefined` rather than throwing matches `createCronStore`: a
10050
+ * deployment on a driver with no SQL admin should degrade to per-process counts
10051
+ * with a warning, not refuse to serve.
10052
+ */
10053
+ function createSqlRateLimitStore(driver, options = {}) {
10054
+ const admin = driver.admin;
10055
+ if (!isSQLAdmin(admin)) {
10056
+ logger.warn("⚠️ [rate-limit] The configured driver has no SQL admin, so rate-limit counts cannot be shared between processes. Falling back to this process's memory — each replica will enforce the limit independently.");
10057
+ return;
10058
+ }
10059
+ const exec = (sqlText, options) => admin.executeSql(sqlText, options?.params ? { params: options.params } : void 0);
10060
+ const ddl = createDdlBootstrapper(exec, "rate-limit-store");
10061
+ const now = options.now ?? Date.now;
10062
+ const sweepMs = options.sweepMs ?? DEFAULT_SWEEP_MS;
10063
+ let sweepTimer;
10064
+ let ready;
10065
+ /**
10066
+ * Create the table once, and remember whether it worked.
10067
+ *
10068
+ * Lazily rather than at boot because the store is constructed while routes
10069
+ * are being mounted, and an `await` there would put a database round trip on
10070
+ * the boot path of every deployment — including the ones that never take a
10071
+ * request. The first request pays for it.
10072
+ *
10073
+ * The result is cached even on failure: a store that cannot create its table
10074
+ * must not retry the DDL on every request, which would turn one broken
10075
+ * permission into a per-request query storm.
10076
+ *
10077
+ * Readiness is decided by reading the table, never by the DDL not throwing —
10078
+ * `ensureObject` routes through `step`, which logs a failure and returns
10079
+ * normally. Keying on the absence of an exception would report a table that
10080
+ * was never created as ready, and every later request would fall into the
10081
+ * per-request error path instead of the one-time fallback.
10082
+ */
10083
+ async function ensureReady() {
10084
+ if (!ready) ready = (async () => {
10085
+ try {
10086
+ await ddl.ensureObject("Creating schema rebase", "CREATE SCHEMA IF NOT EXISTS rebase");
10087
+ await ddl.ensureObject(`Creating ${TABLE$1}`, `
10088
+ CREATE TABLE IF NOT EXISTS ${TABLE$1} (
10089
+ key TEXT NOT NULL,
10090
+ bucket BIGINT NOT NULL,
10091
+ hits INTEGER NOT NULL DEFAULT 0,
10092
+ expires_at TIMESTAMPTZ NOT NULL,
10093
+ PRIMARY KEY (key, bucket)
10094
+ )
10095
+ `);
10096
+ await ddl.ensureObject("Creating idx_rate_limit_hits_expiry", `
10097
+ CREATE INDEX IF NOT EXISTS idx_rate_limit_hits_expiry
10098
+ ON ${TABLE$1}(expires_at)
10099
+ `);
10100
+ const readable = await ddl.isReadable(TABLE$1);
10101
+ if (readable) await ddl.step("Revoking end-user access to rate_limit_hits", () => exec(revokeInternalTableSql(SCHEMA, TABLE_NAME)));
10102
+ else logger.error("[rate-limit] The shared rate-limit table could not be created, so counts stay per-process. Every replica will enforce the limit independently.");
10103
+ return readable;
10104
+ } catch (err) {
10105
+ logger.error("[rate-limit] Could not prepare the shared rate-limit table. Falling back to per-process counts for the life of this process.", { error: err instanceof Error ? err.message : String(err) });
10106
+ return false;
10107
+ }
10108
+ })();
10109
+ return ready;
10110
+ }
10111
+ function startSweeping() {
10112
+ if (sweepTimer) return;
10113
+ sweepTimer = setInterval(() => {
10114
+ exec(`DELETE FROM ${TABLE$1} WHERE expires_at < now()`).catch((err) => {
10115
+ logger.debug("[rate-limit] Bucket sweep failed", { error: err instanceof Error ? err.message : String(err) });
10116
+ });
10117
+ }, sweepMs);
10118
+ sweepTimer.unref?.();
10119
+ }
10120
+ return {
10121
+ async hit(key, windowMs, limit) {
10122
+ if (!await ensureReady()) return {
10123
+ allowed: true,
10124
+ remaining: limit,
10125
+ retryAfterMs: 0
10126
+ };
10127
+ startSweeping();
10128
+ const at = now();
10129
+ const bucket = Math.floor(at / windowMs);
10130
+ const carry = 1 - (at - bucket * windowMs) / windowMs;
10131
+ const expiresAt = new Date((bucket + 2) * windowMs).toISOString();
10132
+ let current;
10133
+ let previous;
10134
+ try {
10135
+ const row = firstRow(await exec(`WITH bumped AS (
10136
+ INSERT INTO ${TABLE$1} AS r (key, bucket, hits, expires_at)
10137
+ VALUES ($1, $2, 1, $3)
10138
+ ON CONFLICT (key, bucket)
10139
+ DO UPDATE SET hits = r.hits + 1, expires_at = EXCLUDED.expires_at
10140
+ RETURNING hits
10141
+ )
10142
+ SELECT
10143
+ (SELECT hits FROM bumped) AS current,
10144
+ COALESCE((SELECT hits FROM ${TABLE$1} WHERE key = $1 AND bucket = $4), 0) AS previous`, { params: [
10145
+ key,
10146
+ bucket,
10147
+ expiresAt,
10148
+ bucket - 1
10149
+ ] }));
10150
+ current = Number(row?.current ?? 1);
10151
+ previous = Number(row?.previous ?? 0);
10152
+ } catch (err) {
10153
+ logger.debug("[rate-limit] Shared count unavailable for this request", { error: err instanceof Error ? err.message : String(err) });
10154
+ return {
10155
+ allowed: true,
10156
+ remaining: limit,
10157
+ retryAfterMs: 0
10158
+ };
10159
+ }
10160
+ const estimate = current + previous * carry;
10161
+ if (estimate > limit) {
10162
+ exec(`UPDATE ${TABLE$1} SET hits = GREATEST(hits - 1, 0) WHERE key = $1 AND bucket = $2`, { params: [key, bucket] }).catch(() => {});
10163
+ return {
10164
+ allowed: false,
10165
+ remaining: 0,
10166
+ retryAfterMs: Math.max(0, (bucket + 1) * windowMs - at)
10167
+ };
10168
+ }
10169
+ return {
10170
+ allowed: true,
10171
+ remaining: Math.max(0, Math.floor(limit - estimate)),
10172
+ retryAfterMs: 0
10173
+ };
10174
+ },
10175
+ dispose() {
10176
+ if (sweepTimer) clearInterval(sweepTimer);
10177
+ sweepTimer = void 0;
10178
+ }
10179
+ };
10180
+ }
10181
+ /**
10182
+ * The first row of whatever shape this driver returns.
10183
+ *
10184
+ * `SqlExec` declares an array of rows, but drivers in this repository have also
10185
+ * returned a `{ rows }` envelope through the same escape hatch. Reading one
10186
+ * shape only is how a store silently counts nothing and reports every caller as
10187
+ * being on their first request — a limiter that never limits, with no error
10188
+ * anywhere. Tolerating both costs one line.
10189
+ */
10190
+ function firstRow(result) {
10191
+ if (Array.isArray(result)) return result[0];
10192
+ const rows = result?.rows;
10193
+ return Array.isArray(rows) ? rows[0] : void 0;
10194
+ }
10195
+ //#endregion
10196
+ //#region src/auth/resolve-rate-limit-store.ts
10197
+ /**
10198
+ * A refusal to boot, phrased as the variable to change.
10199
+ *
10200
+ * Its own class so `bootFromBundle` can present it as a configuration problem
10201
+ * rather than a crash, matching `RoleConfigurationError`.
10202
+ */
10203
+ var RateLimitStoreConfigurationError = class extends Error {
10204
+ hint;
10205
+ constructor(message, hint) {
10206
+ super(message);
10207
+ this.hint = hint;
10208
+ this.name = "RateLimitStoreConfigurationError";
10209
+ }
10210
+ };
10211
+ /**
10212
+ * Which store this process should build.
10213
+ *
10214
+ * A misspelled value **refuses to boot** rather than falling back to memory.
10215
+ * Every other outcome here is silent — the whole failure mode of this setting is
10216
+ * that nothing looks wrong — so the one moment it can be caught is the moment it
10217
+ * is read. `REBASE_RATE_LIMIT_STORE=postgres` is the obvious thing to type and
10218
+ * would otherwise be a per-replica limit wearing the shape of a shared one.
10219
+ *
10220
+ * A blank or whitespace-only value is *unset*, not invalid:
10221
+ * `REBASE_RATE_LIMIT_STORE=${SOMETHING}` with `SOMETHING` undefined is the
10222
+ * ordinary way to write a compose file, and it is how the platform neutralises a
10223
+ * tenant's own variable.
10224
+ */
10225
+ function resolveRateLimitStoreKind(env) {
10226
+ const raw = (env.REBASE_RATE_LIMIT_STORE ?? "").trim().toLowerCase();
10227
+ if (raw === "") return "memory";
10228
+ if (raw === "memory" || raw === "sql") return raw;
10229
+ throw new RateLimitStoreConfigurationError(`REBASE_RATE_LIMIT_STORE="${raw}" is not a rate-limit store.`, "Use \"sql\" to share counts across processes, or \"memory\" (the default) to keep them in this process. A deployment running more than one replica or more than one role wants \"sql\" — otherwise each process enforces the whole limit by itself.");
10230
+ }
10231
+ //#endregion
10039
10232
  //#region src/auth/api-keys/api-key-store.ts
10040
10233
  /**
10041
10234
  * Database operations for Service API Keys.
@@ -10433,6 +10626,7 @@ function createCustomAuthAdapter(options) {
10433
10626
  //#region src/auth/index.ts
10434
10627
  var auth_exports = /* @__PURE__ */ __exportAll({
10435
10628
  MemoryRateLimitStore: () => MemoryRateLimitStore,
10629
+ RateLimitStoreConfigurationError: () => RateLimitStoreConfigurationError,
10436
10630
  apiKeyKeyGenerator: () => apiKeyKeyGenerator,
10437
10631
  configureJwt: () => configureJwt,
10438
10632
  createAdapterAuthMiddleware: () => createAdapterAuthMiddleware,
@@ -10457,6 +10651,7 @@ var auth_exports = /* @__PURE__ */ __exportAll({
10457
10651
  createResetPasswordRoute: () => createResetPasswordRoute,
10458
10652
  createSlackProvider: () => createSlackProvider,
10459
10653
  createSpotifyProvider: () => createSpotifyProvider,
10654
+ createSqlRateLimitStore: () => createSqlRateLimitStore,
10460
10655
  createTwitterProvider: () => createTwitterProvider,
10461
10656
  decideOAuthAutoLink: () => decideOAuthAutoLink,
10462
10657
  defaultAuthLimiter: () => defaultAuthLimiter,
@@ -10490,6 +10685,7 @@ var auth_exports = /* @__PURE__ */ __exportAll({
10490
10685
  requireAdmin: () => requireAdmin,
10491
10686
  requireAuth: () => requireAuth,
10492
10687
  resolveAuthHooks: () => resolveAuthHooks,
10688
+ resolveRateLimitStoreKind: () => resolveRateLimitStoreKind,
10493
10689
  safeCompare: () => safeCompare,
10494
10690
  strictAuthLimiter: () => strictAuthLimiter,
10495
10691
  tryVerifyOidcIdToken: () => tryVerifyOidcIdToken,
@@ -10501,6 +10697,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
10501
10697
  verifyPassword: () => verifyPassword
10502
10698
  });
10503
10699
  //#endregion
10504
- export { extractUserFromToken as $, createDataRateLimiter as A, generateSecurePassword as B, createBuiltinAuthAdapter as C, isPublicStoragePath as Ct, string as D, object as E, resolveEmailLinkBase as F, getWelcomeEmailTemplate as G, getMagicLinkTemplate as H, resolveAuthHooks as I, html as J, RawHtml as K, hashPassword as L, SMTPEmailService as M, createEmailService as N, _coercedNumber as O, assertEmailLinkBases as P, createRequireAuth as Q, validatePasswordStrength as R, createJwksRoutes as S, PUBLIC_STORAGE_PREFIX as St, _enum as T, getPasswordResetTemplate as U, getEmailVerificationTemplate as V, getUserInvitationTemplate as W, createAdapterAuthMiddleware as X, raw as Y, createAuthMiddleware as Z, createLinkedinProvider as _, orderByEntriesToTuples as _t, createSpotifyProvider as a, requireAuth as at, pkceTokenParams as b, parseQueryOptions as bt, createGitLabProvider as c, createStorageApiKeyGuard as ct, createFacebookProvider as d, extractBearerToken as dt, fileTokenAuth as et, createAppleProvider as f, safeCompare as ft, createGitHubProvider as g, isOperationAllowed as gt, verifyOidcIdToken as h, httpMethodToOperation as ht, createApiKeyStore as i, requireAdmin as it, MemoryRateLimitStore as j, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as k, createDiscordProvider as l, isApiKeyToken as lt, tryVerifyOidcIdToken as m, scopeDataDriver as mt, createCustomAuthAdapter as n, publicObjectAuth as nt, createSlackProvider as o, createApiKeyPreAuth as ot, createMicrosoftProvider as p, SERVICE_IDENTITY as pt, escapeHtml as q, createApiKeyRoutes as r, queryTokenAuth as rt, createBitbucketProvider as s, createFunctionApiKeyGuard as st, auth_exports as t, optionalAuth as tt, createTwitterProvider as u, validateApiKey as ut, createGoogleProvider as v, parseAggregateSelect as vt, ZodNumber as w, providerVerifiedEmail as x, resolveListLimitParam as xt, oauthCodeFlowSchema as y, parseGroupBy as yt, verifyPassword as z };
10700
+ export { createAuthMiddleware as $, _coercedNumber as A, validatePasswordStrength as B, providerVerifiedEmail as C, resolveListLimitParam as Ct, _enum as D, ZodNumber as E, createEmailService as F, getPasswordResetTemplate as G, generateSecurePassword as H, assertEmailLinkBases as I, RawHtml as J, getUserInvitationTemplate as K, resolveEmailLinkBase as L, createDataRateLimiter as M, MemoryRateLimitStore as N, object as O, SMTPEmailService as P, createAdapterAuthMiddleware as Q, resolveAuthHooks as R, pkceTokenParams as S, parseQueryOptions as St, createBuiltinAuthAdapter as T, isPublicStoragePath as Tt, getEmailVerificationTemplate as U, verifyPassword as V, getMagicLinkTemplate as W, html as X, escapeHtml as Y, raw as Z, verifyOidcIdToken as _, httpMethodToOperation as _t, resolveRateLimitStoreKind as a, queryTokenAuth as at, createGoogleProvider as b, parseAggregateSelect as bt, createSlackProvider as c, createApiKeyPreAuth as ct, createDiscordProvider as d, isApiKeyToken as dt, createRequireAuth as et, createTwitterProvider as f, validateApiKey as ft, tryVerifyOidcIdToken as g, scopeDataDriver as gt, createMicrosoftProvider as h, SERVICE_IDENTITY as ht, createApiKeyStore as i, publicObjectAuth as it, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as j, string as k, createBitbucketProvider as l, createFunctionApiKeyGuard as lt, createAppleProvider as m, safeCompare as mt, createCustomAuthAdapter as n, fileTokenAuth as nt, createSqlRateLimitStore as o, requireAdmin as ot, createFacebookProvider as p, extractBearerToken as pt, getWelcomeEmailTemplate as q, createApiKeyRoutes as r, optionalAuth as rt, createSpotifyProvider as s, requireAuth as st, auth_exports as t, extractUserFromToken as tt, createGitLabProvider as u, createStorageApiKeyGuard as ut, createGitHubProvider as v, isOperationAllowed as vt, createJwksRoutes as w, PUBLIC_STORAGE_PREFIX as wt, oauthCodeFlowSchema as x, parseGroupBy as xt, createLinkedinProvider as y, orderByEntriesToTuples as yt, hashPassword as z };
10505
10701
 
10506
- //# sourceMappingURL=auth-BobZVd0j.js.map
10702
+ //# sourceMappingURL=auth-DIKS1rsI.js.map