@rebasepro/server-mongo 0.22.0 → 0.23.0

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.
@@ -80,7 +80,7 @@ export declare class MongoDriver implements DataDriver {
80
80
  /**
81
81
  * Check if a field value is unique
82
82
  */
83
- checkUniqueField(path: string, name: string, value: any, id?: string, collection?: CollectionConfig): Promise<boolean>;
83
+ checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean>;
84
84
  /**
85
85
  * Generate a new row ID
86
86
  */
@@ -129,6 +129,14 @@ export declare class AuthenticatedMongoDriver implements DataDriver {
129
129
  * guest's `fetchCollection` on this same driver.
130
130
  */
131
131
  private authContext;
132
+ /**
133
+ * The caller per-field `access` is judged against.
134
+ *
135
+ * Never `undefined`: that is the trusted server plane, which satisfies
136
+ * every non-empty role list, and every call through this driver is made
137
+ * for a user — an anonymous one included, as `roles: ["anon"]`.
138
+ */
139
+ private viewer;
132
140
  /**
133
141
  * Evaluate the collection's rules for one row, fail-closed.
134
142
  *
@@ -151,7 +159,13 @@ export declare class AuthenticatedMongoDriver implements DataDriver {
151
159
  */
152
160
  save<M extends Record<string, any>>(props: SaveProps<M>): Promise<Record<string, unknown>>;
153
161
  delete<M extends Record<string, any>>(props: DeleteProps<M>): Promise<void>;
154
- checkUniqueField(path: string, name: string, value: any, id?: string, collection?: CollectionConfig): Promise<boolean>;
162
+ /**
163
+ * Whether `value` is free in `name`, asked about a field this caller could
164
+ * have read — anything else is an oracle over a value the strip withholds,
165
+ * one guess at a time. A registered collection names its fields, so an
166
+ * undeclared name is refused rather than counted.
167
+ */
168
+ checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean>;
155
169
  generateId(path: string, collection?: CollectionConfig): string;
156
170
  count<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<number>;
157
171
  isReady(): boolean;
@@ -1,5 +1,5 @@
1
1
  import { ANONYMOUS_USER_ID, isDocumentAdmin, isSchemaAdmin } from "@rebasepro/types";
2
- import { ApiError, assertWriteRequestValid, declaredErrorAnswer, extractUserFromToken, logger, resolveRequireAuth } from "@rebasepro/server";
2
+ import { ApiError, assertFieldOpsValid, assertWriteRequestValid, declaredErrorAnswer, extractUserFromToken, logger, resolveRequireAuth } from "@rebasepro/server";
3
3
  import { WebSocketServer } from "ws";
4
4
  //#region src/websocket.ts
5
5
  var WS_RATE_LIMIT = 2e3;
@@ -197,7 +197,8 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
197
197
  if (!path || !values || typeof values !== "object") return;
198
198
  const collection = driver.registry?.getCollectionByPath(path);
199
199
  if (!collection) return;
200
- assertWriteRequestValid(values, collection);
200
+ assertWriteRequestValid(values, collection, { viewer: { roles: sessionUser(clientSessions.get(clientId)).roles ?? [] } });
201
+ assertFieldOpsValid(values, collection);
201
202
  };
202
203
  const getScopedDelegate = () => driver.withAuth(sessionUser(clientSessions.get(clientId)));
203
204
  switch (type) {
@@ -253,9 +254,9 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
253
254
  break;
254
255
  }
255
256
  case "CHECK_UNIQUE_FIELD": {
256
- const { path, name, value, id, collection } = payload;
257
+ const { path, name, value, id } = payload;
257
258
  assertRegisteredPath(path);
258
- const isUnique = await (await getScopedDelegate()).checkUniqueField(path, name, value, id, collection);
259
+ const isUnique = await (await getScopedDelegate()).checkUniqueField(path, name, value, id, driver.registry?.getCollectionByPath(path));
259
260
  ws.send(JSON.stringify({
260
261
  type: "CHECK_UNIQUE_FIELD_SUCCESS",
261
262
  payload: { isUnique },
@@ -382,4 +383,4 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
382
383
  //#endregion
383
384
  export { createMongoWebSocket };
384
385
 
385
- //# sourceMappingURL=websocket-C3iT6srl.js.map
386
+ //# sourceMappingURL=websocket-BtK4_9ZS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-BtK4_9ZS.js","names":[],"sources":["../src/websocket.ts"],"sourcesContent":["import { ANONYMOUS_USER_ID, RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User, AuthAdapter } from \"@rebasepro/types\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { Server } from \"http\";\nimport { inspect } from \"util\";\nimport { extractUserFromToken, resolveRequireAuth, assertWriteRequestValid, assertFieldOpsValid, ApiError, declaredErrorAnswer } from \"@rebasepro/server\";\nimport type { RebaseAuthConfig } from \"@rebasepro/server\";\nimport { MongoRealtimeService } from \"./services/MongoRealtimeService\";\nimport { MongoDriver } from \"./services/MongoDriver\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Normalized user identity for WebSocket sessions — the same shape the Postgres\n * socket keeps, because an `AuthAdapter` user is not an access-token payload.\n */\ninterface WsUserIdentity {\n uid: string;\n email?: string;\n displayName?: string;\n photoURL?: string;\n roles: string[];\n isAdmin: boolean;\n /**\n * Whether this session is a guest — anonymous sign-in rather than an\n * account. Required, so every way of signing in has to say: a guest has a\n * real uid, and `policy.registered()` has nothing else to tell it from an\n * account by. Neither sign-in path read it, and every frame was then\n * scoped as an account.\n */\n isAnonymous: boolean;\n}\n\ninterface ClientSession {\n ws: WebSocket;\n user?: WsUserIdentity;\n authenticated: boolean;\n messageCount: number;\n messageWindowStart: number;\n}\n\nconst WS_RATE_LIMIT = 2000;\nconst WS_RATE_WINDOW_MS = 60_000;\n\nconst ADMIN_ONLY_TYPES = new Set([\n \"EXECUTE_SQL\",\n \"FETCH_DATABASES\",\n \"FETCH_ROLES\",\n \"FETCH_UNMAPPED_TABLES\",\n \"FETCH_TABLE_METADATA\",\n \"FETCH_CURRENT_DATABASE\",\n \"CREATE_BRANCH\",\n \"DELETE_BRANCH\",\n \"LIST_BRANCHES\"\n]);\n\n/**\n * Who a socket reads and writes as, for its request frames and its\n * subscriptions alike — one answer, so the two cannot disagree.\n *\n * A socket with no session (only possible with `requireAuth: false`) is the\n * anonymous user, exactly as REST scopes the same caller. It used to get the\n * base driver instead, which on this engine applies no security rules at all:\n * such a socket read every row and wrote wherever it liked.\n */\nfunction sessionUser(session: ClientSession | undefined): User {\n if (!session?.user) {\n return {\n uid: ANONYMOUS_USER_ID,\n displayName: null,\n email: null,\n photoURL: null,\n providerId: \"websocket\",\n isAnonymous: false,\n roles: [\"anon\"]\n };\n }\n return {\n uid: session.user.uid,\n email: session.user.email ?? \"\",\n displayName: session.user.displayName ?? \"\",\n photoURL: session.user.photoURL ?? \"\",\n providerId: \"jwt\",\n isAnonymous: session.user.isAnonymous,\n roles: session.user.roles ?? []\n };\n}\n\nfunction isAdminSession(session: ClientSession | undefined): boolean {\n if (!session?.user) return false;\n // The adapter's own answer first; a role *named* `admin` is only the\n // fallback for the built-in JWT path.\n if (session.user.isAdmin) return true;\n return (session.user.roles ?? []).some((r) => r === \"admin\");\n}\n\nexport function createMongoWebSocket(\n server: Server,\n realtimeService: MongoRealtimeService,\n driver: MongoDriver,\n authConfig?: RebaseAuthConfig,\n admin?: DatabaseAdmin,\n authAdapter?: AuthAdapter\n) {\n // Scoped to this factory invocation rather than the module, so sessions do\n // not leak across hot reloads or a second server on the same process — the\n // Postgres socket keeps it here for the same reason.\n const clientSessions = new Map<string, ClientSession>();\n\n const isProduction = process.env.NODE_ENV === \"production\";\n const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };\n const wss = new WebSocketServer({ server });\n\n wss.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") return;\n logger.error(\"❌ [WebSocket Server] Error\", { error: err });\n });\n\n // The same predicate the HTTP data routes use, from the same function. See\n // `resolveRequireAuth` for what this socket's local copy got wrong — most\n // importantly that a `false` here does not skip a check, it marks every\n // session `authenticated` at connect time.\n const requireAuth = !!authAdapter || resolveRequireAuth(authConfig);\n\n if (requireAuth && !authAdapter && !authConfig?.jwtSecret) {\n logger.warn(\n \"🔐 [WebSocket Server] Authentication is required but no adapter or jwtSecret is \" +\n \"configured — no client can complete AUTH, so every realtime message will be refused.\"\n );\n }\n\n wss.on(\"connection\", (ws) => {\n const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n wsDebug(`WebSocket client connected: ${clientId}`);\n\n clientSessions.set(clientId, { ws,\nauthenticated: !requireAuth,\nmessageCount: 0,\nmessageWindowStart: Date.now() });\n realtimeService.addClient(clientId, ws);\n\n ws.on(\"close\", () => {\n wsDebug(`WebSocket client disconnected: ${clientId}`);\n clientSessions.delete(clientId);\n });\n\n ws.on(\"message\", async (message) => {\n let requestId: string | undefined;\n try {\n const { type, payload, requestId: reqId } = JSON.parse(message.toString());\n requestId = reqId;\n\n wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : \"\");\n\n const sendError = (errType: \"ERROR\" | \"AUTH_ERROR\", code: string, msg: string) => {\n ws.send(JSON.stringify({ type: errType,\nrequestId,\npayload: { error: { message: msg,\ncode } } }));\n };\n\n if (type === \"AUTHENTICATE\") {\n const { token } = payload || {};\n if (!token) {\n sendError(\"AUTH_ERROR\", \"INVALID_INPUT\", \"Token is required\");\n return;\n }\n\n // The adapter verifies when one is configured, exactly as the\n // HTTP routes do; the built-in JWT path is the fallback.\n let verifiedUser: WsUserIdentity | null = null;\n\n if (authAdapter) {\n try {\n const adapterUser = authAdapter.verifyToken\n ? await authAdapter.verifyToken(token)\n : await authAdapter.verifyRequest(new Request(\"http://localhost/_ws_auth\", {\n headers: { Authorization: `Bearer ${token}` }\n }));\n if (adapterUser) {\n verifiedUser = {\n uid: adapterUser.uid,\n email: adapterUser.email,\n roles: adapterUser.roles ?? [],\n isAdmin: !!adapterUser.isAdmin,\n // Absent from an adapter with no such\n // concept, and absent reads as \"not a guest\".\n isAnonymous: adapterUser.isAnonymous === true\n };\n }\n } catch {\n // Adapter threw — treat as invalid token\n }\n } else {\n const jwtPayload = await extractUserFromToken(token);\n if (jwtPayload) {\n verifiedUser = {\n uid: jwtPayload.uid,\n email: jwtPayload.email,\n displayName: jwtPayload.displayName,\n photoURL: jwtPayload.photoURL,\n roles: jwtPayload.roles ?? [],\n isAdmin: (jwtPayload.roles ?? []).some((r: string) => r === \"admin\"),\n isAnonymous: jwtPayload.isAnonymous === true\n };\n }\n }\n\n if (verifiedUser) {\n const session = clientSessions.get(clientId);\n if (session) {\n session.user = verifiedUser;\n session.authenticated = true;\n }\n ws.send(JSON.stringify({ type: \"AUTH_SUCCESS\",\nrequestId,\npayload: { uid: verifiedUser.uid,\nroles: verifiedUser.roles } }));\n } else {\n sendError(\"AUTH_ERROR\", \"INVALID_TOKEN\", \"Invalid or expired token\");\n }\n return;\n }\n\n if (requireAuth) {\n const session = clientSessions.get(clientId);\n if (!session?.authenticated) {\n sendError(\"ERROR\", \"UNAUTHORIZED\", \"Authentication required\");\n return;\n }\n }\n\n {\n const session = clientSessions.get(clientId);\n if (session) {\n const now = Date.now();\n if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {\n session.messageCount = 0;\n session.messageWindowStart = now;\n }\n session.messageCount++;\n if (session.messageCount > WS_RATE_LIMIT) {\n sendError(\"ERROR\", \"RATE_LIMITED\", \"Too many requests. Please slow down.\");\n return;\n }\n }\n }\n\n if (ADMIN_ONLY_TYPES.has(type)) {\n const session = clientSessions.get(clientId);\n if (!isAdminSession(session)) {\n sendError(\"ERROR\", \"FORBIDDEN\", \"Admin access required for this operation\");\n return;\n }\n }\n\n /**\n * Refuse a frame whose path names no registered data collection.\n *\n * MongoDB has no row-level security, so on this engine the\n * registry *is* the access model: `securityRules` are enforced\n * only for a collection the registry resolves, and\n * `MongoDataService.getCollection` maps any path to a physical\n * collection by replacing `/` with `_`. A path the registry does\n * not know therefore reaches the database with no rule to apply —\n * `AuthenticatedMongoDriver.authorize(undefined)` answers\n * \"allowed\" and the RLS filter for an undefined collection is\n * \"match all\" — which put the auth store (`rebase_users` and its\n * password hashes, `rebase_user_roles`, `rebase_refresh_tokens`)\n * one frame away from any caller: authenticated, or anonymous\n * when `requireAuth` is false.\n *\n * The socket is the one client-facing door onto this driver. REST\n * mounts routes per registered slug and 404s everything else, and\n * in-process writes are trusted server code — so the registry\n * check lives here, at the boundary this door owns, exactly as the\n * write validation beside it does. A `notFound` matches what REST\n * answers for an unknown collection, and does not distinguish a\n * collection that exists in Mongo from one that does not.\n */\n const assertRegisteredPath = (path: string | undefined): void => {\n if (!path || !driver.registry?.getCollectionByPath(path)) {\n throw ApiError.notFound(\n `Unknown collection at path \"${path ?? \"\"}\": it is not a registered data collection.`\n );\n }\n };\n\n /** @see the Postgres socket — same rule, same reason. */\n const assertWriteRequest = (path: string | undefined, values: unknown): void => {\n if (!path || !values || typeof values !== \"object\") return;\n const collection = driver.registry?.getCollectionByPath(path);\n if (!collection) return;\n // The roles this socket's driver is scoped with — `[\"anon\"]`\n // for one with no session. Never `undefined`: that is the\n // trusted server plane, which satisfies every `access.write`\n // list, so omitting it let any caller write any field.\n assertWriteRequestValid(values as Record<string, unknown>, collection, {\n viewer: { roles: sessionUser(clientSessions.get(clientId)).roles ?? [] }\n });\n // `$push` on a number is the caller's mistake, answered here\n // as the 400 `PATCH` gives it — the same check the Postgres\n // socket runs at this door.\n assertFieldOpsValid(values as Record<string, unknown>, collection);\n };\n\n // Always scoped. A failure to scope propagates to the frame's\n // `catch` rather than falling back to the base driver, which\n // is what it did: the fallback is the unscoped read.\n const getScopedDelegate = (): Promise<DataDriver> =>\n driver.withAuth(sessionUser(clientSessions.get(clientId)));\n\n switch (type) {\n case \"FETCH_COLLECTION\": {\n const request: FetchCollectionProps = payload;\n assertRegisteredPath(request.path);\n const delegate = await getScopedDelegate();\n const rows = await delegate.fetchCollection(request);\n ws.send(JSON.stringify({ type: \"FETCH_COLLECTION_SUCCESS\",\npayload: { rows },\nrequestId }));\n break;\n }\n case \"FETCH_ONE\": {\n const request: FetchOneProps = payload;\n assertRegisteredPath(request.path);\n const delegate = await getScopedDelegate();\n const row = await delegate.fetchOne(request);\n ws.send(JSON.stringify({ type: \"FETCH_ONE_SUCCESS\",\npayload: { row },\nrequestId }));\n break;\n }\n case \"SAVE\": {\n const request: SaveProps = payload;\n // The REST layer's write checks, at this boundary too —\n // the socket is a second way in, and it used to be the\n // unchecked one. Collection from the registry by path,\n // never from the client's `request.collection`.\n assertRegisteredPath(request.path);\n assertWriteRequest(request.path, request.values as Record<string, unknown>);\n const delegate = await getScopedDelegate();\n const row = await delegate.save(request);\n ws.send(JSON.stringify({ type: \"SAVE_SUCCESS\",\npayload: { row },\nrequestId }));\n break;\n }\n case \"DELETE\": {\n const request: DeleteProps = payload;\n assertRegisteredPath(request.row?.path);\n const delegate = await getScopedDelegate();\n // The address, and nothing else the frame says: the\n // driver reads the row and resolves the collection by\n // path — see the Postgres socket's DELETE.\n await delegate.delete({\n row: { id: request.row.id, path: request.row.path },\n hard: request.hard\n });\n ws.send(JSON.stringify({ type: \"DELETE_SUCCESS\",\npayload: { success: true },\nrequestId }));\n break;\n }\n case \"CHECK_UNIQUE_FIELD\": {\n const { path, name, value, id } = payload;\n assertRegisteredPath(path);\n const delegate = await getScopedDelegate();\n // The registry's collection, never the frame's: its\n // properties are what decides which names may be asked.\n const isUnique = await delegate.checkUniqueField(path, name, value, id, driver.registry?.getCollectionByPath(path));\n ws.send(JSON.stringify({ type: \"CHECK_UNIQUE_FIELD_SUCCESS\",\npayload: { isUnique },\nrequestId }));\n break;\n }\n case \"COUNT\": {\n const request: FetchCollectionProps = payload;\n assertRegisteredPath(request.path);\n const delegate = await getScopedDelegate();\n const count = await delegate.count!(request);\n ws.send(JSON.stringify({ type: \"COUNT_SUCCESS\",\npayload: { count },\nrequestId }));\n break;\n }\n case \"EXECUTE_SQL\": {\n const { sql, options } = payload;\n if (admin && isDocumentAdmin(admin) && admin.executeAggregate) {\n const result = await admin.executeAggregate(sql as Record<string, unknown>[]);\n ws.send(JSON.stringify({ type: \"EXECUTE_SQL_SUCCESS\",\npayload: { result },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: { message: \"SQL execution not supported for this driver\",\ncode: \"NOT_SUPPORTED\" } } }));\n }\n break;\n }\n case \"FETCH_UNMAPPED_TABLES\": {\n if (admin && isSchemaAdmin(admin)) {\n const tables = await admin.fetchUnmappedTables?.(payload?.mappedPaths) || [];\n ws.send(JSON.stringify({ type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\npayload: { tables },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\npayload: { tables: [] },\nrequestId }));\n }\n break;\n }\n case \"FETCH_TABLE_METADATA\": {\n const { tableName } = payload;\n if (admin && isSchemaAdmin(admin)) {\n const metadata = await admin.fetchTableMetadata?.(tableName);\n ws.send(JSON.stringify({ type: \"FETCH_TABLE_METADATA_SUCCESS\",\npayload: { metadata },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"FETCH_TABLE_METADATA_SUCCESS\",\npayload: { metadata: null },\nrequestId }));\n }\n break;\n }\n case \"subscribe_collection\":\n case \"subscribe_one\":\n case \"unsubscribe\": {\n // A subscription is a read that re-runs on every matching\n // write, so an unregistered path leaks exactly as\n // FETCH_COLLECTION does — and re-leaks. Refused here, with\n // the subscription id the client keys its errors on, since\n // `handleClientMessage`'s frames carry no `requestId`.\n if (type !== \"unsubscribe\" && !driver.registry?.getCollectionByPath(payload?.path)) {\n ws.send(JSON.stringify({\n type: \"ERROR\",\n requestId,\n subscriptionId: payload?.subscriptionId,\n payload: { error: {\n message: `Unknown collection at path \"${payload?.path ?? \"\"}\": it is not a registered data collection.`,\n code: \"NOT_FOUND\"\n } }\n }));\n return;\n }\n // The same principal the request frames use, guest flag\n // included: every re-fetch reads as it.\n const subscriber = sessionUser(clientSessions.get(clientId));\n await realtimeService.handleClientMessage(clientId, {\n type,\n payload,\n subscriptionId: payload?.subscriptionId\n }, {\n uid: subscriber.uid,\n roles: subscriber.roles ?? [],\n isAnonymous: subscriber.isAnonymous\n });\n break;\n }\n default:\n logger.error(\"❌ [WebSocket Server] Unknown message type\", { detail: type });\n }\n } catch (error: unknown) {\n // A refused write keeps its message: it is the only thing that\n // tells the caller what to send instead, and the generic branch\n // below drops it in production. \"Refused\" is whatever REST\n // answers with the error's own status — `ApiError`, and the\n // `RebaseApiError` every collection-callback veto is — by the\n // same predicate, so the two cannot list different classes.\n const answer = declaredErrorAnswer(error);\n if (answer) {\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: {\n message: answer.message,\n code: answer.code,\n ...(answer.details !== undefined && { details: answer.details })\n} } }));\n return;\n }\n const errorMessage = process.env.NODE_ENV === \"production\" ? \"An unexpected error occurred\" : (error instanceof Error ? error.message : \"An unexpected error occurred\");\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: { message: errorMessage,\ncode: \"INTERNAL_ERROR\" } } }));\n }\n });\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/server-mongo",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "MongoDB backend for Rebase",
5
5
  "keywords": [
6
6
  "rebase",
@@ -44,9 +44,9 @@
44
44
  "dependencies": {
45
45
  "mongodb": "^7.5.0",
46
46
  "ws": "^8.21.1",
47
- "@rebasepro/common": "0.22.0",
48
- "@rebasepro/utils": "0.22.0",
49
- "@rebasepro/types": "0.22.0"
47
+ "@rebasepro/common": "0.23.0",
48
+ "@rebasepro/utils": "0.23.0",
49
+ "@rebasepro/types": "0.23.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/jest": "^30.0.0",
@@ -59,7 +59,7 @@
59
59
  "vite": "^8.1.5"
60
60
  },
61
61
  "peerDependencies": {
62
- "@rebasepro/server": "0.22.0"
62
+ "@rebasepro/server": "0.23.0"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "@rebasepro/server": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"websocket-C3iT6srl.js","names":[],"sources":["../src/websocket.ts"],"sourcesContent":["import { ANONYMOUS_USER_ID, RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User, AuthAdapter } from \"@rebasepro/types\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { Server } from \"http\";\nimport { inspect } from \"util\";\nimport { extractUserFromToken, resolveRequireAuth, assertWriteRequestValid, ApiError, declaredErrorAnswer } from \"@rebasepro/server\";\nimport type { RebaseAuthConfig } from \"@rebasepro/server\";\nimport { MongoRealtimeService } from \"./services/MongoRealtimeService\";\nimport { MongoDriver } from \"./services/MongoDriver\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Normalized user identity for WebSocket sessions — the same shape the Postgres\n * socket keeps, because an `AuthAdapter` user is not an access-token payload.\n */\ninterface WsUserIdentity {\n uid: string;\n email?: string;\n displayName?: string;\n photoURL?: string;\n roles: string[];\n isAdmin: boolean;\n /**\n * Whether this session is a guest — anonymous sign-in rather than an\n * account. Required, so every way of signing in has to say: a guest has a\n * real uid, and `policy.registered()` has nothing else to tell it from an\n * account by. Neither sign-in path read it, and every frame was then\n * scoped as an account.\n */\n isAnonymous: boolean;\n}\n\ninterface ClientSession {\n ws: WebSocket;\n user?: WsUserIdentity;\n authenticated: boolean;\n messageCount: number;\n messageWindowStart: number;\n}\n\nconst WS_RATE_LIMIT = 2000;\nconst WS_RATE_WINDOW_MS = 60_000;\n\nconst ADMIN_ONLY_TYPES = new Set([\n \"EXECUTE_SQL\",\n \"FETCH_DATABASES\",\n \"FETCH_ROLES\",\n \"FETCH_UNMAPPED_TABLES\",\n \"FETCH_TABLE_METADATA\",\n \"FETCH_CURRENT_DATABASE\",\n \"CREATE_BRANCH\",\n \"DELETE_BRANCH\",\n \"LIST_BRANCHES\"\n]);\n\n/**\n * Who a socket reads and writes as, for its request frames and its\n * subscriptions alike — one answer, so the two cannot disagree.\n *\n * A socket with no session (only possible with `requireAuth: false`) is the\n * anonymous user, exactly as REST scopes the same caller. It used to get the\n * base driver instead, which on this engine applies no security rules at all:\n * such a socket read every row and wrote wherever it liked.\n */\nfunction sessionUser(session: ClientSession | undefined): User {\n if (!session?.user) {\n return {\n uid: ANONYMOUS_USER_ID,\n displayName: null,\n email: null,\n photoURL: null,\n providerId: \"websocket\",\n isAnonymous: false,\n roles: [\"anon\"]\n };\n }\n return {\n uid: session.user.uid,\n email: session.user.email ?? \"\",\n displayName: session.user.displayName ?? \"\",\n photoURL: session.user.photoURL ?? \"\",\n providerId: \"jwt\",\n isAnonymous: session.user.isAnonymous,\n roles: session.user.roles ?? []\n };\n}\n\nfunction isAdminSession(session: ClientSession | undefined): boolean {\n if (!session?.user) return false;\n // The adapter's own answer first; a role *named* `admin` is only the\n // fallback for the built-in JWT path.\n if (session.user.isAdmin) return true;\n return (session.user.roles ?? []).some((r) => r === \"admin\");\n}\n\nexport function createMongoWebSocket(\n server: Server,\n realtimeService: MongoRealtimeService,\n driver: MongoDriver,\n authConfig?: RebaseAuthConfig,\n admin?: DatabaseAdmin,\n authAdapter?: AuthAdapter\n) {\n // Scoped to this factory invocation rather than the module, so sessions do\n // not leak across hot reloads or a second server on the same process — the\n // Postgres socket keeps it here for the same reason.\n const clientSessions = new Map<string, ClientSession>();\n\n const isProduction = process.env.NODE_ENV === \"production\";\n const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };\n const wss = new WebSocketServer({ server });\n\n wss.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") return;\n logger.error(\"❌ [WebSocket Server] Error\", { error: err });\n });\n\n // The same predicate the HTTP data routes use, from the same function. See\n // `resolveRequireAuth` for what this socket's local copy got wrong — most\n // importantly that a `false` here does not skip a check, it marks every\n // session `authenticated` at connect time.\n const requireAuth = !!authAdapter || resolveRequireAuth(authConfig);\n\n if (requireAuth && !authAdapter && !authConfig?.jwtSecret) {\n logger.warn(\n \"🔐 [WebSocket Server] Authentication is required but no adapter or jwtSecret is \" +\n \"configured — no client can complete AUTH, so every realtime message will be refused.\"\n );\n }\n\n wss.on(\"connection\", (ws) => {\n const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n wsDebug(`WebSocket client connected: ${clientId}`);\n\n clientSessions.set(clientId, { ws,\nauthenticated: !requireAuth,\nmessageCount: 0,\nmessageWindowStart: Date.now() });\n realtimeService.addClient(clientId, ws);\n\n ws.on(\"close\", () => {\n wsDebug(`WebSocket client disconnected: ${clientId}`);\n clientSessions.delete(clientId);\n });\n\n ws.on(\"message\", async (message) => {\n let requestId: string | undefined;\n try {\n const { type, payload, requestId: reqId } = JSON.parse(message.toString());\n requestId = reqId;\n\n wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : \"\");\n\n const sendError = (errType: \"ERROR\" | \"AUTH_ERROR\", code: string, msg: string) => {\n ws.send(JSON.stringify({ type: errType,\nrequestId,\npayload: { error: { message: msg,\ncode } } }));\n };\n\n if (type === \"AUTHENTICATE\") {\n const { token } = payload || {};\n if (!token) {\n sendError(\"AUTH_ERROR\", \"INVALID_INPUT\", \"Token is required\");\n return;\n }\n\n // The adapter verifies when one is configured, exactly as the\n // HTTP routes do; the built-in JWT path is the fallback.\n let verifiedUser: WsUserIdentity | null = null;\n\n if (authAdapter) {\n try {\n const adapterUser = authAdapter.verifyToken\n ? await authAdapter.verifyToken(token)\n : await authAdapter.verifyRequest(new Request(\"http://localhost/_ws_auth\", {\n headers: { Authorization: `Bearer ${token}` }\n }));\n if (adapterUser) {\n verifiedUser = {\n uid: adapterUser.uid,\n email: adapterUser.email,\n roles: adapterUser.roles ?? [],\n isAdmin: !!adapterUser.isAdmin,\n // Absent from an adapter with no such\n // concept, and absent reads as \"not a guest\".\n isAnonymous: adapterUser.isAnonymous === true\n };\n }\n } catch {\n // Adapter threw — treat as invalid token\n }\n } else {\n const jwtPayload = await extractUserFromToken(token);\n if (jwtPayload) {\n verifiedUser = {\n uid: jwtPayload.uid,\n email: jwtPayload.email,\n displayName: jwtPayload.displayName,\n photoURL: jwtPayload.photoURL,\n roles: jwtPayload.roles ?? [],\n isAdmin: (jwtPayload.roles ?? []).some((r: string) => r === \"admin\"),\n isAnonymous: jwtPayload.isAnonymous === true\n };\n }\n }\n\n if (verifiedUser) {\n const session = clientSessions.get(clientId);\n if (session) {\n session.user = verifiedUser;\n session.authenticated = true;\n }\n ws.send(JSON.stringify({ type: \"AUTH_SUCCESS\",\nrequestId,\npayload: { uid: verifiedUser.uid,\nroles: verifiedUser.roles } }));\n } else {\n sendError(\"AUTH_ERROR\", \"INVALID_TOKEN\", \"Invalid or expired token\");\n }\n return;\n }\n\n if (requireAuth) {\n const session = clientSessions.get(clientId);\n if (!session?.authenticated) {\n sendError(\"ERROR\", \"UNAUTHORIZED\", \"Authentication required\");\n return;\n }\n }\n\n {\n const session = clientSessions.get(clientId);\n if (session) {\n const now = Date.now();\n if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {\n session.messageCount = 0;\n session.messageWindowStart = now;\n }\n session.messageCount++;\n if (session.messageCount > WS_RATE_LIMIT) {\n sendError(\"ERROR\", \"RATE_LIMITED\", \"Too many requests. Please slow down.\");\n return;\n }\n }\n }\n\n if (ADMIN_ONLY_TYPES.has(type)) {\n const session = clientSessions.get(clientId);\n if (!isAdminSession(session)) {\n sendError(\"ERROR\", \"FORBIDDEN\", \"Admin access required for this operation\");\n return;\n }\n }\n\n /**\n * Refuse a frame whose path names no registered data collection.\n *\n * MongoDB has no row-level security, so on this engine the\n * registry *is* the access model: `securityRules` are enforced\n * only for a collection the registry resolves, and\n * `MongoDataService.getCollection` maps any path to a physical\n * collection by replacing `/` with `_`. A path the registry does\n * not know therefore reaches the database with no rule to apply —\n * `AuthenticatedMongoDriver.authorize(undefined)` answers\n * \"allowed\" and the RLS filter for an undefined collection is\n * \"match all\" — which put the auth store (`rebase_users` and its\n * password hashes, `rebase_user_roles`, `rebase_refresh_tokens`)\n * one frame away from any caller: authenticated, or anonymous\n * when `requireAuth` is false.\n *\n * The socket is the one client-facing door onto this driver. REST\n * mounts routes per registered slug and 404s everything else, and\n * in-process writes are trusted server code — so the registry\n * check lives here, at the boundary this door owns, exactly as the\n * write validation beside it does. A `notFound` matches what REST\n * answers for an unknown collection, and does not distinguish a\n * collection that exists in Mongo from one that does not.\n */\n const assertRegisteredPath = (path: string | undefined): void => {\n if (!path || !driver.registry?.getCollectionByPath(path)) {\n throw ApiError.notFound(\n `Unknown collection at path \"${path ?? \"\"}\": it is not a registered data collection.`\n );\n }\n };\n\n /** @see the Postgres socket — same rule, same reason. */\n const assertWriteRequest = (path: string | undefined, values: unknown): void => {\n if (!path || !values || typeof values !== \"object\") return;\n const collection = driver.registry?.getCollectionByPath(path);\n if (!collection) return;\n assertWriteRequestValid(values as Record<string, unknown>, collection);\n };\n\n // Always scoped. A failure to scope propagates to the frame's\n // `catch` rather than falling back to the base driver, which\n // is what it did: the fallback is the unscoped read.\n const getScopedDelegate = (): Promise<DataDriver> =>\n driver.withAuth(sessionUser(clientSessions.get(clientId)));\n\n switch (type) {\n case \"FETCH_COLLECTION\": {\n const request: FetchCollectionProps = payload;\n assertRegisteredPath(request.path);\n const delegate = await getScopedDelegate();\n const rows = await delegate.fetchCollection(request);\n ws.send(JSON.stringify({ type: \"FETCH_COLLECTION_SUCCESS\",\npayload: { rows },\nrequestId }));\n break;\n }\n case \"FETCH_ONE\": {\n const request: FetchOneProps = payload;\n assertRegisteredPath(request.path);\n const delegate = await getScopedDelegate();\n const row = await delegate.fetchOne(request);\n ws.send(JSON.stringify({ type: \"FETCH_ONE_SUCCESS\",\npayload: { row },\nrequestId }));\n break;\n }\n case \"SAVE\": {\n const request: SaveProps = payload;\n // The REST layer's write checks, at this boundary too —\n // the socket is a second way in, and it used to be the\n // unchecked one. Collection from the registry by path,\n // never from the client's `request.collection`.\n assertRegisteredPath(request.path);\n assertWriteRequest(request.path, request.values as Record<string, unknown>);\n const delegate = await getScopedDelegate();\n const row = await delegate.save(request);\n ws.send(JSON.stringify({ type: \"SAVE_SUCCESS\",\npayload: { row },\nrequestId }));\n break;\n }\n case \"DELETE\": {\n const request: DeleteProps = payload;\n assertRegisteredPath(request.row?.path);\n const delegate = await getScopedDelegate();\n // The address, and nothing else the frame says: the\n // driver reads the row and resolves the collection by\n // path — see the Postgres socket's DELETE.\n await delegate.delete({\n row: { id: request.row.id, path: request.row.path },\n hard: request.hard\n });\n ws.send(JSON.stringify({ type: \"DELETE_SUCCESS\",\npayload: { success: true },\nrequestId }));\n break;\n }\n case \"CHECK_UNIQUE_FIELD\": {\n const { path, name, value, id, collection } = payload;\n assertRegisteredPath(path);\n const delegate = await getScopedDelegate();\n const isUnique = await delegate.checkUniqueField(path, name, value, id, collection);\n ws.send(JSON.stringify({ type: \"CHECK_UNIQUE_FIELD_SUCCESS\",\npayload: { isUnique },\nrequestId }));\n break;\n }\n case \"COUNT\": {\n const request: FetchCollectionProps = payload;\n assertRegisteredPath(request.path);\n const delegate = await getScopedDelegate();\n const count = await delegate.count!(request);\n ws.send(JSON.stringify({ type: \"COUNT_SUCCESS\",\npayload: { count },\nrequestId }));\n break;\n }\n case \"EXECUTE_SQL\": {\n const { sql, options } = payload;\n if (admin && isDocumentAdmin(admin) && admin.executeAggregate) {\n const result = await admin.executeAggregate(sql as Record<string, unknown>[]);\n ws.send(JSON.stringify({ type: \"EXECUTE_SQL_SUCCESS\",\npayload: { result },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: { message: \"SQL execution not supported for this driver\",\ncode: \"NOT_SUPPORTED\" } } }));\n }\n break;\n }\n case \"FETCH_UNMAPPED_TABLES\": {\n if (admin && isSchemaAdmin(admin)) {\n const tables = await admin.fetchUnmappedTables?.(payload?.mappedPaths) || [];\n ws.send(JSON.stringify({ type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\npayload: { tables },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\npayload: { tables: [] },\nrequestId }));\n }\n break;\n }\n case \"FETCH_TABLE_METADATA\": {\n const { tableName } = payload;\n if (admin && isSchemaAdmin(admin)) {\n const metadata = await admin.fetchTableMetadata?.(tableName);\n ws.send(JSON.stringify({ type: \"FETCH_TABLE_METADATA_SUCCESS\",\npayload: { metadata },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"FETCH_TABLE_METADATA_SUCCESS\",\npayload: { metadata: null },\nrequestId }));\n }\n break;\n }\n case \"subscribe_collection\":\n case \"subscribe_one\":\n case \"unsubscribe\": {\n // A subscription is a read that re-runs on every matching\n // write, so an unregistered path leaks exactly as\n // FETCH_COLLECTION does — and re-leaks. Refused here, with\n // the subscription id the client keys its errors on, since\n // `handleClientMessage`'s frames carry no `requestId`.\n if (type !== \"unsubscribe\" && !driver.registry?.getCollectionByPath(payload?.path)) {\n ws.send(JSON.stringify({\n type: \"ERROR\",\n requestId,\n subscriptionId: payload?.subscriptionId,\n payload: { error: {\n message: `Unknown collection at path \"${payload?.path ?? \"\"}\": it is not a registered data collection.`,\n code: \"NOT_FOUND\"\n } }\n }));\n return;\n }\n // The same principal the request frames use, guest flag\n // included: every re-fetch reads as it.\n const subscriber = sessionUser(clientSessions.get(clientId));\n await realtimeService.handleClientMessage(clientId, {\n type,\n payload,\n subscriptionId: payload?.subscriptionId\n }, {\n uid: subscriber.uid,\n roles: subscriber.roles ?? [],\n isAnonymous: subscriber.isAnonymous\n });\n break;\n }\n default:\n logger.error(\"❌ [WebSocket Server] Unknown message type\", { detail: type });\n }\n } catch (error: unknown) {\n // A refused write keeps its message: it is the only thing that\n // tells the caller what to send instead, and the generic branch\n // below drops it in production. \"Refused\" is whatever REST\n // answers with the error's own status — `ApiError`, and the\n // `RebaseApiError` every collection-callback veto is — by the\n // same predicate, so the two cannot list different classes.\n const answer = declaredErrorAnswer(error);\n if (answer) {\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: {\n message: answer.message,\n code: answer.code,\n ...(answer.details !== undefined && { details: answer.details })\n} } }));\n return;\n }\n const errorMessage = process.env.NODE_ENV === \"production\" ? \"An unexpected error occurred\" : (error instanceof Error ? error.message : \"An unexpected error occurred\");\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: { message: errorMessage,\ncode: \"INTERNAL_ERROR\" } } }));\n }\n });\n });\n}\n"],"mappings":";;;;AAuCA,IAAM,gBAAgB;AACtB,IAAM,oBAAoB;AAE1B,IAAM,mCAAmB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC;;;;;;;;;;AAWD,SAAS,YAAY,SAA0C;CAC3D,IAAI,CAAC,SAAS,MACV,OAAO;EACH,KAAK;EACL,aAAa;EACb,OAAO;EACP,UAAU;EACV,YAAY;EACZ,aAAa;EACb,OAAO,CAAC,MAAM;CAClB;CAEJ,OAAO;EACH,KAAK,QAAQ,KAAK;EAClB,OAAO,QAAQ,KAAK,SAAS;EAC7B,aAAa,QAAQ,KAAK,eAAe;EACzC,UAAU,QAAQ,KAAK,YAAY;EACnC,YAAY;EACZ,aAAa,QAAQ,KAAK;EAC1B,OAAO,QAAQ,KAAK,SAAS,CAAC;CAClC;AACJ;AAEA,SAAS,eAAe,SAA6C;CACjE,IAAI,CAAC,SAAS,MAAM,OAAO;CAG3B,IAAI,QAAQ,KAAK,SAAS,OAAO;CACjC,QAAQ,QAAQ,KAAK,SAAS,CAAC,EAAA,CAAG,MAAM,MAAM,MAAM,OAAO;AAC/D;AAEA,SAAgB,qBACZ,QACA,iBACA,QACA,YACA,OACA,aACF;CAIE,MAAM,iCAAiB,IAAI,IAA2B;CAEtD,MAAM,eAAA,QAAA,IAAA,aAAwC;CAC9C,MAAM,WAAW,GAAG,SAAoB;EAAE,IAAI,CAAC,cAAc,QAAQ,MAAM,GAAG,IAAI;CAAG;CACrF,MAAM,MAAM,IAAI,gBAAgB,EAAE,OAAO,CAAC;CAE1C,IAAI,GAAG,UAAU,QAA+B;EAC5C,IAAI,IAAI,SAAS,cAAc;EAC/B,OAAO,MAAM,8BAA8B,EAAE,OAAO,IAAI,CAAC;CAC7D,CAAC;CAMD,MAAM,cAAc,CAAC,CAAC,eAAe,mBAAmB,UAAU;CAElE,IAAI,eAAe,CAAC,eAAe,CAAC,YAAY,WAC5C,OAAO,KACH,sKAEJ;CAGJ,IAAI,GAAG,eAAe,OAAO;EACzB,MAAM,WAAW,UAAU,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,UAAU,GAAG,CAAC;EAClF,QAAQ,+BAA+B,UAAU;EAEjD,eAAe,IAAI,UAAU;GAAE;GACvC,eAAe,CAAC;GAChB,cAAc;GACd,oBAAoB,KAAK,IAAI;EAAE,CAAC;EACxB,gBAAgB,UAAU,UAAU,EAAE;EAEtC,GAAG,GAAG,eAAe;GACjB,QAAQ,kCAAkC,UAAU;GACpD,eAAe,OAAO,QAAQ;EAClC,CAAC;EAED,GAAG,GAAG,WAAW,OAAO,YAAY;GAChC,IAAI;GACJ,IAAI;IACA,MAAM,EAAE,MAAM,SAAS,WAAW,UAAU,KAAK,MAAM,QAAQ,SAAS,CAAC;IACzE,YAAY;IAEZ,QAAQ,QAAQ,SAAS,KAAK,QAAQ,YAAY,IAAI,UAAU,KAAK,EAAE;IAEvE,MAAM,aAAa,SAAiC,MAAc,QAAgB;KAC9E,GAAG,KAAK,KAAK,UAAU;MAAE,MAAM;MACnD;MACA,SAAS,EAAE,OAAO;OAAE,SAAS;OAC7B;MAAK,EAAE;KAAE,CAAC,CAAC;IACK;IAEA,IAAI,SAAS,gBAAgB;KACzB,MAAM,EAAE,UAAU,WAAW,CAAC;KAC9B,IAAI,CAAC,OAAO;MACR,UAAU,cAAc,iBAAiB,mBAAmB;MAC5D;KACJ;KAIA,IAAI,eAAsC;KAE1C,IAAI,aACA,IAAI;MACA,MAAM,cAAc,YAAY,cAC1B,MAAM,YAAY,YAAY,KAAK,IACnC,MAAM,YAAY,cAAc,IAAI,QAAQ,6BAA6B,EACvE,SAAS,EAAE,eAAe,UAAU,QAAQ,EAChD,CAAC,CAAC;MACN,IAAI,aACA,eAAe;OACX,KAAK,YAAY;OACjB,OAAO,YAAY;OACnB,OAAO,YAAY,SAAS,CAAC;OAC7B,SAAS,CAAC,CAAC,YAAY;OAGvB,aAAa,YAAY,gBAAgB;MAC7C;KAER,QAAQ,CAER;UACG;MACH,MAAM,aAAa,MAAM,qBAAqB,KAAK;MACnD,IAAI,YACA,eAAe;OACX,KAAK,WAAW;OAChB,OAAO,WAAW;OAClB,aAAa,WAAW;OACxB,UAAU,WAAW;OACrB,OAAO,WAAW,SAAS,CAAC;OAC5B,UAAU,WAAW,SAAS,CAAC,EAAA,CAAG,MAAM,MAAc,MAAM,OAAO;OACnE,aAAa,WAAW,gBAAgB;MAC5C;KAER;KAEA,IAAI,cAAc;MACd,MAAM,UAAU,eAAe,IAAI,QAAQ;MAC3C,IAAI,SAAS;OACT,QAAQ,OAAO;OACf,QAAQ,gBAAgB;MAC5B;MACA,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OACvD;OACA,SAAS;QAAE,KAAK,aAAa;QAC7B,OAAO,aAAa;OAAM;MAAE,CAAC,CAAC;KACV,OACI,UAAU,cAAc,iBAAiB,0BAA0B;KAEvE;IACJ;IAEA,IAAI;SAEI,CADY,eAAe,IAAI,QAC9B,CAAA,EAAS,eAAe;MACzB,UAAU,SAAS,gBAAgB,yBAAyB;MAC5D;KACJ;;IAGJ;KACI,MAAM,UAAU,eAAe,IAAI,QAAQ;KAC3C,IAAI,SAAS;MACT,MAAM,MAAM,KAAK,IAAI;MACrB,IAAI,MAAM,QAAQ,qBAAqB,mBAAmB;OACtD,QAAQ,eAAe;OACvB,QAAQ,qBAAqB;MACjC;MACA,QAAQ;MACR,IAAI,QAAQ,eAAe,eAAe;OACtC,UAAU,SAAS,gBAAgB,sCAAsC;OACzE;MACJ;KACJ;IACJ;IAEA,IAAI,iBAAiB,IAAI,IAAI;SAErB,CAAC,eADW,eAAe,IAAI,QACf,CAAO,GAAG;MAC1B,UAAU,SAAS,aAAa,0CAA0C;MAC1E;KACJ;;;;;;;;;;;;;;;;;;;;;;;;;;IA2BJ,MAAM,wBAAwB,SAAmC;KAC7D,IAAI,CAAC,QAAQ,CAAC,OAAO,UAAU,oBAAoB,IAAI,GACnD,MAAM,SAAS,SACX,+BAA+B,QAAQ,GAAG,2CAC9C;IAER;;IAGA,MAAM,sBAAsB,MAA0B,WAA0B;KAC5E,IAAI,CAAC,QAAQ,CAAC,UAAU,OAAO,WAAW,UAAU;KACpD,MAAM,aAAa,OAAO,UAAU,oBAAoB,IAAI;KAC5D,IAAI,CAAC,YAAY;KACjB,wBAAwB,QAAmC,UAAU;IACzE;IAKA,MAAM,0BACF,OAAO,SAAS,YAAY,eAAe,IAAI,QAAQ,CAAC,CAAC;IAE7D,QAAQ,MAAR;KACI,KAAK,oBAAoB;MACrB,MAAM,UAAgC;MACtC,qBAAqB,QAAQ,IAAI;MAEjC,MAAM,OAAO,OAAM,MADI,kBAAkB,EAAA,CACb,gBAAgB,OAAO;MACnD,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OACvD,SAAS,EAAE,KAAK;OAChB;MAAU,CAAC,CAAC;MACY;KACJ;KACA,KAAK,aAAa;MACd,MAAM,UAAyB;MAC/B,qBAAqB,QAAQ,IAAI;MAEjC,MAAM,MAAM,OAAM,MADK,kBAAkB,EAAA,CACd,SAAS,OAAO;MAC3C,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OACvD,SAAS,EAAE,IAAI;OACf;MAAU,CAAC,CAAC;MACY;KACJ;KACA,KAAK,QAAQ;MACT,MAAM,UAAqB;MAK3B,qBAAqB,QAAQ,IAAI;MACjC,mBAAmB,QAAQ,MAAM,QAAQ,MAAiC;MAE1E,MAAM,MAAM,OAAM,MADK,kBAAkB,EAAA,CACd,KAAK,OAAO;MACvC,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OACvD,SAAS,EAAE,IAAI;OACf;MAAU,CAAC,CAAC;MACY;KACJ;KACA,KAAK,UAAU;MACX,MAAM,UAAuB;MAC7B,qBAAqB,QAAQ,KAAK,IAAI;MAKtC,OAAM,MAJiB,kBAAkB,EAAA,CAI1B,OAAO;OAClB,KAAK;QAAE,IAAI,QAAQ,IAAI;QAAI,MAAM,QAAQ,IAAI;OAAK;OAClD,MAAM,QAAQ;MAClB,CAAC;MACD,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OACvD,SAAS,EAAE,SAAS,KAAK;OACzB;MAAU,CAAC,CAAC;MACY;KACJ;KACA,KAAK,sBAAsB;MACvB,MAAM,EAAE,MAAM,MAAM,OAAO,IAAI,eAAe;MAC9C,qBAAqB,IAAI;MAEzB,MAAM,WAAW,OAAM,MADA,kBAAkB,EAAA,CACT,iBAAiB,MAAM,MAAM,OAAO,IAAI,UAAU;MAClF,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OACvD,SAAS,EAAE,SAAS;OACpB;MAAU,CAAC,CAAC;MACY;KACJ;KACA,KAAK,SAAS;MACV,MAAM,UAAgC;MACtC,qBAAqB,QAAQ,IAAI;MAEjC,MAAM,QAAQ,OAAM,MADG,kBAAkB,EAAA,CACZ,MAAO,OAAO;MAC3C,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OACvD,SAAS,EAAE,MAAM;OACjB;MAAU,CAAC,CAAC;MACY;KACJ;KACA,KAAK,eAAe;MAChB,MAAM,EAAE,KAAK,YAAY;MACzB,IAAI,SAAS,gBAAgB,KAAK,KAAK,MAAM,kBAAkB;OAC3D,MAAM,SAAS,MAAM,MAAM,iBAAiB,GAAgC;OAC5E,GAAG,KAAK,KAAK,UAAU;QAAE,MAAM;QAC3D,SAAS,EAAE,OAAO;QAClB;OAAU,CAAC,CAAC;MACY,OACI,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OAC3D;OACA,SAAS,EAAE,OAAO;QAAE,SAAS;QAC7B,MAAM;OAAgB,EAAE;MAAE,CAAC,CAAC;MAEJ;KACJ;KACA,KAAK;MACD,IAAI,SAAS,cAAc,KAAK,GAAG;OAC/B,MAAM,SAAS,MAAM,MAAM,sBAAsB,SAAS,WAAW,KAAK,CAAC;OAC3E,GAAG,KAAK,KAAK,UAAU;QAAE,MAAM;QAC3D,SAAS,EAAE,OAAO;QAClB;OAAU,CAAC,CAAC;MACY,OACI,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OAC3D,SAAS,EAAE,QAAQ,CAAC,EAAE;OACtB;MAAU,CAAC,CAAC;MAEY;KAEJ,KAAK,wBAAwB;MACzB,MAAM,EAAE,cAAc;MACtB,IAAI,SAAS,cAAc,KAAK,GAAG;OAC/B,MAAM,WAAW,MAAM,MAAM,qBAAqB,SAAS;OAC3D,GAAG,KAAK,KAAK,UAAU;QAAE,MAAM;QAC3D,SAAS,EAAE,SAAS;QACpB;OAAU,CAAC,CAAC;MACY,OACI,GAAG,KAAK,KAAK,UAAU;OAAE,MAAM;OAC3D,SAAS,EAAE,UAAU,KAAK;OAC1B;MAAU,CAAC,CAAC;MAEY;KACJ;KACA,KAAK;KACL,KAAK;KACL,KAAK,eAAe;MAMhB,IAAI,SAAS,iBAAiB,CAAC,OAAO,UAAU,oBAAoB,SAAS,IAAI,GAAG;OAChF,GAAG,KAAK,KAAK,UAAU;QACnB,MAAM;QACN;QACA,gBAAgB,SAAS;QACzB,SAAS,EAAE,OAAO;SACd,SAAS,+BAA+B,SAAS,QAAQ,GAAG;SAC5D,MAAM;QACV,EAAE;OACN,CAAC,CAAC;OACF;MACJ;MAGA,MAAM,aAAa,YAAY,eAAe,IAAI,QAAQ,CAAC;MAC3D,MAAM,gBAAgB,oBAAoB,UAAU;OAChD;OACA;OACA,gBAAgB,SAAS;MAC7B,GAAG;OACC,KAAK,WAAW;OAChB,OAAO,WAAW,SAAS,CAAC;OAC5B,aAAa,WAAW;MAC5B,CAAC;MACD;KACJ;KACA,SACI,OAAO,MAAM,6CAA6C,EAAE,QAAQ,KAAK,CAAC;IAClF;GACJ,SAAS,OAAgB;IAOrB,MAAM,SAAS,oBAAoB,KAAK;IACxC,IAAI,QAAQ;KACR,GAAG,KAAK,KAAK,UAAU;MAAE,MAAM;MACnD;MACA,SAAS,EAAE,OAAO;OACd,SAAS,OAAO;OAChB,MAAM,OAAO;OACb,GAAI,OAAO,YAAY,KAAA,KAAa,EAAE,SAAS,OAAO,QAAQ;MAClE,EAAE;KAAE,CAAC,CAAC;KACc;IACJ;IACA,MAAM,eAAA,QAAA,IAAA,aAAwC,eAAe,iCAAkC,iBAAiB,QAAQ,MAAM,UAAU;IACxI,GAAG,KAAK,KAAK,UAAU;KAAE,MAAM;KAC/C;KACA,SAAS,EAAE,OAAO;MAAE,SAAS;MAC7B,MAAM;KAAiB,EAAE;IAAE,CAAC,CAAC;GACjB;EACJ,CAAC;CACL,CAAC;AACL"}