@palbase/backend 17.4.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/bin/palbase-backend.cjs +1848 -0
  2. package/dist/bin/palbase-backend.cjs.map +1 -0
  3. package/dist/bin/palbase-backend.d.cts +1 -0
  4. package/dist/bin/palbase-backend.d.ts +1 -0
  5. package/dist/bin/palbase-backend.js +168 -0
  6. package/dist/bin/palbase-backend.js.map +1 -0
  7. package/dist/chunk-7D4SUZUM.js +38 -0
  8. package/dist/chunk-7D4SUZUM.js.map +1 -0
  9. package/dist/chunk-N32VDWKH.js +172 -0
  10. package/dist/chunk-N32VDWKH.js.map +1 -0
  11. package/dist/chunk-POYAFBLF.js +189 -0
  12. package/dist/chunk-POYAFBLF.js.map +1 -0
  13. package/dist/chunk-QMVK4X3V.js +200 -0
  14. package/dist/chunk-QMVK4X3V.js.map +1 -0
  15. package/dist/chunk-SSGAMC26.js +342 -0
  16. package/dist/chunk-SSGAMC26.js.map +1 -0
  17. package/dist/chunk-VYH4U7ZQ.js +1138 -0
  18. package/dist/chunk-VYH4U7ZQ.js.map +1 -0
  19. package/dist/{chunk-AAN642N5.js → chunk-W5ODXPY3.js} +2 -336
  20. package/dist/chunk-W5ODXPY3.js.map +1 -0
  21. package/dist/chunk-YL4C5NRY.js +90 -0
  22. package/dist/chunk-YL4C5NRY.js.map +1 -0
  23. package/dist/db/env.cjs.map +1 -1
  24. package/dist/db/env.d.cts +21 -1
  25. package/dist/db/env.d.ts +21 -1
  26. package/dist/db/index.cjs.map +1 -1
  27. package/dist/db/index.d.cts +2 -1
  28. package/dist/db/index.d.ts +2 -1
  29. package/dist/db/index.js +9 -6
  30. package/dist/{index-VLrU7rSW.d.ts → endpoint-B0LpZixz.d.cts} +124 -685
  31. package/dist/{index-BA_oFAz9.d.cts → endpoint-B0LpZixz.d.ts} +124 -685
  32. package/dist/engine/index.cjs +1797 -0
  33. package/dist/engine/index.cjs.map +1 -0
  34. package/dist/engine/index.d.cts +7 -0
  35. package/dist/engine/index.d.ts +7 -0
  36. package/dist/engine/index.js +43 -0
  37. package/dist/engine/index.js.map +1 -0
  38. package/dist/index-B46CGNvx.d.cts +839 -0
  39. package/dist/index-BGSCWlUa.d.cts +674 -0
  40. package/dist/index-DZDUMth5.d.ts +839 -0
  41. package/dist/index-g-EzitI-.d.ts +674 -0
  42. package/dist/index.cjs +1031 -11
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +290 -532
  45. package/dist/index.d.ts +290 -532
  46. package/dist/index.js +999 -509
  47. package/dist/index.js.map +1 -1
  48. package/dist/openapi/index.cjs +6464 -0
  49. package/dist/openapi/index.cjs.map +1 -0
  50. package/dist/openapi/index.d.cts +170 -0
  51. package/dist/openapi/index.d.ts +170 -0
  52. package/dist/openapi/index.js +6248 -0
  53. package/dist/openapi/index.js.map +1 -0
  54. package/dist/registry-3BLYv4si.d.ts +338 -0
  55. package/dist/registry-Cw0YEYCg.d.cts +338 -0
  56. package/dist/test/index.js +2 -0
  57. package/dist/test/index.js.map +1 -1
  58. package/docs/database.md +16 -3
  59. package/docs/llms-full.txt +16 -3
  60. package/package.json +43 -13
  61. package/stager/package.json +4 -0
  62. package/stager/return_types.js +338 -0
  63. package/stager/stage.js +78 -0
  64. package/stager/throw_analysis.js +726 -0
  65. package/template/AGENTS.md +261 -0
  66. package/template/config/secrets.ts +24 -0
  67. package/template/controllers/health.controller.ts +30 -0
  68. package/template/db/schema.ts +35 -0
  69. package/template/package.json +18 -0
  70. package/template/tsconfig.json +30 -0
  71. package/LICENSE +0 -21
  72. package/dist/chunk-AAN642N5.js.map +0 -1
package/dist/index.js CHANGED
@@ -2,239 +2,72 @@ import {
2
2
  EXTENSION_DEPENDENCIES,
3
3
  PALBASE_EXTENSIONS,
4
4
  PolicyBuilder,
5
- TxPlanBuilder,
6
- TxPlanError,
7
- TxRefError,
8
5
  bigint,
9
6
  boolean,
10
- dec,
11
7
  defineSchema,
12
8
  enumType,
13
- inc,
14
9
  integer,
15
10
  isPalbaseExtension,
16
11
  jsonb,
17
12
  makeTypedDB,
18
- now,
19
13
  numeric,
20
14
  policy,
21
15
  raw,
22
- runTxPlan,
23
16
  text,
24
17
  timestamp,
25
18
  uuid
26
- } from "./chunk-AAN642N5.js";
27
-
28
- // src/runtime.ts
29
- import { AsyncLocalStorage } from "async_hooks";
30
- var __requestALS = new AsyncLocalStorage();
31
- var runtime = null;
32
- function __setRuntime(services) {
33
- runtime = services;
34
- }
35
- function __runWithRuntime(services, fn) {
36
- return __requestALS.run({ runtime: services }, fn);
37
- }
38
- function __getRuntime() {
39
- const scoped = __requestALS.getStore();
40
- if (scoped) return scoped.runtime;
41
- if (runtime === null) {
42
- throw new Error(
43
- "Palbase services accessed outside a request scope. The Database/Documents/\u2026 singletons are only available inside an endpoint handler (or after the runtime has called __runWithRuntime / __setRuntime)."
44
- );
45
- }
46
- return runtime;
47
- }
48
- function makeServiceProxy(key) {
49
- const handler = {
50
- get(_target, prop, receiver) {
51
- const client = __getRuntime()[key];
52
- const value = Reflect.get(client, prop, receiver);
53
- return typeof value === "function" ? value.bind(client) : value;
54
- }
55
- };
56
- return new Proxy({}, handler);
57
- }
58
- function makeTablesAccessor(ops) {
59
- const tablesProxy = new Proxy(
60
- {},
61
- {
62
- get(_t, prop) {
63
- if (typeof prop !== "string") return void 0;
64
- const name = prop;
65
- return {
66
- insert: (data) => ops().insert(name, data),
67
- update: (id, data) => ops().update(name, id, data),
68
- delete: (id) => ops().delete(name, id),
69
- findById: (id) => ops().findById(name, id),
70
- findMany: (query) => ops().findMany(name, query)
71
- };
72
- }
73
- }
74
- );
75
- return tablesProxy;
76
- }
77
- var rawDatabase = makeServiceProxy("Database");
78
- function makeTypedSurface(raw2) {
79
- const ops = {
80
- query: (sql, params) => raw2.query(sql, params),
81
- insert: (table, data) => raw2.insert(table, data),
82
- update: (table, id, data) => raw2.update(table, id, data),
83
- delete: (table, id) => raw2.delete(table, id),
84
- findById: (table, id) => raw2.findById(table, id),
85
- findMany: (table, query) => raw2.findMany(table, query)
86
- };
87
- return Object.assign(ops, {
88
- tables: makeTablesAccessor(() => raw2),
89
- transaction(fn) {
90
- const builder = new TxPlanBuilder();
91
- return runTxPlan(raw2, makeTxTablesAccessor(builder), builder, fn);
92
- }
93
- });
94
- }
95
- function makeTxTablesAccessor(builder) {
96
- const tablesProxy = new Proxy(
97
- {},
98
- {
99
- get(_t, prop) {
100
- if (typeof prop !== "string") return void 0;
101
- return builder.table(prop);
102
- }
103
- }
104
- );
105
- return tablesProxy;
106
- }
107
- var Database = Object.assign(makeTypedSurface(rawDatabase), {
108
- /**
109
- * Lazily resolve the runtime's service-role sibling on each call. We do NOT
110
- * cache it: `rawDatabase.asService()` reads the CURRENT request scope through
111
- * the runtime proxy, and the per-request runtime injects a service client
112
- * bound to that request's identity headers — caching would leak one request's
113
- * sibling into another concurrent request.
114
- */
115
- asService() {
116
- return makeTypedSurface(rawDatabase.asService());
117
- }
118
- });
119
- var Documents = makeServiceProxy("Documents");
120
- var Storage = makeServiceProxy("Storage");
121
- var Cache = makeServiceProxy("Cache");
122
- var Log = makeServiceProxy("Log");
123
- var Notifications = makeServiceProxy("Notifications");
124
- var Purchases = makeServiceProxy("Purchases");
125
- var rawFlags = makeServiceProxy("Flags");
126
- var Flags = Object.assign(
127
- {
128
- isEnabled(flagName, context) {
129
- return rawFlags.isEnabled(flagName, context);
130
- },
131
- getVariant(flagName, context) {
132
- return rawFlags.getVariant(flagName, context);
133
- },
134
- getAll(context) {
135
- return rawFlags.getAll(context);
136
- },
137
- setOverride(key, value) {
138
- return rawFlags.setOverride(key, value);
139
- }
140
- },
141
- {
142
- /**
143
- * Lazily resolve the runtime's cross-user sibling on each call. We do NOT
144
- * cache it: `rawFlags.asService()` reads the CURRENT request scope through
145
- * the runtime proxy, so caching would leak one request's sibling into
146
- * another concurrent request. Mirrors `Database.asService()`.
147
- */
148
- asService() {
149
- return rawFlags.asService();
150
- }
151
- }
152
- );
153
- var Realtime = makeServiceProxy("Realtime");
154
-
155
- // src/errors.ts
156
- var HttpError = class extends Error {
157
- status;
158
- error;
159
- errorDescription;
160
- data;
161
- constructor(status, error, errorDescription, data) {
162
- super(errorDescription);
163
- this.name = "HttpError";
164
- this.status = status;
165
- this.error = error;
166
- this.errorDescription = errorDescription;
167
- if (data !== void 0) {
168
- this.data = data;
169
- }
170
- }
171
- /**
172
- * Serialize to the standard Palbase error response format.
173
- * The `requestId` is injected by the runtime layer from the request context.
174
- * When called without arguments (e.g. JSON.stringify), request_id is omitted.
175
- * When `data` is set, it is appended as a strict-superset field.
176
- */
177
- toJSON(requestId) {
178
- const result = {
179
- error: this.error,
180
- error_description: this.errorDescription,
181
- status: this.status
182
- };
183
- if (requestId) {
184
- result.request_id = requestId;
185
- }
186
- if (this.data !== void 0) {
187
- result.data = this.data;
188
- }
189
- return result;
190
- }
191
- };
192
- var PalError = class extends HttpError {
193
- constructor(status, code, description, data) {
194
- super(status, code, description, data);
195
- this.name = "PalError";
196
- }
197
- };
198
- var NamedHttpError = class extends HttpError {
199
- constructor(status, defaultCode, name, message, code, data) {
200
- super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
201
- this.name = name;
202
- }
203
- };
204
- function defaultMessage(name) {
205
- const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
206
- return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
207
- }
208
- var BadRequest = class extends NamedHttpError {
209
- constructor(data, message) {
210
- super(400, "bad_request", "BadRequest", message, void 0, data);
211
- }
212
- };
213
- var Unauthorized = class extends NamedHttpError {
214
- constructor(message, code, data) {
215
- super(401, "unauthorized", "Unauthorized", message, code, data);
216
- }
217
- };
218
- var Forbidden = class extends NamedHttpError {
219
- constructor(message, code, data) {
220
- super(403, "forbidden", "Forbidden", message, code, data);
221
- }
222
- };
223
- var NotFound = class extends NamedHttpError {
224
- constructor(message, code, data) {
225
- super(404, "not_found", "NotFound", message, code, data);
226
- }
227
- };
228
- var Conflict = class extends NamedHttpError {
229
- constructor(message, code, data) {
230
- super(409, "conflict", "Conflict", message, code, data);
231
- }
232
- };
233
- var TooManyRequests = class extends NamedHttpError {
234
- constructor(data, message) {
235
- super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
236
- }
237
- };
19
+ } from "./chunk-SSGAMC26.js";
20
+ import {
21
+ defineError,
22
+ entitlementFor,
23
+ getErrorRegistry,
24
+ recordEntitlement,
25
+ recordSpend,
26
+ spendFor
27
+ } from "./chunk-N32VDWKH.js";
28
+ import {
29
+ Cache,
30
+ Database,
31
+ Documents,
32
+ Flags,
33
+ Log,
34
+ Notifications,
35
+ Purchases,
36
+ Realtime,
37
+ Secrets,
38
+ Storage,
39
+ __getRuntime,
40
+ __requestALS,
41
+ __runWithRuntime,
42
+ __setRuntime
43
+ } from "./chunk-POYAFBLF.js";
44
+ import {
45
+ TxPlanError,
46
+ TxRefError,
47
+ dec,
48
+ inc,
49
+ now
50
+ } from "./chunk-W5ODXPY3.js";
51
+ import {
52
+ Controller,
53
+ __resetRegisteredControllers,
54
+ getRegisteredControllers
55
+ } from "./chunk-YL4C5NRY.js";
56
+ import {
57
+ BadRequest,
58
+ Conflict,
59
+ Forbidden,
60
+ HttpError,
61
+ NotFound,
62
+ PalError,
63
+ TooManyRequests,
64
+ Unauthorized,
65
+ getRoutes,
66
+ recordParam,
67
+ recordRoute,
68
+ recordThrows
69
+ } from "./chunk-QMVK4X3V.js";
70
+ import "./chunk-7D4SUZUM.js";
238
71
 
239
72
  // src/purchases/errors.ts
240
73
  function asPalstoreError(err) {
@@ -262,31 +95,6 @@ function toHttpError(err) {
262
95
  }
263
96
  }
264
97
 
265
- // src/purchases/registry.ts
266
- var ENTITLEMENTS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.entitlements");
267
- var SPENDS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.spends");
268
- function carrierOf(target) {
269
- return typeof target === "function" ? target : target.constructor ?? target;
270
- }
271
- function own(carrier, slot) {
272
- if (!Object.prototype.hasOwnProperty.call(carrier, slot)) {
273
- carrier[slot] = {};
274
- }
275
- return carrier[slot];
276
- }
277
- function recordEntitlement(target, fnName, key) {
278
- own(carrierOf(target), ENTITLEMENTS)[fnName] = key;
279
- }
280
- function recordSpend(target, fnName, meta) {
281
- own(carrierOf(target), SPENDS)[fnName] = meta;
282
- }
283
- function entitlementFor(ctor, fnName) {
284
- return carrierOf(ctor)[ENTITLEMENTS]?.[fnName];
285
- }
286
- function spendFor(ctor, fnName) {
287
- return carrierOf(ctor)[SPENDS]?.[fnName];
288
- }
289
-
290
98
  // src/purchases/subject.ts
291
99
  var subjectByRequest = /* @__PURE__ */ new WeakMap();
292
100
  function storeEnv() {
@@ -393,6 +201,795 @@ function Spend(key) {
393
201
  };
394
202
  }
395
203
 
204
+ // src/clients/http.ts
205
+ var PalbaseModuleError = class extends Error {
206
+ code;
207
+ status;
208
+ details;
209
+ constructor(code, message, status, details = {}) {
210
+ super(message);
211
+ this.name = "PalbaseModuleError";
212
+ this.code = code;
213
+ this.status = status;
214
+ this.details = details;
215
+ }
216
+ };
217
+ function makeHttpClient(cfg) {
218
+ async function request(method, path, options = {}) {
219
+ const headers = {
220
+ "Content-Type": "application/json",
221
+ ...options.headers ?? {}
222
+ };
223
+ if (cfg.apiKey) headers["apikey"] = cfg.apiKey;
224
+ const init = { method, headers };
225
+ if (options.body !== void 0) {
226
+ if (typeof options.body === "string" || options.body instanceof Uint8Array) {
227
+ init.body = options.body;
228
+ } else if (typeof FormData !== "undefined" && options.body instanceof FormData) {
229
+ delete headers["Content-Type"];
230
+ init.body = options.body;
231
+ } else if (typeof Blob !== "undefined" && options.body instanceof Blob) {
232
+ delete headers["Content-Type"];
233
+ init.body = options.body;
234
+ } else {
235
+ init.body = JSON.stringify(options.body);
236
+ }
237
+ }
238
+ if (options.signal) init.signal = options.signal;
239
+ const doFetch = cfg.fetchImpl ?? globalThis.fetch;
240
+ let response;
241
+ try {
242
+ response = await doFetch(`${cfg.baseUrl}${path}`, init);
243
+ } catch (err) {
244
+ return {
245
+ data: null,
246
+ error: new PalbaseModuleError(
247
+ "network_error",
248
+ err instanceof Error ? err.message : "Network request failed",
249
+ 0
250
+ ),
251
+ status: 0
252
+ };
253
+ }
254
+ const contentType = response.headers.get("Content-Type") ?? "";
255
+ let parsed = null;
256
+ if (method !== "HEAD" && contentType.includes("json")) {
257
+ parsed = await response.json().catch(() => null);
258
+ } else if (method !== "HEAD" && contentType.startsWith("image/")) {
259
+ parsed = new Uint8Array(await response.arrayBuffer());
260
+ } else if (method !== "HEAD" && response.body) {
261
+ parsed = await response.text().catch(() => null);
262
+ }
263
+ if (!response.ok) {
264
+ const body = parsed && typeof parsed === "object" ? parsed : {};
265
+ const code = typeof body.error === "string" && body.error ? body.error : "unknown_error";
266
+ const message = typeof body.error_description === "string" && body.error_description || response.statusText || "request failed";
267
+ return {
268
+ data: null,
269
+ error: new PalbaseModuleError(code, message, response.status, body),
270
+ status: response.status
271
+ };
272
+ }
273
+ return { data: parsed, error: null, status: response.status };
274
+ }
275
+ return { baseUrl: cfg.baseUrl, request };
276
+ }
277
+
278
+ // src/clients/documents.ts
279
+ var SEGMENT_RE = /^[A-Za-z0-9_-]+$/;
280
+ function validateSegment(segment, label) {
281
+ if (!SEGMENT_RE.test(segment)) {
282
+ throw new Error(`Invalid ${label}: "${segment}". Must match ${SEGMENT_RE.source}`);
283
+ }
284
+ }
285
+ function buildDocumentRef(http, path) {
286
+ return {
287
+ path,
288
+ async set(data) {
289
+ return http.request("PUT", `/v1/docs/${path}`, { body: data });
290
+ },
291
+ async get() {
292
+ const response = await http.request("GET", `/v1/docs/${path}`);
293
+ if (response.error) return { data: null, error: response.error, status: response.status };
294
+ const raw2 = response.data ?? {};
295
+ const exists = raw2.exists !== void 0 ? raw2.exists : Boolean(raw2.data || raw2.id);
296
+ const segments = path.split("/");
297
+ const id = raw2.id || segments[segments.length - 1] || "";
298
+ return {
299
+ data: { id, exists, data: () => raw2.data, ref: { path } },
300
+ error: null,
301
+ status: response.status
302
+ };
303
+ },
304
+ async update(data) {
305
+ return http.request("PATCH", `/v1/docs/${path}`, { body: data });
306
+ },
307
+ async delete() {
308
+ return http.request("DELETE", `/v1/docs/${path}`);
309
+ },
310
+ collection(name) {
311
+ validateSegment(name, "subcollection name");
312
+ return buildCollectionRef(http, `${path}/${name}`);
313
+ }
314
+ };
315
+ }
316
+ function buildCollectionRef(http, path, state = { where: [], orderBy: [] }) {
317
+ function snapshot(doc) {
318
+ return {
319
+ id: doc.id,
320
+ exists: true,
321
+ data: () => doc.data,
322
+ ref: { path: `${path}/${doc.id}` }
323
+ };
324
+ }
325
+ function querySnapshot(docs) {
326
+ return {
327
+ docs,
328
+ empty: docs.length === 0,
329
+ size: docs.length,
330
+ docChanges: () => docs.map((doc) => ({ type: "added", doc }))
331
+ };
332
+ }
333
+ return {
334
+ path,
335
+ doc(id) {
336
+ validateSegment(id, "document ID");
337
+ return buildDocumentRef(http, `${path}/${id}`);
338
+ },
339
+ async add(data) {
340
+ const resp = await http.request("POST", `/v1/docs/${path}`, { body: data });
341
+ if (resp.error || !resp.data) {
342
+ return { data: null, error: resp.error, status: resp.status };
343
+ }
344
+ return {
345
+ data: buildDocumentRef(http, `${path}/${resp.data.id}`),
346
+ error: null,
347
+ status: resp.status
348
+ };
349
+ },
350
+ // Each narrowing returns a NEW ref. A builder that mutated in place would
351
+ // leak one caller's filter into a query another caller had already held.
352
+ where(field, op, value) {
353
+ return buildCollectionRef(http, path, {
354
+ ...state,
355
+ where: [...state.where, { field, op, value }]
356
+ });
357
+ },
358
+ orderBy(field, direction = "asc") {
359
+ return buildCollectionRef(http, path, {
360
+ ...state,
361
+ orderBy: [...state.orderBy, { field, direction }]
362
+ });
363
+ },
364
+ limit(n) {
365
+ return buildCollectionRef(http, path, { ...state, limit: n });
366
+ },
367
+ async get() {
368
+ const narrowed = state.where.length > 0 || state.orderBy.length > 0 || state.limit !== void 0;
369
+ const resp = narrowed ? await http.request(
370
+ "POST",
371
+ `/v1/docs/${path}/query`,
372
+ { body: queryBody(state) }
373
+ ) : await http.request(
374
+ "GET",
375
+ `/v1/docs/${path}`
376
+ );
377
+ if (resp.error) return { data: null, error: resp.error, status: resp.status };
378
+ const docs = (resp.data?.documents ?? []).map(snapshot);
379
+ return { data: querySnapshot(docs), error: null, status: resp.status };
380
+ }
381
+ };
382
+ }
383
+ function queryBody(state) {
384
+ const body = {};
385
+ if (state.where.length > 0) {
386
+ body.where = state.where.map((w) => ({ field: w.field, op: w.op, value: w.value }));
387
+ }
388
+ if (state.orderBy.length > 0) {
389
+ body.orderBy = state.orderBy.map((o) => ({ field: o.field, direction: o.direction }));
390
+ }
391
+ if (state.limit !== void 0) body.limit = state.limit;
392
+ return body;
393
+ }
394
+ var MAX_BATCH = 500;
395
+ function buildDocumentsClient(http) {
396
+ return {
397
+ /**
398
+ * `Documents.doc("users/alice")` — segments are collection/documentId PAIRS,
399
+ * so an odd count addresses a COLLECTION and is refused. Accepting it would
400
+ * write a document whose id happens to be a collection's name.
401
+ */
402
+ doc(path) {
403
+ const segments = String(path).split("/").filter((s) => s.length > 0);
404
+ if (segments.length === 0 || segments.length % 2 !== 0) {
405
+ throw new Error(
406
+ `Invalid document path: "${path}". Expected collection/documentId pairs, got ${segments.length} segment(s).`
407
+ );
408
+ }
409
+ segments.forEach((s, i) => validateSegment(s, i % 2 === 0 ? "collection name" : "document ID"));
410
+ return buildDocumentRef(http, segments.join("/"));
411
+ },
412
+ collection(name) {
413
+ validateSegment(name, "collection name");
414
+ return buildCollectionRef(http, name);
415
+ },
416
+ async batch(operations) {
417
+ if (operations.length > MAX_BATCH) {
418
+ return {
419
+ data: null,
420
+ error: new PalbaseModuleError(
421
+ "batch_too_large",
422
+ `Batch size ${operations.length} exceeds maximum of ${MAX_BATCH}`,
423
+ 400
424
+ ),
425
+ status: 400
426
+ };
427
+ }
428
+ if (operations.length === 0) return { data: null, error: null, status: 200 };
429
+ return http.request("POST", "/v1/docs/batch", {
430
+ body: operations.map((op) => ({ op: op.op, path: op.ref.path, data: op.data }))
431
+ });
432
+ }
433
+ };
434
+ }
435
+
436
+ // src/clients/flags.ts
437
+ var FLAG_NAME_RE = /^[A-Za-z0-9_.-]+$/;
438
+ function assertFlagName(flagName) {
439
+ if (!FLAG_NAME_RE.test(flagName)) {
440
+ throw new Error(`Invalid flag name: "${flagName}". Flag names must match ${FLAG_NAME_RE.source}`);
441
+ }
442
+ }
443
+ function flagEnabled(value) {
444
+ if (typeof value === "boolean") return value;
445
+ return value != null && value !== 0 && value !== "";
446
+ }
447
+ function flagVariant(value) {
448
+ return typeof value === "string" ? { name: value } : null;
449
+ }
450
+ function isContextObject(v) {
451
+ return v !== null && typeof v === "object" && !Array.isArray(v) && ("userId" in v || "properties" in v);
452
+ }
453
+ function buildFlagsClient(http, cfg) {
454
+ function resolveUserId(context) {
455
+ if (context && "userId" in context) return context.userId ?? null;
456
+ return cfg.getCurrentUserId() || void 0;
457
+ }
458
+ function mergedPath(context) {
459
+ const uid = resolveUserId(context);
460
+ return uid ? `/v1/user-flags/users/${encodeURIComponent(uid)}` : "/v1/user-flags";
461
+ }
462
+ function fetchMerged(context) {
463
+ return http.request("GET", mergedPath(context));
464
+ }
465
+ const service = {
466
+ async setOverrideForUser(userId, key, value) {
467
+ return http.request(
468
+ "PUT",
469
+ `/v1/user-flags/users/${encodeURIComponent(userId)}/${encodeURIComponent(key)}`,
470
+ { body: { value } }
471
+ );
472
+ },
473
+ async setOverridesForUser(userId, values) {
474
+ return http.request("PUT", `/v1/user-flags/users/${encodeURIComponent(userId)}`, {
475
+ body: { values }
476
+ });
477
+ },
478
+ async clearOverrideForUser(userId, key) {
479
+ return http.request(
480
+ "DELETE",
481
+ `/v1/user-flags/users/${encodeURIComponent(userId)}/${encodeURIComponent(key)}`
482
+ );
483
+ },
484
+ async clearAllOverridesForUser(userId) {
485
+ return http.request("DELETE", `/v1/user-flags/users/${encodeURIComponent(userId)}`);
486
+ },
487
+ async batchSetOverrides(operations) {
488
+ const ops = (operations ?? []).map((op) => {
489
+ const raw2 = op;
490
+ return { user_id: raw2.userId ?? raw2.user_id, values: raw2.values };
491
+ });
492
+ return http.request("POST", "/v1/user-flags/batch", { body: { operations: ops } });
493
+ }
494
+ };
495
+ return {
496
+ async isEnabled(flagName, context) {
497
+ assertFlagName(flagName);
498
+ const res = await fetchMerged(context);
499
+ if (res.error || res.data == null) return { data: null, error: res.error, status: res.status };
500
+ return { data: flagEnabled(res.data.values?.[flagName]), error: null, status: res.status };
501
+ },
502
+ async getVariant(flagName, context) {
503
+ assertFlagName(flagName);
504
+ const res = await fetchMerged(context);
505
+ if (res.error || res.data == null) return { data: null, error: res.error, status: res.status };
506
+ return { data: flagVariant(res.data.values?.[flagName]), error: null, status: res.status };
507
+ },
508
+ /**
509
+ * `get(key)` · `get(key, default)` · `get(key, default, ctx)` · `get(key, ctx)`.
510
+ *
511
+ * The default is substituted when the flag is ABSENT or when the flags
512
+ * service is unreachable — a product that hides a feature because a lookup
513
+ * timed out is behaving correctly; one that crashes is not.
514
+ */
515
+ async get(flagName, defaultOrContext, maybeContext) {
516
+ assertFlagName(flagName);
517
+ let defaultValue;
518
+ let context;
519
+ let hasDefault = false;
520
+ if (maybeContext !== void 0) {
521
+ defaultValue = defaultOrContext;
522
+ hasDefault = true;
523
+ context = maybeContext;
524
+ } else if (isContextObject(defaultOrContext)) {
525
+ context = defaultOrContext;
526
+ } else if (defaultOrContext !== void 0) {
527
+ defaultValue = defaultOrContext;
528
+ hasDefault = true;
529
+ }
530
+ const resp = await fetchMerged(context);
531
+ if (resp.error !== null) {
532
+ if (hasDefault) return { data: defaultValue ?? null, error: null, status: resp.status };
533
+ return { data: null, error: resp.error, status: resp.status };
534
+ }
535
+ const values = resp.data?.values ?? {};
536
+ const value = values[flagName];
537
+ if (value === void 0) {
538
+ return { data: hasDefault ? defaultValue ?? null : null, error: null, status: resp.status };
539
+ }
540
+ return { data: value, error: null, status: resp.status };
541
+ },
542
+ async getAll(context) {
543
+ const res = await fetchMerged(context);
544
+ if (res.error || res.data == null) return { data: null, error: res.error, status: res.status };
545
+ const values = res.data.values ?? {};
546
+ return {
547
+ data: Object.keys(values).map((name) => {
548
+ const value = values[name];
549
+ const flag2 = {
550
+ name,
551
+ enabled: flagEnabled(value)
552
+ };
553
+ if (typeof value === "string") flag2.variant = { name: value };
554
+ return flag2;
555
+ }),
556
+ error: null,
557
+ status: res.status
558
+ };
559
+ },
560
+ /**
561
+ * Override a flag for the CURRENT request user.
562
+ *
563
+ * On an anonymous request this does not silently no-op and does not write
564
+ * for nobody: it refuses and names the call that does cross-user writes.
565
+ */
566
+ async setOverride(key, value) {
567
+ const uid = cfg.getCurrentUserId();
568
+ if (!uid) {
569
+ return {
570
+ data: null,
571
+ error: {
572
+ message: "setOverride requires a signed-in user; use Flags.asService().setOverrideForUser(userId, key, value) for cross-user writes"
573
+ },
574
+ status: 400
575
+ };
576
+ }
577
+ return http.request(
578
+ "PUT",
579
+ `/v1/user-flags/users/${encodeURIComponent(uid)}/${encodeURIComponent(key)}`,
580
+ { body: { value } }
581
+ );
582
+ },
583
+ asService() {
584
+ return service;
585
+ }
586
+ };
587
+ }
588
+
589
+ // src/clients/notifications.ts
590
+ function mapEnvelope(res, map) {
591
+ if (res.error !== null || res.data === null || res.data === void 0) {
592
+ return { data: null, error: res.error, status: res.status };
593
+ }
594
+ return { data: map(res.data), error: null, status: res.status };
595
+ }
596
+ function toEmailTemplate(wire) {
597
+ const view = {
598
+ id: wire.id,
599
+ slug: wire.slug,
600
+ locale: wire.locale,
601
+ subject: wire.subject,
602
+ htmlBody: wire.html_body,
603
+ variables: wire.variables ?? [],
604
+ isDefault: wire.is_default,
605
+ createdAt: wire.created_at,
606
+ updatedAt: wire.updated_at
607
+ };
608
+ if (wire.text_body !== void 0) view.textBody = wire.text_body;
609
+ return view;
610
+ }
611
+ function toSmsTemplate(wire) {
612
+ return {
613
+ id: wire.id,
614
+ slug: wire.slug,
615
+ locale: wire.locale,
616
+ body: wire.body,
617
+ variables: wire.variables ?? [],
618
+ isDefault: wire.is_default,
619
+ createdAt: wire.created_at,
620
+ updatedAt: wire.updated_at
621
+ };
622
+ }
623
+ function buildNotificationsClient(http) {
624
+ const push = {
625
+ async send(params) {
626
+ return http.request("POST", "/v1/notifications/push", { body: params });
627
+ }
628
+ };
629
+ const email = {
630
+ async send(params) {
631
+ const { templateSlug, html, text: text2, ...rest } = params;
632
+ const body = { ...rest };
633
+ if (templateSlug !== void 0) body.template_slug = templateSlug;
634
+ if (html !== void 0) body.html_body = html;
635
+ if (text2 !== void 0) body.text_body = text2;
636
+ return http.request("POST", "/v1/notifications/email", { body });
637
+ }
638
+ };
639
+ const sms = {
640
+ async send(params) {
641
+ const { templateSlug, ...rest } = params;
642
+ const body = templateSlug !== void 0 ? { ...rest, template_slug: templateSlug } : rest;
643
+ return http.request("POST", "/v1/notifications/sms", { body });
644
+ }
645
+ };
646
+ const verifications = {
647
+ async start(params) {
648
+ return http.request("POST", "/v1/notifications/verifications", { body: params });
649
+ },
650
+ async check(params) {
651
+ return http.request("POST", "/v1/notifications/verifications/check", { body: params });
652
+ }
653
+ };
654
+ const inbox = {
655
+ async send(params) {
656
+ return http.request("POST", "/v1/notifications/inbox", { body: params });
657
+ },
658
+ async list(options) {
659
+ const opts = options ?? {};
660
+ const params = new URLSearchParams();
661
+ if (opts.cursor) params.set("cursor", opts.cursor);
662
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
663
+ if (opts.is_read !== void 0) params.set("is_read", opts.is_read ? "true" : "false");
664
+ if (opts.category) params.set("category", opts.category);
665
+ if (opts.include_archived) params.set("include_archived", "true");
666
+ const query = params.toString();
667
+ return http.request("GET", `/v1/notifications/inbox${query ? `?${query}` : ""}`);
668
+ },
669
+ async unreadCount() {
670
+ return http.request("GET", "/v1/notifications/inbox/unread-count");
671
+ },
672
+ async markRead(id) {
673
+ return http.request("PATCH", `/v1/notifications/inbox/${encodeURIComponent(id)}/read`);
674
+ },
675
+ async markAllRead() {
676
+ return http.request("POST", "/v1/notifications/inbox/read-all");
677
+ },
678
+ async archive(id) {
679
+ return http.request("DELETE", `/v1/notifications/inbox/${encodeURIComponent(id)}`);
680
+ }
681
+ };
682
+ const preferences = {
683
+ async get() {
684
+ return http.request("GET", "/v1/notifications/preferences");
685
+ },
686
+ async update(params) {
687
+ return http.request("PUT", "/v1/notifications/preferences", { body: params });
688
+ }
689
+ };
690
+ const emailTemplates = {
691
+ async list() {
692
+ const resp = await http.request("GET", "/v1/notifications/templates");
693
+ return mapEnvelope(resp, (rows) => (rows ?? []).map(toEmailTemplate));
694
+ },
695
+ async get(id) {
696
+ const resp = await http.request(
697
+ "GET",
698
+ `/v1/notifications/templates/${encodeURIComponent(id)}`
699
+ );
700
+ return mapEnvelope(resp, toEmailTemplate);
701
+ },
702
+ async create(input) {
703
+ const body = {
704
+ slug: input.slug,
705
+ subject: input.subject,
706
+ html_body: input.htmlBody
707
+ };
708
+ if (input.textBody !== void 0) body.text_body = input.textBody;
709
+ if (input.variables !== void 0) body.variables = input.variables;
710
+ const resp = await http.request("POST", "/v1/notifications/templates", { body });
711
+ return mapEnvelope(resp, toEmailTemplate);
712
+ },
713
+ async update(id, input) {
714
+ const body = {};
715
+ if (input.subject !== void 0) body.subject = input.subject;
716
+ if (input.htmlBody !== void 0) body.html_body = input.htmlBody;
717
+ if (input.textBody !== void 0) body.text_body = input.textBody;
718
+ if (input.variables !== void 0) body.variables = input.variables;
719
+ const resp = await http.request(
720
+ "PUT",
721
+ `/v1/notifications/templates/${encodeURIComponent(id)}`,
722
+ { body }
723
+ );
724
+ return mapEnvelope(resp, toEmailTemplate);
725
+ },
726
+ async delete(id) {
727
+ return http.request("DELETE", `/v1/notifications/templates/${encodeURIComponent(id)}`);
728
+ }
729
+ };
730
+ const smsTemplates = {
731
+ async list() {
732
+ const resp = await http.request("GET", "/v1/notifications/sms-templates");
733
+ return mapEnvelope(resp, (rows) => (rows ?? []).map(toSmsTemplate));
734
+ },
735
+ async get(id) {
736
+ const resp = await http.request(
737
+ "GET",
738
+ `/v1/notifications/sms-templates/${encodeURIComponent(id)}`
739
+ );
740
+ return mapEnvelope(resp, toSmsTemplate);
741
+ },
742
+ async create(input) {
743
+ const body = { slug: input.slug, body: input.body };
744
+ if (input.variables !== void 0) body.variables = input.variables;
745
+ const resp = await http.request("POST", "/v1/notifications/sms-templates", { body });
746
+ return mapEnvelope(resp, toSmsTemplate);
747
+ },
748
+ async update(id, input) {
749
+ const body = {};
750
+ if (input.body !== void 0) body.body = input.body;
751
+ if (input.variables !== void 0) body.variables = input.variables;
752
+ const resp = await http.request(
753
+ "PUT",
754
+ `/v1/notifications/sms-templates/${encodeURIComponent(id)}`,
755
+ { body }
756
+ );
757
+ return mapEnvelope(resp, toSmsTemplate);
758
+ },
759
+ async delete(id) {
760
+ return http.request("DELETE", `/v1/notifications/sms-templates/${encodeURIComponent(id)}`);
761
+ }
762
+ };
763
+ return {
764
+ push,
765
+ email,
766
+ sms,
767
+ verifications,
768
+ inbox,
769
+ preferences,
770
+ templates: { email: emailTemplates, sms: smsTemplates },
771
+ async registerDevice(params) {
772
+ return http.request("POST", "/v1/notifications/devices", { body: params });
773
+ },
774
+ async unregisterDevice(deviceId) {
775
+ return http.request("DELETE", `/v1/notifications/devices/${encodeURIComponent(deviceId)}`);
776
+ }
777
+ };
778
+ }
779
+
780
+ // src/clients/realtime.ts
781
+ import { createHmac } from "crypto";
782
+ function mintToken(secret2) {
783
+ const now2 = Math.floor(Date.now() / 1e3);
784
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
785
+ const payload = Buffer.from(
786
+ JSON.stringify({ iss: "backend-runtime", role: "service_role", iat: now2, exp: now2 + 60 })
787
+ ).toString("base64url");
788
+ const sig = createHmac("sha256", secret2).update(`${header}.${payload}`).digest("base64url");
789
+ return `${header}.${payload}.${sig}`;
790
+ }
791
+ function buildRealtimeClient(cfg) {
792
+ const url = `${cfg.baseUrl}/realtime/api/broadcast`;
793
+ return {
794
+ async broadcast(channel, event, payload) {
795
+ if (typeof channel !== "string" || channel.length === 0) {
796
+ return {
797
+ data: null,
798
+ error: new PalbaseModuleError("invalid_argument", "channel must be a non-empty string", 400)
799
+ };
800
+ }
801
+ if (typeof event !== "string" || event.length === 0) {
802
+ return {
803
+ data: null,
804
+ error: new PalbaseModuleError("invalid_argument", "event must be a non-empty string", 400)
805
+ };
806
+ }
807
+ if (!cfg.apiJwtSecret) {
808
+ return {
809
+ data: null,
810
+ error: new PalbaseModuleError(
811
+ "realtime_unconfigured",
812
+ "Realtime.broadcast is unavailable: realtime is not provisioned for this Environment (PALBASE_REALTIME_API_JWT_SECRET unset).",
813
+ 503
814
+ )
815
+ };
816
+ }
817
+ let token;
818
+ try {
819
+ token = mintToken(cfg.apiJwtSecret);
820
+ } catch (err) {
821
+ return {
822
+ data: null,
823
+ error: new PalbaseModuleError(
824
+ "realtime_token_error",
825
+ err instanceof Error ? err.message : "token mint failed",
826
+ 500
827
+ )
828
+ };
829
+ }
830
+ const body = JSON.stringify({
831
+ messages: [{ topic: channel, event, payload: payload ?? {}, private: false }]
832
+ });
833
+ const doFetch = cfg.fetchImpl ?? globalThis.fetch;
834
+ let response;
835
+ try {
836
+ response = await doFetch(url, {
837
+ method: "POST",
838
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
839
+ body
840
+ });
841
+ } catch (err) {
842
+ return {
843
+ data: null,
844
+ error: new PalbaseModuleError(
845
+ "network_error",
846
+ err instanceof Error ? err.message : "broadcast request failed",
847
+ 0
848
+ )
849
+ };
850
+ }
851
+ if (response.status === 202 || response.status === 200) {
852
+ return { data: void 0, error: null };
853
+ }
854
+ const detail = await response.text().catch(() => "");
855
+ return {
856
+ data: null,
857
+ error: new PalbaseModuleError(
858
+ "realtime_broadcast_failed",
859
+ `broadcast returned ${response.status}: ${detail.slice(0, 200)}`,
860
+ response.status
861
+ )
862
+ };
863
+ }
864
+ };
865
+ }
866
+
867
+ // src/clients/storage.ts
868
+ var BUCKET_NAME_RE = /^[a-zA-Z0-9_-]+$/;
869
+ var STORAGE_PATH_RE = /^[a-zA-Z0-9_./-]+$/;
870
+ var STORAGE_TRAVERSAL_RE = /(?:^|\/)\.\.(?:\/|$)/;
871
+ function validateStoragePath(p) {
872
+ if (!STORAGE_PATH_RE.test(p) || STORAGE_TRAVERSAL_RE.test(p)) {
873
+ throw new Error(
874
+ `Invalid file path: "${p}". Paths must not contain traversal sequences and must match ${STORAGE_PATH_RE.source}`
875
+ );
876
+ }
877
+ }
878
+ function toFileObject(bucket2, row) {
879
+ return {
880
+ name: row.path ?? "",
881
+ path: row.path ?? "",
882
+ bucket: row.bucket ?? bucket2,
883
+ size: typeof row.size === "number" ? row.size : 0,
884
+ contentType: row.contentType ?? "",
885
+ checksum: row.checksum ?? "",
886
+ width: row.width,
887
+ height: row.height,
888
+ thumbhash: row.thumbhash,
889
+ variants: row.variants ?? {}
890
+ };
891
+ }
892
+ function mapResponse(res, map) {
893
+ if (res.error || res.data === null || res.data === void 0) {
894
+ return { data: null, error: res.error, status: res.status };
895
+ }
896
+ return { data: map(res.data), error: null, status: res.status };
897
+ }
898
+ function buildBucketClient(http, bucketName) {
899
+ const objectPath = (path) => `/v1/storage/object/${bucketName}/${path}`;
900
+ return {
901
+ async upload(path, file, options) {
902
+ validateStoragePath(path);
903
+ const headers = {
904
+ "Content-Type": options?.contentType ?? "application/octet-stream"
905
+ };
906
+ if (options?.upsert) headers["x-upsert"] = "true";
907
+ const body = file instanceof ArrayBuffer ? new Uint8Array(file) : file;
908
+ const res = await http.request("PUT", objectPath(path), {
909
+ body,
910
+ headers
911
+ });
912
+ return mapResponse(res, (r) => toFileObject(bucketName, r ?? {}));
913
+ },
914
+ async download(path) {
915
+ validateStoragePath(path);
916
+ return http.request("GET", objectPath(path), { headers: { Accept: "*/*" } });
917
+ },
918
+ getPublicUrl(path, options) {
919
+ validateStoragePath(path);
920
+ const variant = options?.variant ? `?variant=${encodeURIComponent(options.variant)}` : "";
921
+ return `/v1/files/${bucketName}/${path}${variant}`;
922
+ },
923
+ async createSignedUrl(path, options) {
924
+ validateStoragePath(path);
925
+ return http.request("POST", `/v1/storage/sign/${bucketName}/${path}`, {
926
+ body: { expiresIn: options.expiresIn }
927
+ });
928
+ },
929
+ async list(prefix, options) {
930
+ if (prefix) validateStoragePath(prefix);
931
+ const params = new URLSearchParams();
932
+ if (prefix) params.set("prefix", prefix);
933
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
934
+ const query = params.toString();
935
+ const res = await http.request(
936
+ "GET",
937
+ `/v1/storage/bucket/${bucketName}${query ? `?${query}` : ""}`
938
+ );
939
+ return mapResponse(res, (body) => (body?.objects ?? []).map((row) => toFileObject(bucketName, row)));
940
+ },
941
+ async remove(paths) {
942
+ for (const p of paths) validateStoragePath(p);
943
+ const removed = [];
944
+ for (const p of paths) {
945
+ const res = await http.request("DELETE", objectPath(p));
946
+ if (res.error) return { data: null, error: res.error, status: res.status };
947
+ removed.push(toFileObject(bucketName, { path: p }));
948
+ }
949
+ return { data: removed, error: null, status: 200 };
950
+ },
951
+ async move(from, to) {
952
+ validateStoragePath(from);
953
+ validateStoragePath(to);
954
+ return http.request("POST", `/v1/storage/move/${bucketName}`, { body: { from, to } });
955
+ },
956
+ async copy(from, to) {
957
+ validateStoragePath(from);
958
+ validateStoragePath(to);
959
+ return http.request("POST", `/v1/storage/copy/${bucketName}`, { body: { from, to } });
960
+ }
961
+ };
962
+ }
963
+ function buildStorageClient(http) {
964
+ return {
965
+ bucket(name) {
966
+ if (!BUCKET_NAME_RE.test(name)) {
967
+ throw new Error(`Invalid bucket name: "${name}". Bucket names must match ${BUCKET_NAME_RE.source}`);
968
+ }
969
+ return buildBucketClient(http, name);
970
+ }
971
+ };
972
+ }
973
+
974
+ // src/clients/index.ts
975
+ function buildModuleClients(cfg) {
976
+ const baseUrl = (cfg.baseUrl || "").replace(/\/+$/, "");
977
+ if (!baseUrl) return {};
978
+ const apiKey = cfg.serviceRoleKey || cfg.apiKey || "";
979
+ const http = makeHttpClient({ baseUrl, apiKey, fetchImpl: cfg.fetchImpl });
980
+ return {
981
+ Documents: buildDocumentsClient(http),
982
+ Storage: buildStorageClient(http),
983
+ Notifications: buildNotificationsClient(http),
984
+ Flags: buildFlagsClient(http, { getCurrentUserId: cfg.getCurrentUserId }),
985
+ Realtime: buildRealtimeClient({
986
+ baseUrl,
987
+ apiJwtSecret: cfg.realtimeApiJwtSecret,
988
+ fetchImpl: cfg.fetchImpl
989
+ })
990
+ };
991
+ }
992
+
396
993
  // src/purchases/keys-gen.ts
397
994
  function liveKeys(entries) {
398
995
  return Object.entries(entries ?? {}).filter(([, def]) => def?.removed !== true).map(([key]) => key).sort();
@@ -422,6 +1019,89 @@ export {};
422
1019
  `;
423
1020
  }
424
1021
 
1022
+ // src/db/schema-json.ts
1023
+ function defOf(column) {
1024
+ return "_def" in column ? column._def : column;
1025
+ }
1026
+ function columnToJSON(column) {
1027
+ const def = defOf(column);
1028
+ const out = {
1029
+ type: def.type,
1030
+ nullable: def.nullable,
1031
+ primaryKey: def.primaryKey
1032
+ };
1033
+ if (def.defaultValue !== void 0) out.defaultValue = def.defaultValue;
1034
+ if (def.defaultRandom === true) out.defaultRandom = true;
1035
+ if (def.defaultNow === true) out.defaultNow = true;
1036
+ if (def.renamedFrom !== void 0) out.renamedFrom = def.renamedFrom;
1037
+ if (def.references !== void 0) {
1038
+ out.references = { table: def.references.table, column: def.references.column };
1039
+ }
1040
+ if (def.onDeleteAction !== void 0) out.onDeleteAction = def.onDeleteAction;
1041
+ if (def.enumName !== void 0) out.enumName = def.enumName;
1042
+ if (def.enumValues !== void 0) out.enumValues = [...def.enumValues];
1043
+ if (def.unique === true) out.unique = true;
1044
+ return out;
1045
+ }
1046
+ function policyToJSON(policy2) {
1047
+ return {
1048
+ name: policy2.name,
1049
+ command: policy2.command ?? "all",
1050
+ roles: policy2.roles ? [...policy2.roles] : [],
1051
+ // null rather than omitted: a policy with no USING clause is a different
1052
+ // thing from one whose clause the emitter forgot, and Go reads the
1053
+ // difference.
1054
+ using: policy2.using ?? null,
1055
+ withCheck: policy2.withCheck ?? null,
1056
+ permissive: policy2.permissive !== false
1057
+ };
1058
+ }
1059
+ function tableToJSON(table) {
1060
+ const columns = {};
1061
+ for (const [name, column] of Object.entries(table.columns)) {
1062
+ columns[name] = columnToJSON(column);
1063
+ }
1064
+ const out = {
1065
+ name: table.name,
1066
+ columns,
1067
+ // Read, not re-derived. `defineSchema` already resolves the fail-closed
1068
+ // default (RLS on unless the author wrote `rls: false`, and forced on by any
1069
+ // policy), and a second copy of a SECURITY default is exactly the thing that
1070
+ // drifts — the direction it drifted last time was "expose everything", and
1071
+ // the live proof was one user reading another's rows.
1072
+ rls: table.rls,
1073
+ policies: (table.policies ?? []).map(policyToJSON)
1074
+ };
1075
+ if (table.primaryKey !== void 0 && table.primaryKey.length > 0) {
1076
+ out.primaryKey = [...table.primaryKey];
1077
+ }
1078
+ if (table.unique !== void 0 && table.unique.length > 0) {
1079
+ out.uniqueConstraints = table.unique.map((u) => ({ name: u.name, columns: [...u.columns] }));
1080
+ }
1081
+ if (table.raw !== void 0 && table.raw.length > 0) {
1082
+ out.rawConstraints = table.raw.map((r) => ({
1083
+ name: r.name,
1084
+ up: r.up,
1085
+ down: r.down ?? null
1086
+ }));
1087
+ }
1088
+ if (table.checks !== void 0 && table.checks.length > 0) {
1089
+ out.checks = table.checks.map((c) => ({ name: c.name, expr: c.expr }));
1090
+ }
1091
+ if (table.indexes !== void 0 && table.indexes.length > 0) {
1092
+ out.indexes = table.indexes.map((i) => ({ name: i.name, columns: [...i.columns] }));
1093
+ }
1094
+ return out;
1095
+ }
1096
+ function toSchemaJSON(schema) {
1097
+ const tables = {};
1098
+ for (const table of Object.values(schema.tables)) {
1099
+ const json = tableToJSON(table);
1100
+ tables[json.name] = json;
1101
+ }
1102
+ return { tables, extensions: [...new Set(schema.extensions ?? [])] };
1103
+ }
1104
+
425
1105
  // src/db/env-gen.ts
426
1106
  function baseTsType(def) {
427
1107
  switch (def.type) {
@@ -552,6 +1232,7 @@ export {};
552
1232
  // src/config/storage.ts
553
1233
  var STORAGE_CONFIG_KIND = "storage";
554
1234
  var VARIANT_FORMATS = ["webp", "avif", "jpeg", "png"];
1235
+ var VARIANT_FITS = ["cover", "contain", "inside"];
555
1236
  function normalizeVariants(input) {
556
1237
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
557
1238
  throw new Error("bucket variants must be an object of { <name>: { width?, height?, format? } }");
@@ -581,10 +1262,26 @@ function normalizeVariants(input) {
581
1262
  `bucket variant "${name}" format must be one of ${VARIANT_FORMATS.join(", ")}, got "${String(spec.format)}"`
582
1263
  );
583
1264
  }
1265
+ if (spec.fit !== void 0 && !VARIANT_FITS.includes(spec.fit)) {
1266
+ throw new Error(
1267
+ `bucket variant "${name}" fit must be one of ${VARIANT_FITS.join(", ")}, got "${String(spec.fit)}"`
1268
+ );
1269
+ }
1270
+ if (spec.quality !== void 0) {
1271
+ const q = spec.quality;
1272
+ if (typeof q !== "number" || !Number.isInteger(q) || q < 1 || q > 100) {
1273
+ throw new Error(`bucket variant "${name}" quality must be an integer 1\u2013100, got ${String(q)}`);
1274
+ }
1275
+ }
1276
+ if (spec.fit === "cover" && (spec.width === void 0 || spec.height === void 0)) {
1277
+ throw new Error(`bucket variant "${name}" uses fit "cover", which needs BOTH width and height`);
1278
+ }
584
1279
  out[name] = {
585
1280
  ...spec.width !== void 0 ? { width: spec.width } : {},
586
1281
  ...spec.height !== void 0 ? { height: spec.height } : {},
587
- ...spec.format !== void 0 ? { format: spec.format } : {}
1282
+ ...spec.fit !== void 0 ? { fit: spec.fit } : {},
1283
+ ...spec.format !== void 0 ? { format: spec.format } : {},
1284
+ ...spec.quality !== void 0 ? { quality: spec.quality } : {}
588
1285
  };
589
1286
  }
590
1287
  return out;
@@ -1115,138 +1812,55 @@ function defineFlags(input) {
1115
1812
  return { __config: FLAGS_CONFIG_KIND, flags };
1116
1813
  }
1117
1814
 
1118
- // src/decorators/registry.ts
1119
- var ROUTES = /* @__PURE__ */ Symbol.for("palbase.backend.routes");
1120
- var PARAM_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.paramBuffer");
1121
- var RETURN_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.returnBuffer");
1122
- var THROWS_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.throwsBuffer");
1123
- function carrierOf2(target) {
1124
- const ctor = typeof target === "function" ? target : target.constructor ?? target;
1125
- return ctor;
1126
- }
1127
- function ownRoutes(carrier) {
1128
- if (!Object.prototype.hasOwnProperty.call(carrier, ROUTES)) {
1129
- carrier[ROUTES] = [];
1130
- }
1131
- return carrier[ROUTES];
1132
- }
1133
- function ownParamBuffer(carrier) {
1134
- if (!Object.prototype.hasOwnProperty.call(carrier, PARAM_BUFFER)) {
1135
- carrier[PARAM_BUFFER] = {};
1136
- }
1137
- return carrier[PARAM_BUFFER];
1138
- }
1139
- function recordRoute(target, fnName, method, subpath, options) {
1140
- const carrier = carrierOf2(target);
1141
- const routes = ownRoutes(carrier);
1142
- const buffer = ownParamBuffer(carrier);
1143
- const params = (buffer[fnName] ?? []).slice().sort((a, b) => a.index - b.index);
1144
- const route = { method, subpath, fnName, options, params };
1145
- const returnBuffer = carrier[RETURN_BUFFER];
1146
- if (returnBuffer && returnBuffer[fnName] !== void 0) {
1147
- route.returnSchema = returnBuffer[fnName];
1148
- }
1149
- const throwsBuffer = carrier[THROWS_BUFFER];
1150
- if (throwsBuffer && throwsBuffer[fnName] !== void 0) {
1151
- route.throws = throwsBuffer[fnName];
1152
- }
1153
- routes.push(route);
1154
- }
1155
- function recordParam(target, fnName, meta) {
1156
- const carrier = carrierOf2(target);
1157
- const buffer = ownParamBuffer(carrier);
1158
- (buffer[fnName] ??= []).push(meta);
1159
- const routes = carrier[ROUTES];
1160
- if (routes) {
1161
- const route = routes.find((r) => r.fnName === fnName);
1162
- if (route) {
1163
- route.params.push(meta);
1164
- route.params.sort((a, b) => a.index - b.index);
1165
- }
1166
- }
1167
- }
1168
- function recordThrows(target, fnName, throws) {
1169
- const carrier = carrierOf2(target);
1170
- const routes = carrier[ROUTES];
1171
- const route = routes?.find((r) => r.fnName === fnName);
1172
- if (route) {
1173
- route.throws = throws;
1174
- return;
1175
- }
1176
- if (!Object.prototype.hasOwnProperty.call(carrier, THROWS_BUFFER)) {
1177
- carrier[THROWS_BUFFER] = {};
1178
- }
1179
- const throwsBuffer = carrier[THROWS_BUFFER];
1180
- if (throwsBuffer) throwsBuffer[fnName] = throws;
1181
- }
1182
- function getRoutes(ctor) {
1183
- const carrier = carrierOf2(ctor);
1184
- const routes = carrier[ROUTES] ?? [];
1185
- const returnBuffer = carrier[RETURN_BUFFER];
1186
- if (returnBuffer) {
1187
- for (const route of routes) {
1188
- const buffered = returnBuffer[route.fnName];
1189
- if (buffered && route.returnSchema === void 0) {
1190
- route.returnSchema = buffered;
1191
- }
1815
+ // src/config/secrets.ts
1816
+ var SECRETS_CONFIG_KIND = "secrets";
1817
+ var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
1818
+ function secret(name, opts = {}) {
1819
+ if (typeof name !== "string" || !SECRET_NAME_RE.test(name)) {
1820
+ throw new Error(
1821
+ `secret name ${JSON.stringify(name)} is invalid \u2014 must be an environment-variable name: an uppercase letter, then uppercase letters, digits, and underscores (e.g. "NEO4J_PASSWORD")`
1822
+ );
1823
+ }
1824
+ if (opts === null || typeof opts !== "object") {
1825
+ throw new Error(`secret("${name}") expects { required?, description? }`);
1826
+ }
1827
+ let required = true;
1828
+ if (opts.required !== void 0) {
1829
+ if (typeof opts.required !== "boolean") {
1830
+ throw new Error(`secret("${name}") required must be a boolean`);
1192
1831
  }
1832
+ required = opts.required;
1193
1833
  }
1194
- const throwsBuffer = carrier[THROWS_BUFFER];
1195
- if (throwsBuffer) {
1196
- for (const route of routes) {
1197
- const buffered = throwsBuffer[route.fnName];
1198
- if (buffered && route.throws === void 0) {
1199
- route.throws = buffered;
1200
- }
1834
+ let description = null;
1835
+ if (opts.description !== void 0) {
1836
+ if (typeof opts.description !== "string") {
1837
+ throw new Error(`secret("${name}") description must be a string`);
1201
1838
  }
1839
+ const trimmed = opts.description.trim();
1840
+ description = trimmed.length > 0 ? trimmed : null;
1202
1841
  }
1203
- return routes.map((r) => ({
1204
- ...r,
1205
- params: r.params.slice(),
1206
- ...r.throws !== void 0 ? { throws: r.throws.slice() } : {}
1207
- }));
1842
+ return { name, required, description };
1208
1843
  }
1209
-
1210
- // src/decorators/controller.ts
1211
- var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
1212
- var RESERVED_FIRST_SEGMENT = "webhooks";
1213
- function assertNotReserved(path, subject) {
1214
- const [first] = path.split("/").filter(Boolean);
1215
- if (first === RESERVED_FIRST_SEGMENT) {
1216
- throw new Error(
1217
- `${subject} resolves under the reserved /${RESERVED_FIRST_SEGMENT} path \u2014 inbound webhooks are served there and would shadow this route`
1218
- );
1844
+ function defineSecrets(input) {
1845
+ if (input === null || typeof input !== "object" || !Array.isArray(input.secrets)) {
1846
+ throw new Error('defineSecrets expects { secrets: [secret("NAME"), ...] }');
1219
1847
  }
1220
- }
1221
- function Controller(basePath, options = {}) {
1222
- return function(ctor) {
1223
- assertNotReserved(basePath, `@Controller("${basePath}")`);
1224
- for (const route of getRoutes(ctor)) {
1225
- assertNotReserved(
1226
- `${basePath}${route.subpath}`,
1227
- `@${route.method}("${route.subpath}") in @Controller("${basePath}")`
1848
+ const byName = /* @__PURE__ */ new Map();
1849
+ for (const def of input.secrets) {
1850
+ if (def === null || typeof def !== "object" || typeof def.name !== "string" || typeof def.required !== "boolean") {
1851
+ throw new Error(
1852
+ `defineSecrets: every entry must come from secret() \u2014 got ${JSON.stringify(def)}`
1228
1853
  );
1229
1854
  }
1230
- const carrier = ctor;
1231
- const meta = {
1232
- __palbase: "controller",
1233
- basePath,
1234
- ...options.auth !== void 0 ? { defaultAuth: options.auth } : {}
1235
- };
1236
- Object.defineProperty(carrier, CONTROLLER_META, {
1237
- value: meta,
1238
- enumerable: false,
1239
- configurable: true,
1240
- writable: false
1241
- });
1242
- Object.defineProperty(carrier, "__palbase", {
1243
- value: "controller",
1244
- enumerable: false,
1245
- configurable: true,
1246
- writable: false
1247
- });
1248
- return ctor;
1249
- };
1855
+ if (byName.has(def.name)) {
1856
+ throw new Error(
1857
+ `secret "${def.name}" is declared twice in config/secrets.ts \u2014 remove one; a name has exactly one requirement`
1858
+ );
1859
+ }
1860
+ byName.set(def.name, def);
1861
+ }
1862
+ const secrets = [...byName.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1863
+ return { __config: SECRETS_CONFIG_KIND, secrets };
1250
1864
  }
1251
1865
 
1252
1866
  // src/decorators/methods.ts
@@ -1358,149 +1972,15 @@ function defineMiddleware(fn) {
1358
1972
  return fn;
1359
1973
  }
1360
1974
 
1361
- // src/error-registry.ts
1362
- import {
1363
- OpenAPIRegistry,
1364
- OpenApiGeneratorV31,
1365
- extendZodWithOpenApi
1366
- } from "@asteasolutions/zod-to-openapi";
1367
- import { z } from "zod";
1368
- extendZodWithOpenApi(z);
1369
- var ERROR_REGISTRY = /* @__PURE__ */ Symbol.for("palbase.backend.errorRegistry");
1370
- var BUILTIN_DATA_SCHEMAS = {
1371
- bad_request: z.object({
1372
- fields: z.array(z.object({ field: z.string(), message: z.string() }))
1373
- }),
1374
- too_many_requests: z.object({ retryAfter: z.number().int() }),
1375
- // Purchases (@RequireEntitlement / @Spend). Their payloads are what make a
1376
- // paywall renderable on the client: WHICH entitlement was missing, and for a
1377
- // spend the real ceiling and reset time rather than a guessed retry delay.
1378
- entitlement_required: z.object({ entitlement: z.string().nullable() }),
1379
- quota_exceeded: z.object({
1380
- key: z.string(),
1381
- scope: z.string(),
1382
- window: z.string(),
1383
- used: z.number().int(),
1384
- reserved: z.number().int(),
1385
- max: z.number().int(),
1386
- remaining: z.number().int(),
1387
- resetAt: z.string()
1388
- }),
1389
- credit_insufficient: z.object({
1390
- key: z.string(),
1391
- balance: z.number().int(),
1392
- reserved: z.number().int(),
1393
- remaining: z.number().int()
1394
- })
1395
- };
1396
- function getErrorRegistry() {
1397
- const g = globalThis;
1398
- if (!g[ERROR_REGISTRY]) {
1399
- const m = /* @__PURE__ */ new Map();
1400
- for (const [code, status, className] of [
1401
- ["bad_request", 400, "BadRequest"],
1402
- ["unauthorized", 401, "Unauthorized"],
1403
- ["forbidden", 403, "Forbidden"],
1404
- ["not_found", 404, "NotFound"],
1405
- ["conflict", 409, "Conflict"],
1406
- ["too_many_requests", 429, "TooManyRequests"],
1407
- // Thrown by the purchases decorators, so they are pre-seeded like the
1408
- // other built-ins: a project never declares them, but every gated or
1409
- // metered route must surface them as TYPED errors on the client.
1410
- ["entitlement_required", 403, "EntitlementRequired"],
1411
- ["quota_exceeded", 429, "QuotaExceeded"],
1412
- ["credit_insufficient", 429, "CreditInsufficient"]
1413
- ]) {
1414
- const dataSchema = BUILTIN_DATA_SCHEMAS[code];
1415
- m.set(code, {
1416
- code,
1417
- status,
1418
- className,
1419
- builtin: true,
1420
- ...dataSchema ? { dataSchema, dataDigest: digestOf(code, dataSchema) } : {}
1421
- });
1422
- }
1423
- g[ERROR_REGISTRY] = m;
1424
- }
1425
- return g[ERROR_REGISTRY];
1426
- }
1427
- function defineError(code, status, dataSchema) {
1428
- if (!Number.isInteger(status) || status < 400 || status > 599) {
1429
- throw new Error(
1430
- `defineError: status for "${code}" must be a 4xx/5xx integer, got ${status} \u2014 error responses must not clobber success responses in the project spec.`
1431
- );
1432
- }
1433
- const registry2 = getErrorRegistry();
1434
- const existing = registry2.get(code);
1435
- const className = defaultClassName(code);
1436
- const dataDigest = dataSchema ? digestOf(code, dataSchema) : void 0;
1437
- if (existing) {
1438
- const sameShape = existing.status === status && !existing.builtin && existing.dataDigest === dataDigest;
1439
- if (!sameShape) {
1440
- throw new Error(
1441
- `defineError: duplicate error code "${code}" with a different shape (existing: status ${existing.status}${existing.builtin ? ", built-in" : ""}${existing.status === status && !existing.builtin ? ", different data schema" : ""}). Error codes are project-unique.`
1442
- );
1443
- }
1444
- }
1445
- const makeWithData = (schema) => class extends HttpError {
1446
- static code = code;
1447
- static status = status;
1448
- constructor(data, message) {
1449
- super(status, code, message ?? humanize(code), schema.parse(data));
1450
- this.name = className;
1451
- }
1452
- };
1453
- const makeWithoutData = () => class extends HttpError {
1454
- static code = code;
1455
- static status = status;
1456
- constructor(message) {
1457
- super(status, code, message ?? humanize(code));
1458
- this.name = className;
1459
- }
1460
- };
1461
- const cls = dataSchema ? makeWithData(dataSchema) : makeWithoutData();
1462
- Object.defineProperty(cls, "name", { value: className });
1463
- registry2.set(code, {
1464
- code,
1465
- status,
1466
- className,
1467
- builtin: false,
1468
- ...dataSchema ? { dataSchema } : {},
1469
- ...dataDigest !== void 0 ? { dataDigest } : {}
1470
- });
1471
- return cls;
1472
- }
1473
- function digestOf(code, dataSchema) {
1474
- const TMP_REF = "__PalbaseErrorDataDigest";
1475
- const tmpRegistry = new OpenAPIRegistry();
1476
- tmpRegistry.register(TMP_REF, z.object({ data: dataSchema }).openapi(TMP_REF));
1477
- const generated = new OpenApiGeneratorV31(tmpRegistry.definitions).generateComponents();
1478
- const out = generated.components?.schemas?.[TMP_REF];
1479
- const digest = JSON.stringify(out ?? null);
1480
- if (digest.includes('"$ref"')) {
1481
- throw new Error(
1482
- `defineError: dataSchema for "${code}" carries .openapi(refId) metadata \u2014 it would emit dangling $ref pointers in the project spec. Use a plain zod schema (z.object({...})) without .openapi(...).`
1483
- );
1484
- }
1485
- return digest;
1486
- }
1487
- function defaultClassName(code) {
1488
- return code.split("_").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
1489
- }
1490
- function humanize(code) {
1491
- const s = code.replace(/_/g, " ");
1492
- return s.charAt(0).toUpperCase() + s.slice(1);
1493
- }
1494
-
1495
1975
  // src/decorators/webhook.ts
1496
1976
  var WEBHOOK_META = /* @__PURE__ */ Symbol.for("palbase.backend.webhookMeta");
1497
1977
  var WEBHOOK_EVENTS = /* @__PURE__ */ Symbol.for("palbase.backend.webhookEvents");
1498
- function carrierOf3(ctor) {
1978
+ function carrierOf(ctor) {
1499
1979
  return ctor;
1500
1980
  }
1501
1981
  function Webhook(options) {
1502
1982
  return function(ctor) {
1503
- const carrier = carrierOf3(ctor);
1983
+ const carrier = carrierOf(ctor);
1504
1984
  Object.defineProperty(carrier, WEBHOOK_META, {
1505
1985
  value: options,
1506
1986
  enumerable: false,
@@ -1518,7 +1998,7 @@ function Webhook(options) {
1518
1998
  }
1519
1999
  function On(event) {
1520
2000
  return function(target, fnName) {
1521
- const carrier = carrierOf3(target.constructor);
2001
+ const carrier = carrierOf(target.constructor);
1522
2002
  const existing = carrier[WEBHOOK_EVENTS];
1523
2003
  const entries = existing ? [...existing] : [];
1524
2004
  entries.push({ event, fnName: String(fnName) });
@@ -1531,7 +2011,7 @@ function On(event) {
1531
2011
  };
1532
2012
  }
1533
2013
  function getWebhookConfig(ctor) {
1534
- const carrier = carrierOf3(ctor);
2014
+ const carrier = carrierOf(ctor);
1535
2015
  const meta = carrier[WEBHOOK_META];
1536
2016
  const entries = carrier[WEBHOOK_EVENTS] ?? [];
1537
2017
  if (!meta) {
@@ -1789,7 +2269,7 @@ var documents = {
1789
2269
  };
1790
2270
 
1791
2271
  // src/index.ts
1792
- import { z as z2 } from "zod";
2272
+ import { z } from "zod";
1793
2273
  export {
1794
2274
  BadRequest,
1795
2275
  Body,
@@ -1822,6 +2302,7 @@ export {
1822
2302
  PALBASE_EXTENSIONS,
1823
2303
  PROVIDER_CATALOG,
1824
2304
  PalError,
2305
+ PalbaseModuleError,
1825
2306
  Param,
1826
2307
  Patch,
1827
2308
  PolicyBuilder,
@@ -1836,7 +2317,9 @@ export {
1836
2317
  RequestId,
1837
2318
  RequireEntitlement,
1838
2319
  Resource,
2320
+ SECRETS_CONFIG_KIND,
1839
2321
  STORAGE_CONFIG_KIND,
2322
+ Secrets,
1840
2323
  Spend,
1841
2324
  Storage,
1842
2325
  TEST_USERS_CONFIG_KIND,
@@ -1852,6 +2335,7 @@ export {
1852
2335
  __getRuntime,
1853
2336
  __registerResource,
1854
2337
  __requestALS,
2338
+ __resetRegisteredControllers,
1855
2339
  __runResourceBoot,
1856
2340
  __runWithRuntime,
1857
2341
  __setRuntime,
@@ -1860,6 +2344,7 @@ export {
1860
2344
  bigint,
1861
2345
  boolean,
1862
2346
  bucket,
2347
+ buildModuleClients,
1863
2348
  buildProvider,
1864
2349
  dec,
1865
2350
  defineEgress,
@@ -1868,6 +2353,7 @@ export {
1868
2353
  defineMiddleware,
1869
2354
  defineNotifications,
1870
2355
  defineSchema,
2356
+ defineSecrets,
1871
2357
  defineStorage,
1872
2358
  defineTestUsers,
1873
2359
  documents,
@@ -1877,6 +2363,7 @@ export {
1877
2363
  flag,
1878
2364
  getErrorRegistry,
1879
2365
  getJobConfig,
2366
+ getRegisteredControllers,
1880
2367
  getRoutes,
1881
2368
  getWebhookConfig,
1882
2369
  inc,
@@ -1884,6 +2371,7 @@ export {
1884
2371
  isPalbaseExtension,
1885
2372
  jsonb,
1886
2373
  makeEnvDts,
2374
+ makeHttpClient,
1887
2375
  makePurchasesDts,
1888
2376
  makeTypedDB,
1889
2377
  now,
@@ -1893,13 +2381,15 @@ export {
1893
2381
  raw,
1894
2382
  recordThrows,
1895
2383
  reservedSecretKey,
2384
+ secret,
1896
2385
  spendFor,
1897
2386
  storage,
1898
2387
  testUser,
1899
2388
  text,
1900
2389
  timestamp,
2390
+ toSchemaJSON,
1901
2391
  uuid,
1902
2392
  validateUploadAgainstStorage,
1903
- z2 as z
2393
+ z
1904
2394
  };
1905
2395
  //# sourceMappingURL=index.js.map