@rebasepro/server-mongo 0.13.0 → 0.13.1-canary.g06dbe5b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/MongoBootstrapper.d.ts.map +1 -1
- package/dist/auth/ensure-collections.d.ts.map +1 -1
- package/dist/db/MongoConditionBuilder.d.ts +28 -1
- package/dist/db/MongoConditionBuilder.d.ts.map +1 -1
- package/dist/db/MongoDataService.d.ts +19 -1
- package/dist/db/MongoDataService.d.ts.map +1 -1
- package/dist/db/securityRuleFilter.d.ts +65 -0
- package/dist/db/securityRuleFilter.d.ts.map +1 -0
- package/dist/ensure-collections-Bl12bSwO.js +130 -0
- package/dist/ensure-collections-Bl12bSwO.js.map +1 -0
- package/dist/factory.d.ts +11 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/index.es.js +487 -249
- package/dist/index.es.js.map +1 -1
- package/dist/services/MongoDriver.d.ts +37 -5
- package/dist/services/MongoDriver.d.ts.map +1 -1
- package/dist/services/MongoRealtimeService.d.ts +23 -14
- package/dist/services/MongoRealtimeService.d.ts.map +1 -1
- package/dist/{websocket-74DDOdK0.js → websocket-Bs54-gR-.js} +32 -11
- package/dist/websocket-Bs54-gR-.js.map +1 -0
- package/dist/websocket.d.ts +2 -2
- package/dist/websocket.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/MongoBootstrapper.ts +10 -2
- package/src/auth/ensure-collections.ts +61 -13
- package/src/db/MongoConditionBuilder.ts +112 -40
- package/src/db/MongoDataService.ts +51 -14
- package/src/db/securityRuleFilter.ts +398 -0
- package/src/factory.ts +17 -4
- package/src/services/MongoDriver.ts +101 -262
- package/src/services/MongoRealtimeService.ts +124 -66
- package/src/websocket.ts +66 -15
- package/dist/ensure-collections-Bkx_O5CQ.js +0 -94
- package/dist/ensure-collections-Bkx_O5CQ.js.map +0 -1
- package/dist/websocket-74DDOdK0.js.map +0 -1
|
@@ -49,11 +49,20 @@ export declare class MongoDriver implements DataDriver {
|
|
|
49
49
|
/**
|
|
50
50
|
* Fetch a collection of rows
|
|
51
51
|
*/
|
|
52
|
-
fetchCollection<M extends Record<string, any>>(
|
|
52
|
+
fetchCollection<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;
|
|
53
53
|
/**
|
|
54
|
-
* Listen to collection changes
|
|
54
|
+
* Listen to collection changes.
|
|
55
|
+
*
|
|
56
|
+
* `authContext` is not part of `ListenCollectionProps`; it is supplied by
|
|
57
|
+
* {@link AuthenticatedMongoDriver}, which is the only caller that has one.
|
|
58
|
+
* It has to travel *into* the subscription config, because that config is
|
|
59
|
+
* what every re-fetch reads — the wrapper used to stamp the field on the
|
|
60
|
+
* `Subscription` object instead, and nothing has ever read that one.
|
|
55
61
|
*/
|
|
56
|
-
listenCollection<M extends Record<string, any>>({ path, collection, filter, limit, startAfter, orderBy, searchString, order, onUpdate, onError }: ListenCollectionProps<M
|
|
62
|
+
listenCollection<M extends Record<string, any>>({ path, collection, filter, limit, startAfter, orderBy, searchString, order, onUpdate, onError }: ListenCollectionProps<M>, authContext?: {
|
|
63
|
+
uid: string;
|
|
64
|
+
roles: string[];
|
|
65
|
+
}): () => void;
|
|
57
66
|
/**
|
|
58
67
|
* Fetch a single row
|
|
59
68
|
*/
|
|
@@ -61,7 +70,10 @@ export declare class MongoDriver implements DataDriver {
|
|
|
61
70
|
/**
|
|
62
71
|
* Listen to row changes
|
|
63
72
|
*/
|
|
64
|
-
listenOne<M extends Record<string, any>>({ path, id, collection, onUpdate, onError }: ListenOneProps<M
|
|
73
|
+
listenOne<M extends Record<string, any>>({ path, id, collection, onUpdate, onError }: ListenOneProps<M>, authContext?: {
|
|
74
|
+
uid: string;
|
|
75
|
+
roles: string[];
|
|
76
|
+
}): () => void;
|
|
65
77
|
/**
|
|
66
78
|
* Save an row (create or update)
|
|
67
79
|
*/
|
|
@@ -81,7 +93,7 @@ export declare class MongoDriver implements DataDriver {
|
|
|
81
93
|
/**
|
|
82
94
|
* Count rows in a collection
|
|
83
95
|
*/
|
|
84
|
-
count<M extends Record<string, any>>({ path, collection, filter }: FetchCollectionProps<M>): Promise<number>;
|
|
96
|
+
count<M extends Record<string, any>>({ path, collection, filter, logical, searchString }: FetchCollectionProps<M>): Promise<number>;
|
|
85
97
|
/**
|
|
86
98
|
* Generate a unique subscription ID
|
|
87
99
|
*/
|
|
@@ -113,8 +125,28 @@ export declare class AuthenticatedMongoDriver implements DataDriver {
|
|
|
113
125
|
currentTime(): Date;
|
|
114
126
|
fetchCollection<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;
|
|
115
127
|
listenCollection<M extends Record<string, any>>(props: ListenCollectionProps<M>): () => void;
|
|
128
|
+
/** The acting user, in the shape the realtime subscriptions carry. */
|
|
129
|
+
private authContext;
|
|
130
|
+
/**
|
|
131
|
+
* Evaluate the collection's rules for one row, fail-closed.
|
|
132
|
+
*
|
|
133
|
+
* A path with no resolvable collection has no declared rules — the same
|
|
134
|
+
* answer `buildMongoFilterFromSecurityRules` gives a listing on such a path,
|
|
135
|
+
* so the two never disagree about whether this engine has row security.
|
|
136
|
+
*/
|
|
137
|
+
private authorize;
|
|
116
138
|
fetchOne<M extends Record<string, any>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined>;
|
|
117
139
|
listenOne<M extends Record<string, any>>(props: ListenOneProps<M>): () => void;
|
|
140
|
+
/**
|
|
141
|
+
* Save, with both halves of the rule checked *before* the write.
|
|
142
|
+
*
|
|
143
|
+
* There is no transaction here, so a check that runs after
|
|
144
|
+
* `delegate.save` cannot undo anything: the document is written, history is
|
|
145
|
+
* recorded and subscribers have been notified by then, and a 403 at that
|
|
146
|
+
* point only misleads the caller about what happened. Postgres evaluates
|
|
147
|
+
* `WITH CHECK` inside the transaction; the closest this driver can get is to
|
|
148
|
+
* evaluate it against the row as it *will* be, and refuse before writing.
|
|
149
|
+
*/
|
|
118
150
|
save<M extends Record<string, any>>(props: SaveProps<M>): Promise<Record<string, unknown>>;
|
|
119
151
|
delete<M extends Record<string, any>>(props: DeleteProps<M>): Promise<void>;
|
|
120
152
|
checkUniqueField(path: string, name: string, value: any, id?: string, collection?: CollectionConfig): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MongoDriver.d.ts","sourceRoot":"","sources":["../../src/services/MongoDriver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EACH,UAAU,EACV,WAAW,EAEX,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,qBAAqB,EACrB,cAAc,EACd,SAAS,EAET,2BAA2B,EAC3B,IAAI,EACJ,YAAY,EAEZ,aAAa,EAEhB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"MongoDriver.d.ts","sourceRoot":"","sources":["../../src/services/MongoDriver.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,15 +8,24 @@ import { Db, ChangeStream } from "mongodb";
|
|
|
8
8
|
import { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from "@rebasepro/types";
|
|
9
9
|
import { WebSocket } from "ws";
|
|
10
10
|
import type { MongoDriver } from "./MongoDriver";
|
|
11
|
+
/** The acting user for a subscription, as the driver and the socket carry it. */
|
|
12
|
+
export interface SubscriptionAuthContext {
|
|
13
|
+
uid: string;
|
|
14
|
+
roles: string[];
|
|
15
|
+
}
|
|
11
16
|
interface Subscription {
|
|
12
17
|
type: "collection" | "single";
|
|
13
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Carries `authContext`. There is deliberately no second copy on this
|
|
20
|
+
* object: every fetch reads `config.authContext`, and the one that used to
|
|
21
|
+
* live here was written from three places and read from none — so a
|
|
22
|
+
* subscription that looked authorized was re-fetched as nobody.
|
|
23
|
+
*/
|
|
24
|
+
config: (CollectionSubscriptionConfig | SingleSubscriptionConfig) & {
|
|
25
|
+
authContext?: SubscriptionAuthContext;
|
|
26
|
+
};
|
|
14
27
|
changeStream?: ChangeStream;
|
|
15
28
|
callback?: (data: any) => void;
|
|
16
|
-
authContext?: {
|
|
17
|
-
uid: string;
|
|
18
|
-
roles: string[];
|
|
19
|
-
};
|
|
20
29
|
}
|
|
21
30
|
/**
|
|
22
31
|
* MongoDB Realtime Service
|
|
@@ -28,7 +37,6 @@ export declare class MongoRealtimeService implements RealtimeProvider {
|
|
|
28
37
|
private db;
|
|
29
38
|
private subscriptions;
|
|
30
39
|
private clients;
|
|
31
|
-
private dataService;
|
|
32
40
|
private driver?;
|
|
33
41
|
constructor(db: Db);
|
|
34
42
|
setDataDriver(driver: MongoDriver): void;
|
|
@@ -40,23 +48,24 @@ export declare class MongoRealtimeService implements RealtimeProvider {
|
|
|
40
48
|
* Subscribe to collection changes
|
|
41
49
|
*/
|
|
42
50
|
subscribeToCollection(subscriptionId: string, config: CollectionSubscriptionConfig & {
|
|
43
|
-
authContext?:
|
|
44
|
-
uid: string;
|
|
45
|
-
roles: string[];
|
|
46
|
-
};
|
|
51
|
+
authContext?: SubscriptionAuthContext;
|
|
47
52
|
}, callback?: (rows: Record<string, unknown>[]) => void): void;
|
|
48
53
|
/**
|
|
49
54
|
* Fetch collection and notify callback
|
|
50
55
|
*/
|
|
51
56
|
private fetchAndNotifyCollection;
|
|
57
|
+
/**
|
|
58
|
+
* The driver scoped to a subscriber.
|
|
59
|
+
*
|
|
60
|
+
* Never the bare repository: everything a subscription delivers has to pass
|
|
61
|
+
* the same row authorization an HTTP read does.
|
|
62
|
+
*/
|
|
63
|
+
private scopedDriver;
|
|
52
64
|
/**
|
|
53
65
|
* Subscribe to single row changes
|
|
54
66
|
*/
|
|
55
67
|
subscribeToOne(subscriptionId: string, config: SingleSubscriptionConfig & {
|
|
56
|
-
authContext?:
|
|
57
|
-
uid: string;
|
|
58
|
-
roles: string[];
|
|
59
|
-
};
|
|
68
|
+
authContext?: SubscriptionAuthContext;
|
|
60
69
|
}, callback?: (row: Record<string, unknown> | null) => void): void;
|
|
61
70
|
/**
|
|
62
71
|
* Fetch row and notify callback
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MongoRealtimeService.d.ts","sourceRoot":"","sources":["../../src/services/MongoRealtimeService.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,EAAE,EAAE,YAAY,EAA4C,MAAM,SAAS,CAAC;AACrF,OAAO,
|
|
1
|
+
{"version":3,"file":"MongoRealtimeService.d.ts","sourceRoot":"","sources":["../../src/services/MongoRealtimeService.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,EAAE,EAAE,YAAY,EAA4C,MAAM,SAAS,CAAC;AACrF,OAAO,EAIH,gBAAgB,EAChB,4BAA4B,EAC5B,wBAAwB,EAK3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjD,iFAAiF;AACjF,MAAM,WAAW,uBAAuB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,UAAU,YAAY;IAClB,IAAI,EAAE,YAAY,GAAG,QAAQ,CAAC;IAC9B;;;;;OAKG;IACH,MAAM,EAAE,CAAC,4BAA4B,GAAG,wBAAwB,CAAC,GAAG;QAAE,WAAW,CAAC,EAAE,uBAAuB,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;CAClC;AAED;;;;;GAKG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;IAK7C,OAAO,CAAC,EAAE;IAJtB,OAAO,CAAC,aAAa,CAAmC;IACxD,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,MAAM,CAAC,CAAc;gBAET,EAAE,EAAE,EAAE;IAE1B,aAAa,CAAC,MAAM,EAAE,WAAW;IAIjC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,qBAAqB,CACjB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,4BAA4B,GAAG;QAAE,WAAW,CAAC,EAAE,uBAAuB,CAAA;KAAE,EAChF,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,IAAI,GACrD,IAAI;IAgEP;;OAEG;YACW,wBAAwB;IAkCtC;;;;;OAKG;YACW,YAAY;IAS1B;;OAEG;IACH,cAAc,CACV,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,wBAAwB,GAAG;QAAE,WAAW,CAAC,EAAE,uBAAuB,CAAA;KAAE,EAC5E,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,KAAK,IAAI,GACzD,IAAI;IAqEP;;OAEG;YACW,iBAAiB;IAsB/B;;OAEG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAUzC;;;OAGG;IACG,YAAY,CACd,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACnC,WAAW,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC;IA2BhB;;OAEG;IACH,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC;IAI7C;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAU/B;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS;IAazC;;OAEG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACG,mBAAmB,CACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,GAAG,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,EACjE,YAAY,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,GACjD,OAAO,CAAC,IAAI,CAAC;CAgHnB"}
|
|
@@ -19,10 +19,11 @@ var ADMIN_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
|
19
19
|
"LIST_BRANCHES"
|
|
20
20
|
]);
|
|
21
21
|
function isAdminSession(session) {
|
|
22
|
-
if (!session?.user
|
|
23
|
-
|
|
22
|
+
if (!session?.user) return false;
|
|
23
|
+
if (session.user.isAdmin) return true;
|
|
24
|
+
return (session.user.roles ?? []).some((r) => r === "admin");
|
|
24
25
|
}
|
|
25
|
-
function createMongoWebSocket(server, realtimeService, driver, authConfig, admin) {
|
|
26
|
+
function createMongoWebSocket(server, realtimeService, driver, authConfig, admin, authAdapter) {
|
|
26
27
|
const clientSessions = /* @__PURE__ */ new Map();
|
|
27
28
|
const isProduction = process.env.NODE_ENV === "production";
|
|
28
29
|
const wsDebug = (...args) => {
|
|
@@ -33,8 +34,8 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
33
34
|
if (err.code === "EADDRINUSE") return;
|
|
34
35
|
logger.error("❌ [WebSocket Server] Error", { error: err });
|
|
35
36
|
});
|
|
36
|
-
const requireAuth = resolveRequireAuth(authConfig);
|
|
37
|
-
if (requireAuth && !authConfig?.jwtSecret) logger.warn("🔐 [WebSocket Server] Authentication is required but no jwtSecret is configured — no client can complete AUTH, so every realtime message will be refused.");
|
|
37
|
+
const requireAuth = !!authAdapter || resolveRequireAuth(authConfig);
|
|
38
|
+
if (requireAuth && !authAdapter && !authConfig?.jwtSecret) logger.warn("🔐 [WebSocket Server] Authentication is required but no adapter or jwtSecret is configured — no client can complete AUTH, so every realtime message will be refused.");
|
|
38
39
|
wss.on("connection", (ws) => {
|
|
39
40
|
const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
40
41
|
wsDebug(`WebSocket client connected: ${clientId}`);
|
|
@@ -71,19 +72,39 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
71
72
|
sendError("AUTH_ERROR", "INVALID_INPUT", "Token is required");
|
|
72
73
|
return;
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
if (
|
|
75
|
+
let verifiedUser = null;
|
|
76
|
+
if (authAdapter) try {
|
|
77
|
+
const adapterUser = authAdapter.verifyToken ? await authAdapter.verifyToken(token) : await authAdapter.verifyRequest(new Request("http://localhost/_ws_auth", { headers: { Authorization: `Bearer ${token}` } }));
|
|
78
|
+
if (adapterUser) verifiedUser = {
|
|
79
|
+
uid: adapterUser.uid,
|
|
80
|
+
email: adapterUser.email,
|
|
81
|
+
roles: adapterUser.roles ?? [],
|
|
82
|
+
isAdmin: !!adapterUser.isAdmin
|
|
83
|
+
};
|
|
84
|
+
} catch {}
|
|
85
|
+
else {
|
|
86
|
+
const jwtPayload = extractUserFromToken(token);
|
|
87
|
+
if (jwtPayload) verifiedUser = {
|
|
88
|
+
uid: jwtPayload.uid,
|
|
89
|
+
email: jwtPayload.email,
|
|
90
|
+
displayName: jwtPayload.displayName,
|
|
91
|
+
photoURL: jwtPayload.photoURL,
|
|
92
|
+
roles: jwtPayload.roles ?? [],
|
|
93
|
+
isAdmin: (jwtPayload.roles ?? []).some((r) => r === "admin")
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (verifiedUser) {
|
|
76
97
|
const session = clientSessions.get(clientId);
|
|
77
98
|
if (session) {
|
|
78
|
-
session.user =
|
|
99
|
+
session.user = verifiedUser;
|
|
79
100
|
session.authenticated = true;
|
|
80
101
|
}
|
|
81
102
|
ws.send(JSON.stringify({
|
|
82
103
|
type: "AUTH_SUCCESS",
|
|
83
104
|
requestId,
|
|
84
105
|
payload: {
|
|
85
|
-
uid:
|
|
86
|
-
roles:
|
|
106
|
+
uid: verifiedUser.uid,
|
|
107
|
+
roles: verifiedUser.roles
|
|
87
108
|
}
|
|
88
109
|
}));
|
|
89
110
|
} else sendError("AUTH_ERROR", "INVALID_TOKEN", "Invalid or expired token");
|
|
@@ -279,4 +300,4 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
279
300
|
//#endregion
|
|
280
301
|
export { createMongoWebSocket };
|
|
281
302
|
|
|
282
|
-
//# sourceMappingURL=websocket-
|
|
303
|
+
//# sourceMappingURL=websocket-Bs54-gR-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-Bs54-gR-.js","names":[],"sources":["../src/websocket.ts"],"sourcesContent":["import { 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 } 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\ninterface DriverWithAuth extends DataDriver {\n withAuth(user: Record<string, unknown>): Promise<DataDriver>;\n}\n\nfunction isDriverWithAuth(driver: DataDriver): driver is DriverWithAuth {\n return \"withAuth\" in driver && typeof (driver as Record<string, unknown>).withAuth === \"function\";\n}\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\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\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 };\n }\n } catch {\n // Adapter threw — treat as invalid token\n }\n } else {\n const jwtPayload = 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 };\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 const getScopedDelegate = async (): Promise<DataDriver> => {\n const session = clientSessions.get(clientId);\n if (session?.user && isDriverWithAuth(driver)) {\n try {\n const userForAuth: User = {\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: false,\n roles: session.user.roles ?? []\n };\n return await driver.withAuth(userForAuth);\n } catch (e) {\n logger.error(\"Failed to create authenticated delegate for WS request\", { error: e });\n return driver;\n }\n }\n return driver;\n };\n\n switch (type) {\n case \"FETCH_COLLECTION\": {\n const request: FetchCollectionProps = payload;\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 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 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 const delegate = await getScopedDelegate();\n await delegate.delete(request);\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 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 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 const session = clientSessions.get(clientId);\n const authContext = session?.user ? { uid: session.user.uid,\nroles: session.user.roles ?? [] } : undefined;\n await realtimeService.handleClientMessage(clientId, {\n type,\n payload,\n subscriptionId: payload?.subscriptionId\n }, authContext);\n break;\n }\n default:\n logger.error(\"❌ [WebSocket Server] Unknown message type\", { detail: type });\n }\n } catch (error: unknown) {\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/dist/websocket.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { DatabaseAdmin } from "@rebasepro/types";
|
|
1
|
+
import { DatabaseAdmin, AuthAdapter } from "@rebasepro/types";
|
|
2
2
|
import { Server } from "http";
|
|
3
3
|
import type { RebaseAuthConfig } from "@rebasepro/server";
|
|
4
4
|
import { MongoRealtimeService } from "./services/MongoRealtimeService";
|
|
5
5
|
import { MongoDriver } from "./services/MongoDriver";
|
|
6
|
-
export declare function createMongoWebSocket(server: Server, realtimeService: MongoRealtimeService, driver: MongoDriver, authConfig?: RebaseAuthConfig, admin?: DatabaseAdmin): void;
|
|
6
|
+
export declare function createMongoWebSocket(server: Server, realtimeService: MongoRealtimeService, driver: MongoDriver, authConfig?: RebaseAuthConfig, admin?: DatabaseAdmin, authAdapter?: AuthAdapter): void;
|
|
7
7
|
//# sourceMappingURL=websocket.d.ts.map
|
package/dist/websocket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4G,aAAa,EAAwC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4G,aAAa,EAAwC,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE9M,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAG9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAuDrD,wBAAgB,oBAAoB,CAChC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,oBAAoB,EACrC,MAAM,EAAE,WAAW,EACnB,UAAU,CAAC,EAAE,gBAAgB,EAC7B,KAAK,CAAC,EAAE,aAAa,EACrB,WAAW,CAAC,EAAE,WAAW,QAsS5B"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-mongo",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.1-canary.g06dbe5b",
|
|
5
5
|
"description": "MongoDB backend for Rebase",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"mongodb": "^7.5.0",
|
|
42
42
|
"ws": "^8.21.1",
|
|
43
|
-
"@rebasepro/common": "0.13.
|
|
44
|
-
"@rebasepro/
|
|
45
|
-
"@rebasepro/
|
|
43
|
+
"@rebasepro/common": "0.13.1-canary.g06dbe5b",
|
|
44
|
+
"@rebasepro/utils": "0.13.1-canary.g06dbe5b",
|
|
45
|
+
"@rebasepro/types": "0.13.1-canary.g06dbe5b"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/jest": "^30.0.0",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"vite": "^8.1.5"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@rebasepro/server": "0.13.
|
|
58
|
+
"@rebasepro/server": "0.13.1-canary.g06dbe5b"
|
|
59
59
|
},
|
|
60
60
|
"peerDependenciesMeta": {
|
|
61
61
|
"@rebasepro/server": {
|
package/src/MongoBootstrapper.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Db, MongoClient } from "mongodb";
|
|
2
2
|
import type {
|
|
3
|
+
AuthAdapter,
|
|
3
4
|
BackendBootstrapper,
|
|
4
5
|
InitializedDriver,
|
|
5
6
|
BootstrappedAuth,
|
|
@@ -182,14 +183,21 @@ policies: [] };
|
|
|
182
183
|
|
|
183
184
|
mountRoutes() {},
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
// Five parameters, not four. `BackendBootstrapper` declares an
|
|
187
|
+
// `authAdapter` here and `init.ts` passes one; dropping it left the
|
|
188
|
+
// socket with only its built-in JWT verifier, so on a backend using an
|
|
189
|
+
// AuthAdapter every realtime AUTHENTICATE failed with "Invalid or
|
|
190
|
+
// expired token" — and nothing on either side could have type-checked
|
|
191
|
+
// the mismatch.
|
|
192
|
+
async initializeWebsockets(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> {
|
|
186
193
|
const { createMongoWebSocket } = await import("./websocket");
|
|
187
194
|
createMongoWebSocket(
|
|
188
195
|
server as import("http").Server,
|
|
189
196
|
realtimeService as MongoRealtimeService,
|
|
190
197
|
driver as MongoDriver,
|
|
191
198
|
config as Record<string, unknown> | undefined,
|
|
192
|
-
cachedAdmin
|
|
199
|
+
cachedAdmin,
|
|
200
|
+
authAdapter
|
|
193
201
|
);
|
|
194
202
|
}
|
|
195
203
|
};
|
|
@@ -40,45 +40,93 @@ delete: false },
|
|
|
40
40
|
}
|
|
41
41
|
];
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Indexes created by releases up to 0.13.0, on snake_case field names the auth
|
|
45
|
+
* services never wrote.
|
|
46
|
+
*
|
|
47
|
+
* MongoDB indexes an absent field as `null`, so a unique index whose every key
|
|
48
|
+
* component is always missing admits exactly **one** document per collection:
|
|
49
|
+
* the second login on the whole deployment failed with `E11000`, as did the
|
|
50
|
+
* second role assignment and the second outstanding password-reset token.
|
|
51
|
+
* Dropping them is therefore part of the fix, not tidying — creating the
|
|
52
|
+
* correct index alongside a broken one leaves the deployment just as broken.
|
|
53
|
+
*
|
|
54
|
+
* `{user_id, user_agent, ip_address}` has a second reason to go: it is the
|
|
55
|
+
* reverted Postgres constraint. `MongoRefreshTokenService.createToken` explains
|
|
56
|
+
* why evicting by that triple signs a second browser out; the unique index
|
|
57
|
+
* expressing the same rule was left behind here.
|
|
58
|
+
*/
|
|
59
|
+
const LEGACY_INDEX_NAMES: Record<string, string[]> = {
|
|
60
|
+
rebase_user_identities: ["provider_1_provider_id_1", "user_id_1"],
|
|
61
|
+
rebase_user_roles: ["user_id_1_role_id_1", "user_id_1"],
|
|
62
|
+
rebase_refresh_tokens: ["token_hash_1", "user_id_1_user_agent_1_ip_address_1", "user_id_1"],
|
|
63
|
+
rebase_password_reset_tokens: ["token_hash_1", "user_id_1"]
|
|
64
|
+
};
|
|
65
|
+
|
|
43
66
|
export async function ensureAuthCollectionsExist(db: Db): Promise<void> {
|
|
44
67
|
logger.info("🔍 Checking MongoDB auth collections and indexes...");
|
|
45
68
|
|
|
46
69
|
try {
|
|
70
|
+
await dropLegacyIndexes(db);
|
|
71
|
+
|
|
72
|
+
// Every key below is a field `auth/services.ts` actually writes —
|
|
73
|
+
// camelCase, matching the documents. Verified by
|
|
74
|
+
// `test/MongoAuthServices.test.ts`, which now runs this function.
|
|
75
|
+
|
|
47
76
|
// Users
|
|
48
77
|
const users = db.collection("rebase_users");
|
|
49
78
|
await users.createIndex({ email: 1 }, { unique: true });
|
|
79
|
+
await users.createIndex({ id: 1 });
|
|
50
80
|
|
|
51
81
|
// User Identities
|
|
52
82
|
const identities = db.collection("rebase_user_identities");
|
|
53
83
|
await identities.createIndex({ provider: 1,
|
|
54
|
-
|
|
55
|
-
await identities.createIndex({
|
|
84
|
+
providerId: 1 }, { unique: true });
|
|
85
|
+
await identities.createIndex({ uid: 1 });
|
|
56
86
|
|
|
57
87
|
// User Roles (junction collection)
|
|
58
88
|
const userRoles = db.collection("rebase_user_roles");
|
|
59
|
-
await userRoles.createIndex({
|
|
60
|
-
|
|
61
|
-
await userRoles.createIndex({
|
|
89
|
+
await userRoles.createIndex({ uid: 1,
|
|
90
|
+
roleId: 1 }, { unique: true });
|
|
91
|
+
await userRoles.createIndex({ roleId: 1 });
|
|
62
92
|
|
|
63
|
-
// Refresh Tokens
|
|
93
|
+
// Refresh Tokens. No unique index on (uid, userAgent, ipAddress):
|
|
94
|
+
// tokens of one sign-in accumulate under a shared sessionId.
|
|
64
95
|
const refreshTokens = db.collection("rebase_refresh_tokens");
|
|
65
|
-
await refreshTokens.createIndex({
|
|
66
|
-
await refreshTokens.createIndex({
|
|
67
|
-
|
|
68
|
-
ip_address: 1 }, { unique: true });
|
|
69
|
-
await refreshTokens.createIndex({ user_id: 1 });
|
|
96
|
+
await refreshTokens.createIndex({ tokenHash: 1 }, { unique: true });
|
|
97
|
+
await refreshTokens.createIndex({ uid: 1 });
|
|
98
|
+
await refreshTokens.createIndex({ sessionId: 1 });
|
|
70
99
|
|
|
71
100
|
// Password Reset Tokens
|
|
72
101
|
const resetTokens = db.collection("rebase_password_reset_tokens");
|
|
73
|
-
await resetTokens.createIndex({
|
|
74
|
-
await resetTokens.createIndex({
|
|
102
|
+
await resetTokens.createIndex({ tokenHash: 1 }, { unique: true });
|
|
103
|
+
await resetTokens.createIndex({ uid: 1 });
|
|
75
104
|
|
|
76
105
|
// Seed roles
|
|
77
106
|
await seedDefaultRoles(db);
|
|
78
107
|
|
|
79
108
|
logger.info("✅ MongoDB Auth collections ready");
|
|
80
109
|
} catch (error) {
|
|
110
|
+
// Not swallowed. Every index here is load-bearing for auth — a boot
|
|
111
|
+
// that logs this and carries on is a deployment whose second login
|
|
112
|
+
// fails, with a green boot log to say nothing is wrong.
|
|
81
113
|
logger.error("❌ Failed to set up MongoDB auth collections", { error: error });
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Drop the indexes of {@link LEGACY_INDEX_NAMES} that this database still has. */
|
|
119
|
+
async function dropLegacyIndexes(db: Db): Promise<void> {
|
|
120
|
+
for (const [collectionName, indexNames] of Object.entries(LEGACY_INDEX_NAMES)) {
|
|
121
|
+
const collection = db.collection(collectionName);
|
|
122
|
+
for (const indexName of indexNames) {
|
|
123
|
+
try {
|
|
124
|
+
await collection.dropIndex(indexName);
|
|
125
|
+
logger.info(`🧹 Dropped stale auth index ${collectionName}.${indexName}`);
|
|
126
|
+
} catch {
|
|
127
|
+
// NamespaceNotFound / IndexNotFound — nothing to drop here.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
82
130
|
}
|
|
83
131
|
}
|
|
84
132
|
|