@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.cjs CHANGED
@@ -49,6 +49,7 @@ __export(src_exports, {
49
49
  Patch: () => Patch,
50
50
  PolicyBuilder: () => PolicyBuilder,
51
51
  Post: () => Post,
52
+ Purchases: () => Purchases,
52
53
  Put: () => Put,
53
54
  Query: () => Query,
54
55
  QueryParams: () => QueryParams,
@@ -57,8 +58,10 @@ __export(src_exports, {
57
58
  Realtime: () => Realtime,
58
59
  Req: () => Req,
59
60
  RequestId: () => RequestId,
61
+ RequireEntitlement: () => RequireEntitlement,
60
62
  Resource: () => Resource,
61
63
  STORAGE_CONFIG_KIND: () => STORAGE_CONFIG_KIND,
64
+ Spend: () => Spend,
62
65
  Storage: () => Storage,
63
66
  TEST_USERS_CONFIG_KIND: () => TEST_USERS_CONFIG_KIND,
64
67
  TooManyRequests: () => TooManyRequests,
@@ -91,6 +94,7 @@ __export(src_exports, {
91
94
  defineWebhook: () => defineWebhook,
92
95
  defineWorker: () => defineWorker,
93
96
  documents: () => documents,
97
+ entitlementFor: () => entitlementFor,
94
98
  enumType: () => enumType,
95
99
  flag: () => flag,
96
100
  getErrorRegistry: () => getErrorRegistry,
@@ -99,6 +103,7 @@ __export(src_exports, {
99
103
  isPalbaseExtension: () => isPalbaseExtension,
100
104
  jsonb: () => jsonb,
101
105
  makeEnvDts: () => makeEnvDts,
106
+ makePurchasesDts: () => makePurchasesDts,
102
107
  makeTypedDB: () => makeTypedDB,
103
108
  numeric: () => numeric,
104
109
  parseFileSizeLimit: () => parseFileSizeLimit,
@@ -106,6 +111,7 @@ __export(src_exports, {
106
111
  raw: () => raw,
107
112
  recordThrows: () => recordThrows,
108
113
  reservedSecretKey: () => reservedSecretKey,
114
+ spendFor: () => spendFor,
109
115
  storage: () => storage,
110
116
  testUser: () => testUser,
111
117
  text: () => text,
@@ -200,6 +206,7 @@ var Cache = makeServiceProxy("Cache");
200
206
  var Queue = makeServiceProxy("Queue");
201
207
  var Log = makeServiceProxy("Log");
202
208
  var Notifications = makeServiceProxy("Notifications");
209
+ var Purchases = makeServiceProxy("Purchases");
203
210
  var rawFlags = makeServiceProxy("Flags");
204
211
  var Flags = Object.assign(
205
212
  {
@@ -230,6 +237,276 @@ var Flags = Object.assign(
230
237
  );
231
238
  var Realtime = makeServiceProxy("Realtime");
232
239
 
240
+ // src/errors.ts
241
+ var HttpError = class extends Error {
242
+ status;
243
+ error;
244
+ errorDescription;
245
+ data;
246
+ constructor(status, error, errorDescription, data) {
247
+ super(errorDescription);
248
+ this.name = "HttpError";
249
+ this.status = status;
250
+ this.error = error;
251
+ this.errorDescription = errorDescription;
252
+ if (data !== void 0) {
253
+ this.data = data;
254
+ }
255
+ }
256
+ /**
257
+ * Serialize to the standard Palbase error response format.
258
+ * The `requestId` is injected by the runtime layer from the request context.
259
+ * When called without arguments (e.g. JSON.stringify), request_id is omitted.
260
+ * When `data` is set, it is appended as a strict-superset field.
261
+ */
262
+ toJSON(requestId) {
263
+ const result = {
264
+ error: this.error,
265
+ error_description: this.errorDescription,
266
+ status: this.status
267
+ };
268
+ if (requestId) {
269
+ result.request_id = requestId;
270
+ }
271
+ if (this.data !== void 0) {
272
+ result.data = this.data;
273
+ }
274
+ return result;
275
+ }
276
+ };
277
+ var PalError = class extends HttpError {
278
+ constructor(status, code, description, data) {
279
+ super(status, code, description, data);
280
+ this.name = "PalError";
281
+ }
282
+ };
283
+ var NamedHttpError = class extends HttpError {
284
+ constructor(status, defaultCode, name, message, code, data) {
285
+ super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
286
+ this.name = name;
287
+ }
288
+ };
289
+ function defaultMessage(name) {
290
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
291
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
292
+ }
293
+ var BadRequest = class extends NamedHttpError {
294
+ constructor(data, message) {
295
+ super(400, "bad_request", "BadRequest", message, void 0, data);
296
+ }
297
+ };
298
+ var Unauthorized = class extends NamedHttpError {
299
+ constructor(message, code, data) {
300
+ super(401, "unauthorized", "Unauthorized", message, code, data);
301
+ }
302
+ };
303
+ var Forbidden = class extends NamedHttpError {
304
+ constructor(message, code, data) {
305
+ super(403, "forbidden", "Forbidden", message, code, data);
306
+ }
307
+ };
308
+ var NotFound = class extends NamedHttpError {
309
+ constructor(message, code, data) {
310
+ super(404, "not_found", "NotFound", message, code, data);
311
+ }
312
+ };
313
+ var Conflict = class extends NamedHttpError {
314
+ constructor(message, code, data) {
315
+ super(409, "conflict", "Conflict", message, code, data);
316
+ }
317
+ };
318
+ var TooManyRequests = class extends NamedHttpError {
319
+ constructor(data, message) {
320
+ super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
321
+ }
322
+ };
323
+
324
+ // src/purchases/errors.ts
325
+ function asPalstoreError(err) {
326
+ if (!(err instanceof Error)) return null;
327
+ const e = err;
328
+ if (typeof e.code !== "string" || typeof e.status !== "number") return null;
329
+ return { code: e.code, status: e.status, message: e.message };
330
+ }
331
+ function toHttpError(err) {
332
+ if (err instanceof HttpError) return err;
333
+ const p = asPalstoreError(err);
334
+ if (!p) return err;
335
+ switch (p.code) {
336
+ case "entitlement_required": {
337
+ const entitlement = err.entitlement ?? null;
338
+ return new PalError(403, "entitlement_required", p.message, { entitlement });
339
+ }
340
+ case "quota_exceeded":
341
+ case "credit_insufficient": {
342
+ const state = err.state;
343
+ return new PalError(429, p.code, p.message, state);
344
+ }
345
+ default:
346
+ return err;
347
+ }
348
+ }
349
+
350
+ // src/purchases/registry.ts
351
+ var ENTITLEMENTS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.entitlements");
352
+ var SPENDS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.spends");
353
+ function carrierOf(target) {
354
+ return typeof target === "function" ? target : target.constructor ?? target;
355
+ }
356
+ function own(carrier, slot) {
357
+ if (!Object.prototype.hasOwnProperty.call(carrier, slot)) {
358
+ carrier[slot] = {};
359
+ }
360
+ return carrier[slot];
361
+ }
362
+ function recordEntitlement(target, fnName, key) {
363
+ own(carrierOf(target), ENTITLEMENTS)[fnName] = key;
364
+ }
365
+ function recordSpend(target, fnName, meta) {
366
+ own(carrierOf(target), SPENDS)[fnName] = meta;
367
+ }
368
+ function entitlementFor(ctor, fnName) {
369
+ return carrierOf(ctor)[ENTITLEMENTS]?.[fnName];
370
+ }
371
+ function spendFor(ctor, fnName) {
372
+ return carrierOf(ctor)[SPENDS]?.[fnName];
373
+ }
374
+
375
+ // src/purchases/subject.ts
376
+ var subjectByRequest = /* @__PURE__ */ new WeakMap();
377
+ function storeEnv() {
378
+ return process.env.PALSTORE_STORE_ENV === "sandbox" ? "sandbox" : "production";
379
+ }
380
+ async function currentSubjectId() {
381
+ const store = __requestALS.getStore();
382
+ if (!store) {
383
+ throw new Error(
384
+ "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."
385
+ );
386
+ }
387
+ const memoized = subjectByRequest.get(store);
388
+ if (memoized) return memoized;
389
+ const userId = store.userId;
390
+ if (!userId) {
391
+ throw new Unauthorized(
392
+ "This endpoint requires an authenticated user: purchases entitlements and quota are per-user, and the subject is resolved from the request's identity."
393
+ );
394
+ }
395
+ const resolving = Purchases.resolveSubject({ userRef: userId, storeEnv: storeEnv() }).then(
396
+ (r) => r.subjectId
397
+ );
398
+ subjectByRequest.set(store, resolving);
399
+ resolving.catch(() => subjectByRequest.delete(store));
400
+ return resolving;
401
+ }
402
+
403
+ // src/purchases/decorators.ts
404
+ var WRAPPED = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.wrapped");
405
+ function idempotencyKeyFor(key) {
406
+ const store = __requestALS.getStore();
407
+ const base = store?.idempotencyKey || store?.requestId;
408
+ if (!base) {
409
+ throw new Error(
410
+ "@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."
411
+ );
412
+ }
413
+ return `${base}:${key}`;
414
+ }
415
+ function ensureWrapper(target, fnName, descriptor) {
416
+ const original = descriptor.value;
417
+ if (typeof original !== "function") {
418
+ throw new Error(
419
+ `@RequireEntitlement/@Spend can only decorate a method; "${fnName}" is not one.`
420
+ );
421
+ }
422
+ if (original[WRAPPED]) return;
423
+ const wrapper = async function(...args) {
424
+ const ctor = this.constructor;
425
+ const entitlement = entitlementFor(ctor, fnName);
426
+ const spend = spendFor(ctor, fnName);
427
+ let subjectId;
428
+ try {
429
+ subjectId = await currentSubjectId();
430
+ } catch (err) {
431
+ throw toHttpError(err);
432
+ }
433
+ if (entitlement !== void 0) {
434
+ try {
435
+ await Purchases.require(subjectId, entitlement);
436
+ } catch (err) {
437
+ throw toHttpError(err);
438
+ }
439
+ }
440
+ if (spend === void 0) {
441
+ return original.apply(this, args);
442
+ }
443
+ let handlerFailure;
444
+ try {
445
+ return await Purchases.withSpend(
446
+ subjectId,
447
+ spend.key,
448
+ { count: spend.count, idempotencyKey: idempotencyKeyFor(spend.key) },
449
+ async () => {
450
+ try {
451
+ return await original.apply(this, args);
452
+ } catch (err) {
453
+ handlerFailure = { err };
454
+ throw err;
455
+ }
456
+ }
457
+ );
458
+ } catch (err) {
459
+ if (handlerFailure !== void 0 && handlerFailure.err === err) throw err;
460
+ throw toHttpError(err);
461
+ }
462
+ };
463
+ wrapper[WRAPPED] = true;
464
+ descriptor.value = wrapper;
465
+ }
466
+ function RequireEntitlement(key) {
467
+ return function(target, propertyKey, descriptor) {
468
+ const fnName = String(propertyKey);
469
+ recordEntitlement(target, fnName, key);
470
+ ensureWrapper(target, fnName, descriptor);
471
+ };
472
+ }
473
+ function Spend(key) {
474
+ return function(target, propertyKey, descriptor) {
475
+ const fnName = String(propertyKey);
476
+ recordSpend(target, fnName, { key, count: 1 });
477
+ ensureWrapper(target, fnName, descriptor);
478
+ };
479
+ }
480
+
481
+ // src/purchases/keys-gen.ts
482
+ function liveKeys(entries) {
483
+ return Object.entries(entries ?? {}).filter(([, def]) => def?.removed !== true).map(([key]) => key).sort();
484
+ }
485
+ function memberName(key) {
486
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
487
+ }
488
+ function members(keys) {
489
+ if (keys.length === 0) return "";
490
+ return `
491
+ ${keys.map((k) => ` ${memberName(k)}: true;`).join("\n")}
492
+ `;
493
+ }
494
+ function makePurchasesDts(manifest) {
495
+ const entitlements = liveKeys(manifest.entitlements);
496
+ const limits = liveKeys({ ...manifest.limits, ...manifest.credits });
497
+ return `// palbase-purchases.d.ts \u2014 GENERATED by @palbase/backend. Do not edit.
498
+ // Source: the project's palstore catalog revision.
499
+
500
+ declare module "@palbase/backend/purchases" {
501
+ interface Entitlements {${members(entitlements)}}
502
+
503
+ interface Limits {${members(limits)}}
504
+ }
505
+
506
+ export {};
507
+ `;
508
+ }
509
+
233
510
  // src/db/policy.ts
234
511
  var PolicyBuilder = class {
235
512
  _def;
@@ -631,7 +908,7 @@ function makeEnvDts(schema) {
631
908
  ${blocks.join("\n")}
632
909
  ` : "";
633
910
  return `// AUTO-GENERATED by @palbase/backend \u2014 DO NOT EDIT.
634
- // Regenerated from db/schema.ts on every \`palbase serve\` / deploy.
911
+ // Regenerated from db/schema.ts on every \`palbase db types\` / deploy.
635
912
  // Augments the @palbase/backend/env \`Tables\` interface so \`Database.tables.*\`
636
913
  // is typed with no import and no generic.
637
914
 
@@ -1081,7 +1358,7 @@ var ROUTES = /* @__PURE__ */ Symbol.for("palbase.backend.routes");
1081
1358
  var PARAM_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.paramBuffer");
1082
1359
  var RETURN_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.returnBuffer");
1083
1360
  var THROWS_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.throwsBuffer");
1084
- function carrierOf(target) {
1361
+ function carrierOf2(target) {
1085
1362
  const ctor = typeof target === "function" ? target : target.constructor ?? target;
1086
1363
  return ctor;
1087
1364
  }
@@ -1098,7 +1375,7 @@ function ownParamBuffer(carrier) {
1098
1375
  return carrier[PARAM_BUFFER];
1099
1376
  }
1100
1377
  function recordRoute(target, fnName, method, subpath, options) {
1101
- const carrier = carrierOf(target);
1378
+ const carrier = carrierOf2(target);
1102
1379
  const routes = ownRoutes(carrier);
1103
1380
  const buffer = ownParamBuffer(carrier);
1104
1381
  const params = (buffer[fnName] ?? []).slice().sort((a, b) => a.index - b.index);
@@ -1114,7 +1391,7 @@ function recordRoute(target, fnName, method, subpath, options) {
1114
1391
  routes.push(route);
1115
1392
  }
1116
1393
  function recordParam(target, fnName, meta) {
1117
- const carrier = carrierOf(target);
1394
+ const carrier = carrierOf2(target);
1118
1395
  const buffer = ownParamBuffer(carrier);
1119
1396
  (buffer[fnName] ??= []).push(meta);
1120
1397
  const routes = carrier[ROUTES];
@@ -1127,7 +1404,7 @@ function recordParam(target, fnName, meta) {
1127
1404
  }
1128
1405
  }
1129
1406
  function recordThrows(target, fnName, throws) {
1130
- const carrier = carrierOf(target);
1407
+ const carrier = carrierOf2(target);
1131
1408
  const routes = carrier[ROUTES];
1132
1409
  const route = routes?.find((r) => r.fnName === fnName);
1133
1410
  if (route) {
@@ -1141,7 +1418,7 @@ function recordThrows(target, fnName, throws) {
1141
1418
  if (throwsBuffer) throwsBuffer[fnName] = throws;
1142
1419
  }
1143
1420
  function getRoutes(ctor) {
1144
- const carrier = carrierOf(ctor);
1421
+ const carrier = carrierOf2(ctor);
1145
1422
  const routes = carrier[ROUTES] ?? [];
1146
1423
  const returnBuffer = carrier[RETURN_BUFFER];
1147
1424
  if (returnBuffer) {
@@ -1277,90 +1554,6 @@ function defineMiddleware(fn) {
1277
1554
  return fn;
1278
1555
  }
1279
1556
 
1280
- // src/errors.ts
1281
- var HttpError = class extends Error {
1282
- status;
1283
- error;
1284
- errorDescription;
1285
- data;
1286
- constructor(status, error, errorDescription, data) {
1287
- super(errorDescription);
1288
- this.name = "HttpError";
1289
- this.status = status;
1290
- this.error = error;
1291
- this.errorDescription = errorDescription;
1292
- if (data !== void 0) {
1293
- this.data = data;
1294
- }
1295
- }
1296
- /**
1297
- * Serialize to the standard Palbase error response format.
1298
- * The `requestId` is injected by the runtime layer from the request context.
1299
- * When called without arguments (e.g. JSON.stringify), request_id is omitted.
1300
- * When `data` is set, it is appended as a strict-superset field.
1301
- */
1302
- toJSON(requestId) {
1303
- const result = {
1304
- error: this.error,
1305
- error_description: this.errorDescription,
1306
- status: this.status
1307
- };
1308
- if (requestId) {
1309
- result.request_id = requestId;
1310
- }
1311
- if (this.data !== void 0) {
1312
- result.data = this.data;
1313
- }
1314
- return result;
1315
- }
1316
- };
1317
- var PalError = class extends HttpError {
1318
- constructor(status, code, description, data) {
1319
- super(status, code, description, data);
1320
- this.name = "PalError";
1321
- }
1322
- };
1323
- var NamedHttpError = class extends HttpError {
1324
- constructor(status, defaultCode, name, message, code, data) {
1325
- super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
1326
- this.name = name;
1327
- }
1328
- };
1329
- function defaultMessage(name) {
1330
- const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
1331
- return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
1332
- }
1333
- var BadRequest = class extends NamedHttpError {
1334
- constructor(data, message) {
1335
- super(400, "bad_request", "BadRequest", message, void 0, data);
1336
- }
1337
- };
1338
- var Unauthorized = class extends NamedHttpError {
1339
- constructor(message, code, data) {
1340
- super(401, "unauthorized", "Unauthorized", message, code, data);
1341
- }
1342
- };
1343
- var Forbidden = class extends NamedHttpError {
1344
- constructor(message, code, data) {
1345
- super(403, "forbidden", "Forbidden", message, code, data);
1346
- }
1347
- };
1348
- var NotFound = class extends NamedHttpError {
1349
- constructor(message, code, data) {
1350
- super(404, "not_found", "NotFound", message, code, data);
1351
- }
1352
- };
1353
- var Conflict = class extends NamedHttpError {
1354
- constructor(message, code, data) {
1355
- super(409, "conflict", "Conflict", message, code, data);
1356
- }
1357
- };
1358
- var TooManyRequests = class extends NamedHttpError {
1359
- constructor(data, message) {
1360
- super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
1361
- }
1362
- };
1363
-
1364
1557
  // src/error-registry.ts
1365
1558
  var import_zod_to_openapi = require("@asteasolutions/zod-to-openapi");
1366
1559
  var import_zod = require("zod");
@@ -1370,7 +1563,27 @@ var BUILTIN_DATA_SCHEMAS = {
1370
1563
  bad_request: import_zod.z.object({
1371
1564
  fields: import_zod.z.array(import_zod.z.object({ field: import_zod.z.string(), message: import_zod.z.string() }))
1372
1565
  }),
1373
- too_many_requests: import_zod.z.object({ retryAfter: import_zod.z.number().int() })
1566
+ too_many_requests: import_zod.z.object({ retryAfter: import_zod.z.number().int() }),
1567
+ // Purchases (@RequireEntitlement / @Spend). Their payloads are what make a
1568
+ // paywall renderable on the client: WHICH entitlement was missing, and for a
1569
+ // spend the real ceiling and reset time rather than a guessed retry delay.
1570
+ entitlement_required: import_zod.z.object({ entitlement: import_zod.z.string().nullable() }),
1571
+ quota_exceeded: import_zod.z.object({
1572
+ key: import_zod.z.string(),
1573
+ scope: import_zod.z.string(),
1574
+ window: import_zod.z.string(),
1575
+ used: import_zod.z.number().int(),
1576
+ reserved: import_zod.z.number().int(),
1577
+ max: import_zod.z.number().int(),
1578
+ remaining: import_zod.z.number().int(),
1579
+ resetAt: import_zod.z.string()
1580
+ }),
1581
+ credit_insufficient: import_zod.z.object({
1582
+ key: import_zod.z.string(),
1583
+ balance: import_zod.z.number().int(),
1584
+ reserved: import_zod.z.number().int(),
1585
+ remaining: import_zod.z.number().int()
1586
+ })
1374
1587
  };
1375
1588
  function getErrorRegistry() {
1376
1589
  const g = globalThis;
@@ -1382,7 +1595,13 @@ function getErrorRegistry() {
1382
1595
  ["forbidden", 403, "Forbidden"],
1383
1596
  ["not_found", 404, "NotFound"],
1384
1597
  ["conflict", 409, "Conflict"],
1385
- ["too_many_requests", 429, "TooManyRequests"]
1598
+ ["too_many_requests", 429, "TooManyRequests"],
1599
+ // Thrown by the purchases decorators, so they are pre-seeded like the
1600
+ // other built-ins: a project never declares them, but every gated or
1601
+ // metered route must surface them as TYPED errors on the client.
1602
+ ["entitlement_required", 403, "EntitlementRequired"],
1603
+ ["quota_exceeded", 429, "QuotaExceeded"],
1604
+ ["credit_insufficient", 429, "CreditInsufficient"]
1386
1605
  ]) {
1387
1606
  const dataSchema = BUILTIN_DATA_SCHEMAS[code];
1388
1607
  m.set(code, {
@@ -1795,6 +2014,7 @@ var import_zod2 = require("zod");
1795
2014
  Patch,
1796
2015
  PolicyBuilder,
1797
2016
  Post,
2017
+ Purchases,
1798
2018
  Put,
1799
2019
  Query,
1800
2020
  QueryParams,
@@ -1803,8 +2023,10 @@ var import_zod2 = require("zod");
1803
2023
  Realtime,
1804
2024
  Req,
1805
2025
  RequestId,
2026
+ RequireEntitlement,
1806
2027
  Resource,
1807
2028
  STORAGE_CONFIG_KIND,
2029
+ Spend,
1808
2030
  Storage,
1809
2031
  TEST_USERS_CONFIG_KIND,
1810
2032
  TooManyRequests,
@@ -1837,6 +2059,7 @@ var import_zod2 = require("zod");
1837
2059
  defineWebhook,
1838
2060
  defineWorker,
1839
2061
  documents,
2062
+ entitlementFor,
1840
2063
  enumType,
1841
2064
  flag,
1842
2065
  getErrorRegistry,
@@ -1845,6 +2068,7 @@ var import_zod2 = require("zod");
1845
2068
  isPalbaseExtension,
1846
2069
  jsonb,
1847
2070
  makeEnvDts,
2071
+ makePurchasesDts,
1848
2072
  makeTypedDB,
1849
2073
  numeric,
1850
2074
  parseFileSizeLimit,
@@ -1852,6 +2076,7 @@ var import_zod2 = require("zod");
1852
2076
  raw,
1853
2077
  recordThrows,
1854
2078
  reservedSecretKey,
2079
+ spendFor,
1855
2080
  storage,
1856
2081
  testUser,
1857
2082
  text,