tetherdb 0.1.0 → 0.1.1
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/CHANGELOG.md +17 -0
- package/README.md +52 -5
- package/dist/cli/index.cjs +17 -2
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +17 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/server/index.cjs +17 -2
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.js +17 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/server.d.cts +6 -0
- package/dist/server/server.d.ts +6 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/vite/index.cjs +2050 -0
- package/dist/vite/index.cjs.map +1 -0
- package/dist/vite/index.d.cts +43 -0
- package/dist/vite/index.d.ts +43 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +2013 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +23 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/vite/index.ts","../../src/server/server.ts","../../src/shared/path.ts","../../src/server/crypto.ts","../../src/server/errors.ts","../../src/server/lock.ts","../../src/server/rate-limiter.ts","../../src/shared/clock.ts","../../src/shared/types.ts","../../src/server/validate.ts","../../src/server/storage/base/app.ts","../../src/server/storage/base/storage.ts","../../src/server/storage/base/table.ts","../../src/server/storage/base/user.ts","../../src/server/storage/memory/table.ts","../../src/server/storage/memory/app.ts","../../src/server/storage/memory/storage.ts","../../src/server/storage/memory/user.ts","../../src/server/sync.ts"],"sourcesContent":["/**\n * TetherDB Vite Plugin — Zero-config local development and preview server integration.\n *\n * @module tetherdb/vite\n */\n\nimport type { Plugin, PreviewServer, ViteDevServer } from 'vite';\nimport { TetherServer, type TetherServerOptions } from '../server/server.js';\nimport { MemoryStorage } from '../server/storage/memory/index.js';\n\n/**\n * Application and table declaration for automatic provisioning on startup.\n */\nexport interface TetherPluginAppDeclaration {\n /** Unique application identifier. */\n appId: string;\n /** Array of table names to declare within the application. */\n tables?: string[];\n}\n\n/**\n * User account declaration for automatic provisioning on startup.\n */\nexport interface TetherPluginUserDeclaration {\n /** Account username. */\n username: string;\n /** Account password. */\n password: string;\n}\n\n/**\n * Options for configuring the TetherDB Vite plugin.\n */\nexport interface TetherPluginOptions extends TetherServerOptions {\n /** Applications and tables to automatically declare on server startup. */\n apps?: TetherPluginAppDeclaration[];\n /** Default user accounts to automatically declare or update on server startup. */\n users?: TetherPluginUserDeclaration[];\n}\n\n/**\n * Creates a Vite plugin that runs an embedded TetherDB synchronization and REST\n * authentication backend directly within the Vite dev and preview servers.\n *\n * @param options - Configuration options for storage, endpoints, apps, and users.\n * @returns Vite plugin object.\n */\nexport function tetherPlugin(options: TetherPluginOptions = {}): Plugin {\n let tetherServer: TetherServer | null = null;\n\n async function setupServer(\n server: ViteDevServer | PreviewServer,\n ): Promise<void> {\n tetherServer = new TetherServer({\n storage: options.storage ?? new MemoryStorage(),\n logger: options.logger ?? false,\n ...options,\n });\n\n if (options.apps) {\n for (const app of options.apps) {\n await tetherServer.declareApp(app.appId, app.tables ?? []);\n }\n }\n\n if (options.users) {\n for (const user of options.users) {\n await tetherServer.declareUser(user.username, user.password);\n }\n }\n\n if (server.httpServer) {\n tetherServer.attach(\n server.httpServer as unknown as import('node:http').Server,\n );\n server.httpServer.on('close', () => {\n tetherServer?.close().catch(() => {\n // Ignore close errors during server shutdown\n });\n });\n }\n\n server.middlewares.use(tetherServer.createMiddleware());\n }\n\n return {\n name: 'vite-plugin-tetherdb',\n configureServer: setupServer,\n configurePreviewServer: setupServer,\n async closeBundle() {\n if (tetherServer) {\n await tetherServer.close();\n tetherServer = null;\n }\n },\n };\n}\n","import * as http from 'node:http';\nimport { WebSocketServer } from 'ws';\nimport { normalizeBasePath } from '../shared/path.js';\nimport { verifyDummyPasswordHash } from './crypto.js';\nimport { TetherServerError, TetherServerErrorCode } from './errors.js';\nimport { acquireServerLock, type ServerLockHandle } from './lock.js';\nimport { RateLimiter } from './rate-limiter.js';\nimport type { Storage, UserStorage } from './storage/index.js';\nimport { MemoryStorage } from './storage/memory/index.js';\nimport { Sync } from './sync.js';\nimport { normalizePassword, normalizeUsername } from './validate.js';\n\n/**\n * Rate limiting and resource control options for authentication endpoints and sync streams.\n */\nexport interface RateLimitOptions {\n /** Maximum login attempts per IP within the time window (defaults to 100). */\n ipLoginMaxRequests?: number;\n /** Maximum login attempts per target username within the time window (defaults to 20). */\n userLoginMaxRequests?: number;\n /** Maximum registration attempts per IP within the time window (defaults to 100). */\n ipRegisterMaxRequests?: number;\n /** Maximum WebSocket connection handshakes per IP within the time window (defaults to 100). */\n ipSyncMaxRequests?: number;\n /** Maximum concurrent active WebSocket connections allowed per user channel (defaults to 20). */\n maxConcurrentConnectionsPerUser?: number;\n /** Maximum duration in milliseconds to wait for authentication before terminating socket (defaults to 10,000ms). */\n authTimeoutMs?: number;\n /** Sliding window duration in milliseconds (defaults to 60,000ms / 1 minute). */\n windowMs?: number;\n /** Consecutive failed attempts before progressive backoff begins (defaults to 5). */\n maxFailures?: number;\n /** Initial backoff duration in milliseconds (defaults to 1,000ms). */\n initialBackoffMs?: number;\n /** Maximum backoff duration in milliseconds (defaults to 900,000ms / 15 minutes). */\n maxBackoffMs?: number;\n}\n\n/**\n * Options for configuring Cross-Origin Resource Sharing (CORS) on HTTP endpoints.\n */\nexport interface CorsOptions {\n /**\n * Allowed origin(s). Can be `'*'` for unrestricted access, a specific origin string (e.g. `'https://example.com'`),\n * an array of allowed origin strings, `true` to reflect the request's `Origin` header, or `false` to disable CORS headers.\n * Defaults to `'*'`.\n */\n origin?: string | string[] | boolean;\n /** Whether to set `Access-Control-Allow-Credentials: true` (defaults to false). */\n credentials?: boolean;\n /** Allowed request headers for preflight OPTIONS checks (defaults to `['Content-Type', 'Authorization']`). */\n allowedHeaders?: string[];\n /** Exposed response headers (Access-Control-Expose-Headers). */\n exposedHeaders?: string[];\n /** Maximum age in seconds to cache preflight responses (Access-Control-Max-Age). */\n maxAge?: number;\n}\n\n/**\n * Pluggable logger interface for TetherServer logging.\n */\nexport interface TetherLogger {\n /** Logs debug information. */\n debug(message: string, ...args: unknown[]): void;\n /** Logs operational information. */\n info(message: string, ...args: unknown[]): void;\n /** Logs warning conditions. */\n warn(message: string, ...args: unknown[]): void;\n /** Logs error conditions. */\n error(message: string, ...args: unknown[]): void;\n}\n\n/**\n * Configuration options for the TetherServer.\n */\nexport interface TetherServerOptions {\n /** Custom storage instance. Defaults to MemoryStorage if not defined. */\n storage?: Storage;\n /** Base path for HTTP REST endpoints (defaults to ''). */\n basePath?: string;\n /** Path for WebSocket upgrade requests (defaults to '/sync'). */\n webSocketPath?: string;\n /** Whether user self-registration is allowed via `/auth/register` (defaults to true). */\n allowRegistration?: boolean;\n /** Rate limiting options for auth and sync endpoints, or `false` to disable rate limiting (defaults to true). */\n rateLimiting?: boolean | RateLimitOptions;\n /** Whether to trust the `X-Forwarded-For` header for resolving client IP addresses (defaults to false). */\n trustProxy?: boolean;\n /** CORS options for HTTP endpoints, `false` to disable CORS headers, or `true` for default permissive CORS (defaults to true). */\n cors?: boolean | CorsOptions;\n /** Optional custom logger instance, or `false` to silence internal server logs (defaults to `console`). */\n logger?: TetherLogger | false;\n}\n\n/**\n * Options for starting the standard server launcher.\n */\nexport interface StartServerOptions extends TetherServerOptions {\n /** Port number to bind (defaults to 8080 or PORT environment variable). */\n port?: number;\n /** Host interface to bind (defaults to '0.0.0.0'). */\n host?: string;\n}\n\n/**\n * Result returned when launching a server using `startServer()`.\n */\nexport interface RunningServer {\n /** The TetherServer instance. */\n server: TetherServer;\n /** The running Node.js HTTP server instance. */\n httpServer: http.Server;\n /** Bound port number. */\n port: number;\n /** Bound host address. */\n host: string;\n /** Closes both HTTP and WebSocket server cleanly. */\n close(): Promise<void>;\n}\n\n/**\n * Starts a complete standalone HTTP & WebSocket synchronization server.\n *\n * @param options - Start options including port, host, storage, and limits.\n * @returns Handle to the running server.\n */\nexport async function startServer(\n options: StartServerOptions = {},\n): Promise<RunningServer> {\n const port =\n options.port ??\n (process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8080);\n const host = options.host ?? '0.0.0.0';\n\n const server = new TetherServer(options);\n const httpServer = await server.listen(port, host);\n const addr = httpServer.address();\n const boundPort = typeof addr === 'object' && addr ? addr.port : port;\n\n return {\n server,\n httpServer,\n port: boundPort,\n host,\n close: async () => {\n await server.close();\n },\n };\n}\n\n/**\n * Unified HTTP and WebSocket server handling authentication endpoints (`/auth/register`, `/auth/login`)\n * and real-time streaming connections (`/sync`).\n */\nexport class TetherServer {\n /** Underlying storage engine for users, apps, and tables. */\n readonly storage: Storage;\n /** Real-time synchronization connection and broadcast coordinator. */\n readonly sync: Sync;\n /** Base path for HTTP REST endpoints. */\n readonly basePath: string;\n /** Path for WebSocket upgrade requests. */\n readonly webSocketPath: string;\n readonly trustProxy: boolean;\n private readonly allowRegistration: boolean;\n private readonly corsConfig: CorsOptions | null;\n private readonly logger: TetherLogger | null;\n private readonly ipLoginLimiter: RateLimiter | null;\n private readonly userLoginLimiter: RateLimiter | null;\n private readonly ipRegisterLimiter: RateLimiter | null;\n private _httpServer: http.Server | null = null;\n private _webSocketServer: WebSocketServer | null = null;\n private lockHandle: ServerLockHandle | null = null;\n\n /**\n * Initializes a new TetherServer instance.\n *\n * @param options - Configuration options for storage, endpoints, and rate limiting.\n */\n constructor(options: TetherServerOptions = {}) {\n this.storage = options.storage ?? new MemoryStorage();\n this.basePath = normalizeBasePath(options.basePath ?? '');\n this.webSocketPath = options.webSocketPath ?? `${this.basePath}/sync`;\n this.allowRegistration = options.allowRegistration ?? true;\n this.trustProxy = options.trustProxy ?? false;\n this.corsConfig =\n options.cors === false\n ? null\n : typeof options.cors === 'object'\n ? options.cors\n : {};\n this.logger = options.logger === false ? null : (options.logger ?? console);\n\n const rateLimitConfig = options.rateLimiting ?? true;\n if (rateLimitConfig === false) {\n this.ipLoginLimiter = null;\n this.userLoginLimiter = null;\n this.ipRegisterLimiter = null;\n this.sync = new Sync(this.storage, {\n maxConcurrentConnectionsPerUser: 1_000,\n authTimeoutMs: 0,\n rateLimiter: null,\n logger: this.logger,\n });\n } else {\n const opts: RateLimitOptions =\n typeof rateLimitConfig === 'object' ? rateLimitConfig : {};\n const windowMs = opts.windowMs ?? 60_000;\n const maxFailures = opts.maxFailures ?? 5;\n const initialBackoffMs = opts.initialBackoffMs ?? 1_000;\n const maxBackoffMs = opts.maxBackoffMs ?? 900_000;\n\n this.ipLoginLimiter = new RateLimiter({\n windowMs,\n maxRequests: opts.ipLoginMaxRequests ?? 100,\n maxFailures,\n initialBackoffMs,\n maxBackoffMs,\n });\n this.userLoginLimiter = new RateLimiter({\n windowMs,\n maxRequests: opts.userLoginMaxRequests ?? 20,\n maxFailures,\n initialBackoffMs,\n maxBackoffMs,\n });\n this.ipRegisterLimiter = new RateLimiter({\n windowMs,\n maxRequests: opts.ipRegisterMaxRequests ?? 100,\n });\n\n const syncLimiter = new RateLimiter({\n windowMs,\n maxRequests: opts.ipSyncMaxRequests ?? 100,\n maxFailures,\n initialBackoffMs,\n maxBackoffMs,\n });\n\n this.sync = new Sync(this.storage, {\n maxConcurrentConnectionsPerUser:\n opts.maxConcurrentConnectionsPerUser ?? 20,\n authTimeoutMs: opts.authTimeoutMs ?? 10_000,\n rateLimiter: syncLimiter,\n logger: this.logger,\n });\n }\n }\n\n /**\n * Active Node.js HTTP server instance, or `null` if not listening.\n */\n get httpServer(): http.Server | null {\n return this._httpServer;\n }\n\n /**\n * Active WebSocketServer instance, or `null` if not listening.\n */\n get webSocketServer(): WebSocketServer | null {\n return this._webSocketServer;\n }\n\n /**\n * Declares an application and its tables.\n * Registers the application and any declared tables if not already present.\n *\n * @param appId - Application identifier.\n * @param tables - Array of table names.\n */\n async declareApp(appId: string, tables: string[] = []): Promise<void> {\n let app = await this.storage.getApp(appId);\n if (!app) {\n app = await this.storage.createApp(appId);\n }\n for (const table of tables) {\n const existing = await app.getTable(table);\n if (!existing) {\n await app.createTable(table);\n }\n }\n }\n\n /**\n * Declares a user account with the specified username and password.\n * Creates the user if not already registered, or updates the existing user's password.\n *\n * @param username - Username for the account.\n * @param password - Plaintext password for the account.\n * @returns UserStorage handle for the declared user.\n */\n async declareUser(username: string, password: string): Promise<UserStorage> {\n const user = await this.storage.getUserByUsername(username);\n if (user) {\n await user.changePassword(password);\n return user;\n }\n return this.storage.createUser(username, password);\n }\n\n /**\n * Attaches WebSocket synchronization handling to an existing HTTP server.\n *\n * @param server - The HTTP server instance to attach to.\n */\n attach(server: http.Server): void {\n if (!this._webSocketServer) {\n this._webSocketServer = new WebSocketServer({\n noServer: true,\n perMessageDeflate: {\n zlibDeflateOptions: {\n level: 6,\n memLevel: 8,\n },\n threshold: 1024,\n clientNoContextTakeover: true,\n serverNoContextTakeover: true,\n },\n });\n this._webSocketServer.on('connection', (ws, req) => {\n const ip = req ? this.getClientIp(req) : '127.0.0.1';\n this.sync.handleConnection(ws, ip);\n });\n }\n server.on('upgrade', (req, socket, head) => {\n const url = new URL(\n req.url ?? '',\n `http://${req.headers.host ?? 'localhost'}`,\n );\n if (url.pathname === this.webSocketPath) {\n this._webSocketServer?.handleUpgrade(req, socket, head, (ws) => {\n this._webSocketServer?.emit('connection', ws, req);\n });\n }\n });\n }\n\n /**\n * Starts the HTTP and WebSocket server listening on the specified port and host.\n *\n * @param port - Port number to bind. Defaults to 8080.\n * @param host - Host interface to bind. Defaults to '0.0.0.0'.\n * @returns The active Node.js HTTP server instance.\n */\n async listen(port = 8080, host = '0.0.0.0'): Promise<http.Server> {\n const storageBaseDir = (\n this.storage as { baseDir?: string; inMemory?: boolean }\n ).baseDir;\n const isMemory =\n (this.storage as { inMemory?: boolean }).inMemory ??\n this.storage instanceof MemoryStorage;\n\n if (storageBaseDir && !isMemory) {\n const status = await this.storage.getStatus();\n this.lockHandle = acquireServerLock(storageBaseDir, {\n port,\n host,\n backend: status.backend,\n });\n }\n\n return new Promise<http.Server>((resolve, reject) => {\n this._httpServer = http.createServer(async (req, res) => {\n const handled = await this.handleHttpRequest(req, res);\n if (!handled) {\n this.sendJson(res, 404, { error: 'Not found' });\n }\n });\n this.attach(this._httpServer);\n this._httpServer.listen(port, host, () => {\n if (this._httpServer) {\n const addr = this._httpServer.address();\n const actualPort =\n typeof addr === 'object' && addr ? addr.port : port;\n if (\n this.lockHandle &&\n storageBaseDir &&\n this.lockHandle.info.port !== actualPort\n ) {\n this.lockHandle.release();\n this.lockHandle = acquireServerLock(storageBaseDir, {\n port: actualPort,\n host,\n backend: this.lockHandle.info.backend,\n });\n }\n resolve(this._httpServer);\n }\n });\n this._httpServer.on('error', (err) => {\n if (this.lockHandle) {\n this.lockHandle.release();\n this.lockHandle = null;\n }\n reject(err);\n });\n });\n }\n\n /**\n * Closes active HTTP server and WebSocket server listeners.\n */\n async close(): Promise<void> {\n if (this.lockHandle) {\n this.lockHandle.release();\n this.lockHandle = null;\n }\n return new Promise<void>((resolve, reject) => {\n if (this._webSocketServer) {\n for (const client of this._webSocketServer.clients) {\n try {\n client.terminate();\n } catch {\n // Ignore termination errors on shutdown\n }\n }\n this._webSocketServer.close();\n this._webSocketServer = null;\n }\n if (this._httpServer) {\n try {\n this._httpServer.closeAllConnections?.();\n } catch {\n // Ignore\n }\n this._httpServer.close((err) => {\n this._httpServer = null;\n if (err) reject(err);\n else resolve();\n });\n } else {\n resolve();\n }\n });\n }\n\n /**\n * Creates a Connect- and Express-compatible HTTP middleware handler.\n *\n * @returns Middleware function `(req, res, next) => void`.\n */\n createMiddleware(): (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n next: (err?: unknown) => void,\n ) => void {\n return (req, res, next) => {\n this.handleHttpRequest(req, res).then(\n (handled) => {\n if (!handled) next();\n },\n (err) => {\n next(err);\n },\n );\n };\n }\n\n /**\n * Handles incoming HTTP requests for authentication and discovery endpoints.\n *\n * @param req - Incoming HTTP request.\n * @param res - Server HTTP response.\n * @returns `true` if the request was handled by TetherDB; `false` if the path did not match.\n */\n async handleHttpRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<boolean> {\n const url = new URL(\n req.url ?? '/',\n `http://${req.headers.host ?? 'localhost'}`,\n );\n const method = req.method?.toUpperCase();\n\n if (method === 'OPTIONS') {\n this.handleOptions(req, res);\n return true;\n }\n\n try {\n if (method === 'GET' && url.pathname === `${this.basePath}/health`) {\n this.handleHealth(req, res);\n return true;\n }\n\n if (method === 'GET' && url.pathname === `${this.basePath}/ready`) {\n await this.handleReady(req, res);\n return true;\n }\n\n if (method === 'GET' && url.pathname === `${this.basePath}/metrics`) {\n await this.handleMetrics(req, res);\n return true;\n }\n\n if (\n this.allowRegistration &&\n method === 'POST' &&\n url.pathname === `${this.basePath}/auth/register`\n ) {\n await this.handleRegister(req, res);\n return true;\n }\n\n if (method === 'POST' && url.pathname === `${this.basePath}/auth/login`) {\n await this.handleLogin(req, res);\n return true;\n }\n\n return false;\n } catch (err) {\n const status = getHttpStatusForError(err);\n if (status >= 500) {\n this.logger?.error('Error handling HTTP request:', err);\n } else {\n this.logger?.debug('Client error handling HTTP request:', err);\n }\n const msg = err instanceof Error ? err.message : 'Internal server error';\n this.sendJson(res, status, { error: msg }, req);\n return true;\n }\n }\n\n // -- Private Helpers ------------------------------------------------------\n\n private getCorsHeaders(req?: http.IncomingMessage): Record<string, string> {\n if (!this.corsConfig) return {};\n\n const headers: Record<string, string> = {\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': (\n this.corsConfig.allowedHeaders ?? ['Content-Type', 'Authorization']\n ).join(', '),\n };\n\n if (\n this.corsConfig.exposedHeaders &&\n this.corsConfig.exposedHeaders.length > 0\n ) {\n headers['Access-Control-Expose-Headers'] =\n this.corsConfig.exposedHeaders.join(', ');\n }\n\n if (this.corsConfig.maxAge !== undefined) {\n headers['Access-Control-Max-Age'] = String(this.corsConfig.maxAge);\n }\n\n const reqOrigin = req?.headers.origin;\n const origin = this.corsConfig.origin ?? '*';\n\n if (origin === '*') {\n if (this.corsConfig.credentials) {\n if (reqOrigin) {\n headers['Access-Control-Allow-Origin'] = reqOrigin;\n headers.Vary = 'Origin';\n }\n } else {\n headers['Access-Control-Allow-Origin'] = '*';\n }\n } else if (typeof origin === 'string') {\n headers['Access-Control-Allow-Origin'] = origin;\n headers.Vary = 'Origin';\n } else if (Array.isArray(origin)) {\n if (reqOrigin && origin.includes(reqOrigin)) {\n headers['Access-Control-Allow-Origin'] = reqOrigin;\n headers.Vary = 'Origin';\n }\n } else if (origin === true && reqOrigin) {\n headers['Access-Control-Allow-Origin'] = reqOrigin;\n headers.Vary = 'Origin';\n }\n\n if (this.corsConfig.credentials) {\n headers['Access-Control-Allow-Credentials'] = 'true';\n }\n\n return headers;\n }\n\n private sendJson(\n res: http.ServerResponse,\n status: number,\n data: unknown,\n req?: http.IncomingMessage,\n ) {\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n ...this.getCorsHeaders(req),\n });\n res.end(JSON.stringify(data));\n }\n\n private async readJsonBody(req: http.IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n if (body.length > 1024 * 1024) {\n reject(\n new TetherServerError(\n TetherServerErrorCode.LimitExceeded,\n 'Payload exceeds maximum allowed size',\n ),\n );\n }\n });\n req.on('end', () => {\n try {\n resolve(body ? JSON.parse(body) : {});\n } catch {\n reject(\n new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n 'Invalid JSON payload',\n ),\n );\n }\n });\n req.on('error', reject);\n });\n }\n\n private handleOptions(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): void {\n res.writeHead(204, this.getCorsHeaders(req));\n res.end();\n }\n\n private handleHealth(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): void {\n this.sendJson(\n res,\n 200,\n {\n status: 'ok',\n uptime: process.uptime(),\n },\n req,\n );\n }\n\n private async handleReady(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<void> {\n try {\n await this.storage.getApps();\n this.sendJson(res, 200, { status: 'ready' }, req);\n } catch (err) {\n const message =\n err instanceof Error ? err.message : 'Storage unavailable';\n this.logger?.error('Storage readiness error:', err);\n this.sendJson(res, 503, { status: 'unready', error: message }, req);\n }\n }\n\n private async handleMetrics(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<void> {\n const apps = await this.storage.getApps();\n this.sendJson(\n res,\n 200,\n {\n uptime: process.uptime(),\n connectedClients: this.sync.connectedClientsCount,\n appsCount: apps.length,\n memoryUsage: process.memoryUsage(),\n },\n req,\n );\n }\n\n private async handleRegister(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<void> {\n const ip = this.getClientIp(req);\n if (this.ipRegisterLimiter && !this.ipRegisterLimiter.consume(ip)) {\n this.sendJson(res, 429, { error: 'Too many registration requests' }, req);\n return;\n }\n\n const credentials = await this.readCredentials(req, res);\n if (!credentials) return;\n\n try {\n const user = await this.storage.createUser(\n credentials.username,\n credentials.password,\n );\n const token = await user.createToken();\n this.sendJson(\n res,\n 201,\n {\n userId: user.id,\n username: user.username,\n token,\n },\n req,\n );\n } catch (err) {\n const status = getHttpStatusForError(err);\n if (status >= 500) {\n this.logger?.error('Registration error:', err);\n } else {\n this.logger?.debug('Client registration error:', err);\n }\n const msg = err instanceof Error ? err.message : 'Registration error';\n this.sendJson(res, status, { error: msg }, req);\n }\n }\n\n private async handleLogin(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<void> {\n const ip = this.getClientIp(req);\n if (this.ipLoginLimiter && !this.ipLoginLimiter.consume(ip)) {\n this.sendJson(res, 429, { error: 'Too many login attempts' }, req);\n return;\n }\n\n const credentials = await this.readCredentials(req, res);\n if (!credentials) return;\n\n const userKey = `${ip}:${credentials.username}`;\n if (this.userLoginLimiter && !this.userLoginLimiter.consume(userKey)) {\n this.sendJson(\n res,\n 429,\n { error: 'Too many login attempts for this account' },\n req,\n );\n return;\n }\n\n const user = await this.storage.getUserByUsername(credentials.username);\n const valid = user\n ? await user.verifyPassword(credentials.password)\n : await verifyDummyPasswordHash(credentials.password);\n\n if (!user || !valid) {\n this.ipLoginLimiter?.recordFailure(ip);\n this.userLoginLimiter?.recordFailure(userKey);\n this.sendJson(\n res,\n 401,\n {\n error: 'Invalid username or password',\n },\n req,\n );\n return;\n }\n\n this.ipLoginLimiter?.reset(ip);\n this.userLoginLimiter?.reset(userKey);\n\n const token = await user.createToken();\n this.sendJson(\n res,\n 200,\n {\n userId: user.id,\n username: user.username,\n token,\n },\n req,\n );\n }\n\n private async readCredentials(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<{ username: string; password: string } | null> {\n const body = await this.readJsonBody(req);\n const { username, password } = body as {\n username?: string;\n password?: string;\n };\n const normUsername = normalizeUsername(username ?? '');\n const normPassword = normalizePassword(password ?? '');\n if (!normUsername || !normPassword) {\n this.sendJson(\n res,\n 400,\n {\n error: 'Missing or invalid required field: username and password',\n },\n req,\n );\n return null;\n }\n return { username: normUsername, password: normPassword };\n }\n\n private getClientIp(req: http.IncomingMessage): string {\n if (this.trustProxy) {\n const forwarded = req.headers['x-forwarded-for'];\n if (typeof forwarded === 'string') {\n const first = forwarded.split(',')[0].trim();\n if (first) return first;\n }\n }\n return req.socket.remoteAddress ?? '127.0.0.1';\n }\n}\n\n// -- Private Helpers --------------------------------------------------------\n\nfunction getHttpStatusForError(err: unknown): number {\n if (err instanceof TetherServerError) {\n switch (err.code) {\n case TetherServerErrorCode.InvalidInput:\n case TetherServerErrorCode.ConfigurationError:\n return 400;\n case TetherServerErrorCode.Unauthorized:\n case TetherServerErrorCode.AuthenticationFailed:\n return 401;\n case TetherServerErrorCode.NotFound:\n return 404;\n case TetherServerErrorCode.AlreadyExists:\n return 409;\n case TetherServerErrorCode.LimitExceeded:\n return 413;\n case TetherServerErrorCode.NotSupported:\n return 501;\n case TetherServerErrorCode.InternalError:\n return 500;\n }\n }\n return 500;\n}\n","/**\n * Utility functions for path normalization across client and server.\n */\n\n/**\n * Normalizes a base path ensuring it starts with a leading slash and has no trailing slash.\n * An empty string or single slash normalizes to an empty string.\n *\n * @param path - The base path to normalize.\n * @returns Normalized base path (e.g. '/api' or '').\n */\nexport function normalizeBasePath(path: string): string {\n if (path === '' || path === '/') return '';\n if (path.endsWith('/')) path = path.slice(0, path.length - 1);\n if (!path.startsWith('/')) path = `/${path}`;\n return path === '/' ? '' : path;\n}\n","import * as crypto from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/** Default token expiration window in seconds (7 days). */\nexport const DEFAULT_TOKEN_EXPIRES_IN = 7 * 24 * 60 * 60;\n\n/**\n * Loads a persistent HMAC signing secret from `<baseDir>/.secret`, or generates and saves one\n * with restricted permissions (`0o600`) if it does not yet exist.\n *\n * @param baseDir - Directory path where `.secret` is stored.\n * @returns 64-character hex secret string.\n */\nexport function getOrCreateKeyfileSecret(baseDir: string): string {\n const secretPath = path.join(baseDir, '.secret');\n try {\n if (fs.existsSync(secretPath)) {\n const existing = fs.readFileSync(secretPath, 'utf-8').trim();\n if (existing.length >= 32) {\n return existing;\n }\n }\n } catch {\n // Ignore read error and fallback to creation\n }\n\n const generated = crypto.randomBytes(32).toString('hex');\n try {\n fs.mkdirSync(baseDir, { recursive: true });\n fs.writeFileSync(secretPath, generated, {\n encoding: 'utf-8',\n mode: 0o600,\n });\n } catch {\n // If writing fails, return generated in-memory secret\n }\n return generated;\n}\n\n/**\n * Hashes a plaintext password using standard scrypt key derivation.\n *\n * @param password - The plaintext password to hash.\n * @returns Formatted hash string `scrypt$salt$derivedKey`.\n */\nexport async function hashPassword(password: string): Promise<string> {\n const salt = crypto.randomBytes(16).toString('hex');\n return new Promise((resolve, reject) => {\n crypto.scrypt(\n password.normalize('NFKC'),\n salt,\n 64,\n SCRYPT_OPTIONS,\n (err, derivedKey) => {\n if (err) return reject(err);\n resolve(`scrypt$${salt}$${derivedKey.toString('hex')}`);\n },\n );\n });\n}\n\n/**\n * Verifies a plaintext password against a stored scrypt hash using timing-safe comparison.\n *\n * @param password - The plaintext password to check.\n * @param storedHash - Stored hash string `scrypt$salt$derivedKey`.\n * @returns `true` if password matches; otherwise `false`.\n */\nexport async function verifyPasswordHash(\n password: string,\n storedHash: string,\n): Promise<boolean> {\n if (!storedHash || typeof storedHash !== 'string') return false;\n const parts = storedHash.split('$');\n if (parts.length !== 3 || parts[0] !== 'scrypt') return false;\n\n const [, salt, expectedHex] = parts;\n return new Promise((resolve) => {\n crypto.scrypt(\n password.normalize('NFKC'),\n salt,\n 64,\n SCRYPT_OPTIONS,\n (err, derivedKey) => {\n if (err) return resolve(false);\n const expectedBuf = Buffer.from(expectedHex, 'hex');\n if (derivedKey.length !== expectedBuf.length) return resolve(false);\n resolve(crypto.timingSafeEqual(derivedKey, expectedBuf));\n },\n );\n });\n}\n\n/**\n * Performs a constant-time dummy password verification to prevent user enumeration timing attacks.\n *\n * @param password - The plaintext password supplied by client.\n * @returns Always resolves to `false`.\n */\nexport async function verifyDummyPasswordHash(\n password: string,\n): Promise<boolean> {\n if (!dummyPasswordHashPromise) {\n dummyPasswordHashPromise = hashPassword('TetherDB:dummy_seed_password');\n }\n const dummyHash = await dummyPasswordHashPromise;\n return verifyPasswordHash(password, dummyHash);\n}\n\n/**\n * Generates a signed, URL-safe session token.\n *\n * @param userId - User account identifier.\n * @param username - Normalized username.\n * @param secret - Signing secret.\n * @param expiresInSeconds - Token expiration duration.\n * @returns Signed token string.\n */\nexport function createSessionToken(\n userId: string,\n username: string,\n secret: string,\n expiresInSeconds = DEFAULT_TOKEN_EXPIRES_IN,\n): string {\n const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;\n const payload = JSON.stringify({\n userId,\n username,\n expiresAt,\n });\n const payloadB64 = Buffer.from(payload, 'utf-8').toString('base64url');\n const signature = crypto\n .createHmac('sha256', secret)\n .update(payloadB64)\n .digest('base64url');\n return `${payloadB64}.${signature}`;\n}\n\n/**\n * Verifies a signed session token and returns decoded payload.\n *\n * @param token - Token string to verify.\n * @param secret - Signing secret.\n * @returns Decoded payload or `null` if invalid or expired.\n */\nexport function verifySessionToken(\n token: string,\n secret: string,\n): { userId: string; username: string; expiresAt: number } | null {\n if (!token || typeof token !== 'string') return null;\n const parts = token.split('.');\n if (parts.length !== 2) return null;\n\n const [payloadB64, signature] = parts;\n const expectedSig = crypto\n .createHmac('sha256', secret)\n .update(payloadB64)\n .digest('base64url');\n\n if (\n signature.length !== expectedSig.length ||\n !crypto.timingSafeEqual(\n Buffer.from(signature, 'utf-8'),\n Buffer.from(expectedSig, 'utf-8'),\n )\n ) {\n return null;\n }\n\n try {\n const raw = Buffer.from(payloadB64, 'base64url').toString('utf-8');\n const parsed = JSON.parse(raw);\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n typeof parsed.userId !== 'string' ||\n !parsed.userId ||\n typeof parsed.username !== 'string' ||\n !parsed.username ||\n typeof parsed.expiresAt !== 'number' ||\n !Number.isFinite(parsed.expiresAt) ||\n parsed.expiresAt < Math.floor(Date.now() / 1000)\n ) {\n return null;\n }\n return {\n userId: parsed.userId,\n username: parsed.username,\n expiresAt: parsed.expiresAt,\n };\n } catch {\n return null;\n }\n}\n\n// -- Private Helpers --------------------------------------------------------\n\n/** Default scrypt parameters matching standard cryptographic best practices. */\nconst SCRYPT_OPTIONS: crypto.ScryptOptions = {\n N: process.env.NODE_ENV === 'test' ? 512 : 16384,\n r: 8,\n p: 1,\n maxmem: 32 * 1024 * 1024,\n};\n\nlet dummyPasswordHashPromise: Promise<string> | null = null;\n","/**\n * Error codes identifying broader server-side error categories.\n */\nexport enum TetherServerErrorCode {\n /** The requested input or parameter is invalid or malformed. */\n InvalidInput,\n /** The requested resource (user, application, or table) was not found. */\n NotFound,\n /** The resource (user, application, or table) already exists. */\n AlreadyExists,\n /** Authentication is missing, invalid, or expired. */\n Unauthorized,\n /** The provided credentials are invalid. */\n AuthenticationFailed,\n /** A storage capacity or payload size limit was exceeded. */\n LimitExceeded,\n /** Configuration or command-line option is invalid. */\n ConfigurationError,\n /** The requested operation is not supported by this backend or engine. */\n NotSupported,\n /** An unexpected internal server error occurred. */\n InternalError,\n}\n\n/**\n * Dedicated error class for TetherDB server errors.\n * Error messages are user-safe and contain no internal technical details.\n */\nexport class TetherServerError extends Error {\n /** Error category code identifying the broad error type. */\n readonly code: TetherServerErrorCode;\n\n /**\n * Initializes a new `TetherServerError`.\n *\n * @param code - The error category code.\n * @param message - User-safe error description message.\n */\n constructor(code: TetherServerErrorCode, message?: string) {\n super(message ?? getDefaultServerErrorMessage(code));\n this.name = 'TetherServerError';\n this.code = code;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// -- Private Helpers --------------------------------------------------------\n\nfunction getDefaultServerErrorMessage(code: TetherServerErrorCode): string {\n switch (code) {\n case TetherServerErrorCode.InvalidInput:\n return 'Invalid request parameter';\n case TetherServerErrorCode.NotFound:\n return 'Requested resource not found';\n case TetherServerErrorCode.AlreadyExists:\n return 'Resource already exists';\n case TetherServerErrorCode.Unauthorized:\n return 'Authentication required';\n case TetherServerErrorCode.AuthenticationFailed:\n return 'Authentication failed';\n case TetherServerErrorCode.LimitExceeded:\n return 'Request or resource limit exceeded';\n case TetherServerErrorCode.ConfigurationError:\n return 'Server configuration error';\n case TetherServerErrorCode.NotSupported:\n return 'Operation not supported';\n case TetherServerErrorCode.InternalError:\n return 'Internal server error';\n }\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { TetherServerError, TetherServerErrorCode } from './errors.js';\n\n/**\n * Metadata recorded inside the server lockfile.\n */\nexport interface ServerLockInfo {\n /** Process identifier running the server. */\n pid: number;\n /** Port number the server is bound to. */\n port: number;\n /** Host interface the server is bound to. */\n host: string;\n /** Storage backend type ('sqlite', 'file', or 'memory'). */\n backend: string;\n /** Epoch timestamp when the server acquired the lock. */\n startedAt: number;\n}\n\n/**\n * Handle representing an active exclusive server lock.\n */\nexport interface ServerLockHandle {\n /** Lock metadata information. */\n readonly info: ServerLockInfo;\n /** Path to the active lockfile on disk. */\n readonly lockPath: string;\n /** Releases the lock and removes the lockfile. */\n release(): void;\n}\n\n/**\n * Checks whether a given operating system process ID is currently alive.\n *\n * @param pid - Process ID to check.\n * @returns `true` if the process is active; otherwise `false`.\n */\nexport function isProcessAlive(pid: number): boolean {\n if (!pid || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n return code === 'EPERM';\n }\n}\n\n/**\n * Reads active server lock metadata from a storage base directory if present and live.\n * Stale locks from terminated processes are ignored.\n *\n * @param baseDir - Storage base directory.\n * @returns ServerLockInfo if an active server is running, or `null`.\n */\nexport function readServerLock(baseDir: string): ServerLockInfo | null {\n const lockPath = path.join(baseDir, 'server.lock');\n try {\n if (!fs.existsSync(lockPath)) return null;\n const content = fs.readFileSync(lockPath, 'utf-8');\n const info = JSON.parse(content) as ServerLockInfo;\n if (typeof info.pid === 'number' && isProcessAlive(info.pid)) {\n return info;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Acquires an exclusive server lock on the specified directory to prevent multiple instances\n * from running against the same data storage.\n *\n * @param baseDir - Directory path where the lockfile will be maintained.\n * @param details - Port, host, and backend details to write to the lockfile.\n * @returns ServerLockHandle representing the active lock.\n * @throws TetherServerError if another active server already holds the lock.\n */\nexport function acquireServerLock(\n baseDir: string,\n details: { port: number; host: string; backend: string },\n): ServerLockHandle {\n fs.mkdirSync(baseDir, { recursive: true });\n const lockPath = path.join(baseDir, 'server.lock');\n\n const existing = readServerLock(baseDir);\n if (existing && existing.pid !== process.pid) {\n throw new TetherServerError(\n TetherServerErrorCode.AlreadyExists,\n 'A TetherDB server is already running on this data directory',\n );\n }\n\n // If a stale lockfile exists from a dead process, remove it\n if (fs.existsSync(lockPath)) {\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // Ignore\n }\n }\n\n const info: ServerLockInfo = {\n pid: process.pid,\n port: details.port,\n host: details.host,\n backend: details.backend,\n startedAt: Date.now(),\n };\n\n fs.writeFileSync(lockPath, JSON.stringify(info, null, 2), {\n encoding: 'utf-8',\n mode: 0o600,\n });\n\n let isReleased = false;\n const release = () => {\n if (isReleased) return;\n isReleased = true;\n try {\n if (fs.existsSync(lockPath)) {\n const current = JSON.parse(\n fs.readFileSync(lockPath, 'utf-8'),\n ) as ServerLockInfo;\n if (current.pid === process.pid) {\n fs.unlinkSync(lockPath);\n }\n }\n } catch {\n // Ignore cleanup error on shutdown\n }\n };\n\n return {\n info,\n lockPath,\n release,\n };\n}\n","/**\n * Configuration options for a RateLimiter instance.\n */\nexport interface RateLimiterOptions {\n /** Time window in milliseconds (defaults to 60,000ms / 1 minute). */\n windowMs?: number;\n /** Maximum number of allowed requests within the time window. */\n maxRequests?: number;\n /** Number of consecutive failures before applying progressive backoff (defaults to 3). */\n maxFailures?: number;\n /** Initial backoff duration in milliseconds after exceeding maxFailures (defaults to 1,000ms). */\n initialBackoffMs?: number;\n /** Maximum backoff duration in milliseconds (defaults to 900,000ms / 15 minutes). */\n maxBackoffMs?: number;\n /** Maximum number of tracking entries kept in memory before evicting (defaults to 10,000). */\n maxEntries?: number;\n}\n\n/**\n * In-memory sliding-window rate limiter with failure-based exponential backoff.\n */\nexport class RateLimiter {\n private readonly store = new Map<string, RateLimitEntry>();\n private readonly windowMs: number;\n private readonly maxRequests: number;\n private readonly maxFailures: number;\n private readonly initialBackoffMs: number;\n private readonly maxBackoffMs: number;\n private readonly maxEntries: number;\n\n /**\n * Initializes a new RateLimiter instance.\n *\n * @param options - Configuration options for window size, request limits, and backoff.\n */\n constructor(options: RateLimiterOptions = {}) {\n this.windowMs = options.windowMs ?? 60_000;\n this.maxRequests = options.maxRequests ?? 60;\n this.maxFailures = options.maxFailures ?? 3;\n this.initialBackoffMs = options.initialBackoffMs ?? 1_000;\n this.maxBackoffMs = options.maxBackoffMs ?? 900_000;\n this.maxEntries = options.maxEntries ?? 10_000;\n }\n\n /**\n * Returns the current number of tracked keys in memory.\n */\n get size(): number {\n return this.store.size;\n }\n\n /**\n * Checks whether the given key is currently rate limited or blocked by backoff cooldown.\n *\n * @param key - Identifier (e.g. IP address or username).\n * @param now - Current timestamp in milliseconds (defaults to Date.now()).\n * @returns `true` if requests for this key are limited; `false` otherwise.\n */\n isLimited(key: string, now = Date.now()): boolean {\n const entry = this.store.get(key);\n if (!entry) return false;\n\n if (entry.blockedUntil > now) {\n return true;\n }\n\n if (entry.resetAt <= now) {\n this.store.delete(key);\n return false;\n }\n\n return entry.count >= this.maxRequests;\n }\n\n /**\n * Consumes one attempt for the given key if not currently limited.\n *\n * @param key - Identifier.\n * @param now - Current timestamp in milliseconds.\n * @returns `true` if request was allowed and consumed; `false` if rate limited.\n */\n consume(key: string, now = Date.now()): boolean {\n if (this.isLimited(key, now)) {\n return false;\n }\n\n const entry = this.store.get(key);\n if (!entry || (entry.resetAt <= now && entry.blockedUntil <= now)) {\n this.setEntry(\n key,\n {\n count: 1,\n resetAt: now + this.windowMs,\n failures: 0,\n blockedUntil: 0,\n },\n now,\n );\n return true;\n }\n\n entry.count++;\n return true;\n }\n\n /**\n * Records a failed attempt for the given key and applies progressive exponential backoff.\n *\n * @param key - Identifier.\n * @param now - Current timestamp in milliseconds.\n * @returns Cooldown duration in milliseconds if blocked, or 0 if under failure threshold.\n */\n recordFailure(key: string, now = Date.now()): number {\n let entry = this.store.get(key);\n if (!entry || (entry.resetAt <= now && entry.blockedUntil <= now)) {\n entry = {\n count: 1,\n resetAt: now + this.windowMs,\n failures: 0,\n blockedUntil: 0,\n };\n this.setEntry(key, entry, now);\n }\n\n entry.failures++;\n\n if (entry.failures >= this.maxFailures) {\n const exponent = entry.failures - this.maxFailures;\n const backoff = Math.min(\n this.initialBackoffMs * 2 ** exponent,\n this.maxBackoffMs,\n );\n entry.blockedUntil = now + backoff;\n return backoff;\n }\n\n return 0;\n }\n\n /**\n * Resets all failure counters and request tracking for the given key.\n *\n * @param key - Identifier to reset.\n */\n reset(key: string): void {\n this.store.delete(key);\n }\n\n /**\n * Clears all stored rate limit entries.\n */\n clear(): void {\n this.store.clear();\n }\n\n /**\n * Purges expired entries from the internal store.\n *\n * @param now - Current timestamp in milliseconds.\n */\n cleanup(now = Date.now()): void {\n for (const [key, entry] of this.store.entries()) {\n if (entry.resetAt <= now && entry.blockedUntil <= now) {\n this.store.delete(key);\n }\n }\n }\n\n // -- Private Helpers --------------------------------------------------------\n\n private setEntry(key: string, entry: RateLimitEntry, now: number): void {\n if (this.store.size >= this.maxEntries && !this.store.has(key)) {\n this.cleanup(now);\n if (this.store.size >= this.maxEntries) {\n const oldestKey = this.store.keys().next().value;\n if (oldestKey !== undefined) {\n this.store.delete(oldestKey);\n }\n }\n }\n this.store.set(key, entry);\n }\n}\n\n// -- Private Helpers --------------------------------------------------------\n\ninterface RateLimitEntry {\n count: number;\n resetAt: number;\n failures: number;\n blockedUntil: number;\n}\n","import type { ChangeRecord, StoredRecord } from './types.js';\n\n/**\n * Determines whether an incoming mutation should overwrite an existing stored record\n * using Last-Write-Wins (LWW) conflict resolution.\n *\n * Conflict resolution rules:\n * 1. If no existing record exists, returns `true`.\n * 2. If incoming timestamp is strictly greater, returns `true`.\n * 3. If incoming timestamp is strictly less, returns `false`.\n * 4. If timestamps are equal, performs deterministic lexicographical tie-breaking using `clientId`.\n *\n * @param incoming - The candidate change record with timestamp and optional client metadata.\n * @param existing - The current record stored locally or on the server.\n * @returns `true` if the incoming change wins the conflict and should overwrite the existing record; otherwise `false`.\n */\nexport function shouldOverwrite(\n incoming: Pick<ChangeRecord, 'timestamp'> & {\n clientId?: string;\n version?: number;\n },\n existing?: Pick<StoredRecord, 'timestamp' | 'version'> & {\n clientId?: string;\n },\n): boolean {\n if (!existing) return true;\n\n if (incoming.timestamp > existing.timestamp) {\n return true;\n }\n if (incoming.timestamp < existing.timestamp) {\n return false;\n }\n\n const incomingClient = incoming.clientId ?? '';\n const existingClient = existing.clientId ?? '';\n return incomingClient >= existingClient;\n}\n","/**\n * The type of mutation operation performed on a record.\n */\nexport enum OperationType {\n /** Insert or update a record payload. */\n Put = 'put',\n /** Delete a record (tombstone). */\n Delete = 'delete',\n}\n\n/**\n * Represents an individual mutation operation record to be synced.\n *\n * @typeParam T - The data type of the record payload.\n */\nexport interface ChangeRecord<T = unknown> {\n /** The target table name. */\n table: string;\n /** The unique record identifier within the table. */\n id: string;\n /** The mutation operation type. */\n op: OperationType;\n /** The data payload for 'put' operations. */\n data?: T;\n /** Monotonic epoch timestamp when the change was initiated. */\n timestamp: number;\n /** Identifier of the client that originated the change. */\n clientId: string;\n /** Incremental version counter for the record. */\n version?: number;\n /** Server-assigned global sequential index. */\n seq?: number;\n}\n\n/**\n * Represents a persisted record with local metadata.\n *\n * @typeParam T - The data type of the stored record value.\n */\nexport interface StoredRecord<T = unknown> {\n /** The unique record identifier. */\n id: string;\n /** The stored value payload. */\n data: T;\n /** Epoch timestamp of the last write. */\n timestamp: number;\n /** Record revision version. */\n version: number;\n /** Flag indicating whether the record is marked as deleted (tombstone). */\n deleted?: boolean;\n /** Identifier of client that performed the write. */\n clientId?: string;\n}\n\n/**\n * Represents a single record entry in a full database snapshot.\n *\n * @typeParam T - The data type of the record payload.\n */\nexport interface SnapshotRecord<T = unknown> extends StoredRecord<T> {\n /** The table name. */\n table: string;\n}\n\n/**\n * Current wire protocol version number.\n */\nexport const PROTOCOL_VERSION = 1;\n\n/**\n * Types of messages sent from the client to the server over the WebSocket sync connection.\n */\nexport enum ClientMessageType {\n /** Authenticate connection with user token and initial sync sequence. */\n Auth = 'auth',\n /** Submit a batch of local pending changes to the server. */\n ChangeBatch = 'change_batch',\n /** Heartbeat ping message to verify connection liveness. */\n Ping = 'ping',\n}\n\n/**\n * Client authentication handshake message.\n */\nexport interface AuthClientMessage {\n type: ClientMessageType.Auth;\n /** Wire protocol version number (must equal `PROTOCOL_VERSION`). */\n protocolVersion: number;\n /** Signed authentication session token. */\n token: string;\n /** Unique client instance identifier. */\n clientId: string;\n /** Last synchronized sequence number known to the client. */\n lastSyncSeq?: number;\n /** Application namespace identifier. */\n appId: string;\n /** Optional client capabilities supported by this client session. */\n capabilities?: string[];\n}\n\n/**\n * Client mutation batch message.\n */\nexport interface ChangeBatchClientMessage {\n type: ClientMessageType.ChangeBatch;\n /** Unique client instance identifier. */\n clientId: string;\n /** Unique batch correlation identifier. */\n batchId: string;\n /** Array of change operations to apply. */\n changes: ChangeRecord[];\n}\n\n/**\n * Client heartbeat ping message.\n */\nexport interface PingClientMessage {\n type: ClientMessageType.Ping;\n}\n\n/**\n * Discriminated union of all messages sent from client to server.\n */\nexport type ClientMessage =\n | AuthClientMessage\n | ChangeBatchClientMessage\n | PingClientMessage;\n\n/**\n * Types of messages sent from the server to the client over the WebSocket sync connection.\n */\nexport enum ServerMessageType {\n /** Authentication succeeded. */\n AuthSuccess = 'auth_success',\n /** Authentication failed. */\n AuthError = 'auth_error',\n /** Full dataset snapshot sent when client connects without prior sync sequence. */\n SyncSnapshot = 'sync_snapshot',\n /** Delta diff of changes that occurred since client's lastSyncSeq. */\n SyncDiff = 'sync_diff',\n /** Confirmation that a client change batch was applied. */\n ChangeAck = 'change_ack',\n /** Real-time broadcast of changes applied by another client session of the same user. */\n BroadcastChanges = 'broadcast_changes',\n /** Heartbeat pong response. */\n Pong = 'pong',\n /** General server error notification. */\n Error = 'error',\n}\n\n/**\n * Server authentication success response message.\n */\nexport interface AuthSuccessServerMessage {\n type: ServerMessageType.AuthSuccess;\n /** Wire protocol version number. */\n protocolVersion: number;\n /** Authenticated user account identifier. */\n userId: string;\n /** Current global sequence number of the user's data on the server. */\n currentSeq: number;\n /** Refreshed session token for sliding session validity. */\n token?: string;\n /** Optional server capabilities supported by this server instance. */\n capabilities?: string[];\n}\n\n/**\n * Server authentication failure response message.\n */\nexport interface AuthErrorServerMessage {\n type: ServerMessageType.AuthError;\n /** Error description message. */\n message: string;\n}\n\n/**\n * Server full dataset snapshot message.\n */\nexport interface SyncSnapshotServerMessage {\n type: ServerMessageType.SyncSnapshot;\n /** Sequence number corresponding to the snapshot state. */\n seq: number;\n /** All active records across tables. */\n snapshot: SnapshotRecord[];\n}\n\n/**\n * Server incremental delta diff message.\n */\nexport interface SyncDiffServerMessage {\n type: ServerMessageType.SyncDiff;\n /** Starting sequence number (exclusive). */\n fromSeq: number;\n /** Ending sequence number (inclusive). */\n toSeq: number;\n /** Array of applied changes in sequential order. */\n changes: ChangeRecord[];\n}\n\n/**\n * Server batch acknowledgement message.\n */\nexport interface ChangeAckServerMessage {\n type: ServerMessageType.ChangeAck;\n /** The correlation identifier of the acknowledged batch. */\n batchId: string;\n /** The new global sequence number after applying the batch. */\n appliedSeq: number;\n}\n\n/**\n * Server real-time changes broadcast message.\n */\nexport interface BroadcastChangesServerMessage {\n type: ServerMessageType.BroadcastChanges;\n /** Client ID that originated the change. */\n fromClientId: string;\n /** Global sequence number assigned to these changes. */\n seq: number;\n /** Array of applied changes. */\n changes: ChangeRecord[];\n}\n\n/**\n * Server heartbeat pong response message.\n */\nexport interface PongServerMessage {\n type: ServerMessageType.Pong;\n}\n\n/**\n * Server error notification message.\n */\nexport interface ErrorServerMessage {\n type: ServerMessageType.Error;\n /** Error description message. */\n message: string;\n}\n\n/**\n * Discriminated union of all messages sent from server to client.\n */\nexport type ServerMessage =\n | AuthSuccessServerMessage\n | AuthErrorServerMessage\n | SyncSnapshotServerMessage\n | SyncDiffServerMessage\n | ChangeAckServerMessage\n | BroadcastChangesServerMessage\n | PongServerMessage\n | ErrorServerMessage;\n\n/**\n * Metadata stored locally tracking synchronization progress.\n */\nexport interface SyncMetadata {\n /** The latest sequence number synchronized with the server. */\n lastSyncSeq: number;\n /** Epoch timestamp of the last successful synchronization. */\n lastSyncTimestamp: number;\n /** Unique client instance identifier. */\n clientId: string;\n}\n","/**\n * Security, input validation, and sanitization utilities for the TetherDB server.\n * Protects server-side storage and synchronization from injection and traversal attacks.\n *\n * @module tetherdb/server/validate\n */\n\nimport { TetherServerError, TetherServerErrorCode } from './errors.js';\n\n/** Minimum allowed username character length. */\nexport const MIN_USERNAME_LENGTH = 4;\n\n/** Maximum allowed username character length. */\nexport const MAX_USERNAME_LENGTH = 128;\n\n/** Minimum allowed password character length. */\nexport const MIN_PASSWORD_LENGTH = 4;\n\n/** Maximum allowed password character length. */\nexport const MAX_PASSWORD_LENGTH = 512;\n\n/** Maximum allowable future timestamp drift in milliseconds (5 minutes). */\nexport const MAX_FUTURE_TIMESTAMP_DRIFT_MS = 5 * 60 * 1000;\n\n/**\n * Validates a change timestamp, ensuring it is a valid finite epoch number\n * and does not exceed the maximum allowable future drift.\n *\n * @param timestamp - The epoch timestamp in milliseconds.\n * @param maxFutureDriftMs - Optional maximum allowable future drift in ms (defaults to 5 minutes).\n * @returns The validated timestamp.\n * @throws TetherServerError if timestamp is not finite, is non-positive, or exceeds drift bounds.\n */\nexport function validateTimestamp(\n timestamp: number,\n maxFutureDriftMs = MAX_FUTURE_TIMESTAMP_DRIFT_MS,\n): number {\n if (\n typeof timestamp !== 'number' ||\n !Number.isFinite(timestamp) ||\n timestamp <= 0\n ) {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n 'Invalid timestamp',\n );\n }\n if (timestamp > Date.now() + maxFutureDriftMs) {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n 'Timestamp drift exceeds maximum allowable threshold',\n );\n }\n return timestamp;\n}\n\n/**\n * Validates a user ID string ensuring it is safe for filesystem use.\n *\n * @param userId - The user ID to validate.\n * @returns The validated user ID.\n * @throws TetherServerError if the user ID is invalid or contains unsafe characters.\n */\nexport function validateUserId(userId: string): string {\n return validateFilesystemSafe(userId, 'user ID');\n}\n\n/**\n * Validates an application namespace identifier.\n *\n * @param appId - The application ID to validate.\n * @returns The validated application ID.\n * @throws TetherServerError if the application ID is invalid.\n */\nexport function validateAppId(appId: string): string {\n return validateFilesystemSafe(appId, 'application ID');\n}\n\n/**\n * Validates a table name ensuring it is safe for filesystem use.\n *\n * @param tableName - The table name to validate.\n * @returns The validated table name.\n * @throws TetherServerError if the table name is invalid.\n */\nexport function validateTableName(tableName: string): string {\n return validateFilesystemSafe(tableName, 'table name');\n}\n\n/**\n * Validates a record ID ensuring it is a non-empty string within size limits.\n *\n * @param id - The record identifier to validate.\n * @returns The validated record ID.\n * @throws TetherServerError if the record ID is invalid or exceeds max length.\n */\nexport function validateRecordId(id: string): string {\n if (typeof id !== 'string' || id.length === 0 || id.length > 512) {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n 'Invalid record ID',\n );\n }\n return id;\n}\n\n/**\n * Normalizes a username by trimming whitespace and converting to lowercase.\n *\n * @param username - The raw username string.\n * @returns The normalized username (lowercase and trimmed).\n */\nexport function normalizeUsername(username: string): string {\n return typeof username === 'string' ? username.trim().toLowerCase() : '';\n}\n\n/**\n * Validates and normalizes a username for account creation or authentication.\n * Usernames must be between 4 and 128 characters long.\n *\n * @param username - The username to validate.\n * @returns The validated and normalized username (trimmed and lowercase).\n * @throws TetherServerError if the username is invalid or out of length bounds.\n */\nexport function validateUsername(username: string): string {\n if (typeof username !== 'string') {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n `Username must be between ${MIN_USERNAME_LENGTH} and ${MAX_USERNAME_LENGTH} characters`,\n );\n }\n const normalized = normalizeUsername(username);\n if (\n normalized.length < MIN_USERNAME_LENGTH ||\n normalized.length > MAX_USERNAME_LENGTH\n ) {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n `Username must be between ${MIN_USERNAME_LENGTH} and ${MAX_USERNAME_LENGTH} characters`,\n );\n }\n return normalized;\n}\n\n/**\n * Normalizes a password by trimming surrounding whitespace.\n *\n * @param password - The raw password string.\n * @returns The normalized password (trimmed).\n */\nexport function normalizePassword(password: string): string {\n return typeof password === 'string' ? password.trim() : '';\n}\n\n/**\n * Validates a password for user account creation or authentication.\n *\n * @param password - The password string to validate.\n * @returns The validated password.\n * @throws TetherServerError if the password is not a string, is empty, or exceeds length bounds.\n */\nexport function validatePassword(password: string): string {\n if (typeof password !== 'string') {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n 'Password must be a valid non-empty string',\n );\n }\n const normalized = normalizePassword(password);\n if (\n normalized.length < MIN_PASSWORD_LENGTH ||\n normalized.length > MAX_PASSWORD_LENGTH\n ) {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n `Password must be between ${MIN_PASSWORD_LENGTH} and ${MAX_PASSWORD_LENGTH} characters`,\n );\n }\n return normalized;\n}\n\n/**\n * Validates a client correlation or batch ID.\n *\n * @param id - The batch or client identifier.\n * @param name - Identifier type description (e.g. 'batchId', 'clientId').\n * @returns The validated ID.\n * @throws TetherServerError if the identifier format is invalid.\n */\nexport function validateIdentifier(id: string, name = 'identifier'): string {\n if (typeof id !== 'string' || !/^[a-zA-Z0-9_-]{2,128}$/.test(id)) {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n `Invalid ${name}`,\n );\n }\n return id;\n}\n\n/**\n * Estimates the byte size of an arbitrary JavaScript value or object when serialized.\n *\n * @param value - The value to measure.\n * @returns Size in bytes.\n */\nexport function calculateByteSize(value: unknown): number {\n if (value === null || value === undefined) return 0;\n if (typeof value === 'string') return Buffer.byteLength(value, 'utf-8');\n if (typeof value === 'number') return 8;\n if (typeof value === 'boolean') return 4;\n try {\n return Buffer.byteLength(JSON.stringify(value), 'utf-8');\n } catch {\n return 0;\n }\n}\n\n/**\n * Calculates a 2-character hex/hash bucket for directory partitioning by user ID.\n *\n * @param userId - Unique user identifier.\n * @returns 2-character bucket string (e.g. 'f4', '0a').\n */\nexport function getUserBucket(userId: string): string {\n const safeId = validateUserId(userId);\n const clean = safeId.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();\n return clean.length >= 2 ? clean.slice(0, 2) : clean.padStart(2, '0');\n}\n\n// -- Private Helpers --------------------------------------------------------\n\nfunction validateFilesystemSafe(id: string, name: string): string {\n if (typeof id !== 'string' || !/^[a-zA-Z0-9_-]{1,64}$/.test(id)) {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n `Invalid ${name}`,\n );\n }\n return id;\n}\n","import {\n type ChangeRecord,\n OperationType,\n type StoredRecord,\n} from '../../../shared/types.js';\nimport type { AppStorage } from '../app.js';\nimport type { TableStorage } from '../table.js';\nimport type { UserStorage } from '../user.js';\n\n/**\n * Common abstract base class for AppStorage implementations.\n */\nexport abstract class AppBaseStorage implements AppStorage {\n readonly id: string;\n\n constructor(id: string) {\n this.id = id;\n }\n\n /** Creates/registers a new table within this application. */\n abstract createTable(name: string): Promise<TableStorage>;\n\n /** Retrieves a table handle if it exists. */\n abstract getTable(name: string): Promise<TableStorage | undefined>;\n\n /** Lists all registered tables in this application. */\n abstract getTables(): Promise<TableStorage[]>;\n\n /** Applies batch mutation changes for a user. */\n abstract applyChanges(\n user: UserStorage,\n changes: ChangeRecord[],\n ): Promise<{ applied: ChangeRecord[]; newSeq: number }>;\n\n /** Retrieves changes for a user since a given sequence number. */\n abstract getChangesSince(\n user: UserStorage,\n fromSeq: number,\n ): Promise<{\n changes: ChangeRecord[];\n currentSeq: number;\n requiresSnapshot?: boolean;\n }>;\n\n /** Returns the current global sequence number for a user within this application. */\n abstract getCurrentSeq(user: UserStorage): Promise<number>;\n\n /** Deletes this entire application and all associated data. */\n abstract delete(): Promise<boolean>;\n}\n\n/**\n * Applies a change to an existing (or undefined) record and assigns the sequence number.\n */\nexport function applyChangeToRecord(\n change: ChangeRecord,\n existing: StoredRecord | undefined,\n seq: number,\n): {\n updatedRecord: StoredRecord;\n appliedChange: ChangeRecord & { seq: number };\n} {\n const isDeleted = change.op === OperationType.Delete;\n const nextVersion = (existing?.version ?? 0) + 1;\n\n const updatedRecord: StoredRecord = {\n id: change.id,\n version: nextVersion,\n timestamp: change.timestamp,\n clientId: change.clientId,\n deleted: isDeleted,\n data: isDeleted ? null : (change.data ?? null),\n };\n\n const appliedChange: ChangeRecord & { seq: number } = {\n seq,\n table: change.table,\n id: change.id,\n op: change.op,\n version: nextVersion,\n timestamp: change.timestamp,\n clientId: change.clientId,\n data: isDeleted ? undefined : change.data,\n };\n\n return { updatedRecord, appliedChange };\n}\n","import { verifySessionToken } from '../../crypto.js';\nimport { TetherServerError, TetherServerErrorCode } from '../../errors.js';\nimport { validateAppId } from '../../validate.js';\nimport type { AppStorage } from '../app.js';\nimport type {\n MaintenanceResult,\n Storage,\n StorageOptions,\n StorageStatus,\n} from '../storage.js';\nimport type { UserStorage } from '../user.js';\n\n/**\n * Common abstract base class for Storage implementations.\n */\nexport abstract class BaseStorage implements Storage {\n readonly options?: StorageOptions;\n\n constructor(options?: StorageOptions) {\n this.options = options;\n }\n\n /** Backend persistence type name ('file', 'memory', 'sqlite'). */\n abstract readonly backend: string;\n\n /** Secret key used for signing session tokens. */\n abstract readonly secret: string;\n\n /** Optional storage base directory if disk-backed. */\n protected getBaseDir(): string | undefined {\n return undefined;\n }\n\n abstract createApp(id: string): Promise<AppStorage>;\n abstract getApp(id: string): Promise<AppStorage | undefined>;\n abstract getApps(): Promise<AppStorage[]>;\n abstract createUser(username: string, password: string): Promise<UserStorage>;\n abstract getUser(id: string): Promise<UserStorage | undefined>;\n abstract getUserByUsername(\n username: string,\n ): Promise<UserStorage | undefined>;\n abstract getUsers(): Promise<UserStorage[]>;\n abstract checkpoint(appId?: string): Promise<MaintenanceResult>;\n abstract vacuum(appId?: string): Promise<MaintenanceResult>;\n abstract prune(\n appId?: string,\n keepCount?: number,\n ): Promise<MaintenanceResult>;\n\n async getUserByToken(token: string): Promise<UserStorage | undefined> {\n const payload = verifySessionToken(token, this.secret);\n if (!payload) return undefined;\n return this.getUser(payload.userId);\n }\n\n async getStatus(appId?: string): Promise<StorageStatus> {\n const users = await this.getUsers();\n const allApps = await this.getApps();\n const targetApps = filterTargetApps(allApps, appId);\n const apps = await buildAppSummaries(targetApps);\n\n const status: StorageStatus = {\n backend: this.backend,\n usersCount: users.length,\n appsCount: allApps.length,\n apps,\n };\n const baseDir = this.getBaseDir();\n if (baseDir !== undefined) {\n status.baseDir = baseDir;\n }\n return status;\n }\n}\n\n/**\n * Filters the list of applications by an optional target appId, validating format and existence.\n */\nexport function filterTargetApps(\n allApps: AppStorage[],\n appId?: string,\n): AppStorage[] {\n const targetApps = appId\n ? allApps.filter((a) => a.id === validateAppId(appId))\n : allApps;\n\n if (appId && targetApps.length === 0) {\n throw new TetherServerError(\n TetherServerErrorCode.NotFound,\n `Application \"${appId}\" not found`,\n );\n }\n\n return targetApps;\n}\n\n/**\n * Builds array of application summaries including their table names.\n */\nexport async function buildAppSummaries(\n apps: AppStorage[],\n): Promise<Array<{ id: string; tables: string[] }>> {\n const appSummaries: Array<{ id: string; tables: string[] }> = [];\n for (const app of apps) {\n const tables = await app.getTables();\n appSummaries.push({\n id: app.id,\n tables: tables.map((t) => t.name),\n });\n }\n return appSummaries;\n}\n","import type {\n ChangeRecord,\n SnapshotRecord,\n StoredRecord,\n} from '../../../shared/types.js';\nimport type { AppStorage } from '../app.js';\nimport type { TableStorage } from '../table.js';\nimport type { UserStorage } from '../user.js';\n\n/**\n * Common abstract base class for TableStorage implementations.\n */\nexport abstract class TableBaseStorage implements TableStorage {\n readonly name: string;\n readonly app: AppStorage;\n\n constructor(name: string, app: AppStorage) {\n this.name = name;\n this.app = app;\n }\n\n /** Retrieves a single record for a user. */\n abstract getRecord(\n user: UserStorage,\n id: string,\n ): Promise<StoredRecord | undefined>;\n\n /** Retrieves all active records in this table for a user. */\n abstract getAllRecords(user: UserStorage): Promise<SnapshotRecord[]>;\n\n /** Deletes this table and its data. */\n abstract delete(): Promise<boolean>;\n\n /** Applies batch mutation changes to this table. */\n async applyChanges(\n user: UserStorage,\n changes: ChangeRecord[],\n ): Promise<{ applied: ChangeRecord[]; newSeq: number }> {\n return this.app.applyChanges(\n user,\n targetChangesForTable(this.name, this.app.id, changes),\n );\n }\n}\n\n/**\n * Filters non-deleted records and attaches table name for snapshot responses.\n */\nexport function filterActiveRecords(\n tableName: string,\n records: Iterable<StoredRecord>,\n): SnapshotRecord[] {\n const items: SnapshotRecord[] = [];\n for (const rec of records) {\n if (!rec.deleted) {\n items.push({\n ...rec,\n table: tableName,\n });\n }\n }\n return items;\n}\n\n/**\n * Targets generic changes with table name and app ID.\n */\nexport function targetChangesForTable(\n tableName: string,\n appId: string,\n changes: ChangeRecord[],\n): ChangeRecord[] {\n return changes.map((c) => ({\n ...c,\n table: tableName,\n appId,\n }));\n}\n","import {\n createSessionToken,\n hashPassword,\n verifyPasswordHash,\n verifySessionToken,\n} from '../../crypto.js';\nimport { normalizePassword, validatePassword } from '../../validate.js';\nimport type { UserStorage } from '../user.js';\n\n/**\n * Common abstract base class for UserStorage implementations.\n */\nexport abstract class UserBaseStorage implements UserStorage {\n readonly id: string;\n readonly username: string;\n readonly createdAt: number;\n\n constructor(id: string, username: string, createdAt: number) {\n this.id = id;\n this.username = username;\n this.createdAt = createdAt;\n }\n\n /** Retrieves the secret key used for signing session tokens. */\n protected abstract getSecret(): string;\n\n /** Verifies if the plaintext password matches. */\n abstract verifyPassword(password: string): Promise<boolean>;\n\n /** Updates user credentials with a new password. */\n abstract changePassword(newPassword: string): Promise<void>;\n\n /** Deletes the user account and associated data. */\n abstract delete(): Promise<boolean>;\n\n /** Creates a signed session token for this user. */\n async createToken(expiresInSeconds?: number): Promise<string> {\n return createSessionToken(\n this.id,\n this.username,\n this.getSecret(),\n expiresInSeconds,\n );\n }\n\n /** Verifies whether the session token is valid for this user. */\n async verifyToken(token: string): Promise<boolean> {\n const payload = verifySessionToken(token, this.getSecret());\n return payload !== null && payload.userId === this.id;\n }\n}\n\n/**\n * Verifies a candidate plaintext password against a stored bcrypt hash.\n */\nexport async function verifyUserPassword(\n password: string,\n passwordHash: string | null | undefined,\n): Promise<boolean> {\n if (!passwordHash) return false;\n const normalized = normalizePassword(password);\n if (!normalized) return false;\n return verifyPasswordHash(normalized, passwordHash);\n}\n\n/**\n * Hashes a validated plaintext password.\n */\nexport async function hashUserPassword(newPassword: string): Promise<string> {\n const valid = validatePassword(newPassword);\n return hashPassword(valid);\n}\n","import type { SnapshotRecord, StoredRecord } from '../../../shared/types.js';\nimport { validateRecordId } from '../../validate.js';\nimport { filterActiveRecords, TableBaseStorage } from '../base/index.js';\nimport type { UserStorage } from '../user.js';\nimport type { AppMemoryStorage } from './app.js';\nimport type { MemoryStorage } from './storage.js';\n\n/**\n * In-memory implementation of `TableStorage`.\n */\nexport class TableMemoryStorage extends TableBaseStorage {\n declare readonly app: AppMemoryStorage;\n private storage: MemoryStorage;\n\n constructor(name: string, app: AppMemoryStorage, storage: MemoryStorage) {\n super(name, app);\n this.storage = storage;\n }\n\n async getRecord(\n user: UserStorage,\n id: string,\n ): Promise<StoredRecord | undefined> {\n const safeId = validateRecordId(id);\n const userState = this.storage.getUserState(user.id, this.app.id);\n const tableMap = userState.tables.get(this.name);\n const record = tableMap?.get(safeId);\n\n if (!record || record.deleted) {\n return undefined;\n }\n\n return record;\n }\n\n async getAllRecords(user: UserStorage): Promise<SnapshotRecord[]> {\n const userState = this.storage.getUserState(user.id, this.app.id);\n const tableMap = userState.tables.get(this.name);\n return tableMap ? filterActiveRecords(this.name, tableMap.values()) : [];\n }\n\n async delete(): Promise<boolean> {\n return this.app.deleteTable(this.name);\n }\n}\n","import { shouldOverwrite } from '../../../shared/clock.js';\nimport {\n type ChangeRecord,\n OperationType,\n type StoredRecord,\n} from '../../../shared/types.js';\nimport { TetherServerError, TetherServerErrorCode } from '../../errors.js';\nimport {\n calculateByteSize,\n validateRecordId,\n validateTableName,\n validateTimestamp,\n} from '../../validate.js';\nimport { AppBaseStorage, applyChangeToRecord } from '../base/index.js';\nimport type { TableStorage } from '../table.js';\nimport type { UserStorage } from '../user.js';\nimport type { MemoryStorage } from './storage.js';\nimport { TableMemoryStorage } from './table.js';\n\n/**\n * In-memory implementation of `AppStorage`.\n */\nexport class AppMemoryStorage extends AppBaseStorage {\n private tables: Map<string, TableMemoryStorage> = new Map();\n private storage: MemoryStorage;\n\n constructor(id: string, storage: MemoryStorage) {\n super(id);\n this.storage = storage;\n }\n\n async createTable(name: string): Promise<TableStorage> {\n const safeName = validateTableName(name);\n if (this.tables.has(safeName)) {\n throw new TetherServerError(\n TetherServerErrorCode.AlreadyExists,\n 'Table already exists in this application',\n );\n }\n const table = new TableMemoryStorage(safeName, this, this.storage);\n this.tables.set(safeName, table);\n return table;\n }\n\n async getTable(name: string): Promise<TableStorage | undefined> {\n const safeName = validateTableName(name);\n return this.tables.get(safeName);\n }\n\n async getTables(): Promise<TableStorage[]> {\n return Array.from(this.tables.values());\n }\n\n async applyChanges(\n user: UserStorage,\n changes: ChangeRecord[],\n ): Promise<{ applied: ChangeRecord[]; newSeq: number }> {\n const userState = this.storage.getUserState(user.id, this.id);\n\n const maxRecords = this.storage.options.maxRecordsPerTable ?? 10000;\n const maxRecordSize = this.storage.options.maxRecordSizeBytes ?? 512 * 1024;\n const maxChangelog = this.storage.options.maxChangelogEntries ?? 1000;\n\n // Phase 1: Pre-validate all changes in the batch\n for (const change of changes) {\n const tableName = validateTableName(change.table);\n validateRecordId(change.id);\n validateTimestamp(change.timestamp);\n\n if (!this.tables.has(tableName)) {\n throw new TetherServerError(\n TetherServerErrorCode.NotFound,\n 'Table not found',\n );\n }\n\n const payloadBytes = calculateByteSize(change.data);\n if (payloadBytes > maxRecordSize) {\n throw new TetherServerError(\n TetherServerErrorCode.LimitExceeded,\n 'Record payload exceeds maximum allowed size',\n );\n }\n }\n\n // Phase 2: Stage mutations against cloned table maps\n const stagedTables = new Map<string, Map<string, StoredRecord>>();\n const stagedApplied: (ChangeRecord & { seq: number })[] = [];\n let stagedCurrentSeq = userState.currentSeq;\n let stagedMinSeq = userState.minSeq;\n\n for (const change of changes) {\n const tableName = validateTableName(change.table);\n const recordId = validateRecordId(change.id);\n\n let tableMap = stagedTables.get(tableName);\n if (!tableMap) {\n const existingTableMap = userState.tables.get(tableName);\n tableMap = new Map(existingTableMap);\n stagedTables.set(tableName, tableMap);\n }\n\n if (\n change.op === OperationType.Put &&\n !tableMap.has(recordId) &&\n tableMap.size >= maxRecords\n ) {\n throw new TetherServerError(\n TetherServerErrorCode.LimitExceeded,\n 'Table record limit reached',\n );\n }\n\n const existing = tableMap.get(recordId);\n const shouldApply = !existing || shouldOverwrite(change, existing);\n\n if (shouldApply) {\n stagedCurrentSeq++;\n const assignedSeq = stagedCurrentSeq;\n\n if (stagedMinSeq === 0) {\n stagedMinSeq = 1;\n }\n\n const { updatedRecord, appliedChange } = applyChangeToRecord(\n change,\n existing,\n assignedSeq,\n );\n\n tableMap.set(recordId, updatedRecord);\n stagedApplied.push(appliedChange);\n }\n }\n\n // Phase 3: Commit staged modifications atomically to userState\n for (const [tableName, stagedMap] of stagedTables.entries()) {\n userState.tables.set(tableName, stagedMap);\n }\n userState.currentSeq = stagedCurrentSeq;\n userState.minSeq = stagedMinSeq;\n userState.changelog.push(...stagedApplied);\n\n if (userState.changelog.length > maxChangelog) {\n const pruneCount = userState.changelog.length - maxChangelog;\n userState.changelog.splice(0, pruneCount);\n if (userState.changelog.length > 0) {\n userState.minSeq = userState.changelog[0].seq;\n }\n }\n\n return { applied: stagedApplied, newSeq: userState.currentSeq };\n }\n\n async getChangesSince(\n user: UserStorage,\n fromSeq: number,\n ): Promise<{\n changes: ChangeRecord[];\n currentSeq: number;\n requiresSnapshot?: boolean;\n }> {\n const userState = this.storage.getUserState(user.id, this.id);\n const currentSeq = userState.currentSeq;\n const minSeq = userState.minSeq;\n\n if ((fromSeq < minSeq && minSeq > 0) || fromSeq > currentSeq) {\n return { changes: [], currentSeq, requiresSnapshot: true };\n }\n\n const changes = userState.changelog.filter((c) => c.seq > fromSeq);\n return { changes, currentSeq, requiresSnapshot: false };\n }\n\n async getCurrentSeq(user: UserStorage): Promise<number> {\n const userState = this.storage.getUserState(user.id, this.id);\n return userState.currentSeq;\n }\n\n async delete(): Promise<boolean> {\n return this.storage.deleteApp(this.id);\n }\n\n deleteTable(name: string): boolean {\n const safeName = validateTableName(name);\n const deleted = this.tables.delete(safeName);\n this.storage.deleteTableInUserStates(this.id, safeName);\n return deleted;\n }\n}\n","import * as crypto from 'node:crypto';\nimport { hashPassword } from '../../crypto.js';\nimport { TetherServerError, TetherServerErrorCode } from '../../errors.js';\nimport {\n normalizeUsername,\n validateAppId,\n validatePassword,\n validateUserId,\n validateUsername,\n} from '../../validate.js';\nimport type { AppStorage } from '../app.js';\nimport { BaseStorage, filterTargetApps } from '../base/index.js';\nimport type { MaintenanceResult, StorageOptions } from '../storage.js';\nimport type { UserStorage } from '../user.js';\nimport { AppMemoryStorage } from './app.js';\nimport { type MemoryUserData, UserMemoryStorage } from './user.js';\n\nexport interface UserState {\n currentSeq: number;\n minSeq: number;\n tables: Map<\n string,\n Map<string, import('../../../shared/types.js').StoredRecord>\n >;\n changelog: Array<\n import('../../../shared/types.js').ChangeRecord & { seq: number }\n >;\n}\n\nexport interface MemoryStorageOptions extends StorageOptions {}\n\n/**\n * In-memory implementation of `Storage`.\n */\nexport class MemoryStorage extends BaseStorage {\n readonly backend = 'memory';\n private apps: Map<string, AppMemoryStorage> = new Map();\n private userStates: Map<string, UserState> = new Map(); // key = `${appId}:${userId}`\n private users: Map<string, MemoryUserData> = new Map(); // key = userId\n private usersByUsername: Map<string, string> = new Map(); // username -> userId\n readonly secret: string;\n readonly options: MemoryStorageOptions;\n\n constructor(options: MemoryStorageOptions = {}) {\n super(options);\n this.options = options;\n this.secret = options.secret ?? crypto.randomBytes(32).toString('hex');\n }\n\n getUserState(userId: string, appId: string): UserState {\n const safeAppId = validateAppId(appId);\n const safeUserId = validateUserId(userId);\n const key = `${safeAppId}:${safeUserId}`;\n let state = this.userStates.get(key);\n if (!state) {\n state = {\n currentSeq: 0,\n minSeq: 0,\n tables: new Map(),\n changelog: [],\n };\n this.userStates.set(key, state);\n }\n return state;\n }\n\n deleteUserState(userId: string): boolean {\n const safeUserId = validateUserId(userId);\n let deleted = false;\n for (const key of Array.from(this.userStates.keys())) {\n if (key.endsWith(`:${safeUserId}`)) {\n this.userStates.delete(key);\n deleted = true;\n }\n }\n return deleted;\n }\n\n deleteAppUserStates(appId: string): void {\n const safeAppId = validateAppId(appId);\n for (const key of Array.from(this.userStates.keys())) {\n if (key.startsWith(`${safeAppId}:`)) {\n this.userStates.delete(key);\n }\n }\n }\n\n deleteTableInUserStates(appId: string, tableName: string): void {\n const safeAppId = validateAppId(appId);\n for (const [key, state] of this.userStates.entries()) {\n if (key.startsWith(`${safeAppId}:`)) {\n state.tables.delete(tableName);\n }\n }\n }\n\n async createApp(id: string): Promise<AppStorage> {\n const safeId = validateAppId(id);\n if (this.apps.has(safeId)) {\n throw new TetherServerError(\n TetherServerErrorCode.AlreadyExists,\n 'Application already exists',\n );\n }\n const app = new AppMemoryStorage(safeId, this);\n this.apps.set(safeId, app);\n return app;\n }\n\n async getApp(id: string): Promise<AppStorage | undefined> {\n const safeId = validateAppId(id);\n return this.apps.get(safeId);\n }\n\n async getApps(): Promise<AppStorage[]> {\n return Array.from(this.apps.values());\n }\n\n async createUser(username: string, password: string): Promise<UserStorage> {\n const safeUsername = validateUsername(username);\n const validPassword = validatePassword(password);\n if (this.usersByUsername.has(safeUsername)) {\n throw new TetherServerError(\n TetherServerErrorCode.AlreadyExists,\n 'Username is already registered',\n );\n }\n\n const userId = crypto.randomUUID();\n const passwordHash = await hashPassword(validPassword);\n const userData: MemoryUserData = {\n id: userId,\n username: safeUsername,\n passwordHash,\n createdAt: Date.now(),\n };\n\n this.users.set(userId, userData);\n this.usersByUsername.set(safeUsername, userId);\n return new UserMemoryStorage(userData, this);\n }\n\n getUserData(userId: string): MemoryUserData | undefined {\n return this.users.get(userId);\n }\n\n async getUser(id: string): Promise<UserStorage | undefined> {\n const safeUserId = validateUserId(id);\n const data = this.users.get(safeUserId);\n if (data) {\n return new UserMemoryStorage(data, this);\n }\n return undefined;\n }\n\n async getUserByUsername(username: string): Promise<UserStorage | undefined> {\n const safeUsername = normalizeUsername(username);\n if (!safeUsername) return undefined;\n const userId = this.usersByUsername.get(safeUsername);\n if (!userId) return undefined;\n const data = this.users.get(userId);\n if (data) {\n return new UserMemoryStorage(data, this);\n }\n return undefined;\n }\n\n async getUsers(): Promise<UserStorage[]> {\n return Array.from(this.users.values()).map(\n (data) => new UserMemoryStorage(data, this),\n );\n }\n\n deleteUser(id: string): boolean {\n const safeUserId = validateUserId(id);\n this.deleteUserState(safeUserId);\n const data = this.users.get(safeUserId);\n if (data) {\n this.usersByUsername.delete(data.username);\n this.users.delete(safeUserId);\n return true;\n }\n return false;\n }\n\n deleteApp(id: string): boolean {\n const safeId = validateAppId(id);\n this.deleteAppUserStates(safeId);\n return this.apps.delete(safeId);\n }\n\n async checkpoint(appId?: string): Promise<MaintenanceResult> {\n throw new TetherServerError(\n TetherServerErrorCode.NotSupported,\n `Checkpoint operation is not supported by memory storage${appId ? ` (app: ${appId})` : ''}`,\n );\n }\n\n async vacuum(appId?: string): Promise<MaintenanceResult> {\n throw new TetherServerError(\n TetherServerErrorCode.NotSupported,\n `Vacuum operation is not supported by memory storage${appId ? ` (app: ${appId})` : ''}`,\n );\n }\n\n async prune(appId?: string, keepCount?: number): Promise<MaintenanceResult> {\n const keep = keepCount ?? this.options.maxChangelogEntries ?? 1000;\n const allApps = await this.getApps();\n const targetApps = filterTargetApps(allApps, appId);\n\n let totalPruned = 0;\n for (const app of targetApps) {\n for (const [key, state] of this.userStates.entries()) {\n if (key.startsWith(`${app.id}:`)) {\n if (state.changelog.length > keep) {\n const pruneCount = state.changelog.length - keep;\n state.changelog.splice(0, pruneCount);\n if (state.changelog.length > 0) {\n state.minSeq = state.changelog[0].seq;\n }\n totalPruned += pruneCount;\n }\n }\n }\n }\n\n return {\n action: 'prune',\n backend: 'memory',\n appId,\n affectedCount: totalPruned,\n message: `Prune completed successfully. Removed ${totalPruned} changelog record(s)`,\n };\n }\n\n async close(): Promise<void> {\n this.apps.clear();\n this.userStates.clear();\n this.users.clear();\n this.usersByUsername.clear();\n }\n}\n","import { TetherServerError, TetherServerErrorCode } from '../../errors.js';\nimport {\n hashUserPassword,\n UserBaseStorage,\n verifyUserPassword,\n} from '../base/index.js';\nimport type { MemoryStorage } from './storage.js';\n\nexport interface MemoryUserData {\n id: string;\n username: string;\n passwordHash: string | null;\n createdAt: number;\n}\n\n/**\n * In-memory implementation of `UserStorage`.\n */\nexport class UserMemoryStorage extends UserBaseStorage {\n private storage: MemoryStorage;\n\n constructor(data: MemoryUserData, storage: MemoryStorage) {\n super(data.id, data.username, data.createdAt);\n this.storage = storage;\n }\n\n protected getSecret(): string {\n return this.storage.secret;\n }\n\n private getUserData(): MemoryUserData {\n const data = this.storage.getUserData(this.id);\n if (!data) {\n throw new TetherServerError(\n TetherServerErrorCode.NotFound,\n 'User not found',\n );\n }\n return data;\n }\n\n async verifyPassword(password: string): Promise<boolean> {\n const data = this.getUserData();\n return verifyUserPassword(password, data.passwordHash);\n }\n\n async changePassword(newPassword: string): Promise<void> {\n const data = this.getUserData();\n data.passwordHash = await hashUserPassword(newPassword);\n }\n\n async delete(): Promise<boolean> {\n return this.storage.deleteUser(this.id);\n }\n}\n","import type { WebSocket } from 'ws';\nimport {\n type AuthClientMessage,\n type ChangeBatchClientMessage,\n type ClientMessage,\n ClientMessageType,\n PROTOCOL_VERSION,\n type ServerMessage,\n ServerMessageType,\n type SnapshotRecord,\n} from '../shared/types.js';\nimport { TetherServerError, TetherServerErrorCode } from './errors.js';\nimport type { RateLimiter } from './rate-limiter.js';\nimport type { TetherLogger } from './server.js';\nimport type { AppStorage } from './storage/app.js';\nimport type { Storage } from './storage/storage.js';\nimport type { UserStorage } from './storage/user.js';\nimport {\n calculateByteSize,\n validateAppId,\n validateIdentifier,\n} from './validate.js';\n\n/**\n * Configuration options for the WebSocket synchronization coordinator.\n */\nexport interface SyncOptions {\n /** Maximum number of concurrent active connections allowed per user channel (defaults to 20). */\n maxConcurrentConnectionsPerUser?: number;\n /** Maximum duration in milliseconds to wait for authentication before terminating socket (defaults to 10,000ms). */\n authTimeoutMs?: number;\n /** Optional rate limiter for connection handshakes and invalid token tracking. */\n rateLimiter?: RateLimiter | null;\n /** Optional logger instance (or null to suppress internal error logs). */\n logger?: TetherLogger | null;\n}\n\n/**\n * Real-time WebSocket synchronization coordinator managing authentication handshakes,\n * snapshot/diff delivery, change ingestion, acknowledgments, and peer broadcasts per application and user.\n */\nexport class Sync {\n private readonly storage: Storage;\n private readonly maxConcurrentConnectionsPerUser: number;\n private readonly authTimeoutMs: number;\n private readonly rateLimiter: RateLimiter | null;\n private readonly logger: TetherLogger | null;\n private readonly userClients = new Map<string, Set<ActiveClient>>(); // key = `${appId}:${userId}`\n private readonly webSocketToClient = new Map<WebSocket, ActiveClient>();\n private readonly pendingAuthTimers = new Map<WebSocket, NodeJS.Timeout>();\n private readonly webSocketToIp = new Map<WebSocket, string>();\n\n /**\n * Initializes a new Sync coordinator instance.\n *\n * @param storage - Pluggable backend storage engine.\n * @param options - Configuration options for concurrency limits, auth timeout, and rate limiting.\n */\n constructor(storage: Storage, options: SyncOptions = {}) {\n this.storage = storage;\n this.maxConcurrentConnectionsPerUser =\n options.maxConcurrentConnectionsPerUser ?? 20;\n this.authTimeoutMs = options.authTimeoutMs ?? 10_000;\n this.rateLimiter = options.rateLimiter ?? null;\n this.logger = options.logger ?? null;\n }\n\n /**\n * Total number of currently active authenticated WebSocket client connections.\n */\n get connectedClientsCount(): number {\n return this.webSocketToClient.size;\n }\n\n /**\n * Handles an incoming WebSocket connection, binding message, error, and disconnection events.\n *\n * @param webSocket - Active WebSocket connection.\n * @param clientIp - Remote client IP address.\n */\n handleConnection(webSocket: WebSocket, clientIp = '127.0.0.1'): void {\n this.webSocketToIp.set(webSocket, clientIp);\n\n if (this.rateLimiter && !this.rateLimiter.consume(clientIp)) {\n this.send(webSocket, {\n type: ServerMessageType.AuthError,\n message: 'Too many connection attempts',\n });\n webSocket.close();\n return;\n }\n\n if (this.authTimeoutMs > 0) {\n const timer = setTimeout(() => {\n if (!this.webSocketToClient.has(webSocket)) {\n this.send(webSocket, {\n type: ServerMessageType.AuthError,\n message: 'Authentication timeout',\n });\n webSocket.close();\n }\n }, this.authTimeoutMs);\n this.pendingAuthTimers.set(webSocket, timer);\n }\n\n let messageQueue: Promise<void> = Promise.resolve();\n\n webSocket.on('message', (data) => {\n const client = this.webSocketToClient.get(webSocket);\n const userContext = client\n ? ` (app: \"${client.appId}\", user: \"${client.user.id}\", client: \"${client.clientId}\")`\n : '';\n\n messageQueue = messageQueue\n .then(async () => {\n try {\n const raw = typeof data === 'string' ? data : data.toString();\n const msg = JSON.parse(raw) as ClientMessage;\n await this.handleMessage(webSocket, msg);\n } catch (err) {\n const message =\n err instanceof Error ? err.message : 'Unknown server error';\n this.logger?.error(\n `[TetherServer.Sync] Error processing WebSocket message${userContext}:`,\n err,\n );\n this.send(webSocket, {\n type: ServerMessageType.Error,\n message,\n });\n }\n })\n .catch((err) => {\n this.logger?.error(\n `[TetherServer.Sync] Unhandled error in message queue${userContext}:`,\n err,\n );\n });\n });\n\n webSocket.on('error', (err) => {\n const client = this.webSocketToClient.get(webSocket);\n const userContext = client\n ? ` (app: \"${client.appId}\", user: \"${client.user.id}\", client: \"${client.clientId}\")`\n : '';\n this.logger?.error(\n `[TetherServer.Sync] WebSocket connection error${userContext}:`,\n err,\n );\n this.cleanupConnection(webSocket);\n });\n\n webSocket.on('close', () => {\n this.cleanupConnection(webSocket);\n });\n }\n\n /**\n * Routes and executes incoming client protocol messages.\n *\n * @param webSocket - The connection that sent the message.\n * @param msg - Parsed client protocol message.\n */\n async handleMessage(webSocket: WebSocket, msg: ClientMessage): Promise<void> {\n if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n 'Invalid message format',\n );\n }\n\n switch (msg.type) {\n case ClientMessageType.Auth:\n await this.handleAuthMessage(webSocket, msg);\n break;\n\n case ClientMessageType.ChangeBatch:\n await this.handleChangeBatchMessage(webSocket, msg);\n break;\n\n case ClientMessageType.Ping:\n this.handlePingMessage(webSocket);\n break;\n\n default:\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n 'Unsupported message type',\n );\n }\n }\n\n // -- Private Message Handlers ---------------------------------------------\n\n private async handleAuthMessage(\n webSocket: WebSocket,\n msg: AuthClientMessage,\n ): Promise<void> {\n const ip = this.webSocketToIp.get(webSocket) ?? '127.0.0.1';\n\n const authTimer = this.pendingAuthTimers.get(webSocket);\n if (authTimer) {\n clearTimeout(authTimer);\n this.pendingAuthTimers.delete(webSocket);\n }\n\n if (msg.protocolVersion !== PROTOCOL_VERSION) {\n this.rateLimiter?.recordFailure(ip);\n this.send(webSocket, {\n type: ServerMessageType.AuthError,\n message: `Unsupported protocol version: expected ${PROTOCOL_VERSION}, got ${msg.protocolVersion}`,\n });\n webSocket.close();\n return;\n }\n\n if (typeof msg.token !== 'string' || !msg.token) {\n this.rateLimiter?.recordFailure(ip);\n this.send(webSocket, {\n type: ServerMessageType.AuthError,\n message: 'Missing or invalid authentication token',\n });\n webSocket.close();\n return;\n }\n\n const user = await this.storage.getUserByToken(msg.token);\n if (!user) {\n this.rateLimiter?.recordFailure(ip);\n this.send(webSocket, {\n type: ServerMessageType.AuthError,\n message: 'Invalid or expired authentication token',\n });\n webSocket.close();\n return;\n }\n\n if (typeof msg.appId !== 'string' || !msg.appId) {\n this.send(webSocket, {\n type: ServerMessageType.AuthError,\n message: 'Missing required field: appId',\n });\n webSocket.close();\n return;\n }\n\n const appId = validateAppId(msg.appId);\n const app = await this.storage.getApp(appId);\n if (!app) {\n this.send(webSocket, {\n type: ServerMessageType.AuthError,\n message: 'Application not found',\n });\n webSocket.close();\n return;\n }\n\n const channelKey = `${appId}:${user.id}`;\n let set = this.userClients.get(channelKey);\n if (!set) {\n set = new Set();\n this.userClients.set(channelKey, set);\n }\n\n if (set.size >= this.maxConcurrentConnectionsPerUser) {\n this.send(webSocket, {\n type: ServerMessageType.AuthError,\n message: 'Maximum concurrent connections exceeded for this user',\n });\n webSocket.close();\n return;\n }\n\n this.rateLimiter?.reset(ip);\n\n const clientId = validateIdentifier(\n msg.clientId ?? 'client_anon',\n 'clientId',\n );\n const existingClient = this.webSocketToClient.get(webSocket);\n if (existingClient) {\n const oldChannelKey = `${existingClient.appId}:${existingClient.user.id}`;\n const oldSet = this.userClients.get(oldChannelKey);\n if (oldSet) {\n oldSet.delete(existingClient);\n if (oldSet.size === 0) {\n this.userClients.delete(oldChannelKey);\n }\n }\n this.webSocketToClient.delete(webSocket);\n }\n\n const client: ActiveClient = {\n webSocket,\n clientId,\n user,\n appId,\n };\n\n this.webSocketToClient.set(webSocket, client);\n set.add(client);\n\n const currentSeq = await app.getCurrentSeq(user);\n const refreshedToken = await user.createToken();\n this.send(webSocket, {\n type: ServerMessageType.AuthSuccess,\n protocolVersion: PROTOCOL_VERSION,\n userId: user.id,\n currentSeq,\n token: refreshedToken,\n });\n\n // Initial sync: snapshot or diff\n await this.performSync(client, msg.lastSyncSeq);\n }\n\n private async handleChangeBatchMessage(\n webSocket: WebSocket,\n msg: ChangeBatchClientMessage,\n ): Promise<void> {\n const client = this.webSocketToClient.get(webSocket);\n if (!client) {\n this.send(webSocket, {\n type: ServerMessageType.AuthError,\n message: 'Not authenticated',\n });\n return;\n }\n\n if (!Array.isArray(msg.changes)) {\n throw new TetherServerError(\n TetherServerErrorCode.InvalidInput,\n 'Invalid change batch: changes must be an array',\n );\n }\n\n const maxBatchSize =\n this.storage.options?.maxBatchSizeBytes ?? 5 * 1024 * 1024;\n const batchBytes = calculateByteSize(msg.changes);\n if (batchBytes > maxBatchSize) {\n throw new TetherServerError(\n TetherServerErrorCode.LimitExceeded,\n 'Change batch exceeds maximum allowed size',\n );\n }\n\n const batchId = validateIdentifier(msg.batchId, 'batchId');\n\n const app = await this.storage.getApp(client.appId);\n if (!app) {\n throw new TetherServerError(\n TetherServerErrorCode.NotFound,\n 'Application not found',\n );\n }\n\n const { applied, newSeq } = await app.applyChanges(\n client.user,\n msg.changes,\n );\n\n // Acknowledge to sender\n this.send(webSocket, {\n type: ServerMessageType.ChangeAck,\n batchId,\n appliedSeq: newSeq,\n });\n\n // Broadcast applied changes to other active clients of the same app and user\n if (applied.length > 0) {\n this.broadcastToAppUser(client.appId, client.user.id, client.clientId, {\n type: ServerMessageType.BroadcastChanges,\n fromClientId: client.clientId,\n changes: applied,\n seq: newSeq,\n });\n }\n }\n\n private handlePingMessage(webSocket: WebSocket): void {\n this.send(webSocket, {\n type: ServerMessageType.Pong,\n });\n }\n\n // -- Private Helpers ------------------------------------------------------\n\n private cleanupConnection(webSocket: WebSocket): void {\n const authTimer = this.pendingAuthTimers.get(webSocket);\n if (authTimer) {\n clearTimeout(authTimer);\n this.pendingAuthTimers.delete(webSocket);\n }\n this.webSocketToIp.delete(webSocket);\n\n const client = this.webSocketToClient.get(webSocket);\n if (!client) return;\n\n this.webSocketToClient.delete(webSocket);\n const channelKey = `${client.appId}:${client.user.id}`;\n const set = this.userClients.get(channelKey);\n if (set) {\n set.delete(client);\n if (set.size === 0) {\n this.userClients.delete(channelKey);\n }\n }\n }\n\n private send(webSocket: WebSocket, msg: ServerMessage): void {\n if (webSocket.readyState === 1 /* OPEN */) {\n webSocket.send(JSON.stringify(msg));\n }\n }\n\n private async performSync(\n client: ActiveClient,\n lastSyncSeq?: number,\n ): Promise<void> {\n const app = await this.storage.getApp(client.appId);\n if (!app) {\n throw new TetherServerError(\n TetherServerErrorCode.NotFound,\n 'Application not found',\n );\n }\n\n const seq = lastSyncSeq ?? 0;\n if (seq === 0) {\n // Client has no sync point: deliver full snapshot for this app\n const snapshot = await this.getAppSnapshot(app, client.user);\n const currentSeq = await app.getCurrentSeq(client.user);\n this.send(client.webSocket, {\n type: ServerMessageType.SyncSnapshot,\n seq: currentSeq,\n snapshot,\n });\n } else {\n // Client has lastSyncSeq: deliver diff or snapshot if compacted\n const { changes, currentSeq, requiresSnapshot } =\n await app.getChangesSince(client.user, seq);\n\n // If changelog was pruned or compacted, deliver full snapshot\n if (requiresSnapshot) {\n const snapshot = await this.getAppSnapshot(app, client.user);\n this.send(client.webSocket, {\n type: ServerMessageType.SyncSnapshot,\n seq: currentSeq,\n snapshot,\n });\n } else {\n this.send(client.webSocket, {\n type: ServerMessageType.SyncDiff,\n fromSeq: seq,\n toSeq: currentSeq,\n changes,\n });\n }\n }\n }\n\n private async getAppSnapshot(\n app: AppStorage,\n user: UserStorage,\n ): Promise<SnapshotRecord[]> {\n const tables = await app.getTables();\n const snapshot: SnapshotRecord[] = [];\n for (const table of tables) {\n const records = await table.getAllRecords(user);\n snapshot.push(...records);\n }\n return snapshot;\n }\n\n private broadcastToAppUser(\n appId: string,\n userId: string,\n excludeClientId: string,\n msg: ServerMessage,\n ): void {\n const channelKey = `${appId}:${userId}`;\n const clients = this.userClients.get(channelKey);\n if (!clients) return;\n\n for (const client of clients) {\n if (client.clientId !== excludeClientId) {\n this.send(client.webSocket, msg);\n }\n }\n }\n}\n\n// -- Private Helpers --------------------------------------------------------\n\ninterface ActiveClient {\n webSocket: WebSocket;\n clientId: string;\n user: UserStorage;\n appId: string;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,WAAsB;AACtB,gBAAgC;;;ACUzB,SAAS,kBAAkBA,OAAsB;AACtD,MAAIA,UAAS,MAAMA,UAAS,IAAK,QAAO;AACxC,MAAIA,MAAK,SAAS,GAAG,EAAG,CAAAA,QAAOA,MAAK,MAAM,GAAGA,MAAK,SAAS,CAAC;AAC5D,MAAI,CAACA,MAAK,WAAW,GAAG,EAAG,CAAAA,QAAO,IAAIA,KAAI;AAC1C,SAAOA,UAAS,MAAM,KAAKA;AAC7B;;;AChBA,aAAwB;AACxB,SAAoB;AACpB,WAAsB;AAGf,IAAM,2BAA2B,IAAI,KAAK,KAAK;AAyCtD,eAAsB,aAAa,UAAmC;AACpE,QAAM,OAAc,mBAAY,EAAE,EAAE,SAAS,KAAK;AAClD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,IAAO;AAAA,MACL,SAAS,UAAU,MAAM;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA,CAAC,KAAK,eAAe;AACnB,YAAI,IAAK,QAAO,OAAO,GAAG;AAC1B,gBAAQ,UAAU,IAAI,IAAI,WAAW,SAAS,KAAK,CAAC,EAAE;AAAA,MACxD;AAAA,IACF;AAAA,EACF,CAAC;AACH;AASA,eAAsB,mBACpB,UACA,YACkB;AAClB,MAAI,CAAC,cAAc,OAAO,eAAe,SAAU,QAAO;AAC1D,QAAM,QAAQ,WAAW,MAAM,GAAG;AAClC,MAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,SAAU,QAAO;AAExD,QAAM,CAAC,EAAE,MAAM,WAAW,IAAI;AAC9B,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,IAAO;AAAA,MACL,SAAS,UAAU,MAAM;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA,CAAC,KAAK,eAAe;AACnB,YAAI,IAAK,QAAO,QAAQ,KAAK;AAC7B,cAAM,cAAc,OAAO,KAAK,aAAa,KAAK;AAClD,YAAI,WAAW,WAAW,YAAY,OAAQ,QAAO,QAAQ,KAAK;AAClE,gBAAe,uBAAgB,YAAY,WAAW,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAQA,eAAsB,wBACpB,UACkB;AAClB,MAAI,CAAC,0BAA0B;AAC7B,+BAA2B,aAAa,8BAA8B;AAAA,EACxE;AACA,QAAM,YAAY,MAAM;AACxB,SAAO,mBAAmB,UAAU,SAAS;AAC/C;AAWO,SAAS,mBACd,QACA,UACA,QACA,mBAAmB,0BACX;AACR,QAAM,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI;AAClD,QAAM,UAAU,KAAK,UAAU;AAAA,IAC7B;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,aAAa,OAAO,KAAK,SAAS,OAAO,EAAE,SAAS,WAAW;AACrE,QAAM,YACH,kBAAW,UAAU,MAAM,EAC3B,OAAO,UAAU,EACjB,OAAO,WAAW;AACrB,SAAO,GAAG,UAAU,IAAI,SAAS;AACnC;AASO,SAAS,mBACd,OACA,QACgE;AAChE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,CAAC,YAAY,SAAS,IAAI;AAChC,QAAM,cACH,kBAAW,UAAU,MAAM,EAC3B,OAAO,UAAU,EACjB,OAAO,WAAW;AAErB,MACE,UAAU,WAAW,YAAY,UACjC,CAAQ;AAAA,IACN,OAAO,KAAK,WAAW,OAAO;AAAA,IAC9B,OAAO,KAAK,aAAa,OAAO;AAAA,EAClC,GACA;AACA,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,MAAM,OAAO,KAAK,YAAY,WAAW,EAAE,SAAS,OAAO;AACjE,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QACE,OAAO,WAAW,YAClB,WAAW,QACX,OAAO,OAAO,WAAW,YACzB,CAAC,OAAO,UACR,OAAO,OAAO,aAAa,YAC3B,CAAC,OAAO,YACR,OAAO,OAAO,cAAc,YAC5B,CAAC,OAAO,SAAS,OAAO,SAAS,KACjC,OAAO,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GAC/C;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,MACL,QAAQ,OAAO;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,WAAW,OAAO;AAAA,IACpB;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,IAAM,iBAAuC;AAAA,EAC3C,GAAG,QAAQ,IAAI,aAAa,SAAS,MAAM;AAAA,EAC3C,GAAG;AAAA,EACH,GAAG;AAAA,EACH,QAAQ,KAAK,OAAO;AACtB;AAEA,IAAI,2BAAmD;;;AClLhD,IAAM,oBAAN,cAAgC,MAAM;AAAA;AAAA,EAElC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQT,YAAY,MAA6B,SAAkB;AACzD,UAAM,WAAW,6BAA6B,IAAI,CAAC;AACnD,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,WAAW,SAAS;AAAA,EAClD;AACF;AAIA,SAAS,6BAA6B,MAAqC;AACzE,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EACX;AACF;;;ACrEA,IAAAC,MAAoB;AACpB,IAAAC,QAAsB;AAqCf,SAAS,eAAe,KAAsB;AACnD,MAAI,CAAC,OAAO,OAAO,EAAG,QAAO;AAC7B,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,UAAM,OAAQ,IAA8B;AAC5C,WAAO,SAAS;AAAA,EAClB;AACF;AASO,SAAS,eAAe,SAAwC;AACrE,QAAM,WAAgB,WAAK,SAAS,aAAa;AACjD,MAAI;AACF,QAAI,CAAI,eAAW,QAAQ,EAAG,QAAO;AACrC,UAAM,UAAa,iBAAa,UAAU,OAAO;AACjD,UAAM,OAAO,KAAK,MAAM,OAAO;AAC/B,QAAI,OAAO,KAAK,QAAQ,YAAY,eAAe,KAAK,GAAG,GAAG;AAC5D,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,SAAS,kBACd,SACA,SACkB;AAClB,EAAG,cAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACzC,QAAM,WAAgB,WAAK,SAAS,aAAa;AAEjD,QAAM,WAAW,eAAe,OAAO;AACvC,MAAI,YAAY,SAAS,QAAQ,QAAQ,KAAK;AAC5C,UAAM,IAAI;AAAA;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAGA,MAAO,eAAW,QAAQ,GAAG;AAC3B,QAAI;AACF,MAAG,eAAW,QAAQ;AAAA,IACxB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,OAAuB;AAAA,IAC3B,KAAK,QAAQ;AAAA,IACb,MAAM,QAAQ;AAAA,IACd,MAAM,QAAQ;AAAA,IACd,SAAS,QAAQ;AAAA,IACjB,WAAW,KAAK,IAAI;AAAA,EACtB;AAEA,EAAG,kBAAc,UAAU,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG;AAAA,IACxD,UAAU;AAAA,IACV,MAAM;AAAA,EACR,CAAC;AAED,MAAI,aAAa;AACjB,QAAM,UAAU,MAAM;AACpB,QAAI,WAAY;AAChB,iBAAa;AACb,QAAI;AACF,UAAO,eAAW,QAAQ,GAAG;AAC3B,cAAM,UAAU,KAAK;AAAA,UAChB,iBAAa,UAAU,OAAO;AAAA,QACnC;AACA,YAAI,QAAQ,QAAQ,QAAQ,KAAK;AAC/B,UAAG,eAAW,QAAQ;AAAA,QACxB;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;ACvHO,IAAM,cAAN,MAAkB;AAAA,EACN,QAAQ,oBAAI,IAA4B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,YAAY,UAA8B,CAAC,GAAG;AAC5C,SAAK,WAAW,QAAQ,YAAY;AACpC,SAAK,cAAc,QAAQ,eAAe;AAC1C,SAAK,cAAc,QAAQ,eAAe;AAC1C,SAAK,mBAAmB,QAAQ,oBAAoB;AACpD,SAAK,eAAe,QAAQ,gBAAgB;AAC5C,SAAK,aAAa,QAAQ,cAAc;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,UAAU,KAAa,MAAM,KAAK,IAAI,GAAY;AAChD,UAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;AAChC,QAAI,CAAC,MAAO,QAAO;AAEnB,QAAI,MAAM,eAAe,KAAK;AAC5B,aAAO;AAAA,IACT;AAEA,QAAI,MAAM,WAAW,KAAK;AACxB,WAAK,MAAM,OAAO,GAAG;AACrB,aAAO;AAAA,IACT;AAEA,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ,KAAa,MAAM,KAAK,IAAI,GAAY;AAC9C,QAAI,KAAK,UAAU,KAAK,GAAG,GAAG;AAC5B,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;AAChC,QAAI,CAAC,SAAU,MAAM,WAAW,OAAO,MAAM,gBAAgB,KAAM;AACjE,WAAK;AAAA,QACH;AAAA,QACA;AAAA,UACE,OAAO;AAAA,UACP,SAAS,MAAM,KAAK;AAAA,UACpB,UAAU;AAAA,UACV,cAAc;AAAA,QAChB;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAEA,UAAM;AACN,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,cAAc,KAAa,MAAM,KAAK,IAAI,GAAW;AACnD,QAAI,QAAQ,KAAK,MAAM,IAAI,GAAG;AAC9B,QAAI,CAAC,SAAU,MAAM,WAAW,OAAO,MAAM,gBAAgB,KAAM;AACjE,cAAQ;AAAA,QACN,OAAO;AAAA,QACP,SAAS,MAAM,KAAK;AAAA,QACpB,UAAU;AAAA,QACV,cAAc;AAAA,MAChB;AACA,WAAK,SAAS,KAAK,OAAO,GAAG;AAAA,IAC/B;AAEA,UAAM;AAEN,QAAI,MAAM,YAAY,KAAK,aAAa;AACtC,YAAM,WAAW,MAAM,WAAW,KAAK;AACvC,YAAM,UAAU,KAAK;AAAA,QACnB,KAAK,mBAAmB,KAAK;AAAA,QAC7B,KAAK;AAAA,MACP;AACA,YAAM,eAAe,MAAM;AAC3B,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,KAAmB;AACvB,SAAK,MAAM,OAAO,GAAG;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ,MAAM,KAAK,IAAI,GAAS;AAC9B,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,MAAM,QAAQ,GAAG;AAC/C,UAAI,MAAM,WAAW,OAAO,MAAM,gBAAgB,KAAK;AACrD,aAAK,MAAM,OAAO,GAAG;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAIQ,SAAS,KAAa,OAAuB,KAAmB;AACtE,QAAI,KAAK,MAAM,QAAQ,KAAK,cAAc,CAAC,KAAK,MAAM,IAAI,GAAG,GAAG;AAC9D,WAAK,QAAQ,GAAG;AAChB,UAAI,KAAK,MAAM,QAAQ,KAAK,YAAY;AACtC,cAAM,YAAY,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE;AAC3C,YAAI,cAAc,QAAW;AAC3B,eAAK,MAAM,OAAO,SAAS;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AACA,SAAK,MAAM,IAAI,KAAK,KAAK;AAAA,EAC3B;AACF;;;ACtKO,SAAS,gBACd,UAIA,UAGS;AACT,MAAI,CAAC,SAAU,QAAO;AAEtB,MAAI,SAAS,YAAY,SAAS,WAAW;AAC3C,WAAO;AAAA,EACT;AACA,MAAI,SAAS,YAAY,SAAS,WAAW;AAC3C,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB,SAAS,YAAY;AAC5C,QAAM,iBAAiB,SAAS,YAAY;AAC5C,SAAO,kBAAkB;AAC3B;;;AC8BO,IAAM,mBAAmB;;;ACzDzB,IAAM,sBAAsB;AAG5B,IAAM,sBAAsB;AAG5B,IAAM,sBAAsB;AAG5B,IAAM,sBAAsB;AAG5B,IAAM,gCAAgC,IAAI,KAAK;AAW/C,SAAS,kBACd,WACA,mBAAmB,+BACX;AACR,MACE,OAAO,cAAc,YACrB,CAAC,OAAO,SAAS,SAAS,KAC1B,aAAa,GACb;AACA,UAAM,IAAI;AAAA;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,MAAI,YAAY,KAAK,IAAI,IAAI,kBAAkB;AAC7C,UAAM,IAAI;AAAA;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AASO,SAAS,eAAe,QAAwB;AACrD,SAAO,uBAAuB,QAAQ,SAAS;AACjD;AASO,SAAS,cAAc,OAAuB;AACnD,SAAO,uBAAuB,OAAO,gBAAgB;AACvD;AASO,SAAS,kBAAkB,WAA2B;AAC3D,SAAO,uBAAuB,WAAW,YAAY;AACvD;AASO,SAAS,iBAAiB,IAAoB;AACnD,MAAI,OAAO,OAAO,YAAY,GAAG,WAAW,KAAK,GAAG,SAAS,KAAK;AAChE,UAAM,IAAI;AAAA;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,kBAAkB,UAA0B;AAC1D,SAAO,OAAO,aAAa,WAAW,SAAS,KAAK,EAAE,YAAY,IAAI;AACxE;AAUO,SAAS,iBAAiB,UAA0B;AACzD,MAAI,OAAO,aAAa,UAAU;AAChC,UAAM,IAAI;AAAA;AAAA,MAER,4BAA4B,mBAAmB,QAAQ,mBAAmB;AAAA,IAC5E;AAAA,EACF;AACA,QAAM,aAAa,kBAAkB,QAAQ;AAC7C,MACE,WAAW,SAAS,uBACpB,WAAW,SAAS,qBACpB;AACA,UAAM,IAAI;AAAA;AAAA,MAER,4BAA4B,mBAAmB,QAAQ,mBAAmB;AAAA,IAC5E;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,kBAAkB,UAA0B;AAC1D,SAAO,OAAO,aAAa,WAAW,SAAS,KAAK,IAAI;AAC1D;AASO,SAAS,iBAAiB,UAA0B;AACzD,MAAI,OAAO,aAAa,UAAU;AAChC,UAAM,IAAI;AAAA;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,QAAM,aAAa,kBAAkB,QAAQ;AAC7C,MACE,WAAW,SAAS,uBACpB,WAAW,SAAS,qBACpB;AACA,UAAM,IAAI;AAAA;AAAA,MAER,4BAA4B,mBAAmB,QAAQ,mBAAmB;AAAA,IAC5E;AAAA,EACF;AACA,SAAO;AACT;AAUO,SAAS,mBAAmB,IAAY,OAAO,cAAsB;AAC1E,MAAI,OAAO,OAAO,YAAY,CAAC,yBAAyB,KAAK,EAAE,GAAG;AAChE,UAAM,IAAI;AAAA;AAAA,MAER,WAAW,IAAI;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,kBAAkB,OAAwB;AACxD,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,WAAW,OAAO,OAAO;AACtE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,UAAW,QAAO;AACvC,MAAI;AACF,WAAO,OAAO,WAAW,KAAK,UAAU,KAAK,GAAG,OAAO;AAAA,EACzD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAgBA,SAAS,uBAAuB,IAAY,MAAsB;AAChE,MAAI,OAAO,OAAO,YAAY,CAAC,wBAAwB,KAAK,EAAE,GAAG;AAC/D,UAAM,IAAI;AAAA;AAAA,MAER,WAAW,IAAI;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;;;ACnOO,IAAe,iBAAf,MAAoD;AAAA,EAChD;AAAA,EAET,YAAY,IAAY;AACtB,SAAK,KAAK;AAAA,EACZ;AAgCF;AAKO,SAAS,oBACd,QACA,UACA,KAIA;AACA,QAAM,YAAY,OAAO;AACzB,QAAM,eAAe,UAAU,WAAW,KAAK;AAE/C,QAAM,gBAA8B;AAAA,IAClC,IAAI,OAAO;AAAA,IACX,SAAS;AAAA,IACT,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO;AAAA,IACjB,SAAS;AAAA,IACT,MAAM,YAAY,OAAQ,OAAO,QAAQ;AAAA,EAC3C;AAEA,QAAM,gBAAgD;AAAA,IACpD;AAAA,IACA,OAAO,OAAO;AAAA,IACd,IAAI,OAAO;AAAA,IACX,IAAI,OAAO;AAAA,IACX,SAAS;AAAA,IACT,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO;AAAA,IACjB,MAAM,YAAY,SAAY,OAAO;AAAA,EACvC;AAEA,SAAO,EAAE,eAAe,cAAc;AACxC;;;ACvEO,IAAe,cAAf,MAA8C;AAAA,EAC1C;AAAA,EAET,YAAY,SAA0B;AACpC,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EASU,aAAiC;AACzC,WAAO;AAAA,EACT;AAAA,EAkBA,MAAM,eAAe,OAAiD;AACpE,UAAM,UAAU,mBAAmB,OAAO,KAAK,MAAM;AACrD,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO,KAAK,QAAQ,QAAQ,MAAM;AAAA,EACpC;AAAA,EAEA,MAAM,UAAU,OAAwC;AACtD,UAAM,QAAQ,MAAM,KAAK,SAAS;AAClC,UAAM,UAAU,MAAM,KAAK,QAAQ;AACnC,UAAM,aAAa,iBAAiB,SAAS,KAAK;AAClD,UAAM,OAAO,MAAM,kBAAkB,UAAU;AAE/C,UAAM,SAAwB;AAAA,MAC5B,SAAS,KAAK;AAAA,MACd,YAAY,MAAM;AAAA,MAClB,WAAW,QAAQ;AAAA,MACnB;AAAA,IACF;AACA,UAAM,UAAU,KAAK,WAAW;AAChC,QAAI,YAAY,QAAW;AACzB,aAAO,UAAU;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AACF;AAKO,SAAS,iBACd,SACA,OACc;AACd,QAAM,aAAa,QACf,QAAQ,OAAO,CAAC,MAAM,EAAE,OAAO,cAAc,KAAK,CAAC,IACnD;AAEJ,MAAI,SAAS,WAAW,WAAW,GAAG;AACpC,UAAM,IAAI;AAAA;AAAA,MAER,gBAAgB,KAAK;AAAA,IACvB;AAAA,EACF;AAEA,SAAO;AACT;AAKA,eAAsB,kBACpB,MACkD;AAClD,QAAM,eAAwD,CAAC;AAC/D,aAAW,OAAO,MAAM;AACtB,UAAM,SAAS,MAAM,IAAI,UAAU;AACnC,iBAAa,KAAK;AAAA,MAChB,IAAI,IAAI;AAAA,MACR,QAAQ,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI;AAAA,IAClC,CAAC;AAAA,EACH;AACA,SAAO;AACT;;;ACnGO,IAAe,mBAAf,MAAwD;AAAA,EACpD;AAAA,EACA;AAAA,EAET,YAAY,MAAc,KAAiB;AACzC,SAAK,OAAO;AACZ,SAAK,MAAM;AAAA,EACb;AAAA;AAAA,EAeA,MAAM,aACJ,MACA,SACsD;AACtD,WAAO,KAAK,IAAI;AAAA,MACd;AAAA,MACA,sBAAsB,KAAK,MAAM,KAAK,IAAI,IAAI,OAAO;AAAA,IACvD;AAAA,EACF;AACF;AAKO,SAAS,oBACd,WACA,SACkB;AAClB,QAAM,QAA0B,CAAC;AACjC,aAAW,OAAO,SAAS;AACzB,QAAI,CAAC,IAAI,SAAS;AAChB,YAAM,KAAK;AAAA,QACT,GAAG;AAAA,QACH,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAKO,SAAS,sBACd,WACA,OACA,SACgB;AAChB,SAAO,QAAQ,IAAI,CAAC,OAAO;AAAA,IACzB,GAAG;AAAA,IACH,OAAO;AAAA,IACP;AAAA,EACF,EAAE;AACJ;;;ACjEO,IAAe,kBAAf,MAAsD;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,IAAY,UAAkB,WAAmB;AAC3D,SAAK,KAAK;AACV,SAAK,WAAW;AAChB,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA,EAeA,MAAM,YAAY,kBAA4C;AAC5D,WAAO;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK,UAAU;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,YAAY,OAAiC;AACjD,UAAM,UAAU,mBAAmB,OAAO,KAAK,UAAU,CAAC;AAC1D,WAAO,YAAY,QAAQ,QAAQ,WAAW,KAAK;AAAA,EACrD;AACF;AAKA,eAAsB,mBACpB,UACA,cACkB;AAClB,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,aAAa,kBAAkB,QAAQ;AAC7C,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,mBAAmB,YAAY,YAAY;AACpD;AAKA,eAAsB,iBAAiB,aAAsC;AAC3E,QAAM,QAAQ,iBAAiB,WAAW;AAC1C,SAAO,aAAa,KAAK;AAC3B;;;AC7DO,IAAM,qBAAN,cAAiC,iBAAiB;AAAA,EAE/C;AAAA,EAER,YAAY,MAAc,KAAuB,SAAwB;AACvE,UAAM,MAAM,GAAG;AACf,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAM,UACJ,MACA,IACmC;AACnC,UAAM,SAAS,iBAAiB,EAAE;AAClC,UAAM,YAAY,KAAK,QAAQ,aAAa,KAAK,IAAI,KAAK,IAAI,EAAE;AAChE,UAAM,WAAW,UAAU,OAAO,IAAI,KAAK,IAAI;AAC/C,UAAM,SAAS,UAAU,IAAI,MAAM;AAEnC,QAAI,CAAC,UAAU,OAAO,SAAS;AAC7B,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,cAAc,MAA8C;AAChE,UAAM,YAAY,KAAK,QAAQ,aAAa,KAAK,IAAI,KAAK,IAAI,EAAE;AAChE,UAAM,WAAW,UAAU,OAAO,IAAI,KAAK,IAAI;AAC/C,WAAO,WAAW,oBAAoB,KAAK,MAAM,SAAS,OAAO,CAAC,IAAI,CAAC;AAAA,EACzE;AAAA,EAEA,MAAM,SAA2B;AAC/B,WAAO,KAAK,IAAI,YAAY,KAAK,IAAI;AAAA,EACvC;AACF;;;ACtBO,IAAM,mBAAN,cAA+B,eAAe;AAAA,EAC3C,SAA0C,oBAAI,IAAI;AAAA,EAClD;AAAA,EAER,YAAY,IAAY,SAAwB;AAC9C,UAAM,EAAE;AACR,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAM,YAAY,MAAqC;AACrD,UAAM,WAAW,kBAAkB,IAAI;AACvC,QAAI,KAAK,OAAO,IAAI,QAAQ,GAAG;AAC7B,YAAM,IAAI;AAAA;AAAA,QAER;AAAA,MACF;AAAA,IACF;AACA,UAAM,QAAQ,IAAI,mBAAmB,UAAU,MAAM,KAAK,OAAO;AACjE,SAAK,OAAO,IAAI,UAAU,KAAK;AAC/B,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,MAAiD;AAC9D,UAAM,WAAW,kBAAkB,IAAI;AACvC,WAAO,KAAK,OAAO,IAAI,QAAQ;AAAA,EACjC;AAAA,EAEA,MAAM,YAAqC;AACzC,WAAO,MAAM,KAAK,KAAK,OAAO,OAAO,CAAC;AAAA,EACxC;AAAA,EAEA,MAAM,aACJ,MACA,SACsD;AACtD,UAAM,YAAY,KAAK,QAAQ,aAAa,KAAK,IAAI,KAAK,EAAE;AAE5D,UAAM,aAAa,KAAK,QAAQ,QAAQ,sBAAsB;AAC9D,UAAM,gBAAgB,KAAK,QAAQ,QAAQ,sBAAsB,MAAM;AACvE,UAAM,eAAe,KAAK,QAAQ,QAAQ,uBAAuB;AAGjE,eAAW,UAAU,SAAS;AAC5B,YAAM,YAAY,kBAAkB,OAAO,KAAK;AAChD,uBAAiB,OAAO,EAAE;AAC1B,wBAAkB,OAAO,SAAS;AAElC,UAAI,CAAC,KAAK,OAAO,IAAI,SAAS,GAAG;AAC/B,cAAM,IAAI;AAAA;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAEA,YAAM,eAAe,kBAAkB,OAAO,IAAI;AAClD,UAAI,eAAe,eAAe;AAChC,cAAM,IAAI;AAAA;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,eAAe,oBAAI,IAAuC;AAChE,UAAM,gBAAoD,CAAC;AAC3D,QAAI,mBAAmB,UAAU;AACjC,QAAI,eAAe,UAAU;AAE7B,eAAW,UAAU,SAAS;AAC5B,YAAM,YAAY,kBAAkB,OAAO,KAAK;AAChD,YAAM,WAAW,iBAAiB,OAAO,EAAE;AAE3C,UAAI,WAAW,aAAa,IAAI,SAAS;AACzC,UAAI,CAAC,UAAU;AACb,cAAM,mBAAmB,UAAU,OAAO,IAAI,SAAS;AACvD,mBAAW,IAAI,IAAI,gBAAgB;AACnC,qBAAa,IAAI,WAAW,QAAQ;AAAA,MACtC;AAEA,UACE,OAAO,0BACP,CAAC,SAAS,IAAI,QAAQ,KACtB,SAAS,QAAQ,YACjB;AACA,cAAM,IAAI;AAAA;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAEA,YAAM,WAAW,SAAS,IAAI,QAAQ;AACtC,YAAM,cAAc,CAAC,YAAY,gBAAgB,QAAQ,QAAQ;AAEjE,UAAI,aAAa;AACf;AACA,cAAM,cAAc;AAEpB,YAAI,iBAAiB,GAAG;AACtB,yBAAe;AAAA,QACjB;AAEA,cAAM,EAAE,eAAe,cAAc,IAAI;AAAA,UACvC;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAEA,iBAAS,IAAI,UAAU,aAAa;AACpC,sBAAc,KAAK,aAAa;AAAA,MAClC;AAAA,IACF;AAGA,eAAW,CAAC,WAAW,SAAS,KAAK,aAAa,QAAQ,GAAG;AAC3D,gBAAU,OAAO,IAAI,WAAW,SAAS;AAAA,IAC3C;AACA,cAAU,aAAa;AACvB,cAAU,SAAS;AACnB,cAAU,UAAU,KAAK,GAAG,aAAa;AAEzC,QAAI,UAAU,UAAU,SAAS,cAAc;AAC7C,YAAM,aAAa,UAAU,UAAU,SAAS;AAChD,gBAAU,UAAU,OAAO,GAAG,UAAU;AACxC,UAAI,UAAU,UAAU,SAAS,GAAG;AAClC,kBAAU,SAAS,UAAU,UAAU,CAAC,EAAE;AAAA,MAC5C;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,eAAe,QAAQ,UAAU,WAAW;AAAA,EAChE;AAAA,EAEA,MAAM,gBACJ,MACA,SAKC;AACD,UAAM,YAAY,KAAK,QAAQ,aAAa,KAAK,IAAI,KAAK,EAAE;AAC5D,UAAM,aAAa,UAAU;AAC7B,UAAM,SAAS,UAAU;AAEzB,QAAK,UAAU,UAAU,SAAS,KAAM,UAAU,YAAY;AAC5D,aAAO,EAAE,SAAS,CAAC,GAAG,YAAY,kBAAkB,KAAK;AAAA,IAC3D;AAEA,UAAM,UAAU,UAAU,UAAU,OAAO,CAAC,MAAM,EAAE,MAAM,OAAO;AACjE,WAAO,EAAE,SAAS,YAAY,kBAAkB,MAAM;AAAA,EACxD;AAAA,EAEA,MAAM,cAAc,MAAoC;AACtD,UAAM,YAAY,KAAK,QAAQ,aAAa,KAAK,IAAI,KAAK,EAAE;AAC5D,WAAO,UAAU;AAAA,EACnB;AAAA,EAEA,MAAM,SAA2B;AAC/B,WAAO,KAAK,QAAQ,UAAU,KAAK,EAAE;AAAA,EACvC;AAAA,EAEA,YAAY,MAAuB;AACjC,UAAM,WAAW,kBAAkB,IAAI;AACvC,UAAM,UAAU,KAAK,OAAO,OAAO,QAAQ;AAC3C,SAAK,QAAQ,wBAAwB,KAAK,IAAI,QAAQ;AACtD,WAAO;AAAA,EACT;AACF;;;AC7LA,IAAAC,UAAwB;;;ACkBjB,IAAM,oBAAN,cAAgC,gBAAgB;AAAA,EAC7C;AAAA,EAER,YAAY,MAAsB,SAAwB;AACxD,UAAM,KAAK,IAAI,KAAK,UAAU,KAAK,SAAS;AAC5C,SAAK,UAAU;AAAA,EACjB;AAAA,EAEU,YAAoB;AAC5B,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEQ,cAA8B;AACpC,UAAM,OAAO,KAAK,QAAQ,YAAY,KAAK,EAAE;AAC7C,QAAI,CAAC,MAAM;AACT,YAAM,IAAI;AAAA;AAAA,QAER;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,eAAe,UAAoC;AACvD,UAAM,OAAO,KAAK,YAAY;AAC9B,WAAO,mBAAmB,UAAU,KAAK,YAAY;AAAA,EACvD;AAAA,EAEA,MAAM,eAAe,aAAoC;AACvD,UAAM,OAAO,KAAK,YAAY;AAC9B,SAAK,eAAe,MAAM,iBAAiB,WAAW;AAAA,EACxD;AAAA,EAEA,MAAM,SAA2B;AAC/B,WAAO,KAAK,QAAQ,WAAW,KAAK,EAAE;AAAA,EACxC;AACF;;;ADpBO,IAAM,gBAAN,cAA4B,YAAY;AAAA,EACpC,UAAU;AAAA,EACX,OAAsC,oBAAI,IAAI;AAAA,EAC9C,aAAqC,oBAAI,IAAI;AAAA;AAAA,EAC7C,QAAqC,oBAAI,IAAI;AAAA;AAAA,EAC7C,kBAAuC,oBAAI,IAAI;AAAA;AAAA,EAC9C;AAAA,EACA;AAAA,EAET,YAAY,UAAgC,CAAC,GAAG;AAC9C,UAAM,OAAO;AACb,SAAK,UAAU;AACf,SAAK,SAAS,QAAQ,UAAiB,oBAAY,EAAE,EAAE,SAAS,KAAK;AAAA,EACvE;AAAA,EAEA,aAAa,QAAgB,OAA0B;AACrD,UAAM,YAAY,cAAc,KAAK;AACrC,UAAM,aAAa,eAAe,MAAM;AACxC,UAAM,MAAM,GAAG,SAAS,IAAI,UAAU;AACtC,QAAI,QAAQ,KAAK,WAAW,IAAI,GAAG;AACnC,QAAI,CAAC,OAAO;AACV,cAAQ;AAAA,QACN,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,QAAQ,oBAAI,IAAI;AAAA,QAChB,WAAW,CAAC;AAAA,MACd;AACA,WAAK,WAAW,IAAI,KAAK,KAAK;AAAA,IAChC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,gBAAgB,QAAyB;AACvC,UAAM,aAAa,eAAe,MAAM;AACxC,QAAI,UAAU;AACd,eAAW,OAAO,MAAM,KAAK,KAAK,WAAW,KAAK,CAAC,GAAG;AACpD,UAAI,IAAI,SAAS,IAAI,UAAU,EAAE,GAAG;AAClC,aAAK,WAAW,OAAO,GAAG;AAC1B,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,oBAAoB,OAAqB;AACvC,UAAM,YAAY,cAAc,KAAK;AACrC,eAAW,OAAO,MAAM,KAAK,KAAK,WAAW,KAAK,CAAC,GAAG;AACpD,UAAI,IAAI,WAAW,GAAG,SAAS,GAAG,GAAG;AACnC,aAAK,WAAW,OAAO,GAAG;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,wBAAwB,OAAe,WAAyB;AAC9D,UAAM,YAAY,cAAc,KAAK;AACrC,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,WAAW,QAAQ,GAAG;AACpD,UAAI,IAAI,WAAW,GAAG,SAAS,GAAG,GAAG;AACnC,cAAM,OAAO,OAAO,SAAS;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,IAAiC;AAC/C,UAAM,SAAS,cAAc,EAAE;AAC/B,QAAI,KAAK,KAAK,IAAI,MAAM,GAAG;AACzB,YAAM,IAAI;AAAA;AAAA,QAER;AAAA,MACF;AAAA,IACF;AACA,UAAM,MAAM,IAAI,iBAAiB,QAAQ,IAAI;AAC7C,SAAK,KAAK,IAAI,QAAQ,GAAG;AACzB,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,OAAO,IAA6C;AACxD,UAAM,SAAS,cAAc,EAAE;AAC/B,WAAO,KAAK,KAAK,IAAI,MAAM;AAAA,EAC7B;AAAA,EAEA,MAAM,UAAiC;AACrC,WAAO,MAAM,KAAK,KAAK,KAAK,OAAO,CAAC;AAAA,EACtC;AAAA,EAEA,MAAM,WAAW,UAAkB,UAAwC;AACzE,UAAM,eAAe,iBAAiB,QAAQ;AAC9C,UAAM,gBAAgB,iBAAiB,QAAQ;AAC/C,QAAI,KAAK,gBAAgB,IAAI,YAAY,GAAG;AAC1C,YAAM,IAAI;AAAA;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,UAAM,SAAgB,mBAAW;AACjC,UAAM,eAAe,MAAM,aAAa,aAAa;AACrD,UAAM,WAA2B;AAAA,MAC/B,IAAI;AAAA,MACJ,UAAU;AAAA,MACV;AAAA,MACA,WAAW,KAAK,IAAI;AAAA,IACtB;AAEA,SAAK,MAAM,IAAI,QAAQ,QAAQ;AAC/B,SAAK,gBAAgB,IAAI,cAAc,MAAM;AAC7C,WAAO,IAAI,kBAAkB,UAAU,IAAI;AAAA,EAC7C;AAAA,EAEA,YAAY,QAA4C;AACtD,WAAO,KAAK,MAAM,IAAI,MAAM;AAAA,EAC9B;AAAA,EAEA,MAAM,QAAQ,IAA8C;AAC1D,UAAM,aAAa,eAAe,EAAE;AACpC,UAAM,OAAO,KAAK,MAAM,IAAI,UAAU;AACtC,QAAI,MAAM;AACR,aAAO,IAAI,kBAAkB,MAAM,IAAI;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,kBAAkB,UAAoD;AAC1E,UAAM,eAAe,kBAAkB,QAAQ;AAC/C,QAAI,CAAC,aAAc,QAAO;AAC1B,UAAM,SAAS,KAAK,gBAAgB,IAAI,YAAY;AACpD,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,OAAO,KAAK,MAAM,IAAI,MAAM;AAClC,QAAI,MAAM;AACR,aAAO,IAAI,kBAAkB,MAAM,IAAI;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAmC;AACvC,WAAO,MAAM,KAAK,KAAK,MAAM,OAAO,CAAC,EAAE;AAAA,MACrC,CAAC,SAAS,IAAI,kBAAkB,MAAM,IAAI;AAAA,IAC5C;AAAA,EACF;AAAA,EAEA,WAAW,IAAqB;AAC9B,UAAM,aAAa,eAAe,EAAE;AACpC,SAAK,gBAAgB,UAAU;AAC/B,UAAM,OAAO,KAAK,MAAM,IAAI,UAAU;AACtC,QAAI,MAAM;AACR,WAAK,gBAAgB,OAAO,KAAK,QAAQ;AACzC,WAAK,MAAM,OAAO,UAAU;AAC5B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,UAAU,IAAqB;AAC7B,UAAM,SAAS,cAAc,EAAE;AAC/B,SAAK,oBAAoB,MAAM;AAC/B,WAAO,KAAK,KAAK,OAAO,MAAM;AAAA,EAChC;AAAA,EAEA,MAAM,WAAW,OAA4C;AAC3D,UAAM,IAAI;AAAA;AAAA,MAER,0DAA0D,QAAQ,UAAU,KAAK,MAAM,EAAE;AAAA,IAC3F;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,OAA4C;AACvD,UAAM,IAAI;AAAA;AAAA,MAER,sDAAsD,QAAQ,UAAU,KAAK,MAAM,EAAE;AAAA,IACvF;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,OAAgB,WAAgD;AAC1E,UAAM,OAAO,aAAa,KAAK,QAAQ,uBAAuB;AAC9D,UAAM,UAAU,MAAM,KAAK,QAAQ;AACnC,UAAM,aAAa,iBAAiB,SAAS,KAAK;AAElD,QAAI,cAAc;AAClB,eAAW,OAAO,YAAY;AAC5B,iBAAW,CAAC,KAAK,KAAK,KAAK,KAAK,WAAW,QAAQ,GAAG;AACpD,YAAI,IAAI,WAAW,GAAG,IAAI,EAAE,GAAG,GAAG;AAChC,cAAI,MAAM,UAAU,SAAS,MAAM;AACjC,kBAAM,aAAa,MAAM,UAAU,SAAS;AAC5C,kBAAM,UAAU,OAAO,GAAG,UAAU;AACpC,gBAAI,MAAM,UAAU,SAAS,GAAG;AAC9B,oBAAM,SAAS,MAAM,UAAU,CAAC,EAAE;AAAA,YACpC;AACA,2BAAe;AAAA,UACjB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,SAAS;AAAA,MACT;AAAA,MACA,eAAe;AAAA,MACf,SAAS,yCAAyC,WAAW;AAAA,IAC/D;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,KAAK,MAAM;AAChB,SAAK,WAAW,MAAM;AACtB,SAAK,MAAM,MAAM;AACjB,SAAK,gBAAgB,MAAM;AAAA,EAC7B;AACF;;;AExMO,IAAM,OAAN,MAAW;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc,oBAAI,IAA+B;AAAA;AAAA,EACjD,oBAAoB,oBAAI,IAA6B;AAAA,EACrD,oBAAoB,oBAAI,IAA+B;AAAA,EACvD,gBAAgB,oBAAI,IAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ5D,YAAY,SAAkB,UAAuB,CAAC,GAAG;AACvD,SAAK,UAAU;AACf,SAAK,kCACH,QAAQ,mCAAmC;AAC7C,SAAK,gBAAgB,QAAQ,iBAAiB;AAC9C,SAAK,cAAc,QAAQ,eAAe;AAC1C,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,wBAAgC;AAClC,WAAO,KAAK,kBAAkB;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iBAAiB,WAAsB,WAAW,aAAmB;AACnE,SAAK,cAAc,IAAI,WAAW,QAAQ;AAE1C,QAAI,KAAK,eAAe,CAAC,KAAK,YAAY,QAAQ,QAAQ,GAAG;AAC3D,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,QACA,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,MAAM;AAChB;AAAA,IACF;AAEA,QAAI,KAAK,gBAAgB,GAAG;AAC1B,YAAM,QAAQ,WAAW,MAAM;AAC7B,YAAI,CAAC,KAAK,kBAAkB,IAAI,SAAS,GAAG;AAC1C,eAAK,KAAK,WAAW;AAAA,YACnB;AAAA,YACA,SAAS;AAAA,UACX,CAAC;AACD,oBAAU,MAAM;AAAA,QAClB;AAAA,MACF,GAAG,KAAK,aAAa;AACrB,WAAK,kBAAkB,IAAI,WAAW,KAAK;AAAA,IAC7C;AAEA,QAAI,eAA8B,QAAQ,QAAQ;AAElD,cAAU,GAAG,WAAW,CAAC,SAAS;AAChC,YAAM,SAAS,KAAK,kBAAkB,IAAI,SAAS;AACnD,YAAM,cAAc,SAChB,WAAW,OAAO,KAAK,aAAa,OAAO,KAAK,EAAE,eAAe,OAAO,QAAQ,OAChF;AAEJ,qBAAe,aACZ,KAAK,YAAY;AAChB,YAAI;AACF,gBAAM,MAAM,OAAO,SAAS,WAAW,OAAO,KAAK,SAAS;AAC5D,gBAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,gBAAM,KAAK,cAAc,WAAW,GAAG;AAAA,QACzC,SAAS,KAAK;AACZ,gBAAM,UACJ,eAAe,QAAQ,IAAI,UAAU;AACvC,eAAK,QAAQ;AAAA,YACX,yDAAyD,WAAW;AAAA,YACpE;AAAA,UACF;AACA,eAAK,KAAK,WAAW;AAAA,YACnB;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,aAAK,QAAQ;AAAA,UACX,uDAAuD,WAAW;AAAA,UAClE;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACL,CAAC;AAED,cAAU,GAAG,SAAS,CAAC,QAAQ;AAC7B,YAAM,SAAS,KAAK,kBAAkB,IAAI,SAAS;AACnD,YAAM,cAAc,SAChB,WAAW,OAAO,KAAK,aAAa,OAAO,KAAK,EAAE,eAAe,OAAO,QAAQ,OAChF;AACJ,WAAK,QAAQ;AAAA,QACX,iDAAiD,WAAW;AAAA,QAC5D;AAAA,MACF;AACA,WAAK,kBAAkB,SAAS;AAAA,IAClC,CAAC;AAED,cAAU,GAAG,SAAS,MAAM;AAC1B,WAAK,kBAAkB,SAAS;AAAA,IAClC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,cAAc,WAAsB,KAAmC;AAC3E,QAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,OAAO,IAAI,SAAS,UAAU;AACnE,YAAM,IAAI;AAAA;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,YAAQ,IAAI,MAAM;AAAA,MAChB;AACE,cAAM,KAAK,kBAAkB,WAAW,GAAG;AAC3C;AAAA,MAEF;AACE,cAAM,KAAK,yBAAyB,WAAW,GAAG;AAClD;AAAA,MAEF;AACE,aAAK,kBAAkB,SAAS;AAChC;AAAA,MAEF;AACE,cAAM,IAAI;AAAA;AAAA,UAER;AAAA,QACF;AAAA,IACJ;AAAA,EACF;AAAA;AAAA,EAIA,MAAc,kBACZ,WACA,KACe;AACf,UAAM,KAAK,KAAK,cAAc,IAAI,SAAS,KAAK;AAEhD,UAAM,YAAY,KAAK,kBAAkB,IAAI,SAAS;AACtD,QAAI,WAAW;AACb,mBAAa,SAAS;AACtB,WAAK,kBAAkB,OAAO,SAAS;AAAA,IACzC;AAEA,QAAI,IAAI,oBAAoB,kBAAkB;AAC5C,WAAK,aAAa,cAAc,EAAE;AAClC,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,QACA,SAAS,0CAA0C,gBAAgB,SAAS,IAAI,eAAe;AAAA,MACjG,CAAC;AACD,gBAAU,MAAM;AAChB;AAAA,IACF;AAEA,QAAI,OAAO,IAAI,UAAU,YAAY,CAAC,IAAI,OAAO;AAC/C,WAAK,aAAa,cAAc,EAAE;AAClC,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,QACA,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,MAAM;AAChB;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,KAAK,QAAQ,eAAe,IAAI,KAAK;AACxD,QAAI,CAAC,MAAM;AACT,WAAK,aAAa,cAAc,EAAE;AAClC,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,QACA,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,MAAM;AAChB;AAAA,IACF;AAEA,QAAI,OAAO,IAAI,UAAU,YAAY,CAAC,IAAI,OAAO;AAC/C,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,QACA,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,MAAM;AAChB;AAAA,IACF;AAEA,UAAM,QAAQ,cAAc,IAAI,KAAK;AACrC,UAAM,MAAM,MAAM,KAAK,QAAQ,OAAO,KAAK;AAC3C,QAAI,CAAC,KAAK;AACR,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,QACA,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,MAAM;AAChB;AAAA,IACF;AAEA,UAAM,aAAa,GAAG,KAAK,IAAI,KAAK,EAAE;AACtC,QAAI,MAAM,KAAK,YAAY,IAAI,UAAU;AACzC,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,YAAY,IAAI,YAAY,GAAG;AAAA,IACtC;AAEA,QAAI,IAAI,QAAQ,KAAK,iCAAiC;AACpD,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,QACA,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,MAAM;AAChB;AAAA,IACF;AAEA,SAAK,aAAa,MAAM,EAAE;AAE1B,UAAM,WAAW;AAAA,MACf,IAAI,YAAY;AAAA,MAChB;AAAA,IACF;AACA,UAAM,iBAAiB,KAAK,kBAAkB,IAAI,SAAS;AAC3D,QAAI,gBAAgB;AAClB,YAAM,gBAAgB,GAAG,eAAe,KAAK,IAAI,eAAe,KAAK,EAAE;AACvE,YAAM,SAAS,KAAK,YAAY,IAAI,aAAa;AACjD,UAAI,QAAQ;AACV,eAAO,OAAO,cAAc;AAC5B,YAAI,OAAO,SAAS,GAAG;AACrB,eAAK,YAAY,OAAO,aAAa;AAAA,QACvC;AAAA,MACF;AACA,WAAK,kBAAkB,OAAO,SAAS;AAAA,IACzC;AAEA,UAAM,SAAuB;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,SAAK,kBAAkB,IAAI,WAAW,MAAM;AAC5C,QAAI,IAAI,MAAM;AAEd,UAAM,aAAa,MAAM,IAAI,cAAc,IAAI;AAC/C,UAAM,iBAAiB,MAAM,KAAK,YAAY;AAC9C,SAAK,KAAK,WAAW;AAAA,MACnB;AAAA,MACA,iBAAiB;AAAA,MACjB,QAAQ,KAAK;AAAA,MACb;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AAGD,UAAM,KAAK,YAAY,QAAQ,IAAI,WAAW;AAAA,EAChD;AAAA,EAEA,MAAc,yBACZ,WACA,KACe;AACf,UAAM,SAAS,KAAK,kBAAkB,IAAI,SAAS;AACnD,QAAI,CAAC,QAAQ;AACX,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,QACA,SAAS;AAAA,MACX,CAAC;AACD;AAAA,IACF;AAEA,QAAI,CAAC,MAAM,QAAQ,IAAI,OAAO,GAAG;AAC/B,YAAM,IAAI;AAAA;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,UAAM,eACJ,KAAK,QAAQ,SAAS,qBAAqB,IAAI,OAAO;AACxD,UAAM,aAAa,kBAAkB,IAAI,OAAO;AAChD,QAAI,aAAa,cAAc;AAC7B,YAAM,IAAI;AAAA;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,UAAM,UAAU,mBAAmB,IAAI,SAAS,SAAS;AAEzD,UAAM,MAAM,MAAM,KAAK,QAAQ,OAAO,OAAO,KAAK;AAClD,QAAI,CAAC,KAAK;AACR,YAAM,IAAI;AAAA;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,UAAM,EAAE,SAAS,OAAO,IAAI,MAAM,IAAI;AAAA,MACpC,OAAO;AAAA,MACP,IAAI;AAAA,IACN;AAGA,SAAK,KAAK,WAAW;AAAA,MACnB;AAAA,MACA;AAAA,MACA,YAAY;AAAA,IACd,CAAC;AAGD,QAAI,QAAQ,SAAS,GAAG;AACtB,WAAK,mBAAmB,OAAO,OAAO,OAAO,KAAK,IAAI,OAAO,UAAU;AAAA,QACrE;AAAA,QACA,cAAc,OAAO;AAAA,QACrB,SAAS;AAAA,QACT,KAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEQ,kBAAkB,WAA4B;AACpD,SAAK,KAAK,WAAW;AAAA,MACnB;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAIQ,kBAAkB,WAA4B;AACpD,UAAM,YAAY,KAAK,kBAAkB,IAAI,SAAS;AACtD,QAAI,WAAW;AACb,mBAAa,SAAS;AACtB,WAAK,kBAAkB,OAAO,SAAS;AAAA,IACzC;AACA,SAAK,cAAc,OAAO,SAAS;AAEnC,UAAM,SAAS,KAAK,kBAAkB,IAAI,SAAS;AACnD,QAAI,CAAC,OAAQ;AAEb,SAAK,kBAAkB,OAAO,SAAS;AACvC,UAAM,aAAa,GAAG,OAAO,KAAK,IAAI,OAAO,KAAK,EAAE;AACpD,UAAM,MAAM,KAAK,YAAY,IAAI,UAAU;AAC3C,QAAI,KAAK;AACP,UAAI,OAAO,MAAM;AACjB,UAAI,IAAI,SAAS,GAAG;AAClB,aAAK,YAAY,OAAO,UAAU;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,KAAK,WAAsB,KAA0B;AAC3D,QAAI,UAAU,eAAe,GAAc;AACzC,gBAAU,KAAK,KAAK,UAAU,GAAG,CAAC;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,MAAc,YACZ,QACA,aACe;AACf,UAAM,MAAM,MAAM,KAAK,QAAQ,OAAO,OAAO,KAAK;AAClD,QAAI,CAAC,KAAK;AACR,YAAM,IAAI;AAAA;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,UAAM,MAAM,eAAe;AAC3B,QAAI,QAAQ,GAAG;AAEb,YAAM,WAAW,MAAM,KAAK,eAAe,KAAK,OAAO,IAAI;AAC3D,YAAM,aAAa,MAAM,IAAI,cAAc,OAAO,IAAI;AACtD,WAAK,KAAK,OAAO,WAAW;AAAA,QAC1B;AAAA,QACA,KAAK;AAAA,QACL;AAAA,MACF,CAAC;AAAA,IACH,OAAO;AAEL,YAAM,EAAE,SAAS,YAAY,iBAAiB,IAC5C,MAAM,IAAI,gBAAgB,OAAO,MAAM,GAAG;AAG5C,UAAI,kBAAkB;AACpB,cAAM,WAAW,MAAM,KAAK,eAAe,KAAK,OAAO,IAAI;AAC3D,aAAK,KAAK,OAAO,WAAW;AAAA,UAC1B;AAAA,UACA,KAAK;AAAA,UACL;AAAA,QACF,CAAC;AAAA,MACH,OAAO;AACL,aAAK,KAAK,OAAO,WAAW;AAAA,UAC1B;AAAA,UACA,SAAS;AAAA,UACT,OAAO;AAAA,UACP;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,eACZ,KACA,MAC2B;AAC3B,UAAM,SAAS,MAAM,IAAI,UAAU;AACnC,UAAM,WAA6B,CAAC;AACpC,eAAW,SAAS,QAAQ;AAC1B,YAAM,UAAU,MAAM,MAAM,cAAc,IAAI;AAC9C,eAAS,KAAK,GAAG,OAAO;AAAA,IAC1B;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,mBACN,OACA,QACA,iBACA,KACM;AACN,UAAM,aAAa,GAAG,KAAK,IAAI,MAAM;AACrC,UAAM,UAAU,KAAK,YAAY,IAAI,UAAU;AAC/C,QAAI,CAAC,QAAS;AAEd,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,aAAa,iBAAiB;AACvC,aAAK,KAAK,OAAO,WAAW,GAAG;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AACF;;;AjBhVO,IAAM,eAAN,MAAmB;AAAA;AAAA,EAEf;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT,cAAkC;AAAA,EAClC,mBAA2C;AAAA,EAC3C,aAAsC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO9C,YAAY,UAA+B,CAAC,GAAG;AAC7C,SAAK,UAAU,QAAQ,WAAW,IAAI,cAAc;AACpD,SAAK,WAAW,kBAAkB,QAAQ,YAAY,EAAE;AACxD,SAAK,gBAAgB,QAAQ,iBAAiB,GAAG,KAAK,QAAQ;AAC9D,SAAK,oBAAoB,QAAQ,qBAAqB;AACtD,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,aACH,QAAQ,SAAS,QACb,OACA,OAAO,QAAQ,SAAS,WACtB,QAAQ,OACR,CAAC;AACT,SAAK,SAAS,QAAQ,WAAW,QAAQ,OAAQ,QAAQ,UAAU;AAEnE,UAAM,kBAAkB,QAAQ,gBAAgB;AAChD,QAAI,oBAAoB,OAAO;AAC7B,WAAK,iBAAiB;AACtB,WAAK,mBAAmB;AACxB,WAAK,oBAAoB;AACzB,WAAK,OAAO,IAAI,KAAK,KAAK,SAAS;AAAA,QACjC,iCAAiC;AAAA,QACjC,eAAe;AAAA,QACf,aAAa;AAAA,QACb,QAAQ,KAAK;AAAA,MACf,CAAC;AAAA,IACH,OAAO;AACL,YAAM,OACJ,OAAO,oBAAoB,WAAW,kBAAkB,CAAC;AAC3D,YAAM,WAAW,KAAK,YAAY;AAClC,YAAM,cAAc,KAAK,eAAe;AACxC,YAAM,mBAAmB,KAAK,oBAAoB;AAClD,YAAM,eAAe,KAAK,gBAAgB;AAE1C,WAAK,iBAAiB,IAAI,YAAY;AAAA,QACpC;AAAA,QACA,aAAa,KAAK,sBAAsB;AAAA,QACxC;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,WAAK,mBAAmB,IAAI,YAAY;AAAA,QACtC;AAAA,QACA,aAAa,KAAK,wBAAwB;AAAA,QAC1C;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,WAAK,oBAAoB,IAAI,YAAY;AAAA,QACvC;AAAA,QACA,aAAa,KAAK,yBAAyB;AAAA,MAC7C,CAAC;AAED,YAAM,cAAc,IAAI,YAAY;AAAA,QAClC;AAAA,QACA,aAAa,KAAK,qBAAqB;AAAA,QACvC;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,WAAK,OAAO,IAAI,KAAK,KAAK,SAAS;AAAA,QACjC,iCACE,KAAK,mCAAmC;AAAA,QAC1C,eAAe,KAAK,iBAAiB;AAAA,QACrC,aAAa;AAAA,QACb,QAAQ,KAAK;AAAA,MACf,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,aAAiC;AACnC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,kBAA0C;AAC5C,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,WAAW,OAAe,SAAmB,CAAC,GAAkB;AACpE,QAAI,MAAM,MAAM,KAAK,QAAQ,OAAO,KAAK;AACzC,QAAI,CAAC,KAAK;AACR,YAAM,MAAM,KAAK,QAAQ,UAAU,KAAK;AAAA,IAC1C;AACA,eAAW,SAAS,QAAQ;AAC1B,YAAM,WAAW,MAAM,IAAI,SAAS,KAAK;AACzC,UAAI,CAAC,UAAU;AACb,cAAM,IAAI,YAAY,KAAK;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,YAAY,UAAkB,UAAwC;AAC1E,UAAM,OAAO,MAAM,KAAK,QAAQ,kBAAkB,QAAQ;AAC1D,QAAI,MAAM;AACR,YAAM,KAAK,eAAe,QAAQ;AAClC,aAAO;AAAA,IACT;AACA,WAAO,KAAK,QAAQ,WAAW,UAAU,QAAQ;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,QAA2B;AAChC,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,0BAAgB;AAAA,QAC1C,UAAU;AAAA,QACV,mBAAmB;AAAA,UACjB,oBAAoB;AAAA,YAClB,OAAO;AAAA,YACP,UAAU;AAAA,UACZ;AAAA,UACA,WAAW;AAAA,UACX,yBAAyB;AAAA,UACzB,yBAAyB;AAAA,QAC3B;AAAA,MACF,CAAC;AACD,WAAK,iBAAiB,GAAG,cAAc,CAAC,IAAI,QAAQ;AAClD,cAAM,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI;AACzC,aAAK,KAAK,iBAAiB,IAAI,EAAE;AAAA,MACnC,CAAC;AAAA,IACH;AACA,WAAO,GAAG,WAAW,CAAC,KAAK,QAAQ,SAAS;AAC1C,YAAM,MAAM,IAAI;AAAA,QACd,IAAI,OAAO;AAAA,QACX,UAAU,IAAI,QAAQ,QAAQ,WAAW;AAAA,MAC3C;AACA,UAAI,IAAI,aAAa,KAAK,eAAe;AACvC,aAAK,kBAAkB,cAAc,KAAK,QAAQ,MAAM,CAAC,OAAO;AAC9D,eAAK,kBAAkB,KAAK,cAAc,IAAI,GAAG;AAAA,QACnD,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAO,OAAO,MAAM,OAAO,WAAiC;AAChE,UAAM,iBACJ,KAAK,QACL;AACF,UAAM,WACH,KAAK,QAAmC,YACzC,KAAK,mBAAmB;AAE1B,QAAI,kBAAkB,CAAC,UAAU;AAC/B,YAAM,SAAS,MAAM,KAAK,QAAQ,UAAU;AAC5C,WAAK,aAAa,kBAAkB,gBAAgB;AAAA,QAClD;AAAA,QACA;AAAA,QACA,SAAS,OAAO;AAAA,MAClB,CAAC;AAAA,IACH;AAEA,WAAO,IAAI,QAAqB,CAAC,SAAS,WAAW;AACnD,WAAK,cAAmB,kBAAa,OAAO,KAAK,QAAQ;AACvD,cAAM,UAAU,MAAM,KAAK,kBAAkB,KAAK,GAAG;AACrD,YAAI,CAAC,SAAS;AACZ,eAAK,SAAS,KAAK,KAAK,EAAE,OAAO,YAAY,CAAC;AAAA,QAChD;AAAA,MACF,CAAC;AACD,WAAK,OAAO,KAAK,WAAW;AAC5B,WAAK,YAAY,OAAO,MAAM,MAAM,MAAM;AACxC,YAAI,KAAK,aAAa;AACpB,gBAAM,OAAO,KAAK,YAAY,QAAQ;AACtC,gBAAM,aACJ,OAAO,SAAS,YAAY,OAAO,KAAK,OAAO;AACjD,cACE,KAAK,cACL,kBACA,KAAK,WAAW,KAAK,SAAS,YAC9B;AACA,iBAAK,WAAW,QAAQ;AACxB,iBAAK,aAAa,kBAAkB,gBAAgB;AAAA,cAClD,MAAM;AAAA,cACN;AAAA,cACA,SAAS,KAAK,WAAW,KAAK;AAAA,YAChC,CAAC;AAAA,UACH;AACA,kBAAQ,KAAK,WAAW;AAAA,QAC1B;AAAA,MACF,CAAC;AACD,WAAK,YAAY,GAAG,SAAS,CAAC,QAAQ;AACpC,YAAI,KAAK,YAAY;AACnB,eAAK,WAAW,QAAQ;AACxB,eAAK,aAAa;AAAA,QACpB;AACA,eAAO,GAAG;AAAA,MACZ,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,QAAI,KAAK,YAAY;AACnB,WAAK,WAAW,QAAQ;AACxB,WAAK,aAAa;AAAA,IACpB;AACA,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,UAAI,KAAK,kBAAkB;AACzB,mBAAW,UAAU,KAAK,iBAAiB,SAAS;AAClD,cAAI;AACF,mBAAO,UAAU;AAAA,UACnB,QAAQ;AAAA,UAER;AAAA,QACF;AACA,aAAK,iBAAiB,MAAM;AAC5B,aAAK,mBAAmB;AAAA,MAC1B;AACA,UAAI,KAAK,aAAa;AACpB,YAAI;AACF,eAAK,YAAY,sBAAsB;AAAA,QACzC,QAAQ;AAAA,QAER;AACA,aAAK,YAAY,MAAM,CAAC,QAAQ;AAC9B,eAAK,cAAc;AACnB,cAAI,IAAK,QAAO,GAAG;AAAA,cACd,SAAQ;AAAA,QACf,CAAC;AAAA,MACH,OAAO;AACL,gBAAQ;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,mBAIU;AACR,WAAO,CAAC,KAAK,KAAK,SAAS;AACzB,WAAK,kBAAkB,KAAK,GAAG,EAAE;AAAA,QAC/B,CAAC,YAAY;AACX,cAAI,CAAC,QAAS,MAAK;AAAA,QACrB;AAAA,QACA,CAAC,QAAQ;AACP,eAAK,GAAG;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,kBACJ,KACA,KACkB;AAClB,UAAM,MAAM,IAAI;AAAA,MACd,IAAI,OAAO;AAAA,MACX,UAAU,IAAI,QAAQ,QAAQ,WAAW;AAAA,IAC3C;AACA,UAAM,SAAS,IAAI,QAAQ,YAAY;AAEvC,QAAI,WAAW,WAAW;AACxB,WAAK,cAAc,KAAK,GAAG;AAC3B,aAAO;AAAA,IACT;AAEA,QAAI;AACF,UAAI,WAAW,SAAS,IAAI,aAAa,GAAG,KAAK,QAAQ,WAAW;AAClE,aAAK,aAAa,KAAK,GAAG;AAC1B,eAAO;AAAA,MACT;AAEA,UAAI,WAAW,SAAS,IAAI,aAAa,GAAG,KAAK,QAAQ,UAAU;AACjE,cAAM,KAAK,YAAY,KAAK,GAAG;AAC/B,eAAO;AAAA,MACT;AAEA,UAAI,WAAW,SAAS,IAAI,aAAa,GAAG,KAAK,QAAQ,YAAY;AACnE,cAAM,KAAK,cAAc,KAAK,GAAG;AACjC,eAAO;AAAA,MACT;AAEA,UACE,KAAK,qBACL,WAAW,UACX,IAAI,aAAa,GAAG,KAAK,QAAQ,kBACjC;AACA,cAAM,KAAK,eAAe,KAAK,GAAG;AAClC,eAAO;AAAA,MACT;AAEA,UAAI,WAAW,UAAU,IAAI,aAAa,GAAG,KAAK,QAAQ,eAAe;AACvE,cAAM,KAAK,YAAY,KAAK,GAAG;AAC/B,eAAO;AAAA,MACT;AAEA,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,YAAM,SAAS,sBAAsB,GAAG;AACxC,UAAI,UAAU,KAAK;AACjB,aAAK,QAAQ,MAAM,gCAAgC,GAAG;AAAA,MACxD,OAAO;AACL,aAAK,QAAQ,MAAM,uCAAuC,GAAG;AAAA,MAC/D;AACA,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU;AACjD,WAAK,SAAS,KAAK,QAAQ,EAAE,OAAO,IAAI,GAAG,GAAG;AAC9C,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAIQ,eAAe,KAAoD;AACzE,QAAI,CAAC,KAAK,WAAY,QAAO,CAAC;AAE9B,UAAM,UAAkC;AAAA,MACtC,gCAAgC;AAAA,MAChC,iCACE,KAAK,WAAW,kBAAkB,CAAC,gBAAgB,eAAe,GAClE,KAAK,IAAI;AAAA,IACb;AAEA,QACE,KAAK,WAAW,kBAChB,KAAK,WAAW,eAAe,SAAS,GACxC;AACA,cAAQ,+BAA+B,IACrC,KAAK,WAAW,eAAe,KAAK,IAAI;AAAA,IAC5C;AAEA,QAAI,KAAK,WAAW,WAAW,QAAW;AACxC,cAAQ,wBAAwB,IAAI,OAAO,KAAK,WAAW,MAAM;AAAA,IACnE;AAEA,UAAM,YAAY,KAAK,QAAQ;AAC/B,UAAM,SAAS,KAAK,WAAW,UAAU;AAEzC,QAAI,WAAW,KAAK;AAClB,UAAI,KAAK,WAAW,aAAa;AAC/B,YAAI,WAAW;AACb,kBAAQ,6BAA6B,IAAI;AACzC,kBAAQ,OAAO;AAAA,QACjB;AAAA,MACF,OAAO;AACL,gBAAQ,6BAA6B,IAAI;AAAA,MAC3C;AAAA,IACF,WAAW,OAAO,WAAW,UAAU;AACrC,cAAQ,6BAA6B,IAAI;AACzC,cAAQ,OAAO;AAAA,IACjB,WAAW,MAAM,QAAQ,MAAM,GAAG;AAChC,UAAI,aAAa,OAAO,SAAS,SAAS,GAAG;AAC3C,gBAAQ,6BAA6B,IAAI;AACzC,gBAAQ,OAAO;AAAA,MACjB;AAAA,IACF,WAAW,WAAW,QAAQ,WAAW;AACvC,cAAQ,6BAA6B,IAAI;AACzC,cAAQ,OAAO;AAAA,IACjB;AAEA,QAAI,KAAK,WAAW,aAAa;AAC/B,cAAQ,kCAAkC,IAAI;AAAA,IAChD;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,SACN,KACA,QACA,MACA,KACA;AACA,QAAI,UAAU,QAAQ;AAAA,MACpB,gBAAgB;AAAA,MAChB,GAAG,KAAK,eAAe,GAAG;AAAA,IAC5B,CAAC;AACD,QAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAAA,EAC9B;AAAA,EAEA,MAAc,aAAa,KAA6C;AACtE,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAI,OAAO;AACX,UAAI,GAAG,QAAQ,CAAC,UAAU;AACxB,gBAAQ;AACR,YAAI,KAAK,SAAS,OAAO,MAAM;AAC7B;AAAA,YACE,IAAI;AAAA;AAAA,cAEF;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF,CAAC;AACD,UAAI,GAAG,OAAO,MAAM;AAClB,YAAI;AACF,kBAAQ,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC;AAAA,QACtC,QAAQ;AACN;AAAA,YACE,IAAI;AAAA;AAAA,cAEF;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF,CAAC;AACD,UAAI,GAAG,SAAS,MAAM;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEQ,cACN,KACA,KACM;AACN,QAAI,UAAU,KAAK,KAAK,eAAe,GAAG,CAAC;AAC3C,QAAI,IAAI;AAAA,EACV;AAAA,EAEQ,aACN,KACA,KACM;AACN,SAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA;AAAA,QACE,QAAQ;AAAA,QACR,QAAQ,QAAQ,OAAO;AAAA,MACzB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,YACZ,KACA,KACe;AACf,QAAI;AACF,YAAM,KAAK,QAAQ,QAAQ;AAC3B,WAAK,SAAS,KAAK,KAAK,EAAE,QAAQ,QAAQ,GAAG,GAAG;AAAA,IAClD,SAAS,KAAK;AACZ,YAAM,UACJ,eAAe,QAAQ,IAAI,UAAU;AACvC,WAAK,QAAQ,MAAM,4BAA4B,GAAG;AAClD,WAAK,SAAS,KAAK,KAAK,EAAE,QAAQ,WAAW,OAAO,QAAQ,GAAG,GAAG;AAAA,IACpE;AAAA,EACF;AAAA,EAEA,MAAc,cACZ,KACA,KACe;AACf,UAAM,OAAO,MAAM,KAAK,QAAQ,QAAQ;AACxC,SAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA;AAAA,QACE,QAAQ,QAAQ,OAAO;AAAA,QACvB,kBAAkB,KAAK,KAAK;AAAA,QAC5B,WAAW,KAAK;AAAA,QAChB,aAAa,QAAQ,YAAY;AAAA,MACnC;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,eACZ,KACA,KACe;AACf,UAAM,KAAK,KAAK,YAAY,GAAG;AAC/B,QAAI,KAAK,qBAAqB,CAAC,KAAK,kBAAkB,QAAQ,EAAE,GAAG;AACjE,WAAK,SAAS,KAAK,KAAK,EAAE,OAAO,iCAAiC,GAAG,GAAG;AACxE;AAAA,IACF;AAEA,UAAM,cAAc,MAAM,KAAK,gBAAgB,KAAK,GAAG;AACvD,QAAI,CAAC,YAAa;AAElB,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,QAAQ;AAAA,QAC9B,YAAY;AAAA,QACZ,YAAY;AAAA,MACd;AACA,YAAM,QAAQ,MAAM,KAAK,YAAY;AACrC,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA;AAAA,UACE,QAAQ,KAAK;AAAA,UACb,UAAU,KAAK;AAAA,UACf;AAAA,QACF;AAAA,QACA;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,SAAS,sBAAsB,GAAG;AACxC,UAAI,UAAU,KAAK;AACjB,aAAK,QAAQ,MAAM,uBAAuB,GAAG;AAAA,MAC/C,OAAO;AACL,aAAK,QAAQ,MAAM,8BAA8B,GAAG;AAAA,MACtD;AACA,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU;AACjD,WAAK,SAAS,KAAK,QAAQ,EAAE,OAAO,IAAI,GAAG,GAAG;AAAA,IAChD;AAAA,EACF;AAAA,EAEA,MAAc,YACZ,KACA,KACe;AACf,UAAM,KAAK,KAAK,YAAY,GAAG;AAC/B,QAAI,KAAK,kBAAkB,CAAC,KAAK,eAAe,QAAQ,EAAE,GAAG;AAC3D,WAAK,SAAS,KAAK,KAAK,EAAE,OAAO,0BAA0B,GAAG,GAAG;AACjE;AAAA,IACF;AAEA,UAAM,cAAc,MAAM,KAAK,gBAAgB,KAAK,GAAG;AACvD,QAAI,CAAC,YAAa;AAElB,UAAM,UAAU,GAAG,EAAE,IAAI,YAAY,QAAQ;AAC7C,QAAI,KAAK,oBAAoB,CAAC,KAAK,iBAAiB,QAAQ,OAAO,GAAG;AACpE,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA,EAAE,OAAO,2CAA2C;AAAA,QACpD;AAAA,MACF;AACA;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,KAAK,QAAQ,kBAAkB,YAAY,QAAQ;AACtE,UAAM,QAAQ,OACV,MAAM,KAAK,eAAe,YAAY,QAAQ,IAC9C,MAAM,wBAAwB,YAAY,QAAQ;AAEtD,QAAI,CAAC,QAAQ,CAAC,OAAO;AACnB,WAAK,gBAAgB,cAAc,EAAE;AACrC,WAAK,kBAAkB,cAAc,OAAO;AAC5C,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA;AAAA,UACE,OAAO;AAAA,QACT;AAAA,QACA;AAAA,MACF;AACA;AAAA,IACF;AAEA,SAAK,gBAAgB,MAAM,EAAE;AAC7B,SAAK,kBAAkB,MAAM,OAAO;AAEpC,UAAM,QAAQ,MAAM,KAAK,YAAY;AACrC,SAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA;AAAA,QACE,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,gBACZ,KACA,KACwD;AACxD,UAAM,OAAO,MAAM,KAAK,aAAa,GAAG;AACxC,UAAM,EAAE,UAAU,SAAS,IAAI;AAI/B,UAAM,eAAe,kBAAkB,YAAY,EAAE;AACrD,UAAM,eAAe,kBAAkB,YAAY,EAAE;AACrD,QAAI,CAAC,gBAAgB,CAAC,cAAc;AAClC,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA;AAAA,UACE,OAAO;AAAA,QACT;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,IACT;AACA,WAAO,EAAE,UAAU,cAAc,UAAU,aAAa;AAAA,EAC1D;AAAA,EAEQ,YAAY,KAAmC;AACrD,QAAI,KAAK,YAAY;AACnB,YAAM,YAAY,IAAI,QAAQ,iBAAiB;AAC/C,UAAI,OAAO,cAAc,UAAU;AACjC,cAAM,QAAQ,UAAU,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAC3C,YAAI,MAAO,QAAO;AAAA,MACpB;AAAA,IACF;AACA,WAAO,IAAI,OAAO,iBAAiB;AAAA,EACrC;AACF;AAIA,SAAS,sBAAsB,KAAsB;AACnD,MAAI,eAAe,mBAAmB;AACpC,YAAQ,IAAI,MAAM;AAAA,MAChB;AAAA,MACA;AACE,eAAO;AAAA,MACT;AAAA,MACA;AACE,eAAO;AAAA,MACT;AACE,eAAO;AAAA,MACT;AACE,eAAO;AAAA,MACT;AACE,eAAO;AAAA,MACT;AACE,eAAO;AAAA,MACT;AACE,eAAO;AAAA,IACX;AAAA,EACF;AACA,SAAO;AACT;;;ADzxBO,SAAS,aAAa,UAA+B,CAAC,GAAW;AACtE,MAAI,eAAoC;AAExC,iBAAe,YACb,QACe;AACf,mBAAe,IAAI,aAAa;AAAA,MAC9B,SAAS,QAAQ,WAAW,IAAI,cAAc;AAAA,MAC9C,QAAQ,QAAQ,UAAU;AAAA,MAC1B,GAAG;AAAA,IACL,CAAC;AAED,QAAI,QAAQ,MAAM;AAChB,iBAAW,OAAO,QAAQ,MAAM;AAC9B,cAAM,aAAa,WAAW,IAAI,OAAO,IAAI,UAAU,CAAC,CAAC;AAAA,MAC3D;AAAA,IACF;AAEA,QAAI,QAAQ,OAAO;AACjB,iBAAW,QAAQ,QAAQ,OAAO;AAChC,cAAM,aAAa,YAAY,KAAK,UAAU,KAAK,QAAQ;AAAA,MAC7D;AAAA,IACF;AAEA,QAAI,OAAO,YAAY;AACrB,mBAAa;AAAA,QACX,OAAO;AAAA,MACT;AACA,aAAO,WAAW,GAAG,SAAS,MAAM;AAClC,sBAAc,MAAM,EAAE,MAAM,MAAM;AAAA,QAElC,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAEA,WAAO,YAAY,IAAI,aAAa,iBAAiB,CAAC;AAAA,EACxD;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,iBAAiB;AAAA,IACjB,wBAAwB;AAAA,IACxB,MAAM,cAAc;AAClB,UAAI,cAAc;AAChB,cAAM,aAAa,MAAM;AACzB,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACF;","names":["path","fs","path","crypto"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TetherDB Vite Plugin — Zero-config local development and preview server integration.
|
|
3
|
+
*
|
|
4
|
+
* @module tetherdb/vite
|
|
5
|
+
*/
|
|
6
|
+
import type { Plugin } from 'vite';
|
|
7
|
+
import { type TetherServerOptions } from '../server/server.cjs';
|
|
8
|
+
/**
|
|
9
|
+
* Application and table declaration for automatic provisioning on startup.
|
|
10
|
+
*/
|
|
11
|
+
export interface TetherPluginAppDeclaration {
|
|
12
|
+
/** Unique application identifier. */
|
|
13
|
+
appId: string;
|
|
14
|
+
/** Array of table names to declare within the application. */
|
|
15
|
+
tables?: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* User account declaration for automatic provisioning on startup.
|
|
19
|
+
*/
|
|
20
|
+
export interface TetherPluginUserDeclaration {
|
|
21
|
+
/** Account username. */
|
|
22
|
+
username: string;
|
|
23
|
+
/** Account password. */
|
|
24
|
+
password: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Options for configuring the TetherDB Vite plugin.
|
|
28
|
+
*/
|
|
29
|
+
export interface TetherPluginOptions extends TetherServerOptions {
|
|
30
|
+
/** Applications and tables to automatically declare on server startup. */
|
|
31
|
+
apps?: TetherPluginAppDeclaration[];
|
|
32
|
+
/** Default user accounts to automatically declare or update on server startup. */
|
|
33
|
+
users?: TetherPluginUserDeclaration[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates a Vite plugin that runs an embedded TetherDB synchronization and REST
|
|
37
|
+
* authentication backend directly within the Vite dev and preview servers.
|
|
38
|
+
*
|
|
39
|
+
* @param options - Configuration options for storage, endpoints, apps, and users.
|
|
40
|
+
* @returns Vite plugin object.
|
|
41
|
+
*/
|
|
42
|
+
export declare function tetherPlugin(options?: TetherPluginOptions): Plugin;
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TetherDB Vite Plugin — Zero-config local development and preview server integration.
|
|
3
|
+
*
|
|
4
|
+
* @module tetherdb/vite
|
|
5
|
+
*/
|
|
6
|
+
import type { Plugin } from 'vite';
|
|
7
|
+
import { type TetherServerOptions } from '../server/server.js';
|
|
8
|
+
/**
|
|
9
|
+
* Application and table declaration for automatic provisioning on startup.
|
|
10
|
+
*/
|
|
11
|
+
export interface TetherPluginAppDeclaration {
|
|
12
|
+
/** Unique application identifier. */
|
|
13
|
+
appId: string;
|
|
14
|
+
/** Array of table names to declare within the application. */
|
|
15
|
+
tables?: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* User account declaration for automatic provisioning on startup.
|
|
19
|
+
*/
|
|
20
|
+
export interface TetherPluginUserDeclaration {
|
|
21
|
+
/** Account username. */
|
|
22
|
+
username: string;
|
|
23
|
+
/** Account password. */
|
|
24
|
+
password: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Options for configuring the TetherDB Vite plugin.
|
|
28
|
+
*/
|
|
29
|
+
export interface TetherPluginOptions extends TetherServerOptions {
|
|
30
|
+
/** Applications and tables to automatically declare on server startup. */
|
|
31
|
+
apps?: TetherPluginAppDeclaration[];
|
|
32
|
+
/** Default user accounts to automatically declare or update on server startup. */
|
|
33
|
+
users?: TetherPluginUserDeclaration[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates a Vite plugin that runs an embedded TetherDB synchronization and REST
|
|
37
|
+
* authentication backend directly within the Vite dev and preview servers.
|
|
38
|
+
*
|
|
39
|
+
* @param options - Configuration options for storage, endpoints, apps, and users.
|
|
40
|
+
* @returns Vite plugin object.
|
|
41
|
+
*/
|
|
42
|
+
export declare function tetherPlugin(options?: TetherPluginOptions): Plugin;
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAgC,MAAM,MAAM,CAAC;AACjE,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG7E;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC9D,0EAA0E;IAC1E,IAAI,CAAC,EAAE,0BAA0B,EAAE,CAAC;IACpC,kFAAkF;IAClF,KAAK,CAAC,EAAE,2BAA2B,EAAE,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAiDtE"}
|