@usebetterdev/console 0.3.0-beta.2
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/drizzle.cjs +220 -0
- package/dist/drizzle.cjs.map +1 -0
- package/dist/drizzle.d.cts +343 -0
- package/dist/drizzle.d.ts +343 -0
- package/dist/drizzle.js +189 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/hono.cjs +101 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +43 -0
- package/dist/hono.d.ts +43 -0
- package/dist/hono.js +74 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +1155 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +162 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.js +1106 -0
- package/dist/index.js.map +1 -0
- package/dist/types-Bm35VBpM.d.cts +169 -0
- package/dist/types-Bm35VBpM.d.ts +169 -0
- package/package.json +71 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/token.ts","../src/config-validation.ts","../src/jwt.ts","../src/permissions.ts","../src/sessions/auto-approve.ts","../src/sessions/email-relay.ts","../src/sessions/magic-link.ts","../src/sessions/verify.ts","../src/router.ts","../src/cors.ts","../src/handlers/health.ts","../src/handlers/capabilities.ts","../src/handlers/session-init.ts","../src/input-validation.ts","../src/handlers/session-verify.ts","../src/handlers/session-poll.ts","../src/handlers/session-claim.ts","../src/better-console.ts"],"sourcesContent":["export class ConsoleError extends Error {\n readonly code: string;\n\n constructor(message: string, code: string) {\n super(message);\n this.name = \"ConsoleError\";\n this.code = code;\n }\n}\n\nexport class ConsoleSessionExpiredError extends ConsoleError {\n constructor(message = \"Session has expired\") {\n super(message, \"SESSION_EXPIRED\");\n this.name = \"ConsoleSessionExpiredError\";\n }\n}\n\nexport class ConsoleEmailNotAllowedError extends ConsoleError {\n constructor(email: string) {\n super(\n `Email \"${email}\" is not in the allowed list. ` +\n \"Update BETTER_CONSOLE_ALLOWED_EMAILS to grant access.\",\n \"EMAIL_NOT_ALLOWED\",\n );\n this.name = \"ConsoleEmailNotAllowedError\";\n }\n}\n\nexport class ConsoleMagicLinkExpiredError extends ConsoleError {\n constructor(message = \"Magic link has expired or was already used\") {\n super(message, \"MAGIC_LINK_EXPIRED\");\n this.name = \"ConsoleMagicLinkExpiredError\";\n }\n}\n\nexport class ConsoleInvalidTokenError extends ConsoleError {\n constructor(message = \"Invalid or expired session token\") {\n super(message, \"INVALID_TOKEN\");\n this.name = \"ConsoleInvalidTokenError\";\n }\n}\n\nexport class ConsoleAdapterRequiredError extends ConsoleError {\n constructor(\n message = \"Magic link sessions require a database adapter. \" +\n \"Either provide an adapter or use autoApprove for development.\",\n ) {\n super(message, \"ADAPTER_REQUIRED\");\n this.name = \"ConsoleAdapterRequiredError\";\n }\n}\n\nexport class ConsoleWeakSecretError extends ConsoleError {\n constructor() {\n super(\n \"connectionTokenHash secret is too short (minimum 32 characters in production). \" +\n \"Generate a strong secret with: npx better-console init\",\n \"WEAK_SECRET\",\n );\n this.name = \"ConsoleWeakSecretError\";\n }\n}\n\nexport class ConsoleEmailRelayError extends ConsoleError {\n constructor(message = \"Failed to deliver verification code via Console email relay\") {\n super(message, \"EMAIL_RELAY_FAILED\");\n this.name = \"ConsoleEmailRelayError\";\n }\n}\n\nexport class ConsoleAutoApproveInProductionError extends ConsoleError {\n constructor() {\n super(\n \"autoApprove is enabled outside of development. \" +\n 'This is a security risk. Set NODE_ENV=\"development\" or disable autoApprove.',\n \"AUTO_APPROVE_IN_PRODUCTION\",\n );\n this.name = \"ConsoleAutoApproveInProductionError\";\n }\n}\n","/**\n * Token utilities using the Web Crypto API (works in Node, Deno, Bun).\n * No node:crypto imports — only globalThis.crypto.\n */\n\nconst ALPHANUMERIC = \"ABCDEFGHJKLMNPQRSTUVWXYZ23456789\"; // 30 chars\nconst ALPHABET_LIMIT = 240; // 30 * 8 — largest multiple of 30 ≤ 256\n\nfunction bytesToHex(bytes: Uint8Array): string {\n let hex = \"\";\n for (const b of bytes) {\n hex += b.toString(16).padStart(2, \"0\");\n }\n return hex;\n}\n\nfunction bytesToBase64url(bytes: Uint8Array): string {\n let binary = \"\";\n for (const b of bytes) {\n binary += String.fromCharCode(b);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Generate a high-entropy session token (32 bytes → base64url).\n * Returns a 43-character string.\n */\nexport function generateSessionToken(): string {\n const bytes = new Uint8Array(32);\n globalThis.crypto.getRandomValues(bytes);\n return bytesToBase64url(bytes);\n}\n\n/**\n * SHA-256 hash a token string. Returns hex-encoded hash.\n */\nexport async function hashToken(token: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(token);\n const hashBuffer = await globalThis.crypto.subtle.digest(\"SHA-256\", data);\n return bytesToHex(new Uint8Array(hashBuffer));\n}\n\n/**\n * Verify a token against its hash using constant-time comparison.\n */\nexport async function verifyTokenHash(\n token: string,\n hash: string,\n): Promise<boolean> {\n const computed = await hashToken(token);\n if (computed.length !== hash.length) {\n return false;\n }\n // Constant-time comparison to prevent timing attacks\n let result = 0;\n for (let i = 0; i < computed.length; i++) {\n result |= computed.charCodeAt(i) ^ hash.charCodeAt(i);\n }\n return result === 0;\n}\n\n/**\n * Generate a 6-character alphanumeric magic link code.\n * Uses a restricted alphabet (no 0/O/1/I) for readability.\n * Rejection sampling eliminates modulo bias: bytes ≥ 240 are discarded\n * because 256 is not evenly divisible by the 30-char alphabet.\n */\nexport function generateMagicLinkCode(): string {\n const codeLength = 6;\n let code = \"\";\n const buffer = new Uint8Array(12);\n globalThis.crypto.getRandomValues(buffer);\n let offset = 0;\n while (code.length < codeLength) {\n if (offset >= buffer.length) {\n globalThis.crypto.getRandomValues(buffer);\n offset = 0;\n }\n const b = buffer[offset]!;\n offset++;\n if (b < ALPHABET_LIMIT) {\n code += ALPHANUMERIC[b % ALPHANUMERIC.length];\n }\n }\n return code;\n}\n\n/**\n * Hash a magic link code using SHA-256. Returns hex-encoded hash.\n */\nexport async function hashMagicLinkCode(code: string): Promise<string> {\n return hashToken(code);\n}\n\n/**\n * Parse a connection token hash from env var format.\n * Strips the \"sha256:\" prefix if present.\n */\nexport function parseConnectionTokenHash(envValue: string): string {\n if (envValue.startsWith(\"sha256:\")) {\n return envValue.slice(7);\n }\n return envValue;\n}\n\n/**\n * Parse a duration string (e.g. \"24h\", \"8h\", \"1h\", \"30m\", \"7d\") to milliseconds.\n * Returns null if the format is unrecognized.\n */\nexport function parseDuration(duration: string): number | null {\n const match = /^(\\d+)(ms|s|m|h|d)$/.exec(duration.trim());\n if (!match) {\n return null;\n }\n const value = Number(match[1]);\n const unit = match[2];\n switch (unit) {\n case \"ms\":\n return value;\n case \"s\":\n return value * 1000;\n case \"m\":\n return value * 60 * 1000;\n case \"h\":\n return value * 60 * 60 * 1000;\n case \"d\":\n return value * 24 * 60 * 60 * 1000;\n default:\n return null;\n }\n}\n","import type { BetterConsoleConfig, ConsolePermission } from \"./types.js\";\nimport {\n ConsoleAdapterRequiredError,\n ConsoleAutoApproveInProductionError,\n ConsoleError,\n ConsoleWeakSecretError,\n} from \"./errors.js\";\nimport { parseDuration, parseConnectionTokenHash } from \"./token.js\";\n\nconst MIN_TOKEN_LIFETIME_MS = 60 * 60 * 1000; // 1 hour\nconst MAX_TOKEN_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000; // 7 days\nconst DEFAULT_TOKEN_LIFETIME_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport interface ValidatedConfig {\n connectionTokenSecret: string;\n tokenLifetimeMs: number;\n allowedOrigins: string[];\n allowedActions: ConsolePermission[];\n allowedEmails: string[];\n}\n\nfunction parseAllowedEmails(\n input: string | string[] | undefined,\n): string[] {\n if (!input) {\n return [];\n }\n const raw = typeof input === \"string\" ? input.split(\",\") : input;\n return raw\n .map((e) => e.trim().toLowerCase())\n .filter((e) => e.length > 0);\n}\n\n/**\n * Validates BetterConsoleConfig at construction time.\n * Returns normalized values or throws on invalid config.\n */\nexport function validateConfig(config: BetterConsoleConfig): ValidatedConfig {\n // Connection token hash is required\n if (!config.connectionTokenHash || config.connectionTokenHash.trim().length === 0) {\n throw new ConsoleError(\n \"connectionTokenHash is required. Run: npx better-console init\",\n \"MISSING_CONNECTION_TOKEN\",\n );\n }\n\n const connectionTokenSecret = parseConnectionTokenHash(\n config.connectionTokenHash,\n );\n\n if (connectionTokenSecret.length === 0) {\n throw new ConsoleError(\n \"connectionTokenHash is empty after parsing. Check your BETTER_CONSOLE_TOKEN_HASH env var.\",\n \"EMPTY_CONNECTION_TOKEN\",\n );\n }\n\n const nodeEnv = (\n typeof process !== \"undefined\" ? process.env?.NODE_ENV : undefined\n ) ?? \"\";\n\n // Enforce minimum secret strength in production\n if (nodeEnv !== \"development\" && nodeEnv !== \"test\" && connectionTokenSecret.length < 32) {\n throw new ConsoleWeakSecretError();\n }\n\n // Auto-approve in production warning\n if (config.sessions.autoApprove) {\n if (nodeEnv !== \"development\" && nodeEnv !== \"test\") {\n throw new ConsoleAutoApproveInProductionError();\n }\n }\n\n // Magic link requires adapter\n if (config.sessions.magicLink && !config.adapter) {\n throw new ConsoleAdapterRequiredError();\n }\n\n // At least one session method must be configured\n if (\n !config.sessions.autoApprove &&\n !config.sessions.magicLink &&\n !config.sessions.authenticate\n ) {\n throw new ConsoleError(\n \"No session method configured. Enable autoApprove, magicLink, or authenticate.\",\n \"NO_SESSION_METHOD\",\n );\n }\n\n // Token lifetime\n let tokenLifetimeMs = DEFAULT_TOKEN_LIFETIME_MS;\n if (config.sessions.tokenLifetime) {\n const parsed = parseDuration(config.sessions.tokenLifetime);\n if (parsed === null) {\n throw new ConsoleError(\n `Invalid tokenLifetime \"${config.sessions.tokenLifetime}\". Use format: \"24h\", \"8h\", \"1h\", \"30m\", \"7d\".`,\n \"INVALID_TOKEN_LIFETIME\",\n );\n }\n if (parsed < MIN_TOKEN_LIFETIME_MS || parsed > MAX_TOKEN_LIFETIME_MS) {\n throw new ConsoleError(\n `tokenLifetime must be between 1h and 7d. Got: \"${config.sessions.tokenLifetime}\".`,\n \"TOKEN_LIFETIME_OUT_OF_RANGE\",\n );\n }\n tokenLifetimeMs = parsed;\n }\n\n // Allowed origins\n const allowedOrigins =\n config.allowedOrigins && config.allowedOrigins.length > 0\n ? config.allowedOrigins\n : [\"https://console.usebetter.dev\"];\n\n // Allowed actions\n const allowedActions: ConsolePermission[] =\n config.allowedActions && config.allowedActions.length > 0\n ? config.allowedActions\n : [\"read\", \"write\", \"admin\"];\n\n // Parse allowed emails for magic link\n const allowedEmails = parseAllowedEmails(\n config.sessions.magicLink?.allowedEmails,\n );\n\n return {\n connectionTokenSecret,\n tokenLifetimeMs,\n allowedOrigins,\n allowedActions,\n allowedEmails,\n };\n}\n","/**\n * Minimal JWT implementation using Web Crypto API (HMAC-SHA256).\n * No external deps. Works in Node, Deno, and Bun.\n *\n * Only supports HS256 (HMAC-SHA256) — sufficient for server-issued,\n * server-verified tokens.\n */\n\nimport type { ConsoleSessionToken } from \"./types.js\";\n\nfunction base64urlEncode(data: string): string {\n return btoa(data).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction base64urlDecode(encoded: string): string {\n const padded = encoded + \"=\".repeat((4 - (encoded.length % 4)) % 4);\n return atob(padded.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n}\n\nasync function hmacSign(data: string, secret: string): Promise<string> {\n const encoder = new TextEncoder();\n const key = await globalThis.crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const signature = await globalThis.crypto.subtle.sign(\n \"HMAC\",\n key,\n encoder.encode(data),\n );\n const bytes = new Uint8Array(signature);\n let binary = \"\";\n for (const b of bytes) {\n binary += String.fromCharCode(b);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmacVerify(\n data: string,\n signature: string,\n secret: string,\n): Promise<boolean> {\n const encoder = new TextEncoder();\n const key = await globalThis.crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"verify\"],\n );\n const sigPadded = signature + \"=\".repeat((4 - (signature.length % 4)) % 4);\n const sigBytes = Uint8Array.from(\n atob(sigPadded.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n (c) => c.charCodeAt(0),\n );\n return globalThis.crypto.subtle.verify(\n \"HMAC\",\n key,\n sigBytes,\n encoder.encode(data),\n );\n}\n\n/**\n * Sign a ConsoleSessionToken into a JWT string.\n */\nexport async function signSessionJwt(\n payload: ConsoleSessionToken,\n secret: string,\n): Promise<string> {\n const header = base64urlEncode(\n JSON.stringify({ alg: \"HS256\", typ: \"JWT\" }),\n );\n const body = base64urlEncode(JSON.stringify(payload));\n const signingInput = `${header}.${body}`;\n const signature = await hmacSign(signingInput, secret);\n return `${signingInput}.${signature}`;\n}\n\n/**\n * Verify and decode a JWT string into a ConsoleSessionToken.\n * Returns null if the token is invalid, tampered, or expired.\n */\nexport async function verifySessionJwt(\n token: string,\n secret: string,\n): Promise<ConsoleSessionToken | null> {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n const [header, body, signature] = parts as [string, string, string];\n const signingInput = `${header}.${body}`;\n\n let valid: boolean;\n try {\n valid = await hmacVerify(signingInput, signature, secret);\n } catch {\n return null;\n }\n if (!valid) {\n return null;\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(base64urlDecode(body));\n } catch {\n return null;\n }\n\n if (\n typeof payload !== \"object\" ||\n payload === null ||\n !(\"sessionId\" in payload) ||\n !(\"email\" in payload) ||\n !(\"permissions\" in payload) ||\n !(\"expiresAt\" in payload)\n ) {\n return null;\n }\n\n const sessionToken = payload as ConsoleSessionToken;\n\n // Check expiry\n const nowSeconds = Math.floor(Date.now() / 1000);\n if (sessionToken.expiresAt <= nowSeconds) {\n return null;\n }\n\n return sessionToken;\n}\n","import type { ConsolePermission } from \"./types.js\";\n\n/**\n * Resolves the highest permission level from a list of allowed actions.\n * Precedence: admin > write > read.\n */\nexport function resolveHighestPermission(\n allowedActions: ConsolePermission[],\n): ConsolePermission {\n if (allowedActions.includes(\"admin\")) {\n return \"admin\";\n }\n if (allowedActions.includes(\"write\")) {\n return \"write\";\n }\n return \"read\";\n}\n","import type { ValidatedConfig } from \"../config-validation.js\";\nimport type { ConsoleSessionToken } from \"../types.js\";\nimport { signSessionJwt } from \"../jwt.js\";\nimport { resolveHighestPermission } from \"../permissions.js\";\n\n/**\n * Creates an auto-approve session (stateless JWT, no database required).\n *\n * Returns a signed JWT and the lifetime in seconds.\n */\nexport async function createAutoApproveSession(\n email: string,\n validated: ValidatedConfig,\n): Promise<{ sessionToken: string; expiresIn: number }> {\n const expiresInSeconds = Math.floor(validated.tokenLifetimeMs / 1000);\n const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;\n\n const highestPermission = resolveHighestPermission(validated.allowedActions);\n\n const payload: ConsoleSessionToken = {\n sessionId: globalThis.crypto.randomUUID(),\n email,\n permissions: highestPermission,\n expiresAt,\n };\n\n const sessionToken = await signSessionJwt(\n payload,\n validated.connectionTokenSecret,\n );\n\n return { sessionToken, expiresIn: expiresInSeconds };\n}\n","import { ConsoleEmailRelayError } from \"../errors.js\";\n\nconst RELAY_URL = \"https://api.usebetter.dev/api/email/send-magic-link\";\n\nexport interface RelayContext {\n appName: string;\n baseUrl: string;\n}\n\nexport async function sendMagicLinkViaRelay(data: {\n email: string;\n code: string;\n sessionId: string;\n appName: string;\n baseUrl: string;\n}): Promise<void> {\n const normalizedBase = data.baseUrl.replace(/\\/+$/, \"\");\n const magicLinkUrl =\n `${normalizedBase}/.well-known/better/console/session/verify` +\n `?code=${encodeURIComponent(data.code)}&session_id=${encodeURIComponent(data.sessionId)}`;\n\n const envName = process.env.NODE_ENV ?? \"production\";\n\n const response = await fetch(RELAY_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n to: data.email,\n magicLinkUrl,\n appName: data.appName,\n envName,\n }),\n });\n\n if (!response.ok) {\n const body = await response.text().catch(() => \"\");\n throw new ConsoleEmailRelayError(\n `Console email relay returned ${String(response.status)}: ${body}`,\n );\n }\n}\n","import type { ValidatedConfig } from \"../config-validation.js\";\nimport type { ConsoleAdapter, MagicLinkConfig } from \"../types.js\";\nimport { ConsoleEmailNotAllowedError } from \"../errors.js\";\nimport { generateMagicLinkCode, hashMagicLinkCode } from \"../token.js\";\nimport { sendMagicLinkViaRelay, type RelayContext } from \"./email-relay.js\";\n\nconst MAGIC_LINK_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes\n\n/**\n * Initiates the magic link session flow.\n *\n * 1. Validates email is in the allowed list\n * 2. Generates a 6-char code and hashes it\n * 3. Stores the magic link (hash only) in the DB\n * 4. Delivers the code: custom callback → Console email relay → silent no-op\n * 5. Returns the sessionId for polling\n */\nexport async function initMagicLinkSession(\n email: string,\n validated: ValidatedConfig,\n adapter: ConsoleAdapter,\n magicLinkConfig: MagicLinkConfig,\n relayContext?: RelayContext,\n): Promise<{ sessionId: string }> {\n const normalizedEmail = email.trim().toLowerCase();\n\n if (!validated.allowedEmails.includes(normalizedEmail)) {\n throw new ConsoleEmailNotAllowedError(normalizedEmail);\n }\n\n const code = generateMagicLinkCode();\n const codeHash = await hashMagicLinkCode(code);\n const sessionId = globalThis.crypto.randomUUID();\n const expiresAt = new Date(Date.now() + MAGIC_LINK_EXPIRY_MS);\n\n await adapter.magicLinks.createMagicLink({\n email: normalizedEmail,\n codeHash,\n sessionId,\n expiresAt,\n });\n\n if (magicLinkConfig.sendMagicLinkEmail) {\n await magicLinkConfig.sendMagicLinkEmail({\n email: normalizedEmail,\n sessionId,\n code,\n });\n } else if (relayContext) {\n await sendMagicLinkViaRelay({\n email: normalizedEmail,\n code,\n sessionId,\n appName: relayContext.appName,\n baseUrl: relayContext.baseUrl,\n });\n }\n\n return { sessionId };\n}\n","import type { ValidatedConfig } from \"../config-validation.js\";\nimport type {\n ConsoleAdapter,\n ConsoleSession,\n} from \"../types.js\";\nimport { verifySessionJwt } from \"../jwt.js\";\nimport { hashToken } from \"../token.js\";\n\n/**\n * Dual-path session verification:\n * 1. Try stateless JWT verification (covers auto-approve tokens)\n * 2. If JWT invalid and adapter exists, hash token and look up in DB\n * 3. Check expiry in both paths\n * 4. Return ConsoleSession or null\n */\nexport async function verifySession(\n token: string,\n validated: ValidatedConfig,\n adapter: ConsoleAdapter | undefined,\n): Promise<ConsoleSession | null> {\n // Path 1: Try JWT verification (auto-approve)\n const jwtPayload = await verifySessionJwt(\n token,\n validated.connectionTokenSecret,\n );\n\n if (jwtPayload) {\n return {\n id: jwtPayload.sessionId,\n email: jwtPayload.email,\n permissions: jwtPayload.permissions,\n expiresAt: new Date(jwtPayload.expiresAt * 1000),\n createdAt: new Date(),\n };\n }\n\n // Path 2: DB lookup (magic link sessions)\n if (adapter) {\n const tokenHash = await hashToken(token);\n const session = await adapter.sessions.getSessionByTokenHash(tokenHash);\n\n if (!session) {\n return null;\n }\n\n // Check expiry\n if (session.expiresAt.getTime() <= Date.now()) {\n return null;\n }\n\n return session;\n }\n\n return null;\n}\n","import type {\n ConsolePermission,\n ConsoleProduct,\n ConsoleRequest,\n ConsoleResponse,\n} from \"./types.js\";\n\nexport type RouteHandler = (\n request: ConsoleRequest,\n) => Promise<ConsoleResponse>;\n\ninterface Route {\n handler: RouteHandler;\n requiresAuth: boolean;\n requiredPermission: ConsolePermission;\n}\n\ninterface MatchResult {\n handler: RouteHandler;\n params: Record<string, string>;\n requiresAuth: boolean;\n requiredPermission: ConsolePermission;\n}\n\n/**\n * Matches a URL path pattern (with :param segments) against an actual path.\n * Returns extracted params or null if no match.\n */\nfunction matchPath(\n pattern: string,\n actual: string,\n): Record<string, string> | null {\n const patternParts = pattern.split(\"/\").filter((p) => p.length > 0);\n const actualParts = actual.split(\"/\").filter((p) => p.length > 0);\n\n if (patternParts.length !== actualParts.length) {\n return null;\n }\n\n const params: Record<string, string> = {};\n for (let i = 0; i < patternParts.length; i++) {\n const pp = patternParts[i] as string;\n const ap = actualParts[i] as string;\n if (pp.startsWith(\":\")) {\n params[pp.slice(1)] = ap;\n } else if (pp !== ap) {\n return null;\n }\n }\n\n return params;\n}\n\n/**\n * Route registry for console and product endpoints.\n *\n * Console routes: /console/<path>\n * Product routes: /<productId>/<path>\n */\nexport class ConsoleRouter {\n private routes: Array<{\n method: string;\n pattern: string;\n route: Route;\n }> = [];\n\n /**\n * Register a built-in console route (e.g. session/init, health).\n */\n addConsoleRoute(\n method: string,\n path: string,\n handler: RouteHandler,\n options: {\n requiresAuth?: boolean;\n requiredPermission?: ConsolePermission;\n } = {},\n ): void {\n this.routes.push({\n method: method.toUpperCase(),\n pattern: `/console${path}`,\n route: {\n handler,\n requiresAuth: options.requiresAuth ?? false,\n requiredPermission: options.requiredPermission ?? \"read\",\n },\n });\n }\n\n /**\n * Register all endpoints from a product.\n */\n registerProduct(product: ConsoleProduct): void {\n for (const endpoint of product.endpoints) {\n // Product handlers expect AuthenticatedConsoleRequest, which extends\n // ConsoleRequest with a guaranteed session field. handleConsoleRequest\n // ensures session is populated before calling auth-required handlers.\n const handler = endpoint.handler as unknown as RouteHandler;\n this.routes.push({\n method: endpoint.method.toUpperCase(),\n pattern: `/${product.id}${endpoint.path}`,\n route: {\n handler,\n requiresAuth: true,\n requiredPermission: endpoint.requiredPermission ?? \"read\",\n },\n });\n }\n }\n\n /**\n * Match an incoming request method + path against registered routes.\n * For OPTIONS, matches any registered path regardless of method.\n */\n match(method: string, path: string): MatchResult | null {\n const upperMethod = method.toUpperCase();\n\n // OPTIONS matches any registered path (for CORS preflight)\n if (upperMethod === \"OPTIONS\") {\n for (const entry of this.routes) {\n const params = matchPath(entry.pattern, path);\n if (params) {\n return {\n handler: entry.route.handler,\n params,\n requiresAuth: false,\n requiredPermission: \"read\",\n };\n }\n }\n return null;\n }\n\n for (const entry of this.routes) {\n if (entry.method !== upperMethod) {\n continue;\n }\n const params = matchPath(entry.pattern, path);\n if (params) {\n return {\n handler: entry.route.handler,\n params,\n requiresAuth: entry.route.requiresAuth,\n requiredPermission: entry.route.requiredPermission,\n };\n }\n }\n\n return null;\n }\n}\n\n/**\n * Check if a session has a given permission level.\n * Permission hierarchy: admin > write > read.\n */\nexport function hasPermission(\n sessionPermission: ConsolePermission,\n required: ConsolePermission,\n): boolean {\n const levels: Record<ConsolePermission, number> = {\n read: 1,\n write: 2,\n admin: 3,\n };\n return levels[sessionPermission] >= levels[required];\n}\n\n/**\n * Extract a bearer token from request headers.\n */\nexport function extractBearerToken(headers: Headers): string | null {\n const auth = headers.get(\"authorization\");\n if (!auth) {\n return null;\n }\n const parts = auth.split(\" \");\n if (parts.length !== 2 || parts[0]?.toLowerCase() !== \"bearer\") {\n return null;\n }\n return parts[1] ?? null;\n}\n","/**\n * CORS utilities for the console.\n *\n * Rules:\n * 1. Default allowedOrigins: [\"https://console.usebetter.dev\"]\n * 2. localhost origins always allowed when NODE_ENV === \"development\" or \"test\"\n * 3. Headers only set when Origin matches\n * 4. Preflight includes Access-Control-Max-Age: 86400\n * 5. No Access-Control-Allow-Credentials (tokens in Authorization header, not cookies)\n */\n\nexport interface CorsConfig {\n allowedOrigins: string[];\n}\n\nconst ALLOWED_HEADERS = \"Authorization, Content-Type\";\nconst ALLOWED_METHODS = \"GET, POST, PUT, PATCH, DELETE, OPTIONS\";\nconst MAX_AGE = \"86400\";\n\nfunction isLocalhost(origin: string): boolean {\n try {\n const url = new URL(origin);\n return (\n url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\"\n );\n } catch {\n return false;\n }\n}\n\nfunction isDevEnvironment(): boolean {\n const nodeEnv = (\n typeof process !== \"undefined\" ? process.env?.NODE_ENV : undefined\n ) ?? \"\";\n return nodeEnv === \"development\" || nodeEnv === \"test\";\n}\n\n/**\n * Check whether an origin is allowed by the CORS policy.\n */\nexport function isOriginAllowed(\n origin: string,\n config: CorsConfig,\n): boolean {\n if (config.allowedOrigins.includes(origin)) {\n return true;\n }\n\n if (isDevEnvironment() && isLocalhost(origin)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Get CORS response headers for a given request origin.\n * Returns null if the origin is not allowed.\n */\nexport function getCorsHeaders(\n origin: string | null,\n config: CorsConfig,\n): Record<string, string> | null {\n if (!origin) {\n return null;\n }\n\n if (!isOriginAllowed(origin, config)) {\n return null;\n }\n\n return {\n \"Access-Control-Allow-Origin\": origin,\n \"Access-Control-Allow-Headers\": ALLOWED_HEADERS,\n \"Access-Control-Allow-Methods\": ALLOWED_METHODS,\n \"Vary\": \"Origin\",\n };\n}\n\n/**\n * Get CORS headers for a preflight (OPTIONS) response.\n * Includes Access-Control-Max-Age.\n */\nexport function getPreflightCorsHeaders(\n origin: string | null,\n config: CorsConfig,\n): Record<string, string> | null {\n const headers = getCorsHeaders(origin, config);\n if (!headers) {\n return null;\n }\n\n return {\n ...headers,\n \"Access-Control-Max-Age\": MAX_AGE,\n };\n}\n","import type { ConsoleRequest, ConsoleResponse } from \"../types.js\";\n\n/**\n * GET /console/health\n * Returns connection health status.\n */\nexport async function handleHealth(\n _request: ConsoleRequest,\n): Promise<ConsoleResponse> {\n return {\n status: 200,\n body: { status: \"ok\" },\n };\n}\n","import type {\n BetterConsoleInstance,\n ConsoleRequest,\n ConsoleResponse,\n} from \"../types.js\";\n\n/**\n * Creates a handler for GET /console/capabilities.\n * Returns registered products, auth methods, and permissions.\n */\nexport function createCapabilitiesHandler(\n instance: BetterConsoleInstance,\n): (request: ConsoleRequest) => Promise<ConsoleResponse> {\n return async (_request: ConsoleRequest): Promise<ConsoleResponse> => {\n return {\n status: 200,\n body: instance.getCapabilities(),\n };\n };\n}\n","import type {\n BetterConsoleInstance,\n ConsoleRequest,\n ConsoleResponse,\n} from \"../types.js\";\nimport { ConsoleError } from \"../errors.js\";\n\n/**\n * Creates a handler for POST /console/session/init.\n * Delegates to instance.initSession().\n */\nexport function createSessionInitHandler(\n instance: BetterConsoleInstance,\n): (request: ConsoleRequest) => Promise<ConsoleResponse> {\n return async (request: ConsoleRequest): Promise<ConsoleResponse> => {\n try {\n const result = await instance.initSession(request);\n return { status: 200, body: result };\n } catch (error) {\n if (error instanceof ConsoleError) {\n return {\n status: 400,\n body: { error: error.message, code: error.code },\n };\n }\n throw error;\n }\n };\n}\n","const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst MAGIC_CODE_RE = /^[A-HJ-NP-Z2-9]{6}$/;\n\nexport function isValidUuid(value: string): boolean {\n return UUID_RE.test(value);\n}\n\nexport function isValidMagicLinkCode(value: string): boolean {\n return MAGIC_CODE_RE.test(value);\n}\n","import type {\n ConsoleAdapter,\n ConsoleRequest,\n ConsoleResponse,\n} from \"../types.js\";\nimport { hashMagicLinkCode } from \"../token.js\";\nimport { isValidMagicLinkCode, isValidUuid } from \"../input-validation.js\";\n\nconst DEFAULT_MAX_ATTEMPTS = 5;\n\ntype VerifyResult =\n | { ok: true }\n | { ok: false; status: number; error: string };\n\nasync function verifyMagicLink(\n code: string | undefined,\n sessionId: string | undefined,\n adapter: ConsoleAdapter,\n maxAttempts: number,\n): Promise<VerifyResult> {\n if (!code || !sessionId) {\n return { ok: false, status: 400, error: \"Missing code or session_id\" };\n }\n\n if (typeof code !== \"string\" || !isValidMagicLinkCode(code)) {\n return { ok: false, status: 400, error: \"Invalid code format\" };\n }\n\n if (typeof sessionId !== \"string\" || !isValidUuid(sessionId)) {\n return { ok: false, status: 400, error: \"Invalid session_id format\" };\n }\n\n const magicLink =\n await adapter.magicLinks.getMagicLinkBySessionId(sessionId);\n\n if (!magicLink) {\n return { ok: false, status: 404, error: \"Magic link not found\" };\n }\n\n if (magicLink.usedAt !== null) {\n return { ok: false, status: 410, error: \"Magic link already used\" };\n }\n\n if (magicLink.expiresAt.getTime() <= Date.now()) {\n return { ok: false, status: 410, error: \"Magic link expired\" };\n }\n\n // Brute-force protection\n if (magicLink.failedAttempts >= maxAttempts) {\n return { ok: false, status: 429, error: \"Too many failed attempts\" };\n }\n\n const providedCodeHash = await hashMagicLinkCode(code);\n if (providedCodeHash !== magicLink.codeHash) {\n await adapter.magicLinks.incrementFailedAttempts(magicLink.id);\n return { ok: false, status: 401, error: \"Invalid code\" };\n }\n\n // Mark the magic link as used\n await adapter.magicLinks.markMagicLinkUsed(magicLink.id);\n\n return { ok: true };\n}\n\n/**\n * Creates a handler for POST /console/session/verify\n *\n * Validates the magic link code and marks it as used.\n * No session creation — the frontend claims the session via /session/claim.\n *\n * Body: { code: string, session_id: string }\n */\nexport function createSessionVerifyHandler(\n adapter: ConsoleAdapter,\n options: { maxAttempts?: number } = {},\n): (request: ConsoleRequest) => Promise<ConsoleResponse> {\n const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n\n return async (request: ConsoleRequest): Promise<ConsoleResponse> => {\n const body = request.body as\n | { code?: string; session_id?: string }\n | null\n | undefined;\n\n const result = await verifyMagicLink(\n body?.code,\n body?.session_id,\n adapter,\n maxAttempts,\n );\n\n if (!result.ok) {\n return {\n status: result.status,\n body: { error: result.error },\n };\n }\n\n return {\n status: 200,\n body: { status: \"verified\" },\n };\n };\n}\n\nfunction htmlPage(title: string, message: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>${title}</title>\n <style>\n body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #fafafa; }\n .card { text-align: center; padding: 2rem; max-width: 400px; }\n h1 { font-size: 1.25rem; margin-bottom: 0.5rem; }\n p { color: #555; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n <h1>${title}</h1>\n <p>${message}</p>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Creates a handler for GET /console/session/verify\n *\n * Same verification logic as the POST handler, but reads code and session_id\n * from query params (for browser link clicks) and returns an HTML page.\n *\n * Query: ?code=...&session_id=...\n */\nexport function createSessionVerifyGetHandler(\n adapter: ConsoleAdapter,\n options: { maxAttempts?: number } = {},\n): (request: ConsoleRequest) => Promise<ConsoleResponse> {\n const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n\n return async (request: ConsoleRequest): Promise<ConsoleResponse> => {\n const code = request.query.code;\n const sessionId = request.query.session_id;\n\n const result = await verifyMagicLink(\n code,\n sessionId,\n adapter,\n maxAttempts,\n );\n\n if (!result.ok) {\n return {\n status: result.status,\n body: htmlPage(\"Verification failed\", result.error),\n headers: { \"content-type\": \"text/html; charset=utf-8\" },\n };\n }\n\n return {\n status: 200,\n body: htmlPage(\n \"Verification successful\",\n \"You can close this tab and return to the Console.\",\n ),\n headers: { \"content-type\": \"text/html; charset=utf-8\" },\n };\n };\n}\n","import type {\n ConsoleAdapter,\n ConsoleRequest,\n ConsoleResponse,\n} from \"../types.js\";\nimport { isValidUuid } from \"../input-validation.js\";\n\n/**\n * Creates a handler for GET /console/session/poll?session_id=...\n *\n * The frontend polls this endpoint while the user enters the magic link code.\n * Returns one of three states: pending, verified, or expired.\n * This is a purely read-only endpoint — no session creation.\n */\nexport function createSessionPollHandler(\n adapter: ConsoleAdapter,\n): (request: ConsoleRequest) => Promise<ConsoleResponse> {\n return async (request: ConsoleRequest): Promise<ConsoleResponse> => {\n const sessionId = request.query.session_id;\n\n if (!sessionId) {\n return {\n status: 400,\n body: { error: \"Missing session_id\" },\n };\n }\n\n if (!isValidUuid(sessionId)) {\n return {\n status: 400,\n body: { error: \"Invalid session_id format\" },\n };\n }\n\n const magicLink =\n await adapter.magicLinks.getMagicLinkBySessionId(sessionId);\n\n if (!magicLink) {\n return {\n status: 404,\n body: { error: \"Magic link not found\" },\n };\n }\n\n // Expired (and not yet verified)\n if (magicLink.usedAt === null && magicLink.expiresAt.getTime() <= Date.now()) {\n return {\n status: 200,\n body: { status: \"expired\" },\n };\n }\n\n // Not yet verified\n if (magicLink.usedAt === null) {\n return {\n status: 200,\n body: { status: \"pending\" },\n };\n }\n\n // Verified\n return {\n status: 200,\n body: { status: \"verified\" },\n };\n };\n}\n","import type {\n ConsoleAdapter,\n ConsoleRequest,\n ConsoleResponse,\n} from \"../types.js\";\nimport type { ValidatedConfig } from \"../config-validation.js\";\nimport { generateSessionToken, hashToken } from \"../token.js\";\nimport { resolveHighestPermission } from \"../permissions.js\";\nimport { isValidUuid } from \"../input-validation.js\";\n\n/** Grace period after magic link expiry during which a verified link can still be claimed. */\nconst CLAIM_GRACE_MS = 60 * 1000; // 60 seconds\n\n/**\n * Creates a handler for POST /console/session/claim\n *\n * Creates a DB session exactly once for a verified magic link.\n * Idempotent via atomic `setMagicLinkTokenHash` (WHERE token_hash IS NULL).\n *\n * A 60-second grace period is applied to the expiry check so that a link\n * verified just before expiry can still be claimed by the frontend.\n *\n * Body: { session_id: string }\n */\nexport function createSessionClaimHandler(\n validated: ValidatedConfig,\n adapter: ConsoleAdapter,\n): (request: ConsoleRequest) => Promise<ConsoleResponse> {\n return async (request: ConsoleRequest): Promise<ConsoleResponse> => {\n const body = request.body as\n | { session_id?: string }\n | null\n | undefined;\n const sessionId = body?.session_id;\n\n if (!sessionId) {\n return {\n status: 400,\n body: { error: \"Missing session_id\" },\n };\n }\n\n if (typeof sessionId !== \"string\" || !isValidUuid(sessionId)) {\n return {\n status: 400,\n body: { error: \"Invalid session_id format\" },\n };\n }\n\n const magicLink =\n await adapter.magicLinks.getMagicLinkBySessionId(sessionId);\n\n if (!magicLink) {\n return {\n status: 404,\n body: { error: \"Magic link not found\" },\n };\n }\n\n // Not yet verified\n if (magicLink.usedAt === null) {\n return {\n status: 409,\n body: { error: \"Magic link not yet verified\" },\n };\n }\n\n // Expired (with grace period for verified links)\n if (magicLink.expiresAt.getTime() + CLAIM_GRACE_MS <= Date.now()) {\n return {\n status: 410,\n body: { error: \"Magic link expired\" },\n };\n }\n\n // Already claimed — idempotent response\n if (magicLink.tokenHash !== null) {\n return {\n status: 200,\n body: { status: \"already_claimed\" },\n };\n }\n\n // Atomic claim: set token hash only if still null\n const sessionToken = generateSessionToken();\n const tokenHash = await hashToken(sessionToken);\n const claimed = await adapter.magicLinks.setMagicLinkTokenHash(\n magicLink.id,\n tokenHash,\n );\n\n if (!claimed) {\n // Race condition: another request claimed it first\n return {\n status: 200,\n body: { status: \"already_claimed\" },\n };\n }\n\n // Create the DB session — compensate on failure so the link can be retried\n const expiresInSeconds = Math.floor(validated.tokenLifetimeMs / 1000);\n const expiresAt = new Date(Date.now() + validated.tokenLifetimeMs);\n const highestPermission = resolveHighestPermission(validated.allowedActions);\n\n try {\n await adapter.sessions.createSession({\n email: magicLink.email,\n tokenHash,\n permissions: highestPermission,\n expiresAt,\n });\n } catch (error) {\n // Compensating action: reset token hash so the user can retry\n await adapter.magicLinks.clearMagicLinkTokenHash(magicLink.id);\n throw error;\n }\n\n return {\n status: 200,\n body: {\n status: \"claimed\",\n sessionToken,\n expiresIn: expiresInSeconds,\n },\n };\n };\n}\n","import type {\n BetterConsoleConfig,\n BetterConsoleInstance,\n ConsoleProduct,\n ConsoleRequest,\n ConsoleResponse,\n ConsoleSession,\n ConsolePermission,\n SessionInitResult,\n} from \"./types.js\";\nimport { validateConfig, type ValidatedConfig } from \"./config-validation.js\";\nimport { ConsoleAdapterRequiredError, ConsoleError } from \"./errors.js\";\nimport { createAutoApproveSession } from \"./sessions/auto-approve.js\";\nimport { initMagicLinkSession } from \"./sessions/magic-link.js\";\nimport { verifySession as verifySessionImpl } from \"./sessions/verify.js\";\nimport {\n ConsoleRouter,\n extractBearerToken,\n hasPermission,\n} from \"./router.js\";\nimport { getCorsHeaders, getPreflightCorsHeaders } from \"./cors.js\";\nimport { handleHealth } from \"./handlers/health.js\";\nimport { createCapabilitiesHandler } from \"./handlers/capabilities.js\";\nimport { createSessionInitHandler } from \"./handlers/session-init.js\";\nimport { createSessionVerifyHandler, createSessionVerifyGetHandler } from \"./handlers/session-verify.js\";\nimport { createSessionPollHandler } from \"./handlers/session-poll.js\";\nimport { createSessionClaimHandler } from \"./handlers/session-claim.js\";\n\nfunction addCorsHeaders(\n response: ConsoleResponse,\n origin: string | null,\n allowedOrigins: string[],\n): ConsoleResponse {\n const corsHeaders = getCorsHeaders(origin, { allowedOrigins });\n if (!corsHeaders) {\n return response;\n }\n return {\n ...response,\n headers: { ...response.headers, ...corsHeaders },\n };\n}\n\n/**\n * Creates the Better Console instance.\n *\n * Config must include connectionTokenHash and at least one session method\n * (autoApprove, magicLink, or authenticate).\n */\nexport function betterConsole(\n config: BetterConsoleConfig,\n): BetterConsoleInstance {\n const validated: ValidatedConfig = validateConfig(config);\n const products: ConsoleProduct[] = [];\n const router = new ConsoleRouter();\n\n // Build the instance first (handlers need a reference to it)\n const instance: BetterConsoleInstance = {\n async initSession(\n request: ConsoleRequest,\n ): Promise<SessionInitResult> {\n if (config.sessions.autoApprove) {\n const body = request.body as\n | { email?: string }\n | null\n | undefined;\n const email = body?.email ?? \"dev@localhost\";\n const result = await createAutoApproveSession(email, validated);\n return { method: \"auto_approve\", ...result };\n }\n\n if (config.sessions.magicLink) {\n if (!config.adapter) {\n throw new ConsoleAdapterRequiredError();\n }\n const body = request.body as\n | { email?: string; appName?: string; baseUrl?: string }\n | null\n | undefined;\n const email = body?.email;\n if (!email || typeof email !== \"string\") {\n throw new ConsoleError(\n \"Email is required for magic link sessions\",\n \"EMAIL_REQUIRED\",\n );\n }\n const relayContext =\n body?.appName && body?.baseUrl\n ? { appName: body.appName, baseUrl: body.baseUrl }\n : undefined;\n const result = await initMagicLinkSession(\n email,\n validated,\n config.adapter,\n config.sessions.magicLink,\n relayContext,\n );\n return { method: \"magic_link\", sessionId: result.sessionId };\n }\n\n throw new ConsoleError(\n \"No session method configured\",\n \"NO_SESSION_METHOD\",\n );\n },\n\n async verifySession(\n token: string,\n ): Promise<ConsoleSession | null> {\n return verifySessionImpl(token, validated, config.adapter);\n },\n\n async refreshSession(\n _token: string,\n ): Promise<{ sessionToken: string; expiresIn: number } | null> {\n // Future: Step 7+\n return null;\n },\n\n async revokeSession(sessionId: string): Promise<void> {\n if (config.adapter) {\n await config.adapter.sessions.deleteSession(sessionId);\n }\n },\n\n async revokeAllSessions(): Promise<void> {\n if (config.adapter) {\n await config.adapter.sessions.deleteAllSessions();\n }\n },\n\n registerProduct(product: ConsoleProduct): void {\n const existing = products.find((p) => p.id === product.id);\n if (existing) {\n throw new ConsoleError(\n `Product \"${product.id}\" is already registered`,\n \"DUPLICATE_PRODUCT\",\n );\n }\n products.push(product);\n router.registerProduct(product);\n },\n\n getRegisteredProducts(): ConsoleProduct[] {\n return [...products];\n },\n\n async handleConsoleRequest(\n request: ConsoleRequest,\n ): Promise<ConsoleResponse> {\n const origin = request.headers.get(\"origin\");\n\n try {\n const match = router.match(request.method, request.path);\n if (!match) {\n return addCorsHeaders(\n { status: 404, body: { error: \"Not found\" } },\n origin,\n validated.allowedOrigins,\n );\n }\n\n // OPTIONS preflight — return 204 with preflight CORS headers\n if (request.method.toUpperCase() === \"OPTIONS\") {\n const preflightHeaders =\n getPreflightCorsHeaders(origin, {\n allowedOrigins: validated.allowedOrigins,\n }) ?? {};\n return { status: 204, body: null, headers: preflightHeaders };\n }\n\n if (match.requiresAuth) {\n const token = extractBearerToken(request.headers);\n if (!token) {\n return addCorsHeaders(\n { status: 401, body: { error: \"Missing session token\" } },\n origin,\n validated.allowedOrigins,\n );\n }\n const session = await instance.verifySession(token);\n if (!session) {\n return addCorsHeaders(\n { status: 401, body: { error: \"Invalid or expired session\" } },\n origin,\n validated.allowedOrigins,\n );\n }\n if (!hasPermission(session.permissions, match.requiredPermission)) {\n return addCorsHeaders(\n { status: 403, body: { error: \"Insufficient permissions\" } },\n origin,\n validated.allowedOrigins,\n );\n }\n\n const enrichedRequest = {\n ...request,\n params: match.params,\n session,\n };\n const response = await match.handler(enrichedRequest);\n return addCorsHeaders(response, origin, validated.allowedOrigins);\n }\n\n const enrichedRequest = { ...request, params: match.params };\n const response = await match.handler(enrichedRequest);\n return addCorsHeaders(response, origin, validated.allowedOrigins);\n } catch (error) {\n if (error instanceof ConsoleError) {\n return addCorsHeaders(\n { status: 400, body: { error: error.message, code: error.code } },\n origin,\n validated.allowedOrigins,\n );\n }\n if (config.onError) {\n config.onError(error);\n }\n return addCorsHeaders(\n { status: 500, body: { error: \"Internal server error\" } },\n origin,\n validated.allowedOrigins,\n );\n }\n },\n\n getCapabilities() {\n const authMethods: string[] = [];\n if (config.sessions.autoApprove) {\n authMethods.push(\"auto_approve\");\n }\n if (config.sessions.magicLink) {\n authMethods.push(\"magic_link\");\n }\n if (config.sessions.authenticate) {\n authMethods.push(\"authenticate\");\n }\n\n return {\n products: products.map((p) => p.id),\n authMethods,\n permissions: validated.allowedActions,\n };\n },\n\n config,\n };\n\n // Register built-in console routes\n router.addConsoleRoute(\"GET\", \"/health\", handleHealth);\n router.addConsoleRoute(\n \"GET\",\n \"/capabilities\",\n createCapabilitiesHandler(instance),\n );\n router.addConsoleRoute(\n \"POST\",\n \"/session/init\",\n createSessionInitHandler(instance),\n );\n\n // Magic link routes (only registered when adapter is available)\n if (config.adapter) {\n const verifyOptions =\n config.sessions.magicLink?.maxAttempts !== undefined\n ? { maxAttempts: config.sessions.magicLink.maxAttempts }\n : {};\n router.addConsoleRoute(\n \"POST\",\n \"/session/verify\",\n createSessionVerifyHandler(config.adapter, verifyOptions),\n );\n router.addConsoleRoute(\n \"GET\",\n \"/session/verify\",\n createSessionVerifyGetHandler(config.adapter, verifyOptions),\n );\n router.addConsoleRoute(\n \"GET\",\n \"/session/poll\",\n createSessionPollHandler(config.adapter),\n );\n router.addConsoleRoute(\n \"POST\",\n \"/session/claim\",\n createSessionClaimHandler(validated, config.adapter),\n );\n }\n\n return instance;\n}\n"],"mappings":"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","names":["enrichedRequest","response"]}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
interface ConsoleSession {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
permissions: ConsolePermission;
|
|
5
|
+
expiresAt: Date;
|
|
6
|
+
createdAt: Date;
|
|
7
|
+
}
|
|
8
|
+
type ConsolePermission = "read" | "write" | "admin";
|
|
9
|
+
interface ConsoleSessionToken {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
email: string;
|
|
12
|
+
permissions: ConsolePermission;
|
|
13
|
+
/** Unix timestamp (seconds) */
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
}
|
|
16
|
+
interface ConsoleMagicLink {
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
codeHash: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
tokenHash: string | null;
|
|
22
|
+
failedAttempts: number;
|
|
23
|
+
expiresAt: Date;
|
|
24
|
+
usedAt: Date | null;
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
}
|
|
27
|
+
type SessionInitResult = {
|
|
28
|
+
method: "auto_approve";
|
|
29
|
+
sessionToken: string;
|
|
30
|
+
expiresIn: number;
|
|
31
|
+
} | {
|
|
32
|
+
method: "magic_link";
|
|
33
|
+
sessionId: string;
|
|
34
|
+
} | {
|
|
35
|
+
method: "redirect";
|
|
36
|
+
redirectUrl: string;
|
|
37
|
+
};
|
|
38
|
+
interface ConsoleSessionRepository {
|
|
39
|
+
createSession(data: {
|
|
40
|
+
email: string;
|
|
41
|
+
tokenHash: string;
|
|
42
|
+
permissions: ConsolePermission;
|
|
43
|
+
expiresAt: Date;
|
|
44
|
+
}): Promise<ConsoleSession>;
|
|
45
|
+
getSessionByTokenHash(tokenHash: string): Promise<ConsoleSession | null>;
|
|
46
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
47
|
+
deleteAllSessions(): Promise<void>;
|
|
48
|
+
deleteExpiredSessions(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
interface ConsoleMagicLinkRepository {
|
|
51
|
+
createMagicLink(data: {
|
|
52
|
+
email: string;
|
|
53
|
+
codeHash: string;
|
|
54
|
+
sessionId: string;
|
|
55
|
+
expiresAt: Date;
|
|
56
|
+
}): Promise<ConsoleMagicLink>;
|
|
57
|
+
getMagicLinkBySessionId(sessionId: string): Promise<ConsoleMagicLink | null>;
|
|
58
|
+
markMagicLinkUsed(id: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Atomically sets the token hash on a magic link.
|
|
61
|
+
* Returns true if the row was updated (first claim), false if already set.
|
|
62
|
+
* Uses `WHERE token_hash IS NULL` for idempotent claim logic.
|
|
63
|
+
*/
|
|
64
|
+
setMagicLinkTokenHash(id: string, tokenHash: string): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Clears the token hash on a magic link, resetting it to null.
|
|
67
|
+
* Used as a compensating action when session creation fails after claiming.
|
|
68
|
+
*/
|
|
69
|
+
clearMagicLinkTokenHash(id: string): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Increments the failed verification attempts counter.
|
|
72
|
+
* Returns the new count after incrementing.
|
|
73
|
+
*/
|
|
74
|
+
incrementFailedAttempts(id: string): Promise<number>;
|
|
75
|
+
deleteExpiredMagicLinks(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
interface ConsoleAdapter {
|
|
78
|
+
sessions: ConsoleSessionRepository;
|
|
79
|
+
magicLinks: ConsoleMagicLinkRepository;
|
|
80
|
+
}
|
|
81
|
+
interface ConsoleProductEndpoint {
|
|
82
|
+
method: "GET" | "POST" | "PATCH" | "DELETE";
|
|
83
|
+
/** Path relative to the product mount, e.g. "/tenants" or "/tenants/:id". */
|
|
84
|
+
path: string;
|
|
85
|
+
handler: (request: AuthenticatedConsoleRequest) => Promise<ConsoleResponse>;
|
|
86
|
+
requiredPermission?: ConsolePermission;
|
|
87
|
+
}
|
|
88
|
+
interface ConsoleProduct {
|
|
89
|
+
/** Short identifier, e.g. "tenant", "audit", "webhook". */
|
|
90
|
+
id: string;
|
|
91
|
+
/** Display name, e.g. "Better Tenant". */
|
|
92
|
+
name: string;
|
|
93
|
+
endpoints: ConsoleProductEndpoint[];
|
|
94
|
+
}
|
|
95
|
+
/** Incoming request before auth. Session is populated by handleConsoleRequest. */
|
|
96
|
+
interface ConsoleRequest {
|
|
97
|
+
method: string;
|
|
98
|
+
path: string;
|
|
99
|
+
url: string;
|
|
100
|
+
headers: Headers;
|
|
101
|
+
query: Record<string, string>;
|
|
102
|
+
params: Record<string, string>;
|
|
103
|
+
body: unknown;
|
|
104
|
+
session?: ConsoleSession;
|
|
105
|
+
}
|
|
106
|
+
/** Request with a verified session, passed to authenticated product handlers. */
|
|
107
|
+
interface AuthenticatedConsoleRequest extends ConsoleRequest {
|
|
108
|
+
session: ConsoleSession;
|
|
109
|
+
}
|
|
110
|
+
interface ConsoleResponse {
|
|
111
|
+
status: number;
|
|
112
|
+
body: unknown;
|
|
113
|
+
headers?: Record<string, string>;
|
|
114
|
+
}
|
|
115
|
+
interface MagicLinkConfig {
|
|
116
|
+
allowedEmails: string | string[];
|
|
117
|
+
sendMagicLinkEmail?: (data: {
|
|
118
|
+
email: string;
|
|
119
|
+
sessionId: string;
|
|
120
|
+
code: string;
|
|
121
|
+
}) => Promise<void>;
|
|
122
|
+
/** Maximum failed code verification attempts before lockout. Default: 5. */
|
|
123
|
+
maxAttempts?: number;
|
|
124
|
+
}
|
|
125
|
+
interface ConsoleSessionConfig {
|
|
126
|
+
magicLink?: MagicLinkConfig;
|
|
127
|
+
autoApprove?: boolean;
|
|
128
|
+
authenticate?: (request: ConsoleRequest) => Promise<{
|
|
129
|
+
email: string;
|
|
130
|
+
permissions: ConsolePermission;
|
|
131
|
+
} | null>;
|
|
132
|
+
/** Token lifetime as a duration string, e.g. "24h", "8h", "1h". Default: "24h". */
|
|
133
|
+
tokenLifetime?: string;
|
|
134
|
+
refreshEnabled?: boolean;
|
|
135
|
+
}
|
|
136
|
+
interface BetterConsoleConfig {
|
|
137
|
+
/** Database adapter. Optional for autoApprove mode; required for magic link sessions. */
|
|
138
|
+
adapter?: ConsoleAdapter;
|
|
139
|
+
/** Hashed connection token from env, e.g. "sha256:abc123...". */
|
|
140
|
+
connectionTokenHash: string;
|
|
141
|
+
/** Origins allowed for CORS. Default: ["https://console.usebetter.dev"]. */
|
|
142
|
+
allowedOrigins?: string[];
|
|
143
|
+
sessions: ConsoleSessionConfig;
|
|
144
|
+
/** Permission levels granted to sessions. Default: ["read", "write", "admin"]. */
|
|
145
|
+
allowedActions?: ConsolePermission[];
|
|
146
|
+
/** Called when handleConsoleRequest catches an unexpected error. */
|
|
147
|
+
onError?: (error: unknown) => void;
|
|
148
|
+
}
|
|
149
|
+
interface BetterConsoleInstance {
|
|
150
|
+
initSession(request: ConsoleRequest): Promise<SessionInitResult>;
|
|
151
|
+
verifySession(token: string): Promise<ConsoleSession | null>;
|
|
152
|
+
refreshSession(token: string): Promise<{
|
|
153
|
+
sessionToken: string;
|
|
154
|
+
expiresIn: number;
|
|
155
|
+
} | null>;
|
|
156
|
+
revokeSession(sessionId: string): Promise<void>;
|
|
157
|
+
revokeAllSessions(): Promise<void>;
|
|
158
|
+
registerProduct(product: ConsoleProduct): void;
|
|
159
|
+
getRegisteredProducts(): ConsoleProduct[];
|
|
160
|
+
handleConsoleRequest(request: ConsoleRequest): Promise<ConsoleResponse>;
|
|
161
|
+
getCapabilities(): {
|
|
162
|
+
products: string[];
|
|
163
|
+
authMethods: string[];
|
|
164
|
+
permissions: ConsolePermission[];
|
|
165
|
+
};
|
|
166
|
+
config: Readonly<BetterConsoleConfig>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type { AuthenticatedConsoleRequest as A, BetterConsoleConfig as B, ConsoleMagicLinkRepository as C, MagicLinkConfig as M, SessionInitResult as S, ConsoleSessionRepository as a, ConsoleAdapter as b, ConsoleRequest as c, ConsoleResponse as d, BetterConsoleInstance as e, ConsoleSessionToken as f, ConsolePermission as g, ConsoleProduct as h, ConsoleMagicLink as i, ConsoleProductEndpoint as j, ConsoleSession as k, ConsoleSessionConfig as l };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
interface ConsoleSession {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
permissions: ConsolePermission;
|
|
5
|
+
expiresAt: Date;
|
|
6
|
+
createdAt: Date;
|
|
7
|
+
}
|
|
8
|
+
type ConsolePermission = "read" | "write" | "admin";
|
|
9
|
+
interface ConsoleSessionToken {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
email: string;
|
|
12
|
+
permissions: ConsolePermission;
|
|
13
|
+
/** Unix timestamp (seconds) */
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
}
|
|
16
|
+
interface ConsoleMagicLink {
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
codeHash: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
tokenHash: string | null;
|
|
22
|
+
failedAttempts: number;
|
|
23
|
+
expiresAt: Date;
|
|
24
|
+
usedAt: Date | null;
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
}
|
|
27
|
+
type SessionInitResult = {
|
|
28
|
+
method: "auto_approve";
|
|
29
|
+
sessionToken: string;
|
|
30
|
+
expiresIn: number;
|
|
31
|
+
} | {
|
|
32
|
+
method: "magic_link";
|
|
33
|
+
sessionId: string;
|
|
34
|
+
} | {
|
|
35
|
+
method: "redirect";
|
|
36
|
+
redirectUrl: string;
|
|
37
|
+
};
|
|
38
|
+
interface ConsoleSessionRepository {
|
|
39
|
+
createSession(data: {
|
|
40
|
+
email: string;
|
|
41
|
+
tokenHash: string;
|
|
42
|
+
permissions: ConsolePermission;
|
|
43
|
+
expiresAt: Date;
|
|
44
|
+
}): Promise<ConsoleSession>;
|
|
45
|
+
getSessionByTokenHash(tokenHash: string): Promise<ConsoleSession | null>;
|
|
46
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
47
|
+
deleteAllSessions(): Promise<void>;
|
|
48
|
+
deleteExpiredSessions(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
interface ConsoleMagicLinkRepository {
|
|
51
|
+
createMagicLink(data: {
|
|
52
|
+
email: string;
|
|
53
|
+
codeHash: string;
|
|
54
|
+
sessionId: string;
|
|
55
|
+
expiresAt: Date;
|
|
56
|
+
}): Promise<ConsoleMagicLink>;
|
|
57
|
+
getMagicLinkBySessionId(sessionId: string): Promise<ConsoleMagicLink | null>;
|
|
58
|
+
markMagicLinkUsed(id: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Atomically sets the token hash on a magic link.
|
|
61
|
+
* Returns true if the row was updated (first claim), false if already set.
|
|
62
|
+
* Uses `WHERE token_hash IS NULL` for idempotent claim logic.
|
|
63
|
+
*/
|
|
64
|
+
setMagicLinkTokenHash(id: string, tokenHash: string): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Clears the token hash on a magic link, resetting it to null.
|
|
67
|
+
* Used as a compensating action when session creation fails after claiming.
|
|
68
|
+
*/
|
|
69
|
+
clearMagicLinkTokenHash(id: string): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Increments the failed verification attempts counter.
|
|
72
|
+
* Returns the new count after incrementing.
|
|
73
|
+
*/
|
|
74
|
+
incrementFailedAttempts(id: string): Promise<number>;
|
|
75
|
+
deleteExpiredMagicLinks(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
interface ConsoleAdapter {
|
|
78
|
+
sessions: ConsoleSessionRepository;
|
|
79
|
+
magicLinks: ConsoleMagicLinkRepository;
|
|
80
|
+
}
|
|
81
|
+
interface ConsoleProductEndpoint {
|
|
82
|
+
method: "GET" | "POST" | "PATCH" | "DELETE";
|
|
83
|
+
/** Path relative to the product mount, e.g. "/tenants" or "/tenants/:id". */
|
|
84
|
+
path: string;
|
|
85
|
+
handler: (request: AuthenticatedConsoleRequest) => Promise<ConsoleResponse>;
|
|
86
|
+
requiredPermission?: ConsolePermission;
|
|
87
|
+
}
|
|
88
|
+
interface ConsoleProduct {
|
|
89
|
+
/** Short identifier, e.g. "tenant", "audit", "webhook". */
|
|
90
|
+
id: string;
|
|
91
|
+
/** Display name, e.g. "Better Tenant". */
|
|
92
|
+
name: string;
|
|
93
|
+
endpoints: ConsoleProductEndpoint[];
|
|
94
|
+
}
|
|
95
|
+
/** Incoming request before auth. Session is populated by handleConsoleRequest. */
|
|
96
|
+
interface ConsoleRequest {
|
|
97
|
+
method: string;
|
|
98
|
+
path: string;
|
|
99
|
+
url: string;
|
|
100
|
+
headers: Headers;
|
|
101
|
+
query: Record<string, string>;
|
|
102
|
+
params: Record<string, string>;
|
|
103
|
+
body: unknown;
|
|
104
|
+
session?: ConsoleSession;
|
|
105
|
+
}
|
|
106
|
+
/** Request with a verified session, passed to authenticated product handlers. */
|
|
107
|
+
interface AuthenticatedConsoleRequest extends ConsoleRequest {
|
|
108
|
+
session: ConsoleSession;
|
|
109
|
+
}
|
|
110
|
+
interface ConsoleResponse {
|
|
111
|
+
status: number;
|
|
112
|
+
body: unknown;
|
|
113
|
+
headers?: Record<string, string>;
|
|
114
|
+
}
|
|
115
|
+
interface MagicLinkConfig {
|
|
116
|
+
allowedEmails: string | string[];
|
|
117
|
+
sendMagicLinkEmail?: (data: {
|
|
118
|
+
email: string;
|
|
119
|
+
sessionId: string;
|
|
120
|
+
code: string;
|
|
121
|
+
}) => Promise<void>;
|
|
122
|
+
/** Maximum failed code verification attempts before lockout. Default: 5. */
|
|
123
|
+
maxAttempts?: number;
|
|
124
|
+
}
|
|
125
|
+
interface ConsoleSessionConfig {
|
|
126
|
+
magicLink?: MagicLinkConfig;
|
|
127
|
+
autoApprove?: boolean;
|
|
128
|
+
authenticate?: (request: ConsoleRequest) => Promise<{
|
|
129
|
+
email: string;
|
|
130
|
+
permissions: ConsolePermission;
|
|
131
|
+
} | null>;
|
|
132
|
+
/** Token lifetime as a duration string, e.g. "24h", "8h", "1h". Default: "24h". */
|
|
133
|
+
tokenLifetime?: string;
|
|
134
|
+
refreshEnabled?: boolean;
|
|
135
|
+
}
|
|
136
|
+
interface BetterConsoleConfig {
|
|
137
|
+
/** Database adapter. Optional for autoApprove mode; required for magic link sessions. */
|
|
138
|
+
adapter?: ConsoleAdapter;
|
|
139
|
+
/** Hashed connection token from env, e.g. "sha256:abc123...". */
|
|
140
|
+
connectionTokenHash: string;
|
|
141
|
+
/** Origins allowed for CORS. Default: ["https://console.usebetter.dev"]. */
|
|
142
|
+
allowedOrigins?: string[];
|
|
143
|
+
sessions: ConsoleSessionConfig;
|
|
144
|
+
/** Permission levels granted to sessions. Default: ["read", "write", "admin"]. */
|
|
145
|
+
allowedActions?: ConsolePermission[];
|
|
146
|
+
/** Called when handleConsoleRequest catches an unexpected error. */
|
|
147
|
+
onError?: (error: unknown) => void;
|
|
148
|
+
}
|
|
149
|
+
interface BetterConsoleInstance {
|
|
150
|
+
initSession(request: ConsoleRequest): Promise<SessionInitResult>;
|
|
151
|
+
verifySession(token: string): Promise<ConsoleSession | null>;
|
|
152
|
+
refreshSession(token: string): Promise<{
|
|
153
|
+
sessionToken: string;
|
|
154
|
+
expiresIn: number;
|
|
155
|
+
} | null>;
|
|
156
|
+
revokeSession(sessionId: string): Promise<void>;
|
|
157
|
+
revokeAllSessions(): Promise<void>;
|
|
158
|
+
registerProduct(product: ConsoleProduct): void;
|
|
159
|
+
getRegisteredProducts(): ConsoleProduct[];
|
|
160
|
+
handleConsoleRequest(request: ConsoleRequest): Promise<ConsoleResponse>;
|
|
161
|
+
getCapabilities(): {
|
|
162
|
+
products: string[];
|
|
163
|
+
authMethods: string[];
|
|
164
|
+
permissions: ConsolePermission[];
|
|
165
|
+
};
|
|
166
|
+
config: Readonly<BetterConsoleConfig>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type { AuthenticatedConsoleRequest as A, BetterConsoleConfig as B, ConsoleMagicLinkRepository as C, MagicLinkConfig as M, SessionInitResult as S, ConsoleSessionRepository as a, ConsoleAdapter as b, ConsoleRequest as c, ConsoleResponse as d, BetterConsoleInstance as e, ConsoleSessionToken as f, ConsolePermission as g, ConsoleProduct as h, ConsoleMagicLink as i, ConsoleProductEndpoint as j, ConsoleSession as k, ConsoleSessionConfig as l };
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usebetterdev/console",
|
|
3
|
+
"description": "Console integration for Better* products. Session auth, magic links, dashboard endpoint registry.",
|
|
4
|
+
"version": "0.3.0-beta.2",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": "github:usebetter-dev/usebetter",
|
|
7
|
+
"bugs": "https://github.com/usebetter-dev/usebetter/issues",
|
|
8
|
+
"homepage": "https://github.com/usebetter-dev/usebetter#readme",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public",
|
|
11
|
+
"registry": "https://registry.npmjs.org/"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./drizzle": {
|
|
25
|
+
"types": "./dist/drizzle.d.ts",
|
|
26
|
+
"import": "./dist/drizzle.js",
|
|
27
|
+
"require": "./dist/drizzle.cjs",
|
|
28
|
+
"default": "./dist/drizzle.js"
|
|
29
|
+
},
|
|
30
|
+
"./hono": {
|
|
31
|
+
"types": "./dist/hono.d.ts",
|
|
32
|
+
"import": "./dist/hono.js",
|
|
33
|
+
"require": "./dist/hono.cjs",
|
|
34
|
+
"default": "./dist/hono.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"lint": "oxlint",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:integration": "vitest run -c vitest.integration.config.ts",
|
|
46
|
+
"typecheck": "tsc --noEmit"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"drizzle-orm": ">=0.36.0",
|
|
50
|
+
"hono": ">=4.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"drizzle-orm": { "optional": true },
|
|
54
|
+
"hono": { "optional": true }
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@testcontainers/postgresql": "^11.11.0",
|
|
58
|
+
"@types/node": "^22.10.0",
|
|
59
|
+
"@types/pg": "^8.11.0",
|
|
60
|
+
"@usebetterdev/test-utils": "workspace:*",
|
|
61
|
+
"drizzle-orm": "^0.36.0",
|
|
62
|
+
"hono": "^4.0.0",
|
|
63
|
+
"pg": "^8.13.0",
|
|
64
|
+
"tsup": "^8.3.5",
|
|
65
|
+
"typescript": "~5.7.2",
|
|
66
|
+
"vitest": "^2.1.6"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=22"
|
|
70
|
+
}
|
|
71
|
+
}
|