@palbase/backend 10.2.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
 
@@ -592,7 +863,7 @@ var ROUTES = /* @__PURE__ */ Symbol.for("palbase.backend.routes");
592
863
  var PARAM_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.paramBuffer");
593
864
  var RETURN_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.returnBuffer");
594
865
  var THROWS_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.throwsBuffer");
595
- function carrierOf(target) {
866
+ function carrierOf2(target) {
596
867
  const ctor = typeof target === "function" ? target : target.constructor ?? target;
597
868
  return ctor;
598
869
  }
@@ -609,7 +880,7 @@ function ownParamBuffer(carrier) {
609
880
  return carrier[PARAM_BUFFER];
610
881
  }
611
882
  function recordRoute(target, fnName, method, subpath, options) {
612
- const carrier = carrierOf(target);
883
+ const carrier = carrierOf2(target);
613
884
  const routes = ownRoutes(carrier);
614
885
  const buffer = ownParamBuffer(carrier);
615
886
  const params = (buffer[fnName] ?? []).slice().sort((a, b) => a.index - b.index);
@@ -625,7 +896,7 @@ function recordRoute(target, fnName, method, subpath, options) {
625
896
  routes.push(route);
626
897
  }
627
898
  function recordParam(target, fnName, meta) {
628
- const carrier = carrierOf(target);
899
+ const carrier = carrierOf2(target);
629
900
  const buffer = ownParamBuffer(carrier);
630
901
  (buffer[fnName] ??= []).push(meta);
631
902
  const routes = carrier[ROUTES];
@@ -638,7 +909,7 @@ function recordParam(target, fnName, meta) {
638
909
  }
639
910
  }
640
911
  function recordThrows(target, fnName, throws) {
641
- const carrier = carrierOf(target);
912
+ const carrier = carrierOf2(target);
642
913
  const routes = carrier[ROUTES];
643
914
  const route = routes?.find((r) => r.fnName === fnName);
644
915
  if (route) {
@@ -652,7 +923,7 @@ function recordThrows(target, fnName, throws) {
652
923
  if (throwsBuffer) throwsBuffer[fnName] = throws;
653
924
  }
654
925
  function getRoutes(ctor) {
655
- const carrier = carrierOf(ctor);
926
+ const carrier = carrierOf2(ctor);
656
927
  const routes = carrier[ROUTES] ?? [];
657
928
  const returnBuffer = carrier[RETURN_BUFFER];
658
929
  if (returnBuffer) {
@@ -788,90 +1059,6 @@ function defineMiddleware(fn) {
788
1059
  return fn;
789
1060
  }
790
1061
 
791
- // src/errors.ts
792
- var HttpError = class extends Error {
793
- status;
794
- error;
795
- errorDescription;
796
- data;
797
- constructor(status, error, errorDescription, data) {
798
- super(errorDescription);
799
- this.name = "HttpError";
800
- this.status = status;
801
- this.error = error;
802
- this.errorDescription = errorDescription;
803
- if (data !== void 0) {
804
- this.data = data;
805
- }
806
- }
807
- /**
808
- * Serialize to the standard Palbase error response format.
809
- * The `requestId` is injected by the runtime layer from the request context.
810
- * When called without arguments (e.g. JSON.stringify), request_id is omitted.
811
- * When `data` is set, it is appended as a strict-superset field.
812
- */
813
- toJSON(requestId) {
814
- const result = {
815
- error: this.error,
816
- error_description: this.errorDescription,
817
- status: this.status
818
- };
819
- if (requestId) {
820
- result.request_id = requestId;
821
- }
822
- if (this.data !== void 0) {
823
- result.data = this.data;
824
- }
825
- return result;
826
- }
827
- };
828
- var PalError = class extends HttpError {
829
- constructor(status, code, description, data) {
830
- super(status, code, description, data);
831
- this.name = "PalError";
832
- }
833
- };
834
- var NamedHttpError = class extends HttpError {
835
- constructor(status, defaultCode, name, message, code, data) {
836
- super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
837
- this.name = name;
838
- }
839
- };
840
- function defaultMessage(name) {
841
- const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
842
- return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
843
- }
844
- var BadRequest = class extends NamedHttpError {
845
- constructor(data, message) {
846
- super(400, "bad_request", "BadRequest", message, void 0, data);
847
- }
848
- };
849
- var Unauthorized = class extends NamedHttpError {
850
- constructor(message, code, data) {
851
- super(401, "unauthorized", "Unauthorized", message, code, data);
852
- }
853
- };
854
- var Forbidden = class extends NamedHttpError {
855
- constructor(message, code, data) {
856
- super(403, "forbidden", "Forbidden", message, code, data);
857
- }
858
- };
859
- var NotFound = class extends NamedHttpError {
860
- constructor(message, code, data) {
861
- super(404, "not_found", "NotFound", message, code, data);
862
- }
863
- };
864
- var Conflict = class extends NamedHttpError {
865
- constructor(message, code, data) {
866
- super(409, "conflict", "Conflict", message, code, data);
867
- }
868
- };
869
- var TooManyRequests = class extends NamedHttpError {
870
- constructor(data, message) {
871
- super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
872
- }
873
- };
874
-
875
1062
  // src/error-registry.ts
876
1063
  import {
877
1064
  OpenAPIRegistry,
@@ -885,7 +1072,27 @@ var BUILTIN_DATA_SCHEMAS = {
885
1072
  bad_request: z.object({
886
1073
  fields: z.array(z.object({ field: z.string(), message: z.string() }))
887
1074
  }),
888
- 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
+ })
889
1096
  };
890
1097
  function getErrorRegistry() {
891
1098
  const g = globalThis;
@@ -897,7 +1104,13 @@ function getErrorRegistry() {
897
1104
  ["forbidden", 403, "Forbidden"],
898
1105
  ["not_found", 404, "NotFound"],
899
1106
  ["conflict", 409, "Conflict"],
900
- ["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"]
901
1114
  ]) {
902
1115
  const dataSchema = BUILTIN_DATA_SCHEMAS[code];
903
1116
  m.set(code, {
@@ -1309,6 +1522,7 @@ export {
1309
1522
  Patch,
1310
1523
  PolicyBuilder,
1311
1524
  Post,
1525
+ Purchases,
1312
1526
  Put,
1313
1527
  Query,
1314
1528
  QueryParams,
@@ -1317,8 +1531,10 @@ export {
1317
1531
  Realtime,
1318
1532
  Req,
1319
1533
  RequestId,
1534
+ RequireEntitlement,
1320
1535
  Resource,
1321
1536
  STORAGE_CONFIG_KIND,
1537
+ Spend,
1322
1538
  Storage,
1323
1539
  TEST_USERS_CONFIG_KIND,
1324
1540
  TooManyRequests,
@@ -1351,6 +1567,7 @@ export {
1351
1567
  defineWebhook,
1352
1568
  defineWorker,
1353
1569
  documents,
1570
+ entitlementFor,
1354
1571
  enumType,
1355
1572
  flag,
1356
1573
  getErrorRegistry,
@@ -1359,6 +1576,7 @@ export {
1359
1576
  isPalbaseExtension,
1360
1577
  jsonb,
1361
1578
  makeEnvDts,
1579
+ makePurchasesDts,
1362
1580
  makeTypedDB,
1363
1581
  numeric,
1364
1582
  parseFileSizeLimit,
@@ -1366,6 +1584,7 @@ export {
1366
1584
  raw,
1367
1585
  recordThrows,
1368
1586
  reservedSecretKey,
1587
+ spendFor,
1369
1588
  storage,
1370
1589
  testUser,
1371
1590
  text,