@getstrata/bootstrap 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,6 @@
1
+ export { type CsrfProtectionOptions, createCsrfProtection, } from "../../core/http/csrfProtection.ts";
1
2
  export interface ParsedForm {
2
3
  fields: Record<string, string>;
3
4
  files: Record<string, File>;
4
5
  }
5
6
  export declare function parseFormBody(request: Request): Promise<ParsedForm>;
6
- export declare function createCsrfProtection(secret: string): {
7
- generate: (sessionKey: string) => string;
8
- verify: (token: string | undefined, maxAgeMs?: number) => boolean;
9
- secret: string;
10
- };
@@ -2,6 +2,6 @@
2
2
  * Web-focused bootstrap utilities for sibling apps (getstrata, marketing sites).
3
3
  */
4
4
  export { createCsrfProtection, type ParsedForm, parseFormBody } from "./forms.ts";
5
- export { createWebServer, type WebServerOptions } from "./server.ts";
5
+ export { convertAppRoutesToBunRoutes, createWebServer, type WebServerOptions } from "./server.ts";
6
6
  export { CookieSessionStore, type SessionUser } from "./session.ts";
7
7
  export { slugify } from "./slug.ts";
@@ -1,7 +1,13 @@
1
+ import type { BunRequest } from "bun";
2
+ import type { AppRouteMap } from "../contracts.ts";
1
3
  export interface WebServerOptions {
2
4
  port: number;
3
- handle: (request: Request) => Promise<Response | null> | Response | null;
5
+ handle?: (request: Request) => Promise<Response | null> | Response | null;
6
+ routes?: AppRouteMap;
4
7
  publicDir?: string;
5
8
  onRequest?: (request: Request) => Promise<void> | void;
6
9
  }
10
+ type BunRouteHandler = (request: BunRequest) => Response | Promise<Response>;
11
+ declare function convertAppRoutesToBunRoutes(routes: AppRouteMap): Record<string, Record<string, BunRouteHandler>>;
7
12
  export declare function createWebServer(options: WebServerOptions): Bun.Server<undefined>;
13
+ export { convertAppRoutesToBunRoutes };
@@ -20,6 +20,5 @@ export declare class CookieSessionStore {
20
20
  destroy(sessionId: string): Promise<void>;
21
21
  read(request: Request): Promise<SessionUser | null>;
22
22
  private sign;
23
- private parseCookie;
24
23
  }
25
24
  export {};
@@ -0,0 +1,2 @@
1
+ declare function nonCryptographicDigest(input: string): string;
2
+ export { nonCryptographicDigest };
@@ -0,0 +1,4 @@
1
+ import type { BunRequest } from "bun";
2
+ declare function readRequestCookie(request: Request, name: string): string | null;
3
+ declare function readBunRequestCookie(request: BunRequest, name: string): string | null;
4
+ export { readBunRequestCookie, readRequestCookie };
@@ -0,0 +1,12 @@
1
+ declare const DEFAULT_CSRF_TTL_MS: number;
2
+ interface CsrfProtectionOptions {
3
+ expiresIn?: number;
4
+ maxAge?: number;
5
+ }
6
+ declare function createCsrfProtection(secret: string, options?: CsrfProtectionOptions): {
7
+ generate(_sessionKey?: string): string;
8
+ verify(token: string | undefined, _sessionKey?: string): boolean;
9
+ secret: string;
10
+ };
11
+ export type { CsrfProtectionOptions };
12
+ export { createCsrfProtection, DEFAULT_CSRF_TTL_MS };
@@ -14,6 +14,7 @@ export { CACHE_TAGS } from "../core/cache/tags.ts";
14
14
  export type { DatabaseConnection } from "../core/database/baseRepository.ts";
15
15
  export { default as BaseRepository } from "../core/database/baseRepository.ts";
16
16
  export { bindDatabaseConnection } from "../core/database/bindConnection.ts";
17
+ export { createDatabaseConnection } from "../core/database/connection.ts";
17
18
  export { getActiveDatabaseConnection, runWithDatabaseConnection, } from "../core/database/connectionContext.ts";
18
19
  export { withMigrationLock } from "../core/database/migrations/advisoryLock.ts";
19
20
  export { freshDatabase, getMigrationStatus, loadMigrationsFromDirectory, migrateDatabase, rollbackDatabase, } from "../core/database/migrations/runner.ts";
@@ -36,7 +37,10 @@ export { BadRequestError, ConflictError, ForbiddenError, NotFoundError, Precondi
36
37
  export { EventBus } from "../core/events/eventBus.ts";
37
38
  export { auth, cache, config, events, log, mail, policyGate, queue, storage, } from "../core/facades/index.ts";
38
39
  export { createBodySizeLimitMiddleware } from "../core/http/bodySizeLimitMiddleware.ts";
40
+ export { readBunRequestCookie, readRequestCookie } from "../core/http/cookies.ts";
39
41
  export { createCsrfMiddleware } from "../core/http/csrfMiddleware.ts";
42
+ export { createCsrfProtection } from "../core/http/csrfProtection.ts";
43
+ export { createCsrfTokenCookie, readSubmittedCsrfToken, readSubmittedCsrfTokenFromBody, resolveCsrfToken, resolveCsrfTokenForRequest, verifyCsrfToken, } from "../core/http/csrfToken.ts";
40
44
  export { assertIfMatch, etagFromResource, isEtagEnabled, } from "../core/http/etag.ts";
41
45
  export { FormRequest } from "../core/http/formRequest.ts";
42
46
  export { applyMiddlewareToRoutes, composeMiddleware, createAuthMiddleware, createAuthorizeMiddleware, createdResponse, createRequireAuthMiddleware, jsonResponse, noContentResponse, paginatedResponse, parsePaginationQuery, securedBindRouteModel, securedBindRouteModelByKey, withErrorHandling, withMiddleware, } from "../core/http/index.ts";
package/dist/index.js CHANGED
@@ -3748,76 +3748,61 @@ function htmlResponse(html, init = {}) {
3748
3748
  });
3749
3749
  }
3750
3750
  // ../../src/core/http/csrfToken.ts
3751
- import { createHmac as createHmac3, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
3751
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
3752
+
3753
+ // ../../src/core/http/cookies.ts
3754
+ function readRequestCookie(request, name) {
3755
+ const cookies = request.cookies;
3756
+ if (cookies && typeof cookies.get === "function") {
3757
+ const value = cookies.get(name);
3758
+ if (value) {
3759
+ return value;
3760
+ }
3761
+ }
3762
+ const header = request.headers.get("cookie");
3763
+ if (!header) {
3764
+ return null;
3765
+ }
3766
+ for (const part of header.split(";")) {
3767
+ const idx = part.indexOf("=");
3768
+ if (idx === -1)
3769
+ continue;
3770
+ const cookieName = part.slice(0, idx).trim();
3771
+ if (cookieName !== name)
3772
+ continue;
3773
+ return decodeURIComponent(part.slice(idx + 1).trim());
3774
+ }
3775
+ return null;
3776
+ }
3777
+
3778
+ // ../../src/core/http/csrfToken.ts
3752
3779
  var CSRF_COOKIE = "workhub_csrf";
3753
3780
  var CSRF_TTL_MS = 60 * 60 * 1000;
3754
3781
  function resolveCsrfSecret() {
3755
3782
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
3756
3783
  }
3757
- function signCsrfToken(token, issuedAt) {
3758
- const payload = `${token}.${issuedAt}`;
3759
- const signature = createHmac3("sha256", resolveCsrfSecret()).update(payload).digest("hex");
3760
- return `${payload}.${signature}`;
3761
- }
3762
- function readCsrfCookie(request) {
3763
- const cookieHeader = request.headers.get("cookie");
3764
- if (!cookieHeader) {
3765
- return null;
3766
- }
3767
- for (const part of cookieHeader.split(";")) {
3768
- const [name, ...rest] = part.trim().split("=");
3769
- if (name === CSRF_COOKIE) {
3770
- return decodeURIComponent(rest.join("="));
3771
- }
3772
- }
3773
- return null;
3784
+ function csrfVerifyOptions() {
3785
+ return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
3774
3786
  }
3775
- function parseSignedCsrfValue(cookieValue) {
3776
- const parts = cookieValue.split(".");
3777
- if (parts.length !== 3) {
3778
- return null;
3779
- }
3780
- const [token, issuedAtRaw, cookieSignature] = parts;
3781
- if (!token || !issuedAtRaw || !cookieSignature) {
3782
- return null;
3783
- }
3784
- const issuedAt = Number.parseInt(issuedAtRaw, 10);
3785
- if (!Number.isFinite(issuedAt)) {
3786
- return null;
3787
- }
3788
- if (Date.now() - issuedAt > CSRF_TTL_MS) {
3789
- return null;
3790
- }
3791
- const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
3792
- if (!expectedSignature) {
3793
- return null;
3794
- }
3795
- const expectedBuffer = Buffer.from(expectedSignature);
3796
- const actualBuffer = Buffer.from(cookieSignature);
3797
- if (expectedBuffer.length !== actualBuffer.length) {
3798
- return null;
3799
- }
3800
- if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
3801
- return null;
3787
+ function tokensMatch(left, right) {
3788
+ const leftBuffer = Buffer.from(left);
3789
+ const rightBuffer = Buffer.from(right);
3790
+ if (leftBuffer.length !== rightBuffer.length) {
3791
+ return false;
3802
3792
  }
3803
- return { token, issuedAt };
3793
+ return timingSafeEqual2(leftBuffer, rightBuffer);
3804
3794
  }
3805
3795
  function createCsrfTokenCookie() {
3806
- const token = randomBytes(24).toString("hex");
3807
- const issuedAt = Date.now();
3808
- const value = signCsrfToken(token, issuedAt);
3796
+ const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
3809
3797
  return {
3810
3798
  token,
3811
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=3600`
3799
+ cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
3812
3800
  };
3813
3801
  }
3814
3802
  function resolveCsrfToken(request) {
3815
- const cookieValue = readCsrfCookie(request);
3816
- if (cookieValue) {
3817
- const parsed = parseSignedCsrfValue(cookieValue);
3818
- if (parsed) {
3819
- return { token: parsed.token };
3820
- }
3803
+ const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3804
+ if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
3805
+ return { token: cookieValue };
3821
3806
  }
3822
3807
  return createCsrfTokenCookie();
3823
3808
  }
@@ -3840,6 +3825,10 @@ async function readSubmittedCsrfTokenFromBody(request) {
3840
3825
  if (typeof field === "string" && field.trim().length > 0) {
3841
3826
  return field.trim();
3842
3827
  }
3828
+ const legacyField = formData.get("_csrf");
3829
+ if (typeof legacyField === "string" && legacyField.trim().length > 0) {
3830
+ return legacyField.trim();
3831
+ }
3843
3832
  }
3844
3833
  return null;
3845
3834
  }
@@ -3847,20 +3836,14 @@ function verifyCsrfToken(request, submittedToken) {
3847
3836
  if (!submittedToken) {
3848
3837
  return false;
3849
3838
  }
3850
- const cookieValue = readCsrfCookie(request);
3839
+ const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3851
3840
  if (!cookieValue) {
3852
3841
  return false;
3853
3842
  }
3854
- const parsed = parseSignedCsrfValue(cookieValue);
3855
- if (!parsed) {
3843
+ if (!tokensMatch(submittedToken, cookieValue)) {
3856
3844
  return false;
3857
3845
  }
3858
- const submittedBuffer = Buffer.from(submittedToken);
3859
- const expectedBuffer = Buffer.from(parsed.token);
3860
- if (submittedBuffer.length !== expectedBuffer.length) {
3861
- return false;
3862
- }
3863
- return timingSafeEqual2(submittedBuffer, expectedBuffer);
3846
+ return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
3864
3847
  }
3865
3848
  function resolveCsrfTokenForRequest(request) {
3866
3849
  const metaToken = currentRequestMeta().csrfToken;
@@ -3871,14 +3854,14 @@ function resolveCsrfTokenForRequest(request) {
3871
3854
  }
3872
3855
 
3873
3856
  // ../../src/core/http/flashSession.ts
3874
- import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
3857
+ import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
3875
3858
  var FLASH_COOKIE = "workhub_flash";
3876
3859
  var FLASH_TTL_MS = 60 * 1000;
3877
3860
  function resolveFlashSecret() {
3878
3861
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
3879
3862
  }
3880
3863
  function signFlashPayload(payload, issuedAt) {
3881
- const signature = createHmac4("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
3864
+ const signature = createHmac3("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
3882
3865
  return `${payload}.${issuedAt}.${signature}`;
3883
3866
  }
3884
3867
  function readFlashCookie(request) {
@@ -4967,9 +4950,9 @@ function createTenantMiddleware() {
4967
4950
  }
4968
4951
 
4969
4952
  // ../../src/core/tracing/otel.ts
4970
- import { randomBytes as randomBytes2 } from "crypto";
4953
+ import { randomBytes } from "crypto";
4971
4954
  function randomHex(bytes) {
4972
- return randomBytes2(bytes).toString("hex");
4955
+ return randomBytes(bytes).toString("hex");
4973
4956
  }
4974
4957
  function createSpan(input) {
4975
4958
  const spanId = randomHex(8);
@@ -5302,6 +5285,25 @@ function prefixRouteMap(prefix, routes) {
5302
5285
  }
5303
5286
  return prefixed;
5304
5287
  }
5288
+ // ../../src/core/http/csrfProtection.ts
5289
+ var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
5290
+ function createCsrfProtection(secret, options = {}) {
5291
+ const expiresIn = options.expiresIn ?? DEFAULT_CSRF_TTL_MS;
5292
+ const maxAge = options.maxAge ?? expiresIn;
5293
+ return {
5294
+ generate(_sessionKey) {
5295
+ return Bun.CSRF.generate(secret, { expiresIn });
5296
+ },
5297
+ verify(token, _sessionKey) {
5298
+ if (!token) {
5299
+ return false;
5300
+ }
5301
+ return Bun.CSRF.verify(token, { secret, maxAge });
5302
+ },
5303
+ secret
5304
+ };
5305
+ }
5306
+
5305
5307
  // ../../src/bootstrap/web/forms.ts
5306
5308
  async function parseFormBody(request) {
5307
5309
  const contentType = request.headers.get("content-type") ?? "";
@@ -5331,30 +5333,41 @@ async function parseFormBody(request) {
5331
5333
  }
5332
5334
  return { fields, files };
5333
5335
  }
5334
- function createCsrfProtection(secret) {
5335
- const tokenCache = new Map;
5336
- function generate(sessionKey) {
5337
- const token = `${sessionKey}.${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}`;
5338
- tokenCache.set(token, Date.now());
5339
- return token;
5340
- }
5341
- function verify(token, maxAgeMs = 60 * 60 * 1000) {
5342
- if (!token || !tokenCache.has(token))
5343
- return false;
5344
- const created = tokenCache.get(token) ?? 0;
5345
- if (Date.now() - created > maxAgeMs) {
5346
- tokenCache.delete(token);
5347
- return false;
5336
+ // ../../src/bootstrap/web/server.ts
5337
+ function wrapRouteHandler2(handler) {
5338
+ return async (request) => {
5339
+ const response = await handler(request);
5340
+ return response ?? new Response("Not Found", { status: 404 });
5341
+ };
5342
+ }
5343
+ function convertAppRoutesToBunRoutes(routes) {
5344
+ const bunRoutes = {};
5345
+ for (const [path, handler] of Object.entries(routes)) {
5346
+ if (typeof handler === "function") {
5347
+ bunRoutes[path] = { GET: wrapRouteHandler2(handler) };
5348
+ continue;
5349
+ }
5350
+ if (handler && typeof handler === "object" && !Array.isArray(handler)) {
5351
+ const methods = {};
5352
+ for (const [method, methodHandler] of Object.entries(handler)) {
5353
+ if (typeof methodHandler !== "function") {
5354
+ continue;
5355
+ }
5356
+ methods[method.toUpperCase()] = wrapRouteHandler2(methodHandler);
5357
+ }
5358
+ if (Object.keys(methods).length > 0) {
5359
+ bunRoutes[path] = methods;
5360
+ }
5348
5361
  }
5349
- return true;
5350
5362
  }
5351
- return { generate, verify, secret };
5363
+ return bunRoutes;
5352
5364
  }
5353
- // ../../src/bootstrap/web/server.ts
5354
5365
  function createWebServer(options) {
5355
5366
  const publicDir = options.publicDir ?? "./public";
5367
+ const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes) : undefined;
5356
5368
  return Bun.serve({
5357
5369
  port: options.port,
5370
+ ...bunRoutes ? { routes: bunRoutes } : {},
5358
5371
  async fetch(request) {
5359
5372
  await options.onRequest?.(request);
5360
5373
  const url = new URL(request.url);
@@ -5364,14 +5377,16 @@ function createWebServer(options) {
5364
5377
  return new Response(file);
5365
5378
  }
5366
5379
  }
5367
- const response = await options.handle(request);
5368
- return response ?? new Response("Not Found", { status: 404 });
5380
+ if (options.handle) {
5381
+ const response = await options.handle(request);
5382
+ return response ?? new Response("Not Found", { status: 404 });
5383
+ }
5384
+ return new Response("Not Found", { status: 404 });
5369
5385
  }
5370
5386
  });
5371
5387
  }
5372
5388
  // ../../src/bootstrap/web/session.ts
5373
- import { createHash, randomBytes as randomBytes3 } from "crypto";
5374
-
5389
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
5375
5390
  class CookieSessionStore {
5376
5391
  sql;
5377
5392
  secret;
@@ -5397,7 +5412,7 @@ class CookieSessionStore {
5397
5412
  return header.includes("Secure") ? header : `${header}; Secure`;
5398
5413
  }
5399
5414
  async create(user) {
5400
- const id = randomBytes3(32).toString("hex");
5415
+ const id = randomBytes2(32).toString("hex");
5401
5416
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
5402
5417
  await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
5403
5418
  id,
@@ -5410,8 +5425,8 @@ class CookieSessionStore {
5410
5425
  await this.sql.unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
5411
5426
  }
5412
5427
  async read(request) {
5413
- const cookie = this.parseCookie(request.headers.get("cookie") ?? "");
5414
- const raw = cookie[this.cookieName];
5428
+ const cookie = readRequestCookie(request, this.cookieName);
5429
+ const raw = cookie ?? null;
5415
5430
  if (!raw)
5416
5431
  return null;
5417
5432
  const [sessionId, signature] = raw.split(".");
@@ -5435,16 +5450,6 @@ class CookieSessionStore {
5435
5450
  sign(value) {
5436
5451
  return createHash("sha256").update(`${value}.${this.secret}`).digest("hex").slice(0, 32);
5437
5452
  }
5438
- parseCookie(header) {
5439
- const out = {};
5440
- for (const part of header.split(";")) {
5441
- const idx = part.indexOf("=");
5442
- if (idx === -1)
5443
- continue;
5444
- out[part.slice(0, idx).trim()] = decodeURIComponent(part.slice(idx + 1).trim());
5445
- }
5446
- return out;
5447
- }
5448
5453
  }
5449
5454
  // ../../src/bootstrap/web/slug.ts
5450
5455
  function slugify(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/bootstrap",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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.5",
76
+ "@getstrata/core": "^0.5.6",
77
77
  "typescript": "^5.9.0"
78
78
  }
79
79
  }