@rayfold/server 0.1.0

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.
Files changed (73) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +70 -0
  4. package/args.d.ts +15 -0
  5. package/args.js +226 -0
  6. package/args.js.map +1 -0
  7. package/batch.d.ts +54 -0
  8. package/batch.js +483 -0
  9. package/batch.js.map +1 -0
  10. package/bindings.d.ts +37 -0
  11. package/bindings.js +284 -0
  12. package/bindings.js.map +1 -0
  13. package/capability-scope.d.ts +8 -0
  14. package/capability-scope.js +19 -0
  15. package/capability-scope.js.map +1 -0
  16. package/capability.d.ts +56 -0
  17. package/capability.js +112 -0
  18. package/capability.js.map +1 -0
  19. package/context.d.ts +74 -0
  20. package/context.js +109 -0
  21. package/context.js.map +1 -0
  22. package/core.d.ts +18 -0
  23. package/core.js +18 -0
  24. package/core.js.map +1 -0
  25. package/cost.d.ts +15 -0
  26. package/cost.js +110 -0
  27. package/cost.js.map +1 -0
  28. package/executor.d.ts +89 -0
  29. package/executor.js +695 -0
  30. package/executor.js.map +1 -0
  31. package/guard.d.ts +33 -0
  32. package/guard.js +65 -0
  33. package/guard.js.map +1 -0
  34. package/http.d.ts +33 -0
  35. package/http.js +379 -0
  36. package/http.js.map +1 -0
  37. package/index.d.ts +8 -0
  38. package/index.js +9 -0
  39. package/index.js.map +1 -0
  40. package/instrumentation.d.ts +36 -0
  41. package/instrumentation.js +2 -0
  42. package/instrumentation.js.map +1 -0
  43. package/live.d.ts +37 -0
  44. package/live.js +240 -0
  45. package/live.js.map +1 -0
  46. package/mcp.d.ts +55 -0
  47. package/mcp.js +314 -0
  48. package/mcp.js.map +1 -0
  49. package/openapi.d.ts +11 -0
  50. package/openapi.js +124 -0
  51. package/openapi.js.map +1 -0
  52. package/package.json +53 -0
  53. package/policy.d.ts +17 -0
  54. package/policy.js +64 -0
  55. package/policy.js.map +1 -0
  56. package/protocol.d.ts +139 -0
  57. package/protocol.js +98 -0
  58. package/protocol.js.map +1 -0
  59. package/server.d.ts +58 -0
  60. package/server.js +79 -0
  61. package/server.js.map +1 -0
  62. package/usage.d.ts +39 -0
  63. package/usage.js +44 -0
  64. package/usage.js.map +1 -0
  65. package/views.d.ts +33 -0
  66. package/views.js +108 -0
  67. package/views.js.map +1 -0
  68. package/wiring.d.ts +16 -0
  69. package/wiring.js +57 -0
  70. package/wiring.js.map +1 -0
  71. package/ws.d.ts +21 -0
  72. package/ws.js +209 -0
  73. package/ws.js.map +1 -0
package/ws.js ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * WebSocket transport (spec 04 §5) with a dependency-free RFC 6455 server side.
3
+ * Text messages carry JSON envelopes/frames and binary messages carry RB (spec 09 §4); a batch is answered in the form
4
+ * it came in. `{ "cancel": id }` stops an op; `{ "id", "item" }` and `{ "id", "fin": true }` feed bidirectional
5
+ * streams (reserved for the `@input` extension).
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { RayfoldError } from "./protocol.js";
9
+ import { hostProblem, originProblem } from "./guard.js";
10
+ import { codecFor } from "./http.js";
11
+ const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
12
+ export const SUBPROTOCOL = "rayfold.0.1";
13
+ /** Attach the Rayfold WebSocket endpoint to a Node HTTP server. */
14
+ export function attachWebSocket(http, server, opts = {}) {
15
+ const path = opts.path ?? "/rayfold/ws";
16
+ http.on("upgrade", (req, socket, head) => {
17
+ const url = new URL(req.url ?? "/", "http://localhost");
18
+ if (url.pathname !== path || (req.headers.upgrade ?? "").toLowerCase() !== "websocket") {
19
+ socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
20
+ socket.destroy();
21
+ return;
22
+ }
23
+ // Browsers send the page's Origin on the handshake and attach cookies: without this check any site could open a socket as the user.
24
+ const refused = hostProblem(req, opts) ?? originProblem(req, opts);
25
+ if (refused) {
26
+ socket.end(`HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\n${refused}`, () => socket.destroy()); // flush the refusal, then close fully
27
+ return;
28
+ }
29
+ const key = req.headers["sec-websocket-key"];
30
+ if (typeof key !== "string") {
31
+ socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
32
+ socket.destroy();
33
+ return;
34
+ }
35
+ const accept = createHash("sha1").update(key + GUID).digest("base64");
36
+ const proto = (req.headers["sec-websocket-protocol"] ?? "").split(",").map((s) => s.trim()).includes(SUBPROTOCOL) ? `Sec-WebSocket-Protocol: ${SUBPROTOCOL}\r\n` : "";
37
+ socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n${proto}\r\n`);
38
+ void handleConnection(socket, head, req, server, opts);
39
+ });
40
+ }
41
+ async function handleConnection(socket, head, req, server, opts) {
42
+ const viewer = opts.viewer ? await opts.viewer(req) : null;
43
+ const ops = new Map();
44
+ // text frames for a JSON batch; for an RB batch, binary messages of one length-prefixed RB frame each
45
+ const send = (obj, binary = false) => socket.write(binary ? encodeFrame(Buffer.from(codecFor(server).encodeFrames([obj])), 0x2) : encodeFrame(Buffer.from(JSON.stringify(obj), "utf8"), 0x1));
46
+ const close = () => {
47
+ for (const ac of ops.values())
48
+ ac.abort(new RayfoldError("canceled", "Canceled"));
49
+ ops.clear();
50
+ socket.end();
51
+ };
52
+ const onMessage = (payload, binary) => {
53
+ let msg;
54
+ try {
55
+ msg = binary ? codecFor(server).decode(new Uint8Array(payload)) : JSON.parse(payload.toString("utf8"));
56
+ }
57
+ catch {
58
+ send({ error: { code: "invalid_argument", message: binary ? "Message is not valid RB" : "Message is not valid JSON" }, fin: true }, binary);
59
+ return;
60
+ }
61
+ // null, a number or a list is not a message; reading a member of null would throw out of the socket's data handler
62
+ const m = (msg !== null && typeof msg === "object" ? msg : {});
63
+ if (typeof m["cancel"] === "number") {
64
+ const ac = ops.get(m["cancel"]);
65
+ if (ac)
66
+ ac.abort(new RayfoldError("canceled", "Canceled"));
67
+ return;
68
+ }
69
+ if (Array.isArray(m["ops"])) {
70
+ const env = m;
71
+ const ac = new AbortController();
72
+ for (const o of env.ops) {
73
+ if (ops.has(o?.id)) {
74
+ send({ error: { code: "invalid_argument", message: `op id ${o.id} is already in use on this connection` }, fin: true }, binary);
75
+ return;
76
+ }
77
+ }
78
+ for (const o of env.ops)
79
+ ops.set(o.id, ac);
80
+ void (async () => {
81
+ try {
82
+ for await (const f of server.execute(env, { viewer, signal: ac.signal })) {
83
+ send(f, binary);
84
+ if ("id" in f && "fin" in f && f.fin)
85
+ ops.delete(f.id);
86
+ }
87
+ }
88
+ finally {
89
+ for (const o of env.ops)
90
+ if (ops.get(o.id) === ac)
91
+ ops.delete(o.id);
92
+ }
93
+ })();
94
+ return;
95
+ }
96
+ send({ error: { code: "invalid_argument", message: "Expected a batch envelope, {cancel}, or a stream item" }, fin: true }, binary);
97
+ };
98
+ let buf = head.length ? Buffer.from(head) : Buffer.alloc(0);
99
+ let fragments = [];
100
+ let assembled = 0;
101
+ let messageBinary = false;
102
+ const maxMessage = opts.maxMessage ?? 1_048_576;
103
+ let refused = false;
104
+ const tooBig = () => {
105
+ refused = true;
106
+ buf = Buffer.alloc(0);
107
+ fragments = [];
108
+ for (const ac of ops.values())
109
+ ac.abort(new RayfoldError("canceled", "Canceled"));
110
+ ops.clear();
111
+ // close code 1009 (message too big); flushed before the socket is closed
112
+ socket.end(encodeFrame(Buffer.concat([Buffer.from([0x03, 0xf1]), Buffer.from("message too big")]), 0x8), () => socket.destroy());
113
+ };
114
+ socket.on("data", (chunk) => {
115
+ if (refused)
116
+ return;
117
+ buf = Buffer.concat([buf, chunk]);
118
+ if (buf.length > maxMessage + 14)
119
+ return tooBig(); // a frame larger than the limit is never buffered to completion
120
+ for (;;) {
121
+ const parsed = decodeFrame(buf);
122
+ if (!parsed)
123
+ break;
124
+ buf = buf.subarray(parsed.length);
125
+ const { opcode, fin, payload } = parsed;
126
+ if (opcode === 0x8) {
127
+ socket.write(encodeFrame(Buffer.alloc(0), 0x8));
128
+ close();
129
+ return;
130
+ }
131
+ if (opcode === 0x9) {
132
+ socket.write(encodeFrame(payload, 0xa));
133
+ continue;
134
+ }
135
+ if (opcode === 0xa)
136
+ continue;
137
+ if (opcode === 0x1 || opcode === 0x2)
138
+ messageBinary = opcode === 0x2; // continuation frames keep the first frame's type
139
+ assembled += payload.length;
140
+ if (assembled > maxMessage)
141
+ return tooBig();
142
+ fragments.push(payload);
143
+ if (fin) {
144
+ const message = Buffer.concat(fragments);
145
+ fragments = [];
146
+ assembled = 0;
147
+ onMessage(message, messageBinary);
148
+ }
149
+ }
150
+ });
151
+ // Node keeps server sockets half-open: a client that drops without a close frame only sends FIN ("end").
152
+ // Closing on "end" too releases the socket and its live subscriptions instead of leaking them.
153
+ socket.on("end", close);
154
+ socket.on("close", close);
155
+ socket.on("error", close);
156
+ }
157
+ export function encodeFrame(payload, opcode) {
158
+ const len = payload.length;
159
+ let header;
160
+ if (len < 126)
161
+ header = Buffer.from([0x80 | opcode, len]);
162
+ else if (len < 65536) {
163
+ header = Buffer.alloc(4);
164
+ header[0] = 0x80 | opcode;
165
+ header[1] = 126;
166
+ header.writeUInt16BE(len, 2);
167
+ }
168
+ else {
169
+ header = Buffer.alloc(10);
170
+ header[0] = 0x80 | opcode;
171
+ header[1] = 127;
172
+ header.writeBigUInt64BE(BigInt(len), 2);
173
+ }
174
+ return Buffer.concat([header, payload]);
175
+ }
176
+ export function decodeFrame(buf) {
177
+ if (buf.length < 2)
178
+ return null;
179
+ const b0 = buf[0];
180
+ const b1 = buf[1];
181
+ const fin = (b0 & 0x80) !== 0;
182
+ const opcode = b0 & 0x0f;
183
+ const masked = (b1 & 0x80) !== 0;
184
+ let len = b1 & 0x7f;
185
+ let off = 2;
186
+ if (len === 126) {
187
+ if (buf.length < 4)
188
+ return null;
189
+ len = buf.readUInt16BE(2);
190
+ off = 4;
191
+ }
192
+ else if (len === 127) {
193
+ if (buf.length < 10)
194
+ return null;
195
+ len = Number(buf.readBigUInt64BE(2));
196
+ off = 10;
197
+ }
198
+ const maskKey = masked ? buf.subarray(off, off + 4) : null;
199
+ if (masked)
200
+ off += 4;
201
+ if (buf.length < off + len)
202
+ return null;
203
+ const payload = Buffer.from(buf.subarray(off, off + len));
204
+ if (maskKey)
205
+ for (let i = 0; i < payload.length; i++)
206
+ payload[i] = payload[i] ^ maskKey[i % 4];
207
+ return { opcode, fin, payload, length: off + len };
208
+ }
209
+ //# sourceMappingURL=ws.js.map
package/ws.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws.js","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WebSocket transport (spec 04 §5) with a dependency-free RFC 6455 server side.\n * Text messages carry JSON envelopes/frames and binary messages carry RB (spec 09 §4); a batch is answered in the form\n * it came in. `{ \"cancel\": id }` stops an op; `{ \"id\", \"item\" }` and `{ \"id\", \"fin\": true }` feed bidirectional\n * streams (reserved for the `@input` extension).\n */\nimport { createHash } from \"node:crypto\";\nimport type { IncomingMessage, Server } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport type { RayfoldServer } from \"./server.ts\";\nimport { RayfoldError, type Frame, type RequestEnvelope } from \"./protocol.ts\";\nimport { hostProblem, originProblem, type OriginOptions } from \"./guard.ts\";\nimport { codecFor } from \"./http.ts\";\n\nconst GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\";\nexport const SUBPROTOCOL = \"rayfold.0.1\";\n\nexport interface WsOptions extends OriginOptions {\n /** Largest frame or assembled message accepted, in bytes. Default 1 MiB. */\n maxMessage?: number;\n path?: string;\n viewer?: (req: IncomingMessage) => unknown | Promise<unknown>;\n}\n\n/** Attach the Rayfold WebSocket endpoint to a Node HTTP server. */\nexport function attachWebSocket(http: Server, server: RayfoldServer, opts: WsOptions = {}): void {\n const path = opts.path ?? \"/rayfold/ws\";\n http.on(\"upgrade\", (req, socket, head) => {\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n if (url.pathname !== path || (req.headers.upgrade ?? \"\").toLowerCase() !== \"websocket\") {\n socket.write(\"HTTP/1.1 404 Not Found\\r\\nConnection: close\\r\\n\\r\\n\");\n socket.destroy();\n return;\n }\n // Browsers send the page's Origin on the handshake and attach cookies: without this check any site could open a socket as the user.\n const refused = hostProblem(req, opts) ?? originProblem(req, opts);\n if (refused) {\n socket.end(`HTTP/1.1 403 Forbidden\\r\\nConnection: close\\r\\nContent-Type: text/plain\\r\\n\\r\\n${refused}`, () => socket.destroy()); // flush the refusal, then close fully\n return;\n }\n const key = req.headers[\"sec-websocket-key\"];\n if (typeof key !== \"string\") {\n socket.write(\"HTTP/1.1 400 Bad Request\\r\\nConnection: close\\r\\n\\r\\n\");\n socket.destroy();\n return;\n }\n const accept = createHash(\"sha1\").update(key + GUID).digest(\"base64\");\n const proto = (req.headers[\"sec-websocket-protocol\"] ?? \"\").split(\",\").map((s) => s.trim()).includes(SUBPROTOCOL) ? `Sec-WebSocket-Protocol: ${SUBPROTOCOL}\\r\\n` : \"\";\n socket.write(`HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ${accept}\\r\\n${proto}\\r\\n`);\n void handleConnection(socket, head, req, server, opts);\n });\n}\n\nasync function handleConnection(socket: Duplex, head: Buffer, req: IncomingMessage, server: RayfoldServer, opts: WsOptions): Promise<void> {\n const viewer = opts.viewer ? await opts.viewer(req) : null;\n const ops = new Map<number, AbortController>();\n // text frames for a JSON batch; for an RB batch, binary messages of one length-prefixed RB frame each\n const send = (obj: unknown, binary = false) =>\n socket.write(binary ? encodeFrame(Buffer.from(codecFor(server).encodeFrames([obj])), 0x2) : encodeFrame(Buffer.from(JSON.stringify(obj), \"utf8\"), 0x1));\n const close = () => {\n for (const ac of ops.values()) ac.abort(new RayfoldError(\"canceled\", \"Canceled\"));\n ops.clear();\n socket.end();\n };\n const onMessage = (payload: Buffer, binary: boolean) => {\n let msg: unknown;\n try {\n msg = binary ? codecFor(server).decode(new Uint8Array(payload)) : JSON.parse(payload.toString(\"utf8\"));\n } catch {\n send({ error: { code: \"invalid_argument\", message: binary ? \"Message is not valid RB\" : \"Message is not valid JSON\" }, fin: true }, binary);\n return;\n }\n // null, a number or a list is not a message; reading a member of null would throw out of the socket's data handler\n const m = (msg !== null && typeof msg === \"object\" ? msg : {}) as Record<string, unknown>;\n if (typeof m[\"cancel\"] === \"number\") {\n const ac = ops.get(m[\"cancel\"]);\n if (ac) ac.abort(new RayfoldError(\"canceled\", \"Canceled\"));\n return;\n }\n if (Array.isArray(m[\"ops\"])) {\n const env = m as unknown as RequestEnvelope;\n const ac = new AbortController();\n for (const o of env.ops) {\n if (ops.has(o?.id)) {\n send({ error: { code: \"invalid_argument\", message: `op id ${o.id} is already in use on this connection` }, fin: true }, binary);\n return;\n }\n }\n for (const o of env.ops) ops.set(o.id, ac);\n void (async () => {\n try {\n for await (const f of server.execute(env, { viewer, signal: ac.signal })) {\n send(f, binary);\n if (\"id\" in f && \"fin\" in f && f.fin) ops.delete(f.id);\n }\n } finally {\n for (const o of env.ops) if (ops.get(o.id) === ac) ops.delete(o.id);\n }\n })();\n return;\n }\n send({ error: { code: \"invalid_argument\", message: \"Expected a batch envelope, {cancel}, or a stream item\" }, fin: true }, binary);\n };\n\n let buf = head.length ? Buffer.from(head) : Buffer.alloc(0);\n let fragments: Buffer[] = [];\n let assembled = 0;\n let messageBinary = false;\n const maxMessage = opts.maxMessage ?? 1_048_576;\n let refused = false;\n const tooBig = () => {\n refused = true;\n buf = Buffer.alloc(0);\n fragments = [];\n for (const ac of ops.values()) ac.abort(new RayfoldError(\"canceled\", \"Canceled\"));\n ops.clear();\n // close code 1009 (message too big); flushed before the socket is closed\n socket.end(encodeFrame(Buffer.concat([Buffer.from([0x03, 0xf1]), Buffer.from(\"message too big\")]), 0x8), () => socket.destroy());\n };\n socket.on(\"data\", (chunk: Buffer) => {\n if (refused) return;\n buf = Buffer.concat([buf, chunk]);\n if (buf.length > maxMessage + 14) return tooBig(); // a frame larger than the limit is never buffered to completion\n for (;;) {\n const parsed = decodeFrame(buf);\n if (!parsed) break;\n buf = buf.subarray(parsed.length);\n const { opcode, fin, payload } = parsed;\n if (opcode === 0x8) {\n socket.write(encodeFrame(Buffer.alloc(0), 0x8));\n close();\n return;\n }\n if (opcode === 0x9) {\n socket.write(encodeFrame(payload, 0xa));\n continue;\n }\n if (opcode === 0xa) continue;\n if (opcode === 0x1 || opcode === 0x2) messageBinary = opcode === 0x2; // continuation frames keep the first frame's type\n assembled += payload.length;\n if (assembled > maxMessage) return tooBig();\n fragments.push(payload);\n if (fin) {\n const message = Buffer.concat(fragments);\n fragments = [];\n assembled = 0;\n onMessage(message, messageBinary);\n }\n }\n });\n // Node keeps server sockets half-open: a client that drops without a close frame only sends FIN (\"end\").\n // Closing on \"end\" too releases the socket and its live subscriptions instead of leaking them.\n socket.on(\"end\", close);\n socket.on(\"close\", close);\n socket.on(\"error\", close);\n}\n\nexport function encodeFrame(payload: Buffer, opcode: number): Buffer {\n const len = payload.length;\n let header: Buffer;\n if (len < 126) header = Buffer.from([0x80 | opcode, len]);\n else if (len < 65536) {\n header = Buffer.alloc(4);\n header[0] = 0x80 | opcode;\n header[1] = 126;\n header.writeUInt16BE(len, 2);\n } else {\n header = Buffer.alloc(10);\n header[0] = 0x80 | opcode;\n header[1] = 127;\n header.writeBigUInt64BE(BigInt(len), 2);\n }\n return Buffer.concat([header, payload]);\n}\n\nexport function decodeFrame(buf: Buffer): { opcode: number; fin: boolean; payload: Buffer; length: number } | null {\n if (buf.length < 2) return null;\n const b0 = buf[0]!;\n const b1 = buf[1]!;\n const fin = (b0 & 0x80) !== 0;\n const opcode = b0 & 0x0f;\n const masked = (b1 & 0x80) !== 0;\n let len = b1 & 0x7f;\n let off = 2;\n if (len === 126) {\n if (buf.length < 4) return null;\n len = buf.readUInt16BE(2);\n off = 4;\n } else if (len === 127) {\n if (buf.length < 10) return null;\n len = Number(buf.readBigUInt64BE(2));\n off = 10;\n }\n const maskKey = masked ? buf.subarray(off, off + 4) : null;\n if (masked) off += 4;\n if (buf.length < off + len) return null;\n const payload = Buffer.from(buf.subarray(off, off + len));\n if (maskKey) for (let i = 0; i < payload.length; i++) payload[i] = payload[i]! ^ maskKey[i % 4]!;\n return { opcode, fin, payload, length: off + len };\n}\n\nexport type { Frame };\n"]}