@rebasepro/server-mongo 0.17.3 → 0.18.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/LICENSE +0 -1
- package/README.md +4 -0
- package/dist/MongoBootstrapper.d.ts +0 -1
- package/dist/auth/ensure-collections.d.ts +0 -1
- package/dist/auth/services.d.ts +0 -1
- package/dist/connection.d.ts +0 -1
- package/dist/db/MongoConditionBuilder.d.ts +0 -1
- package/dist/db/MongoDataService.d.ts +0 -1
- package/dist/db/securityRuleFilter.d.ts +0 -1
- package/dist/factory.d.ts +0 -1
- package/dist/history/ensure-history-collection.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +83 -79
- package/dist/index.es.js.map +1 -1
- package/dist/schema/plan-schema-change.d.ts +0 -1
- package/dist/services/MongoDriver.d.ts +0 -1
- package/dist/services/MongoHistoryService.d.ts +0 -1
- package/dist/services/MongoRealtimeService.d.ts +0 -1
- package/dist/websocket.d.ts +0 -1
- package/package.json +28 -24
- package/dist/MongoBootstrapper.d.ts.map +0 -1
- package/dist/auth/ensure-collections.d.ts.map +0 -1
- package/dist/auth/services.d.ts.map +0 -1
- package/dist/connection.d.ts.map +0 -1
- package/dist/db/MongoConditionBuilder.d.ts.map +0 -1
- package/dist/db/MongoDataService.d.ts.map +0 -1
- package/dist/db/securityRuleFilter.d.ts.map +0 -1
- package/dist/factory.d.ts.map +0 -1
- package/dist/history/ensure-history-collection.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/schema/plan-schema-change.d.ts.map +0 -1
- package/dist/services/MongoDriver.d.ts.map +0 -1
- package/dist/services/MongoHistoryService.d.ts.map +0 -1
- package/dist/services/MongoRealtimeService.d.ts.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/src/MongoBootstrapper.ts +0 -204
- package/src/auth/ensure-collections.ts +0 -153
- package/src/auth/services.ts +0 -866
- package/src/connection.ts +0 -60
- package/src/db/MongoConditionBuilder.ts +0 -348
- package/src/db/MongoDataService.ts +0 -412
- package/src/db/securityRuleFilter.ts +0 -398
- package/src/factory.ts +0 -331
- package/src/history/ensure-history-collection.ts +0 -22
- package/src/index.ts +0 -25
- package/src/schema/plan-schema-change.ts +0 -159
- package/src/services/MongoDriver.ts +0 -950
- package/src/services/MongoHistoryService.ts +0 -186
- package/src/services/MongoRealtimeService.ts +0 -592
- package/src/websocket.ts +0 -387
package/src/websocket.ts
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
import { RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User, AuthAdapter } from "@rebasepro/types";
|
|
2
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
3
|
-
import { Server } from "http";
|
|
4
|
-
import { inspect } from "util";
|
|
5
|
-
import { extractUserFromToken, resolveRequireAuth, assertWriteRequestValid, ApiError } from "@rebasepro/server";
|
|
6
|
-
import type { RebaseAuthConfig } from "@rebasepro/server";
|
|
7
|
-
import { MongoRealtimeService } from "./services/MongoRealtimeService";
|
|
8
|
-
import { MongoDriver } from "./services/MongoDriver";
|
|
9
|
-
import { logger } from "@rebasepro/server";
|
|
10
|
-
|
|
11
|
-
interface DriverWithAuth extends DataDriver {
|
|
12
|
-
withAuth(user: Record<string, unknown>): Promise<DataDriver>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isDriverWithAuth(driver: DataDriver): driver is DriverWithAuth {
|
|
16
|
-
return "withAuth" in driver && typeof (driver as Record<string, unknown>).withAuth === "function";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Normalized user identity for WebSocket sessions — the same shape the Postgres
|
|
21
|
-
* socket keeps, because an `AuthAdapter` user is not an access-token payload.
|
|
22
|
-
*/
|
|
23
|
-
interface WsUserIdentity {
|
|
24
|
-
uid: string;
|
|
25
|
-
email?: string;
|
|
26
|
-
displayName?: string;
|
|
27
|
-
photoURL?: string;
|
|
28
|
-
roles: string[];
|
|
29
|
-
isAdmin: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface ClientSession {
|
|
33
|
-
ws: WebSocket;
|
|
34
|
-
user?: WsUserIdentity;
|
|
35
|
-
authenticated: boolean;
|
|
36
|
-
messageCount: number;
|
|
37
|
-
messageWindowStart: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const WS_RATE_LIMIT = 2000;
|
|
41
|
-
const WS_RATE_WINDOW_MS = 60_000;
|
|
42
|
-
|
|
43
|
-
const ADMIN_ONLY_TYPES = new Set([
|
|
44
|
-
"EXECUTE_SQL",
|
|
45
|
-
"FETCH_DATABASES",
|
|
46
|
-
"FETCH_ROLES",
|
|
47
|
-
"FETCH_UNMAPPED_TABLES",
|
|
48
|
-
"FETCH_TABLE_METADATA",
|
|
49
|
-
"FETCH_CURRENT_DATABASE",
|
|
50
|
-
"CREATE_BRANCH",
|
|
51
|
-
"DELETE_BRANCH",
|
|
52
|
-
"LIST_BRANCHES"
|
|
53
|
-
]);
|
|
54
|
-
|
|
55
|
-
function isAdminSession(session: ClientSession | undefined): boolean {
|
|
56
|
-
if (!session?.user) return false;
|
|
57
|
-
// The adapter's own answer first; a role *named* `admin` is only the
|
|
58
|
-
// fallback for the built-in JWT path.
|
|
59
|
-
if (session.user.isAdmin) return true;
|
|
60
|
-
return (session.user.roles ?? []).some((r) => r === "admin");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function createMongoWebSocket(
|
|
64
|
-
server: Server,
|
|
65
|
-
realtimeService: MongoRealtimeService,
|
|
66
|
-
driver: MongoDriver,
|
|
67
|
-
authConfig?: RebaseAuthConfig,
|
|
68
|
-
admin?: DatabaseAdmin,
|
|
69
|
-
authAdapter?: AuthAdapter
|
|
70
|
-
) {
|
|
71
|
-
// Scoped to this factory invocation rather than the module, so sessions do
|
|
72
|
-
// not leak across hot reloads or a second server on the same process — the
|
|
73
|
-
// Postgres socket keeps it here for the same reason.
|
|
74
|
-
const clientSessions = new Map<string, ClientSession>();
|
|
75
|
-
|
|
76
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
77
|
-
const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };
|
|
78
|
-
const wss = new WebSocketServer({ server });
|
|
79
|
-
|
|
80
|
-
wss.on("error", (err: NodeJS.ErrnoException) => {
|
|
81
|
-
if (err.code === "EADDRINUSE") return;
|
|
82
|
-
logger.error("❌ [WebSocket Server] Error", { error: err });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// The same predicate the HTTP data routes use, from the same function. See
|
|
86
|
-
// `resolveRequireAuth` for what this socket's local copy got wrong — most
|
|
87
|
-
// importantly that a `false` here does not skip a check, it marks every
|
|
88
|
-
// session `authenticated` at connect time.
|
|
89
|
-
const requireAuth = !!authAdapter || resolveRequireAuth(authConfig);
|
|
90
|
-
|
|
91
|
-
if (requireAuth && !authAdapter && !authConfig?.jwtSecret) {
|
|
92
|
-
logger.warn(
|
|
93
|
-
"🔐 [WebSocket Server] Authentication is required but no adapter or jwtSecret is " +
|
|
94
|
-
"configured — no client can complete AUTH, so every realtime message will be refused."
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
wss.on("connection", (ws) => {
|
|
99
|
-
const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
100
|
-
wsDebug(`WebSocket client connected: ${clientId}`);
|
|
101
|
-
|
|
102
|
-
clientSessions.set(clientId, { ws,
|
|
103
|
-
authenticated: !requireAuth,
|
|
104
|
-
messageCount: 0,
|
|
105
|
-
messageWindowStart: Date.now() });
|
|
106
|
-
realtimeService.addClient(clientId, ws);
|
|
107
|
-
|
|
108
|
-
ws.on("close", () => {
|
|
109
|
-
wsDebug(`WebSocket client disconnected: ${clientId}`);
|
|
110
|
-
clientSessions.delete(clientId);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
ws.on("message", async (message) => {
|
|
114
|
-
let requestId: string | undefined;
|
|
115
|
-
try {
|
|
116
|
-
const { type, payload, requestId: reqId } = JSON.parse(message.toString());
|
|
117
|
-
requestId = reqId;
|
|
118
|
-
|
|
119
|
-
wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : "");
|
|
120
|
-
|
|
121
|
-
const sendError = (errType: "ERROR" | "AUTH_ERROR", code: string, msg: string) => {
|
|
122
|
-
ws.send(JSON.stringify({ type: errType,
|
|
123
|
-
requestId,
|
|
124
|
-
payload: { error: { message: msg,
|
|
125
|
-
code } } }));
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
if (type === "AUTHENTICATE") {
|
|
129
|
-
const { token } = payload || {};
|
|
130
|
-
if (!token) {
|
|
131
|
-
sendError("AUTH_ERROR", "INVALID_INPUT", "Token is required");
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// The adapter verifies when one is configured, exactly as the
|
|
136
|
-
// HTTP routes do; the built-in JWT path is the fallback.
|
|
137
|
-
let verifiedUser: WsUserIdentity | null = null;
|
|
138
|
-
|
|
139
|
-
if (authAdapter) {
|
|
140
|
-
try {
|
|
141
|
-
const adapterUser = authAdapter.verifyToken
|
|
142
|
-
? await authAdapter.verifyToken(token)
|
|
143
|
-
: await authAdapter.verifyRequest(new Request("http://localhost/_ws_auth", {
|
|
144
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
145
|
-
}));
|
|
146
|
-
if (adapterUser) {
|
|
147
|
-
verifiedUser = {
|
|
148
|
-
uid: adapterUser.uid,
|
|
149
|
-
email: adapterUser.email,
|
|
150
|
-
roles: adapterUser.roles ?? [],
|
|
151
|
-
isAdmin: !!adapterUser.isAdmin
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
} catch {
|
|
155
|
-
// Adapter threw — treat as invalid token
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
const jwtPayload = await extractUserFromToken(token);
|
|
159
|
-
if (jwtPayload) {
|
|
160
|
-
verifiedUser = {
|
|
161
|
-
uid: jwtPayload.uid,
|
|
162
|
-
email: jwtPayload.email,
|
|
163
|
-
displayName: jwtPayload.displayName,
|
|
164
|
-
photoURL: jwtPayload.photoURL,
|
|
165
|
-
roles: jwtPayload.roles ?? [],
|
|
166
|
-
isAdmin: (jwtPayload.roles ?? []).some((r: string) => r === "admin")
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (verifiedUser) {
|
|
172
|
-
const session = clientSessions.get(clientId);
|
|
173
|
-
if (session) {
|
|
174
|
-
session.user = verifiedUser;
|
|
175
|
-
session.authenticated = true;
|
|
176
|
-
}
|
|
177
|
-
ws.send(JSON.stringify({ type: "AUTH_SUCCESS",
|
|
178
|
-
requestId,
|
|
179
|
-
payload: { uid: verifiedUser.uid,
|
|
180
|
-
roles: verifiedUser.roles } }));
|
|
181
|
-
} else {
|
|
182
|
-
sendError("AUTH_ERROR", "INVALID_TOKEN", "Invalid or expired token");
|
|
183
|
-
}
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (requireAuth) {
|
|
188
|
-
const session = clientSessions.get(clientId);
|
|
189
|
-
if (!session?.authenticated) {
|
|
190
|
-
sendError("ERROR", "UNAUTHORIZED", "Authentication required");
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
{
|
|
196
|
-
const session = clientSessions.get(clientId);
|
|
197
|
-
if (session) {
|
|
198
|
-
const now = Date.now();
|
|
199
|
-
if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {
|
|
200
|
-
session.messageCount = 0;
|
|
201
|
-
session.messageWindowStart = now;
|
|
202
|
-
}
|
|
203
|
-
session.messageCount++;
|
|
204
|
-
if (session.messageCount > WS_RATE_LIMIT) {
|
|
205
|
-
sendError("ERROR", "RATE_LIMITED", "Too many requests. Please slow down.");
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (ADMIN_ONLY_TYPES.has(type)) {
|
|
212
|
-
const session = clientSessions.get(clientId);
|
|
213
|
-
if (!isAdminSession(session)) {
|
|
214
|
-
sendError("ERROR", "FORBIDDEN", "Admin access required for this operation");
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** @see the Postgres socket — same rule, same reason. */
|
|
220
|
-
const assertWriteRequest = (path: string | undefined, values: unknown): void => {
|
|
221
|
-
if (!path || !values || typeof values !== "object") return;
|
|
222
|
-
const collection = driver.registry?.getCollectionByPath(path);
|
|
223
|
-
if (!collection) return;
|
|
224
|
-
assertWriteRequestValid(values as Record<string, unknown>, collection);
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const getScopedDelegate = async (): Promise<DataDriver> => {
|
|
228
|
-
const session = clientSessions.get(clientId);
|
|
229
|
-
if (session?.user && isDriverWithAuth(driver)) {
|
|
230
|
-
try {
|
|
231
|
-
const userForAuth: User = {
|
|
232
|
-
uid: session.user.uid,
|
|
233
|
-
email: session.user.email ?? "",
|
|
234
|
-
displayName: session.user.displayName ?? "",
|
|
235
|
-
photoURL: session.user.photoURL ?? "",
|
|
236
|
-
providerId: "jwt",
|
|
237
|
-
isAnonymous: false,
|
|
238
|
-
roles: session.user.roles ?? []
|
|
239
|
-
};
|
|
240
|
-
return await driver.withAuth(userForAuth);
|
|
241
|
-
} catch (e) {
|
|
242
|
-
logger.error("Failed to create authenticated delegate for WS request", { error: e });
|
|
243
|
-
return driver;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return driver;
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
switch (type) {
|
|
250
|
-
case "FETCH_COLLECTION": {
|
|
251
|
-
const request: FetchCollectionProps = payload;
|
|
252
|
-
const delegate = await getScopedDelegate();
|
|
253
|
-
const rows = await delegate.fetchCollection(request);
|
|
254
|
-
ws.send(JSON.stringify({ type: "FETCH_COLLECTION_SUCCESS",
|
|
255
|
-
payload: { rows },
|
|
256
|
-
requestId }));
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
case "FETCH_ONE": {
|
|
260
|
-
const request: FetchOneProps = payload;
|
|
261
|
-
const delegate = await getScopedDelegate();
|
|
262
|
-
const row = await delegate.fetchOne(request);
|
|
263
|
-
ws.send(JSON.stringify({ type: "FETCH_ONE_SUCCESS",
|
|
264
|
-
payload: { row },
|
|
265
|
-
requestId }));
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
case "SAVE": {
|
|
269
|
-
const request: SaveProps = payload;
|
|
270
|
-
// The REST layer's write checks, at this boundary too —
|
|
271
|
-
// the socket is a second way in, and it used to be the
|
|
272
|
-
// unchecked one. Collection from the registry by path,
|
|
273
|
-
// never from the client's `request.collection`.
|
|
274
|
-
assertWriteRequest(request.path, request.values as Record<string, unknown>);
|
|
275
|
-
const delegate = await getScopedDelegate();
|
|
276
|
-
const row = await delegate.save(request);
|
|
277
|
-
ws.send(JSON.stringify({ type: "SAVE_SUCCESS",
|
|
278
|
-
payload: { row },
|
|
279
|
-
requestId }));
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
case "DELETE": {
|
|
283
|
-
const request: DeleteProps = payload;
|
|
284
|
-
const delegate = await getScopedDelegate();
|
|
285
|
-
await delegate.delete(request);
|
|
286
|
-
ws.send(JSON.stringify({ type: "DELETE_SUCCESS",
|
|
287
|
-
payload: { success: true },
|
|
288
|
-
requestId }));
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
case "CHECK_UNIQUE_FIELD": {
|
|
292
|
-
const { path, name, value, id, collection } = payload;
|
|
293
|
-
const delegate = await getScopedDelegate();
|
|
294
|
-
const isUnique = await delegate.checkUniqueField(path, name, value, id, collection);
|
|
295
|
-
ws.send(JSON.stringify({ type: "CHECK_UNIQUE_FIELD_SUCCESS",
|
|
296
|
-
payload: { isUnique },
|
|
297
|
-
requestId }));
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
case "COUNT": {
|
|
301
|
-
const request: FetchCollectionProps = payload;
|
|
302
|
-
const delegate = await getScopedDelegate();
|
|
303
|
-
const count = await delegate.count!(request);
|
|
304
|
-
ws.send(JSON.stringify({ type: "COUNT_SUCCESS",
|
|
305
|
-
payload: { count },
|
|
306
|
-
requestId }));
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
309
|
-
case "EXECUTE_SQL": {
|
|
310
|
-
const { sql, options } = payload;
|
|
311
|
-
if (admin && isDocumentAdmin(admin) && admin.executeAggregate) {
|
|
312
|
-
const result = await admin.executeAggregate(sql as Record<string, unknown>[]);
|
|
313
|
-
ws.send(JSON.stringify({ type: "EXECUTE_SQL_SUCCESS",
|
|
314
|
-
payload: { result },
|
|
315
|
-
requestId }));
|
|
316
|
-
} else {
|
|
317
|
-
ws.send(JSON.stringify({ type: "ERROR",
|
|
318
|
-
requestId,
|
|
319
|
-
payload: { error: { message: "SQL execution not supported for this driver",
|
|
320
|
-
code: "NOT_SUPPORTED" } } }));
|
|
321
|
-
}
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
|
-
case "FETCH_UNMAPPED_TABLES": {
|
|
325
|
-
if (admin && isSchemaAdmin(admin)) {
|
|
326
|
-
const tables = await admin.fetchUnmappedTables?.(payload?.mappedPaths) || [];
|
|
327
|
-
ws.send(JSON.stringify({ type: "FETCH_UNMAPPED_TABLES_SUCCESS",
|
|
328
|
-
payload: { tables },
|
|
329
|
-
requestId }));
|
|
330
|
-
} else {
|
|
331
|
-
ws.send(JSON.stringify({ type: "FETCH_UNMAPPED_TABLES_SUCCESS",
|
|
332
|
-
payload: { tables: [] },
|
|
333
|
-
requestId }));
|
|
334
|
-
}
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
case "FETCH_TABLE_METADATA": {
|
|
338
|
-
const { tableName } = payload;
|
|
339
|
-
if (admin && isSchemaAdmin(admin)) {
|
|
340
|
-
const metadata = await admin.fetchTableMetadata?.(tableName);
|
|
341
|
-
ws.send(JSON.stringify({ type: "FETCH_TABLE_METADATA_SUCCESS",
|
|
342
|
-
payload: { metadata },
|
|
343
|
-
requestId }));
|
|
344
|
-
} else {
|
|
345
|
-
ws.send(JSON.stringify({ type: "FETCH_TABLE_METADATA_SUCCESS",
|
|
346
|
-
payload: { metadata: null },
|
|
347
|
-
requestId }));
|
|
348
|
-
}
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
case "subscribe_collection":
|
|
352
|
-
case "subscribe_one":
|
|
353
|
-
case "unsubscribe": {
|
|
354
|
-
const session = clientSessions.get(clientId);
|
|
355
|
-
const authContext = session?.user ? { uid: session.user.uid,
|
|
356
|
-
roles: session.user.roles ?? [] } : undefined;
|
|
357
|
-
await realtimeService.handleClientMessage(clientId, {
|
|
358
|
-
type,
|
|
359
|
-
payload,
|
|
360
|
-
subscriptionId: payload?.subscriptionId
|
|
361
|
-
}, authContext);
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
default:
|
|
365
|
-
logger.error("❌ [WebSocket Server] Unknown message type", { detail: type });
|
|
366
|
-
}
|
|
367
|
-
} catch (error: unknown) {
|
|
368
|
-
// A refused write keeps its message: it is the only thing that
|
|
369
|
-
// tells the caller what to send instead, and the generic branch
|
|
370
|
-
// below drops it in production.
|
|
371
|
-
if (error instanceof ApiError || (error as Error)?.name === "ApiError") {
|
|
372
|
-
const apiError = error as ApiError;
|
|
373
|
-
ws.send(JSON.stringify({ type: "ERROR",
|
|
374
|
-
requestId,
|
|
375
|
-
payload: { error: { message: apiError.message,
|
|
376
|
-
code: apiError.code } } }));
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
const errorMessage = process.env.NODE_ENV === "production" ? "An unexpected error occurred" : (error instanceof Error ? error.message : "An unexpected error occurred");
|
|
380
|
-
ws.send(JSON.stringify({ type: "ERROR",
|
|
381
|
-
requestId,
|
|
382
|
-
payload: { error: { message: errorMessage,
|
|
383
|
-
code: "INTERNAL_ERROR" } } }));
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
}
|