@reventlessdev/reventless-local 3.0.0-alpha.173 → 3.0.0-alpha.175

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.
@@ -0,0 +1,341 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Ws from "ws";
4
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
8
+ import * as LocalAuth$ReventlessLocal from "../Auth/LocalAuth.res.mjs";
9
+
10
+ let log = Logger$ReventlessCore.fromEnv();
11
+
12
+ function pathSegment(s) {
13
+ return s.replace(/[^A-Za-z0-9-]/g, "-");
14
+ }
15
+
16
+ let clientChannelPrefix = "/client/";
17
+
18
+ function channelMatches(subscription, channel) {
19
+ if (!subscription.endsWith("/*")) {
20
+ return subscription === channel;
21
+ }
22
+ let prefix = subscription.slice(0, subscription.length - 1 | 0);
23
+ return channel.startsWith(prefix);
24
+ }
25
+
26
+ let connections = {
27
+ contents: []
28
+ };
29
+
30
+ let nextConnectionId = {
31
+ contents: 0
32
+ };
33
+
34
+ function addConnection(send) {
35
+ nextConnectionId.contents = nextConnectionId.contents + 1 | 0;
36
+ let conn_id = nextConnectionId.contents;
37
+ let conn_subscriptions = {};
38
+ let conn = {
39
+ id: conn_id,
40
+ send: send,
41
+ subscriptions: conn_subscriptions
42
+ };
43
+ connections.contents.push(conn);
44
+ return conn;
45
+ }
46
+
47
+ function removeConnection(conn) {
48
+ connections.contents = connections.contents.filter(c => c.id !== conn.id);
49
+ }
50
+
51
+ function resetConnections() {
52
+ connections.contents = [];
53
+ }
54
+
55
+ function frame(fields) {
56
+ return JSON.stringify(Object.fromEntries(fields));
57
+ }
58
+
59
+ let connectionAckFrame = frame([
60
+ [
61
+ "type",
62
+ "connection_ack"
63
+ ],
64
+ [
65
+ "connectionTimeoutMs",
66
+ 300000
67
+ ]
68
+ ]);
69
+
70
+ function dataFrame(subscriptionId, event) {
71
+ return frame([
72
+ [
73
+ "type",
74
+ "data"
75
+ ],
76
+ [
77
+ "id",
78
+ subscriptionId
79
+ ],
80
+ [
81
+ "event",
82
+ event
83
+ ]
84
+ ]);
85
+ }
86
+
87
+ function broadcast(channel, event) {
88
+ connections.contents.forEach(conn => {
89
+ Object.entries(conn.subscriptions).forEach(param => {
90
+ if (channelMatches(param[1], channel)) {
91
+ return conn.send(dataFrame(param[0], event));
92
+ }
93
+ });
94
+ });
95
+ }
96
+
97
+ function broadcastStateChange(name, descriptor) {
98
+ let entityKey = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(descriptor), o => o["id"]), Stdlib_JSON.Decode.string), "");
99
+ if (entityKey === "") {
100
+ return;
101
+ }
102
+ let channel = `/default/` + pathSegment(name) + `/` + pathSegment(entityKey);
103
+ broadcast(channel, JSON.stringify(descriptor));
104
+ }
105
+
106
+ function decodeStringField(json, field) {
107
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o[field]), Stdlib_JSON.Decode.string);
108
+ }
109
+
110
+ function handleFrame(conn, text) {
111
+ let parsed;
112
+ try {
113
+ parsed = JSON.parse(text);
114
+ } catch (exn) {
115
+ parsed = undefined;
116
+ }
117
+ if (parsed === undefined) {
118
+ return;
119
+ }
120
+ let match = decodeStringField(parsed, "type");
121
+ if (match === undefined) {
122
+ return;
123
+ }
124
+ switch (match) {
125
+ case "connection_init" :
126
+ return conn.send(connectionAckFrame);
127
+ case "subscribe" :
128
+ let match$1 = decodeStringField(parsed, "id");
129
+ let match$2 = decodeStringField(parsed, "channel");
130
+ if (match$1 !== undefined && match$2 !== undefined) {
131
+ conn.subscriptions[match$1] = match$2;
132
+ return conn.send(frame([
133
+ [
134
+ "type",
135
+ "subscribe_success"
136
+ ],
137
+ [
138
+ "id",
139
+ match$1
140
+ ]
141
+ ]));
142
+ } else {
143
+ return;
144
+ }
145
+ case "unsubscribe" :
146
+ let id = decodeStringField(parsed, "id");
147
+ if (id !== undefined) {
148
+ Stdlib_Dict.$$delete(conn.subscriptions, id);
149
+ return conn.send(frame([
150
+ [
151
+ "type",
152
+ "unsubscribe_success"
153
+ ],
154
+ [
155
+ "id",
156
+ id
157
+ ]
158
+ ]));
159
+ } else {
160
+ return;
161
+ }
162
+ default:
163
+ return;
164
+ }
165
+ }
166
+
167
+ function stripBearer(token) {
168
+ if (token.startsWith("Bearer ")) {
169
+ return token.slice(7, token.length).trim();
170
+ } else {
171
+ return token.trim();
172
+ }
173
+ }
174
+
175
+ function tokenIsInvalid(token) {
176
+ if (token !== undefined) {
177
+ return Stdlib_Option.isNone(LocalAuth$ReventlessLocal.Login.verifyAndDecode(stripBearer(token)));
178
+ } else {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ function authFromSubprotocolHeader(headerValue) {
184
+ return Stdlib_Option.flatMap(headerValue.split(",").map(prim => prim.trim()).find(p => p.startsWith("header-")), p => {
185
+ let blob = p.slice(7, p.length);
186
+ try {
187
+ return decodeStringField(JSON.parse(Buffer.from(blob, "base64url").toString("utf8")), "Authorization");
188
+ } catch (exn) {
189
+ return;
190
+ }
191
+ });
192
+ }
193
+
194
+ function jsonError(message) {
195
+ return Object.fromEntries([[
196
+ "error",
197
+ message
198
+ ]]);
199
+ }
200
+
201
+ function handlePublish(authorization, body) {
202
+ if (tokenIsInvalid(authorization)) {
203
+ return [
204
+ 401,
205
+ jsonError("Invalid token")
206
+ ];
207
+ }
208
+ let parsed;
209
+ try {
210
+ parsed = JSON.parse(body);
211
+ } catch (exn) {
212
+ parsed = undefined;
213
+ }
214
+ if (parsed === undefined) {
215
+ return [
216
+ 400,
217
+ jsonError("Invalid JSON body")
218
+ ];
219
+ }
220
+ let channel = decodeStringField(parsed, "channel");
221
+ let events = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(parsed), o => o["events"]), Stdlib_JSON.Decode.array);
222
+ if (channel === undefined) {
223
+ return [
224
+ 400,
225
+ jsonError("Body must carry `channel` and a non-empty `events` array")
226
+ ];
227
+ }
228
+ if (events === undefined) {
229
+ return [
230
+ 400,
231
+ jsonError("Body must carry `channel` and a non-empty `events` array")
232
+ ];
233
+ }
234
+ if (events.length === 0) {
235
+ return [
236
+ 400,
237
+ jsonError("Body must carry `channel` and a non-empty `events` array")
238
+ ];
239
+ }
240
+ if (!channel.startsWith(clientChannelPrefix)) {
241
+ return [
242
+ 403,
243
+ jsonError(`Clients may only publish under ` + clientChannelPrefix)
244
+ ];
245
+ }
246
+ let successful = [];
247
+ let failed = [];
248
+ events.forEach((event, index) => {
249
+ let entry = Object.fromEntries([[
250
+ "index",
251
+ index
252
+ ]]);
253
+ let s = Stdlib_JSON.Decode.string(event);
254
+ if (s !== undefined) {
255
+ let valid;
256
+ try {
257
+ JSON.parse(s);
258
+ valid = true;
259
+ } catch (exn) {
260
+ valid = false;
261
+ }
262
+ if (valid) {
263
+ broadcast(channel, s);
264
+ successful.push(entry);
265
+ } else {
266
+ failed.push(entry);
267
+ }
268
+ return;
269
+ }
270
+ failed.push(entry);
271
+ });
272
+ return [
273
+ 200,
274
+ Object.fromEntries([
275
+ [
276
+ "successful",
277
+ successful
278
+ ],
279
+ [
280
+ "failed",
281
+ failed
282
+ ]
283
+ ])
284
+ ];
285
+ }
286
+
287
+ let realtimePath = "/events/realtime";
288
+
289
+ let subprotocol = "aws-appsync-event-ws";
290
+
291
+ function attach(server, port) {
292
+ let wss = new Ws.WebSocketServer({
293
+ server: server,
294
+ path: realtimePath,
295
+ handleProtocols: (_protocols, _req) => subprotocol
296
+ });
297
+ wss.on("connection", (ws, req) => {
298
+ let auth = Stdlib_Option.flatMap(req.headers["sec-websocket-protocol"], authFromSubprotocolHeader);
299
+ if (tokenIsInvalid(auth)) {
300
+ ws.close(4401, "Invalid token");
301
+ return;
302
+ }
303
+ let conn = addConnection(f => {
304
+ ws.send(f);
305
+ });
306
+ ws.on("message", data => handleFrame(conn, data.toString("utf8")));
307
+ ws.on("close", () => removeConnection(conn));
308
+ });
309
+ log.info("Events:Local", undefined, `events transport on ws://localhost:` + port.toString() + realtimePath + ` (publish: POST /events)`);
310
+ }
311
+
312
+ let YG;
313
+
314
+ export {
315
+ YG,
316
+ log,
317
+ pathSegment,
318
+ clientChannelPrefix,
319
+ channelMatches,
320
+ connections,
321
+ nextConnectionId,
322
+ addConnection,
323
+ removeConnection,
324
+ resetConnections,
325
+ frame,
326
+ connectionAckFrame,
327
+ dataFrame,
328
+ broadcast,
329
+ broadcastStateChange,
330
+ decodeStringField,
331
+ handleFrame,
332
+ stripBearer,
333
+ tokenIsInvalid,
334
+ authFromSubprotocolHeader,
335
+ jsonError,
336
+ handlePublish,
337
+ realtimePath,
338
+ subprotocol,
339
+ attach,
340
+ }
341
+ /* log Not a pure module */
@@ -234,6 +234,26 @@ let _dispatch = (req: nodeRequest, res: nodeResponse, yoga: YG.yoga, getSdl: uni
234
234
  handleLogout(req, res)
235
235
  } else if path == uploadPresignPath && req.method == "POST" {
236
236
  handleUploadPresign(req, res)
237
+ } else if path == "/events" && req.method == "POST" {
238
+ // Local AppSync Events publish endpoint (`/client/**` only). The
239
+ // Authorization header carries the token raw (AWS Cognito-publish shape),
240
+ // so it bypasses `_isInvalidBearer` above and is verified inside.
241
+ let authorization = req.headers->Dict.get("authorization")
242
+ readBody(req, body => {
243
+ let (status, json) = LocalEvents_Server.handlePublish(~authorization, ~body)
244
+ _writeJson(res, ~status, json)
245
+ })
246
+ } else if path == "/events" && req.method == "OPTIONS" {
247
+ // Preflight for the cross-origin browser publish (Authorization header).
248
+ res->writeHead(
249
+ 204,
250
+ {
251
+ "Access-Control-Allow-Origin": "*",
252
+ "Access-Control-Allow-Methods": "POST,OPTIONS",
253
+ "Access-Control-Allow-Headers": "*",
254
+ },
255
+ )
256
+ res->endEmpty
237
257
  } else if req.method == "OPTIONS" && LocalObjectStore.servedKey(path)->Option.isSome {
238
258
  // Preflight for the cross-origin browser PUT to a served-object path.
239
259
  res->writeHead(204, _corsWriteHeaders)
@@ -692,6 +712,9 @@ let start = (~port: int=4000, ~contextFactory as _: option<YG.contextFactory>=?,
692
712
  server->YG.listen(port, () =>
693
713
  log.info(~comp="GraphQL:Domain", `listening on http://localhost:${port->Int.toString}/graphql (SDL: /sdl)`)
694
714
  )
715
+ // Local AppSync Events transport (subscribe WS). Attached here so every
716
+ // start mode (split, unified, replay) carries it.
717
+ LocalEvents_Server.attach(~server, ~port)
695
718
  activeServer.contents = Some(server)
696
719
  }
697
720
 
@@ -19,6 +19,7 @@ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/
19
19
  import * as LocalAuth$ReventlessLocal from "./Auth/LocalAuth.res.mjs";
20
20
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
21
21
  import * as LocalObjectStore$ReventlessLocal from "./LocalObjectStore.res.mjs";
22
+ import * as LocalEvents_Server$ReventlessLocal from "./Api/LocalEvents_Server.res.mjs";
22
23
  import * as Auth_GraphqlContext$ReventlessLocal from "./Auth/Auth_GraphqlContext.res.mjs";
23
24
 
24
25
  let log = Logger$ReventlessCore.fromEnv();
@@ -175,30 +176,50 @@ function _dispatch(req, res, yoga, getSdl) {
175
176
  "error",
176
177
  "Invalid bearer token"
177
178
  ]]));
178
- } else if (path === "/sdl") {
179
+ }
180
+ if (path === "/sdl") {
179
181
  res.writeHead(200, {
180
182
  "Content-Type": "text/plain",
181
183
  "Access-Control-Allow-Origin": "*"
182
184
  });
183
185
  res.end(getSdl());
184
186
  return;
185
- } else if (path === "/__inmemory/login" && req.method === "POST") {
187
+ }
188
+ if (path === "/__inmemory/login" && req.method === "POST") {
186
189
  return handleLogin(req, res);
187
- } else if (path === "/__inmemory/logout" && req.method === "POST") {
190
+ }
191
+ if (path === "/__inmemory/logout" && req.method === "POST") {
188
192
  return handleLogout(req, res);
189
- } else if (path === uploadPresignPath && req.method === "POST") {
193
+ }
194
+ if (path === uploadPresignPath && req.method === "POST") {
190
195
  return handleUploadPresign(req, res);
191
- } else if (req.method === "OPTIONS" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
192
- res.writeHead(204, _corsWriteHeaders);
193
- res.end(null);
194
- return;
195
- } else if (req.method === "PUT" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
196
- return handleObjectPut(req, res, Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.servedKey(path), ""));
197
- } else if (req.method === "GET" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
198
- return handleObjectGet(res, Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.servedKey(path), ""));
199
- } else {
200
- return yoga(req, res);
201
196
  }
197
+ if (path !== "/events" || req.method !== "POST") {
198
+ if (path === "/events" && req.method === "OPTIONS") {
199
+ res.writeHead(204, {
200
+ "Access-Control-Allow-Origin": "*",
201
+ "Access-Control-Allow-Methods": "POST,OPTIONS",
202
+ "Access-Control-Allow-Headers": "*"
203
+ });
204
+ res.end(null);
205
+ return;
206
+ } else if (req.method === "OPTIONS" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
207
+ res.writeHead(204, _corsWriteHeaders);
208
+ res.end(null);
209
+ return;
210
+ } else if (req.method === "PUT" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
211
+ return handleObjectPut(req, res, Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.servedKey(path), ""));
212
+ } else if (req.method === "GET" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
213
+ return handleObjectGet(res, Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.servedKey(path), ""));
214
+ } else {
215
+ return yoga(req, res);
216
+ }
217
+ }
218
+ let authorization = req.headers["authorization"];
219
+ readBody(req, body => {
220
+ let match = LocalEvents_Server$ReventlessLocal.handlePublish(authorization, body);
221
+ _writeJson(res, match[0], match[1]);
222
+ });
202
223
  }
203
224
 
204
225
  function encodeGlobalId(typeName, localId) {
@@ -572,6 +593,7 @@ function start(portOpt, param, param$1) {
572
593
  Stdlib_JsError.throwWithMessage(`DomainGraphQL_Server could not bind port ` + port.toString() + `: ` + detail);
573
594
  });
574
595
  server.listen(port, () => log.info("GraphQL:Domain", undefined, `listening on http://localhost:` + port.toString() + `/graphql (SDL: /sdl)`));
596
+ LocalEvents_Server$ReventlessLocal.attach(server, port);
575
597
  activeServer.contents = Primitive_option.some(server);
576
598
  }
577
599