@palbase/backend 11.0.0 → 12.0.1

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,
@@ -72,6 +74,7 @@ __export(src_exports, {
72
74
  Upload: () => Upload,
73
75
  UploadedObject: () => UploadedObject,
74
76
  User: () => User,
77
+ Webhook: () => Webhook,
75
78
  __getRuntime: () => __getRuntime,
76
79
  __registerResource: () => __registerResource,
77
80
  __requestALS: () => __requestALS,
@@ -88,20 +91,20 @@ __export(src_exports, {
88
91
  defineEgress: () => defineEgress,
89
92
  defineError: () => defineError,
90
93
  defineFlags: () => defineFlags,
91
- defineJob: () => defineJob,
92
94
  defineMiddleware: () => defineMiddleware,
93
95
  defineNotifications: () => defineNotifications,
94
96
  defineSchema: () => defineSchema,
95
97
  defineStorage: () => defineStorage,
96
98
  defineTestUsers: () => defineTestUsers,
97
- defineWebhook: () => defineWebhook,
98
99
  defineWorker: () => defineWorker,
99
100
  documents: () => documents,
100
101
  entitlementFor: () => entitlementFor,
101
102
  enumType: () => enumType,
102
103
  flag: () => flag,
103
104
  getErrorRegistry: () => getErrorRegistry,
105
+ getJobConfig: () => getJobConfig,
104
106
  getRoutes: () => getRoutes,
107
+ getWebhookConfig: () => getWebhookConfig,
105
108
  inc: () => inc,
106
109
  integer: () => integer,
107
110
  isPalbaseExtension: () => isPalbaseExtension,
@@ -1769,6 +1772,12 @@ function defineFlags(input) {
1769
1772
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
1770
1773
  function Controller(basePath, options = {}) {
1771
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
+ }
1772
1781
  const carrier = ctor;
1773
1782
  const meta = {
1774
1783
  __palbase: "controller",
@@ -2156,12 +2165,99 @@ function defineWorker(config) {
2156
2165
  };
2157
2166
  }
2158
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.signature) {
2221
+ const sig = meta.signature;
2222
+ if (!sig.header) {
2223
+ throw new Error("@Webhook signature requires `header` \u2014 the header the signature arrives in");
2224
+ }
2225
+ if (sig.algo !== "hmac-sha256" && sig.algo !== "hmac-sha1") {
2226
+ throw new Error(`@Webhook signature has an unsupported algo "${sig.algo}"`);
2227
+ }
2228
+ if (sig.encoding !== "hex" && sig.encoding !== "base64") {
2229
+ throw new Error(`@Webhook signature has an unsupported encoding "${sig.encoding}"`);
2230
+ }
2231
+ if (!sig.signs?.includes("{body}")) {
2232
+ throw new Error("@Webhook signature `signs` must contain {body} \u2014 signing a constant is not a signature");
2233
+ }
2234
+ if (sig.signs.includes("{ts}") && !sig.timestampHeader) {
2235
+ throw new Error("@Webhook signature uses {ts} but declares no `timestampHeader` to read it from");
2236
+ }
2237
+ }
2238
+ if (!meta.secret?.env) {
2239
+ throw new Error('@Webhook requires `secret: { env: "VAR_NAME" }`');
2240
+ }
2241
+ if (entries.length === 0) {
2242
+ throw new Error("@Webhook requires at least one @On handler");
2243
+ }
2244
+ const instance = new ctor();
2245
+ const events = /* @__PURE__ */ Object.create(null);
2246
+ for (const entry of entries) {
2247
+ if (Object.prototype.hasOwnProperty.call(events, entry.event)) {
2248
+ throw new Error(`@On("${entry.event}") declared twice on the same webhook`);
2249
+ }
2250
+ events[entry.event] = (event, metaArg) => instance[entry.fnName].call(instance, event, metaArg);
2251
+ }
2252
+ return {
2253
+ ...meta.provider ? { provider: meta.provider } : {},
2254
+ ...meta.signature ? { signature: meta.signature } : {},
2255
+ secret: meta.secret,
2256
+ events
2257
+ };
2258
+ }
2259
+
2159
2260
  // src/job.ts
2160
- var VALID_JOB_NAME = /^[a-zA-Z0-9_-]+$/;
2161
- var MAX_TIMEOUT_SECONDS = 300;
2162
- var JOB_DEFAULTS = {
2163
- timeout: 30
2164
- };
2165
2261
  function validateCronExpression(expression) {
2166
2262
  const trimmed = expression.trim();
2167
2263
  if (trimmed === "") {
@@ -2227,115 +2323,56 @@ function validateCronField(field, name, min, max) {
2227
2323
  }
2228
2324
  return null;
2229
2325
  }
2230
- function defineJob(config) {
2231
- if (!config.name || config.name.trim() === "") {
2232
- throw new Error("Job name is required");
2233
- }
2234
- if (!VALID_JOB_NAME.test(config.name)) {
2235
- throw new Error(
2236
- `Invalid job name "${config.name}": must match [a-zA-Z0-9_-]+`
2237
- );
2238
- }
2239
- if (!config.schedule || config.schedule.trim() === "") {
2240
- throw new Error("Job schedule is required");
2241
- }
2242
- const cronError = validateCronExpression(config.schedule);
2243
- if (cronError !== null) {
2244
- throw new Error(cronError);
2245
- }
2246
- if (!config.handler) {
2247
- throw new Error("Job handler is required");
2248
- }
2249
- if (config.timeout !== void 0 && config.timeout <= 0) {
2250
- throw new Error("Job timeout must be a positive number");
2251
- }
2252
- if (config.timeout !== void 0 && !Number.isInteger(config.timeout)) {
2253
- throw new Error("Job timeout must be an integer");
2254
- }
2255
- if (config.timeout !== void 0 && config.timeout > MAX_TIMEOUT_SECONDS) {
2256
- throw new Error(
2257
- `Job timeout ${config.timeout}s exceeds maximum ${MAX_TIMEOUT_SECONDS}s`
2258
- );
2259
- }
2260
- return {
2261
- name: config.name,
2262
- schedule: config.schedule.trim(),
2263
- timeout: config.timeout ?? JOB_DEFAULTS.timeout,
2264
- handler: config.handler
2326
+
2327
+ // src/decorators/job.ts
2328
+ var DEFAULT_TIMEOUT_SECONDS = 30;
2329
+ var MAX_TIMEOUT_SECONDS = 300;
2330
+ var JOB_META = /* @__PURE__ */ Symbol.for("palbase.backend.jobMeta");
2331
+ function Job(options) {
2332
+ return function(ctor) {
2333
+ const carrier = ctor;
2334
+ Object.defineProperty(carrier, JOB_META, {
2335
+ value: options,
2336
+ enumerable: false,
2337
+ configurable: true,
2338
+ writable: false
2339
+ });
2340
+ Object.defineProperty(carrier, "__palbase", {
2341
+ value: "job",
2342
+ enumerable: false,
2343
+ configurable: true,
2344
+ writable: false
2345
+ });
2346
+ return ctor;
2265
2347
  };
2266
2348
  }
2267
-
2268
- // src/webhook.ts
2269
- var VALID_WEBHOOK_PATH = /^\/[a-zA-Z0-9/_-]+$/;
2270
- function defineWebhook(config) {
2271
- if ("provider" in config) {
2272
- return validateProviderWebhook(config);
2273
- }
2274
- return validateCustomWebhook(config);
2275
- }
2276
- function validateProviderWebhook(config) {
2277
- if (!config.provider) {
2278
- throw new Error("Webhook provider is required");
2279
- }
2280
- const validProviders = [
2281
- "stripe",
2282
- "github",
2283
- "twilio",
2284
- "sendgrid",
2285
- "slack",
2286
- "discord",
2287
- "livekit"
2288
- ];
2289
- if (!validProviders.includes(config.provider)) {
2349
+ function getJobConfig(ctor) {
2350
+ const meta = ctor[JOB_META];
2351
+ if (!meta) {
2290
2352
  throw new Error(
2291
- `Invalid webhook provider "${config.provider}": must be one of ${validProviders.join(", ")}`
2353
+ `getJobConfig on a class with no @Job decorator (${ctor.name ?? "anonymous"})`
2292
2354
  );
2293
2355
  }
2294
- if (!config.secret) {
2295
- throw new Error('Webhook secret is required (use { env: "SECRET_NAME" })');
2356
+ if (!meta.schedule || meta.schedule.trim() === "") {
2357
+ throw new Error("@Job requires a `schedule` cron expression");
2296
2358
  }
2297
- if (typeof config.secret.env !== "string" || config.secret.env.trim() === "") {
2298
- throw new Error("Webhook secret env name must be a non-empty string");
2359
+ const cronError = validateCronExpression(meta.schedule);
2360
+ if (cronError) {
2361
+ throw new Error(`@Job has an invalid cron schedule: ${cronError}`);
2299
2362
  }
2300
- if (!config.events || Object.keys(config.events).length === 0) {
2301
- throw new Error("At least one event handler is required");
2363
+ const timeout = meta.timeout ?? DEFAULT_TIMEOUT_SECONDS;
2364
+ if (!Number.isInteger(timeout) || timeout <= 0) {
2365
+ throw new Error("@Job `timeout` must be a positive whole number of seconds");
2302
2366
  }
2303
- for (const [eventName, handler] of Object.entries(config.events)) {
2304
- if (typeof handler !== "function") {
2305
- throw new Error(`Event handler for "${eventName}" must be a function`);
2306
- }
2367
+ if (timeout > MAX_TIMEOUT_SECONDS) {
2368
+ throw new Error(`@Job \`timeout\` exceeds the ${MAX_TIMEOUT_SECONDS}s sandbox ceiling`);
2307
2369
  }
2308
- return {
2309
- type: "provider",
2310
- provider: config.provider,
2311
- secret: config.secret,
2312
- events: config.events
2313
- };
2314
- }
2315
- function validateCustomWebhook(config) {
2316
- if (!config.path || config.path.trim() === "") {
2317
- throw new Error("Webhook path is required");
2318
- }
2319
- if (!VALID_WEBHOOK_PATH.test(config.path)) {
2320
- throw new Error(
2321
- `Invalid webhook path "${config.path}": must start with / and contain only alphanumeric, hyphen, underscore, slash`
2322
- );
2323
- }
2324
- if (!config.handler) {
2325
- throw new Error("Webhook handler is required");
2370
+ const instance = new ctor();
2371
+ if (typeof instance.run !== "function") {
2372
+ throw new Error("@Job class must declare an async run() method");
2326
2373
  }
2327
- if (typeof config.handler !== "function") {
2328
- throw new Error("Webhook handler must be a function");
2329
- }
2330
- if (config.verify !== void 0 && typeof config.verify !== "function") {
2331
- throw new Error("Webhook verify must be a function");
2332
- }
2333
- return {
2334
- type: "custom",
2335
- path: config.path,
2336
- verify: config.verify,
2337
- handler: config.handler
2338
- };
2374
+ const run = instance.run.bind(instance);
2375
+ return { schedule: meta.schedule, timeout, handler: run };
2339
2376
  }
2340
2377
 
2341
2378
  // src/resource.ts
@@ -2440,10 +2477,12 @@ var import_zod2 = require("zod");
2440
2477
  Get,
2441
2478
  Headers,
2442
2479
  HttpError,
2480
+ Job,
2443
2481
  Log,
2444
2482
  NOTIFICATIONS_CONFIG_KIND,
2445
2483
  NotFound,
2446
2484
  Notifications,
2485
+ On,
2447
2486
  OptionalUser,
2448
2487
  PALBASE_EXTENSIONS,
2449
2488
  PROVIDER_CATALOG,
@@ -2475,6 +2514,7 @@ var import_zod2 = require("zod");
2475
2514
  Upload,
2476
2515
  UploadedObject,
2477
2516
  User,
2517
+ Webhook,
2478
2518
  __getRuntime,
2479
2519
  __registerResource,
2480
2520
  __requestALS,
@@ -2491,20 +2531,20 @@ var import_zod2 = require("zod");
2491
2531
  defineEgress,
2492
2532
  defineError,
2493
2533
  defineFlags,
2494
- defineJob,
2495
2534
  defineMiddleware,
2496
2535
  defineNotifications,
2497
2536
  defineSchema,
2498
2537
  defineStorage,
2499
2538
  defineTestUsers,
2500
- defineWebhook,
2501
2539
  defineWorker,
2502
2540
  documents,
2503
2541
  entitlementFor,
2504
2542
  enumType,
2505
2543
  flag,
2506
2544
  getErrorRegistry,
2545
+ getJobConfig,
2507
2546
  getRoutes,
2547
+ getWebhookConfig,
2508
2548
  inc,
2509
2549
  integer,
2510
2550
  isPalbaseExtension,