@getstrata/bootstrap 0.2.13 → 0.2.16

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.
@@ -1,4 +1,103 @@
1
1
  // @bun
2
+ // ../../src/core/contracts/applicationContext.ts
3
+ function getRequiredDependency(dependencies, key) {
4
+ const dependency = dependencies[key];
5
+ if (dependency === undefined) {
6
+ throw new Error(`Required dependency "${String(key)}" is not registered.`);
7
+ }
8
+ return dependency;
9
+ }
10
+
11
+ // ../../src/core/contracts/serviceTokens.ts
12
+ var CORE_CONFIG_TOKEN = "core.config";
13
+ var CORE_CACHE_TOKEN = "core.cache";
14
+ var CORE_QUEUE_TOKEN = "core.queue";
15
+ var CORE_POLICY_GATE_TOKEN = "core.policyGate";
16
+ var CORE_AUTH_TOKEN = "core.auth";
17
+ var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
18
+
19
+ // ../../src/core/logging/logger.ts
20
+ class Logger {
21
+ channel;
22
+ constructor(channel = "app") {
23
+ this.channel = channel;
24
+ }
25
+ write(level, message, context = {}) {
26
+ const entry = {
27
+ level,
28
+ channel: this.channel,
29
+ message,
30
+ timestamp: new Date().toISOString(),
31
+ ...context
32
+ };
33
+ const line = JSON.stringify(entry);
34
+ if (level === "error") {
35
+ console.error(line);
36
+ return;
37
+ }
38
+ console.log(line);
39
+ }
40
+ debug(message, context) {
41
+ this.write("debug", message, context);
42
+ }
43
+ info(message, context) {
44
+ this.write("info", message, context);
45
+ }
46
+ warn(message, context) {
47
+ this.write("warn", message, context);
48
+ }
49
+ error(message, context) {
50
+ this.write("error", message, context);
51
+ }
52
+ }
53
+ var appLogger = new Logger("app");
54
+
55
+ // ../../src/core/runtime/applicationRegistry.ts
56
+ var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
57
+ var activeContext;
58
+ function readStoredApplicationContext() {
59
+ if (activeContext) {
60
+ return activeContext;
61
+ }
62
+ const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
63
+ if (globalContext) {
64
+ activeContext = globalContext;
65
+ }
66
+ return activeContext;
67
+ }
68
+ function setActiveApplicationContext(context) {
69
+ activeContext = context;
70
+ globalThis[APPLICATION_CONTEXT_KEY] = context;
71
+ }
72
+ function requireActiveApplicationContext() {
73
+ const context = readStoredApplicationContext();
74
+ if (!context) {
75
+ throw new Error("The application context has not been bootstrapped.");
76
+ }
77
+ return context;
78
+ }
79
+ function resolveApplicationCache() {
80
+ return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
81
+ }
82
+ function resolveApplicationQueue() {
83
+ return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
84
+ }
85
+ function resolveApplicationAuth() {
86
+ return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
87
+ }
88
+ function resolveApplicationPolicyGate() {
89
+ return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
90
+ }
91
+ function resolveApplicationConfig() {
92
+ return requireActiveApplicationContext().config;
93
+ }
94
+ function resolveApplicationLogger() {
95
+ return appLogger;
96
+ }
97
+ function resolveApplicationDependencies() {
98
+ return requireActiveApplicationContext().dependencies;
99
+ }
100
+
2
101
  // ../../src/core/errors/http.ts
3
102
  class HttpError extends Error {
4
103
  status;
@@ -301,105 +400,7 @@ class MembershipService {
301
400
  }
302
401
  var membershipService_default = MembershipService;
303
402
 
304
- // ../../src/core/contracts/applicationContext.ts
305
- function getRequiredDependency(dependencies, key) {
306
- const dependency = dependencies[key];
307
- if (dependency === undefined) {
308
- throw new Error(`Required dependency "${String(key)}" is not registered.`);
309
- }
310
- return dependency;
311
- }
312
-
313
- // ../../src/core/contracts/serviceTokens.ts
314
- var CORE_CONFIG_TOKEN = "core.config";
315
- var CORE_CACHE_TOKEN = "core.cache";
316
- var CORE_QUEUE_TOKEN = "core.queue";
317
- var CORE_POLICY_GATE_TOKEN = "core.policyGate";
318
- var CORE_AUTH_TOKEN = "core.auth";
319
- var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
320
-
321
- // ../../src/core/logging/logger.ts
322
- class Logger {
323
- channel;
324
- constructor(channel = "app") {
325
- this.channel = channel;
326
- }
327
- write(level, message, context = {}) {
328
- const entry = {
329
- level,
330
- channel: this.channel,
331
- message,
332
- timestamp: new Date().toISOString(),
333
- ...context
334
- };
335
- const line = JSON.stringify(entry);
336
- if (level === "error") {
337
- console.error(line);
338
- return;
339
- }
340
- console.log(line);
341
- }
342
- debug(message, context) {
343
- this.write("debug", message, context);
344
- }
345
- info(message, context) {
346
- this.write("info", message, context);
347
- }
348
- warn(message, context) {
349
- this.write("warn", message, context);
350
- }
351
- error(message, context) {
352
- this.write("error", message, context);
353
- }
354
- }
355
- var appLogger = new Logger("app");
356
-
357
- // ../../src/core/runtime/applicationRegistry.ts
358
- var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
359
- var activeContext;
360
- function readStoredApplicationContext() {
361
- if (activeContext) {
362
- return activeContext;
363
- }
364
- const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
365
- if (globalContext) {
366
- activeContext = globalContext;
367
- }
368
- return activeContext;
369
- }
370
- function setActiveApplicationContext(context) {
371
- activeContext = context;
372
- globalThis[APPLICATION_CONTEXT_KEY] = context;
373
- }
374
- function requireActiveApplicationContext() {
375
- const context = readStoredApplicationContext();
376
- if (!context) {
377
- throw new Error("The application context has not been bootstrapped.");
378
- }
379
- return context;
380
- }
381
- function resolveApplicationCache() {
382
- return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
383
- }
384
- function resolveApplicationQueue() {
385
- return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
386
- }
387
- function resolveApplicationAuth() {
388
- return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
389
- }
390
- function resolveApplicationPolicyGate() {
391
- return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
392
- }
393
- function resolveApplicationConfig() {
394
- return requireActiveApplicationContext().config;
395
- }
396
- function resolveApplicationLogger() {
397
- return appLogger;
398
- }
399
- function resolveApplicationDependencies() {
400
- return requireActiveApplicationContext().dependencies;
401
- }
402
- // ../../src/bootstrap/membershipService.ts
403
+ // ../../src/core/auth/resolveMembershipService.ts
403
404
  function resolveMembershipService() {
404
405
  const dependencies = resolveApplicationDependencies();
405
406
  if (dependencies.container.has("core.membership")) {
@@ -3085,7 +3085,6 @@ var failedJobService_default = FailedJobService;
3085
3085
 
3086
3086
  // ../../src/core/queue/jobRegistry.ts
3087
3087
  class JobRegistry {
3088
- constructor() {}
3089
3088
  factories = new Map;
3090
3089
  instances = new WeakMap;
3091
3090
  register(name, factory) {
@@ -3109,7 +3108,17 @@ class JobRegistry {
3109
3108
  return [...this.factories.keys()];
3110
3109
  }
3111
3110
  }
3112
- var jobRegistry = new JobRegistry;
3111
+ var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
3112
+ function readSharedJobRegistry() {
3113
+ const globalRegistry = globalThis[JOB_REGISTRY_KEY];
3114
+ if (globalRegistry) {
3115
+ return globalRegistry;
3116
+ }
3117
+ const registry = new JobRegistry;
3118
+ globalThis[JOB_REGISTRY_KEY] = registry;
3119
+ return registry;
3120
+ }
3121
+ var jobRegistry = readSharedJobRegistry();
3113
3122
 
3114
3123
  // ../../src/core/queue/jobRunner.ts
3115
3124
  async function runQueueJob(envelope, failedJobs) {
@@ -3225,192 +3234,15 @@ function createProductionQueue(driver, options = {}) {
3225
3234
  return new ResilientQueue(failedJobs, driver === "async");
3226
3235
  }
3227
3236
 
3228
- // ../../src/core/jobs/dispatchWebhookJob.ts
3229
- import { createHmac as createHmac2 } from "crypto";
3230
-
3231
- // ../../src/core/security/safeUrl.ts
3232
- import { lookup as dnsLookupImpl } from "dns/promises";
3233
- var dnsLookup = dnsLookupImpl;
3234
- var BLOCKED_HOSTNAMES = new Set([
3235
- "localhost",
3236
- "127.0.0.1",
3237
- "0.0.0.0",
3238
- "::1",
3239
- "metadata.google.internal"
3240
- ]);
3241
- function isPrivateIpv4(hostname) {
3242
- const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
3243
- if (!match) {
3244
- return false;
3245
- }
3246
- const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
3247
- if (octets.some((octet) => octet < 0 || octet > 255)) {
3248
- return true;
3249
- }
3250
- const [a = 0, b = 0] = octets;
3251
- if (a === 10) {
3252
- return true;
3253
- }
3254
- if (a === 127) {
3255
- return true;
3256
- }
3257
- if (a === 0) {
3258
- return true;
3259
- }
3260
- if (a === 169 && b === 254) {
3261
- return true;
3262
- }
3263
- if (a === 172 && b >= 16 && b <= 31) {
3264
- return true;
3265
- }
3266
- if (a === 192 && b === 168) {
3267
- return true;
3268
- }
3269
- return false;
3270
- }
3271
- function isBlockedHostname(hostname) {
3272
- const normalized = hostname.trim().toLowerCase();
3273
- if (normalized.length === 0) {
3274
- return true;
3275
- }
3276
- if (BLOCKED_HOSTNAMES.has(normalized)) {
3277
- return true;
3278
- }
3279
- if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
3280
- return true;
3281
- }
3282
- if (normalized.includes(":")) {
3283
- return true;
3284
- }
3285
- return isPrivateIpv4(normalized);
3286
- }
3287
- function assertSafeOutboundUrl(rawUrl, options = {}) {
3288
- let parsed;
3289
- try {
3290
- parsed = new URL(rawUrl);
3291
- } catch {
3292
- throw new BadRequestError("Webhook URL is invalid.");
3293
- }
3294
- if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
3295
- throw new BadRequestError("Webhook URL must use HTTPS.");
3296
- }
3297
- if (parsed.username || parsed.password) {
3298
- throw new BadRequestError("Webhook URL must not include credentials.");
3299
- }
3300
- if (isBlockedHostname(parsed.hostname)) {
3301
- throw new BadRequestError("Webhook URL targets a blocked host.");
3302
- }
3303
- return parsed;
3304
- }
3305
- function isBlockedIpAddress(address) {
3306
- return isBlockedHostname(address.trim().toLowerCase());
3307
- }
3308
- async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
3309
- const parsed = assertSafeOutboundUrl(rawUrl, options);
3310
- if (options.resolveDns === false) {
3311
- return parsed;
3312
- }
3313
- const hostname = parsed.hostname.trim().toLowerCase();
3314
- const results = await dnsLookup(hostname, { all: true, verbatim: true });
3315
- if (results.some((result) => isBlockedIpAddress(result.address))) {
3316
- throw new BadRequestError("Webhook URL targets a blocked host.");
3317
- }
3318
- return parsed;
3319
- }
3320
-
3321
- // ../../src/core/security/safeFetch.ts
3322
- var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
3323
- async function safeFetch(input, init = {}, options = {}) {
3324
- const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
3325
- const maxRedirects = options.maxRedirects ?? 0;
3326
- const resolveDns = options.resolveDns ?? appConfig.env === "production";
3327
- const urlOptions = { allowHttp: options.allowHttp, resolveDns };
3328
- const controller = new AbortController;
3329
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
3330
- try {
3331
- let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
3332
- let redirectCount = 0;
3333
- while (true) {
3334
- const response = await fetch(currentUrl, {
3335
- ...init,
3336
- signal: controller.signal,
3337
- redirect: "manual"
3338
- });
3339
- if (response.status >= 300 && response.status < 400) {
3340
- const location = response.headers.get("location");
3341
- if (!location || redirectCount >= maxRedirects) {
3342
- return response;
3343
- }
3344
- currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
3345
- redirectCount += 1;
3346
- continue;
3347
- }
3348
- return response;
3349
- }
3350
- } finally {
3351
- clearTimeout(timeout);
3352
- }
3353
- }
3354
-
3355
- // ../../src/core/jobs/dispatchWebhookJob.ts
3356
- class DispatchWebhookJob extends Job {
3357
- maxAttempts = 3;
3358
- backoffMs = 2000;
3359
- async handle(payload) {
3360
- const rows = await repositoryConnection`
3361
- SELECT id, url, secret
3362
- FROM webhook
3363
- WHERE id = ${payload.webhookId} AND active = TRUE
3364
- LIMIT 1
3365
- `;
3366
- const webhook = rows[0];
3367
- if (!webhook) {
3368
- return;
3369
- }
3370
- const body = JSON.stringify({ event: payload.event, payload: payload.payload });
3371
- const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
3372
- assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
3373
- let responseStatus = null;
3374
- let errorMessage = null;
3375
- try {
3376
- const response = await safeFetch(webhook.url, {
3377
- method: "POST",
3378
- headers: {
3379
- "content-type": "application/json",
3380
- "x-workhub-signature": signature
3381
- },
3382
- body
3383
- }, { allowHttp: appConfig.env !== "production" });
3384
- responseStatus = response.status;
3385
- if (!response.ok) {
3386
- throw new Error(`Webhook delivery failed with status ${response.status}.`);
3387
- }
3388
- } catch (error) {
3389
- errorMessage = error instanceof Error ? error.message : String(error);
3390
- await repositoryConnection`
3391
- INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
3392
- VALUES (
3393
- ${webhook.id},
3394
- ${payload.event},
3395
- ${JSON.stringify(payload.payload)}::jsonb,
3396
- ${responseStatus},
3397
- ${errorMessage}
3398
- )
3399
- `;
3400
- throw error instanceof Error ? error : new Error(errorMessage);
3401
- }
3402
- await repositoryConnection`
3403
- INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
3404
- VALUES (
3405
- ${webhook.id},
3406
- ${payload.event},
3407
- ${JSON.stringify(payload.payload)}::jsonb,
3408
- ${responseStatus}
3409
- )
3410
- `;
3411
- }
3237
+ // ../../src/core/queue/createAppQueue.ts
3238
+ var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
3239
+ function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
3240
+ return createProductionQueue(driver, {
3241
+ redisUrl,
3242
+ failedJobs,
3243
+ registerJobs
3244
+ });
3412
3245
  }
3413
- var dispatchWebhookJob_default = DispatchWebhookJob;
3414
3246
 
3415
3247
  // ../../src/core/contracts/applicationContext.ts
3416
3248
  function getRequiredDependency(dependencies, key) {
@@ -3502,24 +3334,6 @@ function resolveApplicationLogger() {
3502
3334
  function resolveApplicationDependencies() {
3503
3335
  return requireActiveApplicationContext().dependencies;
3504
3336
  }
3505
- // ../../src/bootstrap/queue/defaultJobs.ts
3506
- function registerDefaultJobs() {
3507
- jobRegistry.register("cache.invalidate-tags", () => {
3508
- return new invalidateCacheTagsJob_default(resolveApplicationCache());
3509
- });
3510
- jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
3511
- }
3512
-
3513
- // ../../src/core/queue/createAppQueue.ts
3514
- var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
3515
- function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
3516
- return createProductionQueue(driver, {
3517
- redisUrl,
3518
- failedJobs,
3519
- registerJobs
3520
- });
3521
- }
3522
-
3523
3337
  // ../../src/bootstrap/discoverModules.ts
3524
3338
  var appModules = [];
3525
3339
  function discoverModules() {
@@ -3633,6 +3447,201 @@ var policyProvider = {
3633
3447
  };
3634
3448
  var policy_default = policyProvider;
3635
3449
 
3450
+ // ../../src/core/jobs/dispatchWebhookJob.ts
3451
+ import { createHmac as createHmac2 } from "crypto";
3452
+
3453
+ // ../../src/core/security/safeUrl.ts
3454
+ import { lookup as dnsLookupImpl } from "dns/promises";
3455
+ var dnsLookup = dnsLookupImpl;
3456
+ var BLOCKED_HOSTNAMES = new Set([
3457
+ "localhost",
3458
+ "127.0.0.1",
3459
+ "0.0.0.0",
3460
+ "::1",
3461
+ "metadata.google.internal"
3462
+ ]);
3463
+ function isPrivateIpv4(hostname) {
3464
+ const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
3465
+ if (!match) {
3466
+ return false;
3467
+ }
3468
+ const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
3469
+ if (octets.some((octet) => octet < 0 || octet > 255)) {
3470
+ return true;
3471
+ }
3472
+ const [a = 0, b = 0] = octets;
3473
+ if (a === 10) {
3474
+ return true;
3475
+ }
3476
+ if (a === 127) {
3477
+ return true;
3478
+ }
3479
+ if (a === 0) {
3480
+ return true;
3481
+ }
3482
+ if (a === 169 && b === 254) {
3483
+ return true;
3484
+ }
3485
+ if (a === 172 && b >= 16 && b <= 31) {
3486
+ return true;
3487
+ }
3488
+ if (a === 192 && b === 168) {
3489
+ return true;
3490
+ }
3491
+ return false;
3492
+ }
3493
+ function isBlockedHostname(hostname) {
3494
+ const normalized = hostname.trim().toLowerCase();
3495
+ if (normalized.length === 0) {
3496
+ return true;
3497
+ }
3498
+ if (BLOCKED_HOSTNAMES.has(normalized)) {
3499
+ return true;
3500
+ }
3501
+ if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
3502
+ return true;
3503
+ }
3504
+ if (normalized.includes(":")) {
3505
+ return true;
3506
+ }
3507
+ return isPrivateIpv4(normalized);
3508
+ }
3509
+ function assertSafeOutboundUrl(rawUrl, options = {}) {
3510
+ let parsed;
3511
+ try {
3512
+ parsed = new URL(rawUrl);
3513
+ } catch {
3514
+ throw new BadRequestError("Webhook URL is invalid.");
3515
+ }
3516
+ if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
3517
+ throw new BadRequestError("Webhook URL must use HTTPS.");
3518
+ }
3519
+ if (parsed.username || parsed.password) {
3520
+ throw new BadRequestError("Webhook URL must not include credentials.");
3521
+ }
3522
+ if (isBlockedHostname(parsed.hostname)) {
3523
+ throw new BadRequestError("Webhook URL targets a blocked host.");
3524
+ }
3525
+ return parsed;
3526
+ }
3527
+ function isBlockedIpAddress(address) {
3528
+ return isBlockedHostname(address.trim().toLowerCase());
3529
+ }
3530
+ async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
3531
+ const parsed = assertSafeOutboundUrl(rawUrl, options);
3532
+ if (options.resolveDns === false) {
3533
+ return parsed;
3534
+ }
3535
+ const hostname = parsed.hostname.trim().toLowerCase();
3536
+ const results = await dnsLookup(hostname, { all: true, verbatim: true });
3537
+ if (results.some((result) => isBlockedIpAddress(result.address))) {
3538
+ throw new BadRequestError("Webhook URL targets a blocked host.");
3539
+ }
3540
+ return parsed;
3541
+ }
3542
+
3543
+ // ../../src/core/security/safeFetch.ts
3544
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
3545
+ async function safeFetch(input, init = {}, options = {}) {
3546
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
3547
+ const maxRedirects = options.maxRedirects ?? 0;
3548
+ const resolveDns = options.resolveDns ?? appConfig.env === "production";
3549
+ const urlOptions = { allowHttp: options.allowHttp, resolveDns };
3550
+ const controller = new AbortController;
3551
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
3552
+ try {
3553
+ let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
3554
+ let redirectCount = 0;
3555
+ while (true) {
3556
+ const response = await fetch(currentUrl, {
3557
+ ...init,
3558
+ signal: controller.signal,
3559
+ redirect: "manual"
3560
+ });
3561
+ if (response.status >= 300 && response.status < 400) {
3562
+ const location = response.headers.get("location");
3563
+ if (!location || redirectCount >= maxRedirects) {
3564
+ return response;
3565
+ }
3566
+ currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
3567
+ redirectCount += 1;
3568
+ continue;
3569
+ }
3570
+ return response;
3571
+ }
3572
+ } finally {
3573
+ clearTimeout(timeout);
3574
+ }
3575
+ }
3576
+
3577
+ // ../../src/core/jobs/dispatchWebhookJob.ts
3578
+ class DispatchWebhookJob extends Job {
3579
+ maxAttempts = 3;
3580
+ backoffMs = 2000;
3581
+ async handle(payload) {
3582
+ const rows = await repositoryConnection`
3583
+ SELECT id, url, secret
3584
+ FROM webhook
3585
+ WHERE id = ${payload.webhookId} AND active = TRUE
3586
+ LIMIT 1
3587
+ `;
3588
+ const webhook = rows[0];
3589
+ if (!webhook) {
3590
+ return;
3591
+ }
3592
+ const body = JSON.stringify({ event: payload.event, payload: payload.payload });
3593
+ const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
3594
+ assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
3595
+ let responseStatus = null;
3596
+ let errorMessage = null;
3597
+ try {
3598
+ const response = await safeFetch(webhook.url, {
3599
+ method: "POST",
3600
+ headers: {
3601
+ "content-type": "application/json",
3602
+ "x-workhub-signature": signature
3603
+ },
3604
+ body
3605
+ }, { allowHttp: appConfig.env !== "production" });
3606
+ responseStatus = response.status;
3607
+ if (!response.ok) {
3608
+ throw new Error(`Webhook delivery failed with status ${response.status}.`);
3609
+ }
3610
+ } catch (error) {
3611
+ errorMessage = error instanceof Error ? error.message : String(error);
3612
+ await repositoryConnection`
3613
+ INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
3614
+ VALUES (
3615
+ ${webhook.id},
3616
+ ${payload.event},
3617
+ ${JSON.stringify(payload.payload)}::jsonb,
3618
+ ${responseStatus},
3619
+ ${errorMessage}
3620
+ )
3621
+ `;
3622
+ throw error instanceof Error ? error : new Error(errorMessage);
3623
+ }
3624
+ await repositoryConnection`
3625
+ INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
3626
+ VALUES (
3627
+ ${webhook.id},
3628
+ ${payload.event},
3629
+ ${JSON.stringify(payload.payload)}::jsonb,
3630
+ ${responseStatus}
3631
+ )
3632
+ `;
3633
+ }
3634
+ }
3635
+ var dispatchWebhookJob_default = DispatchWebhookJob;
3636
+
3637
+ // ../../src/bootstrap/queue/defaultJobs.ts
3638
+ function registerDefaultJobs() {
3639
+ jobRegistry.register("cache.invalidate-tags", () => {
3640
+ return new invalidateCacheTagsJob_default(resolveApplicationCache());
3641
+ });
3642
+ jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
3643
+ }
3644
+
3636
3645
  // ../../src/bootstrap/providers/queue.ts
3637
3646
  var queueProvider = {
3638
3647
  name: "core.queue",
@@ -349,7 +349,6 @@ var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
349
349
 
350
350
  // ../../src/core/queue/jobRegistry.ts
351
351
  class JobRegistry {
352
- constructor() {}
353
352
  factories = new Map;
354
353
  instances = new WeakMap;
355
354
  register(name, factory) {
@@ -373,7 +372,17 @@ class JobRegistry {
373
372
  return [...this.factories.keys()];
374
373
  }
375
374
  }
376
- var jobRegistry = new JobRegistry;
375
+ var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
376
+ function readSharedJobRegistry() {
377
+ const globalRegistry = globalThis[JOB_REGISTRY_KEY];
378
+ if (globalRegistry) {
379
+ return globalRegistry;
380
+ }
381
+ const registry = new JobRegistry;
382
+ globalThis[JOB_REGISTRY_KEY] = registry;
383
+ return registry;
384
+ }
385
+ var jobRegistry = readSharedJobRegistry();
377
386
 
378
387
  // ../../src/core/contracts/applicationContext.ts
379
388
  function getRequiredDependency(dependencies, key) {
@@ -473,6 +482,7 @@ function resolveApplicationLogger() {
473
482
  function resolveApplicationDependencies() {
474
483
  return requireActiveApplicationContext().dependencies;
475
484
  }
485
+
476
486
  // ../../src/bootstrap/queue/defaultJobs.ts
477
487
  function registerDefaultJobs() {
478
488
  jobRegistry.register("cache.invalidate-tags", () => {
@@ -19,7 +19,6 @@ export { default as MembershipService, resolveMembershipService, } from "../core
19
19
  export { Policy, PolicyGate } from "../core/auth/policy.ts";
20
20
  export { createScimAuthMiddleware } from "../core/auth/scimAuthMiddleware.ts";
21
21
  export { type CacheDriver, type CreateCacheStoreOptions, createCacheStore, } from "../core/cache/createCacheStore.ts";
22
- export { cacheTagsForModelWrite, discoverModelTableNames } from "../core/cache/modelCacheTags.ts";
23
22
  export { default as CacheRepository } from "../core/cache/repository.ts";
24
23
  export { CACHE_TAGS } from "../core/cache/tags.ts";
25
24
  export type { DatabaseConnection } from "../core/database/baseRepository.ts";