@graffy/server 0.17.8-alpha.1 → 0.17.8-alpha.3

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/index.cjs CHANGED
@@ -5,18 +5,23 @@ const common = require("@graffy/common");
5
5
  const debug = require("debug");
6
6
  const ws = require("ws");
7
7
  const log$1 = debug("graffy:server:http");
8
- function server$1(store, { auth } = {}) {
8
+ function server$1(store, { auth, allowedOptions = [] } = {}) {
9
9
  if (!store) throw new Error("server.store_undef");
10
10
  return async (req, res) => {
11
11
  const parsed = url.parse(req.url, true);
12
12
  const optParam = parsed.query.opts && String(parsed.query.opts);
13
- const options = optParam && JSON.parse(decodeURIComponent(optParam));
13
+ const rawOptions = optParam && JSON.parse(decodeURIComponent(optParam));
14
+ const safeOptions = Object.fromEntries(
15
+ Object.entries(rawOptions || {}).filter(
16
+ ([k]) => allowedOptions.includes(k)
17
+ )
18
+ );
14
19
  if (req.method === "GET") {
15
20
  try {
16
21
  const qParam = parsed.query.q && String(parsed.query.q);
17
22
  const query = qParam && common.unpack(JSON.parse(decodeURIComponent(qParam)));
18
23
  if (req.headers.accept === "text/event-stream") {
19
- if (auth && !await auth("watch", common.decodeQuery(query), options)) {
24
+ if (auth && !await auth("watch", common.decodeQuery(query), safeOptions)) {
20
25
  const body = "unauthorized";
21
26
  res.writeHead(401, {
22
27
  "Content-Type": "text/plain",
@@ -35,7 +40,7 @@ function server$1(store, { auth } = {}) {
35
40
  }, 29e3);
36
41
  try {
37
42
  const stream = store.call("watch", query, {
38
- ...options,
43
+ ...safeOptions,
39
44
  raw: true
40
45
  });
41
46
  for await (const value of stream) {
@@ -77,7 +82,7 @@ data: ${e.message}
77
82
  if (auth && !await auth(
78
83
  op,
79
84
  (op === "write" ? common.decodeGraph : common.decodeQuery)(payload),
80
- options
85
+ safeOptions
81
86
  )) {
82
87
  const body2 = "unauthorized";
83
88
  res.writeHead(401, {
@@ -87,7 +92,7 @@ data: ${e.message}
87
92
  res.end(body2);
88
93
  return;
89
94
  }
90
- const value = await store.call(op, payload, options);
95
+ const value = await store.call(op, payload, safeOptions);
91
96
  const body = JSON.stringify(common.pack(value));
92
97
  res.writeHead(200, {
93
98
  "Content-Type": "application/json",
@@ -116,14 +121,19 @@ data: ${e.message}
116
121
  }
117
122
  const log = debug("graffy:server:ws");
118
123
  const PING_INTERVAL = 3e4;
119
- function server(store, { auth } = {}) {
124
+ function server(store, { auth, allowedOptions = [] } = {}) {
120
125
  if (!store) throw new Error("server.store_undef");
121
126
  const wss = new ws.WebSocketServer({ noServer: true });
122
127
  wss.on("connection", function connection(ws2) {
123
128
  ws2.graffyStreams = {};
124
129
  ws2.on("message", async function message(msg) {
125
130
  try {
126
- const [id, op, packedPayload, options] = JSON.parse(msg);
131
+ const [id, op, packedPayload, rawOptions] = JSON.parse(msg);
132
+ const safeOptions = Object.fromEntries(
133
+ Object.entries(rawOptions || {}).filter(
134
+ ([k]) => allowedOptions.includes(k)
135
+ )
136
+ );
127
137
  const payload = common.unpack(packedPayload);
128
138
  if (id === ":pong") {
129
139
  ws2.pingPending = false;
@@ -131,7 +141,7 @@ function server(store, { auth } = {}) {
131
141
  }
132
142
  if (auth && op !== "unwatch") {
133
143
  const decoded = op === "write" ? common.decodeGraph(payload) : common.decodeQuery(payload);
134
- if (!await auth(op, decoded, options)) {
144
+ if (!await auth(op, decoded, safeOptions)) {
135
145
  ws2.send(JSON.stringify([id, "unauthorized"]));
136
146
  return;
137
147
  }
@@ -140,7 +150,7 @@ function server(store, { auth } = {}) {
140
150
  case "read":
141
151
  case "write":
142
152
  try {
143
- const result = await store.call(op, payload, options);
153
+ const result = await store.call(op, payload, safeOptions);
144
154
  ws2.send(JSON.stringify([id, null, common.pack(result)]));
145
155
  } catch (e) {
146
156
  log(`${op}error:${e.message} ${payload}`);
@@ -150,7 +160,7 @@ function server(store, { auth } = {}) {
150
160
  case "watch":
151
161
  try {
152
162
  const stream = store.call("watch", payload, {
153
- ...options,
163
+ ...safeOptions,
154
164
  raw: true
155
165
  });
156
166
  ws2.graffyStreams[id] = stream;
@@ -182,7 +192,10 @@ function server(store, { auth } = {}) {
182
192
  });
183
193
  setInterval(function ping() {
184
194
  wss.clients.forEach(function each(ws2) {
185
- if (ws2.pingPending) return ws2.terminate();
195
+ if (ws2.pingPending) {
196
+ ws2.terminate();
197
+ return;
198
+ }
186
199
  ws2.pingPending = true;
187
200
  ws2.send(JSON.stringify([":ping", Date.now()]));
188
201
  });
package/index.mjs CHANGED
@@ -3,18 +3,23 @@ import { unpack, decodeQuery, pack, decodeGraph } from "@graffy/common";
3
3
  import debug from "debug";
4
4
  import { WebSocketServer } from "ws";
5
5
  const log$1 = debug("graffy:server:http");
6
- function server$1(store, { auth } = {}) {
6
+ function server$1(store, { auth, allowedOptions = [] } = {}) {
7
7
  if (!store) throw new Error("server.store_undef");
8
8
  return async (req, res) => {
9
9
  const parsed = url.parse(req.url, true);
10
10
  const optParam = parsed.query.opts && String(parsed.query.opts);
11
- const options = optParam && JSON.parse(decodeURIComponent(optParam));
11
+ const rawOptions = optParam && JSON.parse(decodeURIComponent(optParam));
12
+ const safeOptions = Object.fromEntries(
13
+ Object.entries(rawOptions || {}).filter(
14
+ ([k]) => allowedOptions.includes(k)
15
+ )
16
+ );
12
17
  if (req.method === "GET") {
13
18
  try {
14
19
  const qParam = parsed.query.q && String(parsed.query.q);
15
20
  const query = qParam && unpack(JSON.parse(decodeURIComponent(qParam)));
16
21
  if (req.headers.accept === "text/event-stream") {
17
- if (auth && !await auth("watch", decodeQuery(query), options)) {
22
+ if (auth && !await auth("watch", decodeQuery(query), safeOptions)) {
18
23
  const body = "unauthorized";
19
24
  res.writeHead(401, {
20
25
  "Content-Type": "text/plain",
@@ -33,7 +38,7 @@ function server$1(store, { auth } = {}) {
33
38
  }, 29e3);
34
39
  try {
35
40
  const stream = store.call("watch", query, {
36
- ...options,
41
+ ...safeOptions,
37
42
  raw: true
38
43
  });
39
44
  for await (const value of stream) {
@@ -75,7 +80,7 @@ data: ${e.message}
75
80
  if (auth && !await auth(
76
81
  op,
77
82
  (op === "write" ? decodeGraph : decodeQuery)(payload),
78
- options
83
+ safeOptions
79
84
  )) {
80
85
  const body2 = "unauthorized";
81
86
  res.writeHead(401, {
@@ -85,7 +90,7 @@ data: ${e.message}
85
90
  res.end(body2);
86
91
  return;
87
92
  }
88
- const value = await store.call(op, payload, options);
93
+ const value = await store.call(op, payload, safeOptions);
89
94
  const body = JSON.stringify(pack(value));
90
95
  res.writeHead(200, {
91
96
  "Content-Type": "application/json",
@@ -114,14 +119,19 @@ data: ${e.message}
114
119
  }
115
120
  const log = debug("graffy:server:ws");
116
121
  const PING_INTERVAL = 3e4;
117
- function server(store, { auth } = {}) {
122
+ function server(store, { auth, allowedOptions = [] } = {}) {
118
123
  if (!store) throw new Error("server.store_undef");
119
124
  const wss = new WebSocketServer({ noServer: true });
120
125
  wss.on("connection", function connection(ws) {
121
126
  ws.graffyStreams = {};
122
127
  ws.on("message", async function message(msg) {
123
128
  try {
124
- const [id, op, packedPayload, options] = JSON.parse(msg);
129
+ const [id, op, packedPayload, rawOptions] = JSON.parse(msg);
130
+ const safeOptions = Object.fromEntries(
131
+ Object.entries(rawOptions || {}).filter(
132
+ ([k]) => allowedOptions.includes(k)
133
+ )
134
+ );
125
135
  const payload = unpack(packedPayload);
126
136
  if (id === ":pong") {
127
137
  ws.pingPending = false;
@@ -129,7 +139,7 @@ function server(store, { auth } = {}) {
129
139
  }
130
140
  if (auth && op !== "unwatch") {
131
141
  const decoded = op === "write" ? decodeGraph(payload) : decodeQuery(payload);
132
- if (!await auth(op, decoded, options)) {
142
+ if (!await auth(op, decoded, safeOptions)) {
133
143
  ws.send(JSON.stringify([id, "unauthorized"]));
134
144
  return;
135
145
  }
@@ -138,7 +148,7 @@ function server(store, { auth } = {}) {
138
148
  case "read":
139
149
  case "write":
140
150
  try {
141
- const result = await store.call(op, payload, options);
151
+ const result = await store.call(op, payload, safeOptions);
142
152
  ws.send(JSON.stringify([id, null, pack(result)]));
143
153
  } catch (e) {
144
154
  log(`${op}error:${e.message} ${payload}`);
@@ -148,7 +158,7 @@ function server(store, { auth } = {}) {
148
158
  case "watch":
149
159
  try {
150
160
  const stream = store.call("watch", payload, {
151
- ...options,
161
+ ...safeOptions,
152
162
  raw: true
153
163
  });
154
164
  ws.graffyStreams[id] = stream;
@@ -180,7 +190,10 @@ function server(store, { auth } = {}) {
180
190
  });
181
191
  setInterval(function ping() {
182
192
  wss.clients.forEach(function each(ws) {
183
- if (ws.pingPending) return ws.terminate();
193
+ if (ws.pingPending) {
194
+ ws.terminate();
195
+ return;
196
+ }
184
197
  ws.pingPending = true;
185
198
  ws.send(JSON.stringify([":ping", Date.now()]));
186
199
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graffy/server",
3
3
  "description": "Node.js library for building an API for a Graffy store.",
4
4
  "author": "aravind (https://github.com/aravindet)",
5
- "version": "0.17.8-alpha.1",
5
+ "version": "0.17.8-alpha.3",
6
6
  "main": "./index.cjs",
7
7
  "exports": {
8
8
  "import": "./index.mjs",
@@ -16,8 +16,8 @@
16
16
  },
17
17
  "license": "Apache-2.0",
18
18
  "dependencies": {
19
- "@graffy/common": "0.17.8-alpha.1",
20
- "debug": "^4.4.1",
21
- "ws": "^8.18.2"
19
+ "@graffy/common": "0.17.8-alpha.3",
20
+ "debug": "^4.4.3",
21
+ "ws": "^8.19.0"
22
22
  }
23
23
  }
@@ -1,12 +1,19 @@
1
1
  /**
2
2
  * @typedef {import('@graffy/core').default} GraffyStore
3
3
  * @param {GraffyStore} store
4
- * @param {{
5
- * auth?: (operation: string, payload: any, options: any) => Promise<boolean>
6
- * } | undefined} options
4
+ * @param {object} [options]
5
+ * @param {(operation: string, payload: any, options: any) => Promise<boolean>} [options.auth]
6
+ * Optional callback to authorize each request. Receives the operation name,
7
+ * decoded payload, and the filtered options. Return `true` to allow, `false`
8
+ * (or a rejected promise) to reject with 401.
9
+ * @param {string[]} [options.allowedOptions]
10
+ * Allowlist of option keys that clients are permitted to pass through to
11
+ * `store.call` and the `auth` callback. Any key not in this list is stripped
12
+ * from the client-supplied options before use. Defaults to `[]` (strip all).
7
13
  * @returns
8
14
  */
9
- export default function server(store: GraffyStore, { auth }?: {
15
+ export default function server(store: GraffyStore, { auth, allowedOptions }?: {
10
16
  auth?: (operation: string, payload: any, options: any) => Promise<boolean>;
11
- } | undefined): (req: any, res: any) => Promise<void>;
17
+ allowedOptions?: string[];
18
+ }): (req: any, res: any) => Promise<void>;
12
19
  export type GraffyStore = import("@graffy/core").default;
@@ -1,12 +1,19 @@
1
1
  /**
2
2
  * @typedef {import('@graffy/core').default} GraffyStore
3
3
  * @param {GraffyStore} store
4
- * @param {{
5
- * auth?: (operation: string, payload: any, options: any) => Promise<boolean>
6
- * } | undefined} options
4
+ * @param {object} [options]
5
+ * @param {(operation: string, payload: any, options: any) => Promise<boolean>} [options.auth]
6
+ * Optional callback to authorize each request. Receives the operation name,
7
+ * decoded payload, and the filtered options. Return `true` to allow, `false`
8
+ * (or a rejected promise) to reject with an error response.
9
+ * @param {string[]} [options.allowedOptions]
10
+ * Allowlist of option keys that clients are permitted to pass through to
11
+ * `store.call` and the `auth` callback. Any key not in this list is stripped
12
+ * from the client-supplied options before use. Defaults to `[]` (strip all).
7
13
  * @returns
8
14
  */
9
- export default function server(store: GraffyStore, { auth }?: {
15
+ export default function server(store: GraffyStore, { auth, allowedOptions }?: {
10
16
  auth?: (operation: string, payload: any, options: any) => Promise<boolean>;
11
- } | undefined): (request: any, socket: any, head: any) => Promise<void>;
17
+ allowedOptions?: string[];
18
+ }): (request: any, socket: any, head: any) => Promise<void>;
12
19
  export type GraffyStore = import("@graffy/core").default;