@palbase/backend 4.0.0 → 5.1.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.
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  text,
28
28
  timestamp,
29
29
  uuid
30
- } from "./chunk-EG7TTYHY.js";
30
+ } from "./chunk-2N4YNN6F.js";
31
31
 
32
32
  // src/db/env-gen.ts
33
33
  function baseTsType(def) {
@@ -98,6 +98,277 @@ export {};
98
98
  `;
99
99
  }
100
100
 
101
+ // src/config/storage.ts
102
+ var STORAGE_CONFIG_KIND = "storage";
103
+ var UNIT_BYTES = {
104
+ b: 1,
105
+ kb: 1024,
106
+ mb: 1024 ** 2,
107
+ gb: 1024 ** 3,
108
+ tb: 1024 ** 4
109
+ };
110
+ function parseFileSizeLimit(input) {
111
+ if (typeof input === "number") {
112
+ if (!Number.isFinite(input) || input < 0 || !Number.isInteger(input)) {
113
+ throw new Error(
114
+ `bucket fileSizeLimit must be a non-negative integer number of bytes, got ${input}`
115
+ );
116
+ }
117
+ return input;
118
+ }
119
+ const trimmed = input.trim();
120
+ const match = /^(\d+(?:\.\d+)?)\s*([a-zA-Z]+)?$/.exec(trimmed);
121
+ if (!match) {
122
+ throw new Error(
123
+ `bucket fileSizeLimit string must be "<number><unit>" like "5MB" or "1GB", got "${input}"`
124
+ );
125
+ }
126
+ const value = Number.parseFloat(match[1]);
127
+ const unit = (match[2] ?? "b").toLowerCase();
128
+ const multiplier = UNIT_BYTES[unit];
129
+ if (multiplier === void 0) {
130
+ throw new Error(
131
+ `bucket fileSizeLimit has an unknown unit "${match[2]}" \u2014 use B, KB, MB, GB, or TB (e.g. "5MB")`
132
+ );
133
+ }
134
+ const bytes = value * multiplier;
135
+ if (!Number.isFinite(bytes) || bytes < 0) {
136
+ throw new Error(`bucket fileSizeLimit resolved to an invalid byte count: ${bytes}`);
137
+ }
138
+ return Math.round(bytes);
139
+ }
140
+ var MIME_RE = /^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*\/(?:\*|[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*)$/;
141
+ function bucket(opts = {}) {
142
+ const fileSizeLimit = opts.fileSizeLimit === void 0 ? null : parseFileSizeLimit(opts.fileSizeLimit);
143
+ let allowedMimeTypes = null;
144
+ if (opts.allowedMimeTypes !== void 0) {
145
+ if (!Array.isArray(opts.allowedMimeTypes)) {
146
+ throw new Error("bucket allowedMimeTypes must be an array of MIME-type strings");
147
+ }
148
+ for (const mime of opts.allowedMimeTypes) {
149
+ if (typeof mime !== "string" || !MIME_RE.test(mime.trim())) {
150
+ throw new Error(
151
+ `bucket allowedMimeTypes entry "${mime}" is not a valid MIME type (expected "type/subtype", e.g. "image/png")`
152
+ );
153
+ }
154
+ }
155
+ allowedMimeTypes = [...new Set(opts.allowedMimeTypes.map((m) => m.trim()))];
156
+ }
157
+ return {
158
+ public: opts.public ?? false,
159
+ fileSizeLimit,
160
+ allowedMimeTypes
161
+ };
162
+ }
163
+ function defineStorage(input) {
164
+ if (input === null || typeof input !== "object" || typeof input.buckets !== "object") {
165
+ throw new Error("defineStorage expects { buckets: { <name>: bucket({...}) } }");
166
+ }
167
+ const buckets = {};
168
+ for (const name of Object.keys(input.buckets)) {
169
+ if (name.length === 0) {
170
+ throw new Error("bucket name must be a non-empty string");
171
+ }
172
+ const def = input.buckets[name];
173
+ if (def === void 0) continue;
174
+ buckets[name] = def;
175
+ }
176
+ return { __config: STORAGE_CONFIG_KIND, buckets };
177
+ }
178
+
179
+ // src/config/notifications.ts
180
+ var NOTIFICATIONS_CONFIG_KIND = "notifications";
181
+ var RESERVED_SECRET_PREFIX = "PB_NOTIFICATIONS";
182
+ var PROVIDER_CATALOG = {
183
+ apns: {
184
+ channel: "push",
185
+ required: ["teamId", "keyId", "bundleId"],
186
+ optional: ["isProduction"],
187
+ secrets: ["p8"]
188
+ },
189
+ fcm: {
190
+ channel: "push",
191
+ required: [],
192
+ optional: [],
193
+ secrets: ["serviceAccount"]
194
+ },
195
+ sendgrid: {
196
+ channel: "email",
197
+ required: ["fromDomain"],
198
+ optional: [],
199
+ secrets: ["apiKey"]
200
+ },
201
+ ses: {
202
+ channel: "email",
203
+ required: ["region", "accessKeyId", "fromDomain"],
204
+ optional: [],
205
+ secrets: ["secretAccessKey"]
206
+ },
207
+ smtp: {
208
+ channel: "email",
209
+ required: ["host", "port", "fromEmail"],
210
+ optional: ["username", "useStarttls"],
211
+ secrets: ["password"]
212
+ },
213
+ acs: {
214
+ channel: "email",
215
+ required: ["fromEmail"],
216
+ optional: ["fromName"],
217
+ secrets: ["connectionString"]
218
+ },
219
+ twilio: {
220
+ channel: "sms",
221
+ // account_sid required; one of from_number / messaging_service_sid required
222
+ // (enforced by the refine in buildProvider, not by the flat `required` list).
223
+ required: ["accountSid"],
224
+ optional: ["fromNumber", "messagingServiceSid"],
225
+ secrets: ["authToken"]
226
+ }
227
+ };
228
+ function buildProvider(name, opts) {
229
+ const entry = PROVIDER_CATALOG[name];
230
+ const src = { ...opts };
231
+ const enabled = src.enabled === void 0 ? false : Boolean(src.enabled);
232
+ if (!enabled) {
233
+ return { enabled: false };
234
+ }
235
+ const def = { enabled: true };
236
+ for (const field of entry.required) {
237
+ const value = src[field];
238
+ if (value === void 0 || value === null || value === "") {
239
+ throw new Error(
240
+ `notifications: provider "${name}" is enabled but missing required field "${field}"`
241
+ );
242
+ }
243
+ def[field] = value;
244
+ }
245
+ for (const field of entry.optional) {
246
+ if (src[field] !== void 0) {
247
+ def[field] = src[field];
248
+ }
249
+ }
250
+ if (name === "twilio") {
251
+ const hasFrom = Boolean(def.fromNumber);
252
+ const hasMsg = Boolean(def.messagingServiceSid);
253
+ if (!hasFrom && !hasMsg) {
254
+ throw new Error(
255
+ 'notifications: provider "twilio" requires one of "fromNumber" or "messagingServiceSid"'
256
+ );
257
+ }
258
+ }
259
+ return def;
260
+ }
261
+ function defineNotifications(input) {
262
+ if (input === null || typeof input !== "object") {
263
+ throw new Error("defineNotifications expects { push?, email?, sms? }");
264
+ }
265
+ const config = {
266
+ __config: NOTIFICATIONS_CONFIG_KIND,
267
+ push: {},
268
+ email: {},
269
+ sms: {}
270
+ };
271
+ const push = input.push ?? {};
272
+ if (push.apns !== void 0) config.push.apns = buildProvider("apns", push.apns);
273
+ if (push.fcm !== void 0) config.push.fcm = buildProvider("fcm", push.fcm);
274
+ const email = input.email ?? {};
275
+ if (email.sendgrid !== void 0) config.email.sendgrid = buildProvider("sendgrid", email.sendgrid);
276
+ if (email.ses !== void 0) config.email.ses = buildProvider("ses", email.ses);
277
+ if (email.smtp !== void 0) config.email.smtp = buildProvider("smtp", email.smtp);
278
+ if (email.acs !== void 0) config.email.acs = buildProvider("acs", email.acs);
279
+ const sms = input.sms ?? {};
280
+ if (sms.twilio !== void 0) config.sms.twilio = buildProvider("twilio", sms.twilio);
281
+ return config;
282
+ }
283
+ function reservedSecretKey(provider, secretField) {
284
+ return `${RESERVED_SECRET_PREFIX}_${camelToUpperSnake(provider)}_${camelToUpperSnake(secretField)}`;
285
+ }
286
+ function camelToUpperSnake(s) {
287
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
288
+ }
289
+
290
+ // src/config/flags.ts
291
+ var FLAGS_CONFIG_KIND = "flags";
292
+ var FLAG_KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
293
+ function valueMatchesType(type, value) {
294
+ switch (type) {
295
+ case "boolean":
296
+ return typeof value === "boolean";
297
+ case "number":
298
+ return typeof value === "number" && Number.isFinite(value);
299
+ case "string":
300
+ return typeof value === "string";
301
+ }
302
+ }
303
+ function flag(opts) {
304
+ if (opts === null || typeof opts !== "object") {
305
+ throw new Error("flag() expects { type, default, variants?, description? }");
306
+ }
307
+ const { type } = opts;
308
+ if (type !== "boolean" && type !== "number" && type !== "string") {
309
+ throw new Error(`flag type must be "boolean", "number", or "string", got ${JSON.stringify(type)}`);
310
+ }
311
+ if (!valueMatchesType(type, opts.default)) {
312
+ throw new Error(
313
+ `flag default ${JSON.stringify(opts.default)} does not match type "${type}"`
314
+ );
315
+ }
316
+ let variants = null;
317
+ if (opts.variants !== void 0) {
318
+ if (type !== "string") {
319
+ throw new Error(`flag variants are only valid for type "string" (got type "${type}")`);
320
+ }
321
+ if (!Array.isArray(opts.variants)) {
322
+ throw new Error("flag variants must be an array of strings");
323
+ }
324
+ for (const v of opts.variants) {
325
+ if (typeof v !== "string" || v.length === 0) {
326
+ throw new Error(`flag variant ${JSON.stringify(v)} must be a non-empty string`);
327
+ }
328
+ }
329
+ variants = [...new Set(opts.variants)];
330
+ if (variants.length === 0) {
331
+ throw new Error("flag variants must be a non-empty list when supplied");
332
+ }
333
+ if (!variants.includes(opts.default)) {
334
+ throw new Error(
335
+ `flag default ${JSON.stringify(opts.default)} is not one of its variants [${variants.map((v) => JSON.stringify(v)).join(", ")}]`
336
+ );
337
+ }
338
+ }
339
+ let description = null;
340
+ if (opts.description !== void 0) {
341
+ if (typeof opts.description !== "string") {
342
+ throw new Error("flag description must be a string");
343
+ }
344
+ const trimmed = opts.description.trim();
345
+ description = trimmed.length > 0 ? trimmed : null;
346
+ }
347
+ return {
348
+ type,
349
+ default: opts.default,
350
+ variants,
351
+ description
352
+ };
353
+ }
354
+ function defineFlags(input) {
355
+ if (input === null || typeof input !== "object" || typeof input.flags !== "object") {
356
+ throw new Error("defineFlags expects { flags: { <key>: flag({...}) } }");
357
+ }
358
+ const flags = {};
359
+ for (const key of Object.keys(input.flags)) {
360
+ if (!FLAG_KEY_RE.test(key)) {
361
+ throw new Error(
362
+ `flag key ${JSON.stringify(key)} is invalid \u2014 must start with a letter and contain only letters, digits, and underscores`
363
+ );
364
+ }
365
+ const def = input.flags[key];
366
+ if (def === void 0) continue;
367
+ flags[key] = def;
368
+ }
369
+ return { __config: FLAGS_CONFIG_KIND, flags };
370
+ }
371
+
101
372
  // src/decorators/controller.ts
102
373
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
103
374
  function Controller(basePath, options = {}) {
@@ -127,6 +398,7 @@ function Controller(basePath, options = {}) {
127
398
  // src/decorators/registry.ts
128
399
  var ROUTES = /* @__PURE__ */ Symbol.for("palbase.backend.routes");
129
400
  var PARAM_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.paramBuffer");
401
+ var RETURN_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.returnBuffer");
130
402
  function carrierOf(target) {
131
403
  const ctor = typeof target === "function" ? target : target.constructor ?? target;
132
404
  return ctor;
@@ -148,7 +420,12 @@ function recordRoute(target, fnName, method, subpath, options) {
148
420
  const routes = ownRoutes(carrier);
149
421
  const buffer = ownParamBuffer(carrier);
150
422
  const params = (buffer[fnName] ?? []).slice().sort((a, b) => a.index - b.index);
151
- routes.push({ method, subpath, fnName, options, params });
423
+ const route = { method, subpath, fnName, options, params };
424
+ const returnBuffer = carrier[RETURN_BUFFER];
425
+ if (returnBuffer && returnBuffer[fnName] !== void 0) {
426
+ route.returnSchema = returnBuffer[fnName];
427
+ }
428
+ routes.push(route);
152
429
  }
153
430
  function recordParam(target, fnName, meta) {
154
431
  const carrier = carrierOf(target);
@@ -163,20 +440,6 @@ function recordParam(target, fnName, meta) {
163
440
  }
164
441
  }
165
442
  }
166
- var RETURN_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.returnBuffer");
167
- function recordReturn(target, fnName, schema) {
168
- const carrier = carrierOf(target);
169
- const routes = carrier[ROUTES];
170
- const route = routes?.find((r) => r.fnName === fnName);
171
- if (route) {
172
- route.returnSchema = schema;
173
- return;
174
- }
175
- if (!Object.prototype.hasOwnProperty.call(carrier, RETURN_BUFFER)) {
176
- carrier[RETURN_BUFFER] = {};
177
- }
178
- carrier[RETURN_BUFFER][fnName] = schema;
179
- }
180
443
 
181
444
  // src/decorators/methods.ts
182
445
  function makeMethodDecorator(method) {
@@ -191,11 +454,6 @@ var Post = makeMethodDecorator("POST");
191
454
  var Put = makeMethodDecorator("PUT");
192
455
  var Patch = makeMethodDecorator("PATCH");
193
456
  var Delete = makeMethodDecorator("DELETE");
194
- function Returns(schema) {
195
- return function(target, propertyKey) {
196
- recordReturn(target, String(propertyKey), schema);
197
- };
198
- }
199
457
 
200
458
  // src/decorators/params.ts
201
459
  function makeParamDecorator(kind, extra) {
@@ -638,16 +896,19 @@ export {
638
896
  Delete,
639
897
  Documents,
640
898
  EXTENSION_DEPENDENCIES,
899
+ FLAGS_CONFIG_KIND,
641
900
  Flags,
642
901
  Forbidden,
643
902
  Get,
644
903
  Headers,
645
904
  HttpError,
646
905
  Log,
906
+ NOTIFICATIONS_CONFIG_KIND,
647
907
  NotFound,
648
908
  Notifications,
649
909
  OptionalUser,
650
910
  PALBASE_EXTENSIONS,
911
+ PROVIDER_CATALOG,
651
912
  PalError,
652
913
  Param,
653
914
  Patch,
@@ -656,10 +917,11 @@ export {
656
917
  Put,
657
918
  Query,
658
919
  Queue,
920
+ RESERVED_SECRET_PREFIX,
659
921
  Req,
660
922
  RequestId,
661
923
  Resource,
662
- Returns,
924
+ STORAGE_CONFIG_KIND,
663
925
  Storage,
664
926
  TooManyRequests,
665
927
  TraceId,
@@ -674,19 +936,27 @@ export {
674
936
  __shutdownResources,
675
937
  auth,
676
938
  boolean,
939
+ bucket,
940
+ buildProvider,
941
+ defineFlags,
677
942
  defineJob,
678
943
  defineMiddleware,
944
+ defineNotifications,
679
945
  defineSchema,
946
+ defineStorage,
680
947
  defineWebhook,
681
948
  defineWorker,
682
949
  documents,
683
950
  enumType,
951
+ flag,
684
952
  integer,
685
953
  isPalbaseExtension,
686
954
  jsonb,
687
955
  makeEnvDts,
688
956
  makeTypedDB,
957
+ parseFileSizeLimit,
689
958
  policy,
959
+ reservedSecretKey,
690
960
  storage,
691
961
  text,
692
962
  timestamp,