@getstrata/core 0.5.16 → 0.5.17

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,222 +1,47 @@
1
1
  // @bun
2
- // ../../src/core/logging/logger.ts
3
- class Logger {
4
- channel;
5
- constructor(channel = "app") {
6
- this.channel = channel;
7
- }
8
- write(level, message, context = {}) {
9
- const entry = {
10
- level,
11
- channel: this.channel,
12
- message,
13
- timestamp: new Date().toISOString(),
14
- ...context
2
+ // ../../src/core/events/eventBus.ts
3
+ class EventBus {
4
+ constructor() {}
5
+ listeners = new Map;
6
+ listen(event, listener) {
7
+ const handlers = this.listeners.get(event) ?? new Set;
8
+ handlers.add(listener);
9
+ this.listeners.set(event, handlers);
10
+ return () => {
11
+ handlers.delete(listener);
12
+ if (handlers.size === 0) {
13
+ this.listeners.delete(event);
14
+ }
15
15
  };
16
- const line = JSON.stringify(entry);
17
- if (level === "error") {
18
- console.error(line);
19
- return;
20
- }
21
- console.log(line);
22
- }
23
- debug(message, context) {
24
- this.write("debug", message, context);
25
- }
26
- info(message, context) {
27
- this.write("info", message, context);
28
- }
29
- warn(message, context) {
30
- this.write("warn", message, context);
31
- }
32
- error(message, context) {
33
- this.write("error", message, context);
34
- }
35
- }
36
- var appLogger = new Logger("app");
37
-
38
- // ../../src/bootstrap/config.ts
39
- var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
40
- var DEFAULT_QUEUE_DRIVER = "sync";
41
-
42
- // ../../src/bootstrap/contracts.ts
43
- class ServiceContainer {
44
- services = new Map;
45
- singletonFactories = new Map;
46
- bindings = new Map;
47
- set(key, value) {
48
- this.singletonFactories.delete(key);
49
- this.bindings.delete(key);
50
- this.services.set(key, value);
51
- return value;
52
- }
53
- singleton(key, factory) {
54
- this.bindings.delete(key);
55
- this.services.delete(key);
56
- this.singletonFactories.set(key, factory);
57
- }
58
- bind(key, factory) {
59
- this.singletonFactories.delete(key);
60
- this.services.delete(key);
61
- this.bindings.set(key, factory);
62
- }
63
- get(key) {
64
- if (this.services.has(key)) {
65
- return this.services.get(key);
66
- }
67
- const singletonFactory = this.singletonFactories.get(key);
68
- if (singletonFactory) {
69
- const value = singletonFactory(this);
70
- this.services.set(key, value);
71
- return value;
72
- }
73
- const binding = this.bindings.get(key);
74
- if (binding) {
75
- return binding(this);
76
- }
77
- throw new Error(`Service "${key}" is not registered.`);
78
- }
79
- resolve(key) {
80
- return this.get(key);
81
- }
82
- has(key) {
83
- return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
84
- }
85
- }
86
-
87
- class ConfigStore {
88
- values = new Map;
89
- set(key, value) {
90
- this.values.set(key, value);
91
- return value;
92
- }
93
- get(key) {
94
- return this.values.get(key);
95
- }
96
- require(key) {
97
- if (!this.values.has(key)) {
98
- throw new Error(`Config key "${key}" is not defined.`);
99
- }
100
- return this.values.get(key);
101
- }
102
- has(key) {
103
- return this.values.has(key);
104
- }
105
- }
106
- function getRequiredDependency(dependencies, key) {
107
- const dependency = dependencies[key];
108
- if (dependency === undefined) {
109
- throw new Error(`Required dependency "${key}" is not registered.`);
110
- }
111
- return dependency;
112
- }
113
-
114
- // ../../src/bootstrap/applicationRegistry.ts
115
- var activeContext;
116
- function requireActiveApplicationContext() {
117
- if (!activeContext) {
118
- throw new Error("The application context has not been bootstrapped.");
119
- }
120
- return activeContext;
121
- }
122
- function resolveApplicationCache() {
123
- return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
124
- }
125
-
126
- // ../../src/core/jobs/dispatchWebhookJob.ts
127
- import { createHmac } from "crypto";
128
-
129
- // ../../src/config/app.ts
130
- var appConfig = {
131
- name: "WorkHub",
132
- env: process.env.APP_ENV ?? "local",
133
- debug: (process.env.APP_DEBUG ?? "true") !== "false",
134
- url: process.env.APP_URL ?? "http://localhost:3000",
135
- apiPrefix: process.env.API_PREFIX ?? "/api/v1"
136
- };
137
-
138
- // ../../src/core/database/boundConnection.ts
139
- var boundConnectionHolder = {
140
- connection: null
141
- };
142
- function getBoundDatabaseConnection() {
143
- return boundConnectionHolder.connection;
144
- }
145
-
146
- // ../../src/core/database/connectionContext.ts
147
- import { AsyncLocalStorage } from "async_hooks";
148
- var activeConnection = new AsyncLocalStorage;
149
- function getActiveDatabaseConnection(fallback) {
150
- return activeConnection.getStore() ?? fallback;
151
- }
152
-
153
- // ../../src/core/database/queryProxy.ts
154
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
155
- function createDatabaseQueryProxy(pool) {
156
- function resolveDatabase() {
157
- return getActiveDatabaseConnection(pool);
158
16
  }
159
- function resolveDatabaseForProperty(property) {
160
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
161
- return pool;
17
+ async dispatch(event, payload) {
18
+ const handlers = this.listeners.get(event);
19
+ if (!handlers || handlers.size === 0) {
20
+ return;
162
21
  }
163
- return resolveDatabase();
164
- }
165
- return new Proxy(function database() {}, {
166
- apply(_target, _thisArg, args) {
167
- return resolveDatabase()(...args);
168
- },
169
- get(_target, property) {
170
- const connection = resolveDatabaseForProperty(property);
171
- const value = connection[property];
172
- return typeof value === "function" ? value.bind(connection) : value;
22
+ for (const handler of handlers) {
23
+ await handler(payload);
173
24
  }
174
- });
175
- }
176
-
177
- // ../../src/core/database/defaultConnection.ts
178
- var defaultPool = {
179
- connection: null
180
- };
181
- var defaultQuery = {
182
- connection: null
183
- };
184
- function registerDefaultDatabasePool(connection) {
185
- defaultPool.connection = connection;
186
- defaultQuery.connection = createDatabaseQueryProxy(connection);
187
- }
188
- function getDefaultDatabaseQuery() {
189
- if (!defaultQuery.connection) {
190
- throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
191
25
  }
192
- return defaultQuery.connection;
193
26
  }
27
+ var eventBus = new EventBus;
194
28
 
195
- // ../../src/core/database/repositoryConnection.ts
196
- function resolveRepositoryConnection() {
197
- return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
29
+ // ../../src/core/events/index.ts
30
+ function modelEventName(tableName, action) {
31
+ return `${tableName}.${action}`;
198
32
  }
199
- var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
200
- apply(_target, _thisArg, args) {
201
- return resolveRepositoryConnection()(...args);
202
- },
203
- get(_target, property) {
204
- const connection = resolveRepositoryConnection();
205
- const value = connection[property];
206
- return typeof value === "function" ? value.bind(connection) : value;
207
- }
208
- });
209
33
 
210
- // ../../src/core/queue/index.ts
211
- class Job {
212
- maxAttempts;
213
- backoffMs;
214
- priority;
34
+ // ../../src/core/pagination/index.ts
35
+ function buildPaginationMeta(input) {
36
+ const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
37
+ return {
38
+ page: input.page,
39
+ per_page: input.perPage,
40
+ total: input.total,
41
+ last_page: lastPage
42
+ };
215
43
  }
216
44
 
217
- // ../../src/core/security/safeUrl.ts
218
- import { lookup as dnsLookupImpl } from "dns/promises";
219
-
220
45
  // ../../src/core/errors/http.ts
221
46
  class HttpError extends Error {
222
47
  status;
@@ -283,279 +108,6 @@ class PreconditionFailedError extends HttpError {
283
108
  }
284
109
  }
285
110
 
286
- // ../../src/core/security/safeUrl.ts
287
- var dnsLookup = dnsLookupImpl;
288
- var BLOCKED_HOSTNAMES = new Set([
289
- "localhost",
290
- "127.0.0.1",
291
- "0.0.0.0",
292
- "::1",
293
- "metadata.google.internal"
294
- ]);
295
- function isPrivateIpv4(hostname) {
296
- const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
297
- if (!match) {
298
- return false;
299
- }
300
- const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
301
- if (octets.some((octet) => octet < 0 || octet > 255)) {
302
- return true;
303
- }
304
- const [a = 0, b = 0] = octets;
305
- if (a === 10) {
306
- return true;
307
- }
308
- if (a === 127) {
309
- return true;
310
- }
311
- if (a === 0) {
312
- return true;
313
- }
314
- if (a === 169 && b === 254) {
315
- return true;
316
- }
317
- if (a === 172 && b >= 16 && b <= 31) {
318
- return true;
319
- }
320
- if (a === 192 && b === 168) {
321
- return true;
322
- }
323
- return false;
324
- }
325
- function isBlockedHostname(hostname) {
326
- const normalized = hostname.trim().toLowerCase();
327
- if (normalized.length === 0) {
328
- return true;
329
- }
330
- if (BLOCKED_HOSTNAMES.has(normalized)) {
331
- return true;
332
- }
333
- if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
334
- return true;
335
- }
336
- if (normalized.includes(":")) {
337
- return true;
338
- }
339
- return isPrivateIpv4(normalized);
340
- }
341
- function assertSafeOutboundUrl(rawUrl, options = {}) {
342
- let parsed;
343
- try {
344
- parsed = new URL(rawUrl);
345
- } catch {
346
- throw new BadRequestError("Webhook URL is invalid.");
347
- }
348
- if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
349
- throw new BadRequestError("Webhook URL must use HTTPS.");
350
- }
351
- if (parsed.username || parsed.password) {
352
- throw new BadRequestError("Webhook URL must not include credentials.");
353
- }
354
- if (isBlockedHostname(parsed.hostname)) {
355
- throw new BadRequestError("Webhook URL targets a blocked host.");
356
- }
357
- return parsed;
358
- }
359
- function isBlockedIpAddress(address) {
360
- return isBlockedHostname(address.trim().toLowerCase());
361
- }
362
- async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
363
- const parsed = assertSafeOutboundUrl(rawUrl, options);
364
- if (options.resolveDns === false) {
365
- return parsed;
366
- }
367
- const hostname = parsed.hostname.trim().toLowerCase();
368
- const results = await dnsLookup(hostname, { all: true, verbatim: true });
369
- if (results.some((result) => isBlockedIpAddress(result.address))) {
370
- throw new BadRequestError("Webhook URL targets a blocked host.");
371
- }
372
- return parsed;
373
- }
374
- function setDnsLookupForTests(lookupFn) {
375
- dnsLookup = lookupFn;
376
- }
377
- function resetDnsLookupForTests() {
378
- dnsLookup = dnsLookupImpl;
379
- }
380
-
381
- // ../../src/core/security/safeFetch.ts
382
- var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
383
- async function safeFetch(input, init = {}, options = {}) {
384
- const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
385
- const maxRedirects = options.maxRedirects ?? 0;
386
- const resolveDns = options.resolveDns ?? appConfig.env === "production";
387
- const urlOptions = { allowHttp: options.allowHttp, resolveDns };
388
- const controller = new AbortController;
389
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
390
- try {
391
- let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
392
- let redirectCount = 0;
393
- while (true) {
394
- const response = await fetch(currentUrl, {
395
- ...init,
396
- signal: controller.signal,
397
- redirect: "manual"
398
- });
399
- if (response.status >= 300 && response.status < 400) {
400
- const location = response.headers.get("location");
401
- if (!location || redirectCount >= maxRedirects) {
402
- return response;
403
- }
404
- currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
405
- redirectCount += 1;
406
- continue;
407
- }
408
- return response;
409
- }
410
- } finally {
411
- clearTimeout(timeout);
412
- }
413
- }
414
-
415
- // ../../src/core/jobs/dispatchWebhookJob.ts
416
- class DispatchWebhookJob extends Job {
417
- maxAttempts = 3;
418
- backoffMs = 2000;
419
- async handle(payload) {
420
- const rows = await repositoryConnection`
421
- SELECT id, url, secret
422
- FROM webhook
423
- WHERE id = ${payload.webhookId} AND active = TRUE
424
- LIMIT 1
425
- `;
426
- const webhook = rows[0];
427
- if (!webhook) {
428
- return;
429
- }
430
- const body = JSON.stringify({ event: payload.event, payload: payload.payload });
431
- const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
432
- assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
433
- let responseStatus = null;
434
- let errorMessage = null;
435
- try {
436
- const response = await safeFetch(webhook.url, {
437
- method: "POST",
438
- headers: {
439
- "content-type": "application/json",
440
- "x-workhub-signature": signature
441
- },
442
- body
443
- }, { allowHttp: appConfig.env !== "production" });
444
- responseStatus = response.status;
445
- if (!response.ok) {
446
- throw new Error(`Webhook delivery failed with status ${response.status}.`);
447
- }
448
- } catch (error) {
449
- errorMessage = error instanceof Error ? error.message : String(error);
450
- await repositoryConnection`
451
- INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
452
- VALUES (
453
- ${webhook.id},
454
- ${payload.event},
455
- ${JSON.stringify(payload.payload)}::jsonb,
456
- ${responseStatus},
457
- ${errorMessage}
458
- )
459
- `;
460
- throw error instanceof Error ? error : new Error(errorMessage);
461
- }
462
- await repositoryConnection`
463
- INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
464
- VALUES (
465
- ${webhook.id},
466
- ${payload.event},
467
- ${JSON.stringify(payload.payload)}::jsonb,
468
- ${responseStatus}
469
- )
470
- `;
471
- }
472
- }
473
- var dispatchWebhookJob_default = DispatchWebhookJob;
474
-
475
- // ../../src/core/jobs/invalidateCacheTagsJob.ts
476
- class InvalidateCacheTagsJob extends Job {
477
- cache;
478
- constructor(cache) {
479
- super();
480
- this.cache = cache;
481
- }
482
- async handle(payload) {
483
- await this.cache.tags(...payload.tags).flush();
484
- }
485
- }
486
- var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
487
-
488
- // ../../src/core/queue/jobRegistry.ts
489
- class JobRegistry {
490
- constructor() {}
491
- factories = new Map;
492
- instances = new WeakMap;
493
- register(name, factory) {
494
- this.factories.set(name, factory);
495
- }
496
- resolveName(job) {
497
- return this.instances.get(job);
498
- }
499
- track(name, job) {
500
- this.instances.set(job, name);
501
- return job;
502
- }
503
- create(name) {
504
- const factory = this.factories.get(name);
505
- if (!factory) {
506
- return;
507
- }
508
- return factory();
509
- }
510
- names() {
511
- return [...this.factories.keys()];
512
- }
513
- }
514
- var jobRegistry = new JobRegistry;
515
-
516
- // ../../src/core/events/eventBus.ts
517
- class EventBus {
518
- constructor() {}
519
- listeners = new Map;
520
- listen(event, listener) {
521
- const handlers = this.listeners.get(event) ?? new Set;
522
- handlers.add(listener);
523
- this.listeners.set(event, handlers);
524
- return () => {
525
- handlers.delete(listener);
526
- if (handlers.size === 0) {
527
- this.listeners.delete(event);
528
- }
529
- };
530
- }
531
- async dispatch(event, payload) {
532
- const handlers = this.listeners.get(event);
533
- if (!handlers || handlers.size === 0) {
534
- return;
535
- }
536
- for (const handler of handlers) {
537
- await handler(payload);
538
- }
539
- }
540
- }
541
- var eventBus = new EventBus;
542
-
543
- // ../../src/core/events/index.ts
544
- function modelEventName(tableName, action) {
545
- return `${tableName}.${action}`;
546
- }
547
-
548
- // ../../src/core/pagination/index.ts
549
- function buildPaginationMeta(input) {
550
- const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
551
- return {
552
- page: input.page,
553
- per_page: input.perPage,
554
- total: input.total,
555
- last_page: lastPage
556
- };
557
- }
558
-
559
111
  // ../../src/core/database/errors.ts
560
112
  function isPostgresError(error) {
561
113
  return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
@@ -1084,6 +636,78 @@ function indexMorphToRelation(children, parentsByType, relation) {
1084
636
  return result;
1085
637
  }
1086
638
 
639
+ // ../../src/core/database/boundConnection.ts
640
+ var boundConnectionHolder = {
641
+ connection: null
642
+ };
643
+ function getBoundDatabaseConnection() {
644
+ return boundConnectionHolder.connection;
645
+ }
646
+
647
+ // ../../src/core/database/connectionContext.ts
648
+ import { AsyncLocalStorage } from "async_hooks";
649
+ var activeConnection = new AsyncLocalStorage;
650
+ function getActiveDatabaseConnection(fallback) {
651
+ return activeConnection.getStore() ?? fallback;
652
+ }
653
+
654
+ // ../../src/core/database/queryProxy.ts
655
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
656
+ function createDatabaseQueryProxy(pool) {
657
+ function resolveDatabase() {
658
+ return getActiveDatabaseConnection(pool);
659
+ }
660
+ function resolveDatabaseForProperty(property) {
661
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
662
+ return pool;
663
+ }
664
+ return resolveDatabase();
665
+ }
666
+ return new Proxy(function database() {}, {
667
+ apply(_target, _thisArg, args) {
668
+ return resolveDatabase()(...args);
669
+ },
670
+ get(_target, property) {
671
+ const connection = resolveDatabaseForProperty(property);
672
+ const value = connection[property];
673
+ return typeof value === "function" ? value.bind(connection) : value;
674
+ }
675
+ });
676
+ }
677
+
678
+ // ../../src/core/database/defaultConnection.ts
679
+ var defaultPool = {
680
+ connection: null
681
+ };
682
+ var defaultQuery = {
683
+ connection: null
684
+ };
685
+ function registerDefaultDatabasePool(connection) {
686
+ defaultPool.connection = connection;
687
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
688
+ }
689
+ function getDefaultDatabaseQuery() {
690
+ if (!defaultQuery.connection) {
691
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
692
+ }
693
+ return defaultQuery.connection;
694
+ }
695
+
696
+ // ../../src/core/database/repositoryConnection.ts
697
+ function resolveRepositoryConnection() {
698
+ return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
699
+ }
700
+ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
701
+ apply(_target, _thisArg, args) {
702
+ return resolveRepositoryConnection()(...args);
703
+ },
704
+ get(_target, property) {
705
+ const connection = resolveRepositoryConnection();
706
+ const value = connection[property];
707
+ return typeof value === "function" ? value.bind(connection) : value;
708
+ }
709
+ });
710
+
1087
711
  // ../../src/core/database/whereBuilder.ts
1088
712
  class WhereBuilder {
1089
713
  nodes = [];
@@ -2272,6 +1896,38 @@ class FailedJobService {
2272
1896
  }
2273
1897
  var failedJobService_default = FailedJobService;
2274
1898
 
1899
+ // ../../src/core/queue/jobRegistry.ts
1900
+ class JobRegistry {
1901
+ constructor() {}
1902
+ factories = new Map;
1903
+ instances = new WeakMap;
1904
+ register(name, factory) {
1905
+ this.factories.set(name, factory);
1906
+ }
1907
+ resolveName(job) {
1908
+ return this.instances.get(job);
1909
+ }
1910
+ track(name, job) {
1911
+ this.instances.set(job, name);
1912
+ return job;
1913
+ }
1914
+ create(name) {
1915
+ const factory = this.factories.get(name);
1916
+ if (!factory) {
1917
+ return;
1918
+ }
1919
+ return factory();
1920
+ }
1921
+ names() {
1922
+ return [...this.factories.keys()];
1923
+ }
1924
+ }
1925
+ var jobRegistry = new JobRegistry;
1926
+
1927
+ // ../../src/bootstrap/config.ts
1928
+ var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
1929
+ var DEFAULT_QUEUE_DRIVER = "sync";
1930
+
2275
1931
  // ../../src/config/queue.ts
2276
1932
  var queueConfig = {
2277
1933
  driver: process.env.QUEUE_DRIVER === "redis" || process.env.QUEUE_DRIVER === "async" || process.env.QUEUE_DRIVER === "sync" ? process.env.QUEUE_DRIVER : DEFAULT_QUEUE_DRIVER,
@@ -2484,19 +2140,363 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
2484
2140
  return new QueueWorker(redisUrl, failedJobs);
2485
2141
  }
2486
2142
 
2487
- // ../../src/core/queue/createAppQueue.ts
2488
- var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
2143
+ // ../../src/core/jobs/dispatchWebhookJob.ts
2144
+ import { createHmac } from "crypto";
2145
+
2146
+ // ../../src/config/app.ts
2147
+ var appConfig = {
2148
+ name: "WorkHub",
2149
+ env: process.env.APP_ENV ?? "local",
2150
+ debug: (process.env.APP_DEBUG ?? "true") !== "false",
2151
+ url: process.env.APP_URL ?? "http://localhost:3000",
2152
+ apiPrefix: process.env.API_PREFIX ?? "/api/v1"
2153
+ };
2154
+
2155
+ // ../../src/core/queue/index.ts
2156
+ class Job {
2157
+ maxAttempts;
2158
+ backoffMs;
2159
+ priority;
2160
+ }
2161
+
2162
+ // ../../src/core/security/safeUrl.ts
2163
+ import { lookup as dnsLookupImpl } from "dns/promises";
2164
+ var dnsLookup = dnsLookupImpl;
2165
+ var BLOCKED_HOSTNAMES = new Set([
2166
+ "localhost",
2167
+ "127.0.0.1",
2168
+ "0.0.0.0",
2169
+ "::1",
2170
+ "metadata.google.internal"
2171
+ ]);
2172
+ function isPrivateIpv4(hostname) {
2173
+ const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
2174
+ if (!match) {
2175
+ return false;
2176
+ }
2177
+ const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
2178
+ if (octets.some((octet) => octet < 0 || octet > 255)) {
2179
+ return true;
2180
+ }
2181
+ const [a = 0, b = 0] = octets;
2182
+ if (a === 10) {
2183
+ return true;
2184
+ }
2185
+ if (a === 127) {
2186
+ return true;
2187
+ }
2188
+ if (a === 0) {
2189
+ return true;
2190
+ }
2191
+ if (a === 169 && b === 254) {
2192
+ return true;
2193
+ }
2194
+ if (a === 172 && b >= 16 && b <= 31) {
2195
+ return true;
2196
+ }
2197
+ if (a === 192 && b === 168) {
2198
+ return true;
2199
+ }
2200
+ return false;
2201
+ }
2202
+ function isBlockedHostname(hostname) {
2203
+ const normalized = hostname.trim().toLowerCase();
2204
+ if (normalized.length === 0) {
2205
+ return true;
2206
+ }
2207
+ if (BLOCKED_HOSTNAMES.has(normalized)) {
2208
+ return true;
2209
+ }
2210
+ if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
2211
+ return true;
2212
+ }
2213
+ if (normalized.includes(":")) {
2214
+ return true;
2215
+ }
2216
+ return isPrivateIpv4(normalized);
2217
+ }
2218
+ function assertSafeOutboundUrl(rawUrl, options = {}) {
2219
+ let parsed;
2220
+ try {
2221
+ parsed = new URL(rawUrl);
2222
+ } catch {
2223
+ throw new BadRequestError("Webhook URL is invalid.");
2224
+ }
2225
+ if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
2226
+ throw new BadRequestError("Webhook URL must use HTTPS.");
2227
+ }
2228
+ if (parsed.username || parsed.password) {
2229
+ throw new BadRequestError("Webhook URL must not include credentials.");
2230
+ }
2231
+ if (isBlockedHostname(parsed.hostname)) {
2232
+ throw new BadRequestError("Webhook URL targets a blocked host.");
2233
+ }
2234
+ return parsed;
2235
+ }
2236
+ function isBlockedIpAddress(address) {
2237
+ return isBlockedHostname(address.trim().toLowerCase());
2238
+ }
2239
+ async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
2240
+ const parsed = assertSafeOutboundUrl(rawUrl, options);
2241
+ if (options.resolveDns === false) {
2242
+ return parsed;
2243
+ }
2244
+ const hostname = parsed.hostname.trim().toLowerCase();
2245
+ const results = await dnsLookup(hostname, { all: true, verbatim: true });
2246
+ if (results.some((result) => isBlockedIpAddress(result.address))) {
2247
+ throw new BadRequestError("Webhook URL targets a blocked host.");
2248
+ }
2249
+ return parsed;
2250
+ }
2251
+ function setDnsLookupForTests(lookupFn) {
2252
+ dnsLookup = lookupFn;
2253
+ }
2254
+ function resetDnsLookupForTests() {
2255
+ dnsLookup = dnsLookupImpl;
2256
+ }
2257
+
2258
+ // ../../src/core/security/safeFetch.ts
2259
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
2260
+ async function safeFetch(input, init = {}, options = {}) {
2261
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
2262
+ const maxRedirects = options.maxRedirects ?? 0;
2263
+ const resolveDns = options.resolveDns ?? appConfig.env === "production";
2264
+ const urlOptions = { allowHttp: options.allowHttp, resolveDns };
2265
+ const controller = new AbortController;
2266
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2267
+ try {
2268
+ let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
2269
+ let redirectCount = 0;
2270
+ while (true) {
2271
+ const response = await fetch(currentUrl, {
2272
+ ...init,
2273
+ signal: controller.signal,
2274
+ redirect: "manual"
2275
+ });
2276
+ if (response.status >= 300 && response.status < 400) {
2277
+ const location = response.headers.get("location");
2278
+ if (!location || redirectCount >= maxRedirects) {
2279
+ return response;
2280
+ }
2281
+ currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
2282
+ redirectCount += 1;
2283
+ continue;
2284
+ }
2285
+ return response;
2286
+ }
2287
+ } finally {
2288
+ clearTimeout(timeout);
2289
+ }
2290
+ }
2291
+
2292
+ // ../../src/core/jobs/dispatchWebhookJob.ts
2293
+ class DispatchWebhookJob extends Job {
2294
+ maxAttempts = 3;
2295
+ backoffMs = 2000;
2296
+ async handle(payload) {
2297
+ const rows = await repositoryConnection`
2298
+ SELECT id, url, secret
2299
+ FROM webhook
2300
+ WHERE id = ${payload.webhookId} AND active = TRUE
2301
+ LIMIT 1
2302
+ `;
2303
+ const webhook = rows[0];
2304
+ if (!webhook) {
2305
+ return;
2306
+ }
2307
+ const body = JSON.stringify({ event: payload.event, payload: payload.payload });
2308
+ const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
2309
+ assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
2310
+ let responseStatus = null;
2311
+ let errorMessage = null;
2312
+ try {
2313
+ const response = await safeFetch(webhook.url, {
2314
+ method: "POST",
2315
+ headers: {
2316
+ "content-type": "application/json",
2317
+ "x-workhub-signature": signature
2318
+ },
2319
+ body
2320
+ }, { allowHttp: appConfig.env !== "production" });
2321
+ responseStatus = response.status;
2322
+ if (!response.ok) {
2323
+ throw new Error(`Webhook delivery failed with status ${response.status}.`);
2324
+ }
2325
+ } catch (error) {
2326
+ errorMessage = error instanceof Error ? error.message : String(error);
2327
+ await repositoryConnection`
2328
+ INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
2329
+ VALUES (
2330
+ ${webhook.id},
2331
+ ${payload.event},
2332
+ ${JSON.stringify(payload.payload)}::jsonb,
2333
+ ${responseStatus},
2334
+ ${errorMessage}
2335
+ )
2336
+ `;
2337
+ throw error instanceof Error ? error : new Error(errorMessage);
2338
+ }
2339
+ await repositoryConnection`
2340
+ INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
2341
+ VALUES (
2342
+ ${webhook.id},
2343
+ ${payload.event},
2344
+ ${JSON.stringify(payload.payload)}::jsonb,
2345
+ ${responseStatus}
2346
+ )
2347
+ `;
2348
+ }
2349
+ }
2350
+ var dispatchWebhookJob_default = DispatchWebhookJob;
2351
+
2352
+ // ../../src/core/jobs/invalidateCacheTagsJob.ts
2353
+ class InvalidateCacheTagsJob extends Job {
2354
+ cache;
2355
+ constructor(cache) {
2356
+ super();
2357
+ this.cache = cache;
2358
+ }
2359
+ async handle(payload) {
2360
+ await this.cache.tags(...payload.tags).flush();
2361
+ }
2362
+ }
2363
+ var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
2364
+
2365
+ // ../../src/core/logging/logger.ts
2366
+ class Logger {
2367
+ channel;
2368
+ constructor(channel = "app") {
2369
+ this.channel = channel;
2370
+ }
2371
+ write(level, message, context = {}) {
2372
+ const entry = {
2373
+ level,
2374
+ channel: this.channel,
2375
+ message,
2376
+ timestamp: new Date().toISOString(),
2377
+ ...context
2378
+ };
2379
+ const line = JSON.stringify(entry);
2380
+ if (level === "error") {
2381
+ console.error(line);
2382
+ return;
2383
+ }
2384
+ console.log(line);
2385
+ }
2386
+ debug(message, context) {
2387
+ this.write("debug", message, context);
2388
+ }
2389
+ info(message, context) {
2390
+ this.write("info", message, context);
2391
+ }
2392
+ warn(message, context) {
2393
+ this.write("warn", message, context);
2394
+ }
2395
+ error(message, context) {
2396
+ this.write("error", message, context);
2397
+ }
2398
+ }
2399
+ var appLogger = new Logger("app");
2400
+
2401
+ // ../../src/bootstrap/contracts.ts
2402
+ class ServiceContainer {
2403
+ services = new Map;
2404
+ singletonFactories = new Map;
2405
+ bindings = new Map;
2406
+ set(key, value) {
2407
+ this.singletonFactories.delete(key);
2408
+ this.bindings.delete(key);
2409
+ this.services.set(key, value);
2410
+ return value;
2411
+ }
2412
+ singleton(key, factory) {
2413
+ this.bindings.delete(key);
2414
+ this.services.delete(key);
2415
+ this.singletonFactories.set(key, factory);
2416
+ }
2417
+ bind(key, factory) {
2418
+ this.singletonFactories.delete(key);
2419
+ this.services.delete(key);
2420
+ this.bindings.set(key, factory);
2421
+ }
2422
+ get(key) {
2423
+ if (this.services.has(key)) {
2424
+ return this.services.get(key);
2425
+ }
2426
+ const singletonFactory = this.singletonFactories.get(key);
2427
+ if (singletonFactory) {
2428
+ const value = singletonFactory(this);
2429
+ this.services.set(key, value);
2430
+ return value;
2431
+ }
2432
+ const binding = this.bindings.get(key);
2433
+ if (binding) {
2434
+ return binding(this);
2435
+ }
2436
+ throw new Error(`Service "${key}" is not registered.`);
2437
+ }
2438
+ resolve(key) {
2439
+ return this.get(key);
2440
+ }
2441
+ has(key) {
2442
+ return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
2443
+ }
2444
+ }
2445
+
2446
+ class ConfigStore {
2447
+ values = new Map;
2448
+ set(key, value) {
2449
+ this.values.set(key, value);
2450
+ return value;
2451
+ }
2452
+ get(key) {
2453
+ return this.values.get(key);
2454
+ }
2455
+ require(key) {
2456
+ if (!this.values.has(key)) {
2457
+ throw new Error(`Config key "${key}" is not defined.`);
2458
+ }
2459
+ return this.values.get(key);
2460
+ }
2461
+ has(key) {
2462
+ return this.values.has(key);
2463
+ }
2464
+ }
2465
+ function getRequiredDependency(dependencies, key) {
2466
+ const dependency = dependencies[key];
2467
+ if (dependency === undefined) {
2468
+ throw new Error(`Required dependency "${key}" is not registered.`);
2469
+ }
2470
+ return dependency;
2471
+ }
2472
+
2473
+ // ../../src/bootstrap/applicationRegistry.ts
2474
+ var activeContext;
2475
+ function requireActiveApplicationContext() {
2476
+ if (!activeContext) {
2477
+ throw new Error("The application context has not been bootstrapped.");
2478
+ }
2479
+ return activeContext;
2480
+ }
2481
+ function resolveApplicationCache() {
2482
+ return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
2483
+ }
2484
+
2485
+ // ../../src/bootstrap/queue/defaultJobs.ts
2489
2486
  function registerDefaultJobs() {
2490
2487
  jobRegistry.register("cache.invalidate-tags", () => {
2491
2488
  return new invalidateCacheTagsJob_default(resolveApplicationCache());
2492
2489
  });
2493
2490
  jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
2494
2491
  }
2495
- function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService()) {
2492
+
2493
+ // ../../src/core/queue/createAppQueue.ts
2494
+ var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
2495
+ function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
2496
2496
  return createProductionQueue(driver, {
2497
2497
  redisUrl,
2498
2498
  failedJobs,
2499
- registerJobs: registerDefaultJobs
2499
+ registerJobs
2500
2500
  });
2501
2501
  }
2502
2502
  export {