@palbase/backend 10.1.0 → 10.3.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
@@ -5,6 +5,7 @@ import {
5
5
  Flags,
6
6
  Log,
7
7
  Notifications,
8
+ Purchases,
8
9
  Queue,
9
10
  Realtime,
10
11
  Storage,
@@ -12,7 +13,7 @@ import {
12
13
  __requestALS,
13
14
  __runWithRuntime,
14
15
  __setRuntime
15
- } from "./chunk-RGQUB66H.js";
16
+ } from "./chunk-2PCNZBNZ.js";
16
17
  import {
17
18
  EXTENSION_DEPENDENCIES,
18
19
  PALBASE_EXTENSIONS,
@@ -33,6 +34,276 @@ import {
33
34
  uuid
34
35
  } from "./chunk-4WOQWFUP.js";
35
36
 
37
+ // src/errors.ts
38
+ var HttpError = class extends Error {
39
+ status;
40
+ error;
41
+ errorDescription;
42
+ data;
43
+ constructor(status, error, errorDescription, data) {
44
+ super(errorDescription);
45
+ this.name = "HttpError";
46
+ this.status = status;
47
+ this.error = error;
48
+ this.errorDescription = errorDescription;
49
+ if (data !== void 0) {
50
+ this.data = data;
51
+ }
52
+ }
53
+ /**
54
+ * Serialize to the standard Palbase error response format.
55
+ * The `requestId` is injected by the runtime layer from the request context.
56
+ * When called without arguments (e.g. JSON.stringify), request_id is omitted.
57
+ * When `data` is set, it is appended as a strict-superset field.
58
+ */
59
+ toJSON(requestId) {
60
+ const result = {
61
+ error: this.error,
62
+ error_description: this.errorDescription,
63
+ status: this.status
64
+ };
65
+ if (requestId) {
66
+ result.request_id = requestId;
67
+ }
68
+ if (this.data !== void 0) {
69
+ result.data = this.data;
70
+ }
71
+ return result;
72
+ }
73
+ };
74
+ var PalError = class extends HttpError {
75
+ constructor(status, code, description, data) {
76
+ super(status, code, description, data);
77
+ this.name = "PalError";
78
+ }
79
+ };
80
+ var NamedHttpError = class extends HttpError {
81
+ constructor(status, defaultCode, name, message, code, data) {
82
+ super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
83
+ this.name = name;
84
+ }
85
+ };
86
+ function defaultMessage(name) {
87
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
88
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
89
+ }
90
+ var BadRequest = class extends NamedHttpError {
91
+ constructor(data, message) {
92
+ super(400, "bad_request", "BadRequest", message, void 0, data);
93
+ }
94
+ };
95
+ var Unauthorized = class extends NamedHttpError {
96
+ constructor(message, code, data) {
97
+ super(401, "unauthorized", "Unauthorized", message, code, data);
98
+ }
99
+ };
100
+ var Forbidden = class extends NamedHttpError {
101
+ constructor(message, code, data) {
102
+ super(403, "forbidden", "Forbidden", message, code, data);
103
+ }
104
+ };
105
+ var NotFound = class extends NamedHttpError {
106
+ constructor(message, code, data) {
107
+ super(404, "not_found", "NotFound", message, code, data);
108
+ }
109
+ };
110
+ var Conflict = class extends NamedHttpError {
111
+ constructor(message, code, data) {
112
+ super(409, "conflict", "Conflict", message, code, data);
113
+ }
114
+ };
115
+ var TooManyRequests = class extends NamedHttpError {
116
+ constructor(data, message) {
117
+ super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
118
+ }
119
+ };
120
+
121
+ // src/purchases/errors.ts
122
+ function asPalstoreError(err) {
123
+ if (!(err instanceof Error)) return null;
124
+ const e = err;
125
+ if (typeof e.code !== "string" || typeof e.status !== "number") return null;
126
+ return { code: e.code, status: e.status, message: e.message };
127
+ }
128
+ function toHttpError(err) {
129
+ if (err instanceof HttpError) return err;
130
+ const p = asPalstoreError(err);
131
+ if (!p) return err;
132
+ switch (p.code) {
133
+ case "entitlement_required": {
134
+ const entitlement = err.entitlement ?? null;
135
+ return new PalError(403, "entitlement_required", p.message, { entitlement });
136
+ }
137
+ case "quota_exceeded":
138
+ case "credit_insufficient": {
139
+ const state = err.state;
140
+ return new PalError(429, p.code, p.message, state);
141
+ }
142
+ default:
143
+ return err;
144
+ }
145
+ }
146
+
147
+ // src/purchases/registry.ts
148
+ var ENTITLEMENTS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.entitlements");
149
+ var SPENDS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.spends");
150
+ function carrierOf(target) {
151
+ return typeof target === "function" ? target : target.constructor ?? target;
152
+ }
153
+ function own(carrier, slot) {
154
+ if (!Object.prototype.hasOwnProperty.call(carrier, slot)) {
155
+ carrier[slot] = {};
156
+ }
157
+ return carrier[slot];
158
+ }
159
+ function recordEntitlement(target, fnName, key) {
160
+ own(carrierOf(target), ENTITLEMENTS)[fnName] = key;
161
+ }
162
+ function recordSpend(target, fnName, meta) {
163
+ own(carrierOf(target), SPENDS)[fnName] = meta;
164
+ }
165
+ function entitlementFor(ctor, fnName) {
166
+ return carrierOf(ctor)[ENTITLEMENTS]?.[fnName];
167
+ }
168
+ function spendFor(ctor, fnName) {
169
+ return carrierOf(ctor)[SPENDS]?.[fnName];
170
+ }
171
+
172
+ // src/purchases/subject.ts
173
+ var subjectByRequest = /* @__PURE__ */ new WeakMap();
174
+ function storeEnv() {
175
+ return process.env.PALSTORE_STORE_ENV === "sandbox" ? "sandbox" : "production";
176
+ }
177
+ async function currentSubjectId() {
178
+ const store = __requestALS.getStore();
179
+ if (!store) {
180
+ throw new Error(
181
+ "Purchases decorators used outside a request scope. @RequireEntitlement/@Spend resolve the subject from the request's authenticated user, so they only work inside an endpoint handler."
182
+ );
183
+ }
184
+ const memoized = subjectByRequest.get(store);
185
+ if (memoized) return memoized;
186
+ const userId = store.userId;
187
+ if (!userId) {
188
+ throw new Unauthorized(
189
+ "This endpoint requires an authenticated user: purchases entitlements and quota are per-user, and the subject is resolved from the request's identity."
190
+ );
191
+ }
192
+ const resolving = Purchases.resolveSubject({ userRef: userId, storeEnv: storeEnv() }).then(
193
+ (r) => r.subjectId
194
+ );
195
+ subjectByRequest.set(store, resolving);
196
+ resolving.catch(() => subjectByRequest.delete(store));
197
+ return resolving;
198
+ }
199
+
200
+ // src/purchases/decorators.ts
201
+ var WRAPPED = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.wrapped");
202
+ function idempotencyKeyFor(key) {
203
+ const store = __requestALS.getStore();
204
+ const base = store?.idempotencyKey || store?.requestId;
205
+ if (!base) {
206
+ throw new Error(
207
+ "@Spend needs a request-scoped idempotency key, and the runtime supplied neither an Idempotency-Key header nor a request id. Spending without one would let a retried request charge the user twice."
208
+ );
209
+ }
210
+ return `${base}:${key}`;
211
+ }
212
+ function ensureWrapper(target, fnName, descriptor) {
213
+ const original = descriptor.value;
214
+ if (typeof original !== "function") {
215
+ throw new Error(
216
+ `@RequireEntitlement/@Spend can only decorate a method; "${fnName}" is not one.`
217
+ );
218
+ }
219
+ if (original[WRAPPED]) return;
220
+ const wrapper = async function(...args) {
221
+ const ctor = this.constructor;
222
+ const entitlement = entitlementFor(ctor, fnName);
223
+ const spend = spendFor(ctor, fnName);
224
+ let subjectId;
225
+ try {
226
+ subjectId = await currentSubjectId();
227
+ } catch (err) {
228
+ throw toHttpError(err);
229
+ }
230
+ if (entitlement !== void 0) {
231
+ try {
232
+ await Purchases.require(subjectId, entitlement);
233
+ } catch (err) {
234
+ throw toHttpError(err);
235
+ }
236
+ }
237
+ if (spend === void 0) {
238
+ return original.apply(this, args);
239
+ }
240
+ let handlerFailure;
241
+ try {
242
+ return await Purchases.withSpend(
243
+ subjectId,
244
+ spend.key,
245
+ { count: spend.count, idempotencyKey: idempotencyKeyFor(spend.key) },
246
+ async () => {
247
+ try {
248
+ return await original.apply(this, args);
249
+ } catch (err) {
250
+ handlerFailure = { err };
251
+ throw err;
252
+ }
253
+ }
254
+ );
255
+ } catch (err) {
256
+ if (handlerFailure !== void 0 && handlerFailure.err === err) throw err;
257
+ throw toHttpError(err);
258
+ }
259
+ };
260
+ wrapper[WRAPPED] = true;
261
+ descriptor.value = wrapper;
262
+ }
263
+ function RequireEntitlement(key) {
264
+ return function(target, propertyKey, descriptor) {
265
+ const fnName = String(propertyKey);
266
+ recordEntitlement(target, fnName, key);
267
+ ensureWrapper(target, fnName, descriptor);
268
+ };
269
+ }
270
+ function Spend(key) {
271
+ return function(target, propertyKey, descriptor) {
272
+ const fnName = String(propertyKey);
273
+ recordSpend(target, fnName, { key, count: 1 });
274
+ ensureWrapper(target, fnName, descriptor);
275
+ };
276
+ }
277
+
278
+ // src/purchases/keys-gen.ts
279
+ function liveKeys(entries) {
280
+ return Object.entries(entries ?? {}).filter(([, def]) => def?.removed !== true).map(([key]) => key).sort();
281
+ }
282
+ function memberName(key) {
283
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
284
+ }
285
+ function members(keys) {
286
+ if (keys.length === 0) return "";
287
+ return `
288
+ ${keys.map((k) => ` ${memberName(k)}: true;`).join("\n")}
289
+ `;
290
+ }
291
+ function makePurchasesDts(manifest) {
292
+ const entitlements = liveKeys(manifest.entitlements);
293
+ const limits = liveKeys({ ...manifest.limits, ...manifest.credits });
294
+ return `// palbase-purchases.d.ts \u2014 GENERATED by @palbase/backend. Do not edit.
295
+ // Source: the project's palstore catalog revision.
296
+
297
+ declare module "@palbase/backend/purchases" {
298
+ interface Entitlements {${members(entitlements)}}
299
+
300
+ interface Limits {${members(limits)}}
301
+ }
302
+
303
+ export {};
304
+ `;
305
+ }
306
+
36
307
  // src/db/env-gen.ts
37
308
  function baseTsType(def) {
38
309
  switch (def.type) {
@@ -142,7 +413,7 @@ function makeEnvDts(schema) {
142
413
  ${blocks.join("\n")}
143
414
  ` : "";
144
415
  return `// AUTO-GENERATED by @palbase/backend \u2014 DO NOT EDIT.
145
- // Regenerated from db/schema.ts on every \`palbase serve\` / deploy.
416
+ // Regenerated from db/schema.ts on every \`palbase db types\` / deploy.
146
417
  // Augments the @palbase/backend/env \`Tables\` interface so \`Database.tables.*\`
147
418
  // is typed with no import and no generic.
148
419
 
@@ -488,6 +759,8 @@ function valueMatchesType(type, value) {
488
759
  return typeof value === "number" && Number.isFinite(value);
489
760
  case "string":
490
761
  return typeof value === "string";
762
+ case "json":
763
+ return typeof value === "object" && value !== null && !Array.isArray(value);
491
764
  }
492
765
  }
493
766
  function flag(opts) {
@@ -495,8 +768,8 @@ function flag(opts) {
495
768
  throw new Error("flag() expects { type, default, variants?, description? }");
496
769
  }
497
770
  const { type } = opts;
498
- if (type !== "boolean" && type !== "number" && type !== "string") {
499
- throw new Error(`flag type must be "boolean", "number", or "string", got ${JSON.stringify(type)}`);
771
+ if (type !== "boolean" && type !== "number" && type !== "string" && type !== "json") {
772
+ throw new Error(`flag type must be "boolean", "number", "string", or "json", got ${JSON.stringify(type)}`);
500
773
  }
501
774
  if (!valueMatchesType(type, opts.default)) {
502
775
  throw new Error(
@@ -505,7 +778,7 @@ function flag(opts) {
505
778
  }
506
779
  let variants = null;
507
780
  if (opts.variants !== void 0) {
508
- if (type !== "string") {
781
+ if (opts.type !== "string") {
509
782
  throw new Error(`flag variants are only valid for type "string" (got type "${type}")`);
510
783
  }
511
784
  if (!Array.isArray(opts.variants)) {
@@ -590,7 +863,7 @@ var ROUTES = /* @__PURE__ */ Symbol.for("palbase.backend.routes");
590
863
  var PARAM_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.paramBuffer");
591
864
  var RETURN_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.returnBuffer");
592
865
  var THROWS_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.throwsBuffer");
593
- function carrierOf(target) {
866
+ function carrierOf2(target) {
594
867
  const ctor = typeof target === "function" ? target : target.constructor ?? target;
595
868
  return ctor;
596
869
  }
@@ -607,7 +880,7 @@ function ownParamBuffer(carrier) {
607
880
  return carrier[PARAM_BUFFER];
608
881
  }
609
882
  function recordRoute(target, fnName, method, subpath, options) {
610
- const carrier = carrierOf(target);
883
+ const carrier = carrierOf2(target);
611
884
  const routes = ownRoutes(carrier);
612
885
  const buffer = ownParamBuffer(carrier);
613
886
  const params = (buffer[fnName] ?? []).slice().sort((a, b) => a.index - b.index);
@@ -623,7 +896,7 @@ function recordRoute(target, fnName, method, subpath, options) {
623
896
  routes.push(route);
624
897
  }
625
898
  function recordParam(target, fnName, meta) {
626
- const carrier = carrierOf(target);
899
+ const carrier = carrierOf2(target);
627
900
  const buffer = ownParamBuffer(carrier);
628
901
  (buffer[fnName] ??= []).push(meta);
629
902
  const routes = carrier[ROUTES];
@@ -636,7 +909,7 @@ function recordParam(target, fnName, meta) {
636
909
  }
637
910
  }
638
911
  function recordThrows(target, fnName, throws) {
639
- const carrier = carrierOf(target);
912
+ const carrier = carrierOf2(target);
640
913
  const routes = carrier[ROUTES];
641
914
  const route = routes?.find((r) => r.fnName === fnName);
642
915
  if (route) {
@@ -650,7 +923,7 @@ function recordThrows(target, fnName, throws) {
650
923
  if (throwsBuffer) throwsBuffer[fnName] = throws;
651
924
  }
652
925
  function getRoutes(ctor) {
653
- const carrier = carrierOf(ctor);
926
+ const carrier = carrierOf2(ctor);
654
927
  const routes = carrier[ROUTES] ?? [];
655
928
  const returnBuffer = carrier[RETURN_BUFFER];
656
929
  if (returnBuffer) {
@@ -786,90 +1059,6 @@ function defineMiddleware(fn) {
786
1059
  return fn;
787
1060
  }
788
1061
 
789
- // src/errors.ts
790
- var HttpError = class extends Error {
791
- status;
792
- error;
793
- errorDescription;
794
- data;
795
- constructor(status, error, errorDescription, data) {
796
- super(errorDescription);
797
- this.name = "HttpError";
798
- this.status = status;
799
- this.error = error;
800
- this.errorDescription = errorDescription;
801
- if (data !== void 0) {
802
- this.data = data;
803
- }
804
- }
805
- /**
806
- * Serialize to the standard Palbase error response format.
807
- * The `requestId` is injected by the runtime layer from the request context.
808
- * When called without arguments (e.g. JSON.stringify), request_id is omitted.
809
- * When `data` is set, it is appended as a strict-superset field.
810
- */
811
- toJSON(requestId) {
812
- const result = {
813
- error: this.error,
814
- error_description: this.errorDescription,
815
- status: this.status
816
- };
817
- if (requestId) {
818
- result.request_id = requestId;
819
- }
820
- if (this.data !== void 0) {
821
- result.data = this.data;
822
- }
823
- return result;
824
- }
825
- };
826
- var PalError = class extends HttpError {
827
- constructor(status, code, description, data) {
828
- super(status, code, description, data);
829
- this.name = "PalError";
830
- }
831
- };
832
- var NamedHttpError = class extends HttpError {
833
- constructor(status, defaultCode, name, message, code, data) {
834
- super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
835
- this.name = name;
836
- }
837
- };
838
- function defaultMessage(name) {
839
- const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
840
- return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
841
- }
842
- var BadRequest = class extends NamedHttpError {
843
- constructor(data, message) {
844
- super(400, "bad_request", "BadRequest", message, void 0, data);
845
- }
846
- };
847
- var Unauthorized = class extends NamedHttpError {
848
- constructor(message, code, data) {
849
- super(401, "unauthorized", "Unauthorized", message, code, data);
850
- }
851
- };
852
- var Forbidden = class extends NamedHttpError {
853
- constructor(message, code, data) {
854
- super(403, "forbidden", "Forbidden", message, code, data);
855
- }
856
- };
857
- var NotFound = class extends NamedHttpError {
858
- constructor(message, code, data) {
859
- super(404, "not_found", "NotFound", message, code, data);
860
- }
861
- };
862
- var Conflict = class extends NamedHttpError {
863
- constructor(message, code, data) {
864
- super(409, "conflict", "Conflict", message, code, data);
865
- }
866
- };
867
- var TooManyRequests = class extends NamedHttpError {
868
- constructor(data, message) {
869
- super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
870
- }
871
- };
872
-
873
1062
  // src/error-registry.ts
874
1063
  import {
875
1064
  OpenAPIRegistry,
@@ -883,7 +1072,27 @@ var BUILTIN_DATA_SCHEMAS = {
883
1072
  bad_request: z.object({
884
1073
  fields: z.array(z.object({ field: z.string(), message: z.string() }))
885
1074
  }),
886
- too_many_requests: z.object({ retryAfter: z.number().int() })
1075
+ too_many_requests: z.object({ retryAfter: z.number().int() }),
1076
+ // Purchases (@RequireEntitlement / @Spend). Their payloads are what make a
1077
+ // paywall renderable on the client: WHICH entitlement was missing, and for a
1078
+ // spend the real ceiling and reset time rather than a guessed retry delay.
1079
+ entitlement_required: z.object({ entitlement: z.string().nullable() }),
1080
+ quota_exceeded: z.object({
1081
+ key: z.string(),
1082
+ scope: z.string(),
1083
+ window: z.string(),
1084
+ used: z.number().int(),
1085
+ reserved: z.number().int(),
1086
+ max: z.number().int(),
1087
+ remaining: z.number().int(),
1088
+ resetAt: z.string()
1089
+ }),
1090
+ credit_insufficient: z.object({
1091
+ key: z.string(),
1092
+ balance: z.number().int(),
1093
+ reserved: z.number().int(),
1094
+ remaining: z.number().int()
1095
+ })
887
1096
  };
888
1097
  function getErrorRegistry() {
889
1098
  const g = globalThis;
@@ -895,7 +1104,13 @@ function getErrorRegistry() {
895
1104
  ["forbidden", 403, "Forbidden"],
896
1105
  ["not_found", 404, "NotFound"],
897
1106
  ["conflict", 409, "Conflict"],
898
- ["too_many_requests", 429, "TooManyRequests"]
1107
+ ["too_many_requests", 429, "TooManyRequests"],
1108
+ // Thrown by the purchases decorators, so they are pre-seeded like the
1109
+ // other built-ins: a project never declares them, but every gated or
1110
+ // metered route must surface them as TYPED errors on the client.
1111
+ ["entitlement_required", 403, "EntitlementRequired"],
1112
+ ["quota_exceeded", 429, "QuotaExceeded"],
1113
+ ["credit_insufficient", 429, "CreditInsufficient"]
899
1114
  ]) {
900
1115
  const dataSchema = BUILTIN_DATA_SCHEMAS[code];
901
1116
  m.set(code, {
@@ -1307,6 +1522,7 @@ export {
1307
1522
  Patch,
1308
1523
  PolicyBuilder,
1309
1524
  Post,
1525
+ Purchases,
1310
1526
  Put,
1311
1527
  Query,
1312
1528
  QueryParams,
@@ -1315,8 +1531,10 @@ export {
1315
1531
  Realtime,
1316
1532
  Req,
1317
1533
  RequestId,
1534
+ RequireEntitlement,
1318
1535
  Resource,
1319
1536
  STORAGE_CONFIG_KIND,
1537
+ Spend,
1320
1538
  Storage,
1321
1539
  TEST_USERS_CONFIG_KIND,
1322
1540
  TooManyRequests,
@@ -1349,6 +1567,7 @@ export {
1349
1567
  defineWebhook,
1350
1568
  defineWorker,
1351
1569
  documents,
1570
+ entitlementFor,
1352
1571
  enumType,
1353
1572
  flag,
1354
1573
  getErrorRegistry,
@@ -1357,6 +1576,7 @@ export {
1357
1576
  isPalbaseExtension,
1358
1577
  jsonb,
1359
1578
  makeEnvDts,
1579
+ makePurchasesDts,
1360
1580
  makeTypedDB,
1361
1581
  numeric,
1362
1582
  parseFileSizeLimit,
@@ -1364,6 +1584,7 @@ export {
1364
1584
  raw,
1365
1585
  recordThrows,
1366
1586
  reservedSecretKey,
1587
+ spendFor,
1367
1588
  storage,
1368
1589
  testUser,
1369
1590
  text,