@getstrata/bootstrap 0.2.2 → 0.2.4

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.
@@ -0,0 +1,7 @@
1
+ declare class App {
2
+ private server?;
3
+ serve(): void;
4
+ stop(force?: boolean): void;
5
+ private resolvePort;
6
+ }
7
+ export default App;
@@ -0,0 +1,3 @@
1
+ import type { AppDependencies, AppRouteMap } from "./contracts";
2
+ declare function createRoutes(dependencies: AppDependencies): AppRouteMap;
3
+ export { createRoutes };
@@ -0,0 +1,5 @@
1
+ import type { AppDependencies, AppRouteMap } from "./contracts";
2
+ declare const SPA_DIST_DIRECTORY: string;
3
+ declare function createSpaRoutes(_dependencies: AppDependencies): AppRouteMap;
4
+ declare function mergeSpaRoutes(dependencies: AppDependencies, routes: AppRouteMap): AppRouteMap;
5
+ export { createSpaRoutes, mergeSpaRoutes, SPA_DIST_DIRECTORY };
@@ -0,0 +1,4 @@
1
+ import { createAppContext } from "./context";
2
+ declare function createAppDependencies(): import("./contracts").AppDependencies;
3
+ export type { AppContext, AppDependencies } from "./contracts";
4
+ export { createAppContext, createAppDependencies };
@@ -0,0 +1,8 @@
1
+ import type { AppDependencies } from "./contracts";
2
+ declare function checkDatabase(): Promise<boolean>;
3
+ declare function checkRedis(redisUrl: string): Promise<boolean>;
4
+ declare function createHealthRoutes(dependencies: AppDependencies): {
5
+ "/health": () => Promise<Response>;
6
+ "/ready": () => Promise<Response>;
7
+ };
8
+ export { checkDatabase, checkRedis, createHealthRoutes };
@@ -0,0 +1,4 @@
1
+ declare function createMetricsRoutes(): {
2
+ "/metrics": () => Promise<Response>;
3
+ };
4
+ export { createMetricsRoutes };
@@ -0,0 +1,4 @@
1
+ import { type Middleware } from "../core/http/middleware";
2
+ import type { AppDependencies } from "./contracts";
3
+ declare function createDefaultMiddleware(dependencies: AppDependencies): Middleware[];
4
+ export { createDefaultMiddleware };
@@ -13,4 +13,4 @@ export { createWebRoutes, mergeWebRoutes } from "./createWebRoutes.ts";
13
13
  export { createHttpKernel, type HttpKernel, type MiddlewareGroupName } from "./httpKernel.ts";
14
14
  export { prefixRouteMap } from "./prefixRouteMap.ts";
15
15
  export { coreProviders } from "./providers/index.ts";
16
- export { CookieSessionStore, createCsrfProtection, createWebServer, type ParsedForm, parseFormBody, type SessionUser, slugify, type WebServerOptions, } from "./web/index.ts";
16
+ export { CookieSessionStore, createCsrfProtection, createRouteKernel, createWebServer, type ParsedForm, parseFormBody, routeParams, type SessionUser, slugify, toRouteRequest, type WebServerOptions, wrapSecuredRouteModelByKey, wrapWebLogin, wrapWebRegister, } from "./web/index.ts";
@@ -0,0 +1,2 @@
1
+ declare const routes: import("./contracts").AppRouteMap;
2
+ export { routes };
@@ -0,0 +1,24 @@
1
+ import type { RouteHandler } from "../core/http/middleware";
2
+ import type { AppDependencies } from "./contracts";
3
+ declare function createScimRoutes(dependencies: AppDependencies): {
4
+ "/scim/v2/ServiceProviderConfig": {
5
+ GET: RouteHandler;
6
+ };
7
+ "/scim/v2/Users": {
8
+ GET: RouteHandler;
9
+ POST: RouteHandler;
10
+ };
11
+ "/scim/v2/Users/:id": {
12
+ GET: RouteHandler;
13
+ PATCH: RouteHandler;
14
+ DELETE: RouteHandler;
15
+ };
16
+ "/scim/v2/Groups": {
17
+ GET: RouteHandler;
18
+ };
19
+ "/scim/v2/Groups/:id": {
20
+ GET: RouteHandler;
21
+ PATCH: RouteHandler;
22
+ };
23
+ };
24
+ export { createScimRoutes };
@@ -0,0 +1 @@
1
+ export {};
@@ -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,7 @@
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 { createRouteKernel, routeParams, toRouteRequest, wrapSecuredRouteModelByKey, wrapWebLogin, wrapWebRegister, } from "./routing.ts";
6
+ export { convertAppRoutesToBunRoutes, createWebServer, type WebServerOptions } from "./server.ts";
6
7
  export { CookieSessionStore, type SessionUser } from "./session.ts";
7
8
  export { slugify } from "./slug.ts";
@@ -0,0 +1,21 @@
1
+ import type { RouteHandler, RouteRequest } from "@getstrata/core";
2
+ import type { AppDependencies } from "../contracts.ts";
3
+ import type { HttpKernel } from "../httpKernel.ts";
4
+ /** Read Bun native `:param` values from a route handler request. */
5
+ export declare function routeParams(request: Request): Record<string, string>;
6
+ /**
7
+ * Attach decoded route params to Bun's native Request for RouteRequest handlers.
8
+ * Mutates the request in place so instanceof Request and Bun internals stay valid.
9
+ */
10
+ export declare function toRouteRequest<TParams extends Record<string, string>>(request: Request): RouteRequest<TParams>;
11
+ /** Laravel-style secured route-model binding for string keys (slugs, UUIDs). */
12
+ export declare function wrapSecuredRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: {
13
+ resource: string;
14
+ action: "view" | "create" | "update" | "delete";
15
+ }, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): RouteHandler;
16
+ /** Converts kernel login throttle 429 JSON into an HTML response for web forms. */
17
+ export declare function wrapWebLogin(kernel: HttpKernel, handler: RouteHandler, onThrottled: (request: Request) => Response | Promise<Response>): RouteHandler;
18
+ /** Converts kernel register throttle 429 JSON into an HTML response for web forms. */
19
+ export declare function wrapWebRegister(kernel: HttpKernel, handler: RouteHandler, onThrottled: (request: Request) => Response | Promise<Response>): RouteHandler;
20
+ /** Convenience alias: HttpKernel is the Laravel-style router middleware wrapper. */
21
+ export declare function createRouteKernel(dependencies: AppDependencies): HttpKernel;
@@ -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,3 @@
1
+ import type { Middleware } from "../http/middleware";
2
+ declare function createScimAuthMiddleware(): Middleware;
3
+ export { createScimAuthMiddleware };
@@ -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 };
@@ -0,0 +1,9 @@
1
+ import type { Middleware } from "./middleware";
2
+ interface ScimThrottleOptions {
3
+ redisUrl?: string;
4
+ maxAttempts: number;
5
+ decaySeconds: number;
6
+ }
7
+ declare function createScimThrottleMiddleware(options: ScimThrottleOptions): Middleware;
8
+ export type { ScimThrottleOptions };
9
+ export { createScimThrottleMiddleware };
@@ -0,0 +1,3 @@
1
+ declare function parseScimTenantTokens(raw: string | undefined): Map<number, string>;
2
+ declare function resolveScimTenantFromToken(token: string): number | null;
3
+ export { parseScimTenantTokens, resolveScimTenantFromToken };
@@ -0,0 +1,2 @@
1
+ declare function timingSafeCompareString(left: string, right: string): boolean;
2
+ export { timingSafeCompareString };
@@ -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}`;
3784
+ function csrfVerifyOptions() {
3785
+ return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
3761
3786
  }
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;
3774
- }
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) {
3856
- return false;
3857
- }
3858
- const submittedBuffer = Buffer.from(submittedToken);
3859
- const expectedBuffer = Buffer.from(parsed.token);
3860
- if (submittedBuffer.length !== expectedBuffer.length) {
3843
+ if (!tokensMatch(submittedToken, cookieValue)) {
3861
3844
  return false;
3862
3845
  }
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,86 @@ 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/routing.ts
5337
+ import { securedBindRouteModelByKey, withErrorHandling } from "@getstrata/core";
5338
+ function routeParams(request) {
5339
+ const normalized = {};
5340
+ const raw = request.params;
5341
+ if (raw && typeof raw === "object") {
5342
+ for (const [key, value] of Object.entries(raw)) {
5343
+ normalized[key] = decodeURIComponent(String(value));
5344
+ }
5345
+ }
5346
+ return normalized;
5347
+ }
5348
+ function toRouteRequest(request) {
5349
+ const params = routeParams(request);
5350
+ Object.defineProperty(request, "params", {
5351
+ value: params,
5352
+ enumerable: true,
5353
+ configurable: true,
5354
+ writable: true
5355
+ });
5356
+ return request;
5357
+ }
5358
+ function wrapSecuredRouteModelByKey(param, resolver, authorization, handler) {
5359
+ const bound = withErrorHandling(securedBindRouteModelByKey(param, resolver, authorization, handler));
5360
+ return async (request) => bound(toRouteRequest(request));
5361
+ }
5362
+ function wrapWebLogin(kernel, handler, onThrottled) {
5363
+ return wrapWebThrottle(kernel, "login", handler, onThrottled);
5364
+ }
5365
+ function wrapWebRegister(kernel, handler, onThrottled) {
5366
+ return wrapWebThrottle(kernel, "register", handler, onThrottled);
5367
+ }
5368
+ function wrapWebThrottle(kernel, scope, handler, onThrottled) {
5369
+ const throttled = scope === "login" ? kernel.wrapLogin(handler) : kernel.wrapRegister(handler);
5370
+ return async (request) => {
5371
+ const response = await throttled(request);
5372
+ if (response.status === 429) {
5373
+ return onThrottled(request);
5348
5374
  }
5349
- return true;
5350
- }
5351
- return { generate, verify, secret };
5375
+ return response;
5376
+ };
5377
+ }
5378
+ function createRouteKernel(dependencies) {
5379
+ return createHttpKernel(dependencies);
5352
5380
  }
5353
5381
  // ../../src/bootstrap/web/server.ts
5382
+ function wrapRouteHandler2(handler) {
5383
+ return async (request) => {
5384
+ const response = await handler(request);
5385
+ return response ?? new Response("Not Found", { status: 404 });
5386
+ };
5387
+ }
5388
+ function convertAppRoutesToBunRoutes(routes) {
5389
+ const bunRoutes = {};
5390
+ for (const [path, handler] of Object.entries(routes)) {
5391
+ if (typeof handler === "function") {
5392
+ bunRoutes[path] = { GET: wrapRouteHandler2(handler) };
5393
+ continue;
5394
+ }
5395
+ if (handler && typeof handler === "object" && !Array.isArray(handler)) {
5396
+ const methods = {};
5397
+ for (const [method, methodHandler] of Object.entries(handler)) {
5398
+ if (typeof methodHandler !== "function") {
5399
+ continue;
5400
+ }
5401
+ methods[method.toUpperCase()] = wrapRouteHandler2(methodHandler);
5402
+ }
5403
+ if (Object.keys(methods).length > 0) {
5404
+ bunRoutes[path] = methods;
5405
+ }
5406
+ }
5407
+ }
5408
+ return bunRoutes;
5409
+ }
5354
5410
  function createWebServer(options) {
5355
5411
  const publicDir = options.publicDir ?? "./public";
5412
+ const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes) : undefined;
5356
5413
  return Bun.serve({
5357
5414
  port: options.port,
5415
+ ...bunRoutes ? { routes: bunRoutes } : {},
5358
5416
  async fetch(request) {
5359
5417
  await options.onRequest?.(request);
5360
5418
  const url = new URL(request.url);
@@ -5364,14 +5422,16 @@ function createWebServer(options) {
5364
5422
  return new Response(file);
5365
5423
  }
5366
5424
  }
5367
- const response = await options.handle(request);
5368
- return response ?? new Response("Not Found", { status: 404 });
5425
+ if (options.handle) {
5426
+ const response = await options.handle(request);
5427
+ return response ?? new Response("Not Found", { status: 404 });
5428
+ }
5429
+ return new Response("Not Found", { status: 404 });
5369
5430
  }
5370
5431
  });
5371
5432
  }
5372
5433
  // ../../src/bootstrap/web/session.ts
5373
- import { createHash, randomBytes as randomBytes3 } from "crypto";
5374
-
5434
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
5375
5435
  class CookieSessionStore {
5376
5436
  sql;
5377
5437
  secret;
@@ -5397,7 +5457,7 @@ class CookieSessionStore {
5397
5457
  return header.includes("Secure") ? header : `${header}; Secure`;
5398
5458
  }
5399
5459
  async create(user) {
5400
- const id = randomBytes3(32).toString("hex");
5460
+ const id = randomBytes2(32).toString("hex");
5401
5461
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
5402
5462
  await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
5403
5463
  id,
@@ -5410,8 +5470,8 @@ class CookieSessionStore {
5410
5470
  await this.sql.unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
5411
5471
  }
5412
5472
  async read(request) {
5413
- const cookie = this.parseCookie(request.headers.get("cookie") ?? "");
5414
- const raw = cookie[this.cookieName];
5473
+ const cookie = readRequestCookie(request, this.cookieName);
5474
+ const raw = cookie ?? null;
5415
5475
  if (!raw)
5416
5476
  return null;
5417
5477
  const [sessionId, signature] = raw.split(".");
@@ -5435,27 +5495,22 @@ class CookieSessionStore {
5435
5495
  sign(value) {
5436
5496
  return createHash("sha256").update(`${value}.${this.secret}`).digest("hex").slice(0, 32);
5437
5497
  }
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
5498
  }
5449
5499
  // ../../src/bootstrap/web/slug.ts
5450
5500
  function slugify(value) {
5451
5501
  return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
5452
5502
  }
5453
5503
  export {
5504
+ wrapWebRegister,
5505
+ wrapWebLogin,
5506
+ wrapSecuredRouteModelByKey,
5507
+ toRouteRequest,
5454
5508
  slugify,
5455
5509
  setActiveApplicationContext2 as setActiveApplicationContext,
5456
5510
  scheduleRunCommand,
5457
5511
  runProviderPhase,
5458
5512
  runDueScheduledTasks,
5513
+ routeParams,
5459
5514
  resolveService,
5460
5515
  resolveApplicationQueue2 as resolveApplicationQueue,
5461
5516
  prefixRouteMap,
@@ -5464,6 +5519,7 @@ export {
5464
5519
  getRequiredDependency,
5465
5520
  createWebServer,
5466
5521
  createWebRoutes,
5522
+ createRouteKernel,
5467
5523
  createHttpKernel,
5468
5524
  createCsrfProtection,
5469
5525
  createAppContext,
@@ -0,0 +1,15 @@
1
+ import type { AppDependencies } from "@getstrata/bootstrap/contracts";
2
+ declare class ScimController {
3
+ private readonly service;
4
+ constructor(dependencies: AppDependencies);
5
+ readonly serviceProviderConfig: () => Promise<Response>;
6
+ readonly listUsers: (request: Request) => Promise<Response>;
7
+ readonly createUser: (request: Request) => Promise<Response>;
8
+ readonly showUser: (request: Request) => Promise<Response>;
9
+ readonly patchUser: (request: Request) => Promise<Response>;
10
+ readonly deleteUser: (request: Request) => Promise<Response>;
11
+ readonly listGroups: (request: Request) => Promise<Response>;
12
+ readonly showGroup: (request: Request) => Promise<Response>;
13
+ readonly patchGroup: (request: Request) => Promise<Response>;
14
+ }
15
+ export default ScimController;
@@ -0,0 +1,10 @@
1
+ import { type EtagVersioned } from "@getstrata/core/http/etag";
2
+ interface ScimResponseOptions {
3
+ status?: number;
4
+ request?: Request;
5
+ etagSource?: EtagVersioned;
6
+ }
7
+ declare function scimResponse(data: unknown, options?: ScimResponseOptions): Response;
8
+ declare function assertScimIfMatch(request: Request, etagSource: EtagVersioned): void;
9
+ export type { ScimResponseOptions };
10
+ export { assertScimIfMatch, scimResponse };
@@ -0,0 +1,217 @@
1
+ import type { AppDependencies } from "@getstrata/bootstrap/contracts";
2
+ import OrganizationMemberRepository from "../organization/memberRepository";
3
+ import type UserRepository from "../user/repository";
4
+ interface ScimUserPayload {
5
+ userName?: string;
6
+ name?: {
7
+ formatted?: string;
8
+ };
9
+ active?: boolean;
10
+ emails?: Array<{
11
+ value: string;
12
+ primary?: boolean;
13
+ }>;
14
+ }
15
+ interface ScimPatchOperation {
16
+ op: string;
17
+ path?: string;
18
+ value?: unknown;
19
+ }
20
+ declare class ScimService {
21
+ private readonly users;
22
+ private readonly members;
23
+ constructor(users: UserRepository, members: OrganizationMemberRepository);
24
+ serviceProviderConfig(): {
25
+ schemas: "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"[];
26
+ patch: {
27
+ supported: boolean;
28
+ };
29
+ bulk: {
30
+ supported: boolean;
31
+ };
32
+ filter: {
33
+ supported: boolean;
34
+ };
35
+ changePassword: {
36
+ supported: boolean;
37
+ };
38
+ sort: {
39
+ supported: boolean;
40
+ };
41
+ etag: {
42
+ supported: boolean;
43
+ };
44
+ authenticationSchemes: {
45
+ type: string;
46
+ name: string;
47
+ description: string;
48
+ }[];
49
+ };
50
+ listUsers(startIndex?: number, count?: number): Promise<{
51
+ schemas: "urn:ietf:params:scim:api:messages:2.0:ListResponse"[];
52
+ totalResults: number;
53
+ startIndex: number;
54
+ itemsPerPage: number;
55
+ Resources: {
56
+ schemas: "urn:ietf:params:scim:schemas:core:2.0:User"[];
57
+ id: string;
58
+ userName: string;
59
+ name: {
60
+ formatted: string;
61
+ };
62
+ displayName: string;
63
+ active: boolean;
64
+ emails: {
65
+ value: string;
66
+ primary: boolean;
67
+ type: string;
68
+ }[];
69
+ roles: {
70
+ value: string;
71
+ primary: boolean;
72
+ }[];
73
+ meta: {
74
+ resourceType: string;
75
+ created: string | undefined;
76
+ lastModified: string | undefined;
77
+ };
78
+ }[];
79
+ }>;
80
+ getUser(id: number): Promise<{
81
+ schemas: "urn:ietf:params:scim:schemas:core:2.0:User"[];
82
+ id: string;
83
+ userName: string;
84
+ name: {
85
+ formatted: string;
86
+ };
87
+ displayName: string;
88
+ active: boolean;
89
+ emails: {
90
+ value: string;
91
+ primary: boolean;
92
+ type: string;
93
+ }[];
94
+ roles: {
95
+ value: string;
96
+ primary: boolean;
97
+ }[];
98
+ meta: {
99
+ resourceType: string;
100
+ created: string | undefined;
101
+ lastModified: string | undefined;
102
+ };
103
+ }>;
104
+ findUserRecord(id: number): Promise<import("../user/types").UserRecord>;
105
+ createUser(payload: ScimUserPayload): Promise<{
106
+ schemas: "urn:ietf:params:scim:schemas:core:2.0:User"[];
107
+ id: string;
108
+ userName: string;
109
+ name: {
110
+ formatted: string;
111
+ };
112
+ displayName: string;
113
+ active: boolean;
114
+ emails: {
115
+ value: string;
116
+ primary: boolean;
117
+ type: string;
118
+ }[];
119
+ roles: {
120
+ value: string;
121
+ primary: boolean;
122
+ }[];
123
+ meta: {
124
+ resourceType: string;
125
+ created: string | undefined;
126
+ lastModified: string | undefined;
127
+ };
128
+ }>;
129
+ patchUser(id: number, operations: ScimPatchOperation[]): Promise<{
130
+ schemas: "urn:ietf:params:scim:schemas:core:2.0:User"[];
131
+ id: string;
132
+ userName: string;
133
+ name: {
134
+ formatted: string;
135
+ };
136
+ displayName: string;
137
+ active: boolean;
138
+ emails: {
139
+ value: string;
140
+ primary: boolean;
141
+ type: string;
142
+ }[];
143
+ roles: {
144
+ value: string;
145
+ primary: boolean;
146
+ }[];
147
+ meta: {
148
+ resourceType: string;
149
+ created: string | undefined;
150
+ lastModified: string | undefined;
151
+ };
152
+ }>;
153
+ deleteUser(id: number): Promise<{
154
+ id: number;
155
+ updated_at: Date;
156
+ }>;
157
+ listGroups(startIndex?: number, count?: number): Promise<{
158
+ schemas: "urn:ietf:params:scim:api:messages:2.0:ListResponse"[];
159
+ totalResults: number;
160
+ startIndex: number;
161
+ itemsPerPage: number;
162
+ Resources: {
163
+ schemas: "urn:ietf:params:scim:schemas:core:2.0:Group"[];
164
+ id: string;
165
+ displayName: string;
166
+ externalId: string;
167
+ members: {
168
+ value: string;
169
+ display: string;
170
+ }[];
171
+ meta: {
172
+ resourceType: string;
173
+ lastModified: string;
174
+ };
175
+ }[];
176
+ }>;
177
+ getGroup(id: number): Promise<{
178
+ schemas: "urn:ietf:params:scim:schemas:core:2.0:Group"[];
179
+ id: string;
180
+ displayName: string;
181
+ externalId: string;
182
+ members: {
183
+ value: string;
184
+ display: string;
185
+ }[];
186
+ meta: {
187
+ resourceType: string;
188
+ lastModified: string;
189
+ };
190
+ }>;
191
+ findOrganizationRecord(id: number): Promise<{
192
+ id: number;
193
+ name: string;
194
+ slug: string;
195
+ updated_at: Date;
196
+ }>;
197
+ patchGroup(id: number, operations: ScimPatchOperation[]): Promise<{
198
+ schemas: "urn:ietf:params:scim:schemas:core:2.0:Group"[];
199
+ id: string;
200
+ displayName: string;
201
+ externalId: string;
202
+ members: {
203
+ value: string;
204
+ display: string;
205
+ }[];
206
+ meta: {
207
+ resourceType: string;
208
+ lastModified: string;
209
+ };
210
+ }>;
211
+ private toScimGroup;
212
+ private toScimUser;
213
+ }
214
+ declare function createScimService(dependencies: AppDependencies): ScimService;
215
+ export default ScimService;
216
+ export type { ScimPatchOperation, ScimUserPayload };
217
+ export { createScimService };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/bootstrap",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,42 +16,42 @@
16
16
  "default": "./dist/index.js"
17
17
  },
18
18
  "./applicationRegistry": {
19
- "types": "./dist/entries/applicationRegistry.d.ts",
19
+ "types": "./dist/bootstrap/applicationRegistry.d.ts",
20
20
  "import": "./dist/entries/applicationRegistry.js",
21
21
  "default": "./dist/entries/applicationRegistry.js"
22
22
  },
23
23
  "./config": {
24
- "types": "./dist/entries/config.d.ts",
24
+ "types": "./dist/bootstrap/config.d.ts",
25
25
  "import": "./dist/entries/config.js",
26
26
  "default": "./dist/entries/config.js"
27
27
  },
28
28
  "./context": {
29
- "types": "./dist/entries/context.d.ts",
29
+ "types": "./dist/bootstrap/context.d.ts",
30
30
  "import": "./dist/entries/context.js",
31
31
  "default": "./dist/entries/context.js"
32
32
  },
33
33
  "./contracts": {
34
- "types": "./dist/entries/contracts.d.ts",
34
+ "types": "./dist/bootstrap/contracts.d.ts",
35
35
  "import": "./dist/entries/contracts.js",
36
36
  "default": "./dist/entries/contracts.js"
37
37
  },
38
38
  "./createWebRoutes": {
39
- "types": "./dist/entries/createWebRoutes.d.ts",
39
+ "types": "./dist/bootstrap/createWebRoutes.d.ts",
40
40
  "import": "./dist/entries/createWebRoutes.js",
41
41
  "default": "./dist/entries/createWebRoutes.js"
42
42
  },
43
43
  "./httpKernel": {
44
- "types": "./dist/entries/httpKernel.d.ts",
44
+ "types": "./dist/bootstrap/httpKernel.d.ts",
45
45
  "import": "./dist/entries/httpKernel.js",
46
46
  "default": "./dist/entries/httpKernel.js"
47
47
  },
48
48
  "./providers": {
49
- "types": "./dist/entries/providers.d.ts",
49
+ "types": "./dist/bootstrap/providers/index.d.ts",
50
50
  "import": "./dist/entries/providers.js",
51
51
  "default": "./dist/entries/providers.js"
52
52
  },
53
53
  "./providers/view": {
54
- "types": "./dist/entries/providers/view.d.ts",
54
+ "types": "./dist/bootstrap/providers/view/index.d.ts",
55
55
  "import": "./dist/entries/providers/view.js",
56
56
  "default": "./dist/entries/providers/view.js"
57
57
  }
@@ -73,7 +73,7 @@
73
73
  "access": "public"
74
74
  },
75
75
  "peerDependencies": {
76
- "@getstrata/core": "^0.5.5",
76
+ "@getstrata/core": "^0.5.9",
77
77
  "typescript": "^5.9.0"
78
78
  }
79
79
  }