@palbase/backend 17.4.0 → 18.0.1

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.cjs CHANGED
@@ -51,6 +51,7 @@ __export(src_exports, {
51
51
  PALBASE_EXTENSIONS: () => PALBASE_EXTENSIONS,
52
52
  PROVIDER_CATALOG: () => PROVIDER_CATALOG,
53
53
  PalError: () => PalError,
54
+ PalbaseModuleError: () => PalbaseModuleError,
54
55
  Param: () => Param,
55
56
  Patch: () => Patch,
56
57
  PolicyBuilder: () => PolicyBuilder,
@@ -65,7 +66,9 @@ __export(src_exports, {
65
66
  RequestId: () => RequestId,
66
67
  RequireEntitlement: () => RequireEntitlement,
67
68
  Resource: () => Resource,
69
+ SECRETS_CONFIG_KIND: () => SECRETS_CONFIG_KIND,
68
70
  STORAGE_CONFIG_KIND: () => STORAGE_CONFIG_KIND,
71
+ Secrets: () => Secrets,
69
72
  Spend: () => Spend,
70
73
  Storage: () => Storage,
71
74
  TEST_USERS_CONFIG_KIND: () => TEST_USERS_CONFIG_KIND,
@@ -81,6 +84,7 @@ __export(src_exports, {
81
84
  __getRuntime: () => __getRuntime,
82
85
  __registerResource: () => __registerResource,
83
86
  __requestALS: () => __requestALS,
87
+ __resetRegisteredControllers: () => __resetRegisteredControllers,
84
88
  __runResourceBoot: () => __runResourceBoot,
85
89
  __runWithRuntime: () => __runWithRuntime,
86
90
  __setRuntime: () => __setRuntime,
@@ -89,6 +93,7 @@ __export(src_exports, {
89
93
  bigint: () => bigint,
90
94
  boolean: () => boolean,
91
95
  bucket: () => bucket,
96
+ buildModuleClients: () => buildModuleClients,
92
97
  buildProvider: () => buildProvider,
93
98
  dec: () => dec,
94
99
  defineEgress: () => defineEgress,
@@ -97,6 +102,7 @@ __export(src_exports, {
97
102
  defineMiddleware: () => defineMiddleware,
98
103
  defineNotifications: () => defineNotifications,
99
104
  defineSchema: () => defineSchema,
105
+ defineSecrets: () => defineSecrets,
100
106
  defineStorage: () => defineStorage,
101
107
  defineTestUsers: () => defineTestUsers,
102
108
  documents: () => documents,
@@ -106,6 +112,7 @@ __export(src_exports, {
106
112
  flag: () => flag,
107
113
  getErrorRegistry: () => getErrorRegistry,
108
114
  getJobConfig: () => getJobConfig,
115
+ getRegisteredControllers: () => getRegisteredControllers,
109
116
  getRoutes: () => getRoutes,
110
117
  getWebhookConfig: () => getWebhookConfig,
111
118
  inc: () => inc,
@@ -113,6 +120,7 @@ __export(src_exports, {
113
120
  isPalbaseExtension: () => isPalbaseExtension,
114
121
  jsonb: () => jsonb,
115
122
  makeEnvDts: () => makeEnvDts,
123
+ makeHttpClient: () => makeHttpClient,
116
124
  makePurchasesDts: () => makePurchasesDts,
117
125
  makeTypedDB: () => makeTypedDB,
118
126
  now: () => now,
@@ -122,11 +130,13 @@ __export(src_exports, {
122
130
  raw: () => raw,
123
131
  recordThrows: () => recordThrows,
124
132
  reservedSecretKey: () => reservedSecretKey,
133
+ secret: () => secret,
125
134
  spendFor: () => spendFor,
126
135
  storage: () => storage,
127
136
  testUser: () => testUser,
128
137
  text: () => text,
129
138
  timestamp: () => timestamp,
139
+ toSchemaJSON: () => toSchemaJSON,
130
140
  uuid: () => uuid,
131
141
  validateUploadAgainstStorage: () => validateUploadAgainstStorage,
132
142
  z: () => import_zod2.z
@@ -635,8 +645,35 @@ var Database = Object.assign(makeTypedSurface(rawDatabase), {
635
645
  }
636
646
  });
637
647
  var Documents = makeServiceProxy("Documents");
638
- var Storage = makeServiceProxy("Storage");
648
+ function makeBucketsAccessor(storage2) {
649
+ return new Proxy(
650
+ {},
651
+ {
652
+ get(_t, prop) {
653
+ if (typeof prop !== "string") return void 0;
654
+ return storage2().bucket(prop);
655
+ }
656
+ }
657
+ );
658
+ }
659
+ var rawStorage = makeServiceProxy("Storage");
660
+ var Storage = Object.assign(
661
+ {
662
+ // FORWARDED explicitly, not assigned onto the service proxy.
663
+ //
664
+ // `Object.assign(rawStorage, {buckets})` writes onto the proxy's TARGET, and
665
+ // the proxy's only trap is `get`, which forwards every read to the module
666
+ // client — so the property landed somewhere nothing reads and
667
+ // `Storage.buckets.docs` was `undefined` in a deployed handler. It
668
+ // typechecked, every test passed, and the live call answered
669
+ // "TypeError: undefined is not an object". `Database` never had the bug
670
+ // because it builds a plain surface the same way this now does.
671
+ bucket: (name) => rawStorage.bucket(name)
672
+ },
673
+ { buckets: makeBucketsAccessor(() => rawStorage) }
674
+ );
639
675
  var Cache = makeServiceProxy("Cache");
676
+ var Secrets = makeServiceProxy("Secrets");
640
677
  var Log = makeServiceProxy("Log");
641
678
  var Notifications = makeServiceProxy("Notifications");
642
679
  var Purchases = makeServiceProxy("Purchases");
@@ -652,6 +689,19 @@ var Flags = Object.assign(
652
689
  getAll(context) {
653
690
  return rawFlags.getAll(context);
654
691
  },
692
+ /**
693
+ * Resolve a flag's value, with an optional fallback.
694
+ *
695
+ * FORWARDED as of 2026-08-15. This surface is written out by hand, method
696
+ * by method, and `get` was missing from it — so the client implemented it,
697
+ * thirty assertions covered it, and `Flags.get("x")` was `undefined` in a
698
+ * deployed handler. Exactly the shape of the `Storage.buckets` defect found
699
+ * the same day: a hand-maintained forwarding list is a list somebody has to
700
+ * remember to update.
701
+ */
702
+ get(flagName, defaultOrContext, maybeContext) {
703
+ return rawFlags.get(flagName, defaultOrContext, maybeContext);
704
+ },
655
705
  setOverride(key, value) {
656
706
  return rawFlags.setOverride(key, value);
657
707
  }
@@ -671,11 +721,14 @@ var Flags = Object.assign(
671
721
  var Realtime = makeServiceProxy("Realtime");
672
722
 
673
723
  // src/errors.ts
724
+ var HTTP_ERROR_BRAND = /* @__PURE__ */ Symbol.for("palbase.backend.httpError");
674
725
  var HttpError = class extends Error {
675
726
  status;
676
727
  error;
677
728
  errorDescription;
678
729
  data;
730
+ /** See {@link HTTP_ERROR_BRAND} — how the engine recognises this across SDK copies. */
731
+ [HTTP_ERROR_BRAND] = true;
679
732
  constructor(status, error, errorDescription, data) {
680
733
  super(errorDescription);
681
734
  this.name = "HttpError";
@@ -911,6 +964,795 @@ function Spend(key) {
911
964
  };
912
965
  }
913
966
 
967
+ // src/clients/http.ts
968
+ var PalbaseModuleError = class extends Error {
969
+ code;
970
+ status;
971
+ details;
972
+ constructor(code, message, status, details = {}) {
973
+ super(message);
974
+ this.name = "PalbaseModuleError";
975
+ this.code = code;
976
+ this.status = status;
977
+ this.details = details;
978
+ }
979
+ };
980
+ function makeHttpClient(cfg) {
981
+ async function request(method, path, options = {}) {
982
+ const headers = {
983
+ "Content-Type": "application/json",
984
+ ...options.headers ?? {}
985
+ };
986
+ if (cfg.apiKey) headers["apikey"] = cfg.apiKey;
987
+ const init = { method, headers };
988
+ if (options.body !== void 0) {
989
+ if (typeof options.body === "string" || options.body instanceof Uint8Array) {
990
+ init.body = options.body;
991
+ } else if (typeof FormData !== "undefined" && options.body instanceof FormData) {
992
+ delete headers["Content-Type"];
993
+ init.body = options.body;
994
+ } else if (typeof Blob !== "undefined" && options.body instanceof Blob) {
995
+ delete headers["Content-Type"];
996
+ init.body = options.body;
997
+ } else {
998
+ init.body = JSON.stringify(options.body);
999
+ }
1000
+ }
1001
+ if (options.signal) init.signal = options.signal;
1002
+ const doFetch = cfg.fetchImpl ?? globalThis.fetch;
1003
+ let response;
1004
+ try {
1005
+ response = await doFetch(`${cfg.baseUrl}${path}`, init);
1006
+ } catch (err) {
1007
+ return {
1008
+ data: null,
1009
+ error: new PalbaseModuleError(
1010
+ "network_error",
1011
+ err instanceof Error ? err.message : "Network request failed",
1012
+ 0
1013
+ ),
1014
+ status: 0
1015
+ };
1016
+ }
1017
+ const contentType = response.headers.get("Content-Type") ?? "";
1018
+ let parsed = null;
1019
+ if (method !== "HEAD" && contentType.includes("json")) {
1020
+ parsed = await response.json().catch(() => null);
1021
+ } else if (method !== "HEAD" && contentType.startsWith("image/")) {
1022
+ parsed = new Uint8Array(await response.arrayBuffer());
1023
+ } else if (method !== "HEAD" && response.body) {
1024
+ parsed = await response.text().catch(() => null);
1025
+ }
1026
+ if (!response.ok) {
1027
+ const body = parsed && typeof parsed === "object" ? parsed : {};
1028
+ const code = typeof body.error === "string" && body.error ? body.error : "unknown_error";
1029
+ const message = typeof body.error_description === "string" && body.error_description || response.statusText || "request failed";
1030
+ return {
1031
+ data: null,
1032
+ error: new PalbaseModuleError(code, message, response.status, body),
1033
+ status: response.status
1034
+ };
1035
+ }
1036
+ return { data: parsed, error: null, status: response.status };
1037
+ }
1038
+ return { baseUrl: cfg.baseUrl, request };
1039
+ }
1040
+
1041
+ // src/clients/documents.ts
1042
+ var SEGMENT_RE = /^[A-Za-z0-9_-]+$/;
1043
+ function validateSegment(segment, label) {
1044
+ if (!SEGMENT_RE.test(segment)) {
1045
+ throw new Error(`Invalid ${label}: "${segment}". Must match ${SEGMENT_RE.source}`);
1046
+ }
1047
+ }
1048
+ function buildDocumentRef(http, path) {
1049
+ return {
1050
+ path,
1051
+ async set(data) {
1052
+ return http.request("PUT", `/v1/docs/${path}`, { body: data });
1053
+ },
1054
+ async get() {
1055
+ const response = await http.request("GET", `/v1/docs/${path}`);
1056
+ if (response.error) return { data: null, error: response.error, status: response.status };
1057
+ const raw2 = response.data ?? {};
1058
+ const exists = raw2.exists !== void 0 ? raw2.exists : Boolean(raw2.data || raw2.id);
1059
+ const segments = path.split("/");
1060
+ const id = raw2.id || segments[segments.length - 1] || "";
1061
+ return {
1062
+ data: { id, exists, data: () => raw2.data, ref: { path } },
1063
+ error: null,
1064
+ status: response.status
1065
+ };
1066
+ },
1067
+ async update(data) {
1068
+ return http.request("PATCH", `/v1/docs/${path}`, { body: data });
1069
+ },
1070
+ async delete() {
1071
+ return http.request("DELETE", `/v1/docs/${path}`);
1072
+ },
1073
+ collection(name) {
1074
+ validateSegment(name, "subcollection name");
1075
+ return buildCollectionRef(http, `${path}/${name}`);
1076
+ }
1077
+ };
1078
+ }
1079
+ function buildCollectionRef(http, path, state = { where: [], orderBy: [] }) {
1080
+ function snapshot(doc) {
1081
+ return {
1082
+ id: doc.id,
1083
+ exists: true,
1084
+ data: () => doc.data,
1085
+ ref: { path: `${path}/${doc.id}` }
1086
+ };
1087
+ }
1088
+ function querySnapshot(docs) {
1089
+ return {
1090
+ docs,
1091
+ empty: docs.length === 0,
1092
+ size: docs.length,
1093
+ docChanges: () => docs.map((doc) => ({ type: "added", doc }))
1094
+ };
1095
+ }
1096
+ return {
1097
+ path,
1098
+ doc(id) {
1099
+ validateSegment(id, "document ID");
1100
+ return buildDocumentRef(http, `${path}/${id}`);
1101
+ },
1102
+ async add(data) {
1103
+ const resp = await http.request("POST", `/v1/docs/${path}`, { body: data });
1104
+ if (resp.error || !resp.data) {
1105
+ return { data: null, error: resp.error, status: resp.status };
1106
+ }
1107
+ return {
1108
+ data: buildDocumentRef(http, `${path}/${resp.data.id}`),
1109
+ error: null,
1110
+ status: resp.status
1111
+ };
1112
+ },
1113
+ // Each narrowing returns a NEW ref. A builder that mutated in place would
1114
+ // leak one caller's filter into a query another caller had already held.
1115
+ where(field, op, value) {
1116
+ return buildCollectionRef(http, path, {
1117
+ ...state,
1118
+ where: [...state.where, { field, op, value }]
1119
+ });
1120
+ },
1121
+ orderBy(field, direction = "asc") {
1122
+ return buildCollectionRef(http, path, {
1123
+ ...state,
1124
+ orderBy: [...state.orderBy, { field, direction }]
1125
+ });
1126
+ },
1127
+ limit(n) {
1128
+ return buildCollectionRef(http, path, { ...state, limit: n });
1129
+ },
1130
+ async get() {
1131
+ const narrowed = state.where.length > 0 || state.orderBy.length > 0 || state.limit !== void 0;
1132
+ const resp = narrowed ? await http.request(
1133
+ "POST",
1134
+ `/v1/docs/${path}/query`,
1135
+ { body: queryBody(state) }
1136
+ ) : await http.request(
1137
+ "GET",
1138
+ `/v1/docs/${path}`
1139
+ );
1140
+ if (resp.error) return { data: null, error: resp.error, status: resp.status };
1141
+ const docs = (resp.data?.documents ?? []).map(snapshot);
1142
+ return { data: querySnapshot(docs), error: null, status: resp.status };
1143
+ }
1144
+ };
1145
+ }
1146
+ function queryBody(state) {
1147
+ const body = {};
1148
+ if (state.where.length > 0) {
1149
+ body.where = state.where.map((w) => ({ field: w.field, op: w.op, value: w.value }));
1150
+ }
1151
+ if (state.orderBy.length > 0) {
1152
+ body.orderBy = state.orderBy.map((o) => ({ field: o.field, direction: o.direction }));
1153
+ }
1154
+ if (state.limit !== void 0) body.limit = state.limit;
1155
+ return body;
1156
+ }
1157
+ var MAX_BATCH = 500;
1158
+ function buildDocumentsClient(http) {
1159
+ return {
1160
+ /**
1161
+ * `Documents.doc("users/alice")` — segments are collection/documentId PAIRS,
1162
+ * so an odd count addresses a COLLECTION and is refused. Accepting it would
1163
+ * write a document whose id happens to be a collection's name.
1164
+ */
1165
+ doc(path) {
1166
+ const segments = String(path).split("/").filter((s) => s.length > 0);
1167
+ if (segments.length === 0 || segments.length % 2 !== 0) {
1168
+ throw new Error(
1169
+ `Invalid document path: "${path}". Expected collection/documentId pairs, got ${segments.length} segment(s).`
1170
+ );
1171
+ }
1172
+ segments.forEach((s, i) => validateSegment(s, i % 2 === 0 ? "collection name" : "document ID"));
1173
+ return buildDocumentRef(http, segments.join("/"));
1174
+ },
1175
+ collection(name) {
1176
+ validateSegment(name, "collection name");
1177
+ return buildCollectionRef(http, name);
1178
+ },
1179
+ async batch(operations) {
1180
+ if (operations.length > MAX_BATCH) {
1181
+ return {
1182
+ data: null,
1183
+ error: new PalbaseModuleError(
1184
+ "batch_too_large",
1185
+ `Batch size ${operations.length} exceeds maximum of ${MAX_BATCH}`,
1186
+ 400
1187
+ ),
1188
+ status: 400
1189
+ };
1190
+ }
1191
+ if (operations.length === 0) return { data: null, error: null, status: 200 };
1192
+ return http.request("POST", "/v1/docs/batch", {
1193
+ body: operations.map((op) => ({ op: op.op, path: op.ref.path, data: op.data }))
1194
+ });
1195
+ }
1196
+ };
1197
+ }
1198
+
1199
+ // src/clients/flags.ts
1200
+ var FLAG_NAME_RE = /^[A-Za-z0-9_.-]+$/;
1201
+ function assertFlagName(flagName) {
1202
+ if (!FLAG_NAME_RE.test(flagName)) {
1203
+ throw new Error(`Invalid flag name: "${flagName}". Flag names must match ${FLAG_NAME_RE.source}`);
1204
+ }
1205
+ }
1206
+ function flagEnabled(value) {
1207
+ if (typeof value === "boolean") return value;
1208
+ return value != null && value !== 0 && value !== "";
1209
+ }
1210
+ function flagVariant(value) {
1211
+ return typeof value === "string" ? { name: value } : null;
1212
+ }
1213
+ function isContextObject(v) {
1214
+ return v !== null && typeof v === "object" && !Array.isArray(v) && ("userId" in v || "properties" in v);
1215
+ }
1216
+ function buildFlagsClient(http, cfg) {
1217
+ function resolveUserId(context) {
1218
+ if (context && "userId" in context) return context.userId ?? null;
1219
+ return cfg.getCurrentUserId() || void 0;
1220
+ }
1221
+ function mergedPath(context) {
1222
+ const uid = resolveUserId(context);
1223
+ return uid ? `/v1/user-flags/users/${encodeURIComponent(uid)}` : "/v1/user-flags";
1224
+ }
1225
+ function fetchMerged(context) {
1226
+ return http.request("GET", mergedPath(context));
1227
+ }
1228
+ const service = {
1229
+ async setOverrideForUser(userId, key, value) {
1230
+ return http.request(
1231
+ "PUT",
1232
+ `/v1/user-flags/users/${encodeURIComponent(userId)}/${encodeURIComponent(key)}`,
1233
+ { body: { value } }
1234
+ );
1235
+ },
1236
+ async setOverridesForUser(userId, values) {
1237
+ return http.request("PUT", `/v1/user-flags/users/${encodeURIComponent(userId)}`, {
1238
+ body: { values }
1239
+ });
1240
+ },
1241
+ async clearOverrideForUser(userId, key) {
1242
+ return http.request(
1243
+ "DELETE",
1244
+ `/v1/user-flags/users/${encodeURIComponent(userId)}/${encodeURIComponent(key)}`
1245
+ );
1246
+ },
1247
+ async clearAllOverridesForUser(userId) {
1248
+ return http.request("DELETE", `/v1/user-flags/users/${encodeURIComponent(userId)}`);
1249
+ },
1250
+ async batchSetOverrides(operations) {
1251
+ const ops = (operations ?? []).map((op) => {
1252
+ const raw2 = op;
1253
+ return { user_id: raw2.userId ?? raw2.user_id, values: raw2.values };
1254
+ });
1255
+ return http.request("POST", "/v1/user-flags/batch", { body: { operations: ops } });
1256
+ }
1257
+ };
1258
+ return {
1259
+ async isEnabled(flagName, context) {
1260
+ assertFlagName(flagName);
1261
+ const res = await fetchMerged(context);
1262
+ if (res.error || res.data == null) return { data: null, error: res.error, status: res.status };
1263
+ return { data: flagEnabled(res.data.values?.[flagName]), error: null, status: res.status };
1264
+ },
1265
+ async getVariant(flagName, context) {
1266
+ assertFlagName(flagName);
1267
+ const res = await fetchMerged(context);
1268
+ if (res.error || res.data == null) return { data: null, error: res.error, status: res.status };
1269
+ return { data: flagVariant(res.data.values?.[flagName]), error: null, status: res.status };
1270
+ },
1271
+ /**
1272
+ * `get(key)` · `get(key, default)` · `get(key, default, ctx)` · `get(key, ctx)`.
1273
+ *
1274
+ * The default is substituted when the flag is ABSENT or when the flags
1275
+ * service is unreachable — a product that hides a feature because a lookup
1276
+ * timed out is behaving correctly; one that crashes is not.
1277
+ */
1278
+ async get(flagName, defaultOrContext, maybeContext) {
1279
+ assertFlagName(flagName);
1280
+ let defaultValue;
1281
+ let context;
1282
+ let hasDefault = false;
1283
+ if (maybeContext !== void 0) {
1284
+ defaultValue = defaultOrContext;
1285
+ hasDefault = true;
1286
+ context = maybeContext;
1287
+ } else if (isContextObject(defaultOrContext)) {
1288
+ context = defaultOrContext;
1289
+ } else if (defaultOrContext !== void 0) {
1290
+ defaultValue = defaultOrContext;
1291
+ hasDefault = true;
1292
+ }
1293
+ const resp = await fetchMerged(context);
1294
+ if (resp.error !== null) {
1295
+ if (hasDefault) return { data: defaultValue ?? null, error: null, status: resp.status };
1296
+ return { data: null, error: resp.error, status: resp.status };
1297
+ }
1298
+ const values = resp.data?.values ?? {};
1299
+ const value = values[flagName];
1300
+ if (value === void 0) {
1301
+ return { data: hasDefault ? defaultValue ?? null : null, error: null, status: resp.status };
1302
+ }
1303
+ return { data: value, error: null, status: resp.status };
1304
+ },
1305
+ async getAll(context) {
1306
+ const res = await fetchMerged(context);
1307
+ if (res.error || res.data == null) return { data: null, error: res.error, status: res.status };
1308
+ const values = res.data.values ?? {};
1309
+ return {
1310
+ data: Object.keys(values).map((name) => {
1311
+ const value = values[name];
1312
+ const flag2 = {
1313
+ name,
1314
+ enabled: flagEnabled(value)
1315
+ };
1316
+ if (typeof value === "string") flag2.variant = { name: value };
1317
+ return flag2;
1318
+ }),
1319
+ error: null,
1320
+ status: res.status
1321
+ };
1322
+ },
1323
+ /**
1324
+ * Override a flag for the CURRENT request user.
1325
+ *
1326
+ * On an anonymous request this does not silently no-op and does not write
1327
+ * for nobody: it refuses and names the call that does cross-user writes.
1328
+ */
1329
+ async setOverride(key, value) {
1330
+ const uid = cfg.getCurrentUserId();
1331
+ if (!uid) {
1332
+ return {
1333
+ data: null,
1334
+ error: {
1335
+ message: "setOverride requires a signed-in user; use Flags.asService().setOverrideForUser(userId, key, value) for cross-user writes"
1336
+ },
1337
+ status: 400
1338
+ };
1339
+ }
1340
+ return http.request(
1341
+ "PUT",
1342
+ `/v1/user-flags/users/${encodeURIComponent(uid)}/${encodeURIComponent(key)}`,
1343
+ { body: { value } }
1344
+ );
1345
+ },
1346
+ asService() {
1347
+ return service;
1348
+ }
1349
+ };
1350
+ }
1351
+
1352
+ // src/clients/notifications.ts
1353
+ function mapEnvelope(res, map) {
1354
+ if (res.error !== null || res.data === null || res.data === void 0) {
1355
+ return { data: null, error: res.error, status: res.status };
1356
+ }
1357
+ return { data: map(res.data), error: null, status: res.status };
1358
+ }
1359
+ function toEmailTemplate(wire) {
1360
+ const view = {
1361
+ id: wire.id,
1362
+ slug: wire.slug,
1363
+ locale: wire.locale,
1364
+ subject: wire.subject,
1365
+ htmlBody: wire.html_body,
1366
+ variables: wire.variables ?? [],
1367
+ isDefault: wire.is_default,
1368
+ createdAt: wire.created_at,
1369
+ updatedAt: wire.updated_at
1370
+ };
1371
+ if (wire.text_body !== void 0) view.textBody = wire.text_body;
1372
+ return view;
1373
+ }
1374
+ function toSmsTemplate(wire) {
1375
+ return {
1376
+ id: wire.id,
1377
+ slug: wire.slug,
1378
+ locale: wire.locale,
1379
+ body: wire.body,
1380
+ variables: wire.variables ?? [],
1381
+ isDefault: wire.is_default,
1382
+ createdAt: wire.created_at,
1383
+ updatedAt: wire.updated_at
1384
+ };
1385
+ }
1386
+ function buildNotificationsClient(http) {
1387
+ const push = {
1388
+ async send(params) {
1389
+ return http.request("POST", "/v1/notifications/push", { body: params });
1390
+ }
1391
+ };
1392
+ const email = {
1393
+ async send(params) {
1394
+ const { templateSlug, html, text: text2, ...rest } = params;
1395
+ const body = { ...rest };
1396
+ if (templateSlug !== void 0) body.template_slug = templateSlug;
1397
+ if (html !== void 0) body.html_body = html;
1398
+ if (text2 !== void 0) body.text_body = text2;
1399
+ return http.request("POST", "/v1/notifications/email", { body });
1400
+ }
1401
+ };
1402
+ const sms = {
1403
+ async send(params) {
1404
+ const { templateSlug, ...rest } = params;
1405
+ const body = templateSlug !== void 0 ? { ...rest, template_slug: templateSlug } : rest;
1406
+ return http.request("POST", "/v1/notifications/sms", { body });
1407
+ }
1408
+ };
1409
+ const verifications = {
1410
+ async start(params) {
1411
+ return http.request("POST", "/v1/notifications/verifications", { body: params });
1412
+ },
1413
+ async check(params) {
1414
+ return http.request("POST", "/v1/notifications/verifications/check", { body: params });
1415
+ }
1416
+ };
1417
+ const inbox = {
1418
+ async send(params) {
1419
+ return http.request("POST", "/v1/notifications/inbox", { body: params });
1420
+ },
1421
+ async list(options) {
1422
+ const opts = options ?? {};
1423
+ const params = new URLSearchParams();
1424
+ if (opts.cursor) params.set("cursor", opts.cursor);
1425
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
1426
+ if (opts.is_read !== void 0) params.set("is_read", opts.is_read ? "true" : "false");
1427
+ if (opts.category) params.set("category", opts.category);
1428
+ if (opts.include_archived) params.set("include_archived", "true");
1429
+ const query = params.toString();
1430
+ return http.request("GET", `/v1/notifications/inbox${query ? `?${query}` : ""}`);
1431
+ },
1432
+ async unreadCount() {
1433
+ return http.request("GET", "/v1/notifications/inbox/unread-count");
1434
+ },
1435
+ async markRead(id) {
1436
+ return http.request("PATCH", `/v1/notifications/inbox/${encodeURIComponent(id)}/read`);
1437
+ },
1438
+ async markAllRead() {
1439
+ return http.request("POST", "/v1/notifications/inbox/read-all");
1440
+ },
1441
+ async archive(id) {
1442
+ return http.request("DELETE", `/v1/notifications/inbox/${encodeURIComponent(id)}`);
1443
+ }
1444
+ };
1445
+ const preferences = {
1446
+ async get() {
1447
+ return http.request("GET", "/v1/notifications/preferences");
1448
+ },
1449
+ async update(params) {
1450
+ return http.request("PUT", "/v1/notifications/preferences", { body: params });
1451
+ }
1452
+ };
1453
+ const emailTemplates = {
1454
+ async list() {
1455
+ const resp = await http.request("GET", "/v1/notifications/templates");
1456
+ return mapEnvelope(resp, (rows) => (rows ?? []).map(toEmailTemplate));
1457
+ },
1458
+ async get(id) {
1459
+ const resp = await http.request(
1460
+ "GET",
1461
+ `/v1/notifications/templates/${encodeURIComponent(id)}`
1462
+ );
1463
+ return mapEnvelope(resp, toEmailTemplate);
1464
+ },
1465
+ async create(input) {
1466
+ const body = {
1467
+ slug: input.slug,
1468
+ subject: input.subject,
1469
+ html_body: input.htmlBody
1470
+ };
1471
+ if (input.textBody !== void 0) body.text_body = input.textBody;
1472
+ if (input.variables !== void 0) body.variables = input.variables;
1473
+ const resp = await http.request("POST", "/v1/notifications/templates", { body });
1474
+ return mapEnvelope(resp, toEmailTemplate);
1475
+ },
1476
+ async update(id, input) {
1477
+ const body = {};
1478
+ if (input.subject !== void 0) body.subject = input.subject;
1479
+ if (input.htmlBody !== void 0) body.html_body = input.htmlBody;
1480
+ if (input.textBody !== void 0) body.text_body = input.textBody;
1481
+ if (input.variables !== void 0) body.variables = input.variables;
1482
+ const resp = await http.request(
1483
+ "PUT",
1484
+ `/v1/notifications/templates/${encodeURIComponent(id)}`,
1485
+ { body }
1486
+ );
1487
+ return mapEnvelope(resp, toEmailTemplate);
1488
+ },
1489
+ async delete(id) {
1490
+ return http.request("DELETE", `/v1/notifications/templates/${encodeURIComponent(id)}`);
1491
+ }
1492
+ };
1493
+ const smsTemplates = {
1494
+ async list() {
1495
+ const resp = await http.request("GET", "/v1/notifications/sms-templates");
1496
+ return mapEnvelope(resp, (rows) => (rows ?? []).map(toSmsTemplate));
1497
+ },
1498
+ async get(id) {
1499
+ const resp = await http.request(
1500
+ "GET",
1501
+ `/v1/notifications/sms-templates/${encodeURIComponent(id)}`
1502
+ );
1503
+ return mapEnvelope(resp, toSmsTemplate);
1504
+ },
1505
+ async create(input) {
1506
+ const body = { slug: input.slug, body: input.body };
1507
+ if (input.variables !== void 0) body.variables = input.variables;
1508
+ const resp = await http.request("POST", "/v1/notifications/sms-templates", { body });
1509
+ return mapEnvelope(resp, toSmsTemplate);
1510
+ },
1511
+ async update(id, input) {
1512
+ const body = {};
1513
+ if (input.body !== void 0) body.body = input.body;
1514
+ if (input.variables !== void 0) body.variables = input.variables;
1515
+ const resp = await http.request(
1516
+ "PUT",
1517
+ `/v1/notifications/sms-templates/${encodeURIComponent(id)}`,
1518
+ { body }
1519
+ );
1520
+ return mapEnvelope(resp, toSmsTemplate);
1521
+ },
1522
+ async delete(id) {
1523
+ return http.request("DELETE", `/v1/notifications/sms-templates/${encodeURIComponent(id)}`);
1524
+ }
1525
+ };
1526
+ return {
1527
+ push,
1528
+ email,
1529
+ sms,
1530
+ verifications,
1531
+ inbox,
1532
+ preferences,
1533
+ templates: { email: emailTemplates, sms: smsTemplates },
1534
+ async registerDevice(params) {
1535
+ return http.request("POST", "/v1/notifications/devices", { body: params });
1536
+ },
1537
+ async unregisterDevice(deviceId) {
1538
+ return http.request("DELETE", `/v1/notifications/devices/${encodeURIComponent(deviceId)}`);
1539
+ }
1540
+ };
1541
+ }
1542
+
1543
+ // src/clients/realtime.ts
1544
+ var import_node_crypto = require("crypto");
1545
+ function mintToken(secret2) {
1546
+ const now2 = Math.floor(Date.now() / 1e3);
1547
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
1548
+ const payload = Buffer.from(
1549
+ JSON.stringify({ iss: "backend-runtime", role: "service_role", iat: now2, exp: now2 + 60 })
1550
+ ).toString("base64url");
1551
+ const sig = (0, import_node_crypto.createHmac)("sha256", secret2).update(`${header}.${payload}`).digest("base64url");
1552
+ return `${header}.${payload}.${sig}`;
1553
+ }
1554
+ function buildRealtimeClient(cfg) {
1555
+ const url = `${cfg.baseUrl}/realtime/api/broadcast`;
1556
+ return {
1557
+ async broadcast(channel, event, payload) {
1558
+ if (typeof channel !== "string" || channel.length === 0) {
1559
+ return {
1560
+ data: null,
1561
+ error: new PalbaseModuleError("invalid_argument", "channel must be a non-empty string", 400)
1562
+ };
1563
+ }
1564
+ if (typeof event !== "string" || event.length === 0) {
1565
+ return {
1566
+ data: null,
1567
+ error: new PalbaseModuleError("invalid_argument", "event must be a non-empty string", 400)
1568
+ };
1569
+ }
1570
+ if (!cfg.apiJwtSecret) {
1571
+ return {
1572
+ data: null,
1573
+ error: new PalbaseModuleError(
1574
+ "realtime_unconfigured",
1575
+ "Realtime.broadcast is unavailable: realtime is not provisioned for this Environment (PALBASE_REALTIME_API_JWT_SECRET unset).",
1576
+ 503
1577
+ )
1578
+ };
1579
+ }
1580
+ let token;
1581
+ try {
1582
+ token = mintToken(cfg.apiJwtSecret);
1583
+ } catch (err) {
1584
+ return {
1585
+ data: null,
1586
+ error: new PalbaseModuleError(
1587
+ "realtime_token_error",
1588
+ err instanceof Error ? err.message : "token mint failed",
1589
+ 500
1590
+ )
1591
+ };
1592
+ }
1593
+ const body = JSON.stringify({
1594
+ messages: [{ topic: channel, event, payload: payload ?? {}, private: false }]
1595
+ });
1596
+ const doFetch = cfg.fetchImpl ?? globalThis.fetch;
1597
+ let response;
1598
+ try {
1599
+ response = await doFetch(url, {
1600
+ method: "POST",
1601
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
1602
+ body
1603
+ });
1604
+ } catch (err) {
1605
+ return {
1606
+ data: null,
1607
+ error: new PalbaseModuleError(
1608
+ "network_error",
1609
+ err instanceof Error ? err.message : "broadcast request failed",
1610
+ 0
1611
+ )
1612
+ };
1613
+ }
1614
+ if (response.status === 202 || response.status === 200) {
1615
+ return { data: void 0, error: null };
1616
+ }
1617
+ const detail = await response.text().catch(() => "");
1618
+ return {
1619
+ data: null,
1620
+ error: new PalbaseModuleError(
1621
+ "realtime_broadcast_failed",
1622
+ `broadcast returned ${response.status}: ${detail.slice(0, 200)}`,
1623
+ response.status
1624
+ )
1625
+ };
1626
+ }
1627
+ };
1628
+ }
1629
+
1630
+ // src/clients/storage.ts
1631
+ var BUCKET_NAME_RE = /^[a-zA-Z0-9_-]+$/;
1632
+ var STORAGE_PATH_RE = /^[a-zA-Z0-9_./-]+$/;
1633
+ var STORAGE_TRAVERSAL_RE = /(?:^|\/)\.\.(?:\/|$)/;
1634
+ function validateStoragePath(p) {
1635
+ if (!STORAGE_PATH_RE.test(p) || STORAGE_TRAVERSAL_RE.test(p)) {
1636
+ throw new Error(
1637
+ `Invalid file path: "${p}". Paths must not contain traversal sequences and must match ${STORAGE_PATH_RE.source}`
1638
+ );
1639
+ }
1640
+ }
1641
+ function toFileObject(bucket2, row) {
1642
+ return {
1643
+ name: row.path ?? "",
1644
+ path: row.path ?? "",
1645
+ bucket: row.bucket ?? bucket2,
1646
+ size: typeof row.size === "number" ? row.size : 0,
1647
+ contentType: row.contentType ?? "",
1648
+ checksum: row.checksum ?? "",
1649
+ width: row.width,
1650
+ height: row.height,
1651
+ thumbhash: row.thumbhash,
1652
+ variants: row.variants ?? {}
1653
+ };
1654
+ }
1655
+ function mapResponse(res, map) {
1656
+ if (res.error || res.data === null || res.data === void 0) {
1657
+ return { data: null, error: res.error, status: res.status };
1658
+ }
1659
+ return { data: map(res.data), error: null, status: res.status };
1660
+ }
1661
+ function buildBucketClient(http, bucketName) {
1662
+ const objectPath = (path) => `/v1/storage/object/${bucketName}/${path}`;
1663
+ return {
1664
+ async upload(path, file, options) {
1665
+ validateStoragePath(path);
1666
+ const headers = {
1667
+ "Content-Type": options?.contentType ?? "application/octet-stream"
1668
+ };
1669
+ if (options?.upsert) headers["x-upsert"] = "true";
1670
+ const body = file instanceof ArrayBuffer ? new Uint8Array(file) : file;
1671
+ const res = await http.request("PUT", objectPath(path), {
1672
+ body,
1673
+ headers
1674
+ });
1675
+ return mapResponse(res, (r) => toFileObject(bucketName, r ?? {}));
1676
+ },
1677
+ async download(path) {
1678
+ validateStoragePath(path);
1679
+ return http.request("GET", objectPath(path), { headers: { Accept: "*/*" } });
1680
+ },
1681
+ getPublicUrl(path, options) {
1682
+ validateStoragePath(path);
1683
+ const variant = options?.variant ? `?variant=${encodeURIComponent(options.variant)}` : "";
1684
+ return `/v1/files/${bucketName}/${path}${variant}`;
1685
+ },
1686
+ async createSignedUrl(path, options) {
1687
+ validateStoragePath(path);
1688
+ return http.request("POST", `/v1/storage/sign/${bucketName}/${path}`, {
1689
+ body: { expiresIn: options.expiresIn }
1690
+ });
1691
+ },
1692
+ async list(prefix, options) {
1693
+ if (prefix) validateStoragePath(prefix);
1694
+ const params = new URLSearchParams();
1695
+ if (prefix) params.set("prefix", prefix);
1696
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
1697
+ const query = params.toString();
1698
+ const res = await http.request(
1699
+ "GET",
1700
+ `/v1/storage/bucket/${bucketName}${query ? `?${query}` : ""}`
1701
+ );
1702
+ return mapResponse(res, (body) => (body?.objects ?? []).map((row) => toFileObject(bucketName, row)));
1703
+ },
1704
+ async remove(paths) {
1705
+ for (const p of paths) validateStoragePath(p);
1706
+ const removed = [];
1707
+ for (const p of paths) {
1708
+ const res = await http.request("DELETE", objectPath(p));
1709
+ if (res.error) return { data: null, error: res.error, status: res.status };
1710
+ removed.push(toFileObject(bucketName, { path: p }));
1711
+ }
1712
+ return { data: removed, error: null, status: 200 };
1713
+ },
1714
+ async move(from, to) {
1715
+ validateStoragePath(from);
1716
+ validateStoragePath(to);
1717
+ return http.request("POST", `/v1/storage/move/${bucketName}`, { body: { from, to } });
1718
+ },
1719
+ async copy(from, to) {
1720
+ validateStoragePath(from);
1721
+ validateStoragePath(to);
1722
+ return http.request("POST", `/v1/storage/copy/${bucketName}`, { body: { from, to } });
1723
+ }
1724
+ };
1725
+ }
1726
+ function buildStorageClient(http) {
1727
+ return {
1728
+ bucket(name) {
1729
+ if (!BUCKET_NAME_RE.test(name)) {
1730
+ throw new Error(`Invalid bucket name: "${name}". Bucket names must match ${BUCKET_NAME_RE.source}`);
1731
+ }
1732
+ return buildBucketClient(http, name);
1733
+ }
1734
+ };
1735
+ }
1736
+
1737
+ // src/clients/index.ts
1738
+ function buildModuleClients(cfg) {
1739
+ const baseUrl = (cfg.baseUrl || "").replace(/\/+$/, "");
1740
+ if (!baseUrl) return {};
1741
+ const apiKey = cfg.serviceRoleKey || cfg.apiKey || "";
1742
+ const http = makeHttpClient({ baseUrl, apiKey, fetchImpl: cfg.fetchImpl });
1743
+ return {
1744
+ Documents: buildDocumentsClient(http),
1745
+ Storage: buildStorageClient(http),
1746
+ Notifications: buildNotificationsClient(http),
1747
+ Flags: buildFlagsClient(http, { getCurrentUserId: cfg.getCurrentUserId }),
1748
+ Realtime: buildRealtimeClient({
1749
+ baseUrl,
1750
+ apiJwtSecret: cfg.realtimeApiJwtSecret,
1751
+ fetchImpl: cfg.fetchImpl
1752
+ })
1753
+ };
1754
+ }
1755
+
914
1756
  // src/purchases/keys-gen.ts
915
1757
  function liveKeys(entries) {
916
1758
  return Object.entries(entries ?? {}).filter(([, def]) => def?.removed !== true).map(([key]) => key).sort();
@@ -1020,6 +1862,89 @@ function defineSchema(input) {
1020
1862
  return { tables, extensions };
1021
1863
  }
1022
1864
 
1865
+ // src/db/schema-json.ts
1866
+ function defOf(column) {
1867
+ return "_def" in column ? column._def : column;
1868
+ }
1869
+ function columnToJSON(column) {
1870
+ const def = defOf(column);
1871
+ const out = {
1872
+ type: def.type,
1873
+ nullable: def.nullable,
1874
+ primaryKey: def.primaryKey
1875
+ };
1876
+ if (def.defaultValue !== void 0) out.defaultValue = def.defaultValue;
1877
+ if (def.defaultRandom === true) out.defaultRandom = true;
1878
+ if (def.defaultNow === true) out.defaultNow = true;
1879
+ if (def.renamedFrom !== void 0) out.renamedFrom = def.renamedFrom;
1880
+ if (def.references !== void 0) {
1881
+ out.references = { table: def.references.table, column: def.references.column };
1882
+ }
1883
+ if (def.onDeleteAction !== void 0) out.onDeleteAction = def.onDeleteAction;
1884
+ if (def.enumName !== void 0) out.enumName = def.enumName;
1885
+ if (def.enumValues !== void 0) out.enumValues = [...def.enumValues];
1886
+ if (def.unique === true) out.unique = true;
1887
+ return out;
1888
+ }
1889
+ function policyToJSON(policy2) {
1890
+ return {
1891
+ name: policy2.name,
1892
+ command: policy2.command ?? "all",
1893
+ roles: policy2.roles ? [...policy2.roles] : [],
1894
+ // null rather than omitted: a policy with no USING clause is a different
1895
+ // thing from one whose clause the emitter forgot, and Go reads the
1896
+ // difference.
1897
+ using: policy2.using ?? null,
1898
+ withCheck: policy2.withCheck ?? null,
1899
+ permissive: policy2.permissive !== false
1900
+ };
1901
+ }
1902
+ function tableToJSON(table) {
1903
+ const columns = {};
1904
+ for (const [name, column] of Object.entries(table.columns)) {
1905
+ columns[name] = columnToJSON(column);
1906
+ }
1907
+ const out = {
1908
+ name: table.name,
1909
+ columns,
1910
+ // Read, not re-derived. `defineSchema` already resolves the fail-closed
1911
+ // default (RLS on unless the author wrote `rls: false`, and forced on by any
1912
+ // policy), and a second copy of a SECURITY default is exactly the thing that
1913
+ // drifts — the direction it drifted last time was "expose everything", and
1914
+ // the live proof was one user reading another's rows.
1915
+ rls: table.rls,
1916
+ policies: (table.policies ?? []).map(policyToJSON)
1917
+ };
1918
+ if (table.primaryKey !== void 0 && table.primaryKey.length > 0) {
1919
+ out.primaryKey = [...table.primaryKey];
1920
+ }
1921
+ if (table.unique !== void 0 && table.unique.length > 0) {
1922
+ out.uniqueConstraints = table.unique.map((u) => ({ name: u.name, columns: [...u.columns] }));
1923
+ }
1924
+ if (table.raw !== void 0 && table.raw.length > 0) {
1925
+ out.rawConstraints = table.raw.map((r) => ({
1926
+ name: r.name,
1927
+ up: r.up,
1928
+ down: r.down ?? null
1929
+ }));
1930
+ }
1931
+ if (table.checks !== void 0 && table.checks.length > 0) {
1932
+ out.checks = table.checks.map((c) => ({ name: c.name, expr: c.expr }));
1933
+ }
1934
+ if (table.indexes !== void 0 && table.indexes.length > 0) {
1935
+ out.indexes = table.indexes.map((i) => ({ name: i.name, columns: [...i.columns] }));
1936
+ }
1937
+ return out;
1938
+ }
1939
+ function toSchemaJSON(schema) {
1940
+ const tables = {};
1941
+ for (const table of Object.values(schema.tables)) {
1942
+ const json = tableToJSON(table);
1943
+ tables[json.name] = json;
1944
+ }
1945
+ return { tables, extensions: [...new Set(schema.extensions ?? [])] };
1946
+ }
1947
+
1023
1948
  // src/db/extensions.ts
1024
1949
  var PALBASE_EXTENSIONS = [
1025
1950
  // Search & text
@@ -1387,6 +2312,7 @@ export {};
1387
2312
  // src/config/storage.ts
1388
2313
  var STORAGE_CONFIG_KIND = "storage";
1389
2314
  var VARIANT_FORMATS = ["webp", "avif", "jpeg", "png"];
2315
+ var VARIANT_FITS = ["cover", "contain", "inside"];
1390
2316
  function normalizeVariants(input) {
1391
2317
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
1392
2318
  throw new Error("bucket variants must be an object of { <name>: { width?, height?, format? } }");
@@ -1416,10 +2342,26 @@ function normalizeVariants(input) {
1416
2342
  `bucket variant "${name}" format must be one of ${VARIANT_FORMATS.join(", ")}, got "${String(spec.format)}"`
1417
2343
  );
1418
2344
  }
2345
+ if (spec.fit !== void 0 && !VARIANT_FITS.includes(spec.fit)) {
2346
+ throw new Error(
2347
+ `bucket variant "${name}" fit must be one of ${VARIANT_FITS.join(", ")}, got "${String(spec.fit)}"`
2348
+ );
2349
+ }
2350
+ if (spec.quality !== void 0) {
2351
+ const q = spec.quality;
2352
+ if (typeof q !== "number" || !Number.isInteger(q) || q < 1 || q > 100) {
2353
+ throw new Error(`bucket variant "${name}" quality must be an integer 1\u2013100, got ${String(q)}`);
2354
+ }
2355
+ }
2356
+ if (spec.fit === "cover" && (spec.width === void 0 || spec.height === void 0)) {
2357
+ throw new Error(`bucket variant "${name}" uses fit "cover", which needs BOTH width and height`);
2358
+ }
1419
2359
  out[name] = {
1420
2360
  ...spec.width !== void 0 ? { width: spec.width } : {},
1421
2361
  ...spec.height !== void 0 ? { height: spec.height } : {},
1422
- ...spec.format !== void 0 ? { format: spec.format } : {}
2362
+ ...spec.fit !== void 0 ? { fit: spec.fit } : {},
2363
+ ...spec.format !== void 0 ? { format: spec.format } : {},
2364
+ ...spec.quality !== void 0 ? { quality: spec.quality } : {}
1423
2365
  };
1424
2366
  }
1425
2367
  return out;
@@ -1950,6 +2892,57 @@ function defineFlags(input) {
1950
2892
  return { __config: FLAGS_CONFIG_KIND, flags };
1951
2893
  }
1952
2894
 
2895
+ // src/config/secrets.ts
2896
+ var SECRETS_CONFIG_KIND = "secrets";
2897
+ var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
2898
+ function secret(name, opts = {}) {
2899
+ if (typeof name !== "string" || !SECRET_NAME_RE.test(name)) {
2900
+ throw new Error(
2901
+ `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")`
2902
+ );
2903
+ }
2904
+ if (opts === null || typeof opts !== "object") {
2905
+ throw new Error(`secret("${name}") expects { required?, description? }`);
2906
+ }
2907
+ let required = true;
2908
+ if (opts.required !== void 0) {
2909
+ if (typeof opts.required !== "boolean") {
2910
+ throw new Error(`secret("${name}") required must be a boolean`);
2911
+ }
2912
+ required = opts.required;
2913
+ }
2914
+ let description = null;
2915
+ if (opts.description !== void 0) {
2916
+ if (typeof opts.description !== "string") {
2917
+ throw new Error(`secret("${name}") description must be a string`);
2918
+ }
2919
+ const trimmed = opts.description.trim();
2920
+ description = trimmed.length > 0 ? trimmed : null;
2921
+ }
2922
+ return { name, required, description };
2923
+ }
2924
+ function defineSecrets(input) {
2925
+ if (input === null || typeof input !== "object" || !Array.isArray(input.secrets)) {
2926
+ throw new Error('defineSecrets expects { secrets: [secret("NAME"), ...] }');
2927
+ }
2928
+ const byName = /* @__PURE__ */ new Map();
2929
+ for (const def of input.secrets) {
2930
+ if (def === null || typeof def !== "object" || typeof def.name !== "string" || typeof def.required !== "boolean") {
2931
+ throw new Error(
2932
+ `defineSecrets: every entry must come from secret() \u2014 got ${JSON.stringify(def)}`
2933
+ );
2934
+ }
2935
+ if (byName.has(def.name)) {
2936
+ throw new Error(
2937
+ `secret "${def.name}" is declared twice in config/secrets.ts \u2014 remove one; a name has exactly one requirement`
2938
+ );
2939
+ }
2940
+ byName.set(def.name, def);
2941
+ }
2942
+ const secrets = [...byName.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
2943
+ return { __config: SECRETS_CONFIG_KIND, secrets };
2944
+ }
2945
+
1953
2946
  // src/decorators/registry.ts
1954
2947
  var ROUTES = /* @__PURE__ */ Symbol.for("palbase.backend.routes");
1955
2948
  var PARAM_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.paramBuffer");
@@ -2044,6 +3037,21 @@ function getRoutes(ctor) {
2044
3037
 
2045
3038
  // src/decorators/controller.ts
2046
3039
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
3040
+ var REGISTRY = /* @__PURE__ */ Symbol.for("palbase.backend.allControllers");
3041
+ function registry() {
3042
+ const g = globalThis;
3043
+ const existing = g[REGISTRY];
3044
+ if (existing) return existing;
3045
+ const fresh = [];
3046
+ g[REGISTRY] = fresh;
3047
+ return fresh;
3048
+ }
3049
+ function getRegisteredControllers() {
3050
+ return registry().slice();
3051
+ }
3052
+ function __resetRegisteredControllers() {
3053
+ registry().length = 0;
3054
+ }
2047
3055
  var RESERVED_FIRST_SEGMENT = "webhooks";
2048
3056
  function assertNotReserved(path, subject) {
2049
3057
  const [first] = path.split("/").filter(Boolean);
@@ -2080,6 +3088,8 @@ function Controller(basePath, options = {}) {
2080
3088
  configurable: true,
2081
3089
  writable: false
2082
3090
  });
3091
+ const all = registry();
3092
+ if (!all.includes(ctor)) all.push(ctor);
2083
3093
  return ctor;
2084
3094
  };
2085
3095
  }
@@ -2261,8 +3271,8 @@ function defineError(code, status, dataSchema) {
2261
3271
  `defineError: status for "${code}" must be a 4xx/5xx integer, got ${status} \u2014 error responses must not clobber success responses in the project spec.`
2262
3272
  );
2263
3273
  }
2264
- const registry2 = getErrorRegistry();
2265
- const existing = registry2.get(code);
3274
+ const registry3 = getErrorRegistry();
3275
+ const existing = registry3.get(code);
2266
3276
  const className = defaultClassName(code);
2267
3277
  const dataDigest = dataSchema ? digestOf(code, dataSchema) : void 0;
2268
3278
  if (existing) {
@@ -2291,7 +3301,7 @@ function defineError(code, status, dataSchema) {
2291
3301
  };
2292
3302
  const cls = dataSchema ? makeWithData(dataSchema) : makeWithoutData();
2293
3303
  Object.defineProperty(cls, "name", { value: className });
2294
- registry2.set(code, {
3304
+ registry3.set(code, {
2295
3305
  code,
2296
3306
  status,
2297
3307
  className,
@@ -2548,16 +3558,16 @@ function hasSecrets(value) {
2548
3558
  const secrets = value.secrets;
2549
3559
  return secrets === void 0 || Array.isArray(secrets);
2550
3560
  }
2551
- var registry = [];
3561
+ var registry2 = [];
2552
3562
  function __registerResource(resource) {
2553
- registry.push({ resource, booted: false });
3563
+ registry2.push({ resource, booted: false });
2554
3564
  }
2555
3565
  function declaredSecrets(resource) {
2556
3566
  const ctor = resource.constructor;
2557
3567
  return hasSecrets(ctor) ? ctor.secrets ?? [] : [];
2558
3568
  }
2559
3569
  async function __runResourceBoot(envMap) {
2560
- for (const entry of registry) {
3570
+ for (const entry of registry2) {
2561
3571
  if (entry.booted) continue;
2562
3572
  const secrets = declaredSecrets(entry.resource);
2563
3573
  const env = {};
@@ -2575,13 +3585,13 @@ async function __runResourceBoot(envMap) {
2575
3585
  }
2576
3586
  }
2577
3587
  async function __shutdownResources() {
2578
- for (let i = registry.length - 1; i >= 0; i -= 1) {
2579
- const entry = registry[i];
3588
+ for (let i = registry2.length - 1; i >= 0; i -= 1) {
3589
+ const entry = registry2[i];
2580
3590
  if (entry.booted && entry.resource.shutdown) {
2581
3591
  await entry.resource.shutdown();
2582
3592
  }
2583
3593
  }
2584
- registry.length = 0;
3594
+ registry2.length = 0;
2585
3595
  }
2586
3596
 
2587
3597
  // src/hooks.ts
@@ -2654,6 +3664,7 @@ var import_zod2 = require("zod");
2654
3664
  PALBASE_EXTENSIONS,
2655
3665
  PROVIDER_CATALOG,
2656
3666
  PalError,
3667
+ PalbaseModuleError,
2657
3668
  Param,
2658
3669
  Patch,
2659
3670
  PolicyBuilder,
@@ -2668,7 +3679,9 @@ var import_zod2 = require("zod");
2668
3679
  RequestId,
2669
3680
  RequireEntitlement,
2670
3681
  Resource,
3682
+ SECRETS_CONFIG_KIND,
2671
3683
  STORAGE_CONFIG_KIND,
3684
+ Secrets,
2672
3685
  Spend,
2673
3686
  Storage,
2674
3687
  TEST_USERS_CONFIG_KIND,
@@ -2684,6 +3697,7 @@ var import_zod2 = require("zod");
2684
3697
  __getRuntime,
2685
3698
  __registerResource,
2686
3699
  __requestALS,
3700
+ __resetRegisteredControllers,
2687
3701
  __runResourceBoot,
2688
3702
  __runWithRuntime,
2689
3703
  __setRuntime,
@@ -2692,6 +3706,7 @@ var import_zod2 = require("zod");
2692
3706
  bigint,
2693
3707
  boolean,
2694
3708
  bucket,
3709
+ buildModuleClients,
2695
3710
  buildProvider,
2696
3711
  dec,
2697
3712
  defineEgress,
@@ -2700,6 +3715,7 @@ var import_zod2 = require("zod");
2700
3715
  defineMiddleware,
2701
3716
  defineNotifications,
2702
3717
  defineSchema,
3718
+ defineSecrets,
2703
3719
  defineStorage,
2704
3720
  defineTestUsers,
2705
3721
  documents,
@@ -2709,6 +3725,7 @@ var import_zod2 = require("zod");
2709
3725
  flag,
2710
3726
  getErrorRegistry,
2711
3727
  getJobConfig,
3728
+ getRegisteredControllers,
2712
3729
  getRoutes,
2713
3730
  getWebhookConfig,
2714
3731
  inc,
@@ -2716,6 +3733,7 @@ var import_zod2 = require("zod");
2716
3733
  isPalbaseExtension,
2717
3734
  jsonb,
2718
3735
  makeEnvDts,
3736
+ makeHttpClient,
2719
3737
  makePurchasesDts,
2720
3738
  makeTypedDB,
2721
3739
  now,
@@ -2725,11 +3743,13 @@ var import_zod2 = require("zod");
2725
3743
  raw,
2726
3744
  recordThrows,
2727
3745
  reservedSecretKey,
3746
+ secret,
2728
3747
  spendFor,
2729
3748
  storage,
2730
3749
  testUser,
2731
3750
  text,
2732
3751
  timestamp,
3752
+ toSchemaJSON,
2733
3753
  uuid,
2734
3754
  validateUploadAgainstStorage,
2735
3755
  z