@meekojs/meeko 0.0.3 → 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.
- package/README.md +35 -16
- package/dist/autoimports-ts.d.ts +26 -0
- package/dist/autoimports-ts.js +183 -0
- package/dist/autoimports-ts.js.map +1 -0
- package/dist/autoimports.d.ts +21 -0
- package/dist/autoimports.js +301 -0
- package/dist/autoimports.js.map +1 -0
- package/dist/check.js +74 -33
- package/dist/check.js.map +1 -0
- package/dist/cli.js +67 -5
- package/dist/cli.js.map +1 -0
- package/dist/compiler/events.d.ts +2 -0
- package/dist/compiler/events.js +6 -0
- package/dist/compiler/events.js.map +1 -0
- package/dist/compiler/import-graph.d.ts +8 -0
- package/dist/compiler/import-graph.js +33 -0
- package/dist/compiler/import-graph.js.map +1 -0
- package/dist/compiler/mapping.js +1 -0
- package/dist/compiler/mapping.js.map +1 -0
- package/dist/compiler/project-root.d.ts +2 -0
- package/dist/compiler/project-root.js +18 -0
- package/dist/compiler/project-root.js.map +1 -0
- package/dist/compiler/props.d.ts +12 -0
- package/dist/compiler/props.js +32 -0
- package/dist/compiler/props.js.map +1 -0
- package/dist/compiler/sfc.js +1 -0
- package/dist/compiler/sfc.js.map +1 -0
- package/dist/compiler/sourcemap.d.ts +3 -0
- package/dist/compiler/sourcemap.js +75 -0
- package/dist/compiler/sourcemap.js.map +1 -0
- package/dist/compiler/template.js +1 -0
- package/dist/compiler/template.js.map +1 -0
- package/dist/compiler.js +66 -17
- package/dist/compiler.js.map +1 -0
- package/dist/component-errors.d.ts +18 -0
- package/dist/component-errors.js +65 -0
- package/dist/component-errors.js.map +1 -0
- package/dist/devtools-ui/index.js +1 -0
- package/dist/devtools-ui/index.js.map +1 -0
- package/dist/devtools.js +3 -4
- package/dist/devtools.js.map +1 -0
- package/dist/dom.d.ts +18 -5
- package/dist/dom.js +295 -90
- package/dist/dom.js.map +1 -0
- package/dist/error-observer.d.ts +6 -0
- package/dist/error-observer.js +41 -0
- package/dist/error-observer.js.map +1 -0
- package/dist/framework/check-app.d.ts +5 -0
- package/dist/framework/check-app.js +214 -0
- package/dist/framework/check-app.js.map +1 -0
- package/dist/framework/check-project.d.ts +4 -0
- package/dist/framework/check-project.js +42 -0
- package/dist/framework/check-project.js.map +1 -0
- package/dist/framework/client.d.ts +48 -0
- package/dist/framework/client.js +249 -0
- package/dist/framework/client.js.map +1 -0
- package/dist/framework/clients.d.ts +82 -0
- package/dist/framework/clients.js +190 -0
- package/dist/framework/clients.js.map +1 -0
- package/dist/framework/config-type.d.ts +10 -0
- package/dist/framework/config-type.js +4 -0
- package/dist/framework/config-type.js.map +1 -0
- package/dist/framework/config.d.ts +15 -0
- package/dist/framework/config.js +138 -0
- package/dist/framework/config.js.map +1 -0
- package/dist/framework/contracts-generate.d.ts +8 -0
- package/dist/framework/contracts-generate.js +290 -0
- package/dist/framework/contracts-generate.js.map +1 -0
- package/dist/framework/generate.d.ts +7 -0
- package/dist/framework/generate.js +67 -0
- package/dist/framework/generate.js.map +1 -0
- package/dist/framework/init.d.ts +2 -0
- package/dist/framework/init.js +183 -0
- package/dist/framework/init.js.map +1 -0
- package/dist/framework/middleware.d.ts +14 -0
- package/dist/framework/middleware.js +16 -0
- package/dist/framework/middleware.js.map +1 -0
- package/dist/framework/modules.d.ts +22 -0
- package/dist/framework/modules.js +29 -0
- package/dist/framework/modules.js.map +1 -0
- package/dist/framework/page-contracts-check.d.ts +8 -0
- package/dist/framework/page-contracts-check.js +110 -0
- package/dist/framework/page-contracts-check.js.map +1 -0
- package/dist/framework/page-contracts-generate.d.ts +2 -0
- package/dist/framework/page-contracts-generate.js +94 -0
- package/dist/framework/page-contracts-generate.js.map +1 -0
- package/dist/framework/page.d.ts +64 -0
- package/dist/framework/page.js +44 -0
- package/dist/framework/page.js.map +1 -0
- package/dist/framework/prerender.d.ts +14 -0
- package/dist/framework/prerender.js +110 -0
- package/dist/framework/prerender.js.map +1 -0
- package/dist/framework/project-templates.d.ts +3 -0
- package/dist/framework/project-templates.js +119 -0
- package/dist/framework/project-templates.js.map +1 -0
- package/dist/framework/project.d.ts +9 -0
- package/dist/framework/project.js +272 -0
- package/dist/framework/project.js.map +1 -0
- package/dist/framework/router.d.ts +41 -0
- package/dist/framework/router.js +259 -0
- package/dist/framework/router.js.map +1 -0
- package/dist/framework/server.d.ts +65 -0
- package/dist/framework/server.js +239 -0
- package/dist/framework/server.js.map +1 -0
- package/dist/framework/socket-contracts-generate.d.ts +5 -0
- package/dist/framework/socket-contracts-generate.js +162 -0
- package/dist/framework/socket-contracts-generate.js.map +1 -0
- package/dist/framework/socket.d.ts +72 -0
- package/dist/framework/socket.js +252 -0
- package/dist/framework/socket.js.map +1 -0
- package/dist/framework/standard-schema-types.d.ts +7 -0
- package/dist/framework/standard-schema-types.js +69 -0
- package/dist/framework/standard-schema-types.js.map +1 -0
- package/dist/framework/state.d.ts +22 -0
- package/dist/framework/state.js +136 -0
- package/dist/framework/state.js.map +1 -0
- package/dist/framework/styles.d.ts +2 -0
- package/dist/framework/styles.js +6 -0
- package/dist/framework/styles.js.map +1 -0
- package/dist/framework/type-declarations.d.ts +5 -0
- package/dist/framework/type-declarations.js +84 -0
- package/dist/framework/type-declarations.js.map +1 -0
- package/dist/framework-server/auto-clients.d.ts +3 -0
- package/dist/framework-server/auto-clients.js +27 -0
- package/dist/framework-server/auto-clients.js.map +1 -0
- package/dist/framework-server/context.d.ts +21 -0
- package/dist/framework-server/context.js +22 -0
- package/dist/framework-server/context.js.map +1 -0
- package/dist/framework-server/contracts.d.ts +73 -0
- package/dist/framework-server/contracts.js +184 -0
- package/dist/framework-server/contracts.js.map +1 -0
- package/dist/framework-server/discovery.d.ts +43 -0
- package/dist/framework-server/discovery.js +257 -0
- package/dist/framework-server/discovery.js.map +1 -0
- package/dist/framework-server/local.d.ts +21 -0
- package/dist/framework-server/local.js +100 -0
- package/dist/framework-server/local.js.map +1 -0
- package/dist/framework-server/middleware.d.ts +4 -0
- package/dist/framework-server/middleware.js +56 -0
- package/dist/framework-server/middleware.js.map +1 -0
- package/dist/framework-server/node-upgrade.d.ts +23 -0
- package/dist/framework-server/node-upgrade.js +96 -0
- package/dist/framework-server/node-upgrade.js.map +1 -0
- package/dist/framework-server/node-websocket.d.ts +16 -0
- package/dist/framework-server/node-websocket.js +127 -0
- package/dist/framework-server/node-websocket.js.map +1 -0
- package/dist/framework-server/openapi.d.ts +17 -0
- package/dist/framework-server/openapi.js +118 -0
- package/dist/framework-server/openapi.js.map +1 -0
- package/dist/framework-server/session-stores.d.ts +73 -0
- package/dist/framework-server/session-stores.js +122 -0
- package/dist/framework-server/session-stores.js.map +1 -0
- package/dist/framework-server/sessions.d.ts +70 -0
- package/dist/framework-server/sessions.js +167 -0
- package/dist/framework-server/sessions.js.map +1 -0
- package/dist/framework-server/socket.d.ts +83 -0
- package/dist/framework-server/socket.js +196 -0
- package/dist/framework-server/socket.js.map +1 -0
- package/dist/framework-server.d.ts +23 -0
- package/dist/framework-server.js +13 -0
- package/dist/framework-server.js.map +1 -0
- package/dist/framework.d.ts +31 -0
- package/dist/framework.js +14 -0
- package/dist/framework.js.map +1 -0
- package/dist/hmr.js +75 -24
- package/dist/hmr.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -0
- package/dist/init.js +31 -3
- package/dist/init.js.map +1 -0
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +3 -2
- package/dist/internal.js.map +1 -0
- package/dist/node.d.ts +16 -0
- package/dist/node.js +122 -0
- package/dist/node.js.map +1 -0
- package/dist/reactivity.d.ts +22 -1
- package/dist/reactivity.js +132 -18
- package/dist/reactivity.js.map +1 -0
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +26 -9
- package/dist/types.js.map +1 -0
- package/dist/vite.js +112 -7
- package/dist/vite.js.map +1 -0
- package/package.json +15 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
3
|
+
import type { SocketHub, SocketPeer } from "./socket.js";
|
|
4
|
+
/** A WebSocket server library owns framing and the HTTP 101 response. */
|
|
5
|
+
export interface NodeAcceptedSocket {
|
|
6
|
+
peer: SocketPeer;
|
|
7
|
+
onMessage(listener: (message: string | Uint8Array) => void): void;
|
|
8
|
+
onClose(listener: () => void): void;
|
|
9
|
+
}
|
|
10
|
+
export interface NodeSocketUpgradeOptions {
|
|
11
|
+
hub: SocketHub;
|
|
12
|
+
/** Fixed public origin; never infer security policy from an untrusted Host header. */
|
|
13
|
+
publicOrigin: string;
|
|
14
|
+
pathnamePrefix?: string;
|
|
15
|
+
upgrade(incoming: IncomingMessage, socket: Duplex, head: Buffer): Promise<NodeAcceptedSocket> | NodeAcceptedSocket;
|
|
16
|
+
reject?(incoming: IncomingMessage, socket: Duplex, status: number): void;
|
|
17
|
+
onError?(error: unknown): void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Handles the Node `upgrade` event after policy preflight. The host supplies a
|
|
21
|
+
* WebSocket implementation; this adapter does not implement wire framing.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createNodeSocketUpgradeHandler(options: NodeSocketUpgradeOptions): (incoming: IncomingMessage, socket: Duplex, head: Buffer) => Promise<boolean>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { normalizeServerError, observeErrorCallback } from "./contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handles the Node `upgrade` event after policy preflight. The host supplies a
|
|
4
|
+
* WebSocket implementation; this adapter does not implement wire framing.
|
|
5
|
+
*/
|
|
6
|
+
export function createNodeSocketUpgradeHandler(options) {
|
|
7
|
+
const origin = new URL(options.publicOrigin);
|
|
8
|
+
if (origin.protocol !== "http:" && origin.protocol !== "https:")
|
|
9
|
+
throw new TypeError("Socket publicOrigin must use HTTP or HTTPS");
|
|
10
|
+
const prefix = options.pathnamePrefix ?? "/_meeko/socket";
|
|
11
|
+
if (!prefix.startsWith("/") || prefix.endsWith("/"))
|
|
12
|
+
throw new TypeError("Invalid socket pathname prefix");
|
|
13
|
+
const observeError = (error) => observeErrorCallback(error, options.onError);
|
|
14
|
+
return async (incoming, socket, head) => {
|
|
15
|
+
let url;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(incoming.url ?? "", origin);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (!url.pathname.startsWith(`${prefix}/`))
|
|
23
|
+
return false;
|
|
24
|
+
const reject = (status) => { if (options.reject)
|
|
25
|
+
options.reject(incoming, socket, status);
|
|
26
|
+
else
|
|
27
|
+
socket.destroy(); };
|
|
28
|
+
const name = url.pathname.slice(prefix.length + 1);
|
|
29
|
+
if (url.origin !== origin.origin || !/^[A-Za-z_$][\w$-]*$/.test(name)) {
|
|
30
|
+
reject(400);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (incoming.method !== "GET" || incoming.headers.upgrade?.toLowerCase() !== "websocket") {
|
|
34
|
+
reject(400);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const headers = new Headers();
|
|
38
|
+
for (const [key, value] of Object.entries(incoming.headers)) {
|
|
39
|
+
if (value !== undefined)
|
|
40
|
+
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
|
41
|
+
}
|
|
42
|
+
const request = new Request(url, { headers });
|
|
43
|
+
const roomId = url.searchParams.get("room") ?? undefined;
|
|
44
|
+
try {
|
|
45
|
+
await options.hub.preflight(name, request, roomId);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
reject(normalizeServerError(error).status);
|
|
49
|
+
observeError(error);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
let accepted;
|
|
53
|
+
try {
|
|
54
|
+
accepted = await options.upgrade(incoming, socket, head);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
socket.destroy();
|
|
58
|
+
observeError(error);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const connection = await options.hub.accept(name, request, accepted.peer, roomId);
|
|
63
|
+
let delivery = Promise.resolve();
|
|
64
|
+
accepted.onMessage((message) => {
|
|
65
|
+
delivery = delivery.then(() => connection.receive(message)).catch((error) => {
|
|
66
|
+
observeError(error);
|
|
67
|
+
try {
|
|
68
|
+
connection.close(normalizeServerError(error).status < 500 ? 1008 : 1011);
|
|
69
|
+
}
|
|
70
|
+
catch (closeError) {
|
|
71
|
+
observeError(closeError);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
accepted.onClose(() => {
|
|
76
|
+
try {
|
|
77
|
+
connection.close();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
observeError(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
try {
|
|
86
|
+
accepted.peer.close(1008, "Unauthorized");
|
|
87
|
+
}
|
|
88
|
+
catch (closeError) {
|
|
89
|
+
observeError(closeError);
|
|
90
|
+
}
|
|
91
|
+
observeError(error);
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=node-upgrade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-upgrade.js","sourceRoot":"","sources":["../../src/framework-server/node-upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport type { SocketHub, SocketPeer } from \"./socket.js\";\nimport { normalizeServerError, observeErrorCallback } from \"./contracts.js\";\n\n/** A WebSocket server library owns framing and the HTTP 101 response. */\nexport interface NodeAcceptedSocket {\n peer: SocketPeer;\n onMessage(listener: (message: string | Uint8Array) => void): void;\n onClose(listener: () => void): void;\n}\n\nexport interface NodeSocketUpgradeOptions {\n hub: SocketHub;\n /** Fixed public origin; never infer security policy from an untrusted Host header. */\n publicOrigin: string;\n pathnamePrefix?: string;\n upgrade(incoming: IncomingMessage, socket: Duplex, head: Buffer): Promise<NodeAcceptedSocket> | NodeAcceptedSocket;\n reject?(incoming: IncomingMessage, socket: Duplex, status: number): void;\n onError?(error: unknown): void;\n}\n\n/**\n * Handles the Node `upgrade` event after policy preflight. The host supplies a\n * WebSocket implementation; this adapter does not implement wire framing.\n */\nexport function createNodeSocketUpgradeHandler(options: NodeSocketUpgradeOptions): (incoming: IncomingMessage, socket: Duplex, head: Buffer) => Promise<boolean> {\n const origin = new URL(options.publicOrigin);\n if (origin.protocol !== \"http:\" && origin.protocol !== \"https:\") throw new TypeError(\"Socket publicOrigin must use HTTP or HTTPS\");\n const prefix = options.pathnamePrefix ?? \"/_meeko/socket\";\n if (!prefix.startsWith(\"/\") || prefix.endsWith(\"/\")) throw new TypeError(\"Invalid socket pathname prefix\");\n const observeError = (error: unknown): void => observeErrorCallback(error, options.onError);\n return async (incoming, socket, head) => {\n let url: URL;\n try { url = new URL(incoming.url ?? \"\", origin); }\n catch { return false; }\n if (!url.pathname.startsWith(`${prefix}/`)) return false;\n const reject = (status: number) => { if (options.reject) options.reject(incoming, socket, status); else socket.destroy(); };\n const name = url.pathname.slice(prefix.length + 1);\n if (url.origin !== origin.origin || !/^[A-Za-z_$][\\w$-]*$/.test(name)) { reject(400); return true; }\n if (incoming.method !== \"GET\" || incoming.headers.upgrade?.toLowerCase() !== \"websocket\") { reject(400); return true; }\n const headers = new Headers();\n for (const [key, value] of Object.entries(incoming.headers)) {\n if (value !== undefined) headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n const request = new Request(url, { headers });\n const roomId = url.searchParams.get(\"room\") ?? undefined;\n try { await options.hub.preflight(name, request, roomId); }\n catch (error) { reject(normalizeServerError(error).status); observeError(error); return true; }\n let accepted: NodeAcceptedSocket;\n try { accepted = await options.upgrade(incoming, socket, head); }\n catch (error) { socket.destroy(); observeError(error); return true; }\n try {\n const connection = await options.hub.accept(name, request, accepted.peer, roomId);\n let delivery = Promise.resolve();\n accepted.onMessage((message) => {\n delivery = delivery.then(() => connection.receive(message)).catch((error: unknown) => {\n observeError(error);\n try { connection.close(normalizeServerError(error).status < 500 ? 1008 : 1011); }\n catch (closeError) { observeError(closeError); }\n });\n });\n accepted.onClose(() => {\n try { connection.close(); }\n catch (error) { observeError(error); }\n });\n } catch (error) {\n try { accepted.peer.close(1008, \"Unauthorized\"); }\n catch (closeError) { observeError(closeError); }\n observeError(error);\n }\n return true;\n };\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Server as HttpServer } from "node:http";
|
|
2
|
+
import type { NodeSocketUpgradeOptions } from "./node-upgrade.js";
|
|
3
|
+
export interface NodeWebSocketOptions extends Omit<NodeSocketUpgradeOptions, "upgrade" | "reject"> {
|
|
4
|
+
/** The same HTTP server used by the Fetch handler. */
|
|
5
|
+
server: HttpServer;
|
|
6
|
+
/** Maximum WebSocket frame payload, enforced before decoding. Defaults to 64 KiB. */
|
|
7
|
+
maxMessageBytes?: number;
|
|
8
|
+
/** Ping interval and idle timeout. Defaults to 30 seconds. */
|
|
9
|
+
heartbeatMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface NodeWebSocketServer {
|
|
12
|
+
/** Detaches the upgrade listener and closes all peers. The HTTP server remains owned by its caller. */
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** Attach Meeko channels to a Node HTTP server using ws for protocol framing. */
|
|
16
|
+
export declare function attachNodeWebSocketServer(options: NodeWebSocketOptions): NodeWebSocketServer;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { STATUS_CODES } from "node:http";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { createNodeSocketUpgradeHandler } from "./node-upgrade.js";
|
|
4
|
+
import { observeErrorCallback } from "./contracts.js";
|
|
5
|
+
/** Attach Meeko channels to a Node HTTP server using ws for protocol framing. */
|
|
6
|
+
export function attachNodeWebSocketServer(options) {
|
|
7
|
+
const maxPayload = options.maxMessageBytes ?? 64 * 1024;
|
|
8
|
+
const heartbeatMs = options.heartbeatMs ?? 30_000;
|
|
9
|
+
if (!Number.isSafeInteger(maxPayload) || maxPayload <= 0)
|
|
10
|
+
throw new RangeError("Invalid socket message limit");
|
|
11
|
+
if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs <= 0)
|
|
12
|
+
throw new RangeError("Invalid socket heartbeat interval");
|
|
13
|
+
// Keep the optional server transport out of engine-only and HTTP-only imports.
|
|
14
|
+
const { WebSocket: WebSocketClass, WebSocketServer } = createRequire(import.meta.url)("ws");
|
|
15
|
+
const webSockets = new WebSocketServer({ noServer: true, maxPayload, perMessageDeflate: false });
|
|
16
|
+
const observeError = (error) => observeErrorCallback(error, options.onError);
|
|
17
|
+
const alive = new WeakMap();
|
|
18
|
+
let closing = false;
|
|
19
|
+
let closePromise;
|
|
20
|
+
const upgrade = (incoming, socket, head) => new Promise((resolve, reject) => {
|
|
21
|
+
try {
|
|
22
|
+
webSockets.handleUpgrade(incoming, socket, head, (peer) => {
|
|
23
|
+
alive.set(peer, true);
|
|
24
|
+
peer.on("pong", () => alive.set(peer, true));
|
|
25
|
+
peer.on("error", observeError);
|
|
26
|
+
const pending = [];
|
|
27
|
+
let pendingBytes = 0;
|
|
28
|
+
let messageListener;
|
|
29
|
+
let closeListener;
|
|
30
|
+
let didClose = false;
|
|
31
|
+
peer.on("message", (data, isBinary) => {
|
|
32
|
+
if (isBinary) {
|
|
33
|
+
peer.close(1003, "Text messages required");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
37
|
+
if (messageListener) {
|
|
38
|
+
messageListener(bytes);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
pendingBytes += bytes.byteLength;
|
|
42
|
+
if (pendingBytes > maxPayload) {
|
|
43
|
+
peer.close(1009, "Message buffer exceeded");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
pending.push(bytes);
|
|
47
|
+
});
|
|
48
|
+
peer.once("close", () => { didClose = true; closeListener?.(); });
|
|
49
|
+
resolve({
|
|
50
|
+
peer: {
|
|
51
|
+
send(message) {
|
|
52
|
+
if (peer.readyState !== WebSocketClass.OPEN)
|
|
53
|
+
throw new Error("Socket is disconnected");
|
|
54
|
+
peer.send(message);
|
|
55
|
+
},
|
|
56
|
+
close(code, reason) { if (peer.readyState === WebSocketClass.OPEN)
|
|
57
|
+
peer.close(code, reason); },
|
|
58
|
+
get bufferedAmount() { return peer.bufferedAmount; },
|
|
59
|
+
},
|
|
60
|
+
onMessage(listener) {
|
|
61
|
+
messageListener = listener;
|
|
62
|
+
for (const message of pending)
|
|
63
|
+
listener(message);
|
|
64
|
+
pending.length = 0;
|
|
65
|
+
pendingBytes = 0;
|
|
66
|
+
},
|
|
67
|
+
onClose(listener) { closeListener = listener; if (didClose)
|
|
68
|
+
listener(); },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
reject(error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const bridge = createNodeSocketUpgradeHandler({
|
|
77
|
+
hub: options.hub,
|
|
78
|
+
publicOrigin: options.publicOrigin,
|
|
79
|
+
...(options.pathnamePrefix ? { pathnamePrefix: options.pathnamePrefix } : {}),
|
|
80
|
+
upgrade,
|
|
81
|
+
reject(_incoming, socket, status) {
|
|
82
|
+
if (!socket.destroyed)
|
|
83
|
+
socket.end(`HTTP/1.1 ${status} ${STATUS_CODES[status] ?? "Error"}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
84
|
+
},
|
|
85
|
+
...(options.onError ? { onError: options.onError } : {}),
|
|
86
|
+
});
|
|
87
|
+
const onUpgrade = (incoming, socket, head) => {
|
|
88
|
+
if (closing) {
|
|
89
|
+
socket.destroy();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
void bridge(incoming, socket, head).then((handled) => {
|
|
93
|
+
if (!handled)
|
|
94
|
+
socket.destroy();
|
|
95
|
+
}).catch((error) => {
|
|
96
|
+
observeError(error);
|
|
97
|
+
socket.destroy();
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
options.server.on("upgrade", onUpgrade);
|
|
101
|
+
const heartbeat = setInterval(() => {
|
|
102
|
+
for (const peer of webSockets.clients) {
|
|
103
|
+
if (alive.get(peer) === false) {
|
|
104
|
+
peer.terminate();
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
alive.set(peer, false);
|
|
108
|
+
if (peer.readyState === WebSocketClass.OPEN)
|
|
109
|
+
peer.ping();
|
|
110
|
+
}
|
|
111
|
+
}, heartbeatMs);
|
|
112
|
+
heartbeat.unref();
|
|
113
|
+
return {
|
|
114
|
+
close() {
|
|
115
|
+
if (closePromise)
|
|
116
|
+
return closePromise;
|
|
117
|
+
closing = true;
|
|
118
|
+
options.server.off("upgrade", onUpgrade);
|
|
119
|
+
clearInterval(heartbeat);
|
|
120
|
+
for (const peer of webSockets.clients)
|
|
121
|
+
peer.terminate();
|
|
122
|
+
closePromise = new Promise((resolve, reject) => webSockets.close((error) => error ? reject(error) : resolve()));
|
|
123
|
+
return closePromise;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=node-websocket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-websocket.js","sourceRoot":"","sources":["../../src/framework-server/node-websocket.ts"],"names":[],"mappings":"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","sourcesContent":["import { STATUS_CODES, type IncomingMessage, type Server as HttpServer } from \"node:http\";\nimport { createRequire } from \"node:module\";\nimport type { Duplex } from \"node:stream\";\nimport type WebSocket from \"ws\";\nimport { createNodeSocketUpgradeHandler } from \"./node-upgrade.js\";\nimport { observeErrorCallback } from \"./contracts.js\";\nimport type { NodeAcceptedSocket, NodeSocketUpgradeOptions } from \"./node-upgrade.js\";\n\nexport interface NodeWebSocketOptions extends Omit<NodeSocketUpgradeOptions, \"upgrade\" | \"reject\"> {\n /** The same HTTP server used by the Fetch handler. */\n server: HttpServer;\n /** Maximum WebSocket frame payload, enforced before decoding. Defaults to 64 KiB. */\n maxMessageBytes?: number;\n /** Ping interval and idle timeout. Defaults to 30 seconds. */\n heartbeatMs?: number;\n}\n\nexport interface NodeWebSocketServer {\n /** Detaches the upgrade listener and closes all peers. The HTTP server remains owned by its caller. */\n close(): Promise<void>;\n}\n\n/** Attach Meeko channels to a Node HTTP server using ws for protocol framing. */\nexport function attachNodeWebSocketServer(options: NodeWebSocketOptions): NodeWebSocketServer {\n const maxPayload = options.maxMessageBytes ?? 64 * 1024;\n const heartbeatMs = options.heartbeatMs ?? 30_000;\n if (!Number.isSafeInteger(maxPayload) || maxPayload <= 0) throw new RangeError(\"Invalid socket message limit\");\n if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs <= 0) throw new RangeError(\"Invalid socket heartbeat interval\");\n // Keep the optional server transport out of engine-only and HTTP-only imports.\n const { WebSocket: WebSocketClass, WebSocketServer } = createRequire(import.meta.url)(\"ws\") as typeof import(\"ws\");\n const webSockets = new WebSocketServer({ noServer: true, maxPayload, perMessageDeflate: false });\n const observeError = (error: unknown): void => observeErrorCallback(error, options.onError);\n const alive = new WeakMap<WebSocket, boolean>();\n let closing = false;\n let closePromise: Promise<void> | undefined;\n\n const upgrade = (incoming: IncomingMessage, socket: Duplex, head: Buffer): Promise<NodeAcceptedSocket> =>\n new Promise((resolve, reject) => {\n try {\n webSockets.handleUpgrade(incoming, socket, head, (peer) => {\n alive.set(peer, true);\n peer.on(\"pong\", () => alive.set(peer, true));\n peer.on(\"error\", observeError);\n const pending: Uint8Array[] = [];\n let pendingBytes = 0;\n let messageListener: ((message: string | Uint8Array) => void) | undefined;\n let closeListener: (() => void) | undefined;\n let didClose = false;\n peer.on(\"message\", (data, isBinary) => {\n if (isBinary) { peer.close(1003, \"Text messages required\"); return; }\n const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data);\n if (messageListener) { messageListener(bytes); return; }\n pendingBytes += bytes.byteLength;\n if (pendingBytes > maxPayload) { peer.close(1009, \"Message buffer exceeded\"); return; }\n pending.push(bytes);\n });\n peer.once(\"close\", () => { didClose = true; closeListener?.(); });\n resolve({\n peer: {\n send(message) {\n if (peer.readyState !== WebSocketClass.OPEN) throw new Error(\"Socket is disconnected\");\n peer.send(message);\n },\n close(code, reason) { if (peer.readyState === WebSocketClass.OPEN) peer.close(code, reason); },\n get bufferedAmount() { return peer.bufferedAmount; },\n },\n onMessage(listener) {\n messageListener = listener;\n for (const message of pending) listener(message);\n pending.length = 0;\n pendingBytes = 0;\n },\n onClose(listener) { closeListener = listener; if (didClose) listener(); },\n });\n });\n } catch (error) { reject(error); }\n });\n\n const bridge = createNodeSocketUpgradeHandler({\n hub: options.hub,\n publicOrigin: options.publicOrigin,\n ...(options.pathnamePrefix ? { pathnamePrefix: options.pathnamePrefix } : {}),\n upgrade,\n reject(_incoming, socket, status) {\n if (!socket.destroyed) socket.end(`HTTP/1.1 ${status} ${STATUS_CODES[status] ?? \"Error\"}\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n`);\n },\n ...(options.onError ? { onError: options.onError } : {}),\n });\n const onUpgrade = (incoming: IncomingMessage, socket: Duplex, head: Buffer): void => {\n if (closing) { socket.destroy(); return; }\n void bridge(incoming, socket, head).then((handled) => {\n if (!handled) socket.destroy();\n }).catch((error: unknown) => {\n observeError(error);\n socket.destroy();\n });\n };\n options.server.on(\"upgrade\", onUpgrade);\n const heartbeat = setInterval(() => {\n for (const peer of webSockets.clients) {\n if (alive.get(peer) === false) { peer.terminate(); continue; }\n alive.set(peer, false);\n if (peer.readyState === WebSocketClass.OPEN) peer.ping();\n }\n }, heartbeatMs);\n heartbeat.unref();\n\n return {\n close() {\n if (closePromise) return closePromise;\n closing = true;\n options.server.off(\"upgrade\", onUpgrade);\n clearInterval(heartbeat);\n for (const peer of webSockets.clients) peer.terminate();\n closePromise = new Promise<void>((resolve, reject) => webSockets.close((error) => error ? reject(error) : resolve()));\n return closePromise;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Schema } from "./contracts.js";
|
|
2
|
+
import type { ServerManifest } from "./discovery.js";
|
|
3
|
+
export interface OpenApiOptions {
|
|
4
|
+
manifest: ServerManifest;
|
|
5
|
+
info: {
|
|
6
|
+
title: string;
|
|
7
|
+
version: string;
|
|
8
|
+
};
|
|
9
|
+
/** Only schemas explicitly exported from shared/schemas belong here. */
|
|
10
|
+
publicSchemas: Readonly<Record<string, Schema>>;
|
|
11
|
+
/** For Valibot, supply a converter from @valibot/to-json-schema. */
|
|
12
|
+
toJsonSchema(schema: Schema): unknown;
|
|
13
|
+
load?: (file: string) => Promise<unknown>;
|
|
14
|
+
cookieName?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Generates OpenAPI only from a caller-provided public schema registry. */
|
|
17
|
+
export declare function generateOpenApi(options: OpenApiOptions): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { serializeJson } from "../server.js";
|
|
3
|
+
function asSchema(value, name) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
throw new Error(`OpenAPI schema ${name} is not representable as JSON Schema`);
|
|
6
|
+
serializeJson(value);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function properties(value, name) {
|
|
10
|
+
if (value.type !== "object" || !value.properties || typeof value.properties !== "object" || Array.isArray(value.properties))
|
|
11
|
+
throw new Error(`OpenAPI ${name} must convert to an object schema with properties`);
|
|
12
|
+
const entries = Object.entries(value.properties);
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const [key, schema] of entries)
|
|
15
|
+
result[key] = asSchema(schema, `${name}.${key}`);
|
|
16
|
+
const required = new Set(Array.isArray(value.required) ? value.required.filter((key) => typeof key === "string") : []);
|
|
17
|
+
return { properties: result, required };
|
|
18
|
+
}
|
|
19
|
+
function publicPath(pathname) {
|
|
20
|
+
return pathname.replace(/\[\.\.\.[^\]]+\]/g, () => { throw new Error(`OpenAPI cannot represent catch-all route ${pathname}`); })
|
|
21
|
+
.replace(/\[([A-Za-z_$][\w$]*)\]/g, "{$1}");
|
|
22
|
+
}
|
|
23
|
+
/** Generates OpenAPI only from a caller-provided public schema registry. */
|
|
24
|
+
export async function generateOpenApi(options) {
|
|
25
|
+
const load = options.load ?? (async (file) => import(pathToFileURL(file).href));
|
|
26
|
+
const registry = new Map();
|
|
27
|
+
for (const [name, schema] of Object.entries(options.publicSchemas)) {
|
|
28
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name))
|
|
29
|
+
throw new Error(`Invalid public schema name: ${name}`);
|
|
30
|
+
if (registry.has(schema))
|
|
31
|
+
throw new Error(`Duplicate public schema instance: ${name}`);
|
|
32
|
+
registry.set(schema, name);
|
|
33
|
+
}
|
|
34
|
+
const schemas = {};
|
|
35
|
+
const ref = (schema, label) => {
|
|
36
|
+
if (!schema)
|
|
37
|
+
throw new Error(`OpenAPI ${label} requires a schema`);
|
|
38
|
+
const name = registry.get(schema);
|
|
39
|
+
if (!name)
|
|
40
|
+
throw new Error(`OpenAPI ${label} uses a schema outside publicSchemas`);
|
|
41
|
+
if (!schemas[name]) {
|
|
42
|
+
let converted;
|
|
43
|
+
try {
|
|
44
|
+
converted = options.toJsonSchema(schema);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new Error(`OpenAPI schema ${name} cannot be converted: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
}
|
|
49
|
+
schemas[name] = asSchema(converted, name);
|
|
50
|
+
}
|
|
51
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
52
|
+
};
|
|
53
|
+
const paths = {};
|
|
54
|
+
const usePath = (path) => paths[path] ??= {};
|
|
55
|
+
const requireDefinition = async (file) => {
|
|
56
|
+
const module = await load(file);
|
|
57
|
+
if (!module || typeof module !== "object" || !("default" in module) || !module.default || typeof module.default !== "object")
|
|
58
|
+
throw new Error(`OpenAPI handler has no default definition: ${file}`);
|
|
59
|
+
return module.default;
|
|
60
|
+
};
|
|
61
|
+
for (const endpoint of options.manifest.api) {
|
|
62
|
+
const definition = await requireDefinition(endpoint.file);
|
|
63
|
+
if (definition.contentType === "response")
|
|
64
|
+
throw new Error(`OpenAPI non-JSON response requires an explicit representation: ${endpoint.file}`);
|
|
65
|
+
const operation = { operationId: `${endpoint.clientKey.replaceAll(".", "_")}_${endpoint.method.toLowerCase()}`, responses: { "200": { description: "OK", content: { "application/json": { schema: ref(definition.response, `${endpoint.file} response`) } } } } };
|
|
66
|
+
const parameters = [];
|
|
67
|
+
const routeParams = [...endpoint.pathname.matchAll(/\[([A-Za-z_$][\w$]*)\]/g)].map((match) => match[1]);
|
|
68
|
+
if (routeParams.length) {
|
|
69
|
+
const { $ref } = ref(definition.params, `${endpoint.file} params`);
|
|
70
|
+
const converted = schemas[$ref.slice("#/components/schemas/".length)];
|
|
71
|
+
const object = properties(converted, `${endpoint.file} params`);
|
|
72
|
+
for (const name of routeParams) {
|
|
73
|
+
const schema = object.properties[name];
|
|
74
|
+
if (!schema)
|
|
75
|
+
throw new Error(`OpenAPI path parameter ${name} is missing from ${endpoint.file} params schema`);
|
|
76
|
+
parameters.push({ in: "path", name, required: true, schema });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (definition.query) {
|
|
80
|
+
const { $ref } = ref(definition.query, `${endpoint.file} query`);
|
|
81
|
+
const converted = schemas[$ref.slice("#/components/schemas/".length)];
|
|
82
|
+
const object = properties(converted, `${endpoint.file} query`);
|
|
83
|
+
for (const [name, schema] of Object.entries(object.properties))
|
|
84
|
+
parameters.push({ in: "query", name, required: object.required.has(name), schema });
|
|
85
|
+
}
|
|
86
|
+
if (parameters.length)
|
|
87
|
+
operation.parameters = parameters;
|
|
88
|
+
if (definition.body)
|
|
89
|
+
operation.requestBody = { required: true, content: { "application/json": { schema: ref(definition.body, `${endpoint.file} body`) } } };
|
|
90
|
+
if (definition.auth === "required")
|
|
91
|
+
operation.security = [{ sessionCookie: [] }];
|
|
92
|
+
const path = publicPath(endpoint.pathname);
|
|
93
|
+
const item = usePath(path);
|
|
94
|
+
const method = endpoint.method.toLowerCase();
|
|
95
|
+
if (item[method])
|
|
96
|
+
throw new Error(`OpenAPI duplicate operation: ${endpoint.method} ${path}`);
|
|
97
|
+
item[method] = operation;
|
|
98
|
+
}
|
|
99
|
+
for (const endpoint of options.manifest.rpc) {
|
|
100
|
+
const definition = await requireDefinition(endpoint.file);
|
|
101
|
+
const item = usePath(endpoint.pathname);
|
|
102
|
+
if (item.post)
|
|
103
|
+
throw new Error(`OpenAPI duplicate operation: POST ${endpoint.pathname}`);
|
|
104
|
+
item.post = {
|
|
105
|
+
operationId: `rpc_${endpoint.id.replaceAll(".", "_")}`,
|
|
106
|
+
requestBody: { required: true, content: { "application/json": { schema: ref(definition.input, `${endpoint.file} input`) } } },
|
|
107
|
+
responses: { "200": { description: "OK", content: { "application/json": { schema: ref(definition.output, `${endpoint.file} output`) } } } },
|
|
108
|
+
...(definition.auth === "required" ? { security: [{ sessionCookie: [] }] } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
openapi: "3.1.0",
|
|
113
|
+
info: options.info,
|
|
114
|
+
paths,
|
|
115
|
+
components: { schemas, securitySchemes: { sessionCookie: { type: "apiKey", in: "cookie", name: options.cookieName ?? "__Host-meeko.sid" } } },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../src/framework-server/openapi.ts"],"names":[],"mappings":"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","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport type { ApiRouteDefinition, RpcDefinition, Schema } from \"./contracts.js\";\nimport type { ServerManifest } from \"./discovery.js\";\nimport { serializeJson } from \"../server.js\";\n\nexport interface OpenApiOptions {\n manifest: ServerManifest;\n info: { title: string; version: string };\n /** Only schemas explicitly exported from shared/schemas belong here. */\n publicSchemas: Readonly<Record<string, Schema>>;\n /** For Valibot, supply a converter from @valibot/to-json-schema. */\n toJsonSchema(schema: Schema): unknown;\n load?: (file: string) => Promise<unknown>;\n cookieName?: string;\n}\n\ntype JsonSchema = Record<string, unknown>;\ntype PathItem = Record<string, unknown>;\n\nfunction asSchema(value: unknown, name: string): JsonSchema {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(`OpenAPI schema ${name} is not representable as JSON Schema`);\n serializeJson(value);\n return value as JsonSchema;\n}\n\nfunction properties(value: JsonSchema, name: string): { properties: Record<string, JsonSchema>; required: Set<string> } {\n if (value.type !== \"object\" || !value.properties || typeof value.properties !== \"object\" || Array.isArray(value.properties)) throw new Error(`OpenAPI ${name} must convert to an object schema with properties`);\n const entries = Object.entries(value.properties as Record<string, unknown>);\n const result: Record<string, JsonSchema> = {};\n for (const [key, schema] of entries) result[key] = asSchema(schema, `${name}.${key}`);\n const required = new Set(Array.isArray(value.required) ? value.required.filter((key): key is string => typeof key === \"string\") : []);\n return { properties: result, required };\n}\n\nfunction publicPath(pathname: string): string {\n return pathname.replace(/\\[\\.\\.\\.[^\\]]+\\]/g, () => { throw new Error(`OpenAPI cannot represent catch-all route ${pathname}`); })\n .replace(/\\[([A-Za-z_$][\\w$]*)\\]/g, \"{$1}\");\n}\n\n/** Generates OpenAPI only from a caller-provided public schema registry. */\nexport async function generateOpenApi(options: OpenApiOptions): Promise<Record<string, unknown>> {\n const load = options.load ?? (async (file: string) => import(pathToFileURL(file).href) as Promise<unknown>);\n const registry = new Map<Schema, string>();\n for (const [name, schema] of Object.entries(options.publicSchemas)) {\n if (!/^[A-Za-z_$][\\w$]*$/.test(name)) throw new Error(`Invalid public schema name: ${name}`);\n if (registry.has(schema)) throw new Error(`Duplicate public schema instance: ${name}`);\n registry.set(schema, name);\n }\n const schemas: Record<string, JsonSchema> = {};\n const ref = (schema: Schema | undefined, label: string): { $ref: string } => {\n if (!schema) throw new Error(`OpenAPI ${label} requires a schema`);\n const name = registry.get(schema);\n if (!name) throw new Error(`OpenAPI ${label} uses a schema outside publicSchemas`);\n if (!schemas[name]) {\n let converted: unknown;\n try { converted = options.toJsonSchema(schema); }\n catch (error) { throw new Error(`OpenAPI schema ${name} cannot be converted: ${error instanceof Error ? error.message : String(error)}`); }\n schemas[name] = asSchema(converted, name);\n }\n return { $ref: `#/components/schemas/${name}` };\n };\n const paths: Record<string, PathItem> = {};\n const usePath = (path: string): PathItem => paths[path] ??= {};\n const requireDefinition = async (file: string): Promise<Record<string, unknown>> => {\n const module = await load(file);\n if (!module || typeof module !== \"object\" || !(\"default\" in module) || !module.default || typeof module.default !== \"object\") throw new Error(`OpenAPI handler has no default definition: ${file}`);\n return module.default as Record<string, unknown>;\n };\n for (const endpoint of options.manifest.api) {\n const definition = await requireDefinition(endpoint.file) as unknown as ApiRouteDefinition<Schema | undefined, Schema | undefined, Schema | undefined, Schema | undefined>;\n if (definition.contentType === \"response\") throw new Error(`OpenAPI non-JSON response requires an explicit representation: ${endpoint.file}`);\n const operation: Record<string, unknown> = { operationId: `${endpoint.clientKey.replaceAll(\".\", \"_\")}_${endpoint.method.toLowerCase()}`, responses: { \"200\": { description: \"OK\", content: { \"application/json\": { schema: ref(definition.response, `${endpoint.file} response`) } } } } };\n const parameters: Array<Record<string, unknown>> = [];\n const routeParams = [...endpoint.pathname.matchAll(/\\[([A-Za-z_$][\\w$]*)\\]/g)].map((match) => match[1]!);\n if (routeParams.length) {\n const { $ref } = ref(definition.params, `${endpoint.file} params`);\n const converted = schemas[$ref.slice(\"#/components/schemas/\".length)]!;\n const object = properties(converted, `${endpoint.file} params`);\n for (const name of routeParams) {\n const schema = object.properties[name];\n if (!schema) throw new Error(`OpenAPI path parameter ${name} is missing from ${endpoint.file} params schema`);\n parameters.push({ in: \"path\", name, required: true, schema });\n }\n }\n if (definition.query) {\n const { $ref } = ref(definition.query, `${endpoint.file} query`);\n const converted = schemas[$ref.slice(\"#/components/schemas/\".length)]!;\n const object = properties(converted, `${endpoint.file} query`);\n for (const [name, schema] of Object.entries(object.properties)) parameters.push({ in: \"query\", name, required: object.required.has(name), schema });\n }\n if (parameters.length) operation.parameters = parameters;\n if (definition.body) operation.requestBody = { required: true, content: { \"application/json\": { schema: ref(definition.body, `${endpoint.file} body`) } } };\n if (definition.auth === \"required\") operation.security = [{ sessionCookie: [] }];\n const path = publicPath(endpoint.pathname);\n const item = usePath(path);\n const method = endpoint.method.toLowerCase();\n if (item[method]) throw new Error(`OpenAPI duplicate operation: ${endpoint.method} ${path}`);\n item[method] = operation;\n }\n for (const endpoint of options.manifest.rpc) {\n const definition = await requireDefinition(endpoint.file) as unknown as RpcDefinition<Schema, Schema>;\n const item = usePath(endpoint.pathname);\n if (item.post) throw new Error(`OpenAPI duplicate operation: POST ${endpoint.pathname}`);\n item.post = {\n operationId: `rpc_${endpoint.id.replaceAll(\".\", \"_\")}`,\n requestBody: { required: true, content: { \"application/json\": { schema: ref(definition.input, `${endpoint.file} input`) } } },\n responses: { \"200\": { description: \"OK\", content: { \"application/json\": { schema: ref(definition.output, `${endpoint.file} output`) } } } },\n ...(definition.auth === \"required\" ? { security: [{ sessionCookie: [] }] } : {}),\n };\n }\n return {\n openapi: \"3.1.0\",\n info: options.info,\n paths,\n components: { schemas, securitySchemes: { sessionCookie: { type: \"apiKey\", in: \"cookie\", name: options.cookieName ?? \"__Host-meeko.sid\" } } },\n };\n}\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { SessionRecord, SessionStore } from "./sessions.js";
|
|
2
|
+
/** Small interface compatible with Redis clients that support SET PX. */
|
|
3
|
+
export interface RedisSessionClient {
|
|
4
|
+
get(key: string): Promise<string | null>;
|
|
5
|
+
set(key: string, value: string, options: {
|
|
6
|
+
PX: number;
|
|
7
|
+
}): Promise<unknown>;
|
|
8
|
+
del(key: string): Promise<unknown>;
|
|
9
|
+
eval(script: string, options: {
|
|
10
|
+
keys: string[];
|
|
11
|
+
arguments: string[];
|
|
12
|
+
}): Promise<unknown>;
|
|
13
|
+
quit?(): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface RedisSessionStoreOptions {
|
|
16
|
+
/** Use a unique prefix for each application and environment. */
|
|
17
|
+
prefix: string;
|
|
18
|
+
/** Close the Redis client when this store owns its connection. */
|
|
19
|
+
closeClient?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Persistent store with Redis-managed expiration and explicit connection ownership. */
|
|
22
|
+
export declare class RedisSessionStore<T = unknown> implements SessionStore<T> {
|
|
23
|
+
private readonly client;
|
|
24
|
+
private readonly options;
|
|
25
|
+
readonly persistent = true;
|
|
26
|
+
constructor(client: RedisSessionClient, options: RedisSessionStoreOptions);
|
|
27
|
+
private key;
|
|
28
|
+
private familyKey;
|
|
29
|
+
get(id: string): Promise<SessionRecord<T> | undefined>;
|
|
30
|
+
set(record: SessionRecord<T>): Promise<void>;
|
|
31
|
+
delete(id: string): Promise<void>;
|
|
32
|
+
replace(previousId: string, record: SessionRecord<T>): Promise<boolean>;
|
|
33
|
+
revokeFamily(familyId: string): Promise<void>;
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** A driver can implement these operations using any SQL client and dialect. */
|
|
37
|
+
export interface SqlSessionRow {
|
|
38
|
+
id: string;
|
|
39
|
+
/** Migrate existing rows by setting family_id = id before deploying this driver. */
|
|
40
|
+
familyId: string;
|
|
41
|
+
csrfToken: string;
|
|
42
|
+
dataJson: string;
|
|
43
|
+
createdAt: number;
|
|
44
|
+
expiresAt: number;
|
|
45
|
+
}
|
|
46
|
+
export interface SqlSessionDriver {
|
|
47
|
+
read(id: string): Promise<SqlSessionRow | undefined>;
|
|
48
|
+
/** Insert or update one row, keyed by its opaque id; enforce one active id per family. */
|
|
49
|
+
write(row: SqlSessionRow): Promise<void>;
|
|
50
|
+
remove(id: string): Promise<void>;
|
|
51
|
+
/** Atomically replace an unexpired row of the same family in one transaction. */
|
|
52
|
+
replace(previousId: string, row: SqlSessionRow, now: number): Promise<boolean>;
|
|
53
|
+
/** Delete the active row for this family in one atomic operation, including stale-handle logout. */
|
|
54
|
+
revokeFamily(familyId: string): Promise<void>;
|
|
55
|
+
close?(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export interface SqlSessionStoreOptions {
|
|
58
|
+
/** Close the SQL driver when this store owns its connection. */
|
|
59
|
+
closeDriver?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Generic SQL store. The application supplies SQL statements through its driver. */
|
|
62
|
+
export declare class SqlSessionStore<T = unknown> implements SessionStore<T> {
|
|
63
|
+
private readonly driver;
|
|
64
|
+
private readonly options;
|
|
65
|
+
readonly persistent = true;
|
|
66
|
+
constructor(driver: SqlSessionDriver, options?: SqlSessionStoreOptions);
|
|
67
|
+
get(id: string): Promise<SessionRecord<T> | undefined>;
|
|
68
|
+
set(record: SessionRecord<T>): Promise<void>;
|
|
69
|
+
delete(id: string): Promise<void>;
|
|
70
|
+
replace(previousId: string, record: SessionRecord<T>): Promise<boolean>;
|
|
71
|
+
revokeFamily(familyId: string): Promise<void>;
|
|
72
|
+
close(): Promise<void>;
|
|
73
|
+
}
|