@getstrata/core 0.5.20 → 0.5.24
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/bootstrap/config.d.ts +2 -7
- package/dist/core/auth/membershipService.d.ts +1 -1
- package/dist/{bootstrap/membershipService.d.ts → core/auth/resolveMembershipService.d.ts} +1 -1
- package/dist/core/contracts/applicationContext.d.ts +18 -0
- package/dist/core/contracts/serviceTokens.d.ts +7 -0
- package/dist/core/http/securedRouteModelBinding.d.ts +11 -2
- package/dist/core/queue/createAppQueue.d.ts +0 -1
- package/dist/core/queue/jobRegistry.d.ts +0 -1
- package/dist/core/runtime/applicationRegistry.d.ts +15 -0
- package/dist/entries/contracts/serviceTokens.js +16 -0
- package/dist/entries/http/webErrorResponse.js +7 -1
- package/dist/entries/jobs/dispatchWebhookJob.js +0 -104
- package/dist/entries/queue/createAppQueue.js +18 -366
- package/dist/entries/queue/jobRegistry.js +11 -2
- package/dist/entries/queue/jobRunner.js +18 -3
- package/dist/entries/queue/publicQueue.js +18 -3
- package/dist/entries/queue/queueMetrics.js +18 -365
- package/dist/entries/runtime/applicationRegistry.js +1 -0
- package/dist/entries/view.js +7 -1
- package/dist/framework/public-api.d.ts +1 -2
- package/dist/index.js +112 -129
- package/package.json +12 -2
- package/dist/bootstrap/applicationRegistry.d.ts +0 -16
- package/dist/bootstrap/cache/modelCacheTags.d.ts +0 -3
- package/dist/bootstrap/discoverModules.d.ts +0 -5
- package/dist/bootstrap/http/securedRouteModelBinding.d.ts +0 -11
- package/dist/bootstrap/queue/defaultJobs.d.ts +0 -2
- package/dist/core/cache/modelCacheTags.d.ts +0 -1
|
@@ -4,12 +4,7 @@ declare const CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
|
4
4
|
declare const CACHE_DRIVER_CONFIG_KEY = "cache.driver";
|
|
5
5
|
declare const REDIS_URL_CONFIG_KEY = "cache.redisUrl";
|
|
6
6
|
declare const DATABASE_URL_CONFIG_KEY = "database.url";
|
|
7
|
-
|
|
8
|
-
declare const CORE_CACHE_TOKEN = "core.cache";
|
|
9
|
-
declare const CORE_QUEUE_TOKEN = "core.queue";
|
|
10
|
-
declare const CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
11
|
-
declare const CORE_AUTH_TOKEN = "core.auth";
|
|
12
|
-
declare const CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
7
|
+
export { CORE_AUTH_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, } from "../core/contracts/serviceTokens.ts";
|
|
13
8
|
declare const AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
|
|
14
9
|
declare const DEFAULT_APP_PORT = 3000;
|
|
15
10
|
declare const DEFAULT_CACHE_TTL_MS = 3600000;
|
|
@@ -17,4 +12,4 @@ declare const DEFAULT_CACHE_MAX_ENTRIES = 100;
|
|
|
17
12
|
declare const DEFAULT_CACHE_DRIVER = "array";
|
|
18
13
|
declare const DEFAULT_API_TOKEN = "";
|
|
19
14
|
declare const DEFAULT_QUEUE_DRIVER = "sync";
|
|
20
|
-
export { APP_PORT_CONFIG_KEY, AUTH_DEV_HEADERS_CONFIG_KEY, CACHE_DRIVER_CONFIG_KEY, CACHE_MAX_ENTRIES_CONFIG_KEY, CACHE_TTL_MS_CONFIG_KEY,
|
|
15
|
+
export { APP_PORT_CONFIG_KEY, AUTH_DEV_HEADERS_CONFIG_KEY, CACHE_DRIVER_CONFIG_KEY, CACHE_MAX_ENTRIES_CONFIG_KEY, CACHE_TTL_MS_CONFIG_KEY, DATABASE_URL_CONFIG_KEY, DEFAULT_API_TOKEN, DEFAULT_APP_PORT, DEFAULT_CACHE_DRIVER, DEFAULT_CACHE_MAX_ENTRIES, DEFAULT_CACHE_TTL_MS, DEFAULT_QUEUE_DRIVER, REDIS_URL_CONFIG_KEY, };
|
|
@@ -20,4 +20,4 @@ declare class MembershipService {
|
|
|
20
20
|
}
|
|
21
21
|
export type { MembershipRepositoryLike };
|
|
22
22
|
export default MembershipService;
|
|
23
|
-
export { resolveMembershipService } from "
|
|
23
|
+
export { resolveMembershipService } from "./resolveMembershipService.ts";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CacheLike } from "../../types/services";
|
|
2
|
+
import type { ServiceContainerLike } from "./serviceContainer";
|
|
3
|
+
interface ConfigStoreLike {
|
|
4
|
+
get<T>(key: string): T | undefined;
|
|
5
|
+
}
|
|
6
|
+
interface ApplicationDependenciesLike {
|
|
7
|
+
container: ServiceContainerLike;
|
|
8
|
+
cache: CacheLike;
|
|
9
|
+
storage?: unknown;
|
|
10
|
+
}
|
|
11
|
+
interface ApplicationContext {
|
|
12
|
+
container: ServiceContainerLike;
|
|
13
|
+
config: ConfigStoreLike;
|
|
14
|
+
dependencies: ApplicationDependenciesLike;
|
|
15
|
+
}
|
|
16
|
+
declare function getRequiredDependency<K extends keyof ApplicationDependenciesLike>(dependencies: Partial<ApplicationDependenciesLike>, key: K): ApplicationDependenciesLike[K];
|
|
17
|
+
export type { ApplicationContext, ApplicationDependenciesLike, ConfigStoreLike };
|
|
18
|
+
export { getRequiredDependency };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare const CORE_CONFIG_TOKEN = "core.config";
|
|
2
|
+
declare const CORE_CACHE_TOKEN = "core.cache";
|
|
3
|
+
declare const CORE_QUEUE_TOKEN = "core.queue";
|
|
4
|
+
declare const CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
5
|
+
declare const CORE_AUTH_TOKEN = "core.auth";
|
|
6
|
+
declare const CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
7
|
+
export { CORE_AUTH_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, };
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { Policy } from "../auth/policy";
|
|
2
|
+
import type { RouteRequest } from "./route";
|
|
3
|
+
interface RouteModelAuthorization {
|
|
4
|
+
resource: string;
|
|
5
|
+
action: keyof Policy;
|
|
6
|
+
requireIfMatch?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare function securedBindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
|
|
9
|
+
declare function securedBindRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
|
|
10
|
+
export type { RouteModelAuthorization };
|
|
11
|
+
export { securedBindRouteModel, securedBindRouteModelByKey };
|
|
@@ -2,5 +2,4 @@ import type { Queue } from "./index";
|
|
|
2
2
|
import { createFailedJobService, createQueueWorker, createTrackedJob, type FailedJobService } from "./publicQueue";
|
|
3
3
|
declare const FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
4
4
|
declare function createAppQueue(driver: "sync" | "async" | "redis", redisUrl?: string, failedJobs?: FailedJobService, registerJobs?: () => void): Queue;
|
|
5
|
-
export { registerDefaultJobs } from "../../bootstrap/queue/defaultJobs.ts";
|
|
6
5
|
export { createAppQueue, createFailedJobService, createQueueWorker, createTrackedJob, FAILED_JOB_SERVICE_TOKEN, };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CacheLike } from "../../types/services";
|
|
2
|
+
import type { AuthManager } from "../auth/guard";
|
|
3
|
+
import type { PolicyGate } from "../auth/policy";
|
|
4
|
+
import type { ApplicationContext } from "../contracts/applicationContext";
|
|
5
|
+
import { type Logger } from "../logging/logger";
|
|
6
|
+
import type { Queue } from "../queue";
|
|
7
|
+
declare function setActiveApplicationContext(context: ApplicationContext): void;
|
|
8
|
+
declare function resolveApplicationCache(): CacheLike;
|
|
9
|
+
declare function resolveApplicationQueue(): Queue;
|
|
10
|
+
declare function resolveApplicationAuth(): AuthManager;
|
|
11
|
+
declare function resolveApplicationPolicyGate(): PolicyGate;
|
|
12
|
+
declare function resolveApplicationConfig(): import("../contracts/applicationContext").ConfigStoreLike;
|
|
13
|
+
declare function resolveApplicationLogger(): Logger;
|
|
14
|
+
declare function resolveApplicationDependencies(): import("../contracts/applicationContext").ApplicationDependenciesLike;
|
|
15
|
+
export { resolveApplicationAuth, resolveApplicationCache, resolveApplicationConfig, resolveApplicationDependencies, resolveApplicationLogger, resolveApplicationPolicyGate, resolveApplicationQueue, setActiveApplicationContext, };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/contracts/serviceTokens.ts
|
|
3
|
+
var CORE_CONFIG_TOKEN = "core.config";
|
|
4
|
+
var CORE_CACHE_TOKEN = "core.cache";
|
|
5
|
+
var CORE_QUEUE_TOKEN = "core.queue";
|
|
6
|
+
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
7
|
+
var CORE_AUTH_TOKEN = "core.auth";
|
|
8
|
+
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
9
|
+
export {
|
|
10
|
+
CORE_TOKEN_SERVICE_TOKEN,
|
|
11
|
+
CORE_QUEUE_TOKEN,
|
|
12
|
+
CORE_POLICY_GATE_TOKEN,
|
|
13
|
+
CORE_CONFIG_TOKEN,
|
|
14
|
+
CORE_CACHE_TOKEN,
|
|
15
|
+
CORE_AUTH_TOKEN
|
|
16
|
+
};
|
|
@@ -182,8 +182,14 @@ function htmlResponse(html, init = {}) {
|
|
|
182
182
|
function isHtmxRequest(request) {
|
|
183
183
|
return request.headers.get("HX-Request") === "true";
|
|
184
184
|
}
|
|
185
|
-
// ../../src/
|
|
185
|
+
// ../../src/core/contracts/serviceTokens.ts
|
|
186
|
+
var CORE_CONFIG_TOKEN = "core.config";
|
|
187
|
+
var CORE_CACHE_TOKEN = "core.cache";
|
|
188
|
+
var CORE_QUEUE_TOKEN = "core.queue";
|
|
189
|
+
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
190
|
+
var CORE_AUTH_TOKEN = "core.auth";
|
|
186
191
|
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
192
|
+
// ../../src/bootstrap/config.ts
|
|
187
193
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
188
194
|
|
|
189
195
|
// ../../src/core/auth/oauth/oidcProvider.ts
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3
|
-
import { createHmac } from "crypto";
|
|
4
|
-
|
|
5
2
|
// ../../src/config/app.ts
|
|
6
3
|
var appConfig = {
|
|
7
4
|
name: "WorkHub",
|
|
@@ -96,13 +93,6 @@ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
|
96
93
|
}
|
|
97
94
|
});
|
|
98
95
|
|
|
99
|
-
// ../../src/core/queue/index.ts
|
|
100
|
-
class Job {
|
|
101
|
-
maxAttempts;
|
|
102
|
-
backoffMs;
|
|
103
|
-
priority;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
96
|
// ../../src/core/security/safeUrl.ts
|
|
107
97
|
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
108
98
|
|
|
@@ -266,97 +256,3 @@ function setDnsLookupForTests(lookupFn) {
|
|
|
266
256
|
function resetDnsLookupForTests() {
|
|
267
257
|
dnsLookup = dnsLookupImpl;
|
|
268
258
|
}
|
|
269
|
-
|
|
270
|
-
// ../../src/core/security/safeFetch.ts
|
|
271
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
272
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
273
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
274
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
275
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
276
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
277
|
-
const controller = new AbortController;
|
|
278
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
279
|
-
try {
|
|
280
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
281
|
-
let redirectCount = 0;
|
|
282
|
-
while (true) {
|
|
283
|
-
const response = await fetch(currentUrl, {
|
|
284
|
-
...init,
|
|
285
|
-
signal: controller.signal,
|
|
286
|
-
redirect: "manual"
|
|
287
|
-
});
|
|
288
|
-
if (response.status >= 300 && response.status < 400) {
|
|
289
|
-
const location = response.headers.get("location");
|
|
290
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
291
|
-
return response;
|
|
292
|
-
}
|
|
293
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
294
|
-
redirectCount += 1;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
return response;
|
|
298
|
-
}
|
|
299
|
-
} finally {
|
|
300
|
-
clearTimeout(timeout);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
305
|
-
class DispatchWebhookJob extends Job {
|
|
306
|
-
maxAttempts = 3;
|
|
307
|
-
backoffMs = 2000;
|
|
308
|
-
async handle(payload) {
|
|
309
|
-
const rows = await repositoryConnection`
|
|
310
|
-
SELECT id, url, secret
|
|
311
|
-
FROM webhook
|
|
312
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
313
|
-
LIMIT 1
|
|
314
|
-
`;
|
|
315
|
-
const webhook = rows[0];
|
|
316
|
-
if (!webhook) {
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
320
|
-
const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
|
|
321
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
322
|
-
let responseStatus = null;
|
|
323
|
-
let errorMessage = null;
|
|
324
|
-
try {
|
|
325
|
-
const response = await safeFetch(webhook.url, {
|
|
326
|
-
method: "POST",
|
|
327
|
-
headers: {
|
|
328
|
-
"content-type": "application/json",
|
|
329
|
-
"x-workhub-signature": signature
|
|
330
|
-
},
|
|
331
|
-
body
|
|
332
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
333
|
-
responseStatus = response.status;
|
|
334
|
-
if (!response.ok) {
|
|
335
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
336
|
-
}
|
|
337
|
-
} catch (error) {
|
|
338
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
339
|
-
await repositoryConnection`
|
|
340
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
341
|
-
VALUES (
|
|
342
|
-
${webhook.id},
|
|
343
|
-
${payload.event},
|
|
344
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
345
|
-
${responseStatus},
|
|
346
|
-
${errorMessage}
|
|
347
|
-
)
|
|
348
|
-
`;
|
|
349
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
350
|
-
}
|
|
351
|
-
await repositoryConnection`
|
|
352
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
353
|
-
VALUES (
|
|
354
|
-
${webhook.id},
|
|
355
|
-
${payload.event},
|
|
356
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
357
|
-
${responseStatus}
|
|
358
|
-
)
|
|
359
|
-
`;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
@@ -1911,7 +1911,6 @@ var failedJobService_default = FailedJobService;
|
|
|
1911
1911
|
|
|
1912
1912
|
// ../../src/core/queue/jobRegistry.ts
|
|
1913
1913
|
class JobRegistry {
|
|
1914
|
-
constructor() {}
|
|
1915
1914
|
factories = new Map;
|
|
1916
1915
|
instances = new WeakMap;
|
|
1917
1916
|
register(name, factory) {
|
|
@@ -1935,10 +1934,26 @@ class JobRegistry {
|
|
|
1935
1934
|
return [...this.factories.keys()];
|
|
1936
1935
|
}
|
|
1937
1936
|
}
|
|
1938
|
-
var
|
|
1937
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
1938
|
+
function readSharedJobRegistry() {
|
|
1939
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
1940
|
+
if (globalRegistry) {
|
|
1941
|
+
return globalRegistry;
|
|
1942
|
+
}
|
|
1943
|
+
const registry = new JobRegistry;
|
|
1944
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
1945
|
+
return registry;
|
|
1946
|
+
}
|
|
1947
|
+
var jobRegistry = readSharedJobRegistry();
|
|
1939
1948
|
|
|
1940
|
-
// ../../src/
|
|
1949
|
+
// ../../src/core/contracts/serviceTokens.ts
|
|
1950
|
+
var CORE_CONFIG_TOKEN = "core.config";
|
|
1951
|
+
var CORE_CACHE_TOKEN = "core.cache";
|
|
1952
|
+
var CORE_QUEUE_TOKEN = "core.queue";
|
|
1953
|
+
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
1954
|
+
var CORE_AUTH_TOKEN = "core.auth";
|
|
1941
1955
|
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
1956
|
+
// ../../src/bootstrap/config.ts
|
|
1942
1957
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
1943
1958
|
|
|
1944
1959
|
// ../../src/config/queue.ts
|
|
@@ -2153,368 +2168,6 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
|
2153
2168
|
return new QueueWorker(redisUrl, failedJobs);
|
|
2154
2169
|
}
|
|
2155
2170
|
|
|
2156
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
2157
|
-
import { createHmac } from "crypto";
|
|
2158
|
-
|
|
2159
|
-
// ../../src/config/app.ts
|
|
2160
|
-
var appConfig = {
|
|
2161
|
-
name: "WorkHub",
|
|
2162
|
-
env: process.env.APP_ENV ?? "local",
|
|
2163
|
-
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
2164
|
-
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
2165
|
-
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
2166
|
-
};
|
|
2167
|
-
|
|
2168
|
-
// ../../src/core/queue/index.ts
|
|
2169
|
-
class Job {
|
|
2170
|
-
maxAttempts;
|
|
2171
|
-
backoffMs;
|
|
2172
|
-
priority;
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
// ../../src/core/security/safeUrl.ts
|
|
2176
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
2177
|
-
var dnsLookup = dnsLookupImpl;
|
|
2178
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
2179
|
-
"localhost",
|
|
2180
|
-
"127.0.0.1",
|
|
2181
|
-
"0.0.0.0",
|
|
2182
|
-
"::1",
|
|
2183
|
-
"metadata.google.internal"
|
|
2184
|
-
]);
|
|
2185
|
-
function isPrivateIpv4(hostname) {
|
|
2186
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
2187
|
-
if (!match) {
|
|
2188
|
-
return false;
|
|
2189
|
-
}
|
|
2190
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
2191
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
2192
|
-
return true;
|
|
2193
|
-
}
|
|
2194
|
-
const [a = 0, b = 0] = octets;
|
|
2195
|
-
if (a === 10) {
|
|
2196
|
-
return true;
|
|
2197
|
-
}
|
|
2198
|
-
if (a === 127) {
|
|
2199
|
-
return true;
|
|
2200
|
-
}
|
|
2201
|
-
if (a === 0) {
|
|
2202
|
-
return true;
|
|
2203
|
-
}
|
|
2204
|
-
if (a === 169 && b === 254) {
|
|
2205
|
-
return true;
|
|
2206
|
-
}
|
|
2207
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
2208
|
-
return true;
|
|
2209
|
-
}
|
|
2210
|
-
if (a === 192 && b === 168) {
|
|
2211
|
-
return true;
|
|
2212
|
-
}
|
|
2213
|
-
return false;
|
|
2214
|
-
}
|
|
2215
|
-
function isBlockedHostname(hostname) {
|
|
2216
|
-
const normalized = hostname.trim().toLowerCase();
|
|
2217
|
-
if (normalized.length === 0) {
|
|
2218
|
-
return true;
|
|
2219
|
-
}
|
|
2220
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
2221
|
-
return true;
|
|
2222
|
-
}
|
|
2223
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
2224
|
-
return true;
|
|
2225
|
-
}
|
|
2226
|
-
if (normalized.includes(":")) {
|
|
2227
|
-
return true;
|
|
2228
|
-
}
|
|
2229
|
-
return isPrivateIpv4(normalized);
|
|
2230
|
-
}
|
|
2231
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
2232
|
-
let parsed;
|
|
2233
|
-
try {
|
|
2234
|
-
parsed = new URL(rawUrl);
|
|
2235
|
-
} catch {
|
|
2236
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
2237
|
-
}
|
|
2238
|
-
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
2239
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
2240
|
-
}
|
|
2241
|
-
if (parsed.username || parsed.password) {
|
|
2242
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
2243
|
-
}
|
|
2244
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
2245
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
2246
|
-
}
|
|
2247
|
-
return parsed;
|
|
2248
|
-
}
|
|
2249
|
-
function isBlockedIpAddress(address) {
|
|
2250
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
2251
|
-
}
|
|
2252
|
-
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
2253
|
-
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
2254
|
-
if (options.resolveDns === false) {
|
|
2255
|
-
return parsed;
|
|
2256
|
-
}
|
|
2257
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
2258
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
2259
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
2260
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
2261
|
-
}
|
|
2262
|
-
return parsed;
|
|
2263
|
-
}
|
|
2264
|
-
function setDnsLookupForTests(lookupFn) {
|
|
2265
|
-
dnsLookup = lookupFn;
|
|
2266
|
-
}
|
|
2267
|
-
function resetDnsLookupForTests() {
|
|
2268
|
-
dnsLookup = dnsLookupImpl;
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
// ../../src/core/security/safeFetch.ts
|
|
2272
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
2273
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
2274
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
2275
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
2276
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
2277
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
2278
|
-
const controller = new AbortController;
|
|
2279
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
2280
|
-
try {
|
|
2281
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
2282
|
-
let redirectCount = 0;
|
|
2283
|
-
while (true) {
|
|
2284
|
-
const response = await fetch(currentUrl, {
|
|
2285
|
-
...init,
|
|
2286
|
-
signal: controller.signal,
|
|
2287
|
-
redirect: "manual"
|
|
2288
|
-
});
|
|
2289
|
-
if (response.status >= 300 && response.status < 400) {
|
|
2290
|
-
const location = response.headers.get("location");
|
|
2291
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
2292
|
-
return response;
|
|
2293
|
-
}
|
|
2294
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
2295
|
-
redirectCount += 1;
|
|
2296
|
-
continue;
|
|
2297
|
-
}
|
|
2298
|
-
return response;
|
|
2299
|
-
}
|
|
2300
|
-
} finally {
|
|
2301
|
-
clearTimeout(timeout);
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
2306
|
-
class DispatchWebhookJob extends Job {
|
|
2307
|
-
maxAttempts = 3;
|
|
2308
|
-
backoffMs = 2000;
|
|
2309
|
-
async handle(payload) {
|
|
2310
|
-
const rows = await repositoryConnection`
|
|
2311
|
-
SELECT id, url, secret
|
|
2312
|
-
FROM webhook
|
|
2313
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
2314
|
-
LIMIT 1
|
|
2315
|
-
`;
|
|
2316
|
-
const webhook = rows[0];
|
|
2317
|
-
if (!webhook) {
|
|
2318
|
-
return;
|
|
2319
|
-
}
|
|
2320
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
2321
|
-
const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
|
|
2322
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
2323
|
-
let responseStatus = null;
|
|
2324
|
-
let errorMessage = null;
|
|
2325
|
-
try {
|
|
2326
|
-
const response = await safeFetch(webhook.url, {
|
|
2327
|
-
method: "POST",
|
|
2328
|
-
headers: {
|
|
2329
|
-
"content-type": "application/json",
|
|
2330
|
-
"x-workhub-signature": signature
|
|
2331
|
-
},
|
|
2332
|
-
body
|
|
2333
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
2334
|
-
responseStatus = response.status;
|
|
2335
|
-
if (!response.ok) {
|
|
2336
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
2337
|
-
}
|
|
2338
|
-
} catch (error) {
|
|
2339
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
2340
|
-
await repositoryConnection`
|
|
2341
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
2342
|
-
VALUES (
|
|
2343
|
-
${webhook.id},
|
|
2344
|
-
${payload.event},
|
|
2345
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
2346
|
-
${responseStatus},
|
|
2347
|
-
${errorMessage}
|
|
2348
|
-
)
|
|
2349
|
-
`;
|
|
2350
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
2351
|
-
}
|
|
2352
|
-
await repositoryConnection`
|
|
2353
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
2354
|
-
VALUES (
|
|
2355
|
-
${webhook.id},
|
|
2356
|
-
${payload.event},
|
|
2357
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
2358
|
-
${responseStatus}
|
|
2359
|
-
)
|
|
2360
|
-
`;
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
2364
|
-
|
|
2365
|
-
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
2366
|
-
class InvalidateCacheTagsJob extends Job {
|
|
2367
|
-
cache;
|
|
2368
|
-
constructor(cache) {
|
|
2369
|
-
super();
|
|
2370
|
-
this.cache = cache;
|
|
2371
|
-
}
|
|
2372
|
-
async handle(payload) {
|
|
2373
|
-
await this.cache.tags(...payload.tags).flush();
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
2377
|
-
|
|
2378
|
-
// ../../src/core/logging/logger.ts
|
|
2379
|
-
class Logger {
|
|
2380
|
-
channel;
|
|
2381
|
-
constructor(channel = "app") {
|
|
2382
|
-
this.channel = channel;
|
|
2383
|
-
}
|
|
2384
|
-
write(level, message, context = {}) {
|
|
2385
|
-
const entry = {
|
|
2386
|
-
level,
|
|
2387
|
-
channel: this.channel,
|
|
2388
|
-
message,
|
|
2389
|
-
timestamp: new Date().toISOString(),
|
|
2390
|
-
...context
|
|
2391
|
-
};
|
|
2392
|
-
const line = JSON.stringify(entry);
|
|
2393
|
-
if (level === "error") {
|
|
2394
|
-
console.error(line);
|
|
2395
|
-
return;
|
|
2396
|
-
}
|
|
2397
|
-
console.log(line);
|
|
2398
|
-
}
|
|
2399
|
-
debug(message, context) {
|
|
2400
|
-
this.write("debug", message, context);
|
|
2401
|
-
}
|
|
2402
|
-
info(message, context) {
|
|
2403
|
-
this.write("info", message, context);
|
|
2404
|
-
}
|
|
2405
|
-
warn(message, context) {
|
|
2406
|
-
this.write("warn", message, context);
|
|
2407
|
-
}
|
|
2408
|
-
error(message, context) {
|
|
2409
|
-
this.write("error", message, context);
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
var appLogger = new Logger("app");
|
|
2413
|
-
|
|
2414
|
-
// ../../src/bootstrap/contracts.ts
|
|
2415
|
-
class ServiceContainer {
|
|
2416
|
-
services = new Map;
|
|
2417
|
-
singletonFactories = new Map;
|
|
2418
|
-
bindings = new Map;
|
|
2419
|
-
set(key, value) {
|
|
2420
|
-
this.singletonFactories.delete(key);
|
|
2421
|
-
this.bindings.delete(key);
|
|
2422
|
-
this.services.set(key, value);
|
|
2423
|
-
return value;
|
|
2424
|
-
}
|
|
2425
|
-
singleton(key, factory) {
|
|
2426
|
-
this.bindings.delete(key);
|
|
2427
|
-
this.services.delete(key);
|
|
2428
|
-
this.singletonFactories.set(key, factory);
|
|
2429
|
-
}
|
|
2430
|
-
bind(key, factory) {
|
|
2431
|
-
this.singletonFactories.delete(key);
|
|
2432
|
-
this.services.delete(key);
|
|
2433
|
-
this.bindings.set(key, factory);
|
|
2434
|
-
}
|
|
2435
|
-
get(key) {
|
|
2436
|
-
if (this.services.has(key)) {
|
|
2437
|
-
return this.services.get(key);
|
|
2438
|
-
}
|
|
2439
|
-
const singletonFactory = this.singletonFactories.get(key);
|
|
2440
|
-
if (singletonFactory) {
|
|
2441
|
-
const value = singletonFactory(this);
|
|
2442
|
-
this.services.set(key, value);
|
|
2443
|
-
return value;
|
|
2444
|
-
}
|
|
2445
|
-
const binding = this.bindings.get(key);
|
|
2446
|
-
if (binding) {
|
|
2447
|
-
return binding(this);
|
|
2448
|
-
}
|
|
2449
|
-
throw new Error(`Service "${key}" is not registered.`);
|
|
2450
|
-
}
|
|
2451
|
-
resolve(key) {
|
|
2452
|
-
return this.get(key);
|
|
2453
|
-
}
|
|
2454
|
-
has(key) {
|
|
2455
|
-
return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
class ConfigStore {
|
|
2460
|
-
values = new Map;
|
|
2461
|
-
set(key, value) {
|
|
2462
|
-
this.values.set(key, value);
|
|
2463
|
-
return value;
|
|
2464
|
-
}
|
|
2465
|
-
get(key) {
|
|
2466
|
-
return this.values.get(key);
|
|
2467
|
-
}
|
|
2468
|
-
require(key) {
|
|
2469
|
-
if (!this.values.has(key)) {
|
|
2470
|
-
throw new Error(`Config key "${key}" is not defined.`);
|
|
2471
|
-
}
|
|
2472
|
-
return this.values.get(key);
|
|
2473
|
-
}
|
|
2474
|
-
has(key) {
|
|
2475
|
-
return this.values.has(key);
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
function getRequiredDependency(dependencies, key) {
|
|
2479
|
-
const dependency = dependencies[key];
|
|
2480
|
-
if (dependency === undefined) {
|
|
2481
|
-
throw new Error(`Required dependency "${key}" is not registered.`);
|
|
2482
|
-
}
|
|
2483
|
-
return dependency;
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
// ../../src/bootstrap/applicationRegistry.ts
|
|
2487
|
-
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
2488
|
-
var activeContext;
|
|
2489
|
-
function readStoredApplicationContext() {
|
|
2490
|
-
if (activeContext) {
|
|
2491
|
-
return activeContext;
|
|
2492
|
-
}
|
|
2493
|
-
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
2494
|
-
if (globalContext) {
|
|
2495
|
-
activeContext = globalContext;
|
|
2496
|
-
}
|
|
2497
|
-
return activeContext;
|
|
2498
|
-
}
|
|
2499
|
-
function requireActiveApplicationContext() {
|
|
2500
|
-
const context = readStoredApplicationContext();
|
|
2501
|
-
if (!context) {
|
|
2502
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
2503
|
-
}
|
|
2504
|
-
return context;
|
|
2505
|
-
}
|
|
2506
|
-
function resolveApplicationCache() {
|
|
2507
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
|
-
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
2511
|
-
function registerDefaultJobs() {
|
|
2512
|
-
jobRegistry.register("cache.invalidate-tags", () => {
|
|
2513
|
-
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
2514
|
-
});
|
|
2515
|
-
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
2171
|
// ../../src/core/queue/createAppQueue.ts
|
|
2519
2172
|
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
2520
2173
|
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
@@ -2525,7 +2178,6 @@ function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(),
|
|
|
2525
2178
|
});
|
|
2526
2179
|
}
|
|
2527
2180
|
export {
|
|
2528
|
-
registerDefaultJobs,
|
|
2529
2181
|
createTrackedJob,
|
|
2530
2182
|
createQueueWorker,
|
|
2531
2183
|
createFailedJobService,
|