@getstrata/bootstrap 0.2.5 → 0.2.6
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/README.md +3 -1
- package/dist/bootstrap/httpKernel.d.ts +1 -2
- package/dist/bootstrap/middleware.d.ts +1 -1
- package/dist/core/admin/registry.d.ts +1 -0
- package/dist/core/storage/storage.d.ts +6 -3
- package/dist/framework/public-api.d.ts +11 -2
- package/dist/index.js +38 -937
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @getstrata/bootstrap
|
|
2
2
|
|
|
3
|
-
Application bootstrap for Strata sibling apps
|
|
3
|
+
Application bootstrap for Strata sibling apps: HttpKernel, service container, and web utilities.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
} from "@getstrata/bootstrap";
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
`CookieSessionStore` reads optional `is_admin` from the user row for global admin routing via `wrapWebGlobalAdmin`.
|
|
24
|
+
|
|
23
25
|
## HttpKernel (API / module apps)
|
|
24
26
|
|
|
25
27
|
```typescript
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { type Middleware, type RouteHandler } from "../core/http/middleware";
|
|
1
|
+
import { type Middleware, type Policy, type RouteHandler } from "@getstrata/core";
|
|
3
2
|
import type { AppDependencies } from "./contracts";
|
|
4
3
|
type MiddlewareGroupName = "api" | "authenticated" | "web";
|
|
5
4
|
declare class HttpKernel {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Middleware } from "
|
|
1
|
+
import { type Middleware } from "@getstrata/core";
|
|
2
2
|
import type { AppDependencies } from "./contracts";
|
|
3
3
|
declare function createDefaultMiddleware(dependencies: AppDependencies): Middleware[];
|
|
4
4
|
export { createDefaultMiddleware };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AdminResource, AdminResourceDefinition } from "./types.ts";
|
|
2
2
|
declare class AdminResourceRegistry {
|
|
3
3
|
private readonly resources;
|
|
4
|
+
constructor();
|
|
4
5
|
register<TEntity extends object>(resource: AdminResource<TEntity>): void;
|
|
5
6
|
get(name: string): AdminResource<object> | undefined;
|
|
6
7
|
list(): AdminResourceDefinition[];
|
|
@@ -12,8 +12,9 @@ interface S3StorageConfig {
|
|
|
12
12
|
endpoint?: string;
|
|
13
13
|
}
|
|
14
14
|
declare class LocalStorageDriver implements StorageDriver {
|
|
15
|
-
private readonly rootDirectory
|
|
16
|
-
constructor(rootDirectory
|
|
15
|
+
private readonly rootDirectory?;
|
|
16
|
+
constructor(rootDirectory?: string | undefined);
|
|
17
|
+
private resolveRootDirectory;
|
|
17
18
|
private resolvePath;
|
|
18
19
|
put(path: string, contents: string | Uint8Array): Promise<string>;
|
|
19
20
|
get(path: string): Promise<Uint8Array | null>;
|
|
@@ -37,5 +38,7 @@ declare function resolveS3Config(): S3StorageConfig;
|
|
|
37
38
|
declare function createS3Client(config?: S3StorageConfig): S3Client;
|
|
38
39
|
declare function createStorageDriver(): StorageDriver;
|
|
39
40
|
declare function storage(): StorageManager;
|
|
41
|
+
/** Test hook: drop the process-wide storage singleton (e.g. after changing STORAGE_PATH). */
|
|
42
|
+
declare function resetDefaultStorage(): void;
|
|
40
43
|
export type { S3StorageConfig, StorageDriver };
|
|
41
|
-
export { createS3Client, createStorageDriver, LocalStorageDriver, resolveS3Config, S3StorageDriver, StorageManager, storage, };
|
|
44
|
+
export { createS3Client, createStorageDriver, LocalStorageDriver, resetDefaultStorage, resolveS3Config, S3StorageDriver, StorageManager, storage, };
|
|
@@ -10,6 +10,7 @@ export { AdminResourceRegistry, formatAdminValue, } from "../core/admin/index.ts
|
|
|
10
10
|
export type { AbilityChecker } from "../core/auth/abilityChecker.ts";
|
|
11
11
|
export type { AuthUser } from "../core/auth/authContext.ts";
|
|
12
12
|
export { currentAuthUser, runWithAuthUser } from "../core/auth/authContext.ts";
|
|
13
|
+
export { createMembershipMiddleware } from "../core/auth/membershipMiddleware.ts";
|
|
13
14
|
export { Policy, PolicyGate } from "../core/auth/policy.ts";
|
|
14
15
|
export { default as CacheRepository } from "../core/cache/repository.ts";
|
|
15
16
|
export { CACHE_TAGS } from "../core/cache/tags.ts";
|
|
@@ -40,16 +41,20 @@ export { EventBus } from "../core/events/eventBus.ts";
|
|
|
40
41
|
export { auth, cache, config, events, log, mail, policyGate, queue, storage, } from "../core/facades/index.ts";
|
|
41
42
|
export { createBodySizeLimitMiddleware } from "../core/http/bodySizeLimitMiddleware.ts";
|
|
42
43
|
export { readBunRequestCookie, readRequestCookie } from "../core/http/cookies.ts";
|
|
44
|
+
export { createCorsMiddleware } from "../core/http/corsMiddleware.ts";
|
|
43
45
|
export { createCsrfMiddleware } from "../core/http/csrfMiddleware.ts";
|
|
44
46
|
export { createCsrfProtection } from "../core/http/csrfProtection.ts";
|
|
45
47
|
export { createCsrfTokenCookie, readSubmittedCsrfToken, readSubmittedCsrfTokenFromBody, resolveCsrfToken, resolveCsrfTokenForRequest, verifyCsrfToken, } from "../core/http/csrfToken.ts";
|
|
46
48
|
export { assertIfMatch, etagFromResource, isEtagEnabled, } from "../core/http/etag.ts";
|
|
49
|
+
export { createFlashMiddleware } from "../core/http/flashMiddleware.ts";
|
|
47
50
|
export { FormRequest } from "../core/http/formRequest.ts";
|
|
48
|
-
export { applyMiddlewareToRoutes, composeMiddleware, createAuthMiddleware, createAuthorizeMiddleware, createdResponse, createRequireAuthMiddleware, jsonResponse, noContentResponse, paginatedResponse, parsePaginationQuery, securedBindRouteModel, securedBindRouteModelByKey, withErrorHandling, withMiddleware, } from "../core/http/index.ts";
|
|
51
|
+
export { applyMiddlewareToRoutes, composeMiddleware, createAuthMiddleware, createAuthorizeMiddleware, createdResponse, createRequireAuthMiddleware, jsonResponse, noContentResponse, paginatedResponse, parsePaginationQuery, requestIdMiddleware, securedBindRouteModel, securedBindRouteModelByKey, withErrorHandling, withMiddleware, } from "../core/http/index.ts";
|
|
49
52
|
export { createLoginThrottleMiddleware } from "../core/http/loginThrottleMiddleware.ts";
|
|
50
53
|
export { createMemoryThrottleMiddleware } from "../core/http/memoryThrottleMiddleware.ts";
|
|
51
54
|
export { createMetricsMiddleware, normalizeMetricPath } from "../core/http/metricsMiddleware.ts";
|
|
52
55
|
export type { Middleware, RouteHandler } from "../core/http/middleware.ts";
|
|
56
|
+
export { createRequireAbilityMiddleware } from "../core/http/requireAbilityMiddleware.ts";
|
|
57
|
+
export { createRequireGlobalAdminMiddleware } from "../core/http/requireGlobalAdminMiddleware.ts";
|
|
53
58
|
export { createRequireWebAuthMiddleware } from "../core/http/requireWebAuthMiddleware.ts";
|
|
54
59
|
export { serializeDate, toPaginatedResourceCollection, toResourceCollection, } from "../core/http/resources.ts";
|
|
55
60
|
export type { RouteRequest } from "../core/http/route.ts";
|
|
@@ -57,6 +62,7 @@ export { createSecurityHeadersMiddleware } from "../core/http/securityHeadersMid
|
|
|
57
62
|
export { createThrottleMiddleware } from "../core/http/throttleMiddleware.ts";
|
|
58
63
|
export { WebFormRequest } from "../core/http/webFormRequest.ts";
|
|
59
64
|
export { installGracefulShutdownSignals, registerShutdownHandler, runGracefulShutdown, } from "../core/lifecycle/gracefulShutdown.ts";
|
|
65
|
+
export { createRequestLoggingMiddleware } from "../core/logging/requestLoggingMiddleware.ts";
|
|
60
66
|
export type { MailDriver, MailMessage } from "../core/mail/mailer.ts";
|
|
61
67
|
export { buildSmtpPayload, LogMailDriver, Mailer, mailer, } from "../core/mail/mailer.ts";
|
|
62
68
|
export type { MarkdownMailLayoutOptions, RenderedMarkdownMail } from "../core/mail/markdownMail.ts";
|
|
@@ -73,8 +79,11 @@ export { AsyncQueue, createQueue, Job, SyncQueue } from "../core/queue/index.ts"
|
|
|
73
79
|
export { createFailedJobService, createProductionQueue, createQueueWorker, createTrackedJob, FailedJobRepository, FailedJobService, jobRegistry, QueueWorker, RedisQueue, ResilientQueue, runQueueJob, } from "../core/queue/publicQueue.ts";
|
|
74
80
|
export type { ScheduledTask } from "../core/scheduler/schedule.ts";
|
|
75
81
|
export { appSchedule, runDueScheduledTasks, Schedule } from "../core/scheduler/schedule.ts";
|
|
82
|
+
export { isPublicReadsEnabled } from "../core/security/publicReads.ts";
|
|
76
83
|
export type { StorageDriver } from "../core/storage/storage.ts";
|
|
77
|
-
export { LocalStorageDriver, StorageManager } from "../core/storage/storage.ts";
|
|
84
|
+
export { LocalStorageDriver, resetDefaultStorage, StorageManager, } from "../core/storage/storage.ts";
|
|
85
|
+
export { createTenantMiddleware } from "../core/tenant/tenantMiddleware.ts";
|
|
86
|
+
export { createTracingMiddleware } from "../core/tracing/tracingMiddleware.ts";
|
|
78
87
|
export type { ValidationRule, ValidationSchema } from "../core/validation/rules.ts";
|
|
79
88
|
export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
|
|
80
89
|
export { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, htmlResponse, isHtmxRequest, resolveWebLayoutData, } from "../core/view/index.ts";
|
package/dist/index.js
CHANGED
|
@@ -81,9 +81,6 @@ var databaseConfig = {
|
|
|
81
81
|
// ../../src/core/database/connectionContext.ts
|
|
82
82
|
import { AsyncLocalStorage } from "async_hooks";
|
|
83
83
|
var activeConnection = new AsyncLocalStorage;
|
|
84
|
-
function runWithDatabaseConnection(connection, callback) {
|
|
85
|
-
return activeConnection.run(connection, callback);
|
|
86
|
-
}
|
|
87
84
|
function getActiveDatabaseConnection(fallback) {
|
|
88
85
|
return activeConnection.getStore() ?? fallback;
|
|
89
86
|
}
|
|
@@ -207,12 +204,6 @@ class UnauthorizedError extends HttpError {
|
|
|
207
204
|
}
|
|
208
205
|
}
|
|
209
206
|
|
|
210
|
-
class PayloadTooLargeError extends HttpError {
|
|
211
|
-
constructor(message = "Payload Too Large", details) {
|
|
212
|
-
super(413, message, details);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
207
|
// ../../src/core/security/safeUrl.ts
|
|
217
208
|
var BLOCKED_HOSTNAMES = new Set([
|
|
218
209
|
"localhost",
|
|
@@ -836,9 +827,6 @@ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
|
836
827
|
// ../../src/core/auth/authContext.ts
|
|
837
828
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
838
829
|
var authContext = new AsyncLocalStorage2;
|
|
839
|
-
function runWithAuthUser(user, callback) {
|
|
840
|
-
return authContext.run(user, callback);
|
|
841
|
-
}
|
|
842
830
|
function currentAuthUser() {
|
|
843
831
|
return authContext.getStore() ?? null;
|
|
844
832
|
}
|
|
@@ -3729,8 +3717,14 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
|
3729
3717
|
if (tags.length === 0) {
|
|
3730
3718
|
return;
|
|
3731
3719
|
}
|
|
3732
|
-
|
|
3733
|
-
|
|
3720
|
+
let cache;
|
|
3721
|
+
let queue;
|
|
3722
|
+
try {
|
|
3723
|
+
cache = resolveApplicationCache();
|
|
3724
|
+
queue = resolveApplicationQueue();
|
|
3725
|
+
} catch {
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3734
3728
|
const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
|
|
3735
3729
|
await queue.dispatch(job, { tags });
|
|
3736
3730
|
});
|
|
@@ -3840,9 +3834,6 @@ function isViewsEnabled() {
|
|
|
3840
3834
|
// ../../src/core/http/requestMetaContext.ts
|
|
3841
3835
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
3842
3836
|
var requestMetaContext = new AsyncLocalStorage3;
|
|
3843
|
-
function runWithRequestMeta(meta, callback) {
|
|
3844
|
-
return requestMetaContext.run(meta, callback);
|
|
3845
|
-
}
|
|
3846
3837
|
function currentRequestMeta() {
|
|
3847
3838
|
return requestMetaContext.getStore() ?? {
|
|
3848
3839
|
ipAddress: null,
|
|
@@ -3892,9 +3883,6 @@ function htmlResponse(html, init = {}) {
|
|
|
3892
3883
|
}
|
|
3893
3884
|
});
|
|
3894
3885
|
}
|
|
3895
|
-
// ../../src/core/http/csrfToken.ts
|
|
3896
|
-
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3897
|
-
|
|
3898
3886
|
// ../../src/core/http/cookies.ts
|
|
3899
3887
|
function readRequestCookie(request, name) {
|
|
3900
3888
|
const cookies = request.cookies;
|
|
@@ -3929,14 +3917,6 @@ function resolveCsrfSecret() {
|
|
|
3929
3917
|
function csrfVerifyOptions() {
|
|
3930
3918
|
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
3931
3919
|
}
|
|
3932
|
-
function tokensMatch(left, right) {
|
|
3933
|
-
const leftBuffer = Buffer.from(left);
|
|
3934
|
-
const rightBuffer = Buffer.from(right);
|
|
3935
|
-
if (leftBuffer.length !== rightBuffer.length) {
|
|
3936
|
-
return false;
|
|
3937
|
-
}
|
|
3938
|
-
return timingSafeEqual2(leftBuffer, rightBuffer);
|
|
3939
|
-
}
|
|
3940
3920
|
function createCsrfTokenCookie() {
|
|
3941
3921
|
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
3942
3922
|
return {
|
|
@@ -3951,45 +3931,6 @@ function resolveCsrfToken(request) {
|
|
|
3951
3931
|
}
|
|
3952
3932
|
return createCsrfTokenCookie();
|
|
3953
3933
|
}
|
|
3954
|
-
function readSubmittedCsrfToken(request) {
|
|
3955
|
-
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
3956
|
-
if (headerToken) {
|
|
3957
|
-
return headerToken;
|
|
3958
|
-
}
|
|
3959
|
-
return null;
|
|
3960
|
-
}
|
|
3961
|
-
async function readSubmittedCsrfTokenFromBody(request) {
|
|
3962
|
-
const headerToken = readSubmittedCsrfToken(request);
|
|
3963
|
-
if (headerToken) {
|
|
3964
|
-
return headerToken;
|
|
3965
|
-
}
|
|
3966
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
3967
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
3968
|
-
const formData = await request.clone().formData();
|
|
3969
|
-
const field = formData.get("_token");
|
|
3970
|
-
if (typeof field === "string" && field.trim().length > 0) {
|
|
3971
|
-
return field.trim();
|
|
3972
|
-
}
|
|
3973
|
-
const legacyField = formData.get("_csrf");
|
|
3974
|
-
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
3975
|
-
return legacyField.trim();
|
|
3976
|
-
}
|
|
3977
|
-
}
|
|
3978
|
-
return null;
|
|
3979
|
-
}
|
|
3980
|
-
function verifyCsrfToken(request, submittedToken) {
|
|
3981
|
-
if (!submittedToken) {
|
|
3982
|
-
return false;
|
|
3983
|
-
}
|
|
3984
|
-
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
3985
|
-
if (!cookieValue) {
|
|
3986
|
-
return false;
|
|
3987
|
-
}
|
|
3988
|
-
if (!tokensMatch(submittedToken, cookieValue)) {
|
|
3989
|
-
return false;
|
|
3990
|
-
}
|
|
3991
|
-
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
3992
|
-
}
|
|
3993
3934
|
function resolveCsrfTokenForRequest(request) {
|
|
3994
3935
|
const metaToken = currentRequestMeta().csrfToken;
|
|
3995
3936
|
if (metaToken) {
|
|
@@ -3999,7 +3940,7 @@ function resolveCsrfTokenForRequest(request) {
|
|
|
3999
3940
|
}
|
|
4000
3941
|
|
|
4001
3942
|
// ../../src/core/http/flashSession.ts
|
|
4002
|
-
import { createHmac as createHmac3, timingSafeEqual as
|
|
3943
|
+
import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4003
3944
|
var FLASH_COOKIE = "workhub_flash";
|
|
4004
3945
|
var FLASH_TTL_MS = 60 * 1000;
|
|
4005
3946
|
function resolveFlashSecret() {
|
|
@@ -4046,7 +3987,7 @@ function parseFlashCookie(cookieValue) {
|
|
|
4046
3987
|
if (expectedBuffer.length !== actualBuffer.length) {
|
|
4047
3988
|
return null;
|
|
4048
3989
|
}
|
|
4049
|
-
if (!
|
|
3990
|
+
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
4050
3991
|
return null;
|
|
4051
3992
|
}
|
|
4052
3993
|
try {
|
|
@@ -4062,9 +4003,6 @@ function parseFlashCookie(cookieValue) {
|
|
|
4062
4003
|
return null;
|
|
4063
4004
|
}
|
|
4064
4005
|
}
|
|
4065
|
-
function clearFlashCookie() {
|
|
4066
|
-
return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
4067
|
-
}
|
|
4068
4006
|
function pullFlash(request) {
|
|
4069
4007
|
const cookieValue = readFlashCookie(request);
|
|
4070
4008
|
if (!cookieValue) {
|
|
@@ -4072,15 +4010,6 @@ function pullFlash(request) {
|
|
|
4072
4010
|
}
|
|
4073
4011
|
return parseFlashCookie(cookieValue);
|
|
4074
4012
|
}
|
|
4075
|
-
function withFlashClear(response) {
|
|
4076
|
-
const headers = new Headers(response.headers);
|
|
4077
|
-
headers.append("set-cookie", clearFlashCookie());
|
|
4078
|
-
return new Response(response.body, {
|
|
4079
|
-
status: response.status,
|
|
4080
|
-
statusText: response.statusText,
|
|
4081
|
-
headers
|
|
4082
|
-
});
|
|
4083
|
-
}
|
|
4084
4013
|
|
|
4085
4014
|
// ../../src/core/view/webLayoutData.ts
|
|
4086
4015
|
async function resolveWebLayoutData(container, request) {
|
|
@@ -4272,17 +4201,6 @@ function composeMiddleware(...middleware) {
|
|
|
4272
4201
|
};
|
|
4273
4202
|
};
|
|
4274
4203
|
}
|
|
4275
|
-
async function requestIdMiddleware(request, next) {
|
|
4276
|
-
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
4277
|
-
const response = await next();
|
|
4278
|
-
const headers = new Headers(response.headers);
|
|
4279
|
-
headers.set("x-request-id", requestId);
|
|
4280
|
-
return new Response(response.body, {
|
|
4281
|
-
status: response.status,
|
|
4282
|
-
statusText: response.statusText,
|
|
4283
|
-
headers
|
|
4284
|
-
});
|
|
4285
|
-
}
|
|
4286
4204
|
function wrapRouteHandler(handler, middleware) {
|
|
4287
4205
|
if (isMethodRouteMap(handler)) {
|
|
4288
4206
|
const wrapped = {};
|
|
@@ -4304,6 +4222,32 @@ function applyMiddlewareToRoutes(routes, middleware) {
|
|
|
4304
4222
|
return wrapped;
|
|
4305
4223
|
}
|
|
4306
4224
|
|
|
4225
|
+
// ../../src/bootstrap/httpKernel.ts
|
|
4226
|
+
import {
|
|
4227
|
+
createAuthMiddleware,
|
|
4228
|
+
createAuthorizeMiddleware,
|
|
4229
|
+
createBodySizeLimitMiddleware,
|
|
4230
|
+
createCorsMiddleware,
|
|
4231
|
+
createCsrfMiddleware,
|
|
4232
|
+
createFlashMiddleware,
|
|
4233
|
+
createLoginThrottleMiddleware,
|
|
4234
|
+
createMembershipMiddleware,
|
|
4235
|
+
createMemoryThrottleMiddleware,
|
|
4236
|
+
createMetricsMiddleware,
|
|
4237
|
+
createRequestLoggingMiddleware,
|
|
4238
|
+
createRequireAbilityMiddleware,
|
|
4239
|
+
createRequireAuthMiddleware,
|
|
4240
|
+
createRequireGlobalAdminMiddleware,
|
|
4241
|
+
createRequireWebAuthMiddleware,
|
|
4242
|
+
createSecurityHeadersMiddleware,
|
|
4243
|
+
createTenantMiddleware,
|
|
4244
|
+
createThrottleMiddleware,
|
|
4245
|
+
createTracingMiddleware,
|
|
4246
|
+
isPublicReadsEnabled,
|
|
4247
|
+
requestIdMiddleware,
|
|
4248
|
+
withMiddleware
|
|
4249
|
+
} from "@getstrata/core";
|
|
4250
|
+
|
|
4307
4251
|
// ../../src/config/rateLimit.ts
|
|
4308
4252
|
var LOCAL_LOGIN_RATE_LIMIT = {
|
|
4309
4253
|
maxAttempts: 100,
|
|
@@ -4338,849 +4282,6 @@ function resolveRegisterRateLimit() {
|
|
|
4338
4282
|
};
|
|
4339
4283
|
}
|
|
4340
4284
|
|
|
4341
|
-
// ../../src/core/auth/membershipContext.ts
|
|
4342
|
-
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
4343
|
-
|
|
4344
|
-
// ../../src/modules/organization/memberRepository.ts
|
|
4345
|
-
class OrganizationMemberRepository {
|
|
4346
|
-
constructor() {}
|
|
4347
|
-
async findMembership(userId, organizationId) {
|
|
4348
|
-
const rows = await connection_default`
|
|
4349
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
4350
|
-
FROM organization_member
|
|
4351
|
-
WHERE user_id = ${userId} AND organization_id = ${organizationId}
|
|
4352
|
-
LIMIT 1
|
|
4353
|
-
`;
|
|
4354
|
-
return rows[0] ?? null;
|
|
4355
|
-
}
|
|
4356
|
-
async listForUser(userId) {
|
|
4357
|
-
return await connection_default`
|
|
4358
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
4359
|
-
FROM organization_member
|
|
4360
|
-
WHERE user_id = ${userId}
|
|
4361
|
-
ORDER BY organization_id
|
|
4362
|
-
`;
|
|
4363
|
-
}
|
|
4364
|
-
async listForOrganization(organizationId) {
|
|
4365
|
-
return await connection_default`
|
|
4366
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
4367
|
-
FROM organization_member
|
|
4368
|
-
WHERE organization_id = ${organizationId}
|
|
4369
|
-
ORDER BY id
|
|
4370
|
-
`;
|
|
4371
|
-
}
|
|
4372
|
-
async addMember(input) {
|
|
4373
|
-
const rows = await connection_default`
|
|
4374
|
-
INSERT INTO organization_member (organization_id, user_id, role)
|
|
4375
|
-
VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
|
|
4376
|
-
RETURNING id, organization_id, user_id, role, created_at
|
|
4377
|
-
`;
|
|
4378
|
-
const row = rows[0];
|
|
4379
|
-
if (!row) {
|
|
4380
|
-
throw new Error("Organization member insert did not return a row.");
|
|
4381
|
-
}
|
|
4382
|
-
return row;
|
|
4383
|
-
}
|
|
4384
|
-
async removeMember(organizationId, userId) {
|
|
4385
|
-
const rows = await connection_default`
|
|
4386
|
-
DELETE FROM organization_member
|
|
4387
|
-
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
4388
|
-
RETURNING id
|
|
4389
|
-
`;
|
|
4390
|
-
return rows.length > 0;
|
|
4391
|
-
}
|
|
4392
|
-
}
|
|
4393
|
-
var memberRepository_default = OrganizationMemberRepository;
|
|
4394
|
-
|
|
4395
|
-
// ../../src/core/auth/accessControl.ts
|
|
4396
|
-
function isGlobalAdmin(user) {
|
|
4397
|
-
return user?.role === "admin";
|
|
4398
|
-
}
|
|
4399
|
-
function resolveUserId(user) {
|
|
4400
|
-
const userId = typeof user.id === "number" ? user.id : Number(user.id);
|
|
4401
|
-
if (!Number.isInteger(userId) || userId <= 0) {
|
|
4402
|
-
throw new ForbiddenError("Invalid authenticated user.");
|
|
4403
|
-
}
|
|
4404
|
-
return userId;
|
|
4405
|
-
}
|
|
4406
|
-
|
|
4407
|
-
// ../../src/core/auth/membershipContext.ts
|
|
4408
|
-
var membershipContext = new AsyncLocalStorage4;
|
|
4409
|
-
var membershipRepository = new memberRepository_default;
|
|
4410
|
-
async function runWithMembershipContext(callback) {
|
|
4411
|
-
const user = currentAuthUser();
|
|
4412
|
-
if (!user || isGlobalAdmin(user)) {
|
|
4413
|
-
return await callback();
|
|
4414
|
-
}
|
|
4415
|
-
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
4416
|
-
const context = {
|
|
4417
|
-
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
4418
|
-
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
4419
|
-
};
|
|
4420
|
-
return await membershipContext.run(context, callback);
|
|
4421
|
-
}
|
|
4422
|
-
|
|
4423
|
-
// ../../src/core/auth/membershipContextMiddleware.ts
|
|
4424
|
-
function createMembershipContextMiddleware() {
|
|
4425
|
-
return async (_request, next) => {
|
|
4426
|
-
return await runWithMembershipContext(async () => await next());
|
|
4427
|
-
};
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
// ../../src/core/auth/membershipMiddleware.ts
|
|
4431
|
-
function createMembershipMiddleware() {
|
|
4432
|
-
return createMembershipContextMiddleware();
|
|
4433
|
-
}
|
|
4434
|
-
|
|
4435
|
-
// ../../src/core/http/authMiddleware.ts
|
|
4436
|
-
function createAuthMiddleware(auth) {
|
|
4437
|
-
return async (request, next) => {
|
|
4438
|
-
const user = await auth.resolve(request);
|
|
4439
|
-
return await runWithAuthUser(user, async () => {
|
|
4440
|
-
const response = await next();
|
|
4441
|
-
if (user) {
|
|
4442
|
-
const headers = new Headers(response.headers);
|
|
4443
|
-
headers.set("x-authenticated-user-id", String(user.id));
|
|
4444
|
-
return new Response(response.body, {
|
|
4445
|
-
status: response.status,
|
|
4446
|
-
statusText: response.statusText,
|
|
4447
|
-
headers
|
|
4448
|
-
});
|
|
4449
|
-
}
|
|
4450
|
-
return response;
|
|
4451
|
-
});
|
|
4452
|
-
};
|
|
4453
|
-
}
|
|
4454
|
-
|
|
4455
|
-
// ../../src/core/http/authorizeMiddleware.ts
|
|
4456
|
-
function createAuthorizeMiddleware(gate, auth, resource, action) {
|
|
4457
|
-
return async (request, next) => {
|
|
4458
|
-
const user = await auth.resolve(request);
|
|
4459
|
-
if (!gate.allows(resource, action, user)) {
|
|
4460
|
-
const error = new ForbiddenError;
|
|
4461
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4462
|
-
}
|
|
4463
|
-
return await next();
|
|
4464
|
-
};
|
|
4465
|
-
}
|
|
4466
|
-
|
|
4467
|
-
// ../../src/core/http/bodySizeLimitMiddleware.ts
|
|
4468
|
-
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
4469
|
-
function resolveMaxBodyBytes() {
|
|
4470
|
-
const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
4471
|
-
if (!raw) {
|
|
4472
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
4473
|
-
}
|
|
4474
|
-
const parsed = Number.parseInt(raw, 10);
|
|
4475
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4476
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
4477
|
-
}
|
|
4478
|
-
return parsed;
|
|
4479
|
-
}
|
|
4480
|
-
function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
|
|
4481
|
-
return async (request, next) => {
|
|
4482
|
-
const contentLength = request.headers.get("content-length");
|
|
4483
|
-
if (contentLength) {
|
|
4484
|
-
const bytes = Number.parseInt(contentLength, 10);
|
|
4485
|
-
if (Number.isInteger(bytes) && bytes > maxBytes) {
|
|
4486
|
-
const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
4487
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
return await next();
|
|
4491
|
-
};
|
|
4492
|
-
}
|
|
4493
|
-
|
|
4494
|
-
// ../../src/config/cors.ts
|
|
4495
|
-
var corsConfig = {
|
|
4496
|
-
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
4497
|
-
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
4498
|
-
allowedHeaders: [
|
|
4499
|
-
"Authorization",
|
|
4500
|
-
"Content-Type",
|
|
4501
|
-
"X-Request-Id",
|
|
4502
|
-
"X-Tenant-Id",
|
|
4503
|
-
"X-Authenticated-User-Id",
|
|
4504
|
-
"X-Authenticated-User-Role",
|
|
4505
|
-
"If-Match",
|
|
4506
|
-
"If-None-Match"
|
|
4507
|
-
],
|
|
4508
|
-
maxAgeSeconds: 86400
|
|
4509
|
-
};
|
|
4510
|
-
|
|
4511
|
-
// ../../src/core/http/corsMiddleware.ts
|
|
4512
|
-
function createCorsMiddleware() {
|
|
4513
|
-
return async (request, next) => {
|
|
4514
|
-
if (request.method === "OPTIONS") {
|
|
4515
|
-
return new Response(null, {
|
|
4516
|
-
status: 204,
|
|
4517
|
-
headers: buildCorsHeaders(request)
|
|
4518
|
-
});
|
|
4519
|
-
}
|
|
4520
|
-
const response = await next();
|
|
4521
|
-
const headers = new Headers(response.headers);
|
|
4522
|
-
for (const [key, value] of buildCorsHeaders(request)) {
|
|
4523
|
-
headers.set(key, value);
|
|
4524
|
-
}
|
|
4525
|
-
return new Response(response.body, {
|
|
4526
|
-
status: response.status,
|
|
4527
|
-
statusText: response.statusText,
|
|
4528
|
-
headers
|
|
4529
|
-
});
|
|
4530
|
-
};
|
|
4531
|
-
}
|
|
4532
|
-
function buildCorsHeaders(request) {
|
|
4533
|
-
const headers = new Headers;
|
|
4534
|
-
const origin = request.headers.get("origin");
|
|
4535
|
-
const allowedOrigins = corsConfig.allowedOrigins;
|
|
4536
|
-
const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
|
|
4537
|
-
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
4538
|
-
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
4539
|
-
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
4540
|
-
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
4541
|
-
headers.set("Vary", "Origin");
|
|
4542
|
-
return headers;
|
|
4543
|
-
}
|
|
4544
|
-
|
|
4545
|
-
// ../../src/core/http/csrfMiddleware.ts
|
|
4546
|
-
var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4547
|
-
function appendSetCookie(response, cookie) {
|
|
4548
|
-
const headers = new Headers(response.headers);
|
|
4549
|
-
headers.append("set-cookie", cookie);
|
|
4550
|
-
return new Response(response.body, {
|
|
4551
|
-
status: response.status,
|
|
4552
|
-
statusText: response.statusText,
|
|
4553
|
-
headers
|
|
4554
|
-
});
|
|
4555
|
-
}
|
|
4556
|
-
function createCsrfMiddleware() {
|
|
4557
|
-
return async (request, next) => {
|
|
4558
|
-
const method = request.method.toUpperCase();
|
|
4559
|
-
if (!MUTATING_METHODS.has(method)) {
|
|
4560
|
-
const csrf = resolveCsrfToken(request);
|
|
4561
|
-
const meta = currentRequestMeta();
|
|
4562
|
-
meta.csrfToken = csrf.token;
|
|
4563
|
-
const response = await next();
|
|
4564
|
-
if (!csrf.cookie) {
|
|
4565
|
-
return response;
|
|
4566
|
-
}
|
|
4567
|
-
return appendSetCookie(response, csrf.cookie);
|
|
4568
|
-
}
|
|
4569
|
-
const submitted = await readSubmittedCsrfTokenFromBody(request);
|
|
4570
|
-
if (!verifyCsrfToken(request, submitted)) {
|
|
4571
|
-
throw new ForbiddenError("Invalid or missing CSRF token.");
|
|
4572
|
-
}
|
|
4573
|
-
return await next();
|
|
4574
|
-
};
|
|
4575
|
-
}
|
|
4576
|
-
|
|
4577
|
-
// ../../src/core/http/flashMiddleware.ts
|
|
4578
|
-
function createFlashMiddleware() {
|
|
4579
|
-
return async (request, next) => {
|
|
4580
|
-
const flash = pullFlash(request);
|
|
4581
|
-
const meta = currentRequestMeta();
|
|
4582
|
-
return await runWithRequestMeta({ ...meta, request, flash }, async () => {
|
|
4583
|
-
const response = await next();
|
|
4584
|
-
if (flash) {
|
|
4585
|
-
return withFlashClear(response);
|
|
4586
|
-
}
|
|
4587
|
-
return response;
|
|
4588
|
-
});
|
|
4589
|
-
};
|
|
4590
|
-
}
|
|
4591
|
-
|
|
4592
|
-
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
4593
|
-
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
4594
|
-
function resolveLoginIdentity(request) {
|
|
4595
|
-
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
4596
|
-
}
|
|
4597
|
-
async function resolveLoginEmail(request) {
|
|
4598
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4599
|
-
try {
|
|
4600
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
4601
|
-
const formData = await request.clone().formData();
|
|
4602
|
-
const email = formData.get("email");
|
|
4603
|
-
return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
|
|
4604
|
-
}
|
|
4605
|
-
const payload = await request.clone().json();
|
|
4606
|
-
return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
|
|
4607
|
-
} catch {
|
|
4608
|
-
return "unknown";
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4611
|
-
function createLoginThrottleMiddleware(options) {
|
|
4612
|
-
const client = new RedisClient3(options.redisUrl);
|
|
4613
|
-
const prefix = options.keyPrefix ?? "workhub:login-throttle:";
|
|
4614
|
-
return async (request, next) => {
|
|
4615
|
-
const identity = resolveLoginIdentity(request);
|
|
4616
|
-
const email = await resolveLoginEmail(request);
|
|
4617
|
-
const throttleKey = `${prefix}${identity}:${email}`;
|
|
4618
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
4619
|
-
if (attempts === 1) {
|
|
4620
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
4621
|
-
}
|
|
4622
|
-
if (attempts > options.maxAttempts) {
|
|
4623
|
-
return Response.json({ error: "Too many login attempts. Try again later." }, {
|
|
4624
|
-
status: 429,
|
|
4625
|
-
headers: {
|
|
4626
|
-
"retry-after": String(options.decaySeconds)
|
|
4627
|
-
}
|
|
4628
|
-
});
|
|
4629
|
-
}
|
|
4630
|
-
return await next();
|
|
4631
|
-
};
|
|
4632
|
-
}
|
|
4633
|
-
|
|
4634
|
-
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
4635
|
-
var buckets = new Map;
|
|
4636
|
-
function createMemoryThrottleMiddleware(options) {
|
|
4637
|
-
const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
|
|
4638
|
-
return async (request, next) => {
|
|
4639
|
-
const path = new URL(request.url).pathname;
|
|
4640
|
-
const identity = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
|
|
4641
|
-
const key = `${prefix}${identity}:${path}`;
|
|
4642
|
-
const now = Date.now();
|
|
4643
|
-
const existing = buckets.get(key);
|
|
4644
|
-
if (!existing || existing.resetAt <= now) {
|
|
4645
|
-
buckets.set(key, { count: 1, resetAt: now + options.decaySeconds * 1000 });
|
|
4646
|
-
return await next();
|
|
4647
|
-
}
|
|
4648
|
-
existing.count += 1;
|
|
4649
|
-
if (existing.count > options.maxAttempts) {
|
|
4650
|
-
return Response.json({ error: "Too many requests." }, {
|
|
4651
|
-
status: 429,
|
|
4652
|
-
headers: {
|
|
4653
|
-
"retry-after": String(options.decaySeconds)
|
|
4654
|
-
}
|
|
4655
|
-
});
|
|
4656
|
-
}
|
|
4657
|
-
return await next();
|
|
4658
|
-
};
|
|
4659
|
-
}
|
|
4660
|
-
|
|
4661
|
-
// ../../src/core/metrics/prometheus.ts
|
|
4662
|
-
class PrometheusRegistry {
|
|
4663
|
-
httpRequestsTotal = new Map;
|
|
4664
|
-
httpRequestDurationMs = new Map;
|
|
4665
|
-
incrementHttpRequest(labels) {
|
|
4666
|
-
const key = this.metricKey(labels);
|
|
4667
|
-
this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
|
|
4668
|
-
}
|
|
4669
|
-
observeHttpDuration(labels, durationMs) {
|
|
4670
|
-
const key = this.metricKey(labels);
|
|
4671
|
-
const samples = this.httpRequestDurationMs.get(key) ?? [];
|
|
4672
|
-
samples.push(durationMs);
|
|
4673
|
-
this.httpRequestDurationMs.set(key, samples);
|
|
4674
|
-
}
|
|
4675
|
-
renderMetrics() {
|
|
4676
|
-
const lines = [
|
|
4677
|
-
"# HELP http_requests_total Total HTTP requests processed.",
|
|
4678
|
-
"# TYPE http_requests_total counter"
|
|
4679
|
-
];
|
|
4680
|
-
for (const [key, value] of this.httpRequestsTotal) {
|
|
4681
|
-
lines.push(`http_requests_total{${key}} ${value}`);
|
|
4682
|
-
}
|
|
4683
|
-
lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
|
|
4684
|
-
for (const [key, samples] of this.httpRequestDurationMs) {
|
|
4685
|
-
const sum = samples.reduce((total, sample) => total + sample, 0);
|
|
4686
|
-
lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
|
|
4687
|
-
}
|
|
4688
|
-
return `${lines.join(`
|
|
4689
|
-
`)}
|
|
4690
|
-
`;
|
|
4691
|
-
}
|
|
4692
|
-
resetForTests() {
|
|
4693
|
-
this.httpRequestsTotal.clear();
|
|
4694
|
-
this.httpRequestDurationMs.clear();
|
|
4695
|
-
}
|
|
4696
|
-
getHttpRequestSummary() {
|
|
4697
|
-
const byStatus = {};
|
|
4698
|
-
const pathCounts = new Map;
|
|
4699
|
-
let totalRequests = 0;
|
|
4700
|
-
for (const [key, count] of this.httpRequestsTotal) {
|
|
4701
|
-
totalRequests += count;
|
|
4702
|
-
const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
|
|
4703
|
-
const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
|
|
4704
|
-
const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
|
|
4705
|
-
byStatus[status] = (byStatus[status] ?? 0) + count;
|
|
4706
|
-
const pathKey = `${method} ${path}`;
|
|
4707
|
-
const existing = pathCounts.get(pathKey);
|
|
4708
|
-
if (existing) {
|
|
4709
|
-
existing.count += count;
|
|
4710
|
-
} else {
|
|
4711
|
-
pathCounts.set(pathKey, { method, path, count });
|
|
4712
|
-
}
|
|
4713
|
-
}
|
|
4714
|
-
const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
|
|
4715
|
-
return {
|
|
4716
|
-
totalRequests,
|
|
4717
|
-
byStatus,
|
|
4718
|
-
topPaths
|
|
4719
|
-
};
|
|
4720
|
-
}
|
|
4721
|
-
metricKey(labels) {
|
|
4722
|
-
return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
|
|
4723
|
-
}
|
|
4724
|
-
}
|
|
4725
|
-
var prometheusRegistry = new PrometheusRegistry;
|
|
4726
|
-
|
|
4727
|
-
// ../../src/core/http/metricsMiddleware.ts
|
|
4728
|
-
function normalizeMetricPath(pathname) {
|
|
4729
|
-
return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
|
|
4730
|
-
}
|
|
4731
|
-
function createMetricsMiddleware() {
|
|
4732
|
-
return async (request, next) => {
|
|
4733
|
-
const startedAt = performance.now();
|
|
4734
|
-
const response = await next();
|
|
4735
|
-
const durationMs = performance.now() - startedAt;
|
|
4736
|
-
const path = normalizeMetricPath(new URL(request.url).pathname);
|
|
4737
|
-
const labels = {
|
|
4738
|
-
method: request.method,
|
|
4739
|
-
path,
|
|
4740
|
-
status: String(response.status)
|
|
4741
|
-
};
|
|
4742
|
-
prometheusRegistry.incrementHttpRequest(labels);
|
|
4743
|
-
prometheusRegistry.observeHttpDuration(labels, durationMs);
|
|
4744
|
-
return response;
|
|
4745
|
-
};
|
|
4746
|
-
}
|
|
4747
|
-
|
|
4748
|
-
// ../../src/core/http/requireAbilityMiddleware.ts
|
|
4749
|
-
function createRequireAbilityMiddleware(abilityChecker) {
|
|
4750
|
-
return (ability) => {
|
|
4751
|
-
return async (_request, next) => {
|
|
4752
|
-
const user = currentAuthUser();
|
|
4753
|
-
try {
|
|
4754
|
-
abilityChecker.requireAbility(user, ability);
|
|
4755
|
-
} catch (error) {
|
|
4756
|
-
if (error instanceof ForbiddenError) {
|
|
4757
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4758
|
-
}
|
|
4759
|
-
throw error;
|
|
4760
|
-
}
|
|
4761
|
-
return await next();
|
|
4762
|
-
};
|
|
4763
|
-
};
|
|
4764
|
-
}
|
|
4765
|
-
|
|
4766
|
-
// ../../src/core/http/requireAuthMiddleware.ts
|
|
4767
|
-
function createRequireAuthMiddleware(auth) {
|
|
4768
|
-
return async (request, next) => {
|
|
4769
|
-
if (!await auth.check(request)) {
|
|
4770
|
-
const error = new UnauthorizedError;
|
|
4771
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4772
|
-
}
|
|
4773
|
-
return await next();
|
|
4774
|
-
};
|
|
4775
|
-
}
|
|
4776
|
-
|
|
4777
|
-
// ../../src/core/security/securityEvents.ts
|
|
4778
|
-
function logSecurityEvent2(event, details = {}) {
|
|
4779
|
-
const meta = currentRequestMeta();
|
|
4780
|
-
const user = currentAuthUser();
|
|
4781
|
-
console.log(JSON.stringify({
|
|
4782
|
-
level: "security",
|
|
4783
|
-
event,
|
|
4784
|
-
timestamp: new Date().toISOString(),
|
|
4785
|
-
ip_address: meta.ipAddress ?? null,
|
|
4786
|
-
user_agent: meta.userAgent ?? null,
|
|
4787
|
-
user_id: user?.id ?? null,
|
|
4788
|
-
...details
|
|
4789
|
-
}));
|
|
4790
|
-
}
|
|
4791
|
-
|
|
4792
|
-
// ../../src/core/http/requireGlobalAdminMiddleware.ts
|
|
4793
|
-
function createRequireGlobalAdminMiddleware() {
|
|
4794
|
-
return async (_request, next) => {
|
|
4795
|
-
const user = currentAuthUser();
|
|
4796
|
-
if (!isGlobalAdmin(user)) {
|
|
4797
|
-
logSecurityEvent2("privilege_escalation_blocked", {
|
|
4798
|
-
required_role: "platform_admin",
|
|
4799
|
-
path: new URL(_request.url).pathname
|
|
4800
|
-
});
|
|
4801
|
-
const error = new ForbiddenError("Platform admin access required.");
|
|
4802
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4803
|
-
}
|
|
4804
|
-
return await next();
|
|
4805
|
-
};
|
|
4806
|
-
}
|
|
4807
|
-
|
|
4808
|
-
// ../../src/core/http/contentNegotiation.ts
|
|
4809
|
-
function requestPrefersJson(request) {
|
|
4810
|
-
if (!request) {
|
|
4811
|
-
return true;
|
|
4812
|
-
}
|
|
4813
|
-
if (request.headers.get("HX-Request") === "true") {
|
|
4814
|
-
return false;
|
|
4815
|
-
}
|
|
4816
|
-
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
4817
|
-
if (accept.includes("text/html")) {
|
|
4818
|
-
return false;
|
|
4819
|
-
}
|
|
4820
|
-
if (accept.includes("application/json")) {
|
|
4821
|
-
return true;
|
|
4822
|
-
}
|
|
4823
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4824
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
4825
|
-
return false;
|
|
4826
|
-
}
|
|
4827
|
-
const pathname = new URL(request.url).pathname;
|
|
4828
|
-
return pathname.startsWith("/api/");
|
|
4829
|
-
}
|
|
4830
|
-
|
|
4831
|
-
// ../../src/core/http/requireWebAuthMiddleware.ts
|
|
4832
|
-
function createRequireWebAuthMiddleware(auth) {
|
|
4833
|
-
return async (request, next) => {
|
|
4834
|
-
const user = await auth.resolve(request);
|
|
4835
|
-
if (user) {
|
|
4836
|
-
return await next();
|
|
4837
|
-
}
|
|
4838
|
-
if (requestPrefersJson(request)) {
|
|
4839
|
-
throw new UnauthorizedError;
|
|
4840
|
-
}
|
|
4841
|
-
const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
|
|
4842
|
-
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
4843
|
-
};
|
|
4844
|
-
}
|
|
4845
|
-
|
|
4846
|
-
// ../../src/core/http/routeMiddleware.ts
|
|
4847
|
-
function withMiddleware(...middleware) {
|
|
4848
|
-
const wrap = composeMiddleware(...middleware);
|
|
4849
|
-
return (handler) => {
|
|
4850
|
-
return wrap(handler);
|
|
4851
|
-
};
|
|
4852
|
-
}
|
|
4853
|
-
|
|
4854
|
-
// ../../src/config/contentSecurityPolicy.ts
|
|
4855
|
-
function strictApiContentSecurityPolicy() {
|
|
4856
|
-
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
4857
|
-
}
|
|
4858
|
-
function serverHtmxContentSecurityPolicy() {
|
|
4859
|
-
return [
|
|
4860
|
-
"default-src 'self'",
|
|
4861
|
-
"script-src 'self' https://unpkg.com",
|
|
4862
|
-
"style-src 'self'",
|
|
4863
|
-
"connect-src 'self'",
|
|
4864
|
-
"img-src 'self'",
|
|
4865
|
-
"font-src 'self'",
|
|
4866
|
-
"form-action 'self'",
|
|
4867
|
-
"frame-ancestors 'none'",
|
|
4868
|
-
"base-uri 'self'"
|
|
4869
|
-
].join("; ");
|
|
4870
|
-
}
|
|
4871
|
-
function spaContentSecurityPolicy() {
|
|
4872
|
-
return [
|
|
4873
|
-
"default-src 'self'",
|
|
4874
|
-
"script-src 'self'",
|
|
4875
|
-
"style-src 'self'",
|
|
4876
|
-
"connect-src 'self'",
|
|
4877
|
-
"img-src 'self'",
|
|
4878
|
-
"font-src 'self'",
|
|
4879
|
-
"frame-ancestors 'none'",
|
|
4880
|
-
"base-uri 'self'"
|
|
4881
|
-
].join("; ");
|
|
4882
|
-
}
|
|
4883
|
-
function resolveContentSecurityPolicy(response) {
|
|
4884
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4885
|
-
if (!contentType.includes("text/html")) {
|
|
4886
|
-
return strictApiContentSecurityPolicy();
|
|
4887
|
-
}
|
|
4888
|
-
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
4889
|
-
if (frontendMode === "server-htmx") {
|
|
4890
|
-
return serverHtmxContentSecurityPolicy();
|
|
4891
|
-
}
|
|
4892
|
-
if (frontendMode === "spa-react") {
|
|
4893
|
-
return spaContentSecurityPolicy();
|
|
4894
|
-
}
|
|
4895
|
-
return strictApiContentSecurityPolicy();
|
|
4896
|
-
}
|
|
4897
|
-
|
|
4898
|
-
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
4899
|
-
function createSecurityHeadersMiddleware() {
|
|
4900
|
-
return async (_request, next) => {
|
|
4901
|
-
const response = await next();
|
|
4902
|
-
const headers = new Headers(response.headers);
|
|
4903
|
-
headers.set("X-Content-Type-Options", "nosniff");
|
|
4904
|
-
headers.set("X-Frame-Options", "DENY");
|
|
4905
|
-
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
4906
|
-
headers.set("X-XSS-Protection", "0");
|
|
4907
|
-
headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
|
|
4908
|
-
if (appConfig.env === "production") {
|
|
4909
|
-
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
4910
|
-
}
|
|
4911
|
-
return new Response(response.body, {
|
|
4912
|
-
status: response.status,
|
|
4913
|
-
statusText: response.statusText,
|
|
4914
|
-
headers
|
|
4915
|
-
});
|
|
4916
|
-
};
|
|
4917
|
-
}
|
|
4918
|
-
|
|
4919
|
-
// ../../src/core/http/throttleMiddleware.ts
|
|
4920
|
-
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
4921
|
-
|
|
4922
|
-
// ../../src/core/tenant/tenantContext.ts
|
|
4923
|
-
import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
|
|
4924
|
-
var tenantContext = new AsyncLocalStorage5;
|
|
4925
|
-
function runWithTenant(tenant, callback) {
|
|
4926
|
-
return tenantContext.run(tenant, callback);
|
|
4927
|
-
}
|
|
4928
|
-
function currentTenant() {
|
|
4929
|
-
return tenantContext.getStore() ?? null;
|
|
4930
|
-
}
|
|
4931
|
-
function rateLimitMultiplierForPlan(plan) {
|
|
4932
|
-
switch (plan) {
|
|
4933
|
-
case "enterprise":
|
|
4934
|
-
return 4;
|
|
4935
|
-
case "pro":
|
|
4936
|
-
return 2;
|
|
4937
|
-
default:
|
|
4938
|
-
return 1;
|
|
4939
|
-
}
|
|
4940
|
-
}
|
|
4941
|
-
|
|
4942
|
-
// ../../src/core/http/throttleMiddleware.ts
|
|
4943
|
-
function resolveThrottleIdentity(request) {
|
|
4944
|
-
const user = currentAuthUser();
|
|
4945
|
-
if (user?.tokenId !== undefined) {
|
|
4946
|
-
return `token:${user.tokenId}`;
|
|
4947
|
-
}
|
|
4948
|
-
if (user) {
|
|
4949
|
-
return `user:${user.id}`;
|
|
4950
|
-
}
|
|
4951
|
-
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
4952
|
-
}
|
|
4953
|
-
function createThrottleMiddleware(options) {
|
|
4954
|
-
const client = new RedisClient4(options.redisUrl);
|
|
4955
|
-
const prefix = options.keyPrefix ?? "workhub:throttle:";
|
|
4956
|
-
return async (request, next) => {
|
|
4957
|
-
const identity = resolveThrottleIdentity(request);
|
|
4958
|
-
const path = new URL(request.url).pathname;
|
|
4959
|
-
const throttleKey = `${prefix}${identity}:${path}`;
|
|
4960
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
4961
|
-
if (attempts === 1) {
|
|
4962
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
4963
|
-
}
|
|
4964
|
-
const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
|
|
4965
|
-
if (attempts > maxAttempts) {
|
|
4966
|
-
return Response.json({ error: "Too many requests." }, {
|
|
4967
|
-
status: 429,
|
|
4968
|
-
headers: {
|
|
4969
|
-
"retry-after": String(options.decaySeconds)
|
|
4970
|
-
}
|
|
4971
|
-
});
|
|
4972
|
-
}
|
|
4973
|
-
return await next();
|
|
4974
|
-
};
|
|
4975
|
-
}
|
|
4976
|
-
|
|
4977
|
-
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
4978
|
-
function createRequestLoggingMiddleware() {
|
|
4979
|
-
return async (request, next) => {
|
|
4980
|
-
return await runWithRequestMeta({
|
|
4981
|
-
ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
|
|
4982
|
-
userAgent: request.headers.get("user-agent"),
|
|
4983
|
-
request
|
|
4984
|
-
}, async () => {
|
|
4985
|
-
const startedAt = performance.now();
|
|
4986
|
-
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
4987
|
-
const response = await next();
|
|
4988
|
-
const durationMs = Math.round(performance.now() - startedAt);
|
|
4989
|
-
appLogger.info("HTTP request completed", {
|
|
4990
|
-
requestId,
|
|
4991
|
-
method: request.method,
|
|
4992
|
-
path: new URL(request.url).pathname,
|
|
4993
|
-
status: response.status,
|
|
4994
|
-
durationMs
|
|
4995
|
-
});
|
|
4996
|
-
return response;
|
|
4997
|
-
});
|
|
4998
|
-
};
|
|
4999
|
-
}
|
|
5000
|
-
|
|
5001
|
-
// ../../src/core/security/publicReads.ts
|
|
5002
|
-
function isPublicReadsEnabled() {
|
|
5003
|
-
return isFeatureEnabled("publicReads");
|
|
5004
|
-
}
|
|
5005
|
-
|
|
5006
|
-
// ../../src/core/tenant/resolveTenant.ts
|
|
5007
|
-
async function resolveTenant(tenantId) {
|
|
5008
|
-
const rows = await connection_default`
|
|
5009
|
-
SELECT id, slug, plan, region
|
|
5010
|
-
FROM tenant
|
|
5011
|
-
WHERE id = ${tenantId}
|
|
5012
|
-
LIMIT 1
|
|
5013
|
-
`;
|
|
5014
|
-
const row = rows[0];
|
|
5015
|
-
return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
|
|
5016
|
-
}
|
|
5017
|
-
|
|
5018
|
-
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
5019
|
-
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
5020
|
-
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
5021
|
-
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
5022
|
-
}
|
|
5023
|
-
async function runWithTenantDatabase(tenant, callback) {
|
|
5024
|
-
return await getDatabase().begin(async (transaction) => {
|
|
5025
|
-
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
5026
|
-
return await runWithDatabaseConnection(transaction, async () => {
|
|
5027
|
-
return await runWithTenant(tenant, callback);
|
|
5028
|
-
});
|
|
5029
|
-
});
|
|
5030
|
-
}
|
|
5031
|
-
|
|
5032
|
-
// ../../src/core/tenant/tenantMiddleware.ts
|
|
5033
|
-
var DEFAULT_TENANT = {
|
|
5034
|
-
id: 1,
|
|
5035
|
-
slug: "default",
|
|
5036
|
-
plan: "enterprise",
|
|
5037
|
-
region: "eu"
|
|
5038
|
-
};
|
|
5039
|
-
async function resolveUserTenantId(userId) {
|
|
5040
|
-
return await runWithMigrationBypass(async () => {
|
|
5041
|
-
const rows = await connection_default`
|
|
5042
|
-
SELECT tenant_id
|
|
5043
|
-
FROM users
|
|
5044
|
-
WHERE id = ${userId}
|
|
5045
|
-
LIMIT 1
|
|
5046
|
-
`;
|
|
5047
|
-
return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
|
|
5048
|
-
});
|
|
5049
|
-
}
|
|
5050
|
-
async function resolveTenantForRequest(request) {
|
|
5051
|
-
const user = currentAuthUser();
|
|
5052
|
-
const headerValue = request.headers.get("x-tenant-id")?.trim();
|
|
5053
|
-
const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
|
|
5054
|
-
if (user) {
|
|
5055
|
-
const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
|
|
5056
|
-
if (Number.isInteger(userId) && userId > 0) {
|
|
5057
|
-
const userTenantId = await resolveUserTenantId(userId);
|
|
5058
|
-
if (!isGlobalAdmin(user)) {
|
|
5059
|
-
if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
|
|
5060
|
-
throw new ForbiddenError("Tenant header does not match your account.");
|
|
5061
|
-
}
|
|
5062
|
-
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
5063
|
-
}
|
|
5064
|
-
if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
|
|
5065
|
-
return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
|
|
5066
|
-
}
|
|
5067
|
-
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
5068
|
-
}
|
|
5069
|
-
}
|
|
5070
|
-
const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
|
|
5071
|
-
return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
|
|
5072
|
-
}
|
|
5073
|
-
function createTenantMiddleware() {
|
|
5074
|
-
return async (request, next) => {
|
|
5075
|
-
try {
|
|
5076
|
-
const tenant = await resolveTenantForRequest(request);
|
|
5077
|
-
return await runWithTenantDatabase(tenant, async () => {
|
|
5078
|
-
const response = await next();
|
|
5079
|
-
const headers = new Headers(response.headers);
|
|
5080
|
-
headers.set("x-tenant-id", String(tenant.id));
|
|
5081
|
-
headers.set("x-tenant-region", tenant.region);
|
|
5082
|
-
return new Response(response.body, {
|
|
5083
|
-
status: response.status,
|
|
5084
|
-
statusText: response.statusText,
|
|
5085
|
-
headers
|
|
5086
|
-
});
|
|
5087
|
-
});
|
|
5088
|
-
} catch (error) {
|
|
5089
|
-
if (error instanceof HttpError) {
|
|
5090
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
5091
|
-
}
|
|
5092
|
-
throw error;
|
|
5093
|
-
}
|
|
5094
|
-
};
|
|
5095
|
-
}
|
|
5096
|
-
|
|
5097
|
-
// ../../src/core/tracing/otel.ts
|
|
5098
|
-
import { randomBytes } from "crypto";
|
|
5099
|
-
function randomHex(bytes) {
|
|
5100
|
-
return randomBytes(bytes).toString("hex");
|
|
5101
|
-
}
|
|
5102
|
-
function createSpan(input) {
|
|
5103
|
-
const spanId = randomHex(8);
|
|
5104
|
-
return {
|
|
5105
|
-
traceId: input.traceId,
|
|
5106
|
-
spanId,
|
|
5107
|
-
name: input.name,
|
|
5108
|
-
startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
|
|
5109
|
-
endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
|
|
5110
|
-
attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
|
|
5111
|
-
key,
|
|
5112
|
-
value: { stringValue: value }
|
|
5113
|
-
})),
|
|
5114
|
-
status: { code: 1 }
|
|
5115
|
-
};
|
|
5116
|
-
}
|
|
5117
|
-
async function exportOtelSpan(span) {
|
|
5118
|
-
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
5119
|
-
if (!endpoint) {
|
|
5120
|
-
return;
|
|
5121
|
-
}
|
|
5122
|
-
const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
|
|
5123
|
-
const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
|
|
5124
|
-
await fetch(url, {
|
|
5125
|
-
method: "POST",
|
|
5126
|
-
headers: { "content-type": "application/json" },
|
|
5127
|
-
body: JSON.stringify({
|
|
5128
|
-
resourceSpans: [
|
|
5129
|
-
{
|
|
5130
|
-
resource: {
|
|
5131
|
-
attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
|
|
5132
|
-
},
|
|
5133
|
-
scopeSpans: [{ spans: [span] }]
|
|
5134
|
-
}
|
|
5135
|
-
]
|
|
5136
|
-
})
|
|
5137
|
-
});
|
|
5138
|
-
}
|
|
5139
|
-
|
|
5140
|
-
// ../../src/core/tracing/traceContext.ts
|
|
5141
|
-
import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
|
|
5142
|
-
var traceContextStorage = new AsyncLocalStorage6;
|
|
5143
|
-
function runWithTraceContext(context, callback) {
|
|
5144
|
-
return traceContextStorage.run(context, callback);
|
|
5145
|
-
}
|
|
5146
|
-
|
|
5147
|
-
// ../../src/core/tracing/tracingMiddleware.ts
|
|
5148
|
-
function createTracingMiddleware() {
|
|
5149
|
-
return async (request, next) => {
|
|
5150
|
-
const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
|
|
5151
|
-
const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
5152
|
-
const startedAt = performance.now();
|
|
5153
|
-
const path = new URL(request.url).pathname;
|
|
5154
|
-
return await runWithTraceContext({ traceId, spanId }, async () => {
|
|
5155
|
-
const response = await next();
|
|
5156
|
-
const endedAt = performance.now();
|
|
5157
|
-
const headers = new Headers(response.headers);
|
|
5158
|
-
headers.set("x-trace-id", traceId);
|
|
5159
|
-
headers.set("x-span-id", spanId);
|
|
5160
|
-
headers.set("traceparent", `00-${traceId}-${spanId}-01`);
|
|
5161
|
-
headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
|
|
5162
|
-
exportOtelSpan(createSpan({
|
|
5163
|
-
traceId,
|
|
5164
|
-
name: `${request.method} ${path}`,
|
|
5165
|
-
startedAt,
|
|
5166
|
-
endedAt,
|
|
5167
|
-
attributes: {
|
|
5168
|
-
"http.method": request.method,
|
|
5169
|
-
"http.route": path,
|
|
5170
|
-
"http.status_code": String(response.status)
|
|
5171
|
-
}
|
|
5172
|
-
})).catch(() => {
|
|
5173
|
-
return;
|
|
5174
|
-
});
|
|
5175
|
-
return new Response(response.body, {
|
|
5176
|
-
status: response.status,
|
|
5177
|
-
statusText: response.statusText,
|
|
5178
|
-
headers
|
|
5179
|
-
});
|
|
5180
|
-
});
|
|
5181
|
-
};
|
|
5182
|
-
}
|
|
5183
|
-
|
|
5184
4285
|
// ../../src/bootstrap/httpKernel.ts
|
|
5185
4286
|
class HttpKernel {
|
|
5186
4287
|
dependencies;
|
|
@@ -5576,7 +4677,7 @@ function createWebServer(options) {
|
|
|
5576
4677
|
});
|
|
5577
4678
|
}
|
|
5578
4679
|
// ../../src/bootstrap/web/session.ts
|
|
5579
|
-
import { createHash, randomBytes
|
|
4680
|
+
import { createHash, randomBytes } from "crypto";
|
|
5580
4681
|
class CookieSessionStore {
|
|
5581
4682
|
sql;
|
|
5582
4683
|
secret;
|
|
@@ -5602,7 +4703,7 @@ class CookieSessionStore {
|
|
|
5602
4703
|
return header.includes("Secure") ? header : `${header}; Secure`;
|
|
5603
4704
|
}
|
|
5604
4705
|
async create(user) {
|
|
5605
|
-
const id =
|
|
4706
|
+
const id = randomBytes(32).toString("hex");
|
|
5606
4707
|
const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
|
|
5607
4708
|
await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
|
|
5608
4709
|
id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/bootstrap",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"access": "public"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@getstrata/core": "^0.5.
|
|
76
|
+
"@getstrata/core": "^0.5.14",
|
|
77
77
|
"typescript": "^5.9.0"
|
|
78
78
|
}
|
|
79
79
|
}
|