@graffy/server 0.17.7 → 0.17.8-alpha.2

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,17 +5,31 @@ 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") {
24
+ if (auth && !await auth("watch", common.decodeQuery(query), safeOptions)) {
25
+ const body = "unauthorized";
26
+ res.writeHead(401, {
27
+ "Content-Type": "text/plain",
28
+ "Content-Length": Buffer.byteLength(body)
29
+ });
30
+ res.end(body);
31
+ return;
32
+ }
19
33
  res.setHeader("content-type", "text/event-stream");
20
34
  const keepAlive = setInterval(() => {
21
35
  if (req.aborted || res.finished) {
@@ -26,7 +40,7 @@ function server$1(store, { auth } = {}) {
26
40
  }, 29e3);
27
41
  try {
28
42
  const stream = store.call("watch", query, {
29
- ...options,
43
+ ...safeOptions,
30
44
  raw: true
31
45
  });
32
46
  for await (const value of stream) {
@@ -68,7 +82,7 @@ data: ${e.message}
68
82
  if (auth && !await auth(
69
83
  op,
70
84
  (op === "write" ? common.decodeGraph : common.decodeQuery)(payload),
71
- options
85
+ safeOptions
72
86
  )) {
73
87
  const body2 = "unauthorized";
74
88
  res.writeHead(401, {
@@ -78,7 +92,7 @@ data: ${e.message}
78
92
  res.end(body2);
79
93
  return;
80
94
  }
81
- const value = await store.call(op, payload, options);
95
+ const value = await store.call(op, payload, safeOptions);
82
96
  const body = JSON.stringify(common.pack(value));
83
97
  res.writeHead(200, {
84
98
  "Content-Type": "application/json",
@@ -107,24 +121,36 @@ data: ${e.message}
107
121
  }
108
122
  const log = debug("graffy:server:ws");
109
123
  const PING_INTERVAL = 3e4;
110
- function server(store) {
124
+ function server(store, { auth, allowedOptions = [] } = {}) {
111
125
  if (!store) throw new Error("server.store_undef");
112
126
  const wss = new ws.WebSocketServer({ noServer: true });
113
127
  wss.on("connection", function connection(ws2) {
114
128
  ws2.graffyStreams = {};
115
129
  ws2.on("message", async function message(msg) {
116
130
  try {
117
- 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
+ );
118
137
  const payload = common.unpack(packedPayload);
119
138
  if (id === ":pong") {
120
139
  ws2.pingPending = false;
121
140
  return;
122
141
  }
142
+ if (auth && op !== "unwatch") {
143
+ const decoded = op === "write" ? common.decodeGraph(payload) : common.decodeQuery(payload);
144
+ if (!await auth(op, decoded, safeOptions)) {
145
+ ws2.send(JSON.stringify([id, "unauthorized"]));
146
+ return;
147
+ }
148
+ }
123
149
  switch (op) {
124
150
  case "read":
125
151
  case "write":
126
152
  try {
127
- const result = await store.call(op, payload, options);
153
+ const result = await store.call(op, payload, safeOptions);
128
154
  ws2.send(JSON.stringify([id, null, common.pack(result)]));
129
155
  } catch (e) {
130
156
  log(`${op}error:${e.message} ${payload}`);
@@ -134,7 +160,7 @@ function server(store) {
134
160
  case "watch":
135
161
  try {
136
162
  const stream = store.call("watch", payload, {
137
- ...options,
163
+ ...safeOptions,
138
164
  raw: true
139
165
  });
140
166
  ws2.graffyStreams[id] = stream;
package/index.mjs CHANGED
@@ -1,19 +1,33 @@
1
1
  import url from "node:url";
2
- import { unpack, pack, decodeGraph, decodeQuery } from "@graffy/common";
2
+ 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") {
22
+ if (auth && !await auth("watch", decodeQuery(query), safeOptions)) {
23
+ const body = "unauthorized";
24
+ res.writeHead(401, {
25
+ "Content-Type": "text/plain",
26
+ "Content-Length": Buffer.byteLength(body)
27
+ });
28
+ res.end(body);
29
+ return;
30
+ }
17
31
  res.setHeader("content-type", "text/event-stream");
18
32
  const keepAlive = setInterval(() => {
19
33
  if (req.aborted || res.finished) {
@@ -24,7 +38,7 @@ function server$1(store, { auth } = {}) {
24
38
  }, 29e3);
25
39
  try {
26
40
  const stream = store.call("watch", query, {
27
- ...options,
41
+ ...safeOptions,
28
42
  raw: true
29
43
  });
30
44
  for await (const value of stream) {
@@ -66,7 +80,7 @@ data: ${e.message}
66
80
  if (auth && !await auth(
67
81
  op,
68
82
  (op === "write" ? decodeGraph : decodeQuery)(payload),
69
- options
83
+ safeOptions
70
84
  )) {
71
85
  const body2 = "unauthorized";
72
86
  res.writeHead(401, {
@@ -76,7 +90,7 @@ data: ${e.message}
76
90
  res.end(body2);
77
91
  return;
78
92
  }
79
- const value = await store.call(op, payload, options);
93
+ const value = await store.call(op, payload, safeOptions);
80
94
  const body = JSON.stringify(pack(value));
81
95
  res.writeHead(200, {
82
96
  "Content-Type": "application/json",
@@ -105,24 +119,36 @@ data: ${e.message}
105
119
  }
106
120
  const log = debug("graffy:server:ws");
107
121
  const PING_INTERVAL = 3e4;
108
- function server(store) {
122
+ function server(store, { auth, allowedOptions = [] } = {}) {
109
123
  if (!store) throw new Error("server.store_undef");
110
124
  const wss = new WebSocketServer({ noServer: true });
111
125
  wss.on("connection", function connection(ws) {
112
126
  ws.graffyStreams = {};
113
127
  ws.on("message", async function message(msg) {
114
128
  try {
115
- 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
+ );
116
135
  const payload = unpack(packedPayload);
117
136
  if (id === ":pong") {
118
137
  ws.pingPending = false;
119
138
  return;
120
139
  }
140
+ if (auth && op !== "unwatch") {
141
+ const decoded = op === "write" ? decodeGraph(payload) : decodeQuery(payload);
142
+ if (!await auth(op, decoded, safeOptions)) {
143
+ ws.send(JSON.stringify([id, "unauthorized"]));
144
+ return;
145
+ }
146
+ }
121
147
  switch (op) {
122
148
  case "read":
123
149
  case "write":
124
150
  try {
125
- const result = await store.call(op, payload, options);
151
+ const result = await store.call(op, payload, safeOptions);
126
152
  ws.send(JSON.stringify([id, null, pack(result)]));
127
153
  } catch (e) {
128
154
  log(`${op}error:${e.message} ${payload}`);
@@ -132,7 +158,7 @@ function server(store) {
132
158
  case "watch":
133
159
  try {
134
160
  const stream = store.call("watch", payload, {
135
- ...options,
161
+ ...safeOptions,
136
162
  raw: true
137
163
  });
138
164
  ws.graffyStreams[id] = stream;
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.7",
5
+ "version": "0.17.8-alpha.2",
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.7",
20
- "ws": "^8.18.2",
21
- "debug": "^4.4.1"
19
+ "@graffy/common": "0.17.8-alpha.2",
20
+ "debug": "^4.4.1",
21
+ "ws": "^8.18.2"
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 +1,19 @@
1
- export default function server(store: any): (request: any, socket: any, head: any) => Promise<void>;
1
+ /**
2
+ * @typedef {import('@graffy/core').default} GraffyStore
3
+ * @param {GraffyStore} store
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).
13
+ * @returns
14
+ */
15
+ export default function server(store: GraffyStore, { auth, allowedOptions }?: {
16
+ auth?: (operation: string, payload: any, options: any) => Promise<boolean>;
17
+ allowedOptions?: string[];
18
+ }): (request: any, socket: any, head: any) => Promise<void>;
19
+ export type GraffyStore = import("@graffy/core").default;