@rebasepro/server 0.16.0 → 0.16.1-canary.g0d7af95

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 (219) hide show
  1. package/dist/{GCSStorageController-BEmDYFKc.js → GCSStorageController-Bl7nmhdv.js} +7 -6
  2. package/dist/{GCSStorageController-BEmDYFKc.js.map → GCSStorageController-Bl7nmhdv.js.map} +1 -1
  3. package/dist/{S3StorageController-B2EDXXMH.js → S3StorageController-CWvvrRpF.js} +7 -6
  4. package/dist/{S3StorageController-B2EDXXMH.js.map → S3StorageController-CWvvrRpF.js.map} +1 -1
  5. package/dist/{admin_block-DLILvzle.js → admin_block-BeypnEfb.js} +6 -6
  6. package/dist/admin_block-BeypnEfb.js.map +1 -0
  7. package/dist/api/ast-schema-editor.d.ts +23 -0
  8. package/dist/api/contract-routes.d.ts +1 -1
  9. package/dist/api/errors.d.ts +1 -1
  10. package/dist/api/index.d.ts +3 -3
  11. package/dist/api/live-schema-routes.d.ts +122 -0
  12. package/dist/api/logs-routes.d.ts +1 -1
  13. package/dist/api/mount.d.ts +41 -0
  14. package/dist/api/rest/api-generator.d.ts +2 -2
  15. package/dist/api/rest/index.d.ts +1 -1
  16. package/dist/api/rest/query-parser.d.ts +1 -1
  17. package/dist/api/schema-editor-routes.d.ts +1 -1
  18. package/dist/api/types.d.ts +12 -4
  19. package/dist/{schema-editor-routes-CV9k0w3G.js → ast-schema-editor-BrarYZCq.js} +37 -45
  20. package/dist/ast-schema-editor-BrarYZCq.js.map +1 -0
  21. package/dist/auth/adapter-middleware.d.ts +2 -2
  22. package/dist/auth/admin-roles-route.d.ts +2 -2
  23. package/dist/auth/admin-user-ops.d.ts +3 -3
  24. package/dist/auth/admin-users-route.d.ts +4 -6
  25. package/dist/auth/api-keys/api-key-middleware.d.ts +2 -2
  26. package/dist/auth/api-keys/api-key-permission-guard.d.ts +1 -1
  27. package/dist/auth/api-keys/api-key-routes.d.ts +2 -2
  28. package/dist/auth/api-keys/api-key-store.d.ts +1 -1
  29. package/dist/auth/api-keys/index.d.ts +9 -9
  30. package/dist/auth/apple-oauth.d.ts +2 -2
  31. package/dist/auth/auth-hooks.d.ts +4 -4
  32. package/dist/auth/bitbucket-oauth.d.ts +2 -2
  33. package/dist/auth/builtin-auth-adapter.d.ts +8 -4
  34. package/dist/auth/capabilities.d.ts +51 -0
  35. package/dist/auth/captcha.d.ts +86 -0
  36. package/dist/auth/cookie-utils.d.ts +2 -2
  37. package/dist/auth/discord-oauth.d.ts +2 -2
  38. package/dist/auth/facebook-oauth.d.ts +2 -2
  39. package/dist/auth/github-oauth.d.ts +2 -2
  40. package/dist/auth/gitlab-oauth.d.ts +2 -2
  41. package/dist/auth/google-oauth.d.ts +1 -1
  42. package/dist/auth/index.d.ts +58 -54
  43. package/dist/auth/jwks-routes.d.ts +1 -1
  44. package/dist/auth/jwt.d.ts +1 -1
  45. package/dist/auth/linkedin-oauth.d.ts +2 -2
  46. package/dist/auth/magic-link-routes.d.ts +10 -3
  47. package/dist/auth/mfa-gate.d.ts +1 -1
  48. package/dist/auth/mfa-routes.d.ts +3 -3
  49. package/dist/auth/microsoft-oauth.d.ts +2 -2
  50. package/dist/auth/middleware.d.ts +6 -7
  51. package/dist/auth/otp-routes.d.ts +89 -0
  52. package/dist/auth/rate-limiter.d.ts +2 -2
  53. package/dist/auth/require-auth.d.ts +1 -1
  54. package/dist/auth/reset-password-admin.d.ts +4 -4
  55. package/dist/auth/routes.d.ts +18 -10
  56. package/dist/auth/session-routes.d.ts +3 -3
  57. package/dist/auth/slack-oauth.d.ts +2 -2
  58. package/dist/auth/spotify-oauth.d.ts +2 -2
  59. package/dist/auth/token-revocation.d.ts +2 -2
  60. package/dist/auth/twitter-oauth.d.ts +2 -2
  61. package/dist/{auth-5Et5mnUA.js → auth-DU-nUjPp.js} +604 -4376
  62. package/dist/auth-DU-nUjPp.js.map +1 -0
  63. package/dist/backup/backup-common.d.ts +1 -1
  64. package/dist/backup/backup-routes.d.ts +3 -3
  65. package/dist/backup/index.d.ts +3 -3
  66. package/dist/{backup-C6ljYVTp.js → backup-CRdZkA6c.js} +7 -6
  67. package/dist/{backup-C6ljYVTp.js.map → backup-CRdZkA6c.js.map} +1 -1
  68. package/dist/boot/boot.d.ts +5 -5
  69. package/dist/boot/bundle.d.ts +10 -1
  70. package/dist/boot/driver.d.ts +1 -1
  71. package/dist/boot/env.d.ts +28 -1
  72. package/dist/boot/fetch-bundle.d.ts +144 -1
  73. package/dist/boot/options.d.ts +30 -6
  74. package/dist/boot/resource-adapters.d.ts +31 -0
  75. package/dist/boot/resource-loading.d.ts +10 -0
  76. package/dist/boot/role.d.ts +3 -2
  77. package/dist/boot/sources.d.ts +8 -2
  78. package/dist/boot/version-skew.d.ts +1 -1
  79. package/dist/collections/BackendCollectionRegistry.d.ts +1 -1
  80. package/dist/collections/loader.d.ts +1 -1
  81. package/dist/collections/validate-config.d.ts +13 -0
  82. package/dist/{contract-routes-DZ-LBpSL.js → contract-routes-D8TvxPLo.js} +8 -7
  83. package/dist/contract-routes-D8TvxPLo.js.map +1 -0
  84. package/dist/cron/cron-routes.d.ts +2 -2
  85. package/dist/cron/cron-scheduler.d.ts +3 -3
  86. package/dist/cron/index.d.ts +8 -8
  87. package/dist/{cron-loader-YhhQeVBM.js → cron-loader-d9WMFENB.js} +8 -7
  88. package/dist/{cron-loader-YhhQeVBM.js.map → cron-loader-d9WMFENB.js.map} +1 -1
  89. package/dist/{cron-routes-maM_RlUu.js → cron-routes-BsukhMGm.js} +9 -8
  90. package/dist/cron-routes-BsukhMGm.js.map +1 -0
  91. package/dist/{cron-scheduler-DIpYBmZP.js → cron-scheduler-BB82dWuU.js} +9 -9
  92. package/dist/cron-scheduler-BB82dWuU.js.map +1 -0
  93. package/dist/{cron-store-DfH_4Cd9.js → cron-store-CbFfQhbg.js} +11 -9
  94. package/dist/{cron-store-DfH_4Cd9.js.map → cron-store-CbFfQhbg.js.map} +1 -1
  95. package/dist/ddl-bootstrap-5YZCZ8qk.js +146 -0
  96. package/dist/ddl-bootstrap-5YZCZ8qk.js.map +1 -0
  97. package/dist/deploy/pod-contract.d.ts +139 -0
  98. package/dist/dev-secrets.d.ts +52 -0
  99. package/dist/{dynamic-import-Dvh-K5fl.js → dynamic-import-X40pTZUQ.js} +5 -4
  100. package/dist/{dynamic-import-Dvh-K5fl.js.map → dynamic-import-X40pTZUQ.js.map} +1 -1
  101. package/dist/email/dev-sink.d.ts +88 -0
  102. package/dist/email/index.d.ts +9 -7
  103. package/dist/email/link-base.d.ts +1 -1
  104. package/dist/email/smtp-email-service.d.ts +2 -2
  105. package/dist/email/templates.d.ts +12 -0
  106. package/dist/email/types.d.ts +18 -6
  107. package/dist/env.d.ts +1 -1
  108. package/dist/{errors-EBYiaJ2E.js → errors-DBwpj9N8.js} +8 -7
  109. package/dist/errors-DBwpj9N8.js.map +1 -0
  110. package/dist/{function-loader-DDS1v7YX.js → function-loader-LLdmBFoL.js} +9 -8
  111. package/dist/{function-loader-DDS1v7YX.js.map → function-loader-LLdmBFoL.js.map} +1 -1
  112. package/dist/{function-routes-Btcez1T-.js → function-routes-ClT6UQpD.js} +8 -7
  113. package/dist/function-routes-ClT6UQpD.js.map +1 -0
  114. package/dist/functions/context.d.ts +141 -0
  115. package/dist/functions/define-function.d.ts +1 -1
  116. package/dist/functions/function-routes.d.ts +9 -3
  117. package/dist/functions/guards.d.ts +76 -0
  118. package/dist/functions/index.d.ts +96 -5
  119. package/dist/functions/index.js +919 -0
  120. package/dist/functions/index.js.map +1 -0
  121. package/dist/functions/internal.d.ts +26 -0
  122. package/dist/functions/proxy.d.ts +1 -1
  123. package/dist/functions/request-timeout.d.ts +9 -1
  124. package/dist/functions/runtime-env.d.ts +92 -0
  125. package/dist/functions/wait-until.d.ts +74 -0
  126. package/dist/history/history-routes.d.ts +2 -2
  127. package/dist/history/index.d.ts +1 -1
  128. package/dist/history-recorder-B67maiTi.js +76 -0
  129. package/dist/history-recorder-B67maiTi.js.map +1 -0
  130. package/dist/history-store-oiqhb3HU.js +210 -0
  131. package/dist/history-store-oiqhb3HU.js.map +1 -0
  132. package/dist/index.d.ts +65 -54
  133. package/dist/index.es.js +3667 -328
  134. package/dist/index.es.js.map +1 -1
  135. package/dist/init/docs.d.ts +1 -1
  136. package/dist/init/middlewares.d.ts +1 -1
  137. package/dist/init/shutdown.d.ts +4 -0
  138. package/dist/init/storage.d.ts +1 -1
  139. package/dist/init/surfaces.d.ts +10 -0
  140. package/dist/init.d.ts +179 -22
  141. package/dist/internal-tables-DpxfPaEB.js +97 -0
  142. package/dist/internal-tables-DpxfPaEB.js.map +1 -0
  143. package/dist/jobs/index.d.ts +5 -5
  144. package/dist/{jobs-CyOKXXlu.js → jobs-B2ELModo.js} +11 -9
  145. package/dist/{jobs-CyOKXXlu.js.map → jobs-B2ELModo.js.map} +1 -1
  146. package/dist/{jwt-DxH9fLPt.js → jwt-DoHkMMWF.js} +9 -8
  147. package/dist/jwt-DoHkMMWF.js.map +1 -0
  148. package/dist/{logger-DfvF_8r-.js → logger-DS03e908.js} +104 -10
  149. package/dist/logger-DS03e908.js.map +1 -0
  150. package/dist/{logs-routes-CWBLQj2l.js → logs-routes-DB72iQSr.js} +36 -9
  151. package/dist/logs-routes-DB72iQSr.js.map +1 -0
  152. package/dist/metrics/history-recorder.d.ts +18 -0
  153. package/dist/metrics/history-store.d.ts +123 -0
  154. package/dist/metrics/index.d.ts +10 -2
  155. package/dist/{openapi-generator-BCKJRUS4.js → openapi-generator-FFGT0H6O.js} +7 -11
  156. package/dist/{openapi-generator-BCKJRUS4.js.map → openapi-generator-FFGT0H6O.js.map} +1 -1
  157. package/dist/{proxy-Bj5DVllb.js → proxy-CMymhnwG.js} +9 -6
  158. package/dist/{proxy-Bj5DVllb.js.map → proxy-CMymhnwG.js.map} +1 -1
  159. package/dist/query-parser--nstjRCh.js +355 -0
  160. package/dist/query-parser--nstjRCh.js.map +1 -0
  161. package/dist/{request-timeout-BuFoEKwT.js → request-timeout-C8gkc-j7.js} +21 -6
  162. package/dist/request-timeout-C8gkc-j7.js.map +1 -0
  163. package/dist/rls-audit/index.d.ts +111 -0
  164. package/dist/{rolldown-runtime-DSJWtz9O.js → rolldown-runtime-dW7B1o5h.js} +4 -3
  165. package/dist/schema-edit/apply-schema-change.d.ts +141 -0
  166. package/dist/schema-edit/github-repository.d.ts +67 -0
  167. package/dist/schema-edit/local-git-repository.d.ts +28 -0
  168. package/dist/schema-edit/project-root.d.ts +43 -0
  169. package/dist/schema-edit/remote-source.d.ts +25 -0
  170. package/dist/schema-edit/schema-edit-permissions.d.ts +129 -0
  171. package/dist/schema-editor-routes-Bf5h5Emf.js +87 -0
  172. package/dist/schema-editor-routes-Bf5h5Emf.js.map +1 -0
  173. package/dist/schemas-DBxgjM9A.js +4153 -0
  174. package/dist/schemas-DBxgjM9A.js.map +1 -0
  175. package/dist/{selection-_z6TM1DB.js → selection-CRpqKUbt.js} +6 -5
  176. package/dist/{selection-_z6TM1DB.js.map → selection-CRpqKUbt.js.map} +1 -1
  177. package/dist/services/webhook-service.d.ts +1 -1
  178. package/dist/singleton.d.ts +7 -0
  179. package/dist/{src-BPfYOeN4.js → src-B-CmIFMr.js} +8 -6
  180. package/dist/src-B-CmIFMr.js.map +1 -0
  181. package/dist/{src-CrCxd8km.js → src-Cdsw7DqV.js} +126 -5
  182. package/dist/src-Cdsw7DqV.js.map +1 -0
  183. package/dist/storage/GCSStorageController.d.ts +1 -1
  184. package/dist/storage/LocalStorageController.d.ts +1 -1
  185. package/dist/storage/S3StorageController.d.ts +1 -1
  186. package/dist/storage/cache-headers.d.ts +87 -0
  187. package/dist/storage/index.d.ts +11 -11
  188. package/dist/storage/path-pattern.d.ts +58 -0
  189. package/dist/storage/policies.d.ts +88 -0
  190. package/dist/storage/range.d.ts +63 -0
  191. package/dist/storage/rendition-cache.d.ts +45 -0
  192. package/dist/storage/routes.d.ts +17 -5
  193. package/dist/storage/storage-registry.d.ts +1 -1
  194. package/dist/storage/triggers.d.ts +66 -0
  195. package/dist/storage/tus-handler.d.ts +34 -3
  196. package/dist/topics/runtime.d.ts +68 -0
  197. package/dist/{types-DSnOC4mF.js → types-BfKcm9do.js} +5 -4
  198. package/dist/{types-DSnOC4mF.js.map → types-BfKcm9do.js.map} +1 -1
  199. package/dist/utils/host.d.ts +58 -0
  200. package/dist/utils/logger.d.ts +0 -15
  201. package/dist/utils/request-id.d.ts +1 -1
  202. package/functions/package.json +24 -0
  203. package/package.json +13 -7
  204. package/dist/admin_block-DLILvzle.js.map +0 -1
  205. package/dist/auth-5Et5mnUA.js.map +0 -1
  206. package/dist/contract-routes-DZ-LBpSL.js.map +0 -1
  207. package/dist/cron-routes-maM_RlUu.js.map +0 -1
  208. package/dist/cron-scheduler-DIpYBmZP.js.map +0 -1
  209. package/dist/ddl-bootstrap-Cywoj8Ta.js +0 -221
  210. package/dist/ddl-bootstrap-Cywoj8Ta.js.map +0 -1
  211. package/dist/errors-EBYiaJ2E.js.map +0 -1
  212. package/dist/function-routes-Btcez1T-.js.map +0 -1
  213. package/dist/jwt-DxH9fLPt.js.map +0 -1
  214. package/dist/logger-DfvF_8r-.js.map +0 -1
  215. package/dist/logs-routes-CWBLQj2l.js.map +0 -1
  216. package/dist/request-timeout-BuFoEKwT.js.map +0 -1
  217. package/dist/schema-editor-routes-CV9k0w3G.js.map +0 -1
  218. package/dist/src-BPfYOeN4.js.map +0 -1
  219. package/dist/src-CrCxd8km.js.map +0 -1
@@ -0,0 +1,355 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ import { C as ListLimitError, T as resolveClientListLimit, a as deserializeLogicalCondition, i as deserializeFilter, r as UnknownFilterOperatorError } from "./src-B-CmIFMr.js";
6
+ import "./src-Cdsw7DqV.js";
7
+ import { t as ApiError } from "./errors-DBwpj9N8.js";
8
+ //#region src/api/rest/query-parser.ts
9
+ /**
10
+ * A malformed query parameter, refused with a 400.
11
+ *
12
+ * Every rejection in this file is one of these, and every one of them is
13
+ * `expected` — the flag `errorHandler` reads to log a routine outcome at debug
14
+ * instead of warn. A client that mistypes an operator, a sort direction or a
15
+ * limit is not an incident: nothing on the server is wrong, the request never
16
+ * reached the database, and the caller has already been told what to fix in the
17
+ * response body. Left at warn, a single frontend holding a stale field name
18
+ * writes a `⚠️` line per request forever, and the warn level stops meaning
19
+ * anything — which is why "routine 4xx logs at WARN" is a standing finding
20
+ * against this API.
21
+ *
22
+ * Not a factory on `ApiError`: the class's members are part of the tracked
23
+ * runtime surface (`api-surface/server.api.txt`), and this needs no addition to
24
+ * it. `ApiError.unauthenticated` is the same idea one status code up.
25
+ */
26
+ function invalidParam(message, code, details) {
27
+ return new ApiError(400, code, message, details, true);
28
+ }
29
+ /**
30
+ * Decode a filter, turning the shared codec's operator rejection into a 400.
31
+ *
32
+ * `deserializeFilter` lives in `@rebasepro/common`, which cannot throw an
33
+ * `ApiError` — it does not depend on this package, and the browser SDK decodes
34
+ * through the same function and has nothing to render one with. So it throws
35
+ * `UnknownFilterOperatorError`, and the HTTP boundary is where that becomes a
36
+ * status code. Same seam `parseLogicalGroup` uses for the nesting bound.
37
+ *
38
+ * Without this the operator string became a *value*: `?where={"title":
39
+ * ["!!","Hello"]}` compiled to `title IN ('!!','Hello')` and answered 200 with
40
+ * the row the caller was filtering out, and `{"id":[">>",0]}` reached Postgres
41
+ * and came back a 500 quoting `invalid input syntax for type integer`. Both are
42
+ * malformed requests and now say so.
43
+ */
44
+ function decodeFilter(query) {
45
+ try {
46
+ return deserializeFilter(query);
47
+ } catch (e) {
48
+ if (e instanceof UnknownFilterOperatorError) throw invalidParam(e.message, e.code, e.details);
49
+ throw e;
50
+ }
51
+ }
52
+ function getLastValue(val) {
53
+ if (Array.isArray(val)) return val[val.length - 1];
54
+ return val;
55
+ }
56
+ /**
57
+ * Parse an `or(...)` / `and(...)` logical group from its wire form.
58
+ *
59
+ * The wire carries the inner conditions wrapped in parens (e.g.
60
+ * `(status.eq.active,age.gte.18)`); we re-attach the `or`/`and` prefix and
61
+ * delegate to the canonical filter dialect (`@rebasepro/common`). Values are
62
+ * preserved as strings — type coercion is the schema-aware driver's job, so
63
+ * this path stays byte-for-byte consistent with the SDK/admin path (which
64
+ * also parses via the shared dialect).
65
+ */
66
+ function parseLogicalGroup(type, raw) {
67
+ let inner = String(raw).trim();
68
+ if (inner.startsWith("(") && inner.endsWith(")")) inner = inner.slice(1, -1);
69
+ inner = inner.trim();
70
+ if (!inner) return void 0;
71
+ let parsed;
72
+ try {
73
+ parsed = deserializeLogicalCondition(`${type}(${inner})`);
74
+ } catch (e) {
75
+ throw invalidParam(`Invalid \`${type}\` parameter: ${e instanceof Error ? e.message : String(e)}`, "INVALID_LOGICAL_GROUP");
76
+ }
77
+ return "type" in parsed ? parsed : void 0;
78
+ }
79
+ /**
80
+ * Parse the `?where=` JSON filter object.
81
+ *
82
+ * This is the dialect the OpenAPI document publishes on every
83
+ * `GET /api/data/{slug}` — `{"status":["==","active"]}`: field → canonical
84
+ * `[WhereFilterOp, value]` tuple. It is normalized through the same
85
+ * `deserializeFilter` as the `?field=op.value` params below, so a value that
86
+ * arrives as a PostgREST dot-string (`{"status":"eq.active"}`) or as a bare
87
+ * scalar (`{"status":"active"}`) compiles to the same condition. Unlike the
88
+ * querystring dialect, JSON carries types — a number stays a number.
89
+ *
90
+ * A malformed value is a 400 rather than a silent drop: dropping the filter
91
+ * would run the read unfiltered and return everything RLS happens to allow.
92
+ */
93
+ function parseWhereParam(raw) {
94
+ const str = String(raw).trim();
95
+ if (!str) return void 0;
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(str);
99
+ } catch {
100
+ throw invalidParam("Invalid `where` parameter: expected a JSON object, e.g. {\"status\":[\"==\",\"active\"]}", "INVALID_WHERE");
101
+ }
102
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw invalidParam("Invalid `where` parameter: expected a JSON object mapping fields to conditions, e.g. {\"status\":[\"==\",\"active\"]}", "INVALID_WHERE");
103
+ const filter = decodeFilter(parsed);
104
+ return Object.keys(filter).length > 0 ? filter : void 0;
105
+ }
106
+ /**
107
+ * The parsed entries as the driver contract spells them: `[field, direction]`
108
+ * tuples in order of significance.
109
+ *
110
+ * The REST layer used to hand the driver `orderBy[0].field` and drop the rest,
111
+ * so `?orderBy=[{"field":"roles"},{"field":"created_at","direction":"desc"}]`
112
+ * — a shape this parser has always accepted and validated in full — sorted by
113
+ * `roles` alone and returned the ties in whatever order Postgres pleased.
114
+ */
115
+ function orderByEntriesToTuples(entries) {
116
+ if (!entries || entries.length === 0) return void 0;
117
+ return entries.map(({ field, direction }) => [field, direction]);
118
+ }
119
+ function invalidOrderBy(detail) {
120
+ throw invalidParam(`Invalid \`orderBy\` parameter: ${detail}. Expected \`field\`, \`field:desc\`, or a JSON array like [{"field":"created_at","direction":"desc"}]`, "INVALID_ORDER_BY");
121
+ }
122
+ /** The aggregate functions `?select=` accepts. */
123
+ var AGGREGATE_FUNCTIONS = /* @__PURE__ */ new Set([
124
+ "count",
125
+ "sum",
126
+ "avg",
127
+ "min",
128
+ "max"
129
+ ]);
130
+ /**
131
+ * Parse `?select=count(),sum(total),avg(total)`.
132
+ *
133
+ * The spelling is SQL's, because whoever writes it is thinking in SQL and
134
+ * because any other spelling has to be learned first. `count()` with no field
135
+ * counts rows; every other function names a column.
136
+ *
137
+ * Aliases are derived rather than accepted: `sum(total)` returns as
138
+ * `sum_total`, `count()` as `count`. Letting a caller choose would mean
139
+ * checking their alias is not also a `groupBy` field — a rule nobody would
140
+ * guess, and a silently overwritten value if it went unchecked.
141
+ */
142
+ function parseAggregateSelect(raw) {
143
+ const value = getLastValue(raw);
144
+ if (!value) return void 0;
145
+ const entries = String(value).split(",").map((s) => s.trim()).filter(Boolean);
146
+ if (entries.length === 0) return void 0;
147
+ return entries.map((entry) => {
148
+ const match = /^([a-z]+)\(\s*([A-Za-z0-9_]*)\s*\)$/i.exec(entry);
149
+ if (!match) throw invalidParam(`Invalid \`select\` entry "${entry}". Expected \`fn(field)\`, e.g. \`sum(total)\` or \`count()\`.`, "INVALID_AGGREGATE_SELECT");
150
+ const fn = match[1].toLowerCase();
151
+ const field = match[2] || void 0;
152
+ if (!AGGREGATE_FUNCTIONS.has(fn)) throw invalidParam(`Unknown aggregate function "${fn}". Expected: ${[...AGGREGATE_FUNCTIONS].join(", ")}.`, "INVALID_AGGREGATE_FUNCTION");
153
+ if (fn !== "count" && !field) throw invalidParam(`\`${fn}()\` needs a field, e.g. \`${fn}(total)\`. Only \`count()\` may be empty.`, "INVALID_AGGREGATE_SELECT");
154
+ return {
155
+ fn,
156
+ field,
157
+ alias: field ? `${fn}_${field}` : fn
158
+ };
159
+ });
160
+ }
161
+ /** Parse `?groupBy=status,country`. */
162
+ function parseGroupBy(raw) {
163
+ const value = getLastValue(raw);
164
+ if (!value) return void 0;
165
+ const fields = String(value).split(",").map((s) => s.trim()).filter(Boolean);
166
+ return fields.length > 0 ? fields : void 0;
167
+ }
168
+ /** `asc`/`desc`, in any case. Anything else is a request to sort in a way that does not exist. */
169
+ function toDirection(raw, context) {
170
+ if (raw === void 0 || raw === null) return "asc";
171
+ if (typeof raw !== "string") invalidOrderBy(`${context} has a non-string \`direction\``);
172
+ const lowered = raw.toLowerCase();
173
+ if (lowered !== "asc" && lowered !== "desc") invalidOrderBy(`${context} has direction '${raw}'`);
174
+ return lowered;
175
+ }
176
+ /** One entry: the canonical `{field, direction}`, or the `field:direction` shorthand as a string. */
177
+ function toOrderByEntry(raw, index) {
178
+ const context = `entry ${index}`;
179
+ if (typeof raw === "string") {
180
+ const idx = raw.indexOf(":");
181
+ const field = (idx === -1 ? raw : raw.slice(0, idx)).trim();
182
+ if (!field) invalidOrderBy(`${context} is an empty field name`);
183
+ return {
184
+ field,
185
+ direction: idx === -1 ? "asc" : toDirection(raw.slice(idx + 1), context)
186
+ };
187
+ }
188
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) invalidOrderBy(`${context} is not a field name or a {field, direction} object`);
189
+ const entry = raw;
190
+ if (typeof entry.field !== "string" || entry.field.trim() === "") invalidOrderBy(`${context} has no \`field\``);
191
+ return {
192
+ field: entry.field,
193
+ direction: toDirection(entry.direction, context)
194
+ };
195
+ }
196
+ /**
197
+ * Parse the `orderBy` query parameter.
198
+ *
199
+ * The field *name* has been validated against the schema for a while — an
200
+ * `?orderBy=titel` is a 400 rather than 200 with unsorted rows, on the grounds
201
+ * that silently dropping the sort leaves the caller believing in an order that
202
+ * is not there. The parameter's *shape* was never checked the same way, and it
203
+ * failed in exactly the same silent manner one layer earlier: whatever
204
+ * `JSON.parse` returned was assigned to an option declared as an array of
205
+ * `{field, direction}`, and the REST layer reads only `orderBy[0].field`. So
206
+ * `?orderBy={"field":"name"}` — an object rather than an array, and the most
207
+ * natural thing for a client to try — read `undefined`, dropped the ORDER BY,
208
+ * and answered 200. So did a number, a boolean, `null`, and `["name"]`.
209
+ *
210
+ * This refuses those, the way `parseWhereParam` above already refuses a
211
+ * malformed filter and for the same reason. What it keeps working is every
212
+ * shape that worked before: the `field` and `field:desc` shorthands, and the
213
+ * canonical JSON array.
214
+ */
215
+ function parseOrderByParam(raw) {
216
+ if (Array.isArray(raw)) return raw.length === 0 ? void 0 : raw.map(toOrderByEntry);
217
+ const str = String(raw).trim();
218
+ if (!str) return void 0;
219
+ let parsed;
220
+ try {
221
+ parsed = JSON.parse(str);
222
+ } catch {
223
+ return [toOrderByEntry(str, 0)];
224
+ }
225
+ if (Array.isArray(parsed)) {
226
+ if (parsed.length === 0) return void 0;
227
+ return parsed.map(toOrderByEntry);
228
+ }
229
+ if (typeof parsed === "string") return [toOrderByEntry(parsed, 0)];
230
+ if (typeof parsed === "object" && parsed !== null) invalidOrderBy("a single object was given where a JSON array was expected");
231
+ invalidOrderBy(`${typeof parsed} is not a field name or a list of them`);
232
+ }
233
+ /**
234
+ * {@link resolveClientListLimit} for an HTTP route: the same bounds, answered
235
+ * with a 400 rather than a 500.
236
+ *
237
+ * The shared resolver throws a `ListLimitError`, which carries `status` — but
238
+ * the Hono error handler discriminates on `statusCode`, so an unconverted one
239
+ * reaches the client as `INTERNAL_ERROR` with its message stripped, telling the
240
+ * caller nothing about the parameter it got wrong. Every REST list ingress
241
+ * routes its `limit` through here so all of them name the ceiling the same way.
242
+ */
243
+ function resolveListLimitParam(rawLimit, opts = {}) {
244
+ try {
245
+ return resolveClientListLimit(rawLimit, opts);
246
+ } catch (e) {
247
+ if (e instanceof ListLimitError) throw invalidParam(e.message, "INVALID_LIMIT");
248
+ throw e;
249
+ }
250
+ }
251
+ /**
252
+ * Parse query parameters into QueryOptions
253
+ */
254
+ function parseQueryOptions(query, limits = {}) {
255
+ const options = {};
256
+ const rawLimit = getLastValue(query.limit);
257
+ const offsetVal = getLastValue(query.offset);
258
+ if (offsetVal) options.offset = parseInt(String(offsetVal));
259
+ const pageVal = getLastValue(query.page);
260
+ if (pageVal) {
261
+ const page = parseInt(String(pageVal));
262
+ const limit = resolveListLimitParam(rawLimit, {
263
+ defaultLimit: limits.defaultLimit,
264
+ maxLimit: limits.maxLimit
265
+ });
266
+ options.offset = (page - 1) * limit;
267
+ }
268
+ const orVal = getLastValue(query.or);
269
+ const andVal = getLastValue(query.and);
270
+ if (orVal) {
271
+ const logical = parseLogicalGroup("or", orVal);
272
+ if (logical) options.logical = logical;
273
+ } else if (andVal) {
274
+ const logical = parseLogicalGroup("and", andVal);
275
+ if (logical) options.logical = logical;
276
+ }
277
+ const reservedQueryKeys = [
278
+ "limit",
279
+ "offset",
280
+ "page",
281
+ "orderBy",
282
+ "include",
283
+ "fields",
284
+ "searchString",
285
+ "searchExplain",
286
+ "vector_search",
287
+ "vector",
288
+ "vector_distance",
289
+ "vector_threshold",
290
+ "or",
291
+ "and",
292
+ "where",
293
+ "select",
294
+ "groupBy"
295
+ ];
296
+ const filterDict = {};
297
+ for (const [key, rawValue] of Object.entries(query)) {
298
+ if (reservedQueryKeys.includes(key)) continue;
299
+ filterDict[key] = rawValue;
300
+ }
301
+ const whereVal = getLastValue(query.where);
302
+ const where = {
303
+ ...whereVal !== void 0 && whereVal !== null ? parseWhereParam(whereVal) : void 0,
304
+ ...decodeFilter(filterDict)
305
+ };
306
+ if (Object.keys(where).length > 0) options.where = where;
307
+ const orderByVal = getLastValue(query.orderBy);
308
+ if (orderByVal) options.orderBy = parseOrderByParam(orderByVal);
309
+ const includeVal = getLastValue(query.include);
310
+ if (includeVal) {
311
+ const includeStr = String(includeVal).trim();
312
+ if (includeStr === "*") options.include = ["*"];
313
+ else options.include = includeStr.split(",").map((s) => s.trim()).filter(Boolean);
314
+ }
315
+ const fieldsVal = getLastValue(query.fields);
316
+ if (fieldsVal) options.fields = String(fieldsVal).trim().split(",").map((s) => s.trim()).filter(Boolean);
317
+ const vectorSearchVal = getLastValue(query.vector_search);
318
+ const vectorVal = getLastValue(query.vector);
319
+ if (vectorSearchVal && vectorVal) {
320
+ const vectorStr = String(vectorVal);
321
+ let decoded;
322
+ try {
323
+ decoded = JSON.parse(vectorStr);
324
+ } catch {
325
+ decoded = void 0;
326
+ }
327
+ if (!Array.isArray(decoded) || !decoded.every((v) => typeof v === "number")) throw invalidParam("Invalid `vector` format. Expected a JSON array of numbers, e.g. [0.1,0.2,0.3]", "INVALID_VECTOR");
328
+ const queryVector = decoded;
329
+ const distanceParamVal = getLastValue(query.vector_distance);
330
+ const distanceParam = distanceParamVal ? String(distanceParamVal) : "cosine";
331
+ if (distanceParam !== "cosine" && distanceParam !== "l2" && distanceParam !== "inner_product") throw invalidParam(`Invalid \`vector_distance\`: ${distanceParam}. Expected: cosine, l2, or inner_product`, "INVALID_VECTOR_DISTANCE");
332
+ const vectorSearch = {
333
+ property: String(vectorSearchVal),
334
+ vector: queryVector,
335
+ distance: distanceParam
336
+ };
337
+ const thresholdVal = getLastValue(query.vector_threshold);
338
+ if (thresholdVal) {
339
+ const threshold = parseFloat(String(thresholdVal));
340
+ if (isNaN(threshold)) throw invalidParam("Invalid `vector_threshold`. Expected a number.", "INVALID_VECTOR_THRESHOLD");
341
+ vectorSearch.threshold = threshold;
342
+ }
343
+ options.vectorSearch = vectorSearch;
344
+ }
345
+ options.limit = resolveListLimitParam(rawLimit, {
346
+ vectorSearch: !!options.vectorSearch,
347
+ defaultLimit: limits.defaultLimit,
348
+ maxLimit: limits.maxLimit
349
+ });
350
+ return options;
351
+ }
352
+ //#endregion
353
+ export { resolveListLimitParam as a, parseQueryOptions as i, parseAggregateSelect as n, parseGroupBy as r, orderByEntriesToTuples as t };
354
+
355
+ //# sourceMappingURL=query-parser--nstjRCh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-parser--nstjRCh.js","names":[],"sources":["../src/api/rest/query-parser.ts"],"sourcesContent":["import type { FilterValues, ListLimitBounds, LogicalCondition, OrderByTuple, VectorSearchParams } from \"@rebasepro/types\";\nimport { toCanonicalOp, resolveClientListLimit, ListLimitError, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from \"@rebasepro/types\";\nimport { deserializeFilter, deserializeLogicalCondition, UnknownFilterOperatorError } from \"@rebasepro/common\";\nimport { QueryOptions } from \"../types\";\nimport { ApiError } from \"../errors\";\n\nexport const mapOperator = (op: string) => toCanonicalOp(op) ?? null;\n\n/**\n * A malformed query parameter, refused with a 400.\n *\n * Every rejection in this file is one of these, and every one of them is\n * `expected` — the flag `errorHandler` reads to log a routine outcome at debug\n * instead of warn. A client that mistypes an operator, a sort direction or a\n * limit is not an incident: nothing on the server is wrong, the request never\n * reached the database, and the caller has already been told what to fix in the\n * response body. Left at warn, a single frontend holding a stale field name\n * writes a `⚠️` line per request forever, and the warn level stops meaning\n * anything — which is why \"routine 4xx logs at WARN\" is a standing finding\n * against this API.\n *\n * Not a factory on `ApiError`: the class's members are part of the tracked\n * runtime surface (`api-surface/server.api.txt`), and this needs no addition to\n * it. `ApiError.unauthenticated` is the same idea one status code up.\n */\nfunction invalidParam(message: string, code: string, details?: unknown): ApiError {\n return new ApiError(400, code, message, details, true);\n}\n\n/**\n * Decode a filter, turning the shared codec's operator rejection into a 400.\n *\n * `deserializeFilter` lives in `@rebasepro/common`, which cannot throw an\n * `ApiError` — it does not depend on this package, and the browser SDK decodes\n * through the same function and has nothing to render one with. So it throws\n * `UnknownFilterOperatorError`, and the HTTP boundary is where that becomes a\n * status code. Same seam `parseLogicalGroup` uses for the nesting bound.\n *\n * Without this the operator string became a *value*: `?where={\"title\":\n * [\"!!\",\"Hello\"]}` compiled to `title IN ('!!','Hello')` and answered 200 with\n * the row the caller was filtering out, and `{\"id\":[\">>\",0]}` reached Postgres\n * and came back a 500 quoting `invalid input syntax for type integer`. Both are\n * malformed requests and now say so.\n */\nfunction decodeFilter(query: Record<string, unknown>): FilterValues<string> {\n try {\n return deserializeFilter(query);\n } catch (e) {\n if (e instanceof UnknownFilterOperatorError) {\n throw invalidParam(e.message, e.code, e.details);\n }\n throw e;\n }\n}\n\nfunction getLastValue(val: unknown): unknown {\n if (Array.isArray(val)) {\n return val[val.length - 1];\n }\n return val;\n}\n\n/**\n * Parse an `or(...)` / `and(...)` logical group from its wire form.\n *\n * The wire carries the inner conditions wrapped in parens (e.g.\n * `(status.eq.active,age.gte.18)`); we re-attach the `or`/`and` prefix and\n * delegate to the canonical filter dialect (`@rebasepro/common`). Values are\n * preserved as strings — type coercion is the schema-aware driver's job, so\n * this path stays byte-for-byte consistent with the SDK/admin path (which\n * also parses via the shared dialect).\n */\nfunction parseLogicalGroup(type: \"or\" | \"and\", raw: unknown): LogicalCondition | undefined {\n let inner = String(raw).trim();\n if (inner.startsWith(\"(\") && inner.endsWith(\")\")) {\n inner = inner.slice(1, -1);\n }\n inner = inner.trim();\n if (!inner) return undefined;\n let parsed;\n try {\n parsed = deserializeLogicalCondition(`${type}(${inner})`);\n } catch (e) {\n // The parser refuses a nesting depth no real filter reaches. That is a\n // request problem, and without this it surfaced as a 500 — the\n // unbounded version reached `RangeError: Maximum call stack size\n // exceeded`, which tells the caller nothing about their filter.\n throw invalidParam(\n `Invalid \\`${type}\\` parameter: ${e instanceof Error ? e.message : String(e)}`,\n \"INVALID_LOGICAL_GROUP\"\n );\n }\n return \"type\" in parsed ? parsed : undefined;\n}\n\n/**\n * Parse the `?where=` JSON filter object.\n *\n * This is the dialect the OpenAPI document publishes on every\n * `GET /api/data/{slug}` — `{\"status\":[\"==\",\"active\"]}`: field → canonical\n * `[WhereFilterOp, value]` tuple. It is normalized through the same\n * `deserializeFilter` as the `?field=op.value` params below, so a value that\n * arrives as a PostgREST dot-string (`{\"status\":\"eq.active\"}`) or as a bare\n * scalar (`{\"status\":\"active\"}`) compiles to the same condition. Unlike the\n * querystring dialect, JSON carries types — a number stays a number.\n *\n * A malformed value is a 400 rather than a silent drop: dropping the filter\n * would run the read unfiltered and return everything RLS happens to allow.\n */\nfunction parseWhereParam(raw: unknown): FilterValues<string> | undefined {\n const str = String(raw).trim();\n if (!str) return undefined;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(str);\n } catch {\n throw invalidParam(\n \"Invalid `where` parameter: expected a JSON object, e.g. {\\\"status\\\":[\\\"==\\\",\\\"active\\\"]}\",\n \"INVALID_WHERE\"\n );\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw invalidParam(\n \"Invalid `where` parameter: expected a JSON object mapping fields to conditions, \"\n + \"e.g. {\\\"status\\\":[\\\"==\\\",\\\"active\\\"]}\",\n \"INVALID_WHERE\"\n );\n }\n\n const filter = decodeFilter(parsed as Record<string, unknown>);\n return Object.keys(filter).length > 0 ? filter : undefined;\n}\n\ntype OrderByEntry = { field: string; direction: \"asc\" | \"desc\" };\n\n/**\n * The parsed entries as the driver contract spells them: `[field, direction]`\n * tuples in order of significance.\n *\n * The REST layer used to hand the driver `orderBy[0].field` and drop the rest,\n * so `?orderBy=[{\"field\":\"roles\"},{\"field\":\"created_at\",\"direction\":\"desc\"}]`\n * — a shape this parser has always accepted and validated in full — sorted by\n * `roles` alone and returned the ties in whatever order Postgres pleased.\n */\nexport function orderByEntriesToTuples(entries?: OrderByEntry[]): OrderByTuple[] | undefined {\n if (!entries || entries.length === 0) return undefined;\n return entries.map(({ field, direction }) => [field, direction] as OrderByTuple);\n}\n\nfunction invalidOrderBy(detail: string): never {\n throw invalidParam(\n `Invalid \\`orderBy\\` parameter: ${detail}. Expected \\`field\\`, \\`field:desc\\`, `\n + \"or a JSON array like [{\\\"field\\\":\\\"created_at\\\",\\\"direction\\\":\\\"desc\\\"}]\",\n \"INVALID_ORDER_BY\"\n );\n}\n\n/** The aggregate functions `?select=` accepts. */\nconst AGGREGATE_FUNCTIONS = new Set([\"count\", \"sum\", \"avg\", \"min\", \"max\"]);\n\nexport interface ParsedAggregate {\n fn: \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\";\n /** Absent only for `count()`, which counts rows rather than values. */\n field?: string;\n /** The key this appears under in the response. */\n alias: string;\n}\n\n/**\n * Parse `?select=count(),sum(total),avg(total)`.\n *\n * The spelling is SQL's, because whoever writes it is thinking in SQL and\n * because any other spelling has to be learned first. `count()` with no field\n * counts rows; every other function names a column.\n *\n * Aliases are derived rather than accepted: `sum(total)` returns as\n * `sum_total`, `count()` as `count`. Letting a caller choose would mean\n * checking their alias is not also a `groupBy` field — a rule nobody would\n * guess, and a silently overwritten value if it went unchecked.\n */\nexport function parseAggregateSelect(raw: unknown): ParsedAggregate[] | undefined {\n const value = getLastValue(raw);\n if (!value) return undefined;\n\n const entries = String(value).split(\",\").map(s => s.trim()).filter(Boolean);\n if (entries.length === 0) return undefined;\n\n return entries.map((entry) => {\n const match = /^([a-z]+)\\(\\s*([A-Za-z0-9_]*)\\s*\\)$/i.exec(entry);\n if (!match) {\n throw invalidParam(\n `Invalid \\`select\\` entry \"${entry}\". Expected \\`fn(field)\\`, e.g. \\`sum(total)\\` or \\`count()\\`.`,\n \"INVALID_AGGREGATE_SELECT\"\n );\n }\n\n const fn = match[1].toLowerCase();\n const field = match[2] || undefined;\n\n if (!AGGREGATE_FUNCTIONS.has(fn)) {\n throw invalidParam(\n `Unknown aggregate function \"${fn}\". Expected: ${[...AGGREGATE_FUNCTIONS].join(\", \")}.`,\n \"INVALID_AGGREGATE_FUNCTION\"\n );\n }\n if (fn !== \"count\" && !field) {\n // `sum()` has no sensible reading, and guessing one would be\n // inventing a column on the caller's behalf.\n throw invalidParam(\n `\\`${fn}()\\` needs a field, e.g. \\`${fn}(total)\\`. Only \\`count()\\` may be empty.`,\n \"INVALID_AGGREGATE_SELECT\"\n );\n }\n\n return {\n fn: fn as ParsedAggregate[\"fn\"],\n field,\n alias: field ? `${fn}_${field}` : fn\n };\n });\n}\n\n/** Parse `?groupBy=status,country`. */\nexport function parseGroupBy(raw: unknown): string[] | undefined {\n const value = getLastValue(raw);\n if (!value) return undefined;\n const fields = String(value).split(\",\").map(s => s.trim()).filter(Boolean);\n return fields.length > 0 ? fields : undefined;\n}\n\n/** `asc`/`desc`, in any case. Anything else is a request to sort in a way that does not exist. */\nfunction toDirection(raw: unknown, context: string): \"asc\" | \"desc\" {\n if (raw === undefined || raw === null) return \"asc\";\n if (typeof raw !== \"string\") invalidOrderBy(`${context} has a non-string \\`direction\\``);\n const lowered = raw.toLowerCase();\n if (lowered !== \"asc\" && lowered !== \"desc\") {\n invalidOrderBy(`${context} has direction '${raw}'`);\n }\n return lowered;\n}\n\n/** One entry: the canonical `{field, direction}`, or the `field:direction` shorthand as a string. */\nfunction toOrderByEntry(raw: unknown, index: number): OrderByEntry {\n const context = `entry ${index}`;\n if (typeof raw === \"string\") {\n // Split here rather than through `deserializeOrderBy`, which is the\n // *client* end of the codec and normalises anything that is not\n // literally \"desc\" to \"asc\". Routed through it, `?orderBy=x:DESC`\n // reached `toDirection` already collapsed to \"asc\" and answered 200\n // with the rows in the opposite order — a newest-first list showing\n // the oldest rows — and `x:sideways` did the same. The direction token\n // has to arrive here raw for `toDirection` to have anything to refuse.\n const idx = raw.indexOf(\":\");\n const field = (idx === -1 ? raw : raw.slice(0, idx)).trim();\n if (!field) invalidOrderBy(`${context} is an empty field name`);\n return { field, direction: idx === -1 ? \"asc\" : toDirection(raw.slice(idx + 1), context) };\n }\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n invalidOrderBy(`${context} is not a field name or a {field, direction} object`);\n }\n const entry = raw as Record<string, unknown>;\n if (typeof entry.field !== \"string\" || entry.field.trim() === \"\") {\n invalidOrderBy(`${context} has no \\`field\\``);\n }\n return { field: entry.field, direction: toDirection(entry.direction, context) };\n}\n\n/**\n * Parse the `orderBy` query parameter.\n *\n * The field *name* has been validated against the schema for a while — an\n * `?orderBy=titel` is a 400 rather than 200 with unsorted rows, on the grounds\n * that silently dropping the sort leaves the caller believing in an order that\n * is not there. The parameter's *shape* was never checked the same way, and it\n * failed in exactly the same silent manner one layer earlier: whatever\n * `JSON.parse` returned was assigned to an option declared as an array of\n * `{field, direction}`, and the REST layer reads only `orderBy[0].field`. So\n * `?orderBy={\"field\":\"name\"}` — an object rather than an array, and the most\n * natural thing for a client to try — read `undefined`, dropped the ORDER BY,\n * and answered 200. So did a number, a boolean, `null`, and `[\"name\"]`.\n *\n * This refuses those, the way `parseWhereParam` above already refuses a\n * malformed filter and for the same reason. What it keeps working is every\n * shape that worked before: the `field` and `field:desc` shorthands, and the\n * canonical JSON array.\n */\nfunction parseOrderByParam(raw: unknown): OrderByEntry[] | undefined {\n if (Array.isArray(raw)) {\n // A repeated query parameter arrives pre-split; treat it as the list.\n return raw.length === 0 ? undefined : raw.map(toOrderByEntry);\n }\n\n const str = String(raw).trim();\n if (!str) return undefined;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(str);\n } catch {\n // Not JSON at all, so it is the `field:direction` shorthand.\n return [toOrderByEntry(str, 0)];\n }\n\n // `JSON.parse` succeeding says nothing about the shape being usable.\n if (Array.isArray(parsed)) {\n if (parsed.length === 0) return undefined;\n return parsed.map(toOrderByEntry);\n }\n if (typeof parsed === \"string\") return [toOrderByEntry(parsed, 0)];\n if (typeof parsed === \"object\" && parsed !== null) {\n // A bare `{field, direction}` is a near miss rather than nonsense, but\n // accepting it would leave two spellings of one parameter. Name it.\n invalidOrderBy(\"a single object was given where a JSON array was expected\");\n }\n invalidOrderBy(`${typeof parsed} is not a field name or a list of them`);\n}\n\n// Re-exported for callers/tests that reference the REST list bounds. The\n// numbers and the rule live in `@rebasepro/types` so the REST parser and the\n// WebSocket ingress enforce ONE shared guarantee. See `resolveClientListLimit`.\nexport { DEFAULT_LIST_LIMIT, DEFAULT_VECTOR_LIST_LIMIT, MAX_LIST_LIMIT } from \"@rebasepro/types\";\n\n/**\n * Overridable list-pagination bounds for {@link parseQueryOptions}. Without\n * these, `GET /<collection>` with no `?limit` would buffer the ENTIRE table\n * into a JS array + JSON response (a trivial OOM/DoS), and `?limit=100000000`\n * would be honoured verbatim.\n */\nexport interface ListLimitOptions {\n /**\n * Page size used when the client sends no `?limit`. Applied to plain and\n * text-search reads — a vector search falls back to its own default (10).\n */\n defaultLimit?: number;\n /** Largest `?limit` a client may ask for. A larger one is a 400, not a clamp. */\n maxLimit?: number;\n}\n\n/**\n * {@link resolveClientListLimit} for an HTTP route: the same bounds, answered\n * with a 400 rather than a 500.\n *\n * The shared resolver throws a `ListLimitError`, which carries `status` — but\n * the Hono error handler discriminates on `statusCode`, so an unconverted one\n * reaches the client as `INTERNAL_ERROR` with its message stripped, telling the\n * caller nothing about the parameter it got wrong. Every REST list ingress\n * routes its `limit` through here so all of them name the ceiling the same way.\n */\nexport function resolveListLimitParam(\n rawLimit: number | string | null | undefined,\n opts: ListLimitBounds & { vectorSearch?: boolean } = {}\n): number {\n try {\n return resolveClientListLimit(rawLimit, opts);\n } catch (e) {\n if (e instanceof ListLimitError) {\n throw invalidParam(e.message, \"INVALID_LIMIT\");\n }\n throw e;\n }\n}\n\n/**\n * Parse query parameters into QueryOptions\n */\nexport function parseQueryOptions(\n query: Record<string, unknown>,\n limits: ListLimitOptions = {}\n): QueryOptions {\n const options: QueryOptions = {};\n const rawLimit = getLastValue(query.limit) as number | string | null | undefined;\n\n const offsetVal = getLastValue(query.offset);\n if (offsetVal) options.offset = parseInt(String(offsetVal));\n\n const pageVal = getLastValue(query.page);\n if (pageVal) {\n const page = parseInt(String(pageVal));\n // Page stride uses the same bounded page size the read will use, so\n // pages neither overlap nor gap. (Vector search never paginates by\n // page, so the plain/text default is correct here.)\n const limit = resolveListLimitParam(rawLimit, {\n defaultLimit: limits.defaultLimit,\n maxLimit: limits.maxLimit\n });\n options.offset = (page - 1) * limit;\n }\n\n // ── Logical conditions (or / and) ──────────────────────────────────\n const orVal = getLastValue(query.or);\n const andVal = getLastValue(query.and);\n if (orVal) {\n const logical = parseLogicalGroup(\"or\", orVal);\n if (logical) options.logical = logical;\n } else if (andVal) {\n const logical = parseLogicalGroup(\"and\", andVal);\n if (logical) options.logical = logical;\n }\n\n // ── PostgREST-style field filters: ?field=op.value ─────────────────\n // Delegate to the canonical filter dialect (the single source of truth\n // for the wire grammar: operator codes, list/escape handling, implicit\n // eq). Values stay strings; the schema-aware driver coerces them to\n // column types. This keeps the REST path byte-for-byte consistent with\n // the SDK/admin path, which parses through the same `deserializeFilter`.\n //\n // `where` is reserved: it is the JSON filter dialect (see\n // `parseWhereParam`), not a column named \"where\". Leaving it out of this\n // list made the documented `?where={...}` compile as a filter on a\n // nonexistent field — which used to be dropped, widening the read to the\n // whole table, and is now a 400 `UNKNOWN_FILTER_FIELD`.\n //\n // `select` and `groupBy` are reserved for the same reason: on\n // `/aggregate` they are the request, and left out of this list\n // `?select=sum(total)` compiles into the filter as a comparison on a\n // column named \"select\" — a 400 on the one endpoint that requires it.\n const reservedQueryKeys = [\"limit\", \"offset\", \"page\", \"orderBy\", \"include\", \"fields\", \"searchString\", \"searchExplain\", \"vector_search\", \"vector\", \"vector_distance\", \"vector_threshold\", \"or\", \"and\", \"where\", \"select\", \"groupBy\"];\n const filterDict: Record<string, unknown> = {};\n for (const [key, rawValue] of Object.entries(query)) {\n if (reservedQueryKeys.includes(key)) continue;\n filterDict[key] = rawValue;\n }\n // Both dialects may be sent together; an explicit `?field=op.value` wins\n // over the same field inside `where`, being the more specific request.\n const whereVal = getLastValue(query.where);\n const where = {\n ...(whereVal !== undefined && whereVal !== null ? parseWhereParam(whereVal) : undefined),\n ...decodeFilter(filterDict)\n };\n if (Object.keys(where).length > 0) {\n options.where = where;\n }\n\n // Sorting\n const orderByVal = getLastValue(query.orderBy);\n if (orderByVal) {\n options.orderBy = parseOrderByParam(orderByVal);\n }\n\n // Relation includes\n const includeVal = getLastValue(query.include);\n if (includeVal) {\n const includeStr = String(includeVal).trim();\n if (includeStr === \"*\") {\n options.include = [\"*\"];\n } else {\n options.include = includeStr.split(\",\").map(s => s.trim()).filter(Boolean);\n }\n }\n\n // Field selection\n const fieldsVal = getLastValue(query.fields);\n if (fieldsVal) {\n const fieldsStr = String(fieldsVal).trim();\n options.fields = fieldsStr.split(\",\").map(s => s.trim()).filter(Boolean);\n }\n\n // ── Vector similarity search ───────────────────────────────────────\n // Every rejection here is a malformed *request*, so it must carry a 400.\n // A bare `Error` reaches the handler with no `statusCode` and no known\n // `code`, which makes it a 500 — logged with a full stack as an incident,\n // and answered with \"An unexpected error occurred\", because the handler\n // only forwards a message to the client below 500. The caller was told\n // nothing about what it got wrong.\n const vectorSearchVal = getLastValue(query.vector_search);\n const vectorVal = getLastValue(query.vector);\n if (vectorSearchVal && vectorVal) {\n const vectorStr = String(vectorVal);\n let decoded: unknown;\n try {\n decoded = JSON.parse(vectorStr);\n } catch {\n decoded = undefined;\n }\n // Validated outside the `try` on purpose: inside it, the thrown\n // ApiError would be caught by its own `catch` and re-thrown as\n // something else.\n if (!Array.isArray(decoded) || !decoded.every(v => typeof v === \"number\")) {\n throw invalidParam(\n \"Invalid `vector` format. Expected a JSON array of numbers, e.g. [0.1,0.2,0.3]\",\n \"INVALID_VECTOR\"\n );\n }\n const queryVector = decoded as number[];\n\n const distanceParamVal = getLastValue(query.vector_distance);\n const distanceParam = distanceParamVal ? String(distanceParamVal) : \"cosine\";\n if (distanceParam !== \"cosine\" && distanceParam !== \"l2\" && distanceParam !== \"inner_product\") {\n throw invalidParam(\n `Invalid \\`vector_distance\\`: ${distanceParam}. Expected: cosine, l2, or inner_product`,\n \"INVALID_VECTOR_DISTANCE\"\n );\n }\n\n const vectorSearch: VectorSearchParams = {\n property: String(vectorSearchVal),\n vector: queryVector,\n distance: distanceParam\n };\n\n const thresholdVal = getLastValue(query.vector_threshold);\n if (thresholdVal) {\n const threshold = parseFloat(String(thresholdVal));\n if (isNaN(threshold)) {\n throw invalidParam(\n \"Invalid `vector_threshold`. Expected a number.\",\n \"INVALID_VECTOR_THRESHOLD\"\n );\n }\n vectorSearch.threshold = threshold;\n }\n\n options.vectorSearch = vectorSearch;\n }\n\n // Resolve the limit LAST — once we know whether this is a vector search —\n // so a client-supplied limit above the ceiling is refused with a 400 and an\n // absent one falls back to the correct mode default (plain/text =\n // defaultLimit, vector = 10). Without this a bare `GET /<collection>` would\n // return the whole table. Shared with the WebSocket ingress via\n // `resolveClientListLimit`.\n options.limit = resolveListLimitParam(rawLimit, {\n vectorSearch: !!options.vectorSearch,\n defaultLimit: limits.defaultLimit,\n maxLimit: limits.maxLimit\n });\n\n return options;\n}\n"],"mappings":"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"}
@@ -1,8 +1,15 @@
1
- import { createRequire as __createRequire } from "module";
2
- import process from "process";
3
- __createRequire(import.meta.url);
4
- import { t as logger } from "./logger-DfvF_8r-.js";
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
+ import { t as logger } from "./logger-DS03e908.js";
5
7
  //#region src/functions/request-timeout.ts
8
+ var request_timeout_exports = /* @__PURE__ */ __exportAll({
9
+ DEFAULT_FUNCTIONS_TIMEOUT_MS: () => DEFAULT_FUNCTIONS_TIMEOUT_MS,
10
+ createFunctionsRequestTimeout: () => createFunctionsRequestTimeout,
11
+ resolveFunctionsTimeoutMs: () => resolveFunctionsTimeoutMs
12
+ });
6
13
  /** Default ceiling for a custom function request, in milliseconds. */
7
14
  var DEFAULT_FUNCTIONS_TIMEOUT_MS = 3e4;
8
15
  /**
@@ -39,6 +46,14 @@ function resolveFunctionsTimeoutMs(configured) {
39
46
  * eventual result is dropped. It is a ceiling, not a kill switch, and the exact
40
47
  * number matters far less than its existence.
41
48
  *
49
+ * "The handler keeps running" is a **Node** guarantee, not a property of the
50
+ * contract. It follows from the process outliving the request, which is not
51
+ * true on an isolate-based host: there, work still in flight when the response
52
+ * resolves is terminated rather than orphaned. So a handler must not depend on
53
+ * finishing after its 504 — anything that has to complete belongs in
54
+ * `waitUntil()`, which is the one construct both hosts honour. See
55
+ * `./wait-until.ts`.
56
+ *
42
57
  * Mounted in front of the auth middleware rather than behind it, so a wedged
43
58
  * driver — the failure that also hangs `withAuth()` — is covered too.
44
59
  */
@@ -63,6 +78,6 @@ function createFunctionsRequestTimeout(ms) {
63
78
  };
64
79
  }
65
80
  //#endregion
66
- export { createFunctionsRequestTimeout, resolveFunctionsTimeoutMs };
81
+ export { request_timeout_exports as t };
67
82
 
68
- //# sourceMappingURL=request-timeout-BuFoEKwT.js.map
83
+ //# sourceMappingURL=request-timeout-C8gkc-j7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-timeout-C8gkc-j7.js","names":[],"sources":["../src/functions/request-timeout.ts"],"sourcesContent":["import type { MiddlewareHandler } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { logger } from \"../utils/logger\";\n\n/** Default ceiling for a custom function request, in milliseconds. */\nexport const DEFAULT_FUNCTIONS_TIMEOUT_MS = 30_000;\n\n/**\n * Resolve the functions request timeout from config, then env, then the default.\n *\n * `0` (or any non-positive number) disables the ceiling — for a deployment\n * whose proxy already imposes one, or a function that legitimately streams for\n * minutes.\n */\nexport function resolveFunctionsTimeoutMs(configured?: number): number {\n if (typeof configured === \"number\" && Number.isFinite(configured)) {\n return Math.max(0, Math.floor(configured));\n }\n // Blank means unset, not zero. `Number(\"\")` and `Number(\" \")` are both 0,\n // and 0 here means \"no ceiling\" — so a compose file with\n // `REBASE_FUNCTIONS_TIMEOUT_MS=${SOMETHING}` and `SOMETHING` undefined, or\n // a `.env` line with the name and no value, silently switched off the one\n // bound on how long code the framework did not write may hold a socket.\n // Declaring a variable without setting it is the ordinary way to write\n // both of those files, and the failure is invisible: nothing logs, and the\n // deployment behaves exactly as it did before the ceiling existed.\n const raw = process.env.REBASE_FUNCTIONS_TIMEOUT_MS?.trim();\n if (raw) {\n const fromEnv = Number(raw);\n if (Number.isFinite(fromEnv) && fromEnv >= 0) {\n return Math.floor(fromEnv);\n }\n }\n return DEFAULT_FUNCTIONS_TIMEOUT_MS;\n}\n\n/**\n * A per-request ceiling for the custom functions router.\n *\n * Custom functions are the one router that runs code the framework did not\n * write, and nothing else in the stack bounds how long that code takes: the\n * Node server is constructed without `requestTimeout`/`headersTimeout`, so a\n * handler awaiting a promise that never settles — a `fetch` to an unreachable\n * third party with no `AbortSignal`, a query on a wedged connection — holds its\n * socket and its request object until the client gives up. On the managed\n * runtime the process is shared between tenants, so that is not only the slow\n * caller's problem.\n *\n * The handler is **not** cancelled — it cannot be, there is no cancellation\n * token to hand user code. What is bounded is the client-visible request: after\n * `ms` the caller gets a 504 and the socket is released, and the handler's\n * eventual result is dropped. It is a ceiling, not a kill switch, and the exact\n * number matters far less than its existence.\n *\n * \"The handler keeps running\" is a **Node** guarantee, not a property of the\n * contract. It follows from the process outliving the request, which is not\n * true on an isolate-based host: there, work still in flight when the response\n * resolves is terminated rather than orphaned. So a handler must not depend on\n * finishing after its 504 — anything that has to complete belongs in\n * `waitUntil()`, which is the one construct both hosts honour. See\n * `./wait-until.ts`.\n *\n * Mounted in front of the auth middleware rather than behind it, so a wedged\n * driver — the failure that also hangs `withAuth()` — is covered too.\n */\nexport function createFunctionsRequestTimeout(ms: number): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n if (ms <= 0) return next();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), ms);\n });\n\n try {\n const outcome = await Promise.race([next().then(() => \"done\" as const), timedOut]);\n if (outcome === \"timeout\") {\n logger.warn(\n `[functions] ${c.req.method} ${c.req.path} exceeded the ${ms}ms request timeout — ` +\n \"answering 504. The handler is still running; it cannot be cancelled from here. \" +\n \"Give outbound calls an AbortSignal, or raise `functionsTimeoutMs` / REBASE_FUNCTIONS_TIMEOUT_MS.\"\n );\n return c.json({\n error: {\n message: \"Function timed out\",\n code: \"FUNCTION_TIMEOUT\"\n }\n }, 504);\n }\n } finally {\n if (timer) clearTimeout(timer);\n }\n return undefined;\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAKA,IAAa,+BAA+B;;;;;;;;AAS5C,SAAgB,0BAA0B,YAA6B;CACnE,IAAI,OAAO,eAAe,YAAY,OAAO,SAAS,UAAU,GAC5D,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,CAAC;CAU7C,MAAM,MAAM,QAAQ,IAAI,6BAA6B,KAAK;CAC1D,IAAI,KAAK;EACL,MAAM,UAAU,OAAO,GAAG;EAC1B,IAAI,OAAO,SAAS,OAAO,KAAK,WAAW,GACvC,OAAO,KAAK,MAAM,OAAO;CAEjC;CACA,OAAO;AACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,8BAA8B,IAAwC;CAClF,OAAO,OAAO,GAAG,SAAS;EACtB,IAAI,MAAM,GAAG,OAAO,KAAK;EAEzB,IAAI;EACJ,MAAM,WAAW,IAAI,SAAoB,YAAY;GACjD,QAAQ,iBAAiB,QAAQ,SAAS,GAAG,EAAE;EACnD,CAAC;EAED,IAAI;GAEA,IAAI,MADkB,QAAQ,KAAK,CAAC,KAAK,CAAC,CAAC,WAAW,MAAe,GAAG,QAAQ,CAAC,MACjE,WAAW;IACvB,OAAO,KACH,eAAe,EAAE,IAAI,OAAO,GAAG,EAAE,IAAI,KAAK,gBAAgB,GAAG,uMAGjE;IACA,OAAO,EAAE,KAAK,EACV,OAAO;KACH,SAAS;KACT,MAAM;IACV,EACJ,GAAG,GAAG;GACV;EACJ,UAAU;GACN,IAAI,OAAO,aAAa,KAAK;EACjC;CAEJ;AACJ"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The shape `@rebasepro/rls-check` returns. Declared structurally rather than
3
+ * imported as a type, so this module carries no dependency on it at all — see
4
+ * the module comment.
5
+ */
6
+ export interface RlsScanFinding {
7
+ id: string;
8
+ severity: "info" | "low" | "medium" | "high" | "critical";
9
+ title: string;
10
+ target: {
11
+ schema: string;
12
+ table?: string;
13
+ policy?: string;
14
+ };
15
+ }
16
+ export interface RlsScanResult {
17
+ scannedAt: string;
18
+ /** Host and database name only — the checker never returns credentials. */
19
+ database: {
20
+ host: string;
21
+ name: string;
22
+ };
23
+ stats: {
24
+ schemas: number;
25
+ tables: number;
26
+ policies: number;
27
+ tablesWithoutRls: number;
28
+ checksRun: number;
29
+ };
30
+ findings: RlsScanFinding[];
31
+ }
32
+ declare const SEVERITY_ORDER: readonly ["info", "low", "medium", "high", "critical"];
33
+ export type RlsSeverity = (typeof SEVERITY_ORDER)[number];
34
+ export interface RlsAuditConfig {
35
+ /** Off unless set. See the module comment for why. */
36
+ enabled?: boolean;
37
+ /**
38
+ * How often to run, in milliseconds. Default 24 hours.
39
+ *
40
+ * The thing being watched is a schema, which changes on deploys, not on
41
+ * traffic — so this is a daily safety net, not a monitor.
42
+ */
43
+ intervalMs?: number;
44
+ /** Run once at startup as well as on the interval. Default true. */
45
+ runOnBoot?: boolean;
46
+ /** Restrict the scan to these schemas. Default: every user schema. */
47
+ schemas?: string[];
48
+ /**
49
+ * Where to connect. Defaults to `DATABASE_URL`.
50
+ *
51
+ * Taken as a string rather than borrowed from the driver because the checker
52
+ * connects on its own terms — its own session, its own statement timeout —
53
+ * and threading it through a driver would make the audit's behaviour depend
54
+ * on the pool it borrowed from.
55
+ */
56
+ connectionString?: string;
57
+ /**
58
+ * Severity at or above which a completed run logs a warning rather than an
59
+ * info line. Default `high`.
60
+ */
61
+ warnAtSeverity?: RlsSeverity;
62
+ /** Statement timeout for the scan's own queries. Default 30s. */
63
+ statementTimeoutMs?: number;
64
+ /**
65
+ * The scanner. Pass `scan` from `@rebasepro/rls-check`:
66
+ *
67
+ * ```ts
68
+ * import { scan } from "@rebasepro/rls-check";
69
+ * rlsAudit: { enabled: true, scan }
70
+ * ```
71
+ *
72
+ * Required when `enabled`. See the module comment for why it is not
73
+ * imported here.
74
+ */
75
+ scan?: RlsScanner;
76
+ }
77
+ /** The one function this module needs from `@rebasepro/rls-check`. */
78
+ export type RlsScanner = (options: {
79
+ connectionString: string;
80
+ schemas?: string[];
81
+ statementTimeoutMs?: number;
82
+ }) => Promise<RlsScanResult>;
83
+ export interface RlsAuditStatus {
84
+ enabled: boolean;
85
+ /** Why it is not running, when it is not. */
86
+ reason?: string;
87
+ lastRunAt?: string;
88
+ lastError?: string;
89
+ result?: RlsScanResult;
90
+ }
91
+ export interface RlsAudit {
92
+ start(): void;
93
+ stop(): void;
94
+ /** Run now, outside the schedule. Resolves when the run finishes. */
95
+ runNow(): Promise<void>;
96
+ status(): RlsAuditStatus;
97
+ }
98
+ /** One line naming what the run found, at the right level to be noticed. */
99
+ export declare function summarize(result: RlsScanResult, warnAt: RlsSeverity): {
100
+ level: "warn" | "info";
101
+ message: string;
102
+ };
103
+ /**
104
+ * Create the audit. Nothing runs until {@link RlsAudit.start}.
105
+ *
106
+ * Because the scanner is supplied by the caller, the schedule, the summary and
107
+ * the failure handling are all testable without a database and without
108
+ * `@rebasepro/rls-check` present.
109
+ */
110
+ export declare function createRlsAudit(config: RlsAuditConfig): RlsAudit;
111
+ export {};
@@ -1,6 +1,7 @@
1
- import { createRequire as __createRequire } from "module";
2
- import "process";
3
- const require = __createRequire(import.meta.url);
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ const require = __rebaseCreateRequire(import.meta.url);
4
5
  //#region \0rolldown/runtime.js
5
6
  var __create = Object.create;
6
7
  var __defProp = Object.defineProperty;