@getstrata/core 0.5.21 → 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/core/auth/membershipService.d.ts +1 -1
- package/dist/{bootstrap/membershipService.d.ts → core/auth/resolveMembershipService.d.ts} +1 -1
- 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/entries/contracts/serviceTokens.js +16 -0
- package/dist/entries/http/webErrorResponse.js +5 -0
- package/dist/entries/jobs/dispatchWebhookJob.js +0 -104
- package/dist/entries/queue/createAppQueue.js +16 -301
- package/dist/entries/queue/jobRegistry.js +11 -2
- package/dist/entries/queue/jobRunner.js +16 -2
- package/dist/entries/queue/publicQueue.js +16 -2
- package/dist/entries/queue/queueMetrics.js +16 -300
- package/dist/entries/view.js +5 -0
- package/dist/framework/public-api.d.ts +0 -1
- package/dist/index.js +14 -33
- package/package.json +7 -2
- package/dist/bootstrap/applicationRegistry.d.ts +0 -1
- 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
|
@@ -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";
|
|
@@ -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,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
|
+
};
|
|
@@ -183,6 +183,11 @@ function isHtmxRequest(request) {
|
|
|
183
183
|
return request.headers.get("HX-Request") === "true";
|
|
184
184
|
}
|
|
185
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";
|
|
187
192
|
// ../../src/bootstrap/config.ts
|
|
188
193
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
@@ -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,9 +1934,24 @@ 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
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";
|
|
1942
1956
|
// ../../src/bootstrap/config.ts
|
|
1943
1957
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
@@ -2154,304 +2168,6 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
|
2154
2168
|
return new QueueWorker(redisUrl, failedJobs);
|
|
2155
2169
|
}
|
|
2156
2170
|
|
|
2157
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
2158
|
-
import { createHmac } from "crypto";
|
|
2159
|
-
|
|
2160
|
-
// ../../src/config/app.ts
|
|
2161
|
-
var appConfig = {
|
|
2162
|
-
name: "WorkHub",
|
|
2163
|
-
env: process.env.APP_ENV ?? "local",
|
|
2164
|
-
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
2165
|
-
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
2166
|
-
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
2167
|
-
};
|
|
2168
|
-
|
|
2169
|
-
// ../../src/core/queue/index.ts
|
|
2170
|
-
class Job {
|
|
2171
|
-
maxAttempts;
|
|
2172
|
-
backoffMs;
|
|
2173
|
-
priority;
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
// ../../src/core/security/safeUrl.ts
|
|
2177
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
2178
|
-
var dnsLookup = dnsLookupImpl;
|
|
2179
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
2180
|
-
"localhost",
|
|
2181
|
-
"127.0.0.1",
|
|
2182
|
-
"0.0.0.0",
|
|
2183
|
-
"::1",
|
|
2184
|
-
"metadata.google.internal"
|
|
2185
|
-
]);
|
|
2186
|
-
function isPrivateIpv4(hostname) {
|
|
2187
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
2188
|
-
if (!match) {
|
|
2189
|
-
return false;
|
|
2190
|
-
}
|
|
2191
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
2192
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
2193
|
-
return true;
|
|
2194
|
-
}
|
|
2195
|
-
const [a = 0, b = 0] = octets;
|
|
2196
|
-
if (a === 10) {
|
|
2197
|
-
return true;
|
|
2198
|
-
}
|
|
2199
|
-
if (a === 127) {
|
|
2200
|
-
return true;
|
|
2201
|
-
}
|
|
2202
|
-
if (a === 0) {
|
|
2203
|
-
return true;
|
|
2204
|
-
}
|
|
2205
|
-
if (a === 169 && b === 254) {
|
|
2206
|
-
return true;
|
|
2207
|
-
}
|
|
2208
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
2209
|
-
return true;
|
|
2210
|
-
}
|
|
2211
|
-
if (a === 192 && b === 168) {
|
|
2212
|
-
return true;
|
|
2213
|
-
}
|
|
2214
|
-
return false;
|
|
2215
|
-
}
|
|
2216
|
-
function isBlockedHostname(hostname) {
|
|
2217
|
-
const normalized = hostname.trim().toLowerCase();
|
|
2218
|
-
if (normalized.length === 0) {
|
|
2219
|
-
return true;
|
|
2220
|
-
}
|
|
2221
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
2222
|
-
return true;
|
|
2223
|
-
}
|
|
2224
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
2225
|
-
return true;
|
|
2226
|
-
}
|
|
2227
|
-
if (normalized.includes(":")) {
|
|
2228
|
-
return true;
|
|
2229
|
-
}
|
|
2230
|
-
return isPrivateIpv4(normalized);
|
|
2231
|
-
}
|
|
2232
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
2233
|
-
let parsed;
|
|
2234
|
-
try {
|
|
2235
|
-
parsed = new URL(rawUrl);
|
|
2236
|
-
} catch {
|
|
2237
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
2238
|
-
}
|
|
2239
|
-
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
2240
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
2241
|
-
}
|
|
2242
|
-
if (parsed.username || parsed.password) {
|
|
2243
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
2244
|
-
}
|
|
2245
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
2246
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
2247
|
-
}
|
|
2248
|
-
return parsed;
|
|
2249
|
-
}
|
|
2250
|
-
function isBlockedIpAddress(address) {
|
|
2251
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
2252
|
-
}
|
|
2253
|
-
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
2254
|
-
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
2255
|
-
if (options.resolveDns === false) {
|
|
2256
|
-
return parsed;
|
|
2257
|
-
}
|
|
2258
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
2259
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
2260
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
2261
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
2262
|
-
}
|
|
2263
|
-
return parsed;
|
|
2264
|
-
}
|
|
2265
|
-
function setDnsLookupForTests(lookupFn) {
|
|
2266
|
-
dnsLookup = lookupFn;
|
|
2267
|
-
}
|
|
2268
|
-
function resetDnsLookupForTests() {
|
|
2269
|
-
dnsLookup = dnsLookupImpl;
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
// ../../src/core/security/safeFetch.ts
|
|
2273
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
2274
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
2275
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
2276
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
2277
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
2278
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
2279
|
-
const controller = new AbortController;
|
|
2280
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
2281
|
-
try {
|
|
2282
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
2283
|
-
let redirectCount = 0;
|
|
2284
|
-
while (true) {
|
|
2285
|
-
const response = await fetch(currentUrl, {
|
|
2286
|
-
...init,
|
|
2287
|
-
signal: controller.signal,
|
|
2288
|
-
redirect: "manual"
|
|
2289
|
-
});
|
|
2290
|
-
if (response.status >= 300 && response.status < 400) {
|
|
2291
|
-
const location = response.headers.get("location");
|
|
2292
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
2293
|
-
return response;
|
|
2294
|
-
}
|
|
2295
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
2296
|
-
redirectCount += 1;
|
|
2297
|
-
continue;
|
|
2298
|
-
}
|
|
2299
|
-
return response;
|
|
2300
|
-
}
|
|
2301
|
-
} finally {
|
|
2302
|
-
clearTimeout(timeout);
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
2307
|
-
class DispatchWebhookJob extends Job {
|
|
2308
|
-
maxAttempts = 3;
|
|
2309
|
-
backoffMs = 2000;
|
|
2310
|
-
async handle(payload) {
|
|
2311
|
-
const rows = await repositoryConnection`
|
|
2312
|
-
SELECT id, url, secret
|
|
2313
|
-
FROM webhook
|
|
2314
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
2315
|
-
LIMIT 1
|
|
2316
|
-
`;
|
|
2317
|
-
const webhook = rows[0];
|
|
2318
|
-
if (!webhook) {
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
2322
|
-
const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
|
|
2323
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
2324
|
-
let responseStatus = null;
|
|
2325
|
-
let errorMessage = null;
|
|
2326
|
-
try {
|
|
2327
|
-
const response = await safeFetch(webhook.url, {
|
|
2328
|
-
method: "POST",
|
|
2329
|
-
headers: {
|
|
2330
|
-
"content-type": "application/json",
|
|
2331
|
-
"x-workhub-signature": signature
|
|
2332
|
-
},
|
|
2333
|
-
body
|
|
2334
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
2335
|
-
responseStatus = response.status;
|
|
2336
|
-
if (!response.ok) {
|
|
2337
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
2338
|
-
}
|
|
2339
|
-
} catch (error) {
|
|
2340
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
2341
|
-
await repositoryConnection`
|
|
2342
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
2343
|
-
VALUES (
|
|
2344
|
-
${webhook.id},
|
|
2345
|
-
${payload.event},
|
|
2346
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
2347
|
-
${responseStatus},
|
|
2348
|
-
${errorMessage}
|
|
2349
|
-
)
|
|
2350
|
-
`;
|
|
2351
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
2352
|
-
}
|
|
2353
|
-
await repositoryConnection`
|
|
2354
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
2355
|
-
VALUES (
|
|
2356
|
-
${webhook.id},
|
|
2357
|
-
${payload.event},
|
|
2358
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
2359
|
-
${responseStatus}
|
|
2360
|
-
)
|
|
2361
|
-
`;
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
2365
|
-
|
|
2366
|
-
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
2367
|
-
class InvalidateCacheTagsJob extends Job {
|
|
2368
|
-
cache;
|
|
2369
|
-
constructor(cache) {
|
|
2370
|
-
super();
|
|
2371
|
-
this.cache = cache;
|
|
2372
|
-
}
|
|
2373
|
-
async handle(payload) {
|
|
2374
|
-
await this.cache.tags(...payload.tags).flush();
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
2378
|
-
|
|
2379
|
-
// ../../src/core/contracts/applicationContext.ts
|
|
2380
|
-
function getRequiredDependency(dependencies, key) {
|
|
2381
|
-
const dependency = dependencies[key];
|
|
2382
|
-
if (dependency === undefined) {
|
|
2383
|
-
throw new Error(`Required dependency "${String(key)}" is not registered.`);
|
|
2384
|
-
}
|
|
2385
|
-
return dependency;
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
// ../../src/core/logging/logger.ts
|
|
2389
|
-
class Logger {
|
|
2390
|
-
channel;
|
|
2391
|
-
constructor(channel = "app") {
|
|
2392
|
-
this.channel = channel;
|
|
2393
|
-
}
|
|
2394
|
-
write(level, message, context = {}) {
|
|
2395
|
-
const entry = {
|
|
2396
|
-
level,
|
|
2397
|
-
channel: this.channel,
|
|
2398
|
-
message,
|
|
2399
|
-
timestamp: new Date().toISOString(),
|
|
2400
|
-
...context
|
|
2401
|
-
};
|
|
2402
|
-
const line = JSON.stringify(entry);
|
|
2403
|
-
if (level === "error") {
|
|
2404
|
-
console.error(line);
|
|
2405
|
-
return;
|
|
2406
|
-
}
|
|
2407
|
-
console.log(line);
|
|
2408
|
-
}
|
|
2409
|
-
debug(message, context) {
|
|
2410
|
-
this.write("debug", message, context);
|
|
2411
|
-
}
|
|
2412
|
-
info(message, context) {
|
|
2413
|
-
this.write("info", message, context);
|
|
2414
|
-
}
|
|
2415
|
-
warn(message, context) {
|
|
2416
|
-
this.write("warn", message, context);
|
|
2417
|
-
}
|
|
2418
|
-
error(message, context) {
|
|
2419
|
-
this.write("error", message, context);
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
var appLogger = new Logger("app");
|
|
2423
|
-
|
|
2424
|
-
// ../../src/core/runtime/applicationRegistry.ts
|
|
2425
|
-
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
2426
|
-
var activeContext;
|
|
2427
|
-
function readStoredApplicationContext() {
|
|
2428
|
-
if (activeContext) {
|
|
2429
|
-
return activeContext;
|
|
2430
|
-
}
|
|
2431
|
-
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
2432
|
-
if (globalContext) {
|
|
2433
|
-
activeContext = globalContext;
|
|
2434
|
-
}
|
|
2435
|
-
return activeContext;
|
|
2436
|
-
}
|
|
2437
|
-
function requireActiveApplicationContext() {
|
|
2438
|
-
const context = readStoredApplicationContext();
|
|
2439
|
-
if (!context) {
|
|
2440
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
2441
|
-
}
|
|
2442
|
-
return context;
|
|
2443
|
-
}
|
|
2444
|
-
function resolveApplicationCache() {
|
|
2445
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
2446
|
-
}
|
|
2447
|
-
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
2448
|
-
function registerDefaultJobs() {
|
|
2449
|
-
jobRegistry.register("cache.invalidate-tags", () => {
|
|
2450
|
-
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
2451
|
-
});
|
|
2452
|
-
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
2171
|
// ../../src/core/queue/createAppQueue.ts
|
|
2456
2172
|
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
2457
2173
|
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
@@ -2462,7 +2178,6 @@ function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(),
|
|
|
2462
2178
|
});
|
|
2463
2179
|
}
|
|
2464
2180
|
export {
|
|
2465
|
-
registerDefaultJobs,
|
|
2466
2181
|
createTrackedJob,
|
|
2467
2182
|
createQueueWorker,
|
|
2468
2183
|
createFailedJobService,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/queue/jobRegistry.ts
|
|
3
3
|
class JobRegistry {
|
|
4
|
-
constructor() {}
|
|
5
4
|
factories = new Map;
|
|
6
5
|
instances = new WeakMap;
|
|
7
6
|
register(name, factory) {
|
|
@@ -25,7 +24,17 @@ class JobRegistry {
|
|
|
25
24
|
return [...this.factories.keys()];
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
|
-
var
|
|
27
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
28
|
+
function readSharedJobRegistry() {
|
|
29
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
30
|
+
if (globalRegistry) {
|
|
31
|
+
return globalRegistry;
|
|
32
|
+
}
|
|
33
|
+
const registry = new JobRegistry;
|
|
34
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
35
|
+
return registry;
|
|
36
|
+
}
|
|
37
|
+
var jobRegistry = readSharedJobRegistry();
|
|
29
38
|
export {
|
|
30
39
|
jobRegistry,
|
|
31
40
|
JobRegistry
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// @bun
|
|
2
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";
|
|
3
8
|
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
4
9
|
// ../../src/bootstrap/config.ts
|
|
5
10
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
@@ -13,7 +18,6 @@ var queueConfig = {
|
|
|
13
18
|
|
|
14
19
|
// ../../src/core/queue/jobRegistry.ts
|
|
15
20
|
class JobRegistry {
|
|
16
|
-
constructor() {}
|
|
17
21
|
factories = new Map;
|
|
18
22
|
instances = new WeakMap;
|
|
19
23
|
register(name, factory) {
|
|
@@ -37,7 +41,17 @@ class JobRegistry {
|
|
|
37
41
|
return [...this.factories.keys()];
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
|
-
var
|
|
44
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
45
|
+
function readSharedJobRegistry() {
|
|
46
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
47
|
+
if (globalRegistry) {
|
|
48
|
+
return globalRegistry;
|
|
49
|
+
}
|
|
50
|
+
const registry = new JobRegistry;
|
|
51
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
52
|
+
return registry;
|
|
53
|
+
}
|
|
54
|
+
var jobRegistry = readSharedJobRegistry();
|
|
41
55
|
|
|
42
56
|
// ../../src/core/queue/jobRunner.ts
|
|
43
57
|
async function runQueueJob(envelope, failedJobs) {
|
|
@@ -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,9 +1934,24 @@ 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
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";
|
|
1942
1956
|
// ../../src/bootstrap/config.ts
|
|
1943
1957
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
4
4
|
|
|
5
5
|
// ../../src/core/contracts/serviceTokens.ts
|
|
6
|
+
var CORE_CONFIG_TOKEN = "core.config";
|
|
7
|
+
var CORE_CACHE_TOKEN = "core.cache";
|
|
8
|
+
var CORE_QUEUE_TOKEN = "core.queue";
|
|
9
|
+
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
10
|
+
var CORE_AUTH_TOKEN = "core.auth";
|
|
6
11
|
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
7
12
|
// ../../src/bootstrap/config.ts
|
|
8
13
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
@@ -1926,7 +1931,6 @@ var failedJobService_default = FailedJobService;
|
|
|
1926
1931
|
|
|
1927
1932
|
// ../../src/core/queue/jobRegistry.ts
|
|
1928
1933
|
class JobRegistry {
|
|
1929
|
-
constructor() {}
|
|
1930
1934
|
factories = new Map;
|
|
1931
1935
|
instances = new WeakMap;
|
|
1932
1936
|
register(name, factory) {
|
|
@@ -1950,7 +1954,17 @@ class JobRegistry {
|
|
|
1950
1954
|
return [...this.factories.keys()];
|
|
1951
1955
|
}
|
|
1952
1956
|
}
|
|
1953
|
-
var
|
|
1957
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
1958
|
+
function readSharedJobRegistry() {
|
|
1959
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
1960
|
+
if (globalRegistry) {
|
|
1961
|
+
return globalRegistry;
|
|
1962
|
+
}
|
|
1963
|
+
const registry = new JobRegistry;
|
|
1964
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
1965
|
+
return registry;
|
|
1966
|
+
}
|
|
1967
|
+
var jobRegistry = readSharedJobRegistry();
|
|
1954
1968
|
|
|
1955
1969
|
// ../../src/core/queue/jobRunner.ts
|
|
1956
1970
|
async function runQueueJob(envelope, failedJobs) {
|
|
@@ -2157,304 +2171,6 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
|
2157
2171
|
return new QueueWorker(redisUrl, failedJobs);
|
|
2158
2172
|
}
|
|
2159
2173
|
|
|
2160
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
2161
|
-
import { createHmac } from "crypto";
|
|
2162
|
-
|
|
2163
|
-
// ../../src/config/app.ts
|
|
2164
|
-
var appConfig = {
|
|
2165
|
-
name: "WorkHub",
|
|
2166
|
-
env: process.env.APP_ENV ?? "local",
|
|
2167
|
-
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
2168
|
-
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
2169
|
-
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
2170
|
-
};
|
|
2171
|
-
|
|
2172
|
-
// ../../src/core/queue/index.ts
|
|
2173
|
-
class Job {
|
|
2174
|
-
maxAttempts;
|
|
2175
|
-
backoffMs;
|
|
2176
|
-
priority;
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
// ../../src/core/security/safeUrl.ts
|
|
2180
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
2181
|
-
var dnsLookup = dnsLookupImpl;
|
|
2182
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
2183
|
-
"localhost",
|
|
2184
|
-
"127.0.0.1",
|
|
2185
|
-
"0.0.0.0",
|
|
2186
|
-
"::1",
|
|
2187
|
-
"metadata.google.internal"
|
|
2188
|
-
]);
|
|
2189
|
-
function isPrivateIpv4(hostname) {
|
|
2190
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
2191
|
-
if (!match) {
|
|
2192
|
-
return false;
|
|
2193
|
-
}
|
|
2194
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
2195
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
2196
|
-
return true;
|
|
2197
|
-
}
|
|
2198
|
-
const [a = 0, b = 0] = octets;
|
|
2199
|
-
if (a === 10) {
|
|
2200
|
-
return true;
|
|
2201
|
-
}
|
|
2202
|
-
if (a === 127) {
|
|
2203
|
-
return true;
|
|
2204
|
-
}
|
|
2205
|
-
if (a === 0) {
|
|
2206
|
-
return true;
|
|
2207
|
-
}
|
|
2208
|
-
if (a === 169 && b === 254) {
|
|
2209
|
-
return true;
|
|
2210
|
-
}
|
|
2211
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
2212
|
-
return true;
|
|
2213
|
-
}
|
|
2214
|
-
if (a === 192 && b === 168) {
|
|
2215
|
-
return true;
|
|
2216
|
-
}
|
|
2217
|
-
return false;
|
|
2218
|
-
}
|
|
2219
|
-
function isBlockedHostname(hostname) {
|
|
2220
|
-
const normalized = hostname.trim().toLowerCase();
|
|
2221
|
-
if (normalized.length === 0) {
|
|
2222
|
-
return true;
|
|
2223
|
-
}
|
|
2224
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
2225
|
-
return true;
|
|
2226
|
-
}
|
|
2227
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
2228
|
-
return true;
|
|
2229
|
-
}
|
|
2230
|
-
if (normalized.includes(":")) {
|
|
2231
|
-
return true;
|
|
2232
|
-
}
|
|
2233
|
-
return isPrivateIpv4(normalized);
|
|
2234
|
-
}
|
|
2235
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
2236
|
-
let parsed;
|
|
2237
|
-
try {
|
|
2238
|
-
parsed = new URL(rawUrl);
|
|
2239
|
-
} catch {
|
|
2240
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
2241
|
-
}
|
|
2242
|
-
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
2243
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
2244
|
-
}
|
|
2245
|
-
if (parsed.username || parsed.password) {
|
|
2246
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
2247
|
-
}
|
|
2248
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
2249
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
2250
|
-
}
|
|
2251
|
-
return parsed;
|
|
2252
|
-
}
|
|
2253
|
-
function isBlockedIpAddress(address) {
|
|
2254
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
2255
|
-
}
|
|
2256
|
-
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
2257
|
-
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
2258
|
-
if (options.resolveDns === false) {
|
|
2259
|
-
return parsed;
|
|
2260
|
-
}
|
|
2261
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
2262
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
2263
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
2264
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
2265
|
-
}
|
|
2266
|
-
return parsed;
|
|
2267
|
-
}
|
|
2268
|
-
function setDnsLookupForTests(lookupFn) {
|
|
2269
|
-
dnsLookup = lookupFn;
|
|
2270
|
-
}
|
|
2271
|
-
function resetDnsLookupForTests() {
|
|
2272
|
-
dnsLookup = dnsLookupImpl;
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
// ../../src/core/security/safeFetch.ts
|
|
2276
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
2277
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
2278
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
2279
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
2280
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
2281
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
2282
|
-
const controller = new AbortController;
|
|
2283
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
2284
|
-
try {
|
|
2285
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
2286
|
-
let redirectCount = 0;
|
|
2287
|
-
while (true) {
|
|
2288
|
-
const response = await fetch(currentUrl, {
|
|
2289
|
-
...init,
|
|
2290
|
-
signal: controller.signal,
|
|
2291
|
-
redirect: "manual"
|
|
2292
|
-
});
|
|
2293
|
-
if (response.status >= 300 && response.status < 400) {
|
|
2294
|
-
const location = response.headers.get("location");
|
|
2295
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
2296
|
-
return response;
|
|
2297
|
-
}
|
|
2298
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
2299
|
-
redirectCount += 1;
|
|
2300
|
-
continue;
|
|
2301
|
-
}
|
|
2302
|
-
return response;
|
|
2303
|
-
}
|
|
2304
|
-
} finally {
|
|
2305
|
-
clearTimeout(timeout);
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
2310
|
-
class DispatchWebhookJob extends Job {
|
|
2311
|
-
maxAttempts = 3;
|
|
2312
|
-
backoffMs = 2000;
|
|
2313
|
-
async handle(payload) {
|
|
2314
|
-
const rows = await repositoryConnection`
|
|
2315
|
-
SELECT id, url, secret
|
|
2316
|
-
FROM webhook
|
|
2317
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
2318
|
-
LIMIT 1
|
|
2319
|
-
`;
|
|
2320
|
-
const webhook = rows[0];
|
|
2321
|
-
if (!webhook) {
|
|
2322
|
-
return;
|
|
2323
|
-
}
|
|
2324
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
2325
|
-
const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
|
|
2326
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
2327
|
-
let responseStatus = null;
|
|
2328
|
-
let errorMessage = null;
|
|
2329
|
-
try {
|
|
2330
|
-
const response = await safeFetch(webhook.url, {
|
|
2331
|
-
method: "POST",
|
|
2332
|
-
headers: {
|
|
2333
|
-
"content-type": "application/json",
|
|
2334
|
-
"x-workhub-signature": signature
|
|
2335
|
-
},
|
|
2336
|
-
body
|
|
2337
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
2338
|
-
responseStatus = response.status;
|
|
2339
|
-
if (!response.ok) {
|
|
2340
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
2341
|
-
}
|
|
2342
|
-
} catch (error) {
|
|
2343
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
2344
|
-
await repositoryConnection`
|
|
2345
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
2346
|
-
VALUES (
|
|
2347
|
-
${webhook.id},
|
|
2348
|
-
${payload.event},
|
|
2349
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
2350
|
-
${responseStatus},
|
|
2351
|
-
${errorMessage}
|
|
2352
|
-
)
|
|
2353
|
-
`;
|
|
2354
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
2355
|
-
}
|
|
2356
|
-
await repositoryConnection`
|
|
2357
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
2358
|
-
VALUES (
|
|
2359
|
-
${webhook.id},
|
|
2360
|
-
${payload.event},
|
|
2361
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
2362
|
-
${responseStatus}
|
|
2363
|
-
)
|
|
2364
|
-
`;
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
2368
|
-
|
|
2369
|
-
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
2370
|
-
class InvalidateCacheTagsJob extends Job {
|
|
2371
|
-
cache;
|
|
2372
|
-
constructor(cache) {
|
|
2373
|
-
super();
|
|
2374
|
-
this.cache = cache;
|
|
2375
|
-
}
|
|
2376
|
-
async handle(payload) {
|
|
2377
|
-
await this.cache.tags(...payload.tags).flush();
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
2381
|
-
|
|
2382
|
-
// ../../src/core/contracts/applicationContext.ts
|
|
2383
|
-
function getRequiredDependency(dependencies, key) {
|
|
2384
|
-
const dependency = dependencies[key];
|
|
2385
|
-
if (dependency === undefined) {
|
|
2386
|
-
throw new Error(`Required dependency "${String(key)}" is not registered.`);
|
|
2387
|
-
}
|
|
2388
|
-
return dependency;
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
// ../../src/core/logging/logger.ts
|
|
2392
|
-
class Logger {
|
|
2393
|
-
channel;
|
|
2394
|
-
constructor(channel = "app") {
|
|
2395
|
-
this.channel = channel;
|
|
2396
|
-
}
|
|
2397
|
-
write(level, message, context = {}) {
|
|
2398
|
-
const entry = {
|
|
2399
|
-
level,
|
|
2400
|
-
channel: this.channel,
|
|
2401
|
-
message,
|
|
2402
|
-
timestamp: new Date().toISOString(),
|
|
2403
|
-
...context
|
|
2404
|
-
};
|
|
2405
|
-
const line = JSON.stringify(entry);
|
|
2406
|
-
if (level === "error") {
|
|
2407
|
-
console.error(line);
|
|
2408
|
-
return;
|
|
2409
|
-
}
|
|
2410
|
-
console.log(line);
|
|
2411
|
-
}
|
|
2412
|
-
debug(message, context) {
|
|
2413
|
-
this.write("debug", message, context);
|
|
2414
|
-
}
|
|
2415
|
-
info(message, context) {
|
|
2416
|
-
this.write("info", message, context);
|
|
2417
|
-
}
|
|
2418
|
-
warn(message, context) {
|
|
2419
|
-
this.write("warn", message, context);
|
|
2420
|
-
}
|
|
2421
|
-
error(message, context) {
|
|
2422
|
-
this.write("error", message, context);
|
|
2423
|
-
}
|
|
2424
|
-
}
|
|
2425
|
-
var appLogger = new Logger("app");
|
|
2426
|
-
|
|
2427
|
-
// ../../src/core/runtime/applicationRegistry.ts
|
|
2428
|
-
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
2429
|
-
var activeContext;
|
|
2430
|
-
function readStoredApplicationContext() {
|
|
2431
|
-
if (activeContext) {
|
|
2432
|
-
return activeContext;
|
|
2433
|
-
}
|
|
2434
|
-
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
2435
|
-
if (globalContext) {
|
|
2436
|
-
activeContext = globalContext;
|
|
2437
|
-
}
|
|
2438
|
-
return activeContext;
|
|
2439
|
-
}
|
|
2440
|
-
function requireActiveApplicationContext() {
|
|
2441
|
-
const context = readStoredApplicationContext();
|
|
2442
|
-
if (!context) {
|
|
2443
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
2444
|
-
}
|
|
2445
|
-
return context;
|
|
2446
|
-
}
|
|
2447
|
-
function resolveApplicationCache() {
|
|
2448
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
2449
|
-
}
|
|
2450
|
-
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
2451
|
-
function registerDefaultJobs() {
|
|
2452
|
-
jobRegistry.register("cache.invalidate-tags", () => {
|
|
2453
|
-
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
2454
|
-
});
|
|
2455
|
-
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
2174
|
// ../../src/core/queue/createAppQueue.ts
|
|
2459
2175
|
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
2460
2176
|
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
package/dist/entries/view.js
CHANGED
|
@@ -45,6 +45,11 @@ function isHtmxRequest(request) {
|
|
|
45
45
|
return request.headers.get("HX-Request") === "true";
|
|
46
46
|
}
|
|
47
47
|
// ../../src/core/contracts/serviceTokens.ts
|
|
48
|
+
var CORE_CONFIG_TOKEN = "core.config";
|
|
49
|
+
var CORE_CACHE_TOKEN = "core.cache";
|
|
50
|
+
var CORE_QUEUE_TOKEN = "core.queue";
|
|
51
|
+
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
52
|
+
var CORE_AUTH_TOKEN = "core.auth";
|
|
48
53
|
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
49
54
|
// ../../src/bootstrap/config.ts
|
|
50
55
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
@@ -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";
|
package/dist/index.js
CHANGED
|
@@ -3311,7 +3311,8 @@ function resolveApplicationLogger() {
|
|
|
3311
3311
|
function resolveApplicationDependencies() {
|
|
3312
3312
|
return requireActiveApplicationContext().dependencies;
|
|
3313
3313
|
}
|
|
3314
|
-
|
|
3314
|
+
|
|
3315
|
+
// ../../src/core/auth/resolveMembershipService.ts
|
|
3315
3316
|
function resolveMembershipService() {
|
|
3316
3317
|
const dependencies = resolveApplicationDependencies();
|
|
3317
3318
|
if (dependencies.container.has("core.membership")) {
|
|
@@ -3922,23 +3923,6 @@ function createCacheStore(options) {
|
|
|
3922
3923
|
}
|
|
3923
3924
|
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
3924
3925
|
}
|
|
3925
|
-
// ../../src/bootstrap/discoverModules.ts
|
|
3926
|
-
var appModules = [];
|
|
3927
|
-
function discoverModules() {
|
|
3928
|
-
return appModules;
|
|
3929
|
-
}
|
|
3930
|
-
|
|
3931
|
-
// ../../src/bootstrap/cache/modelCacheTags.ts
|
|
3932
|
-
function cacheTagsForModelWrite(tableName, action) {
|
|
3933
|
-
const module = discoverModules().find((entry) => entry.tableName === tableName);
|
|
3934
|
-
const baseTags = module?.cacheTags ?? [`${tableName}s`];
|
|
3935
|
-
const isDelete = action === "deleted" || action === "force-deleted";
|
|
3936
|
-
const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
|
|
3937
|
-
return [...new Set([...baseTags, ...extraTags])];
|
|
3938
|
-
}
|
|
3939
|
-
function discoverModelTableNames() {
|
|
3940
|
-
return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
|
|
3941
|
-
}
|
|
3942
3926
|
// ../../src/core/cache/taggedCache.ts
|
|
3943
3927
|
class TaggedCache {
|
|
3944
3928
|
store;
|
|
@@ -5307,7 +5291,7 @@ function bindRouteModel(param, resolver, handler) {
|
|
|
5307
5291
|
return await handler(request, model);
|
|
5308
5292
|
};
|
|
5309
5293
|
}
|
|
5310
|
-
// ../../src/
|
|
5294
|
+
// ../../src/core/http/securedRouteModelBinding.ts
|
|
5311
5295
|
function isMutatingPolicyAction(action) {
|
|
5312
5296
|
return action === "update" || action === "delete";
|
|
5313
5297
|
}
|
|
@@ -6121,7 +6105,6 @@ var failedJobService_default = FailedJobService;
|
|
|
6121
6105
|
|
|
6122
6106
|
// ../../src/core/queue/jobRegistry.ts
|
|
6123
6107
|
class JobRegistry {
|
|
6124
|
-
constructor() {}
|
|
6125
6108
|
factories = new Map;
|
|
6126
6109
|
instances = new WeakMap;
|
|
6127
6110
|
register(name, factory) {
|
|
@@ -6145,7 +6128,17 @@ class JobRegistry {
|
|
|
6145
6128
|
return [...this.factories.keys()];
|
|
6146
6129
|
}
|
|
6147
6130
|
}
|
|
6148
|
-
var
|
|
6131
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
6132
|
+
function readSharedJobRegistry() {
|
|
6133
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
6134
|
+
if (globalRegistry) {
|
|
6135
|
+
return globalRegistry;
|
|
6136
|
+
}
|
|
6137
|
+
const registry = new JobRegistry;
|
|
6138
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
6139
|
+
return registry;
|
|
6140
|
+
}
|
|
6141
|
+
var jobRegistry = readSharedJobRegistry();
|
|
6149
6142
|
|
|
6150
6143
|
// ../../src/config/queue.ts
|
|
6151
6144
|
var queueConfig = {
|
|
@@ -6360,16 +6353,6 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
|
6360
6353
|
}
|
|
6361
6354
|
// ../../src/core/queue/queueMetrics.ts
|
|
6362
6355
|
var {RedisClient: RedisClient6 } = globalThis.Bun;
|
|
6363
|
-
|
|
6364
|
-
// ../../src/core/security/safeUrl.ts
|
|
6365
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
6366
|
-
"localhost",
|
|
6367
|
-
"127.0.0.1",
|
|
6368
|
-
"0.0.0.0",
|
|
6369
|
-
"::1",
|
|
6370
|
-
"metadata.google.internal"
|
|
6371
|
-
]);
|
|
6372
|
-
// ../../src/core/queue/queueMetrics.ts
|
|
6373
6356
|
async function readRedisQueueDepth(redisUrl) {
|
|
6374
6357
|
const client = new RedisClient6(redisUrl);
|
|
6375
6358
|
const [high, defaultQueue, low] = await Promise.all([
|
|
@@ -6823,7 +6806,6 @@ export {
|
|
|
6823
6806
|
etagFromResource,
|
|
6824
6807
|
emptyPaginateResult,
|
|
6825
6808
|
emailRule,
|
|
6826
|
-
discoverModelTableNames,
|
|
6827
6809
|
dehydrateValue,
|
|
6828
6810
|
defineTable,
|
|
6829
6811
|
currentTraceId,
|
|
@@ -6873,7 +6855,6 @@ export {
|
|
|
6873
6855
|
composeMiddleware,
|
|
6874
6856
|
compileBlueprint,
|
|
6875
6857
|
collectQueueMetrics,
|
|
6876
|
-
cacheTagsForModelWrite,
|
|
6877
6858
|
cache,
|
|
6878
6859
|
buildSmtpPayload,
|
|
6879
6860
|
buildRequestCacheKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.24",
|
|
4
4
|
"description": "Strata — Laravel-inspired Bun framework public API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -95,6 +95,11 @@
|
|
|
95
95
|
"import": "./dist/entries/cache/createCacheStore.js",
|
|
96
96
|
"default": "./dist/entries/cache/createCacheStore.js"
|
|
97
97
|
},
|
|
98
|
+
"./contracts/serviceTokens": {
|
|
99
|
+
"types": "./dist/core/contracts/serviceTokens.d.ts",
|
|
100
|
+
"import": "./dist/entries/contracts/serviceTokens.js",
|
|
101
|
+
"default": "./dist/entries/contracts/serviceTokens.js"
|
|
102
|
+
},
|
|
98
103
|
"./crypto/fieldEncryption": {
|
|
99
104
|
"types": "./dist/core/crypto/fieldEncryption.d.ts",
|
|
100
105
|
"import": "./dist/entries/crypto/fieldEncryption.js",
|
|
@@ -322,7 +327,7 @@
|
|
|
322
327
|
"build:bundle": "bun build index.ts --outdir dist --target bun --external bun --external eta",
|
|
323
328
|
"build:types": "tsc -p tsconfig.types.json",
|
|
324
329
|
"prepublishOnly": "bun run build && bun ../../scripts/prepare-core-package-publish.ts",
|
|
325
|
-
"build:subpaths": "bun build entries/auth/oauth/oidcProvider.ts entries/auth/oauth/providers.ts entries/auth/oauth/samlProvider.ts entries/auth/oauth/types.ts entries/auth/password.ts entries/auth/sessionCookie.ts entries/auth/tokenHash.ts entries/cache/tags.ts entries/cache/createCacheStore.ts entries/crypto/fieldEncryption.ts entries/crypto/mfaSecret.ts entries/database/factory.ts entries/database/seeders.ts entries/database/types.ts entries/errors/http.ts entries/http/contentNegotiation.ts entries/http/csrfToken.ts entries/http/etag.ts entries/http/flashSession.ts entries/http/parseFormBody.ts entries/http/resources.ts entries/http/webErrorResponse.ts entries/http/webFormRequest.ts entries/jobs/dispatchWebhookJob.ts entries/lifecycle/gracefulShutdown.ts entries/metrics/prometheus.ts entries/pagination.ts entries/queue/createAppQueue.ts entries/queue/failedJobService.ts entries/queue/jobRegistry.ts entries/queue/jobRunner.ts entries/queue/queueMetrics.ts entries/queue/publicQueue.ts entries/queue/types.ts entries/security/oauthState.ts entries/security/publicReads.ts entries/security/safeUrl.ts entries/security/stripeWebhook.ts entries/security/tokenExpiry.ts entries/security/totp.ts entries/storage/storage.ts entries/validation/rules.ts entries/view.ts --outdir dist --root . --target bun --external bun --external eta",
|
|
330
|
+
"build:subpaths": "bun build entries/auth/oauth/oidcProvider.ts entries/auth/oauth/providers.ts entries/auth/oauth/samlProvider.ts entries/auth/oauth/types.ts entries/auth/password.ts entries/auth/sessionCookie.ts entries/auth/tokenHash.ts entries/cache/tags.ts entries/cache/createCacheStore.ts entries/contracts/serviceTokens.ts entries/crypto/fieldEncryption.ts entries/crypto/mfaSecret.ts entries/database/factory.ts entries/database/seeders.ts entries/database/types.ts entries/errors/http.ts entries/http/contentNegotiation.ts entries/http/csrfToken.ts entries/http/etag.ts entries/http/flashSession.ts entries/http/parseFormBody.ts entries/http/resources.ts entries/http/webErrorResponse.ts entries/http/webFormRequest.ts entries/jobs/dispatchWebhookJob.ts entries/lifecycle/gracefulShutdown.ts entries/metrics/prometheus.ts entries/pagination.ts entries/queue/createAppQueue.ts entries/queue/failedJobService.ts entries/queue/jobRegistry.ts entries/queue/jobRunner.ts entries/queue/queueMetrics.ts entries/queue/publicQueue.ts entries/queue/types.ts entries/security/oauthState.ts entries/security/publicReads.ts entries/security/safeUrl.ts entries/security/stripeWebhook.ts entries/security/tokenExpiry.ts entries/security/totp.ts entries/storage/storage.ts entries/validation/rules.ts entries/view.ts --outdir dist --root . --target bun --external bun --external eta",
|
|
326
331
|
"build:shims": "bun ../../scripts/write-core-shared-shims.ts"
|
|
327
332
|
},
|
|
328
333
|
"publishConfig": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { resolveApplicationAuth, resolveApplicationCache, resolveApplicationConfig, resolveApplicationDependencies, resolveApplicationLogger, resolveApplicationPolicyGate, resolveApplicationQueue, setActiveApplicationContext, } from "../core/runtime/applicationRegistry.ts";
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Policy } from "../../core/auth/policy";
|
|
2
|
-
import type { RouteRequest } from "../../core/http/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 };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { cacheTagsForModelWrite, discoverModelTableNames, } from "../../bootstrap/cache/modelCacheTags.ts";
|