@palbase/backend 10.3.0 → 12.0.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
@@ -37,10 +37,12 @@ __export(src_exports, {
37
37
  Get: () => Get,
38
38
  Headers: () => Headers,
39
39
  HttpError: () => HttpError,
40
+ Job: () => Job,
40
41
  Log: () => Log,
41
42
  NOTIFICATIONS_CONFIG_KIND: () => NOTIFICATIONS_CONFIG_KIND,
42
43
  NotFound: () => NotFound,
43
44
  Notifications: () => Notifications,
45
+ On: () => On,
44
46
  OptionalUser: () => OptionalUser,
45
47
  PALBASE_EXTENSIONS: () => PALBASE_EXTENSIONS,
46
48
  PROVIDER_CATALOG: () => PROVIDER_CATALOG,
@@ -66,10 +68,13 @@ __export(src_exports, {
66
68
  TEST_USERS_CONFIG_KIND: () => TEST_USERS_CONFIG_KIND,
67
69
  TooManyRequests: () => TooManyRequests,
68
70
  TraceId: () => TraceId,
71
+ TxPlanError: () => TxPlanError,
72
+ TxRefError: () => TxRefError,
69
73
  Unauthorized: () => Unauthorized,
70
74
  Upload: () => Upload,
71
75
  UploadedObject: () => UploadedObject,
72
76
  User: () => User,
77
+ Webhook: () => Webhook,
73
78
  __getRuntime: () => __getRuntime,
74
79
  __registerResource: () => __registerResource,
75
80
  __requestALS: () => __requestALS,
@@ -82,29 +87,32 @@ __export(src_exports, {
82
87
  boolean: () => boolean,
83
88
  bucket: () => bucket,
84
89
  buildProvider: () => buildProvider,
90
+ dec: () => dec,
85
91
  defineEgress: () => defineEgress,
86
92
  defineError: () => defineError,
87
93
  defineFlags: () => defineFlags,
88
- defineJob: () => defineJob,
89
94
  defineMiddleware: () => defineMiddleware,
90
95
  defineNotifications: () => defineNotifications,
91
96
  defineSchema: () => defineSchema,
92
97
  defineStorage: () => defineStorage,
93
98
  defineTestUsers: () => defineTestUsers,
94
- defineWebhook: () => defineWebhook,
95
99
  defineWorker: () => defineWorker,
96
100
  documents: () => documents,
97
101
  entitlementFor: () => entitlementFor,
98
102
  enumType: () => enumType,
99
103
  flag: () => flag,
100
104
  getErrorRegistry: () => getErrorRegistry,
105
+ getJobConfig: () => getJobConfig,
101
106
  getRoutes: () => getRoutes,
107
+ getWebhookConfig: () => getWebhookConfig,
108
+ inc: () => inc,
102
109
  integer: () => integer,
103
110
  isPalbaseExtension: () => isPalbaseExtension,
104
111
  jsonb: () => jsonb,
105
112
  makeEnvDts: () => makeEnvDts,
106
113
  makePurchasesDts: () => makePurchasesDts,
107
114
  makeTypedDB: () => makeTypedDB,
115
+ now: () => now,
108
116
  numeric: () => numeric,
109
117
  parseFileSizeLimit: () => parseFileSizeLimit,
110
118
  policy: () => policy,
@@ -124,6 +132,416 @@ module.exports = __toCommonJS(src_exports);
124
132
 
125
133
  // src/runtime.ts
126
134
  var import_node_async_hooks = require("async_hooks");
135
+
136
+ // src/db/tx-plan.ts
137
+ var TxRefError = class extends Error {
138
+ constructor(message) {
139
+ super(message);
140
+ this.name = "TxRefError";
141
+ }
142
+ };
143
+ var TxPlanError = class extends Error {
144
+ constructor(message) {
145
+ super(message);
146
+ this.name = "TxPlanError";
147
+ }
148
+ };
149
+ var EXPR = /* @__PURE__ */ Symbol.for("palbase.tx.expr");
150
+ var REF = /* @__PURE__ */ Symbol.for("palbase.tx.ref");
151
+ var ROW = /* @__PURE__ */ Symbol.for("palbase.tx.row");
152
+ var ROWS = /* @__PURE__ */ Symbol.for("palbase.tx.rows");
153
+ var TRAPPED_PROPS = [
154
+ "then",
155
+ "valueOf",
156
+ "toString",
157
+ "toJSON",
158
+ Symbol.toPrimitive
159
+ ];
160
+ function trap(prop, what, hint) {
161
+ const name = typeof prop === "symbol" ? prop.description ?? String(prop) : prop;
162
+ throw new TxRefError(
163
+ `${what} was used as a value (via \`${name}\`). Nothing in a transaction callback has run yet, so there is no value to read. ${hint}`
164
+ );
165
+ }
166
+ function now() {
167
+ return makeExpr({ fn: "now" });
168
+ }
169
+ function inc(by) {
170
+ assertFiniteNumber(by, "inc");
171
+ return makeExpr({ fn: "inc", by });
172
+ }
173
+ function dec(by) {
174
+ assertFiniteNumber(by, "dec");
175
+ return makeExpr({ fn: "dec", by });
176
+ }
177
+ function assertFiniteNumber(by, fn) {
178
+ if (typeof by !== "number" || !Number.isFinite(by)) {
179
+ throw new TxPlanError(`${fn}() needs a finite number, got ${String(by)}`);
180
+ }
181
+ }
182
+ function makeExpr(expr) {
183
+ return new Proxy(
184
+ { [EXPR]: expr },
185
+ {
186
+ get(target, prop) {
187
+ if (prop === EXPR) return target[EXPR];
188
+ if (TRAPPED_PROPS.includes(prop)) {
189
+ trap(prop, "A plan expression", "Write it into an operation instead.");
190
+ }
191
+ return void 0;
192
+ }
193
+ }
194
+ );
195
+ }
196
+ function makeRef(op, field) {
197
+ const target = { [REF]: { op, field } };
198
+ return new Proxy(target, {
199
+ get(t, prop) {
200
+ if (prop === REF) return t[REF];
201
+ if (TRAPPED_PROPS.includes(prop)) {
202
+ trap(
203
+ prop,
204
+ `\`${field}\` of a row this transaction has not written yet`,
205
+ "Pass it to another operation in the same plan, or return it from the callback and read it after `transaction()` resolves."
206
+ );
207
+ }
208
+ return void 0;
209
+ }
210
+ });
211
+ }
212
+ function makeRowHandle(op) {
213
+ const target = { [ROW]: op };
214
+ return new Proxy(target, {
215
+ get(t, prop) {
216
+ if (prop === ROW) return t[ROW];
217
+ if (TRAPPED_PROPS.includes(prop)) {
218
+ trap(
219
+ prop,
220
+ "A row this transaction has not written yet",
221
+ "Read one of its columns to reference it, or return the row from the callback and read it after `transaction()` resolves."
222
+ );
223
+ }
224
+ if (typeof prop === "symbol") return void 0;
225
+ return makeRef(op, prop);
226
+ }
227
+ });
228
+ }
229
+ function refDescriptor(v) {
230
+ if (typeof v !== "object" || v === null) return null;
231
+ const d = v[REF];
232
+ return isRefDescriptor(d) ? d : null;
233
+ }
234
+ function isRefDescriptor(d) {
235
+ return typeof d === "object" && d !== null && typeof d.op === "number" && typeof d.field === "string";
236
+ }
237
+ function rowOpIndex(v) {
238
+ if (typeof v !== "object" || v === null) return null;
239
+ const op = v[ROW];
240
+ return typeof op === "number" ? op : null;
241
+ }
242
+ function exprOf(v) {
243
+ if (typeof v !== "object" || v === null) return null;
244
+ const e = v[EXPR];
245
+ return typeof e === "object" && e !== null ? e : null;
246
+ }
247
+ function isRowsHandle(v) {
248
+ return typeof v === "object" && v !== null && v[ROWS] !== void 0;
249
+ }
250
+ function encodeValue(value, column, allowColumnExpr) {
251
+ const ref = refDescriptor(value);
252
+ if (ref) return { $ref: { op: ref.op, field: ref.field } };
253
+ const expr = exprOf(value);
254
+ if (expr) {
255
+ if (expr.fn !== "now" && !allowColumnExpr) {
256
+ throw new TxPlanError(
257
+ `\`${column}\`: ${expr.fn}() reads the column's current value, so it is only valid in updateWhere(where, set).`
258
+ );
259
+ }
260
+ return { $expr: expr };
261
+ }
262
+ if (rowOpIndex(value) !== null) {
263
+ throw new TxPlanError(
264
+ `\`${column}\`: a row handle is not a value. Read the column you meant (e.g. \`row.id\`).`
265
+ );
266
+ }
267
+ if (isRowsHandle(value)) {
268
+ throw new TxPlanError(
269
+ `\`${column}\`: an operation result is not a value. Declare an expectation first (\`.expectOne(err)\`) and read a column from the row.`
270
+ );
271
+ }
272
+ assertNoNestedHandles(value, column);
273
+ return value;
274
+ }
275
+ function assertNoNestedHandles(value, column) {
276
+ if (typeof value !== "object" || value === null) return;
277
+ if (value instanceof Date) return;
278
+ if (refDescriptor(value) || exprOf(value) || rowOpIndex(value) !== null || isRowsHandle(value)) {
279
+ throw new TxPlanError(
280
+ `\`${column}\`: a plan handle is nested inside a value. The server would store it as literal JSON, not resolve it. Put the reference directly in the column.`
281
+ );
282
+ }
283
+ if (Array.isArray(value)) {
284
+ for (const item of value) assertNoNestedHandles(item, column);
285
+ return;
286
+ }
287
+ for (const item of Object.values(value)) {
288
+ assertNoNestedHandles(item, column);
289
+ }
290
+ }
291
+ function encodeMap(map, allowColumnExpr) {
292
+ const out = {};
293
+ for (const key of Object.keys(map).sort()) {
294
+ const value = map[key];
295
+ if (value === void 0) continue;
296
+ out[key] = encodeValue(value, key, allowColumnExpr);
297
+ }
298
+ return out;
299
+ }
300
+ var SKIPPED_OP = -1;
301
+ var TxRowsImpl = class {
302
+ constructor(builder, opIndex, what) {
303
+ this.builder = builder;
304
+ this.opIndex = opIndex;
305
+ this.what = what;
306
+ }
307
+ builder;
308
+ opIndex;
309
+ what;
310
+ // Present so `isRowsHandle` recognises the object; never read for its value.
311
+ [ROWS] = true;
312
+ guarded = false;
313
+ // The type-level `await` guard made real: TS rejects `await rows` at compile
314
+ // time, and reaching this means someone called `.then(...)` by hand.
315
+ then() {
316
+ throw new TxRefError(
317
+ `${this.what} cannot be awaited: a transaction callback builds a plan, it does not run statements. Remove the \`await\`.`
318
+ );
319
+ }
320
+ expectOne(error) {
321
+ this.declareGuard("one", 1, error);
322
+ if (this.opIndex === SKIPPED_OP) throw error;
323
+ return makeRowHandle(this.opIndex);
324
+ }
325
+ expectNone(error) {
326
+ this.declareGuard("none", 0, error);
327
+ }
328
+ expectAtLeast(n, error) {
329
+ assertGuardCount(n, "expectAtLeast");
330
+ this.declareGuard("atLeast", n, error);
331
+ if (this.opIndex === SKIPPED_OP && n > 0) throw error;
332
+ }
333
+ expectAtMost(n, error) {
334
+ assertGuardCount(n, "expectAtMost");
335
+ this.declareGuard("atMost", n, error);
336
+ }
337
+ declareGuard(kind, n, error) {
338
+ if (!(error instanceof Error)) {
339
+ throw new TxPlanError(
340
+ `${this.what}: an expectation needs the Error to throw when it does not hold (e.g. \`.expect\u2026(new Conflict("already accepted"))\`).`
341
+ );
342
+ }
343
+ if (this.guarded) {
344
+ throw new TxPlanError(
345
+ `${this.what} already has an expectation. One operation carries one expectation; declare the second one on its own operation.`
346
+ );
347
+ }
348
+ this.guarded = true;
349
+ if (this.opIndex === SKIPPED_OP) return;
350
+ this.builder.attachGuard(this.opIndex, kind, n, error);
351
+ }
352
+ };
353
+ function assertGuardCount(n, fn) {
354
+ if (!Number.isInteger(n) || n < 0) {
355
+ throw new TxPlanError(`${fn}(n) needs a non-negative integer, got ${String(n)}`);
356
+ }
357
+ }
358
+ var MAX_OPS = 1e3;
359
+ var MAX_ROWS = 5e3;
360
+ var TxPlanBuilder = class {
361
+ ops = [];
362
+ /** Errors handed to expectations, indexed by the `slot` the server echoes. */
363
+ slots = [];
364
+ /** The table surface handed to the callback. Untyped here; the public
365
+ * `transaction()` signatures put the schema types on top. */
366
+ table(name) {
367
+ return {
368
+ insert: (values) => {
369
+ const encoded = encodeMap(values, false);
370
+ if (Object.keys(encoded).length === 0) {
371
+ throw new TxPlanError(`${name}.insert() needs at least one column`);
372
+ }
373
+ return this.push({ op: "insert", table: name, values: encoded }, `${name}.insert()`);
374
+ },
375
+ insertMany: (rows) => {
376
+ if (rows.length === 0) {
377
+ return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);
378
+ }
379
+ if (rows.length > MAX_ROWS) {
380
+ throw new TxPlanError(
381
+ `${name}.insertMany() has ${rows.length} rows; the limit is ${MAX_ROWS}. Split the write across requests.`
382
+ );
383
+ }
384
+ const encoded = rows.map((row) => encodeMap(row, false));
385
+ assertUniformRows(encoded, name);
386
+ return this.push({ op: "insertMany", table: name, rows: encoded }, `${name}.insertMany()`);
387
+ },
388
+ updateWhere: (where, set) => {
389
+ const encodedWhere = encodeMap(where, false);
390
+ const encodedSet = encodeMap(set, true);
391
+ if (Object.keys(encodedWhere).length === 0) {
392
+ throw new TxPlanError(
393
+ `${name}.updateWhere() needs a filter. An update with no filter rewrites the whole table.`
394
+ );
395
+ }
396
+ if (Object.keys(encodedSet).length === 0) {
397
+ throw new TxPlanError(`${name}.updateWhere() needs at least one column to set`);
398
+ }
399
+ return this.push(
400
+ { op: "update", table: name, set: encodedSet, where: encodedWhere },
401
+ `${name}.updateWhere()`
402
+ );
403
+ },
404
+ deleteWhere: (where) => {
405
+ const encodedWhere = encodeMap(where, false);
406
+ if (Object.keys(encodedWhere).length === 0) {
407
+ throw new TxPlanError(
408
+ `${name}.deleteWhere() needs a filter. A delete with no filter empties the table.`
409
+ );
410
+ }
411
+ return this.push(
412
+ { op: "delete", table: name, where: encodedWhere },
413
+ `${name}.deleteWhere()`
414
+ );
415
+ },
416
+ select: (where, options) => {
417
+ const op = { op: "select", table: name };
418
+ const encodedWhere = encodeMap(where ?? {}, false);
419
+ if (Object.keys(encodedWhere).length > 0) op.where = encodedWhere;
420
+ if (options?.limit !== void 0) {
421
+ if (!Number.isInteger(options.limit) || options.limit < 0) {
422
+ throw new TxPlanError(
423
+ `${name}.select(): limit needs a non-negative integer, got ${String(options.limit)}`
424
+ );
425
+ }
426
+ op.limit = options.limit;
427
+ }
428
+ if (options?.lock !== void 0) op.lock = options.lock;
429
+ return this.push(op, `${name}.select()`);
430
+ }
431
+ };
432
+ }
433
+ push(op, what) {
434
+ if (this.ops.length >= MAX_OPS) {
435
+ throw new TxPlanError(
436
+ `this transaction has ${MAX_OPS} operations, which is the limit. Use insertMany() for bulk writes, or split the work across requests.`
437
+ );
438
+ }
439
+ const index = this.ops.length;
440
+ this.ops.push(op);
441
+ return new TxRowsImpl(this, index, what);
442
+ }
443
+ /** Attach an expectation to an op and record its error in the slot table. */
444
+ attachGuard(opIndex, kind, n, error) {
445
+ const op = this.ops[opIndex];
446
+ if (!op) throw new TxPlanError(`internal: expectation on unknown operation ${opIndex}`);
447
+ const slot = this.slots.length;
448
+ this.slots.push(error);
449
+ op.guard = { kind, n, slot };
450
+ }
451
+ /** The serialisable plan. Empty when the callback described no writes. */
452
+ body() {
453
+ return { ops: this.ops };
454
+ }
455
+ /** The error the server's `slot` selects, or `null` when it names one this
456
+ * plan never declared (a server/client disagreement, not a tenant error). */
457
+ errorForSlot(slot) {
458
+ return this.slots[slot] ?? null;
459
+ }
460
+ };
461
+ function assertUniformRows(rows, table) {
462
+ const first = rows[0];
463
+ if (!first) return;
464
+ const want = Object.keys(first);
465
+ const wantKey = want.join(",");
466
+ for (let i = 1; i < rows.length; i++) {
467
+ const got = Object.keys(rows[i]);
468
+ if (got.join(",") !== wantKey) {
469
+ throw new TxPlanError(
470
+ `${table}.insertMany(): every row must set the same columns. Row 0 sets [${want.join(", ")}] but row ${i} sets [${got.join(", ")}]. (A property set to \`undefined\` counts as absent \u2014 use \`null\`.)`
471
+ );
472
+ }
473
+ }
474
+ }
475
+ function materializeResult(value, results) {
476
+ const ref = refDescriptor(value);
477
+ if (ref) {
478
+ const row = rowOf(results, ref.op, `\`${ref.field}\``);
479
+ if (!(ref.field in row)) {
480
+ throw new TxPlanError(
481
+ `the transaction's operation ${ref.op} returned no column \`${ref.field}\`.`
482
+ );
483
+ }
484
+ return row[ref.field];
485
+ }
486
+ const rowOp = rowOpIndex(value);
487
+ if (rowOp !== null) return rowOf(results, rowOp, "a row");
488
+ if (isRowsHandle(value)) {
489
+ throw new TxPlanError(
490
+ "an operation result cannot be returned from a transaction callback: its row count is not known until the plan runs. Declare an expectation (`.expectOne(err)`) and return the row, or a column of it."
491
+ );
492
+ }
493
+ if (Array.isArray(value)) return value.map((item) => materializeResult(item, results));
494
+ if (isPlainObject(value)) {
495
+ const out = {};
496
+ for (const [key, item] of Object.entries(value)) out[key] = materializeResult(item, results);
497
+ return out;
498
+ }
499
+ return value;
500
+ }
501
+ function rowOf(results, opIndex, what) {
502
+ const result = results[opIndex];
503
+ if (!result) {
504
+ throw new TxPlanError(
505
+ `the transaction returned no result for operation ${opIndex}, so ${what} cannot be read.`
506
+ );
507
+ }
508
+ const row = result.rows[0];
509
+ if (!row) {
510
+ throw new TxPlanError(
511
+ `the transaction's operation ${opIndex} returned no row, so ${what} cannot be read.`
512
+ );
513
+ }
514
+ return row;
515
+ }
516
+ function isPlainObject(value) {
517
+ if (typeof value !== "object" || value === null) return false;
518
+ const proto = Object.getPrototypeOf(value);
519
+ return proto === Object.prototype || proto === null;
520
+ }
521
+ async function runTxPlan(transport, tables, builder, fn) {
522
+ const returned = fn({ tables });
523
+ const body = builder.body();
524
+ if (body.ops.length === 0) {
525
+ return materializeResult(returned, []);
526
+ }
527
+ let response;
528
+ try {
529
+ response = await transport.txPlan(body);
530
+ } catch (err) {
531
+ throw translateRejection(err, builder);
532
+ }
533
+ return materializeResult(returned, response.results);
534
+ }
535
+ function translateRejection(err, builder) {
536
+ if (typeof err !== "object" || err === null) return err;
537
+ const rejection = err;
538
+ if (rejection.error_code !== "tx_guard_failed" || typeof rejection.slot !== "number") {
539
+ return err;
540
+ }
541
+ return builder.errorForSlot(rejection.slot) ?? err;
542
+ }
543
+
544
+ // src/runtime.ts
127
545
  var __requestALS = new import_node_async_hooks.AsyncLocalStorage();
128
546
  var runtime = null;
129
547
  function __setRuntime(services) {
@@ -184,10 +602,23 @@ function makeTypedSurface(raw2) {
184
602
  return Object.assign(ops, {
185
603
  tables: makeTablesAccessor(() => raw2),
186
604
  transaction(fn) {
187
- return raw2.transaction((rawTx) => fn({ tables: makeTablesAccessor(() => rawTx) }));
605
+ const builder = new TxPlanBuilder();
606
+ return runTxPlan(raw2, makeTxTablesAccessor(builder), builder, fn);
188
607
  }
189
608
  });
190
609
  }
610
+ function makeTxTablesAccessor(builder) {
611
+ const tablesProxy = new Proxy(
612
+ {},
613
+ {
614
+ get(_t, prop) {
615
+ if (typeof prop !== "string") return void 0;
616
+ return builder.table(prop);
617
+ }
618
+ }
619
+ );
620
+ return tablesProxy;
621
+ }
191
622
  var Database = Object.assign(makeTypedSurface(rawDatabase), {
192
623
  /**
193
624
  * Lazily resolve the runtime's service-role sibling on each call. We do NOT
@@ -782,19 +1213,29 @@ function makeTypedTable(name, raw2) {
782
1213
  };
783
1214
  }
784
1215
  function makeTypedDB(schema, raw2) {
785
- function buildTables(client) {
786
- const tables = {};
787
- for (const key of Object.keys(schema.tables)) {
788
- const tableDef = schema.tables[key];
789
- if (tableDef !== void 0) {
790
- tables[key] = makeTypedTable(tableDef.name, client);
791
- }
1216
+ const tables = {};
1217
+ for (const key of Object.keys(schema.tables)) {
1218
+ const tableDef = schema.tables[key];
1219
+ if (tableDef !== void 0) {
1220
+ tables[key] = makeTypedTable(tableDef.name, raw2);
792
1221
  }
793
- return tables;
794
1222
  }
795
1223
  const result = {
796
- tables: buildTables(raw2),
797
- transaction: (fn) => raw2.transaction((rawTx) => fn({ tables: buildTables(rawTx) }))
1224
+ tables,
1225
+ transaction(fn) {
1226
+ const builder = new TxPlanBuilder();
1227
+ const planTables = {};
1228
+ for (const key of Object.keys(schema.tables)) {
1229
+ const tableDef = schema.tables[key];
1230
+ if (tableDef !== void 0) planTables[key] = builder.table(tableDef.name);
1231
+ }
1232
+ return runTxPlan(
1233
+ raw2,
1234
+ planTables,
1235
+ builder,
1236
+ fn
1237
+ );
1238
+ }
798
1239
  };
799
1240
  return result;
800
1241
  }
@@ -1133,12 +1574,12 @@ var E164_RE = /^\+[1-9]\d{1,14}$/;
1133
1574
  var OTP_LENGTH = 6;
1134
1575
  var OTP_RE = /^\d{6}$/;
1135
1576
  var MIN_PASSWORD_LENGTH = 8;
1136
- function isPlainObject(value) {
1577
+ function isPlainObject2(value) {
1137
1578
  return typeof value === "object" && value !== null && !Array.isArray(value);
1138
1579
  }
1139
1580
  function normalizeSeed(seed, userName) {
1140
1581
  if (seed === void 0) return {};
1141
- if (!isPlainObject(seed)) {
1582
+ if (!isPlainObject2(seed)) {
1142
1583
  throw new Error(`testUser ${JSON.stringify(userName)}: seed must be { <table>: [ {...} ] }`);
1143
1584
  }
1144
1585
  const out = {};
@@ -1154,7 +1595,7 @@ function normalizeSeed(seed, userName) {
1154
1595
  );
1155
1596
  }
1156
1597
  for (const row of rows) {
1157
- if (!isPlainObject(row)) {
1598
+ if (!isPlainObject2(row)) {
1158
1599
  throw new Error(
1159
1600
  `testUser ${JSON.stringify(userName)}: every row in seed.${table} must be an object`
1160
1601
  );
@@ -1165,7 +1606,7 @@ function normalizeSeed(seed, userName) {
1165
1606
  return out;
1166
1607
  }
1167
1608
  function testUser(opts = {}) {
1168
- if (!isPlainObject(opts)) {
1609
+ if (!isPlainObject2(opts)) {
1169
1610
  throw new Error("testUser() expects { email?, password?, phone?, otp?, seed? }");
1170
1611
  }
1171
1612
  const hasEmail = opts.email !== void 0;
@@ -1215,7 +1656,7 @@ function testUser(opts = {}) {
1215
1656
  return { email, password, phone, otp, seed: normalizeSeed(opts.seed, email ?? "<template>") };
1216
1657
  }
1217
1658
  function defineTestUsers(input) {
1218
- if (!isPlainObject(input) || !isPlainObject(input.users)) {
1659
+ if (!isPlainObject2(input) || !isPlainObject2(input.users)) {
1219
1660
  throw new Error("defineTestUsers expects { users: { <name>: testUser({...}) } }");
1220
1661
  }
1221
1662
  const users = {};
@@ -1331,6 +1772,12 @@ function defineFlags(input) {
1331
1772
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
1332
1773
  function Controller(basePath, options = {}) {
1333
1774
  return function(ctor) {
1775
+ const normalized = basePath.replace(/\/+$/, "");
1776
+ if (normalized === "/webhooks" || normalized.startsWith("/webhooks/")) {
1777
+ throw new Error(
1778
+ `@Controller("${basePath}") uses the reserved /webhooks path \u2014 inbound webhooks are served there`
1779
+ );
1780
+ }
1334
1781
  const carrier = ctor;
1335
1782
  const meta = {
1336
1783
  __palbase: "controller",
@@ -1718,12 +2165,81 @@ function defineWorker(config) {
1718
2165
  };
1719
2166
  }
1720
2167
 
2168
+ // src/decorators/webhook.ts
2169
+ var WEBHOOK_META = /* @__PURE__ */ Symbol.for("palbase.backend.webhookMeta");
2170
+ var WEBHOOK_EVENTS = /* @__PURE__ */ Symbol.for("palbase.backend.webhookEvents");
2171
+ function carrierOf3(ctor) {
2172
+ return ctor;
2173
+ }
2174
+ function Webhook(options) {
2175
+ return function(ctor) {
2176
+ const carrier = carrierOf3(ctor);
2177
+ Object.defineProperty(carrier, WEBHOOK_META, {
2178
+ value: options,
2179
+ enumerable: false,
2180
+ configurable: true,
2181
+ writable: false
2182
+ });
2183
+ Object.defineProperty(carrier, "__palbase", {
2184
+ value: "webhook",
2185
+ enumerable: false,
2186
+ configurable: true,
2187
+ writable: false
2188
+ });
2189
+ return ctor;
2190
+ };
2191
+ }
2192
+ function On(event) {
2193
+ return function(target, fnName) {
2194
+ const carrier = carrierOf3(target.constructor);
2195
+ const existing = carrier[WEBHOOK_EVENTS];
2196
+ const entries = existing ? [...existing] : [];
2197
+ entries.push({ event, fnName: String(fnName) });
2198
+ Object.defineProperty(carrier, WEBHOOK_EVENTS, {
2199
+ value: entries,
2200
+ enumerable: false,
2201
+ configurable: true,
2202
+ writable: false
2203
+ });
2204
+ };
2205
+ }
2206
+ function getWebhookConfig(ctor) {
2207
+ const carrier = carrierOf3(ctor);
2208
+ const meta = carrier[WEBHOOK_META];
2209
+ const entries = carrier[WEBHOOK_EVENTS] ?? [];
2210
+ if (!meta) {
2211
+ throw new Error(
2212
+ `@On used on a class that is not decorated with @Webhook (${ctor.name ?? "anonymous"})`
2213
+ );
2214
+ }
2215
+ if (!meta.provider && !meta.signature) {
2216
+ throw new Error(
2217
+ "@Webhook requires either a `provider` preset or an explicit `signature` \u2014 an endpoint with no verification would accept forged deliveries"
2218
+ );
2219
+ }
2220
+ if (!meta.secret?.env) {
2221
+ throw new Error('@Webhook requires `secret: { env: "VAR_NAME" }`');
2222
+ }
2223
+ if (entries.length === 0) {
2224
+ throw new Error("@Webhook requires at least one @On handler");
2225
+ }
2226
+ const instance = new ctor();
2227
+ const events = /* @__PURE__ */ Object.create(null);
2228
+ for (const entry of entries) {
2229
+ if (Object.prototype.hasOwnProperty.call(events, entry.event)) {
2230
+ throw new Error(`@On("${entry.event}") declared twice on the same webhook`);
2231
+ }
2232
+ events[entry.event] = (event, metaArg) => instance[entry.fnName].call(instance, event, metaArg);
2233
+ }
2234
+ return {
2235
+ ...meta.provider ? { provider: meta.provider } : {},
2236
+ ...meta.signature ? { signature: meta.signature } : {},
2237
+ secret: meta.secret,
2238
+ events
2239
+ };
2240
+ }
2241
+
1721
2242
  // src/job.ts
1722
- var VALID_JOB_NAME = /^[a-zA-Z0-9_-]+$/;
1723
- var MAX_TIMEOUT_SECONDS = 300;
1724
- var JOB_DEFAULTS = {
1725
- timeout: 30
1726
- };
1727
2243
  function validateCronExpression(expression) {
1728
2244
  const trimmed = expression.trim();
1729
2245
  if (trimmed === "") {
@@ -1789,115 +2305,56 @@ function validateCronField(field, name, min, max) {
1789
2305
  }
1790
2306
  return null;
1791
2307
  }
1792
- function defineJob(config) {
1793
- if (!config.name || config.name.trim() === "") {
1794
- throw new Error("Job name is required");
1795
- }
1796
- if (!VALID_JOB_NAME.test(config.name)) {
1797
- throw new Error(
1798
- `Invalid job name "${config.name}": must match [a-zA-Z0-9_-]+`
1799
- );
1800
- }
1801
- if (!config.schedule || config.schedule.trim() === "") {
1802
- throw new Error("Job schedule is required");
1803
- }
1804
- const cronError = validateCronExpression(config.schedule);
1805
- if (cronError !== null) {
1806
- throw new Error(cronError);
1807
- }
1808
- if (!config.handler) {
1809
- throw new Error("Job handler is required");
1810
- }
1811
- if (config.timeout !== void 0 && config.timeout <= 0) {
1812
- throw new Error("Job timeout must be a positive number");
1813
- }
1814
- if (config.timeout !== void 0 && !Number.isInteger(config.timeout)) {
1815
- throw new Error("Job timeout must be an integer");
1816
- }
1817
- if (config.timeout !== void 0 && config.timeout > MAX_TIMEOUT_SECONDS) {
1818
- throw new Error(
1819
- `Job timeout ${config.timeout}s exceeds maximum ${MAX_TIMEOUT_SECONDS}s`
1820
- );
1821
- }
1822
- return {
1823
- name: config.name,
1824
- schedule: config.schedule.trim(),
1825
- timeout: config.timeout ?? JOB_DEFAULTS.timeout,
1826
- handler: config.handler
2308
+
2309
+ // src/decorators/job.ts
2310
+ var DEFAULT_TIMEOUT_SECONDS = 30;
2311
+ var MAX_TIMEOUT_SECONDS = 300;
2312
+ var JOB_META = /* @__PURE__ */ Symbol.for("palbase.backend.jobMeta");
2313
+ function Job(options) {
2314
+ return function(ctor) {
2315
+ const carrier = ctor;
2316
+ Object.defineProperty(carrier, JOB_META, {
2317
+ value: options,
2318
+ enumerable: false,
2319
+ configurable: true,
2320
+ writable: false
2321
+ });
2322
+ Object.defineProperty(carrier, "__palbase", {
2323
+ value: "job",
2324
+ enumerable: false,
2325
+ configurable: true,
2326
+ writable: false
2327
+ });
2328
+ return ctor;
1827
2329
  };
1828
2330
  }
1829
-
1830
- // src/webhook.ts
1831
- var VALID_WEBHOOK_PATH = /^\/[a-zA-Z0-9/_-]+$/;
1832
- function defineWebhook(config) {
1833
- if ("provider" in config) {
1834
- return validateProviderWebhook(config);
1835
- }
1836
- return validateCustomWebhook(config);
1837
- }
1838
- function validateProviderWebhook(config) {
1839
- if (!config.provider) {
1840
- throw new Error("Webhook provider is required");
1841
- }
1842
- const validProviders = [
1843
- "stripe",
1844
- "github",
1845
- "twilio",
1846
- "sendgrid",
1847
- "slack",
1848
- "discord",
1849
- "livekit"
1850
- ];
1851
- if (!validProviders.includes(config.provider)) {
2331
+ function getJobConfig(ctor) {
2332
+ const meta = ctor[JOB_META];
2333
+ if (!meta) {
1852
2334
  throw new Error(
1853
- `Invalid webhook provider "${config.provider}": must be one of ${validProviders.join(", ")}`
2335
+ `getJobConfig on a class with no @Job decorator (${ctor.name ?? "anonymous"})`
1854
2336
  );
1855
2337
  }
1856
- if (!config.secret) {
1857
- throw new Error('Webhook secret is required (use { env: "SECRET_NAME" })');
2338
+ if (!meta.schedule || meta.schedule.trim() === "") {
2339
+ throw new Error("@Job requires a `schedule` cron expression");
1858
2340
  }
1859
- if (typeof config.secret.env !== "string" || config.secret.env.trim() === "") {
1860
- throw new Error("Webhook secret env name must be a non-empty string");
2341
+ const cronError = validateCronExpression(meta.schedule);
2342
+ if (cronError) {
2343
+ throw new Error(`@Job has an invalid cron schedule: ${cronError}`);
1861
2344
  }
1862
- if (!config.events || Object.keys(config.events).length === 0) {
1863
- throw new Error("At least one event handler is required");
2345
+ const timeout = meta.timeout ?? DEFAULT_TIMEOUT_SECONDS;
2346
+ if (!Number.isInteger(timeout) || timeout <= 0) {
2347
+ throw new Error("@Job `timeout` must be a positive whole number of seconds");
1864
2348
  }
1865
- for (const [eventName, handler] of Object.entries(config.events)) {
1866
- if (typeof handler !== "function") {
1867
- throw new Error(`Event handler for "${eventName}" must be a function`);
1868
- }
2349
+ if (timeout > MAX_TIMEOUT_SECONDS) {
2350
+ throw new Error(`@Job \`timeout\` exceeds the ${MAX_TIMEOUT_SECONDS}s sandbox ceiling`);
1869
2351
  }
1870
- return {
1871
- type: "provider",
1872
- provider: config.provider,
1873
- secret: config.secret,
1874
- events: config.events
1875
- };
1876
- }
1877
- function validateCustomWebhook(config) {
1878
- if (!config.path || config.path.trim() === "") {
1879
- throw new Error("Webhook path is required");
1880
- }
1881
- if (!VALID_WEBHOOK_PATH.test(config.path)) {
1882
- throw new Error(
1883
- `Invalid webhook path "${config.path}": must start with / and contain only alphanumeric, hyphen, underscore, slash`
1884
- );
2352
+ const instance = new ctor();
2353
+ if (typeof instance.run !== "function") {
2354
+ throw new Error("@Job class must declare an async run() method");
1885
2355
  }
1886
- if (!config.handler) {
1887
- throw new Error("Webhook handler is required");
1888
- }
1889
- if (typeof config.handler !== "function") {
1890
- throw new Error("Webhook handler must be a function");
1891
- }
1892
- if (config.verify !== void 0 && typeof config.verify !== "function") {
1893
- throw new Error("Webhook verify must be a function");
1894
- }
1895
- return {
1896
- type: "custom",
1897
- path: config.path,
1898
- verify: config.verify,
1899
- handler: config.handler
1900
- };
2356
+ const run = instance.run.bind(instance);
2357
+ return { schedule: meta.schedule, timeout, handler: run };
1901
2358
  }
1902
2359
 
1903
2360
  // src/resource.ts
@@ -2002,10 +2459,12 @@ var import_zod2 = require("zod");
2002
2459
  Get,
2003
2460
  Headers,
2004
2461
  HttpError,
2462
+ Job,
2005
2463
  Log,
2006
2464
  NOTIFICATIONS_CONFIG_KIND,
2007
2465
  NotFound,
2008
2466
  Notifications,
2467
+ On,
2009
2468
  OptionalUser,
2010
2469
  PALBASE_EXTENSIONS,
2011
2470
  PROVIDER_CATALOG,
@@ -2031,10 +2490,13 @@ var import_zod2 = require("zod");
2031
2490
  TEST_USERS_CONFIG_KIND,
2032
2491
  TooManyRequests,
2033
2492
  TraceId,
2493
+ TxPlanError,
2494
+ TxRefError,
2034
2495
  Unauthorized,
2035
2496
  Upload,
2036
2497
  UploadedObject,
2037
2498
  User,
2499
+ Webhook,
2038
2500
  __getRuntime,
2039
2501
  __registerResource,
2040
2502
  __requestALS,
@@ -2047,29 +2509,32 @@ var import_zod2 = require("zod");
2047
2509
  boolean,
2048
2510
  bucket,
2049
2511
  buildProvider,
2512
+ dec,
2050
2513
  defineEgress,
2051
2514
  defineError,
2052
2515
  defineFlags,
2053
- defineJob,
2054
2516
  defineMiddleware,
2055
2517
  defineNotifications,
2056
2518
  defineSchema,
2057
2519
  defineStorage,
2058
2520
  defineTestUsers,
2059
- defineWebhook,
2060
2521
  defineWorker,
2061
2522
  documents,
2062
2523
  entitlementFor,
2063
2524
  enumType,
2064
2525
  flag,
2065
2526
  getErrorRegistry,
2527
+ getJobConfig,
2066
2528
  getRoutes,
2529
+ getWebhookConfig,
2530
+ inc,
2067
2531
  integer,
2068
2532
  isPalbaseExtension,
2069
2533
  jsonb,
2070
2534
  makeEnvDts,
2071
2535
  makePurchasesDts,
2072
2536
  makeTypedDB,
2537
+ now,
2073
2538
  numeric,
2074
2539
  parseFileSizeLimit,
2075
2540
  policy,