@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 +35 -9
- package/index.mjs +36 -10
- package/package.json +4 -4
- package/types/httpServer.d.ts +12 -5
- package/types/wsServer.d.ts +19 -1
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
|
|
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
|
-
...
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
...
|
|
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.
|
|
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.
|
|
20
|
-
"
|
|
21
|
-
"
|
|
19
|
+
"@graffy/common": "0.17.8-alpha.2",
|
|
20
|
+
"debug": "^4.4.1",
|
|
21
|
+
"ws": "^8.18.2"
|
|
22
22
|
}
|
|
23
23
|
}
|
package/types/httpServer.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {import('@graffy/core').default} GraffyStore
|
|
3
3
|
* @param {GraffyStore} store
|
|
4
|
-
* @param {
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
17
|
+
allowedOptions?: string[];
|
|
18
|
+
}): (req: any, res: any) => Promise<void>;
|
|
12
19
|
export type GraffyStore = import("@graffy/core").default;
|
package/types/wsServer.d.ts
CHANGED
|
@@ -1 +1,19 @@
|
|
|
1
|
-
|
|
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;
|