@getstrata/core 0.5.36 → 0.5.38
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/jobs/dispatchWebhookJob.d.ts +1 -0
- package/dist/core/jobs/invalidateCacheTagsJob.d.ts +1 -0
- package/dist/entries/http/cookies.js +32 -0
- package/dist/entries/http/csrfProtection.js +23 -0
- package/dist/entries/http/csrfToken.js +3 -0
- package/dist/entries/http/securedRouteModelBinding.js +354 -0
- package/dist/entries/http/webErrorResponse.js +3 -0
- package/dist/entries/http/webFormRequest.js +7 -0
- package/dist/entries/jobs/dispatchWebhookJob.js +65 -0
- package/dist/entries/jobs/invalidateCacheTagsJob.js +15 -0
- package/dist/entries/view.js +3 -0
- package/package.json +17 -2
|
@@ -9,5 +9,6 @@ declare class DispatchWebhookJob extends Job<DispatchWebhookPayload> {
|
|
|
9
9
|
readonly backoffMs = 2000;
|
|
10
10
|
handle(payload: DispatchWebhookPayload): Promise<void>;
|
|
11
11
|
}
|
|
12
|
+
export { DispatchWebhookJob };
|
|
12
13
|
export default DispatchWebhookJob;
|
|
13
14
|
export type { DispatchWebhookPayload };
|
|
@@ -8,5 +8,6 @@ declare class InvalidateCacheTagsJob extends Job<InvalidateCacheTagsPayload> {
|
|
|
8
8
|
constructor(cache: CacheLike);
|
|
9
9
|
handle(payload: InvalidateCacheTagsPayload): Promise<void>;
|
|
10
10
|
}
|
|
11
|
+
export { InvalidateCacheTagsJob };
|
|
11
12
|
export default InvalidateCacheTagsJob;
|
|
12
13
|
export type { InvalidateCacheTagsPayload };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/http/cookies.ts
|
|
3
|
+
function readRequestCookie(request, name) {
|
|
4
|
+
const cookies = request.cookies;
|
|
5
|
+
if (cookies && typeof cookies.get === "function") {
|
|
6
|
+
const value = cookies.get(name);
|
|
7
|
+
if (value) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const header = request.headers.get("cookie");
|
|
12
|
+
if (!header) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
for (const part of header.split(";")) {
|
|
16
|
+
const idx = part.indexOf("=");
|
|
17
|
+
if (idx === -1)
|
|
18
|
+
continue;
|
|
19
|
+
const cookieName = part.slice(0, idx).trim();
|
|
20
|
+
if (cookieName !== name)
|
|
21
|
+
continue;
|
|
22
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function readBunRequestCookie(request, name) {
|
|
27
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
readRequestCookie,
|
|
31
|
+
readBunRequestCookie
|
|
32
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/http/csrfProtection.ts
|
|
3
|
+
var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
|
|
4
|
+
function createCsrfProtection(secret, options = {}) {
|
|
5
|
+
const expiresIn = options.expiresIn ?? DEFAULT_CSRF_TTL_MS;
|
|
6
|
+
const maxAge = options.maxAge ?? expiresIn;
|
|
7
|
+
return {
|
|
8
|
+
generate(_sessionKey) {
|
|
9
|
+
return Bun.CSRF.generate(secret, { expiresIn });
|
|
10
|
+
},
|
|
11
|
+
verify(token, _sessionKey) {
|
|
12
|
+
if (!token) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return Bun.CSRF.verify(token, { secret, maxAge });
|
|
16
|
+
},
|
|
17
|
+
secret
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
createCsrfProtection,
|
|
22
|
+
DEFAULT_CSRF_TTL_MS
|
|
23
|
+
};
|
|
@@ -26,6 +26,9 @@ function readRequestCookie(request, name) {
|
|
|
26
26
|
}
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
|
+
function readBunRequestCookie(request, name) {
|
|
30
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
31
|
+
}
|
|
29
32
|
|
|
30
33
|
// ../../src/core/runtime/asyncContextStore.ts
|
|
31
34
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
3
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
4
|
+
function createAsyncContextStore(key) {
|
|
5
|
+
const symbol = Symbol.for(key);
|
|
6
|
+
const globalRecord = globalThis;
|
|
7
|
+
const existing = globalRecord[symbol];
|
|
8
|
+
if (existing) {
|
|
9
|
+
return existing;
|
|
10
|
+
}
|
|
11
|
+
const store = new AsyncLocalStorage;
|
|
12
|
+
globalRecord[symbol] = store;
|
|
13
|
+
return store;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ../../src/core/auth/authContext.ts
|
|
17
|
+
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
18
|
+
function currentAuthUser() {
|
|
19
|
+
return authContext.getStore() ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ../../src/core/errors/http.ts
|
|
23
|
+
class HttpError extends Error {
|
|
24
|
+
status;
|
|
25
|
+
details;
|
|
26
|
+
constructor(status, message, details) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = new.target.name;
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.details = details;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class BadRequestError extends HttpError {
|
|
35
|
+
constructor(message = "Bad Request", details) {
|
|
36
|
+
super(400, message, details);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class NotFoundError extends HttpError {
|
|
41
|
+
constructor(message = "Not Found", details) {
|
|
42
|
+
super(404, message, details);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class ConflictError extends HttpError {
|
|
47
|
+
constructor(message = "Conflict", details) {
|
|
48
|
+
super(409, message, details);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class UnprocessableEntityError extends HttpError {
|
|
53
|
+
constructor(message = "Unprocessable Entity", details) {
|
|
54
|
+
super(422, message, details);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class ValidationError extends HttpError {
|
|
59
|
+
constructor(message = "Validation failed", details) {
|
|
60
|
+
super(422, message, details);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class ForbiddenError extends HttpError {
|
|
65
|
+
constructor(message = "Forbidden", details) {
|
|
66
|
+
super(403, message, details);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class UnauthorizedError extends HttpError {
|
|
71
|
+
constructor(message = "Unauthorized", details) {
|
|
72
|
+
super(401, message, details);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class PayloadTooLargeError extends HttpError {
|
|
77
|
+
constructor(message = "Payload Too Large", details) {
|
|
78
|
+
super(413, message, details);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class PreconditionFailedError extends HttpError {
|
|
83
|
+
constructor(message = "Precondition Failed", details) {
|
|
84
|
+
super(412, message, details);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ../../src/core/contracts/di.ts
|
|
89
|
+
var requiredDependencyKeys = [
|
|
90
|
+
"container",
|
|
91
|
+
"cache",
|
|
92
|
+
"storage"
|
|
93
|
+
];
|
|
94
|
+
function getRequiredDependency(dependencies, key) {
|
|
95
|
+
const dependency = dependencies[key];
|
|
96
|
+
if (dependency === undefined) {
|
|
97
|
+
throw new Error(`Required dependency "${key}" is not registered.`);
|
|
98
|
+
}
|
|
99
|
+
return dependency;
|
|
100
|
+
}
|
|
101
|
+
function assertAppDependenciesComplete(dependencies) {
|
|
102
|
+
for (const key of requiredDependencyKeys) {
|
|
103
|
+
getRequiredDependency(dependencies, key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function resolveService(dependencies, token) {
|
|
107
|
+
return dependencies.container.resolve(token);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ../../src/core/contracts/serviceTokens.ts
|
|
111
|
+
var CORE_CONFIG_TOKEN = "core.config";
|
|
112
|
+
var CORE_CACHE_TOKEN = "core.cache";
|
|
113
|
+
var CORE_QUEUE_TOKEN = "core.queue";
|
|
114
|
+
var CORE_EVENT_BUS_TOKEN = "core.eventBus";
|
|
115
|
+
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
116
|
+
var CORE_AUTH_TOKEN = "core.auth";
|
|
117
|
+
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
118
|
+
|
|
119
|
+
// ../../src/core/logging/logger.ts
|
|
120
|
+
class Logger {
|
|
121
|
+
channel;
|
|
122
|
+
constructor(channel = "app") {
|
|
123
|
+
this.channel = channel;
|
|
124
|
+
}
|
|
125
|
+
write(level, message, context = {}) {
|
|
126
|
+
const entry = {
|
|
127
|
+
level,
|
|
128
|
+
channel: this.channel,
|
|
129
|
+
message,
|
|
130
|
+
timestamp: new Date().toISOString(),
|
|
131
|
+
...context
|
|
132
|
+
};
|
|
133
|
+
const line = JSON.stringify(entry);
|
|
134
|
+
if (level === "error") {
|
|
135
|
+
console.error(line);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
console.log(line);
|
|
139
|
+
}
|
|
140
|
+
debug(message, context) {
|
|
141
|
+
this.write("debug", message, context);
|
|
142
|
+
}
|
|
143
|
+
info(message, context) {
|
|
144
|
+
this.write("info", message, context);
|
|
145
|
+
}
|
|
146
|
+
warn(message, context) {
|
|
147
|
+
this.write("warn", message, context);
|
|
148
|
+
}
|
|
149
|
+
error(message, context) {
|
|
150
|
+
this.write("error", message, context);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
var appLogger = new Logger("app");
|
|
154
|
+
|
|
155
|
+
// ../../src/core/runtime/applicationRegistry.ts
|
|
156
|
+
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
157
|
+
var activeContext;
|
|
158
|
+
function readStoredApplicationContext() {
|
|
159
|
+
if (activeContext) {
|
|
160
|
+
return activeContext;
|
|
161
|
+
}
|
|
162
|
+
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
163
|
+
if (globalContext) {
|
|
164
|
+
activeContext = globalContext;
|
|
165
|
+
}
|
|
166
|
+
return activeContext;
|
|
167
|
+
}
|
|
168
|
+
function requireActiveApplicationContext() {
|
|
169
|
+
const context = readStoredApplicationContext();
|
|
170
|
+
if (!context) {
|
|
171
|
+
throw new Error("The application context has not been bootstrapped.");
|
|
172
|
+
}
|
|
173
|
+
return context;
|
|
174
|
+
}
|
|
175
|
+
function resolveApplicationAuth() {
|
|
176
|
+
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
177
|
+
}
|
|
178
|
+
function resolveApplicationPolicyGate() {
|
|
179
|
+
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
183
|
+
function nonCryptographicDigest(input) {
|
|
184
|
+
return Bun.hash(input).toString(16);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ../../src/core/http/etag.ts
|
|
188
|
+
function isEtagEnabled() {
|
|
189
|
+
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
190
|
+
}
|
|
191
|
+
function formatWeakEtag(digest) {
|
|
192
|
+
return `W/"${digest}"`;
|
|
193
|
+
}
|
|
194
|
+
function computeEtagFromJson(data) {
|
|
195
|
+
const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
|
|
196
|
+
return formatWeakEtag(digest);
|
|
197
|
+
}
|
|
198
|
+
function etagFromResource(resource) {
|
|
199
|
+
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
200
|
+
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
201
|
+
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
202
|
+
return formatWeakEtag(digest);
|
|
203
|
+
}
|
|
204
|
+
function normalizeEtag(value) {
|
|
205
|
+
return value.trim();
|
|
206
|
+
}
|
|
207
|
+
function etagValuesMatch(left, right) {
|
|
208
|
+
return normalizeEtag(left) === normalizeEtag(right);
|
|
209
|
+
}
|
|
210
|
+
function parseEtagList(header) {
|
|
211
|
+
if (!header) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
215
|
+
}
|
|
216
|
+
function ifNoneMatchSatisfied(request, etag) {
|
|
217
|
+
const header = request.headers.get("if-none-match");
|
|
218
|
+
if (!header) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
if (header.trim() === "*") {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
225
|
+
}
|
|
226
|
+
function ifMatchSatisfied(request, etag) {
|
|
227
|
+
const header = request.headers.get("if-match");
|
|
228
|
+
if (!header) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
if (header.trim() === "*") {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
235
|
+
}
|
|
236
|
+
function assertIfMatch(request, etag, options = {}) {
|
|
237
|
+
const header = request.headers.get("if-match");
|
|
238
|
+
if (!header) {
|
|
239
|
+
if (options.required) {
|
|
240
|
+
throw new PreconditionFailedError("If-Match header is required.");
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!ifMatchSatisfied(request, etag)) {
|
|
245
|
+
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function applyEtagHeaders(headers, etag) {
|
|
249
|
+
const next = new Headers(headers);
|
|
250
|
+
next.set("ETag", etag);
|
|
251
|
+
next.set("Cache-Control", "private, must-revalidate");
|
|
252
|
+
next.append("Vary", "Authorization");
|
|
253
|
+
next.append("Vary", "X-Tenant-Id");
|
|
254
|
+
return next;
|
|
255
|
+
}
|
|
256
|
+
function notModifiedResponse(etag) {
|
|
257
|
+
return new Response(null, {
|
|
258
|
+
status: 304,
|
|
259
|
+
headers: applyEtagHeaders(new Headers, etag)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function applyConditionalGet(request, response, etag) {
|
|
263
|
+
if (!isEtagEnabled()) {
|
|
264
|
+
return response;
|
|
265
|
+
}
|
|
266
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
267
|
+
return notModifiedResponse(etag);
|
|
268
|
+
}
|
|
269
|
+
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
270
|
+
return new Response(response.body, {
|
|
271
|
+
status: response.status,
|
|
272
|
+
statusText: response.statusText,
|
|
273
|
+
headers
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
278
|
+
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
279
|
+
function currentTenant() {
|
|
280
|
+
return tenantContext.getStore() ?? null;
|
|
281
|
+
}
|
|
282
|
+
function currentTenantId() {
|
|
283
|
+
return currentTenant()?.id ?? 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ../../src/core/http/validation.ts
|
|
287
|
+
async function parseJsonBody(request, validator) {
|
|
288
|
+
let payload;
|
|
289
|
+
try {
|
|
290
|
+
payload = await request.json();
|
|
291
|
+
} catch {
|
|
292
|
+
throw new BadRequestError("Request body must be valid JSON.");
|
|
293
|
+
}
|
|
294
|
+
return validator(payload);
|
|
295
|
+
}
|
|
296
|
+
function parsePositiveIntParam(value, name = "id") {
|
|
297
|
+
const parsed = Number.parseInt(value, 10);
|
|
298
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
299
|
+
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
300
|
+
}
|
|
301
|
+
return parsed;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ../../src/core/http/securedRouteModelBinding.ts
|
|
305
|
+
function isMutatingPolicyAction(action) {
|
|
306
|
+
return action === "update" || action === "delete";
|
|
307
|
+
}
|
|
308
|
+
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
309
|
+
return async (request) => {
|
|
310
|
+
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
311
|
+
const model = await resolver(id, request);
|
|
312
|
+
const gate = resolveApplicationPolicyGate();
|
|
313
|
+
const auth = resolveApplicationAuth();
|
|
314
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
315
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
316
|
+
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
317
|
+
assertIfMatch(request, etagFromResource(model), {
|
|
318
|
+
required: authorization.requireIfMatch ?? true
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const response = await handler(request, model);
|
|
322
|
+
if (isEtagEnabled() && authorization.action === "view") {
|
|
323
|
+
return applyConditionalGet(request, response, etagFromResource(model));
|
|
324
|
+
}
|
|
325
|
+
return response;
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
329
|
+
return async (request) => {
|
|
330
|
+
const key = String(request.params[param] ?? "").trim();
|
|
331
|
+
if (!key) {
|
|
332
|
+
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
333
|
+
}
|
|
334
|
+
const model = await resolver(key, request);
|
|
335
|
+
const gate = resolveApplicationPolicyGate();
|
|
336
|
+
const auth = resolveApplicationAuth();
|
|
337
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
338
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
339
|
+
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
340
|
+
assertIfMatch(request, etagFromResource(model), {
|
|
341
|
+
required: authorization.requireIfMatch ?? true
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const response = await handler(request, model);
|
|
345
|
+
if (isEtagEnabled() && authorization.action === "view") {
|
|
346
|
+
return applyConditionalGet(request, response, etagFromResource(model));
|
|
347
|
+
}
|
|
348
|
+
return response;
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
export {
|
|
352
|
+
securedBindRouteModelByKey,
|
|
353
|
+
securedBindRouteModel
|
|
354
|
+
};
|
|
@@ -2569,6 +2569,9 @@ function readRequestCookie(request, name) {
|
|
|
2569
2569
|
}
|
|
2570
2570
|
return null;
|
|
2571
2571
|
}
|
|
2572
|
+
function readBunRequestCookie(request, name) {
|
|
2573
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
2574
|
+
}
|
|
2572
2575
|
|
|
2573
2576
|
// ../../src/core/http/csrfToken.ts
|
|
2574
2577
|
var CSRF_COOKIE = "workhub_csrf";
|
|
@@ -145,6 +145,13 @@ async function parseJsonBody(request, validator) {
|
|
|
145
145
|
}
|
|
146
146
|
return validator(payload);
|
|
147
147
|
}
|
|
148
|
+
function parsePositiveIntParam(value, name = "id") {
|
|
149
|
+
const parsed = Number.parseInt(value, 10);
|
|
150
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
151
|
+
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
152
|
+
}
|
|
153
|
+
return parsed;
|
|
154
|
+
}
|
|
148
155
|
|
|
149
156
|
// ../../src/core/http/webFormRequest.ts
|
|
150
157
|
class WebFormRequest {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3
|
+
import { createHmac } from "crypto";
|
|
4
|
+
|
|
2
5
|
// ../../src/config/app.ts
|
|
3
6
|
var appConfig = {
|
|
4
7
|
name: "WorkHub",
|
|
@@ -316,3 +319,65 @@ async function safeFetch(input, init = {}, options = {}) {
|
|
|
316
319
|
clearTimeout(timeout);
|
|
317
320
|
}
|
|
318
321
|
}
|
|
322
|
+
|
|
323
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
324
|
+
class DispatchWebhookJob extends Job {
|
|
325
|
+
maxAttempts = 3;
|
|
326
|
+
backoffMs = 2000;
|
|
327
|
+
async handle(payload) {
|
|
328
|
+
const rows = await repositoryConnection`
|
|
329
|
+
SELECT id, url, secret
|
|
330
|
+
FROM webhook
|
|
331
|
+
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
332
|
+
LIMIT 1
|
|
333
|
+
`;
|
|
334
|
+
const webhook = rows[0];
|
|
335
|
+
if (!webhook) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
339
|
+
const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
|
|
340
|
+
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
341
|
+
let responseStatus = null;
|
|
342
|
+
let errorMessage = null;
|
|
343
|
+
try {
|
|
344
|
+
const response = await safeFetch(webhook.url, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: {
|
|
347
|
+
"content-type": "application/json",
|
|
348
|
+
"x-workhub-signature": signature
|
|
349
|
+
},
|
|
350
|
+
body
|
|
351
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
352
|
+
responseStatus = response.status;
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
358
|
+
await repositoryConnection`
|
|
359
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
360
|
+
VALUES (
|
|
361
|
+
${webhook.id},
|
|
362
|
+
${payload.event},
|
|
363
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
364
|
+
${responseStatus},
|
|
365
|
+
${errorMessage}
|
|
366
|
+
)
|
|
367
|
+
`;
|
|
368
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
369
|
+
}
|
|
370
|
+
await repositoryConnection`
|
|
371
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
372
|
+
VALUES (
|
|
373
|
+
${webhook.id},
|
|
374
|
+
${payload.event},
|
|
375
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
376
|
+
${responseStatus}
|
|
377
|
+
)
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
export {
|
|
382
|
+
DispatchWebhookJob
|
|
383
|
+
};
|
|
@@ -24,3 +24,18 @@ class AsyncQueue {
|
|
|
24
24
|
function createQueue(driver) {
|
|
25
25
|
return driver === "async" ? new AsyncQueue : new SyncQueue;
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
29
|
+
class InvalidateCacheTagsJob extends Job {
|
|
30
|
+
cache;
|
|
31
|
+
constructor(cache) {
|
|
32
|
+
super();
|
|
33
|
+
this.cache = cache;
|
|
34
|
+
}
|
|
35
|
+
async handle(payload) {
|
|
36
|
+
await this.cache.tags(...payload.tags).flush();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
InvalidateCacheTagsJob
|
|
41
|
+
};
|
package/dist/entries/view.js
CHANGED
|
@@ -2554,6 +2554,9 @@ function readRequestCookie(request, name) {
|
|
|
2554
2554
|
}
|
|
2555
2555
|
return null;
|
|
2556
2556
|
}
|
|
2557
|
+
function readBunRequestCookie(request, name) {
|
|
2558
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
2559
|
+
}
|
|
2557
2560
|
|
|
2558
2561
|
// ../../src/core/http/csrfToken.ts
|
|
2559
2562
|
var CSRF_COOKIE = "workhub_csrf";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.38",
|
|
4
4
|
"description": "Strata — Laravel-inspired Bun framework public API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -170,6 +170,16 @@
|
|
|
170
170
|
"import": "./dist/entries/http/bodySizeLimitMiddleware.js",
|
|
171
171
|
"default": "./dist/entries/http/bodySizeLimitMiddleware.js"
|
|
172
172
|
},
|
|
173
|
+
"./http/cookies": {
|
|
174
|
+
"types": "./dist/core/http/cookies.d.ts",
|
|
175
|
+
"import": "./dist/entries/http/cookies.js",
|
|
176
|
+
"default": "./dist/entries/http/cookies.js"
|
|
177
|
+
},
|
|
178
|
+
"./http/csrfProtection": {
|
|
179
|
+
"types": "./dist/core/http/csrfProtection.d.ts",
|
|
180
|
+
"import": "./dist/entries/http/csrfProtection.js",
|
|
181
|
+
"default": "./dist/entries/http/csrfProtection.js"
|
|
182
|
+
},
|
|
173
183
|
"./http/contentNegotiation": {
|
|
174
184
|
"types": "./dist/core/http/contentNegotiation.d.ts",
|
|
175
185
|
"import": "./dist/entries/http/contentNegotiation.js",
|
|
@@ -205,6 +215,11 @@
|
|
|
205
215
|
"import": "./dist/entries/http/resources.js",
|
|
206
216
|
"default": "./dist/entries/http/resources.js"
|
|
207
217
|
},
|
|
218
|
+
"./http/securedRouteModelBinding": {
|
|
219
|
+
"types": "./dist/core/http/securedRouteModelBinding.d.ts",
|
|
220
|
+
"import": "./dist/entries/http/securedRouteModelBinding.js",
|
|
221
|
+
"default": "./dist/entries/http/securedRouteModelBinding.js"
|
|
222
|
+
},
|
|
208
223
|
"./http/requestMetaContext": {
|
|
209
224
|
"types": "./dist/core/http/requestMetaContext.d.ts",
|
|
210
225
|
"import": "./dist/entries/http/requestMetaContext.js",
|
|
@@ -382,7 +397,7 @@
|
|
|
382
397
|
"build:bundle": "bun build index.ts --outdir dist --target bun --external bun --external eta",
|
|
383
398
|
"build:types": "tsc -p tsconfig.types.json",
|
|
384
399
|
"prepublishOnly": "bun run build && bun ../../scripts/prepare-core-package-publish.ts",
|
|
385
|
-
"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/audit/exportAuditLogs.ts entries/cache/tags.ts entries/cache/createCacheStore.ts entries/config/envSchema.ts entries/contracts/serviceTokens.ts entries/contracts/container.ts entries/contracts/di.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/bodySizeLimitMiddleware.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/jobs/invalidateCacheTagsJob.ts entries/lifecycle/gracefulShutdown.ts entries/logging/logger.ts entries/metrics/prometheus.ts entries/openapi/registeredRoute.ts entries/pagination.ts entries/queue.ts entries/queue/createAppQueue.ts entries/queue/failedJobService.ts entries/queue/jobRunner.ts entries/queue/queueMetrics.ts entries/queue/publicQueue.ts entries/queue/types.ts entries/scheduler/schedule.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",
|
|
400
|
+
"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/audit/exportAuditLogs.ts entries/cache/tags.ts entries/cache/createCacheStore.ts entries/config/envSchema.ts entries/contracts/serviceTokens.ts entries/contracts/container.ts entries/contracts/di.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/bodySizeLimitMiddleware.ts entries/http/cookies.ts entries/http/csrfProtection.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/securedRouteModelBinding.ts entries/http/webErrorResponse.ts entries/http/webFormRequest.ts entries/jobs/dispatchWebhookJob.ts entries/jobs/invalidateCacheTagsJob.ts entries/lifecycle/gracefulShutdown.ts entries/logging/logger.ts entries/metrics/prometheus.ts entries/openapi/registeredRoute.ts entries/pagination.ts entries/queue.ts entries/queue/createAppQueue.ts entries/queue/failedJobService.ts entries/queue/jobRunner.ts entries/queue/queueMetrics.ts entries/queue/publicQueue.ts entries/queue/types.ts entries/scheduler/schedule.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",
|
|
386
401
|
"build:shims": "bun ../../scripts/write-core-shared-shims.ts"
|
|
387
402
|
},
|
|
388
403
|
"publishConfig": {
|