@getstrata/bootstrap 0.2.1 → 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.
- package/dist/bootstrap/public-api.d.ts +1 -1
- package/dist/bootstrap/web/forms.d.ts +1 -5
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/server.d.ts +7 -1
- package/dist/bootstrap/web/session.d.ts +0 -1
- package/dist/config/uploads.d.ts +5 -0
- package/dist/core/auth/membershipMiddleware.d.ts +1 -1
- package/dist/core/cache/tags.d.ts +11 -0
- package/dist/core/crypto/nonCryptographicHash.d.ts +2 -0
- package/dist/core/database/bindConnection.d.ts +3 -0
- package/dist/core/database/migrations/advisoryLock.d.ts +6 -0
- package/dist/core/database/migrations/runner.d.ts +25 -0
- package/dist/core/database/seeders/runner.d.ts +6 -0
- package/dist/core/database/seeders/types.d.ts +8 -0
- package/dist/core/facades/index.d.ts +10 -0
- package/dist/core/http/conditionalResponse.d.ts +2 -0
- package/dist/core/http/cookies.d.ts +4 -0
- package/dist/core/http/csrfProtection.d.ts +12 -0
- package/dist/core/http/etag.d.ts +18 -0
- package/dist/core/http/formRequest.d.ts +10 -0
- package/dist/core/http/index.d.ts +27 -0
- package/dist/core/http/pagination.d.ts +11 -0
- package/dist/core/http/parseFormBody.d.ts +6 -0
- package/dist/core/http/parseMultipartUpload.d.ts +10 -0
- package/dist/core/http/resources.d.ts +7 -0
- package/dist/core/http/route.d.ts +6 -0
- package/dist/core/http/routeModelBinding.d.ts +3 -0
- package/dist/core/http/securedRouteModelBinding.d.ts +11 -0
- package/dist/core/http/validation.d.ts +23 -0
- package/dist/core/http/webErrorResponse.d.ts +5 -0
- package/dist/core/http/webFormRequest.d.ts +7 -0
- package/dist/core/lifecycle/gracefulShutdown.d.ts +6 -0
- package/dist/core/mail/mailer.d.ts +36 -0
- package/dist/core/storage/storage.d.ts +41 -0
- package/dist/core/validation/rules.d.ts +17 -0
- package/dist/framework/public-api.d.ts +74 -0
- package/dist/index.js +130 -122
- package/package.json +2 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @getstrata/bootstrap — application shell for Strata sibling apps.
|
|
3
3
|
*/
|
|
4
|
+
export { resolveApplicationQueue, setActiveApplicationContext } from "@getstrata/core";
|
|
4
5
|
export { scheduleRunCommand } from "../cli/commands/scheduleRun.ts";
|
|
5
6
|
export type { ScheduledTask } from "../core/scheduler/schedule.ts";
|
|
6
7
|
export { appSchedule, runDueScheduledTasks, Schedule } from "../core/scheduler/schedule.ts";
|
|
7
|
-
export { resolveApplicationQueue, setActiveApplicationContext } from "./applicationRegistry.ts";
|
|
8
8
|
export { APP_PORT_CONFIG_KEY, CORE_AUTH_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, DATABASE_URL_CONFIG_KEY, DEFAULT_APP_PORT, REDIS_URL_CONFIG_KEY, } from "./config.ts";
|
|
9
9
|
export { collectProviders, createAppContext, runProviderPhase } from "./context.ts";
|
|
10
10
|
export type { AppContext, AppDependencies, AppModule, AppRouteMap, CachedJson, ConfigStore, ModuleRouteContext, MutableAppDependencies, ProviderContext, ServiceFactory, ServiceProvider, } from "./contracts.ts";
|
|
@@ -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
|
|
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 };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
2
|
+
declare const ALLOWED_UPLOAD_MIME_TYPES: Set<string>;
|
|
3
|
+
declare function resolveMaxUploadBytes(): number;
|
|
4
|
+
declare function isAllowedMimeType(mimeType: string): boolean;
|
|
5
|
+
export { ALLOWED_UPLOAD_MIME_TYPES, DEFAULT_MAX_UPLOAD_BYTES, isAllowedMimeType, resolveMaxUploadBytes, };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare function createMembershipMiddleware(): import("../http
|
|
1
|
+
declare function createMembershipMiddleware(): import("../http").Middleware;
|
|
2
2
|
export { createMembershipMiddleware };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare const CACHE_TAGS: {
|
|
2
|
+
readonly organizations: "organizations";
|
|
3
|
+
readonly projects: "projects";
|
|
4
|
+
readonly tasks: "tasks";
|
|
5
|
+
readonly comments: "comments";
|
|
6
|
+
readonly attachments: "attachments";
|
|
7
|
+
readonly reports: "reports";
|
|
8
|
+
};
|
|
9
|
+
type CacheTag = (typeof CACHE_TAGS)[keyof typeof CACHE_TAGS];
|
|
10
|
+
export type { CacheTag };
|
|
11
|
+
export { CACHE_TAGS };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare const MIGRATION_LOCK_KEY = 42424242;
|
|
2
|
+
type LockableDatabase = {
|
|
3
|
+
unsafe<T>(query: string, params?: readonly unknown[]): Promise<T[]>;
|
|
4
|
+
};
|
|
5
|
+
declare function withMigrationLock<T>(db: LockableDatabase, callback: () => Promise<T>, lockKey?: number): Promise<T>;
|
|
6
|
+
export { MIGRATION_LOCK_KEY, withMigrationLock };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Migration, MigrationDatabase, MigrationStatus } from "./types.ts";
|
|
2
|
+
type AppliedMigrationRow = {
|
|
3
|
+
name: string;
|
|
4
|
+
batch: number | string;
|
|
5
|
+
};
|
|
6
|
+
declare function ensureMigrationsTable(db: MigrationDatabase): Promise<void>;
|
|
7
|
+
declare function getAppliedMigrations(db: MigrationDatabase): Promise<AppliedMigrationRow[]>;
|
|
8
|
+
declare function loadMigrationsFromDirectory(directory: string): Promise<Migration[]>;
|
|
9
|
+
declare function getMigrationStatus(db: MigrationDatabase, migrations: Migration[]): Promise<MigrationStatus[]>;
|
|
10
|
+
declare function runPendingMigrations(db: MigrationDatabase, migrations: Migration[], options?: {
|
|
11
|
+
onMigration?: (name: string) => void;
|
|
12
|
+
}): Promise<number>;
|
|
13
|
+
declare function migrateDatabase(db: MigrationDatabase, migrations: Migration[], options?: {
|
|
14
|
+
advisoryLock?: boolean;
|
|
15
|
+
onMigration?: (name: string) => void;
|
|
16
|
+
}): Promise<number>;
|
|
17
|
+
declare function rollbackDatabase(db: MigrationDatabase, migrations: Migration[], options?: {
|
|
18
|
+
onMigration?: (name: string) => void;
|
|
19
|
+
}): Promise<number>;
|
|
20
|
+
declare function freshDatabase(db: MigrationDatabase, migrations: Migration[], options?: {
|
|
21
|
+
advisoryLock?: boolean;
|
|
22
|
+
onMigration?: (name: string) => void;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
export { withMigrationLock } from "./advisoryLock.ts";
|
|
25
|
+
export { ensureMigrationsTable, freshDatabase, getAppliedMigrations, getMigrationStatus, loadMigrationsFromDirectory, migrateDatabase, rollbackDatabase, runPendingMigrations, };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Seeder, SeederDatabase } from "./types.ts";
|
|
2
|
+
declare function loadSeedersFromDirectory(directory: string): Promise<Seeder[]>;
|
|
3
|
+
declare function runSeedersFromDirectory(directory: string, db: SeederDatabase, options?: {
|
|
4
|
+
onSeeder?: (name: string) => void;
|
|
5
|
+
}): Promise<number>;
|
|
6
|
+
export { loadSeedersFromDirectory, runSeedersFromDirectory };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare function cache(): import("../../types/services").CacheLike;
|
|
2
|
+
declare function auth(): import("../auth/guard").AuthManager;
|
|
3
|
+
declare function policyGate(): import("../auth/policy").PolicyGate;
|
|
4
|
+
declare function queue(): import("../queue").Queue;
|
|
5
|
+
declare function events(): import("../events").EventBus;
|
|
6
|
+
declare function config<T>(key: string): T | undefined;
|
|
7
|
+
declare function log(): import("../logging/logger").Logger;
|
|
8
|
+
declare function mail(): import("../mail/mailer").Mailer;
|
|
9
|
+
declare function storageFacade(): import("../storage/storage").StorageManager;
|
|
10
|
+
export { auth, cache, config, events, log, mail, policyGate, queue, storageFacade as storage };
|
|
@@ -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,18 @@
|
|
|
1
|
+
interface EtagVersioned {
|
|
2
|
+
id?: number | string;
|
|
3
|
+
updated_at?: Date | string | null;
|
|
4
|
+
created_at?: Date | string | null;
|
|
5
|
+
}
|
|
6
|
+
declare function isEtagEnabled(): boolean;
|
|
7
|
+
declare function computeEtagFromJson(data: unknown): string;
|
|
8
|
+
declare function etagFromResource(resource: EtagVersioned): string;
|
|
9
|
+
declare function etagValuesMatch(left: string, right: string): boolean;
|
|
10
|
+
declare function ifNoneMatchSatisfied(request: Request, etag: string): boolean;
|
|
11
|
+
declare function ifMatchSatisfied(request: Request, etag: string): boolean;
|
|
12
|
+
declare function assertIfMatch(request: Request, etag: string, options?: {
|
|
13
|
+
required?: boolean;
|
|
14
|
+
}): void;
|
|
15
|
+
declare function notModifiedResponse(etag: string): Response;
|
|
16
|
+
declare function applyConditionalGet(request: Request, response: Response, etag: string): Response;
|
|
17
|
+
export type { EtagVersioned };
|
|
18
|
+
export { applyConditionalGet, assertIfMatch, computeEtagFromJson, etagFromResource, etagValuesMatch, ifMatchSatisfied, ifNoneMatchSatisfied, isEtagEnabled, notModifiedResponse, };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare abstract class FormRequest<TOutput> {
|
|
2
|
+
authorize(_request: Request): boolean | Promise<boolean>;
|
|
3
|
+
protected abstract parse(payload: unknown): TOutput;
|
|
4
|
+
validate(request: Request): Promise<TOutput>;
|
|
5
|
+
}
|
|
6
|
+
declare abstract class QueryFormRequest<TOutput> {
|
|
7
|
+
validate(request?: Request): TOutput;
|
|
8
|
+
protected abstract parseQuery(request?: Request): TOutput;
|
|
9
|
+
}
|
|
10
|
+
export { FormRequest, QueryFormRequest };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PayloadTooLargeError, PreconditionFailedError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "../errors/http";
|
|
2
|
+
export type { PaginatedResult, PaginationMeta } from "../pagination";
|
|
3
|
+
export { createAuthMiddleware } from "./authMiddleware";
|
|
4
|
+
export { createAuthorizeMiddleware } from "./authorizeMiddleware";
|
|
5
|
+
export { conditionalJsonResponse } from "./conditionalResponse";
|
|
6
|
+
export type { EtagVersioned } from "./etag";
|
|
7
|
+
export { applyConditionalGet, assertIfMatch, computeEtagFromJson, etagFromResource, etagValuesMatch, ifMatchSatisfied, ifNoneMatchSatisfied, isEtagEnabled, notModifiedResponse, } from "./etag";
|
|
8
|
+
export { FormRequest, QueryFormRequest } from "./formRequest";
|
|
9
|
+
export type { Middleware, RouteHandler } from "./middleware";
|
|
10
|
+
export { applyMiddlewareToRoutes, composeMiddleware, requestIdMiddleware, wrapRouteHandler, } from "./middleware";
|
|
11
|
+
export { buildPaginationMeta, DEFAULT_PER_PAGE, MAX_PER_PAGE, paginatedResponse, parsePaginationQuery, } from "./pagination";
|
|
12
|
+
export { createRequireAuthMiddleware } from "./requireAuthMiddleware";
|
|
13
|
+
export { serializeDate, toPaginatedResourceCollection, toResourceCollection } from "./resources";
|
|
14
|
+
export { withMiddleware } from "./routeMiddleware";
|
|
15
|
+
export { bindRouteModel } from "./routeModelBinding";
|
|
16
|
+
export { securedBindRouteModel, securedBindRouteModelByKey, } from "./securedRouteModelBinding";
|
|
17
|
+
export { buildRequestCacheKey, expectObject, getQueryParams, parseJsonBody, parseOptionalBooleanQueryParam, parseOptionalEnumQueryParam, parseOptionalPositiveIntQueryParam, parsePositiveIntParam, readOptionalEnum, readOptionalPositiveInt, readOptionalString, readRequiredEnum, readRequiredPositiveInt, readRequiredString, } from "./validation";
|
|
18
|
+
declare function jsonResponse(data: unknown, init?: ResponseInit): Response;
|
|
19
|
+
declare function createdResponse(data: unknown, init?: ResponseInit): Response;
|
|
20
|
+
declare function noContentResponse(): Response;
|
|
21
|
+
declare function errorResponse(error: unknown): Response;
|
|
22
|
+
declare function withErrorHandling<TArgs extends unknown[]>(handler: (...args: TArgs) => Response | Promise<Response>): (...args: TArgs) => Promise<Response>;
|
|
23
|
+
export type { ParsedUpload } from "./parseMultipartUpload";
|
|
24
|
+
export { parseMultipartUpload, sanitizeUploadFileName } from "./parseMultipartUpload";
|
|
25
|
+
export type { RouteRequest } from "./route";
|
|
26
|
+
export { getRouteParams } from "./route";
|
|
27
|
+
export { createdResponse, errorResponse, jsonResponse, noContentResponse, withErrorHandling };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { buildPaginationMeta, type PaginatedResult, type PaginationMeta } from "../pagination";
|
|
2
|
+
declare const DEFAULT_PER_PAGE = 15;
|
|
3
|
+
declare const MAX_PER_PAGE = 100;
|
|
4
|
+
interface PaginationQuery {
|
|
5
|
+
page: number;
|
|
6
|
+
perPage: number;
|
|
7
|
+
}
|
|
8
|
+
declare function parsePaginationQuery(request?: Request): PaginationQuery;
|
|
9
|
+
declare function paginatedResponse<T>(data: T[], meta: PaginationMeta, init?: ResponseInit): Response;
|
|
10
|
+
export type { PaginatedResult, PaginationMeta, PaginationQuery };
|
|
11
|
+
export { buildPaginationMeta, DEFAULT_PER_PAGE, MAX_PER_PAGE, paginatedResponse, parsePaginationQuery, };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface FormDataLike {
|
|
2
|
+
entries(): Iterable<[string, unknown]>;
|
|
3
|
+
}
|
|
4
|
+
declare function parseFormBody(request: Request): Promise<Record<string, string>>;
|
|
5
|
+
declare function formDataToRecord(formData: FormDataLike): Record<string, string>;
|
|
6
|
+
export { formDataToRecord, parseFormBody };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface ParsedUpload {
|
|
2
|
+
fileName: string;
|
|
3
|
+
mimeType: string;
|
|
4
|
+
size: number;
|
|
5
|
+
contents: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
declare function sanitizeUploadFileName(name: string): string;
|
|
8
|
+
declare function parseMultipartUpload(request: Request, fieldName?: string): Promise<ParsedUpload>;
|
|
9
|
+
export type { ParsedUpload };
|
|
10
|
+
export { parseMultipartUpload, sanitizeUploadFileName };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare function serializeDate(value: Date | string): string;
|
|
2
|
+
declare function toResourceCollection<TInput, TOutput>(items: readonly TInput[], transformer: (item: TInput) => TOutput): TOutput[];
|
|
3
|
+
declare function toPaginatedResourceCollection<TInput, TOutput, TMeta extends object>(items: readonly TInput[], meta: TMeta, transformer: (item: TInput) => TOutput): {
|
|
4
|
+
data: TOutput[];
|
|
5
|
+
meta: TMeta;
|
|
6
|
+
};
|
|
7
|
+
export { serializeDate, toPaginatedResourceCollection, toResourceCollection };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type RouteRequest<TParams extends Record<string, string>> = Request & {
|
|
2
|
+
params: TParams;
|
|
3
|
+
};
|
|
4
|
+
declare function getRouteParams<TParams extends Record<string, string>>(request: RouteRequest<TParams>): TParams;
|
|
5
|
+
export type { RouteRequest };
|
|
6
|
+
export { getRouteParams };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RouteRequest } from "./route";
|
|
2
|
+
declare function bindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
|
|
3
|
+
export { bindRouteModel };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Policy } from "../auth/policy";
|
|
2
|
+
import type { RouteRequest } from "./route";
|
|
3
|
+
interface RouteModelAuthorization {
|
|
4
|
+
resource: string;
|
|
5
|
+
action: keyof Policy;
|
|
6
|
+
requireIfMatch?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare function securedBindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
|
|
9
|
+
declare function securedBindRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
|
|
10
|
+
export type { RouteModelAuthorization };
|
|
11
|
+
export { securedBindRouteModel, securedBindRouteModelByKey };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
declare function buildRequestCacheKey(fallbackPath: string, request?: Request): string;
|
|
2
|
+
declare function getQueryParams(request?: Request): URLSearchParams;
|
|
3
|
+
declare function parseOptionalPositiveIntQueryParam(params: URLSearchParams, name: string): number | undefined;
|
|
4
|
+
declare function parseOptionalBooleanQueryParam(params: URLSearchParams, name: string): boolean | undefined;
|
|
5
|
+
declare function parseOptionalEnumQueryParam<TValue extends string>(params: URLSearchParams, name: string, allowedValues: readonly TValue[]): TValue | undefined;
|
|
6
|
+
declare function expectObject(value: unknown, label?: string): Record<string, unknown>;
|
|
7
|
+
declare function parseJsonBody<TValue>(request: Request, validator: (payload: unknown) => TValue): Promise<TValue>;
|
|
8
|
+
declare function readRequiredString(payload: Record<string, unknown>, field: string, options?: {
|
|
9
|
+
minLength?: number;
|
|
10
|
+
maxLength?: number;
|
|
11
|
+
pattern?: RegExp;
|
|
12
|
+
}): string;
|
|
13
|
+
declare function readOptionalString(payload: Record<string, unknown>, field: string, options?: {
|
|
14
|
+
minLength?: number;
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
pattern?: RegExp;
|
|
17
|
+
}): string | undefined;
|
|
18
|
+
declare function readRequiredEnum<TValue extends string>(payload: Record<string, unknown>, field: string, allowedValues: readonly TValue[]): TValue;
|
|
19
|
+
declare function readOptionalEnum<TValue extends string>(payload: Record<string, unknown>, field: string, allowedValues: readonly TValue[]): TValue | undefined;
|
|
20
|
+
declare function readRequiredPositiveInt(payload: Record<string, unknown>, field: string): number;
|
|
21
|
+
declare function readOptionalPositiveInt(payload: Record<string, unknown>, field: string): number | undefined;
|
|
22
|
+
declare function parsePositiveIntParam(value: string, name?: string): number;
|
|
23
|
+
export { buildRequestCacheKey, expectObject, getQueryParams, parseJsonBody, parseOptionalBooleanQueryParam, parseOptionalEnumQueryParam, parseOptionalPositiveIntQueryParam, parsePositiveIntParam, readOptionalEnum, readOptionalPositiveInt, readOptionalString, readRequiredEnum, readRequiredPositiveInt, readRequiredString, };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type FieldErrors = Record<string, string[]>;
|
|
2
|
+
declare function normalizeFieldErrors(details: unknown): FieldErrors;
|
|
3
|
+
declare function webErrorResponse(error: unknown, request?: Request): Response | null;
|
|
4
|
+
export type { FieldErrors };
|
|
5
|
+
export { normalizeFieldErrors, webErrorResponse };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare abstract class WebFormRequest<TOutput> {
|
|
2
|
+
authorize(_request: Request): boolean | Promise<boolean>;
|
|
3
|
+
protected abstract parse(payload: unknown): TOutput;
|
|
4
|
+
validatePayload(payload: unknown): TOutput;
|
|
5
|
+
validate(request: Request): Promise<TOutput>;
|
|
6
|
+
}
|
|
7
|
+
export { WebFormRequest };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type ShutdownHandler = () => void | Promise<void>;
|
|
2
|
+
declare function registerShutdownHandler(name: string, handler: ShutdownHandler): () => void;
|
|
3
|
+
declare function runGracefulShutdown(signal: string): Promise<void>;
|
|
4
|
+
declare function installGracefulShutdownSignals(signals?: NodeJS.Signals[]): void;
|
|
5
|
+
declare function resetGracefulShutdownForTests(): void;
|
|
6
|
+
export { installGracefulShutdownSignals, registerShutdownHandler, resetGracefulShutdownForTests, runGracefulShutdown, };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface MailMessage {
|
|
2
|
+
to: string;
|
|
3
|
+
subject: string;
|
|
4
|
+
body: string;
|
|
5
|
+
}
|
|
6
|
+
interface MailDriver {
|
|
7
|
+
send(message: MailMessage): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
interface SmtpConfig {
|
|
10
|
+
host: string;
|
|
11
|
+
port: number;
|
|
12
|
+
username?: string;
|
|
13
|
+
password?: string;
|
|
14
|
+
from: string;
|
|
15
|
+
secure: boolean;
|
|
16
|
+
}
|
|
17
|
+
type SmtpTransport = (config: SmtpConfig, message: MailMessage) => Promise<void>;
|
|
18
|
+
declare function resolveSmtpConfig(): SmtpConfig;
|
|
19
|
+
declare class LogMailDriver implements MailDriver {
|
|
20
|
+
send(message: MailMessage): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
declare class SmtpMailDriver implements MailDriver {
|
|
23
|
+
private readonly config;
|
|
24
|
+
private readonly transport;
|
|
25
|
+
constructor(config: SmtpConfig, transport?: SmtpTransport);
|
|
26
|
+
send(message: MailMessage): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
declare class Mailer {
|
|
29
|
+
private readonly driver;
|
|
30
|
+
constructor(driver: MailDriver);
|
|
31
|
+
send(message: MailMessage): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
declare function createMailDriver(): MailDriver;
|
|
34
|
+
declare function mailer(): Mailer;
|
|
35
|
+
export type { MailDriver, MailMessage, SmtpConfig, SmtpTransport };
|
|
36
|
+
export { createMailDriver, LogMailDriver, Mailer, mailer, resolveSmtpConfig, SmtpMailDriver };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { S3Client } from "bun";
|
|
2
|
+
interface StorageDriver {
|
|
3
|
+
put(path: string, contents: string | Uint8Array): Promise<string>;
|
|
4
|
+
get(path: string): Promise<Uint8Array | null>;
|
|
5
|
+
delete(path: string): Promise<boolean>;
|
|
6
|
+
}
|
|
7
|
+
interface S3StorageConfig {
|
|
8
|
+
accessKeyId: string;
|
|
9
|
+
secretAccessKey: string;
|
|
10
|
+
bucket: string;
|
|
11
|
+
region?: string;
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
}
|
|
14
|
+
declare class LocalStorageDriver implements StorageDriver {
|
|
15
|
+
private readonly rootDirectory;
|
|
16
|
+
constructor(rootDirectory: string);
|
|
17
|
+
private resolvePath;
|
|
18
|
+
put(path: string, contents: string | Uint8Array): Promise<string>;
|
|
19
|
+
get(path: string): Promise<Uint8Array | null>;
|
|
20
|
+
delete(path: string): Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
declare class S3StorageDriver implements StorageDriver {
|
|
23
|
+
private readonly client;
|
|
24
|
+
constructor(client: S3Client);
|
|
25
|
+
put(path: string, contents: string | Uint8Array): Promise<string>;
|
|
26
|
+
get(path: string): Promise<Uint8Array | null>;
|
|
27
|
+
delete(path: string): Promise<boolean>;
|
|
28
|
+
}
|
|
29
|
+
declare class StorageManager {
|
|
30
|
+
private readonly driver;
|
|
31
|
+
constructor(driver: StorageDriver);
|
|
32
|
+
put(path: string, contents: string | Uint8Array): Promise<string>;
|
|
33
|
+
get(path: string): Promise<Uint8Array | null>;
|
|
34
|
+
delete(path: string): Promise<boolean>;
|
|
35
|
+
}
|
|
36
|
+
declare function resolveS3Config(): S3StorageConfig;
|
|
37
|
+
declare function createS3Client(config?: S3StorageConfig): S3Client;
|
|
38
|
+
declare function createStorageDriver(): StorageDriver;
|
|
39
|
+
declare function storage(): StorageManager;
|
|
40
|
+
export type { S3StorageConfig, StorageDriver };
|
|
41
|
+
export { createS3Client, createStorageDriver, LocalStorageDriver, resolveS3Config, S3StorageDriver, StorageManager, storage, };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type ValidationRule = (field: string, value: unknown, payload: Record<string, unknown>) => string | undefined;
|
|
2
|
+
type ValidationSchema = Record<string, ValidationRule[]>;
|
|
3
|
+
declare function required(): ValidationRule;
|
|
4
|
+
declare function stringRule(): ValidationRule;
|
|
5
|
+
declare function minLength(minimum: number): ValidationRule;
|
|
6
|
+
declare function maxLength(maximum: number): ValidationRule;
|
|
7
|
+
declare function pattern(expression: RegExp): ValidationRule;
|
|
8
|
+
declare function enumRule<TValue extends string>(allowedValues: readonly TValue[]): ValidationRule;
|
|
9
|
+
declare function optional(): ValidationRule;
|
|
10
|
+
declare function integerRule(): ValidationRule;
|
|
11
|
+
declare function emailRule(): ValidationRule;
|
|
12
|
+
declare function confirmed(fieldName: string): ValidationRule;
|
|
13
|
+
declare function positiveIntegerRule(): ValidationRule;
|
|
14
|
+
declare function integerRange(minimum: number, maximum: number): ValidationRule;
|
|
15
|
+
declare function validateObject(payload: unknown, schema: ValidationSchema): Record<string, unknown>;
|
|
16
|
+
export type { ValidationRule, ValidationSchema };
|
|
17
|
+
export { confirmed, emailRule, enumRule, integerRange, integerRule, maxLength, minLength, optional, pattern, positiveIntegerRule, required, stringRule, validateObject, };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable framework surface for application modules and future package extraction.
|
|
3
|
+
* Import from `@getstrata/core` (workspace) or `src/framework/public-api`.
|
|
4
|
+
*/
|
|
5
|
+
export { resolveApplicationAuth, resolveApplicationCache, resolveApplicationConfig, resolveApplicationDependencies, resolveApplicationLogger, resolveApplicationPolicyGate, resolveApplicationQueue, setActiveApplicationContext, } from "../bootstrap/applicationRegistry.ts";
|
|
6
|
+
export type { ServiceProvider } from "../bootstrap/contracts.ts";
|
|
7
|
+
export { ConfigStore, resolveService, ServiceContainer, } from "../bootstrap/contracts.ts";
|
|
8
|
+
export type { AbilityChecker } from "../core/auth/abilityChecker.ts";
|
|
9
|
+
export type { AuthUser } from "../core/auth/authContext.ts";
|
|
10
|
+
export { currentAuthUser, runWithAuthUser } from "../core/auth/authContext.ts";
|
|
11
|
+
export { Policy, PolicyGate } from "../core/auth/policy.ts";
|
|
12
|
+
export { default as CacheRepository } from "../core/cache/repository.ts";
|
|
13
|
+
export { CACHE_TAGS } from "../core/cache/tags.ts";
|
|
14
|
+
export type { DatabaseConnection } from "../core/database/baseRepository.ts";
|
|
15
|
+
export { default as BaseRepository } from "../core/database/baseRepository.ts";
|
|
16
|
+
export { bindDatabaseConnection } from "../core/database/bindConnection.ts";
|
|
17
|
+
export { createDatabaseConnection } from "../core/database/connection.ts";
|
|
18
|
+
export { getActiveDatabaseConnection, runWithDatabaseConnection, } from "../core/database/connectionContext.ts";
|
|
19
|
+
export { withMigrationLock } from "../core/database/migrations/advisoryLock.ts";
|
|
20
|
+
export { freshDatabase, getMigrationStatus, loadMigrationsFromDirectory, migrateDatabase, rollbackDatabase, } from "../core/database/migrations/runner.ts";
|
|
21
|
+
export type { Migration, MigrationDatabase, MigrationStatus, } from "../core/database/migrations/types.ts";
|
|
22
|
+
export type { CastType, GlobalScopeFn, ModelConstructor } from "../core/database/model.ts";
|
|
23
|
+
export { applyCasts, dehydrateValue, filterMassAssignable, hydrateValue, Model, registerModelRepository, } from "../core/database/model.ts";
|
|
24
|
+
export type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation, } from "../core/database/relationships.ts";
|
|
25
|
+
export { belongsTo, belongsToMany, hasMany, hasOne, indexBelongsToManyRelation, indexBelongsToRelation, indexHasManyRelation, indexHasOneRelation, } from "../core/database/relationships.ts";
|
|
26
|
+
export { RepositoryQuery } from "../core/database/repositoryQuery.ts";
|
|
27
|
+
export type { BlueprintAction, BlueprintCallback, ColumnKind, DatabaseDriver, ForeignKeyOptions, Grammar, IndexDefinition, IndexKind, ResolveDatabaseDriverOptions, SchemaBuilder, } from "../core/database/schema/index.ts";
|
|
28
|
+
export { Blueprint, ColumnDefinition, compileBlueprint, createSchemaBuilder, ForeignIdColumnDefinition, grammarForDriver, inferReferencedTable, MySqlGrammar, PostgresGrammar, resolveDatabaseDriver, Schema, SqliteGrammar, UnsupportedSchemaFeatureError, } from "../core/database/schema/index.ts";
|
|
29
|
+
export { loadSeedersFromDirectory, runSeedersFromDirectory, } from "../core/database/seeders/runner.ts";
|
|
30
|
+
export type { Seeder, SeederDatabase } from "../core/database/seeders/types.ts";
|
|
31
|
+
export { defineTable } from "../core/database/table.ts";
|
|
32
|
+
export { runInTransaction } from "../core/database/transaction.ts";
|
|
33
|
+
export type { QueryJoin, QueryJoinOn, QueryOptions, QueryOrder, QuerySelectItem, QueryWhere, } from "../core/database/types.ts";
|
|
34
|
+
export type { WhereNode } from "../core/database/whereBuilder.ts";
|
|
35
|
+
export { WhereBuilder } from "../core/database/whereBuilder.ts";
|
|
36
|
+
export { BadRequestError, ConflictError, ForbiddenError, NotFoundError, PreconditionFailedError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "../core/errors/http.ts";
|
|
37
|
+
export { EventBus } from "../core/events/eventBus.ts";
|
|
38
|
+
export { auth, cache, config, events, log, mail, policyGate, queue, storage, } from "../core/facades/index.ts";
|
|
39
|
+
export { createBodySizeLimitMiddleware } from "../core/http/bodySizeLimitMiddleware.ts";
|
|
40
|
+
export { readBunRequestCookie, readRequestCookie } from "../core/http/cookies.ts";
|
|
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";
|
|
44
|
+
export { assertIfMatch, etagFromResource, isEtagEnabled, } from "../core/http/etag.ts";
|
|
45
|
+
export { FormRequest } from "../core/http/formRequest.ts";
|
|
46
|
+
export { applyMiddlewareToRoutes, composeMiddleware, createAuthMiddleware, createAuthorizeMiddleware, createdResponse, createRequireAuthMiddleware, jsonResponse, noContentResponse, paginatedResponse, parsePaginationQuery, securedBindRouteModel, securedBindRouteModelByKey, withErrorHandling, withMiddleware, } from "../core/http/index.ts";
|
|
47
|
+
export { createLoginThrottleMiddleware } from "../core/http/loginThrottleMiddleware.ts";
|
|
48
|
+
export { createMemoryThrottleMiddleware } from "../core/http/memoryThrottleMiddleware.ts";
|
|
49
|
+
export { createMetricsMiddleware, normalizeMetricPath } from "../core/http/metricsMiddleware.ts";
|
|
50
|
+
export type { Middleware, RouteHandler } from "../core/http/middleware.ts";
|
|
51
|
+
export { createRequireWebAuthMiddleware } from "../core/http/requireWebAuthMiddleware.ts";
|
|
52
|
+
export { serializeDate, toPaginatedResourceCollection, toResourceCollection, } from "../core/http/resources.ts";
|
|
53
|
+
export type { RouteRequest } from "../core/http/route.ts";
|
|
54
|
+
export { createSecurityHeadersMiddleware } from "../core/http/securityHeadersMiddleware.ts";
|
|
55
|
+
export { createThrottleMiddleware } from "../core/http/throttleMiddleware.ts";
|
|
56
|
+
export { WebFormRequest } from "../core/http/webFormRequest.ts";
|
|
57
|
+
export { installGracefulShutdownSignals, registerShutdownHandler, runGracefulShutdown, } from "../core/lifecycle/gracefulShutdown.ts";
|
|
58
|
+
export type { MailDriver, MailMessage } from "../core/mail/mailer.ts";
|
|
59
|
+
export { LogMailDriver, Mailer, mailer } from "../core/mail/mailer.ts";
|
|
60
|
+
export type { MetricLabels } from "../core/metrics/prometheus.ts";
|
|
61
|
+
export { PrometheusRegistry, prometheusRegistry } from "../core/metrics/prometheus.ts";
|
|
62
|
+
export type { CursorPaginatedResult, PaginatedResult, PaginationMeta, } from "../core/pagination/index.ts";
|
|
63
|
+
export type { Queue, QueuePriority } from "../core/queue/index.ts";
|
|
64
|
+
export { AsyncQueue, createQueue, Job, SyncQueue } from "../core/queue/index.ts";
|
|
65
|
+
export { createFailedJobService, createProductionQueue, createQueueWorker, createTrackedJob, FailedJobRepository, FailedJobService, jobRegistry, QueueWorker, RedisQueue, ResilientQueue, } from "../core/queue/publicQueue.ts";
|
|
66
|
+
export type { ScheduledTask } from "../core/scheduler/schedule.ts";
|
|
67
|
+
export { appSchedule, runDueScheduledTasks, Schedule } from "../core/scheduler/schedule.ts";
|
|
68
|
+
export type { StorageDriver } from "../core/storage/storage.ts";
|
|
69
|
+
export { LocalStorageDriver, StorageManager } from "../core/storage/storage.ts";
|
|
70
|
+
export type { ValidationRule, ValidationSchema } from "../core/validation/rules.ts";
|
|
71
|
+
export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
|
|
72
|
+
export { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, htmlResponse, isHtmxRequest, resolveWebLayoutData, } from "../core/view/index.ts";
|
|
73
|
+
export type { ViewEngine } from "../core/view/viewEngine.ts";
|
|
74
|
+
export type { WebLayoutAuthUser, WebLayoutData } from "../core/view/webLayoutData.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/bootstrap/public-api.ts
|
|
3
|
+
import { resolveApplicationQueue as resolveApplicationQueue2, setActiveApplicationContext as setActiveApplicationContext2 } from "@getstrata/core";
|
|
4
|
+
|
|
2
5
|
// ../../src/core/scheduler/schedule.ts
|
|
3
6
|
class Schedule {
|
|
4
7
|
tasks = [];
|
|
@@ -487,6 +490,8 @@ var DEFAULT_CACHE_MAX_ENTRIES = 100;
|
|
|
487
490
|
var DEFAULT_CACHE_DRIVER = "array";
|
|
488
491
|
var DEFAULT_API_TOKEN = "";
|
|
489
492
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
493
|
+
// ../../src/bootstrap/context.ts
|
|
494
|
+
import { setActiveApplicationContext } from "@getstrata/core";
|
|
490
495
|
|
|
491
496
|
// ../../src/bootstrap/contracts.ts
|
|
492
497
|
class ServiceContainer {
|
|
@@ -572,23 +577,6 @@ function resolveService(dependencies, token) {
|
|
|
572
577
|
return dependencies.container.resolve(token);
|
|
573
578
|
}
|
|
574
579
|
|
|
575
|
-
// ../../src/bootstrap/applicationRegistry.ts
|
|
576
|
-
var activeContext;
|
|
577
|
-
function setActiveApplicationContext(context) {
|
|
578
|
-
activeContext = context;
|
|
579
|
-
}
|
|
580
|
-
function requireActiveApplicationContext() {
|
|
581
|
-
if (!activeContext) {
|
|
582
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
583
|
-
}
|
|
584
|
-
return activeContext;
|
|
585
|
-
}
|
|
586
|
-
function resolveApplicationCache() {
|
|
587
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
588
|
-
}
|
|
589
|
-
function resolveApplicationQueue() {
|
|
590
|
-
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
591
|
-
}
|
|
592
580
|
// ../../src/bootstrap/discoverModules.ts
|
|
593
581
|
import { readdirSync } from "fs";
|
|
594
582
|
import { join } from "path";
|
|
@@ -1747,6 +1735,21 @@ class InvalidateCacheTagsJob extends Job {
|
|
|
1747
1735
|
}
|
|
1748
1736
|
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
1749
1737
|
|
|
1738
|
+
// ../../src/bootstrap/applicationRegistry.ts
|
|
1739
|
+
var activeContext;
|
|
1740
|
+
function requireActiveApplicationContext() {
|
|
1741
|
+
if (!activeContext) {
|
|
1742
|
+
throw new Error("The application context has not been bootstrapped.");
|
|
1743
|
+
}
|
|
1744
|
+
return activeContext;
|
|
1745
|
+
}
|
|
1746
|
+
function resolveApplicationCache() {
|
|
1747
|
+
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
1748
|
+
}
|
|
1749
|
+
function resolveApplicationQueue() {
|
|
1750
|
+
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1750
1753
|
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1751
1754
|
import { createHmac as createHmac2 } from "crypto";
|
|
1752
1755
|
class DispatchWebhookJob extends Job {
|
|
@@ -3745,76 +3748,61 @@ function htmlResponse(html, init = {}) {
|
|
|
3745
3748
|
});
|
|
3746
3749
|
}
|
|
3747
3750
|
// ../../src/core/http/csrfToken.ts
|
|
3748
|
-
import {
|
|
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
|
|
3749
3779
|
var CSRF_COOKIE = "workhub_csrf";
|
|
3750
3780
|
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
3751
3781
|
function resolveCsrfSecret() {
|
|
3752
3782
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
3753
3783
|
}
|
|
3754
|
-
function
|
|
3755
|
-
|
|
3756
|
-
const signature = createHmac3("sha256", resolveCsrfSecret()).update(payload).digest("hex");
|
|
3757
|
-
return `${payload}.${signature}`;
|
|
3758
|
-
}
|
|
3759
|
-
function readCsrfCookie(request) {
|
|
3760
|
-
const cookieHeader = request.headers.get("cookie");
|
|
3761
|
-
if (!cookieHeader) {
|
|
3762
|
-
return null;
|
|
3763
|
-
}
|
|
3764
|
-
for (const part of cookieHeader.split(";")) {
|
|
3765
|
-
const [name, ...rest] = part.trim().split("=");
|
|
3766
|
-
if (name === CSRF_COOKIE) {
|
|
3767
|
-
return decodeURIComponent(rest.join("="));
|
|
3768
|
-
}
|
|
3769
|
-
}
|
|
3770
|
-
return null;
|
|
3784
|
+
function csrfVerifyOptions() {
|
|
3785
|
+
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
3771
3786
|
}
|
|
3772
|
-
function
|
|
3773
|
-
const
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
const [token, issuedAtRaw, cookieSignature] = parts;
|
|
3778
|
-
if (!token || !issuedAtRaw || !cookieSignature) {
|
|
3779
|
-
return null;
|
|
3780
|
-
}
|
|
3781
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
3782
|
-
if (!Number.isFinite(issuedAt)) {
|
|
3783
|
-
return null;
|
|
3784
|
-
}
|
|
3785
|
-
if (Date.now() - issuedAt > CSRF_TTL_MS) {
|
|
3786
|
-
return null;
|
|
3787
|
-
}
|
|
3788
|
-
const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
|
|
3789
|
-
if (!expectedSignature) {
|
|
3790
|
-
return null;
|
|
3791
|
-
}
|
|
3792
|
-
const expectedBuffer = Buffer.from(expectedSignature);
|
|
3793
|
-
const actualBuffer = Buffer.from(cookieSignature);
|
|
3794
|
-
if (expectedBuffer.length !== actualBuffer.length) {
|
|
3795
|
-
return null;
|
|
3796
|
-
}
|
|
3797
|
-
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
3798
|
-
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;
|
|
3799
3792
|
}
|
|
3800
|
-
return
|
|
3793
|
+
return timingSafeEqual2(leftBuffer, rightBuffer);
|
|
3801
3794
|
}
|
|
3802
3795
|
function createCsrfTokenCookie() {
|
|
3803
|
-
const token =
|
|
3804
|
-
const issuedAt = Date.now();
|
|
3805
|
-
const value = signCsrfToken(token, issuedAt);
|
|
3796
|
+
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
3806
3797
|
return {
|
|
3807
3798
|
token,
|
|
3808
|
-
cookie: `${CSRF_COOKIE}=${encodeURIComponent(
|
|
3799
|
+
cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
|
|
3809
3800
|
};
|
|
3810
3801
|
}
|
|
3811
3802
|
function resolveCsrfToken(request) {
|
|
3812
|
-
const cookieValue =
|
|
3813
|
-
if (cookieValue) {
|
|
3814
|
-
|
|
3815
|
-
if (parsed) {
|
|
3816
|
-
return { token: parsed.token };
|
|
3817
|
-
}
|
|
3803
|
+
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
3804
|
+
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
3805
|
+
return { token: cookieValue };
|
|
3818
3806
|
}
|
|
3819
3807
|
return createCsrfTokenCookie();
|
|
3820
3808
|
}
|
|
@@ -3837,6 +3825,10 @@ async function readSubmittedCsrfTokenFromBody(request) {
|
|
|
3837
3825
|
if (typeof field === "string" && field.trim().length > 0) {
|
|
3838
3826
|
return field.trim();
|
|
3839
3827
|
}
|
|
3828
|
+
const legacyField = formData.get("_csrf");
|
|
3829
|
+
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
3830
|
+
return legacyField.trim();
|
|
3831
|
+
}
|
|
3840
3832
|
}
|
|
3841
3833
|
return null;
|
|
3842
3834
|
}
|
|
@@ -3844,20 +3836,14 @@ function verifyCsrfToken(request, submittedToken) {
|
|
|
3844
3836
|
if (!submittedToken) {
|
|
3845
3837
|
return false;
|
|
3846
3838
|
}
|
|
3847
|
-
const cookieValue =
|
|
3839
|
+
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
3848
3840
|
if (!cookieValue) {
|
|
3849
3841
|
return false;
|
|
3850
3842
|
}
|
|
3851
|
-
|
|
3852
|
-
if (!parsed) {
|
|
3853
|
-
return false;
|
|
3854
|
-
}
|
|
3855
|
-
const submittedBuffer = Buffer.from(submittedToken);
|
|
3856
|
-
const expectedBuffer = Buffer.from(parsed.token);
|
|
3857
|
-
if (submittedBuffer.length !== expectedBuffer.length) {
|
|
3843
|
+
if (!tokensMatch(submittedToken, cookieValue)) {
|
|
3858
3844
|
return false;
|
|
3859
3845
|
}
|
|
3860
|
-
return
|
|
3846
|
+
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
3861
3847
|
}
|
|
3862
3848
|
function resolveCsrfTokenForRequest(request) {
|
|
3863
3849
|
const metaToken = currentRequestMeta().csrfToken;
|
|
@@ -3868,14 +3854,14 @@ function resolveCsrfTokenForRequest(request) {
|
|
|
3868
3854
|
}
|
|
3869
3855
|
|
|
3870
3856
|
// ../../src/core/http/flashSession.ts
|
|
3871
|
-
import { createHmac as
|
|
3857
|
+
import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
3872
3858
|
var FLASH_COOKIE = "workhub_flash";
|
|
3873
3859
|
var FLASH_TTL_MS = 60 * 1000;
|
|
3874
3860
|
function resolveFlashSecret() {
|
|
3875
3861
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
3876
3862
|
}
|
|
3877
3863
|
function signFlashPayload(payload, issuedAt) {
|
|
3878
|
-
const signature =
|
|
3864
|
+
const signature = createHmac3("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
3879
3865
|
return `${payload}.${issuedAt}.${signature}`;
|
|
3880
3866
|
}
|
|
3881
3867
|
function readFlashCookie(request) {
|
|
@@ -4964,9 +4950,9 @@ function createTenantMiddleware() {
|
|
|
4964
4950
|
}
|
|
4965
4951
|
|
|
4966
4952
|
// ../../src/core/tracing/otel.ts
|
|
4967
|
-
import { randomBytes
|
|
4953
|
+
import { randomBytes } from "crypto";
|
|
4968
4954
|
function randomHex(bytes) {
|
|
4969
|
-
return
|
|
4955
|
+
return randomBytes(bytes).toString("hex");
|
|
4970
4956
|
}
|
|
4971
4957
|
function createSpan(input) {
|
|
4972
4958
|
const spanId = randomHex(8);
|
|
@@ -5299,6 +5285,25 @@ function prefixRouteMap(prefix, routes) {
|
|
|
5299
5285
|
}
|
|
5300
5286
|
return prefixed;
|
|
5301
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
|
+
|
|
5302
5307
|
// ../../src/bootstrap/web/forms.ts
|
|
5303
5308
|
async function parseFormBody(request) {
|
|
5304
5309
|
const contentType = request.headers.get("content-type") ?? "";
|
|
@@ -5328,30 +5333,41 @@ async function parseFormBody(request) {
|
|
|
5328
5333
|
}
|
|
5329
5334
|
return { fields, files };
|
|
5330
5335
|
}
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
const
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
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
|
+
}
|
|
5345
5361
|
}
|
|
5346
|
-
return true;
|
|
5347
5362
|
}
|
|
5348
|
-
return
|
|
5363
|
+
return bunRoutes;
|
|
5349
5364
|
}
|
|
5350
|
-
// ../../src/bootstrap/web/server.ts
|
|
5351
5365
|
function createWebServer(options) {
|
|
5352
5366
|
const publicDir = options.publicDir ?? "./public";
|
|
5367
|
+
const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes) : undefined;
|
|
5353
5368
|
return Bun.serve({
|
|
5354
5369
|
port: options.port,
|
|
5370
|
+
...bunRoutes ? { routes: bunRoutes } : {},
|
|
5355
5371
|
async fetch(request) {
|
|
5356
5372
|
await options.onRequest?.(request);
|
|
5357
5373
|
const url = new URL(request.url);
|
|
@@ -5361,14 +5377,16 @@ function createWebServer(options) {
|
|
|
5361
5377
|
return new Response(file);
|
|
5362
5378
|
}
|
|
5363
5379
|
}
|
|
5364
|
-
|
|
5365
|
-
|
|
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 });
|
|
5366
5385
|
}
|
|
5367
5386
|
});
|
|
5368
5387
|
}
|
|
5369
5388
|
// ../../src/bootstrap/web/session.ts
|
|
5370
|
-
import { createHash, randomBytes as
|
|
5371
|
-
|
|
5389
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
5372
5390
|
class CookieSessionStore {
|
|
5373
5391
|
sql;
|
|
5374
5392
|
secret;
|
|
@@ -5394,7 +5412,7 @@ class CookieSessionStore {
|
|
|
5394
5412
|
return header.includes("Secure") ? header : `${header}; Secure`;
|
|
5395
5413
|
}
|
|
5396
5414
|
async create(user) {
|
|
5397
|
-
const id =
|
|
5415
|
+
const id = randomBytes2(32).toString("hex");
|
|
5398
5416
|
const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
|
|
5399
5417
|
await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
|
|
5400
5418
|
id,
|
|
@@ -5407,8 +5425,8 @@ class CookieSessionStore {
|
|
|
5407
5425
|
await this.sql.unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
|
|
5408
5426
|
}
|
|
5409
5427
|
async read(request) {
|
|
5410
|
-
const cookie =
|
|
5411
|
-
const raw = cookie
|
|
5428
|
+
const cookie = readRequestCookie(request, this.cookieName);
|
|
5429
|
+
const raw = cookie ?? null;
|
|
5412
5430
|
if (!raw)
|
|
5413
5431
|
return null;
|
|
5414
5432
|
const [sessionId, signature] = raw.split(".");
|
|
@@ -5432,16 +5450,6 @@ class CookieSessionStore {
|
|
|
5432
5450
|
sign(value) {
|
|
5433
5451
|
return createHash("sha256").update(`${value}.${this.secret}`).digest("hex").slice(0, 32);
|
|
5434
5452
|
}
|
|
5435
|
-
parseCookie(header) {
|
|
5436
|
-
const out = {};
|
|
5437
|
-
for (const part of header.split(";")) {
|
|
5438
|
-
const idx = part.indexOf("=");
|
|
5439
|
-
if (idx === -1)
|
|
5440
|
-
continue;
|
|
5441
|
-
out[part.slice(0, idx).trim()] = decodeURIComponent(part.slice(idx + 1).trim());
|
|
5442
|
-
}
|
|
5443
|
-
return out;
|
|
5444
|
-
}
|
|
5445
5453
|
}
|
|
5446
5454
|
// ../../src/bootstrap/web/slug.ts
|
|
5447
5455
|
function slugify(value) {
|
|
@@ -5449,12 +5457,12 @@ function slugify(value) {
|
|
|
5449
5457
|
}
|
|
5450
5458
|
export {
|
|
5451
5459
|
slugify,
|
|
5452
|
-
setActiveApplicationContext,
|
|
5460
|
+
setActiveApplicationContext2 as setActiveApplicationContext,
|
|
5453
5461
|
scheduleRunCommand,
|
|
5454
5462
|
runProviderPhase,
|
|
5455
5463
|
runDueScheduledTasks,
|
|
5456
5464
|
resolveService,
|
|
5457
|
-
resolveApplicationQueue,
|
|
5465
|
+
resolveApplicationQueue2 as resolveApplicationQueue,
|
|
5458
5466
|
prefixRouteMap,
|
|
5459
5467
|
parseFormBody,
|
|
5460
5468
|
mergeWebRoutes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/bootstrap",
|
|
3
|
-
"version": "0.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.
|
|
76
|
+
"@getstrata/core": "^0.5.6",
|
|
77
77
|
"typescript": "^5.9.0"
|
|
78
78
|
}
|
|
79
79
|
}
|