@palbase/backend 5.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.cjs CHANGED
@@ -30,16 +30,19 @@ __export(src_exports, {
30
30
  Delete: () => Delete,
31
31
  Documents: () => Documents,
32
32
  EXTENSION_DEPENDENCIES: () => EXTENSION_DEPENDENCIES,
33
+ FLAGS_CONFIG_KIND: () => FLAGS_CONFIG_KIND,
33
34
  Flags: () => Flags,
34
35
  Forbidden: () => Forbidden,
35
36
  Get: () => Get,
36
37
  Headers: () => Headers,
37
38
  HttpError: () => HttpError,
38
39
  Log: () => Log,
40
+ NOTIFICATIONS_CONFIG_KIND: () => NOTIFICATIONS_CONFIG_KIND,
39
41
  NotFound: () => NotFound,
40
42
  Notifications: () => Notifications,
41
43
  OptionalUser: () => OptionalUser,
42
44
  PALBASE_EXTENSIONS: () => PALBASE_EXTENSIONS,
45
+ PROVIDER_CATALOG: () => PROVIDER_CATALOG,
43
46
  PalError: () => PalError,
44
47
  Param: () => Param,
45
48
  Patch: () => Patch,
@@ -48,9 +51,11 @@ __export(src_exports, {
48
51
  Put: () => Put,
49
52
  Query: () => Query,
50
53
  Queue: () => Queue,
54
+ RESERVED_SECRET_PREFIX: () => RESERVED_SECRET_PREFIX,
51
55
  Req: () => Req,
52
56
  RequestId: () => RequestId,
53
57
  Resource: () => Resource,
58
+ STORAGE_CONFIG_KIND: () => STORAGE_CONFIG_KIND,
54
59
  Storage: () => Storage,
55
60
  TooManyRequests: () => TooManyRequests,
56
61
  TraceId: () => TraceId,
@@ -65,19 +70,27 @@ __export(src_exports, {
65
70
  __shutdownResources: () => __shutdownResources,
66
71
  auth: () => auth,
67
72
  boolean: () => boolean,
73
+ bucket: () => bucket,
74
+ buildProvider: () => buildProvider,
75
+ defineFlags: () => defineFlags,
68
76
  defineJob: () => defineJob,
69
77
  defineMiddleware: () => defineMiddleware,
78
+ defineNotifications: () => defineNotifications,
70
79
  defineSchema: () => defineSchema,
80
+ defineStorage: () => defineStorage,
71
81
  defineWebhook: () => defineWebhook,
72
82
  defineWorker: () => defineWorker,
73
83
  documents: () => documents,
74
84
  enumType: () => enumType,
85
+ flag: () => flag,
75
86
  integer: () => integer,
76
87
  isPalbaseExtension: () => isPalbaseExtension,
77
88
  jsonb: () => jsonb,
78
89
  makeEnvDts: () => makeEnvDts,
79
90
  makeTypedDB: () => makeTypedDB,
91
+ parseFileSizeLimit: () => parseFileSizeLimit,
80
92
  policy: () => policy,
93
+ reservedSecretKey: () => reservedSecretKey,
81
94
  storage: () => storage,
82
95
  text: () => text,
83
96
  timestamp: () => timestamp,
@@ -459,6 +472,277 @@ export {};
459
472
  `;
460
473
  }
461
474
 
475
+ // src/config/storage.ts
476
+ var STORAGE_CONFIG_KIND = "storage";
477
+ var UNIT_BYTES = {
478
+ b: 1,
479
+ kb: 1024,
480
+ mb: 1024 ** 2,
481
+ gb: 1024 ** 3,
482
+ tb: 1024 ** 4
483
+ };
484
+ function parseFileSizeLimit(input) {
485
+ if (typeof input === "number") {
486
+ if (!Number.isFinite(input) || input < 0 || !Number.isInteger(input)) {
487
+ throw new Error(
488
+ `bucket fileSizeLimit must be a non-negative integer number of bytes, got ${input}`
489
+ );
490
+ }
491
+ return input;
492
+ }
493
+ const trimmed = input.trim();
494
+ const match = /^(\d+(?:\.\d+)?)\s*([a-zA-Z]+)?$/.exec(trimmed);
495
+ if (!match) {
496
+ throw new Error(
497
+ `bucket fileSizeLimit string must be "<number><unit>" like "5MB" or "1GB", got "${input}"`
498
+ );
499
+ }
500
+ const value = Number.parseFloat(match[1]);
501
+ const unit = (match[2] ?? "b").toLowerCase();
502
+ const multiplier = UNIT_BYTES[unit];
503
+ if (multiplier === void 0) {
504
+ throw new Error(
505
+ `bucket fileSizeLimit has an unknown unit "${match[2]}" \u2014 use B, KB, MB, GB, or TB (e.g. "5MB")`
506
+ );
507
+ }
508
+ const bytes = value * multiplier;
509
+ if (!Number.isFinite(bytes) || bytes < 0) {
510
+ throw new Error(`bucket fileSizeLimit resolved to an invalid byte count: ${bytes}`);
511
+ }
512
+ return Math.round(bytes);
513
+ }
514
+ var MIME_RE = /^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*\/(?:\*|[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*)$/;
515
+ function bucket(opts = {}) {
516
+ const fileSizeLimit = opts.fileSizeLimit === void 0 ? null : parseFileSizeLimit(opts.fileSizeLimit);
517
+ let allowedMimeTypes = null;
518
+ if (opts.allowedMimeTypes !== void 0) {
519
+ if (!Array.isArray(opts.allowedMimeTypes)) {
520
+ throw new Error("bucket allowedMimeTypes must be an array of MIME-type strings");
521
+ }
522
+ for (const mime of opts.allowedMimeTypes) {
523
+ if (typeof mime !== "string" || !MIME_RE.test(mime.trim())) {
524
+ throw new Error(
525
+ `bucket allowedMimeTypes entry "${mime}" is not a valid MIME type (expected "type/subtype", e.g. "image/png")`
526
+ );
527
+ }
528
+ }
529
+ allowedMimeTypes = [...new Set(opts.allowedMimeTypes.map((m) => m.trim()))];
530
+ }
531
+ return {
532
+ public: opts.public ?? false,
533
+ fileSizeLimit,
534
+ allowedMimeTypes
535
+ };
536
+ }
537
+ function defineStorage(input) {
538
+ if (input === null || typeof input !== "object" || typeof input.buckets !== "object") {
539
+ throw new Error("defineStorage expects { buckets: { <name>: bucket({...}) } }");
540
+ }
541
+ const buckets = {};
542
+ for (const name of Object.keys(input.buckets)) {
543
+ if (name.length === 0) {
544
+ throw new Error("bucket name must be a non-empty string");
545
+ }
546
+ const def = input.buckets[name];
547
+ if (def === void 0) continue;
548
+ buckets[name] = def;
549
+ }
550
+ return { __config: STORAGE_CONFIG_KIND, buckets };
551
+ }
552
+
553
+ // src/config/notifications.ts
554
+ var NOTIFICATIONS_CONFIG_KIND = "notifications";
555
+ var RESERVED_SECRET_PREFIX = "PB_NOTIFICATIONS";
556
+ var PROVIDER_CATALOG = {
557
+ apns: {
558
+ channel: "push",
559
+ required: ["teamId", "keyId", "bundleId"],
560
+ optional: ["isProduction"],
561
+ secrets: ["p8"]
562
+ },
563
+ fcm: {
564
+ channel: "push",
565
+ required: [],
566
+ optional: [],
567
+ secrets: ["serviceAccount"]
568
+ },
569
+ sendgrid: {
570
+ channel: "email",
571
+ required: ["fromDomain"],
572
+ optional: [],
573
+ secrets: ["apiKey"]
574
+ },
575
+ ses: {
576
+ channel: "email",
577
+ required: ["region", "accessKeyId", "fromDomain"],
578
+ optional: [],
579
+ secrets: ["secretAccessKey"]
580
+ },
581
+ smtp: {
582
+ channel: "email",
583
+ required: ["host", "port", "fromEmail"],
584
+ optional: ["username", "useStarttls"],
585
+ secrets: ["password"]
586
+ },
587
+ acs: {
588
+ channel: "email",
589
+ required: ["fromEmail"],
590
+ optional: ["fromName"],
591
+ secrets: ["connectionString"]
592
+ },
593
+ twilio: {
594
+ channel: "sms",
595
+ // account_sid required; one of from_number / messaging_service_sid required
596
+ // (enforced by the refine in buildProvider, not by the flat `required` list).
597
+ required: ["accountSid"],
598
+ optional: ["fromNumber", "messagingServiceSid"],
599
+ secrets: ["authToken"]
600
+ }
601
+ };
602
+ function buildProvider(name, opts) {
603
+ const entry = PROVIDER_CATALOG[name];
604
+ const src = { ...opts };
605
+ const enabled = src.enabled === void 0 ? false : Boolean(src.enabled);
606
+ if (!enabled) {
607
+ return { enabled: false };
608
+ }
609
+ const def = { enabled: true };
610
+ for (const field of entry.required) {
611
+ const value = src[field];
612
+ if (value === void 0 || value === null || value === "") {
613
+ throw new Error(
614
+ `notifications: provider "${name}" is enabled but missing required field "${field}"`
615
+ );
616
+ }
617
+ def[field] = value;
618
+ }
619
+ for (const field of entry.optional) {
620
+ if (src[field] !== void 0) {
621
+ def[field] = src[field];
622
+ }
623
+ }
624
+ if (name === "twilio") {
625
+ const hasFrom = Boolean(def.fromNumber);
626
+ const hasMsg = Boolean(def.messagingServiceSid);
627
+ if (!hasFrom && !hasMsg) {
628
+ throw new Error(
629
+ 'notifications: provider "twilio" requires one of "fromNumber" or "messagingServiceSid"'
630
+ );
631
+ }
632
+ }
633
+ return def;
634
+ }
635
+ function defineNotifications(input) {
636
+ if (input === null || typeof input !== "object") {
637
+ throw new Error("defineNotifications expects { push?, email?, sms? }");
638
+ }
639
+ const config = {
640
+ __config: NOTIFICATIONS_CONFIG_KIND,
641
+ push: {},
642
+ email: {},
643
+ sms: {}
644
+ };
645
+ const push = input.push ?? {};
646
+ if (push.apns !== void 0) config.push.apns = buildProvider("apns", push.apns);
647
+ if (push.fcm !== void 0) config.push.fcm = buildProvider("fcm", push.fcm);
648
+ const email = input.email ?? {};
649
+ if (email.sendgrid !== void 0) config.email.sendgrid = buildProvider("sendgrid", email.sendgrid);
650
+ if (email.ses !== void 0) config.email.ses = buildProvider("ses", email.ses);
651
+ if (email.smtp !== void 0) config.email.smtp = buildProvider("smtp", email.smtp);
652
+ if (email.acs !== void 0) config.email.acs = buildProvider("acs", email.acs);
653
+ const sms = input.sms ?? {};
654
+ if (sms.twilio !== void 0) config.sms.twilio = buildProvider("twilio", sms.twilio);
655
+ return config;
656
+ }
657
+ function reservedSecretKey(provider, secretField) {
658
+ return `${RESERVED_SECRET_PREFIX}_${camelToUpperSnake(provider)}_${camelToUpperSnake(secretField)}`;
659
+ }
660
+ function camelToUpperSnake(s) {
661
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
662
+ }
663
+
664
+ // src/config/flags.ts
665
+ var FLAGS_CONFIG_KIND = "flags";
666
+ var FLAG_KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
667
+ function valueMatchesType(type, value) {
668
+ switch (type) {
669
+ case "boolean":
670
+ return typeof value === "boolean";
671
+ case "number":
672
+ return typeof value === "number" && Number.isFinite(value);
673
+ case "string":
674
+ return typeof value === "string";
675
+ }
676
+ }
677
+ function flag(opts) {
678
+ if (opts === null || typeof opts !== "object") {
679
+ throw new Error("flag() expects { type, default, variants?, description? }");
680
+ }
681
+ const { type } = opts;
682
+ if (type !== "boolean" && type !== "number" && type !== "string") {
683
+ throw new Error(`flag type must be "boolean", "number", or "string", got ${JSON.stringify(type)}`);
684
+ }
685
+ if (!valueMatchesType(type, opts.default)) {
686
+ throw new Error(
687
+ `flag default ${JSON.stringify(opts.default)} does not match type "${type}"`
688
+ );
689
+ }
690
+ let variants = null;
691
+ if (opts.variants !== void 0) {
692
+ if (type !== "string") {
693
+ throw new Error(`flag variants are only valid for type "string" (got type "${type}")`);
694
+ }
695
+ if (!Array.isArray(opts.variants)) {
696
+ throw new Error("flag variants must be an array of strings");
697
+ }
698
+ for (const v of opts.variants) {
699
+ if (typeof v !== "string" || v.length === 0) {
700
+ throw new Error(`flag variant ${JSON.stringify(v)} must be a non-empty string`);
701
+ }
702
+ }
703
+ variants = [...new Set(opts.variants)];
704
+ if (variants.length === 0) {
705
+ throw new Error("flag variants must be a non-empty list when supplied");
706
+ }
707
+ if (!variants.includes(opts.default)) {
708
+ throw new Error(
709
+ `flag default ${JSON.stringify(opts.default)} is not one of its variants [${variants.map((v) => JSON.stringify(v)).join(", ")}]`
710
+ );
711
+ }
712
+ }
713
+ let description = null;
714
+ if (opts.description !== void 0) {
715
+ if (typeof opts.description !== "string") {
716
+ throw new Error("flag description must be a string");
717
+ }
718
+ const trimmed = opts.description.trim();
719
+ description = trimmed.length > 0 ? trimmed : null;
720
+ }
721
+ return {
722
+ type,
723
+ default: opts.default,
724
+ variants,
725
+ description
726
+ };
727
+ }
728
+ function defineFlags(input) {
729
+ if (input === null || typeof input !== "object" || typeof input.flags !== "object") {
730
+ throw new Error("defineFlags expects { flags: { <key>: flag({...}) } }");
731
+ }
732
+ const flags = {};
733
+ for (const key of Object.keys(input.flags)) {
734
+ if (!FLAG_KEY_RE.test(key)) {
735
+ throw new Error(
736
+ `flag key ${JSON.stringify(key)} is invalid \u2014 must start with a letter and contain only letters, digits, and underscores`
737
+ );
738
+ }
739
+ const def = input.flags[key];
740
+ if (def === void 0) continue;
741
+ flags[key] = def;
742
+ }
743
+ return { __config: FLAGS_CONFIG_KIND, flags };
744
+ }
745
+
462
746
  // src/decorators/controller.ts
463
747
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
464
748
  function Controller(basePath, options = {}) {
@@ -987,16 +1271,19 @@ var import_zod = require("zod");
987
1271
  Delete,
988
1272
  Documents,
989
1273
  EXTENSION_DEPENDENCIES,
1274
+ FLAGS_CONFIG_KIND,
990
1275
  Flags,
991
1276
  Forbidden,
992
1277
  Get,
993
1278
  Headers,
994
1279
  HttpError,
995
1280
  Log,
1281
+ NOTIFICATIONS_CONFIG_KIND,
996
1282
  NotFound,
997
1283
  Notifications,
998
1284
  OptionalUser,
999
1285
  PALBASE_EXTENSIONS,
1286
+ PROVIDER_CATALOG,
1000
1287
  PalError,
1001
1288
  Param,
1002
1289
  Patch,
@@ -1005,9 +1292,11 @@ var import_zod = require("zod");
1005
1292
  Put,
1006
1293
  Query,
1007
1294
  Queue,
1295
+ RESERVED_SECRET_PREFIX,
1008
1296
  Req,
1009
1297
  RequestId,
1010
1298
  Resource,
1299
+ STORAGE_CONFIG_KIND,
1011
1300
  Storage,
1012
1301
  TooManyRequests,
1013
1302
  TraceId,
@@ -1022,19 +1311,27 @@ var import_zod = require("zod");
1022
1311
  __shutdownResources,
1023
1312
  auth,
1024
1313
  boolean,
1314
+ bucket,
1315
+ buildProvider,
1316
+ defineFlags,
1025
1317
  defineJob,
1026
1318
  defineMiddleware,
1319
+ defineNotifications,
1027
1320
  defineSchema,
1321
+ defineStorage,
1028
1322
  defineWebhook,
1029
1323
  defineWorker,
1030
1324
  documents,
1031
1325
  enumType,
1326
+ flag,
1032
1327
  integer,
1033
1328
  isPalbaseExtension,
1034
1329
  jsonb,
1035
1330
  makeEnvDts,
1036
1331
  makeTypedDB,
1332
+ parseFileSizeLimit,
1037
1333
  policy,
1334
+ reservedSecretKey,
1038
1335
  storage,
1039
1336
  text,
1040
1337
  timestamp,