@graphorin/server 0.5.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/app.d.ts +174 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +684 -0
- package/dist/app.js.map +1 -0
- package/dist/commentary/audit-bridge.d.ts +57 -0
- package/dist/commentary/audit-bridge.d.ts.map +1 -0
- package/dist/commentary/audit-bridge.js +92 -0
- package/dist/commentary/audit-bridge.js.map +1 -0
- package/dist/commentary/built-in-patterns.d.ts +24 -0
- package/dist/commentary/built-in-patterns.d.ts.map +1 -0
- package/dist/commentary/built-in-patterns.js +62 -0
- package/dist/commentary/built-in-patterns.js.map +1 -0
- package/dist/commentary/index.d.ts +5 -0
- package/dist/commentary/index.js +5 -0
- package/dist/commentary/sanitizer.d.ts +37 -0
- package/dist/commentary/sanitizer.d.ts.map +1 -0
- package/dist/commentary/sanitizer.js +182 -0
- package/dist/commentary/sanitizer.js.map +1 -0
- package/dist/commentary/types.d.ts +120 -0
- package/dist/commentary/types.d.ts.map +1 -0
- package/dist/config.d.ts +760 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +217 -0
- package/dist/config.js.map +1 -0
- package/dist/consolidator/daemon.d.ts +88 -0
- package/dist/consolidator/daemon.d.ts.map +1 -0
- package/dist/consolidator/daemon.js +54 -0
- package/dist/consolidator/daemon.js.map +1 -0
- package/dist/consolidator/index.d.ts +2 -0
- package/dist/consolidator/index.js +3 -0
- package/dist/errors/index.d.ts +104 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +131 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/health/checks.d.ts +120 -0
- package/dist/health/checks.d.ts.map +1 -0
- package/dist/health/checks.js +127 -0
- package/dist/health/checks.js.map +1 -0
- package/dist/health/index.d.ts +3 -0
- package/dist/health/index.js +4 -0
- package/dist/health/routes.d.ts +66 -0
- package/dist/health/routes.d.ts.map +1 -0
- package/dist/health/routes.js +96 -0
- package/dist/health/routes.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/context.d.ts +66 -0
- package/dist/internal/context.d.ts.map +1 -0
- package/dist/internal/ids.js +21 -0
- package/dist/internal/ids.js.map +1 -0
- package/dist/internal/json.js +46 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/lifecycle/hooks.d.ts +57 -0
- package/dist/lifecycle/hooks.d.ts.map +1 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +3 -0
- package/dist/lifecycle/pre-bind.d.ts +38 -0
- package/dist/lifecycle/pre-bind.d.ts.map +1 -0
- package/dist/lifecycle/pre-bind.js +62 -0
- package/dist/lifecycle/pre-bind.js.map +1 -0
- package/dist/metrics/catalog.d.ts +34 -0
- package/dist/metrics/catalog.d.ts.map +1 -0
- package/dist/metrics/catalog.js +61 -0
- package/dist/metrics/catalog.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.js +4 -0
- package/dist/metrics/registry.d.ts +63 -0
- package/dist/metrics/registry.d.ts.map +1 -0
- package/dist/metrics/registry.js +222 -0
- package/dist/metrics/registry.js.map +1 -0
- package/dist/middleware/audit.d.ts +47 -0
- package/dist/middleware/audit.d.ts.map +1 -0
- package/dist/middleware/audit.js +71 -0
- package/dist/middleware/audit.js.map +1 -0
- package/dist/middleware/auth.d.ts +50 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +164 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +15 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +45 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/csrf.d.ts +15 -0
- package/dist/middleware/csrf.d.ts.map +1 -0
- package/dist/middleware/csrf.js +77 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/idempotency.d.ts +41 -0
- package/dist/middleware/idempotency.d.ts.map +1 -0
- package/dist/middleware/idempotency.js +198 -0
- package/dist/middleware/idempotency.js.map +1 -0
- package/dist/middleware/index.d.ts +9 -0
- package/dist/middleware/index.js +10 -0
- package/dist/middleware/rate-limit.d.ts +17 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +47 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/request-state.d.ts +27 -0
- package/dist/middleware/request-state.d.ts.map +1 -0
- package/dist/middleware/request-state.js +42 -0
- package/dist/middleware/request-state.js.map +1 -0
- package/dist/middleware/scope.d.ts +24 -0
- package/dist/middleware/scope.d.ts.map +1 -0
- package/dist/middleware/scope.js +50 -0
- package/dist/middleware/scope.js.map +1 -0
- package/dist/registry/index.d.ts +135 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +96 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/replay/index.d.ts +2 -0
- package/dist/replay/index.js +3 -0
- package/dist/replay/routes.d.ts +46 -0
- package/dist/replay/routes.d.ts.map +1 -0
- package/dist/replay/routes.js +189 -0
- package/dist/replay/routes.js.map +1 -0
- package/dist/routes/agents.d.ts +41 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +213 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/audit.d.ts +57 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +116 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/auth.d.ts +26 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +54 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/health.d.ts +22 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +33 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +10 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/mcp.d.ts +32 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +61 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/memory.d.ts +141 -0
- package/dist/routes/memory.d.ts.map +1 -0
- package/dist/routes/memory.js +102 -0
- package/dist/routes/memory.js.map +1 -0
- package/dist/routes/sessions.d.ts +54 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +135 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/skills.d.ts +34 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +54 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/tokens.d.ts +25 -0
- package/dist/routes/tokens.d.ts.map +1 -0
- package/dist/routes/tokens.js +87 -0
- package/dist/routes/tokens.js.map +1 -0
- package/dist/routes/workflows.d.ts +27 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +220 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/runtime/run-state.d.ts +158 -0
- package/dist/runtime/run-state.d.ts.map +1 -0
- package/dist/runtime/run-state.js +182 -0
- package/dist/runtime/run-state.js.map +1 -0
- package/dist/sse/events.d.ts +44 -0
- package/dist/sse/events.d.ts.map +1 -0
- package/dist/sse/events.js +200 -0
- package/dist/sse/events.js.map +1 -0
- package/dist/sse/index.d.ts +2 -0
- package/dist/sse/index.js +3 -0
- package/dist/triggers/daemon.d.ts +74 -0
- package/dist/triggers/daemon.d.ts.map +1 -0
- package/dist/triggers/daemon.js +114 -0
- package/dist/triggers/daemon.js.map +1 -0
- package/dist/triggers/index.d.ts +3 -0
- package/dist/triggers/index.js +4 -0
- package/dist/triggers/routes.d.ts +21 -0
- package/dist/triggers/routes.d.ts.map +1 -0
- package/dist/triggers/routes.js +117 -0
- package/dist/triggers/routes.js.map +1 -0
- package/dist/ws/dispatcher.d.ts +169 -0
- package/dist/ws/dispatcher.d.ts.map +1 -0
- package/dist/ws/dispatcher.js +303 -0
- package/dist/ws/dispatcher.js.map +1 -0
- package/dist/ws/index.d.ts +6 -0
- package/dist/ws/index.js +7 -0
- package/dist/ws/replay-buffer.d.ts +47 -0
- package/dist/ws/replay-buffer.d.ts.map +1 -0
- package/dist/ws/replay-buffer.js +88 -0
- package/dist/ws/replay-buffer.js.map +1 -0
- package/dist/ws/subjects.d.ts +71 -0
- package/dist/ws/subjects.d.ts.map +1 -0
- package/dist/ws/subjects.js +112 -0
- package/dist/ws/subjects.js.map +1 -0
- package/dist/ws/ticket.d.ts +74 -0
- package/dist/ws/ticket.d.ts.map +1 -0
- package/dist/ws/ticket.js +112 -0
- package/dist/ws/ticket.js.map +1 -0
- package/dist/ws/upgrade.d.ts +63 -0
- package/dist/ws/upgrade.d.ts.map +1 -0
- package/dist/ws/upgrade.js +324 -0
- package/dist/ws/upgrade.js.map +1 -0
- package/package.json +156 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { ClientMessageSchema, RPC_ERROR_CODES, SUBPROTOCOL_NAME, closeCodeFor, isCancelledNotification, isInitializeRequest, isPingRequest, isRunCancelRequest, isSubscribeRequest, isUnsubscribeRequest, negotiateSubprotocol, parseTicketSubprotocol } from "@graphorin/protocol";
|
|
2
|
+
import { parseScope, scopeMatches } from "@graphorin/security/auth";
|
|
3
|
+
|
|
4
|
+
//#region src/ws/upgrade.ts
|
|
5
|
+
/**
|
|
6
|
+
* Hono WebSocket upgrade handler. Wires the `@hono/node-ws` adapter
|
|
7
|
+
* to the dispatcher: validates the subprotocol, runs auth (bearer
|
|
8
|
+
* via `requireAuth` middleware OR ticket via the in-memory store),
|
|
9
|
+
* and bridges the per-connection `WSContext` callbacks to the
|
|
10
|
+
* dispatcher's RPC handler.
|
|
11
|
+
*
|
|
12
|
+
* ## Subprotocol + browser ticket flow
|
|
13
|
+
*
|
|
14
|
+
* The negotiated subprotocol is always {@link SUBPROTOCOL_NAME}
|
|
15
|
+
* (`graphorin.protocol.v1`) — the canonical wire contract lives in
|
|
16
|
+
* `@graphorin/protocol`'s `subprotocol.ts`. Two auth paths exist:
|
|
17
|
+
*
|
|
18
|
+
* - **Bearer (non-browser):** the client sends `Authorization:
|
|
19
|
+
* Bearer <token>` and a single `Sec-WebSocket-Protocol:
|
|
20
|
+
* graphorin.protocol.v1` token.
|
|
21
|
+
* - **Ticket (browser):** the `WebSocket` constructor cannot set
|
|
22
|
+
* headers, so the browser client offers two subprotocol tokens —
|
|
23
|
+
* `graphorin.protocol.v1` and `ticket.<value>`. The server echoes
|
|
24
|
+
* back only the canonical name (in `app.ts`'s `handleProtocols`),
|
|
25
|
+
* and {@link resolveUpgradeAuth} extracts the ticket via
|
|
26
|
+
* `parseTicketSubprotocol` and redeems it against the single-use
|
|
27
|
+
* {@link WsTicketStore}. Tickets are short-lived and one-shot to
|
|
28
|
+
* bound replay.
|
|
29
|
+
*
|
|
30
|
+
* @packageDocumentation
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Build the `WSEvents` callback bag the Hono helper consumes. The
|
|
34
|
+
* function takes the request `Context` so the upgrade can read the
|
|
35
|
+
* `Authorization` header / `Sec-WebSocket-Protocol` ticket directly.
|
|
36
|
+
*
|
|
37
|
+
* Production wiring on `@hono/node-ws`:
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });
|
|
41
|
+
* app.get('/v1/ws', upgradeWebSocket((c) => createWsUpgradeEvents(c, deps)));
|
|
42
|
+
* injectWebSocket(serve(...));
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @stable
|
|
46
|
+
*/
|
|
47
|
+
async function createWsUpgradeEvents(c, options) {
|
|
48
|
+
if (negotiateSubprotocol(c.req.header("sec-websocket-protocol") ?? "") === null) return rejectImmediately(closeCodeFor("protocol.violation"), "protocol.violation");
|
|
49
|
+
const auth = await resolveUpgradeAuth(c, options);
|
|
50
|
+
if (auth.kind === "denied") return rejectImmediately(closeCodeFor(auth.reason), auth.reason);
|
|
51
|
+
const subscriberId = options.newSubscriberId?.() ?? defaultSubscriberId();
|
|
52
|
+
const newSubscriptionId = options.newSubscriptionId ?? (() => defaultSubscriptionId(subscriberId));
|
|
53
|
+
let unregister;
|
|
54
|
+
let initialized = false;
|
|
55
|
+
return {
|
|
56
|
+
onOpen: (_event, ws) => {
|
|
57
|
+
const handle = {
|
|
58
|
+
id: subscriberId,
|
|
59
|
+
tokenId: auth.tokenId,
|
|
60
|
+
grantedScopes: auth.grantedScopes,
|
|
61
|
+
send: (frame) => {
|
|
62
|
+
ws.send(JSON.stringify(frame));
|
|
63
|
+
},
|
|
64
|
+
close: (code, reason) => {
|
|
65
|
+
ws.close(code, reason);
|
|
66
|
+
},
|
|
67
|
+
bufferedAmount: () => {
|
|
68
|
+
return ws.raw?.bufferedAmount ?? 0;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
unregister = options.dispatcher.registerSubscriber(handle).unregister;
|
|
72
|
+
},
|
|
73
|
+
onMessage: (event, ws) => {
|
|
74
|
+
const raw = typeof event.data === "string" ? event.data : "";
|
|
75
|
+
let payload;
|
|
76
|
+
try {
|
|
77
|
+
payload = JSON.parse(raw);
|
|
78
|
+
} catch {
|
|
79
|
+
sendRpcError(ws, void 0, RPC_ERROR_CODES.PARSE_ERROR, "Frame is not valid JSON.");
|
|
80
|
+
ws.close(closeCodeFor("protocol.violation"), "protocol.violation");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const parsed = ClientMessageSchema.safeParse(payload);
|
|
84
|
+
if (!parsed.success) {
|
|
85
|
+
sendRpcError(ws, payload?.id, RPC_ERROR_CODES.INVALID_REQUEST, "Frame failed schema validation.");
|
|
86
|
+
ws.close(closeCodeFor("protocol.violation"), "protocol.violation");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const message = parsed.data;
|
|
90
|
+
if (isCancelledNotification(message)) {
|
|
91
|
+
if (initialized && grantsInvokeScope(auth.grantedScopes)) options.runs?.abort(message.params.requestId, "mcp-cancel");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!initialized && !isInitializeRequest(message) && !isPingRequest(message)) {
|
|
95
|
+
sendRpcError(ws, message.id, RPC_ERROR_CODES.PROTOCOL_VIOLATION, "Send `initialize` before any other RPC.");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (isInitializeRequest(message)) {
|
|
99
|
+
initialized = true;
|
|
100
|
+
sendRpcSuccess(ws, message.id, {
|
|
101
|
+
serverInfo: {
|
|
102
|
+
name: "graphorin-server",
|
|
103
|
+
version: "0.5.0"
|
|
104
|
+
},
|
|
105
|
+
capabilities: {
|
|
106
|
+
subscriptions: true,
|
|
107
|
+
replayBuffer: true,
|
|
108
|
+
sseFallback: true
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (isPingRequest(message)) {
|
|
114
|
+
const nonce = message.params?.nonce;
|
|
115
|
+
ws.send(JSON.stringify({
|
|
116
|
+
v: "1",
|
|
117
|
+
kind: "pong",
|
|
118
|
+
...nonce !== void 0 ? { nonce } : {}
|
|
119
|
+
}));
|
|
120
|
+
sendRpcSuccess(ws, message.id, { ok: true });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (isSubscribeRequest(message)) {
|
|
124
|
+
const subscriptionId = newSubscriptionId();
|
|
125
|
+
const result = options.dispatcher.subscribe({
|
|
126
|
+
subscriberId,
|
|
127
|
+
subject: message.params.subject,
|
|
128
|
+
subscriptionId,
|
|
129
|
+
...message.params.sinceEventId !== void 0 ? { sinceEventId: message.params.sinceEventId } : {}
|
|
130
|
+
});
|
|
131
|
+
if (!result.ok) {
|
|
132
|
+
sendRpcError(ws, message.id, mapSubscribeErrorCode(result.reason), mapSubscribeErrorMessage(result.reason, message.params.subject));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
sendRpcSuccess(ws, message.id, {
|
|
136
|
+
subscriptionId,
|
|
137
|
+
subject: message.params.subject,
|
|
138
|
+
snapshotEventId: result.snapshotEventId,
|
|
139
|
+
replayedCount: result.replayedCount
|
|
140
|
+
});
|
|
141
|
+
ws.send(JSON.stringify({
|
|
142
|
+
v: "1",
|
|
143
|
+
kind: "subscribed",
|
|
144
|
+
subscriptionId,
|
|
145
|
+
subject: message.params.subject,
|
|
146
|
+
...result.snapshotEventId !== void 0 ? { snapshotEventId: result.snapshotEventId } : {}
|
|
147
|
+
}));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (isUnsubscribeRequest(message)) {
|
|
151
|
+
if (!options.dispatcher.unsubscribe(message.params.subscriptionId)) {
|
|
152
|
+
sendRpcError(ws, message.id, RPC_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, `Subscription '${message.params.subscriptionId}' not active.`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
sendRpcSuccess(ws, message.id, {
|
|
156
|
+
subscriptionId: message.params.subscriptionId,
|
|
157
|
+
unsubscribed: true
|
|
158
|
+
});
|
|
159
|
+
ws.send(JSON.stringify({
|
|
160
|
+
v: "1",
|
|
161
|
+
kind: "unsubscribed",
|
|
162
|
+
subscriptionId: message.params.subscriptionId
|
|
163
|
+
}));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (isRunCancelRequest(message)) {
|
|
167
|
+
if (!grantsInvokeScope(auth.grantedScopes)) {
|
|
168
|
+
sendRpcError(ws, message.id, RPC_ERROR_CODES.SCOPE_DENIED, "Token lacks required scope 'agents:invoke'.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const runId = message.params.runId;
|
|
172
|
+
if (!(options.runs?.abort(runId, message.params.reason ?? "rpc-cancel") === true)) {
|
|
173
|
+
sendRpcError(ws, message.id, RPC_ERROR_CODES.RUN_NOT_FOUND, `Run '${runId}' is not active.`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
sendRpcSuccess(ws, message.id, {
|
|
177
|
+
runId,
|
|
178
|
+
cancelled: true,
|
|
179
|
+
partialStateAvailable: true
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
onClose: () => {
|
|
185
|
+
try {
|
|
186
|
+
unregister?.();
|
|
187
|
+
} finally {
|
|
188
|
+
unregister = void 0;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
onError: () => {
|
|
192
|
+
try {
|
|
193
|
+
unregister?.();
|
|
194
|
+
} finally {
|
|
195
|
+
unregister = void 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function sendRpcSuccess(ws, id, result) {
|
|
201
|
+
const frame = {
|
|
202
|
+
v: "1",
|
|
203
|
+
jsonrpc: "2.0",
|
|
204
|
+
id,
|
|
205
|
+
result
|
|
206
|
+
};
|
|
207
|
+
ws.send(JSON.stringify(frame));
|
|
208
|
+
}
|
|
209
|
+
function sendRpcError(ws, id, code, message) {
|
|
210
|
+
const frame = {
|
|
211
|
+
v: "1",
|
|
212
|
+
jsonrpc: "2.0",
|
|
213
|
+
id: id ?? 0,
|
|
214
|
+
error: {
|
|
215
|
+
code,
|
|
216
|
+
message
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
ws.send(JSON.stringify(frame));
|
|
220
|
+
}
|
|
221
|
+
function rejectImmediately(code, reason) {
|
|
222
|
+
return { onOpen: (_event, ws) => {
|
|
223
|
+
ws.close(code, reason);
|
|
224
|
+
} };
|
|
225
|
+
}
|
|
226
|
+
async function resolveUpgradeAuth(c, options) {
|
|
227
|
+
const state = c.var.state;
|
|
228
|
+
if (state?.auth?.kind === "token") return {
|
|
229
|
+
kind: "ok",
|
|
230
|
+
tokenId: state.auth.token.tokenId,
|
|
231
|
+
grantedScopes: state.auth.grantedScopes
|
|
232
|
+
};
|
|
233
|
+
if (options.anonymous === true) return {
|
|
234
|
+
kind: "ok",
|
|
235
|
+
tokenId: "anonymous",
|
|
236
|
+
grantedScopes: ANONYMOUS_WS_SCOPES
|
|
237
|
+
};
|
|
238
|
+
const ticketValue = parseTicketSubprotocol(c.req.header("sec-websocket-protocol") ?? "");
|
|
239
|
+
if (ticketValue !== void 0) {
|
|
240
|
+
const consumed = options.tickets.consume(ticketValue);
|
|
241
|
+
if (!consumed.ok) return {
|
|
242
|
+
kind: "denied",
|
|
243
|
+
reason: "auth.invalid"
|
|
244
|
+
};
|
|
245
|
+
return acceptTicket(consumed.ticket);
|
|
246
|
+
}
|
|
247
|
+
const header = c.req.header("authorization") ?? c.req.header("Authorization");
|
|
248
|
+
if (header?.toLowerCase().startsWith("bearer ") === true) {
|
|
249
|
+
if (options.verifier === void 0) return {
|
|
250
|
+
kind: "denied",
|
|
251
|
+
reason: "auth.required"
|
|
252
|
+
};
|
|
253
|
+
const token = header.slice(7).trim();
|
|
254
|
+
const verified = await options.verifier.verify(token);
|
|
255
|
+
if (!verified.ok) return {
|
|
256
|
+
kind: "denied",
|
|
257
|
+
reason: "auth.invalid"
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
kind: "ok",
|
|
261
|
+
tokenId: verified.token.tokenId,
|
|
262
|
+
grantedScopes: verified.token.scopes
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
kind: "denied",
|
|
267
|
+
reason: "auth.required"
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* IP-8: the `agents:invoke` scope required to cancel a run over the WS
|
|
272
|
+
* transport — the same requirement the REST `POST /runs/:runId/abort` route
|
|
273
|
+
* enforces. `scopeMatches` honours wildcards (`agents:*`, `admin:*`).
|
|
274
|
+
*/
|
|
275
|
+
const INVOKE_SCOPE = parseScope("agents:invoke");
|
|
276
|
+
/**
|
|
277
|
+
* IP-13: full scope grant handed to a no-auth (`auth.kind='none'`) upgrade.
|
|
278
|
+
* `admin:*` matches every required scope, including the {@link INVOKE_SCOPE}
|
|
279
|
+
* gate on run cancellation.
|
|
280
|
+
*/
|
|
281
|
+
const ANONYMOUS_WS_SCOPES = [parseScope("admin:*")];
|
|
282
|
+
function grantsInvokeScope(granted) {
|
|
283
|
+
for (const scope of granted) if (scopeMatches(scope, INVOKE_SCOPE)) return true;
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
function acceptTicket(ticket) {
|
|
287
|
+
return {
|
|
288
|
+
kind: "ok",
|
|
289
|
+
tokenId: ticket.tokenId,
|
|
290
|
+
grantedScopes: ticket.scopes
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function mapSubscribeErrorCode(reason) {
|
|
294
|
+
switch (reason) {
|
|
295
|
+
case "scope-denied": return RPC_ERROR_CODES.SCOPE_DENIED;
|
|
296
|
+
case "subject-wildcard": return RPC_ERROR_CODES.INVALID_PARAMS;
|
|
297
|
+
case "subject-unknown": return RPC_ERROR_CODES.METHOD_NOT_FOUND;
|
|
298
|
+
case "subject-malformed": return RPC_ERROR_CODES.INVALID_PARAMS;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function mapSubscribeErrorMessage(reason, subject) {
|
|
302
|
+
switch (reason) {
|
|
303
|
+
case "scope-denied": return `Token lacks required scope for subject '${subject}'.`;
|
|
304
|
+
case "subject-wildcard": return `Wildcard subjects are not supported in this version. Subject: '${subject}'.`;
|
|
305
|
+
case "subject-unknown": return `Unknown subject grammar: '${subject}'.`;
|
|
306
|
+
case "subject-malformed": return `Subject '${subject}' is malformed.`;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function defaultSubscriberId() {
|
|
310
|
+
return `sub-conn-${randomToken(8)}`;
|
|
311
|
+
}
|
|
312
|
+
function defaultSubscriptionId(subscriberId) {
|
|
313
|
+
return `${subscriberId}-${randomToken(6)}`;
|
|
314
|
+
}
|
|
315
|
+
function randomToken(length) {
|
|
316
|
+
const bytes = new Uint8Array(length);
|
|
317
|
+
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.getRandomValues === "function") globalThis.crypto.getRandomValues(bytes);
|
|
318
|
+
else for (let i = 0; i < length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
319
|
+
return Buffer.from(bytes).toString("hex");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
//#endregion
|
|
323
|
+
export { createWsUpgradeEvents };
|
|
324
|
+
//# sourceMappingURL=upgrade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upgrade.js","names":["unregister: (() => void) | undefined","payload: unknown","frame: ServerMessage","INVOKE_SCOPE: ParsedScope","ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope>"],"sources":["../../src/ws/upgrade.ts"],"sourcesContent":["/**\n * Hono WebSocket upgrade handler. Wires the `@hono/node-ws` adapter\n * to the dispatcher: validates the subprotocol, runs auth (bearer\n * via `requireAuth` middleware OR ticket via the in-memory store),\n * and bridges the per-connection `WSContext` callbacks to the\n * dispatcher's RPC handler.\n *\n * ## Subprotocol + browser ticket flow\n *\n * The negotiated subprotocol is always {@link SUBPROTOCOL_NAME}\n * (`graphorin.protocol.v1`) — the canonical wire contract lives in\n * `@graphorin/protocol`'s `subprotocol.ts`. Two auth paths exist:\n *\n * - **Bearer (non-browser):** the client sends `Authorization:\n * Bearer <token>` and a single `Sec-WebSocket-Protocol:\n * graphorin.protocol.v1` token.\n * - **Ticket (browser):** the `WebSocket` constructor cannot set\n * headers, so the browser client offers two subprotocol tokens —\n * `graphorin.protocol.v1` and `ticket.<value>`. The server echoes\n * back only the canonical name (in `app.ts`'s `handleProtocols`),\n * and {@link resolveUpgradeAuth} extracts the ticket via\n * `parseTicketSubprotocol` and redeems it against the single-use\n * {@link WsTicketStore}. Tickets are short-lived and one-shot to\n * bound replay.\n *\n * @packageDocumentation\n */\n\nimport {\n ClientMessageSchema,\n closeCodeFor,\n isCancelledNotification,\n isInitializeRequest,\n isPingRequest,\n isRunCancelRequest,\n isSubscribeRequest,\n isUnsubscribeRequest,\n negotiateSubprotocol,\n parseTicketSubprotocol,\n RPC_ERROR_CODES,\n type ServerMessage,\n SUBPROTOCOL_NAME,\n} from '@graphorin/protocol';\nimport type { ParsedScope, TokenVerifier } from '@graphorin/security/auth';\nimport { parseScope, scopeMatches } from '@graphorin/security/auth';\nimport type { Context } from 'hono';\nimport type { WSContext, WSEvents } from 'hono/ws';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\nimport type { WsDispatcher } from './dispatcher.js';\nimport type { WsTicket, WsTicketStore } from './ticket.js';\n\n/**\n * Public configuration accepted by {@link createWsUpgradeEvents}.\n *\n * @stable\n */\nexport interface WsUpgradeOptions {\n readonly dispatcher: WsDispatcher;\n readonly tickets: WsTicketStore;\n /**\n * Token verifier for bearer / ticket upgrades. Optional only in the\n * IP-13 no-auth loopback mode (`anonymous: true`), where there is no\n * verifier to construct and every upgrade is accepted.\n */\n readonly verifier?: TokenVerifier;\n /**\n * IP-13: authentication is disabled server-wide (`auth.kind='none'`).\n * Accept the upgrade unconditionally with a full (`admin:*`) scope grant\n * instead of silently refusing to mount the WS route. Trusted-loopback\n * / single-operator mode only.\n */\n readonly anonymous?: boolean;\n readonly runs?: RunStateTracker;\n readonly now?: () => number;\n /**\n * Subprotocol the server advertises. Defaults to\n * {@link SUBPROTOCOL_NAME}; tests can override to exercise the\n * mismatch path.\n */\n readonly serverSubprotocol?: string;\n readonly newSubscriptionId?: () => string;\n readonly newSubscriberId?: () => string;\n}\n\n/**\n * Build the `WSEvents` callback bag the Hono helper consumes. The\n * function takes the request `Context` so the upgrade can read the\n * `Authorization` header / `Sec-WebSocket-Protocol` ticket directly.\n *\n * Production wiring on `@hono/node-ws`:\n *\n * ```ts\n * const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });\n * app.get('/v1/ws', upgradeWebSocket((c) => createWsUpgradeEvents(c, deps)));\n * injectWebSocket(serve(...));\n * ```\n *\n * @stable\n */\nexport async function createWsUpgradeEvents(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<WSEvents> {\n const negotiated = negotiateSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (negotiated === null) {\n return rejectImmediately(closeCodeFor('protocol.violation'), 'protocol.violation');\n }\n const auth = await resolveUpgradeAuth(c, options);\n if (auth.kind === 'denied') {\n return rejectImmediately(closeCodeFor(auth.reason), auth.reason);\n }\n\n const subscriberId = options.newSubscriberId?.() ?? defaultSubscriberId();\n const newSubscriptionId =\n options.newSubscriptionId ?? (() => defaultSubscriptionId(subscriberId));\n\n let unregister: (() => void) | undefined;\n let initialized = false;\n\n return {\n onOpen: (_event, ws) => {\n const handle = {\n id: subscriberId,\n tokenId: auth.tokenId,\n grantedScopes: auth.grantedScopes,\n send: (frame: ServerMessage) => {\n ws.send(JSON.stringify(frame));\n },\n close: (code: number, reason: string) => {\n ws.close(code, reason);\n },\n // IP-9: surface the socket's real backlog so the dispatcher's\n // backpressure threshold measures something — the synchronous\n // outstanding-events counter never accumulates.\n bufferedAmount: () => {\n const raw = (ws as { raw?: { bufferedAmount?: number } }).raw;\n return raw?.bufferedAmount ?? 0;\n },\n };\n const reg = options.dispatcher.registerSubscriber(handle);\n unregister = reg.unregister;\n },\n onMessage: (event, ws) => {\n const raw = typeof event.data === 'string' ? event.data : '';\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n sendRpcError(ws, undefined, RPC_ERROR_CODES.PARSE_ERROR, 'Frame is not valid JSON.');\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const parsed = ClientMessageSchema.safeParse(payload);\n if (!parsed.success) {\n sendRpcError(\n ws,\n (payload as { id?: string | number } | null)?.id,\n RPC_ERROR_CODES.INVALID_REQUEST,\n 'Frame failed schema validation.',\n );\n ws.close(closeCodeFor('protocol.violation'), 'protocol.violation');\n return;\n }\n const message = parsed.data;\n if (isCancelledNotification(message)) {\n // IP-8: only an initialized connection holding `agents:invoke` may\n // cancel a run. A notification carries no id, so an unauthorised one\n // is silently ignored (there is no error channel) rather than aborting\n // an arbitrary run.\n if (initialized && grantsInvokeScope(auth.grantedScopes)) {\n options.runs?.abort(message.params.requestId, 'mcp-cancel');\n }\n return;\n }\n if (!initialized && !isInitializeRequest(message) && !isPingRequest(message)) {\n // IP-21: a frame before `initialize` is a protocol-sequencing error,\n // not an auth failure — the connection is already authenticated.\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.PROTOCOL_VIOLATION,\n 'Send `initialize` before any other RPC.',\n );\n return;\n }\n if (isInitializeRequest(message)) {\n initialized = true;\n sendRpcSuccess(ws, message.id, {\n serverInfo: {\n name: 'graphorin-server',\n version: '0.5.0',\n },\n capabilities: {\n subscriptions: true,\n replayBuffer: true,\n sseFallback: true,\n },\n });\n return;\n }\n if (isPingRequest(message)) {\n const nonce = message.params?.nonce;\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'pong',\n ...(nonce !== undefined ? { nonce } : {}),\n } satisfies ServerMessage),\n );\n sendRpcSuccess(ws, message.id, { ok: true });\n return;\n }\n if (isSubscribeRequest(message)) {\n const subscriptionId = newSubscriptionId();\n const result = options.dispatcher.subscribe({\n subscriberId,\n subject: message.params.subject,\n subscriptionId,\n ...(message.params.sinceEventId !== undefined\n ? { sinceEventId: message.params.sinceEventId }\n : {}),\n });\n if (!result.ok) {\n sendRpcError(\n ws,\n message.id,\n mapSubscribeErrorCode(result.reason),\n mapSubscribeErrorMessage(result.reason, message.params.subject),\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId,\n subject: message.params.subject,\n snapshotEventId: result.snapshotEventId,\n replayedCount: result.replayedCount,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'subscribed',\n subscriptionId,\n subject: message.params.subject,\n ...(result.snapshotEventId !== undefined\n ? { snapshotEventId: result.snapshotEventId }\n : {}),\n } satisfies ServerMessage),\n );\n return;\n }\n if (isUnsubscribeRequest(message)) {\n const removed = options.dispatcher.unsubscribe(message.params.subscriptionId);\n if (!removed) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,\n `Subscription '${message.params.subscriptionId}' not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n subscriptionId: message.params.subscriptionId,\n unsubscribed: true,\n });\n ws.send(\n JSON.stringify({\n v: '1',\n kind: 'unsubscribed',\n subscriptionId: message.params.subscriptionId,\n } satisfies ServerMessage),\n );\n return;\n }\n if (isRunCancelRequest(message)) {\n // IP-8: mirror the REST `POST /runs/:runId/abort` scope gate so a\n // bearer token cannot abort an arbitrary run without `agents:invoke`.\n if (!grantsInvokeScope(auth.grantedScopes)) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.SCOPE_DENIED,\n \"Token lacks required scope 'agents:invoke'.\",\n );\n return;\n }\n const runId = message.params.runId;\n const aborted = options.runs?.abort(runId, message.params.reason ?? 'rpc-cancel') === true;\n if (!aborted) {\n sendRpcError(\n ws,\n message.id,\n RPC_ERROR_CODES.RUN_NOT_FOUND,\n `Run '${runId}' is not active.`,\n );\n return;\n }\n sendRpcSuccess(ws, message.id, {\n runId,\n cancelled: true,\n partialStateAvailable: true,\n });\n return;\n }\n },\n onClose: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n onError: () => {\n try {\n unregister?.();\n } finally {\n unregister = undefined;\n }\n },\n };\n}\n\nfunction sendRpcSuccess(ws: WSContext, id: string | number, result: unknown): void {\n const frame: ServerMessage = { v: '1', jsonrpc: '2.0', id, result };\n ws.send(JSON.stringify(frame));\n}\n\nfunction sendRpcError(\n ws: WSContext,\n id: string | number | undefined,\n code: number,\n message: string,\n): void {\n const frame: ServerMessage = {\n v: '1',\n jsonrpc: '2.0',\n id: id ?? 0,\n error: { code, message },\n };\n ws.send(JSON.stringify(frame));\n}\n\nfunction rejectImmediately(code: number, reason: string): WSEvents {\n return {\n onOpen: (_event, ws) => {\n ws.close(code, reason);\n },\n };\n}\n\ninterface ResolvedUpgradeAuthOk {\n readonly kind: 'ok';\n readonly tokenId: string;\n readonly grantedScopes: ReadonlyArray<ParsedScope>;\n}\n\ninterface ResolvedUpgradeAuthDenied {\n readonly kind: 'denied';\n readonly reason: 'auth.required' | 'auth.invalid' | 'auth.scope_denied';\n}\n\ntype ResolvedUpgradeAuth = ResolvedUpgradeAuthOk | ResolvedUpgradeAuthDenied;\n\nasync function resolveUpgradeAuth(\n c: Context<{ Variables: ServerVariables }>,\n options: WsUpgradeOptions,\n): Promise<ResolvedUpgradeAuth> {\n // Phase 14a's HTTP middleware may have already verified the\n // bearer token (`requireAuth` runs before the upgrade route on\n // the authenticated subtree); if so, reuse the result.\n const state = c.var.state;\n if (state?.auth?.kind === 'token') {\n return {\n kind: 'ok',\n tokenId: state.auth.token.tokenId,\n grantedScopes: state.auth.grantedScopes,\n };\n }\n // IP-13: no-auth loopback mode — accept the upgrade with a full scope grant\n // instead of silently failing to mount. Checked before ticket/bearer so the\n // verifier may legitimately be absent.\n if (options.anonymous === true) {\n return { kind: 'ok', tokenId: 'anonymous', grantedScopes: ANONYMOUS_WS_SCOPES };\n }\n const ticketValue = parseTicketSubprotocol(c.req.header('sec-websocket-protocol') ?? '');\n if (ticketValue !== undefined) {\n const consumed = options.tickets.consume(ticketValue);\n if (!consumed.ok) {\n return { kind: 'denied', reason: 'auth.invalid' };\n }\n return acceptTicket(consumed.ticket);\n }\n const header = c.req.header('authorization') ?? c.req.header('Authorization');\n if (header?.toLowerCase().startsWith('bearer ') === true) {\n if (options.verifier === undefined) return { kind: 'denied', reason: 'auth.required' };\n const token = header.slice(7).trim();\n const verified = await options.verifier.verify(token);\n if (!verified.ok) return { kind: 'denied', reason: 'auth.invalid' };\n return {\n kind: 'ok',\n tokenId: verified.token.tokenId,\n grantedScopes: verified.token.scopes,\n };\n }\n return { kind: 'denied', reason: 'auth.required' };\n}\n\n/**\n * IP-8: the `agents:invoke` scope required to cancel a run over the WS\n * transport — the same requirement the REST `POST /runs/:runId/abort` route\n * enforces. `scopeMatches` honours wildcards (`agents:*`, `admin:*`).\n */\nconst INVOKE_SCOPE: ParsedScope = parseScope('agents:invoke');\n\n/**\n * IP-13: full scope grant handed to a no-auth (`auth.kind='none'`) upgrade.\n * `admin:*` matches every required scope, including the {@link INVOKE_SCOPE}\n * gate on run cancellation.\n */\nconst ANONYMOUS_WS_SCOPES: ReadonlyArray<ParsedScope> = [parseScope('admin:*')];\n\nfunction grantsInvokeScope(granted: ReadonlyArray<ParsedScope>): boolean {\n for (const scope of granted) {\n if (scopeMatches(scope, INVOKE_SCOPE)) return true;\n }\n return false;\n}\n\nfunction acceptTicket(ticket: WsTicket): ResolvedUpgradeAuth {\n return {\n kind: 'ok',\n tokenId: ticket.tokenId,\n grantedScopes: ticket.scopes,\n };\n}\n\nfunction mapSubscribeErrorCode(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n): number {\n switch (reason) {\n case 'scope-denied':\n return RPC_ERROR_CODES.SCOPE_DENIED;\n case 'subject-wildcard':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n case 'subject-unknown':\n return RPC_ERROR_CODES.METHOD_NOT_FOUND;\n case 'subject-malformed':\n return RPC_ERROR_CODES.INVALID_PARAMS;\n }\n}\n\nfunction mapSubscribeErrorMessage(\n reason: 'subject-malformed' | 'subject-unknown' | 'subject-wildcard' | 'scope-denied',\n subject: string,\n): string {\n switch (reason) {\n case 'scope-denied':\n return `Token lacks required scope for subject '${subject}'.`;\n case 'subject-wildcard':\n return `Wildcard subjects are not supported in this version. Subject: '${subject}'.`;\n case 'subject-unknown':\n return `Unknown subject grammar: '${subject}'.`;\n case 'subject-malformed':\n return `Subject '${subject}' is malformed.`;\n }\n}\n\nfunction defaultSubscriberId(): string {\n return `sub-conn-${randomToken(8)}`;\n}\n\nfunction defaultSubscriptionId(subscriberId: string): string {\n return `${subscriberId}-${randomToken(6)}`;\n}\n\nfunction randomToken(length: number): string {\n const bytes = new Uint8Array(length);\n if (\n typeof globalThis.crypto !== 'undefined' &&\n typeof globalThis.crypto.getRandomValues === 'function'\n ) {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < length; i += 1) {\n bytes[i] = Math.floor(Math.random() * 256);\n }\n }\n return Buffer.from(bytes).toString('hex');\n}\n\nvoid SUBPROTOCOL_NAME;\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graphorin/server",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Standalone server runtime for the Graphorin framework: createServer({...}) factory, Hono HTTP app, REST endpoints (agents / workflows / sessions / memory / skills / mcp / tokens / audit / health / health/secrets / metrics / triggers / runs/:runId/replay / sessions/:id/replay / audit/verify), per-request authentication + scope middleware (HMAC-SHA256 + pepper, server pepper required), brute-force lockout, IETF draft-07 Idempotency-Key middleware with SQLite backing + LRU read cache, deny-by-default CORS, double-submit CSRF for browser flows, sliding-window rate limit, audit middleware that streams every authenticated request through the tamper-evident hash chain, programmatic AgentRegistry / WorkflowRegistry, lifecycle hooks (beforeStart / onReady / beforeShutdown), pre-bind secrets resolution + storage migration runner that fails fast on missing pepper / unresolvable SecretRef / missing encryption peer, graceful SIGTERM drain with state preservation, durable triggers daemon (cron / interval / idle / event survive process restart per the catch-up policy contract), consolidator daemon lifecycle integration with hard stop timeout, per-subsystem health rollup (storage / embedder / secrets / encryption / consolidator / triggers / replay-buffer), Prometheus text exposition `/v1/metrics` with the canonical graphorin_* metric inventory, scope-enforced replay endpoints (sanitized by default, raw requires `traces:read:raw` admin scope, every invocation appended to the audit chain), and `POST /v1/audit/verify` chain integrity check. Created and maintained by Oleksiy Stepurenko.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Oleksiy Stepurenko",
|
|
7
|
+
"homepage": "https://github.com/o-stepper/graphorin/tree/main/packages/server",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/o-stepper/graphorin.git",
|
|
11
|
+
"directory": "packages/server"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/o-stepper/graphorin/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"graphorin",
|
|
18
|
+
"ai",
|
|
19
|
+
"agents",
|
|
20
|
+
"framework",
|
|
21
|
+
"server",
|
|
22
|
+
"http",
|
|
23
|
+
"rest",
|
|
24
|
+
"hono",
|
|
25
|
+
"idempotency",
|
|
26
|
+
"auth",
|
|
27
|
+
"scopes",
|
|
28
|
+
"rate-limit",
|
|
29
|
+
"cors",
|
|
30
|
+
"csrf",
|
|
31
|
+
"audit",
|
|
32
|
+
"lifecycle",
|
|
33
|
+
"graceful-shutdown"
|
|
34
|
+
],
|
|
35
|
+
"type": "module",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.0.0"
|
|
38
|
+
},
|
|
39
|
+
"main": "./dist/index.js",
|
|
40
|
+
"module": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"import": "./dist/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./config": {
|
|
48
|
+
"types": "./dist/config.d.ts",
|
|
49
|
+
"import": "./dist/config.js"
|
|
50
|
+
},
|
|
51
|
+
"./middleware": {
|
|
52
|
+
"types": "./dist/middleware/index.d.ts",
|
|
53
|
+
"import": "./dist/middleware/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./errors": {
|
|
56
|
+
"types": "./dist/errors/index.d.ts",
|
|
57
|
+
"import": "./dist/errors/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./registry": {
|
|
60
|
+
"types": "./dist/registry/index.d.ts",
|
|
61
|
+
"import": "./dist/registry/index.js"
|
|
62
|
+
},
|
|
63
|
+
"./ws": {
|
|
64
|
+
"types": "./dist/ws/index.d.ts",
|
|
65
|
+
"import": "./dist/ws/index.js"
|
|
66
|
+
},
|
|
67
|
+
"./sse": {
|
|
68
|
+
"types": "./dist/sse/index.d.ts",
|
|
69
|
+
"import": "./dist/sse/index.js"
|
|
70
|
+
},
|
|
71
|
+
"./commentary": {
|
|
72
|
+
"types": "./dist/commentary/index.d.ts",
|
|
73
|
+
"import": "./dist/commentary/index.js"
|
|
74
|
+
},
|
|
75
|
+
"./triggers": {
|
|
76
|
+
"types": "./dist/triggers/index.d.ts",
|
|
77
|
+
"import": "./dist/triggers/index.js"
|
|
78
|
+
},
|
|
79
|
+
"./consolidator": {
|
|
80
|
+
"types": "./dist/consolidator/index.d.ts",
|
|
81
|
+
"import": "./dist/consolidator/index.js"
|
|
82
|
+
},
|
|
83
|
+
"./health": {
|
|
84
|
+
"types": "./dist/health/index.d.ts",
|
|
85
|
+
"import": "./dist/health/index.js"
|
|
86
|
+
},
|
|
87
|
+
"./metrics": {
|
|
88
|
+
"types": "./dist/metrics/index.d.ts",
|
|
89
|
+
"import": "./dist/metrics/index.js"
|
|
90
|
+
},
|
|
91
|
+
"./replay": {
|
|
92
|
+
"types": "./dist/replay/index.d.ts",
|
|
93
|
+
"import": "./dist/replay/index.js"
|
|
94
|
+
},
|
|
95
|
+
"./package.json": "./package.json"
|
|
96
|
+
},
|
|
97
|
+
"files": [
|
|
98
|
+
"dist",
|
|
99
|
+
"README.md",
|
|
100
|
+
"CHANGELOG.md",
|
|
101
|
+
"LICENSE"
|
|
102
|
+
],
|
|
103
|
+
"dependencies": {
|
|
104
|
+
"@hono/node-server": "^1.19.0",
|
|
105
|
+
"@hono/node-ws": "^1.3.0",
|
|
106
|
+
"hono": "^4.12.21",
|
|
107
|
+
"zod": "^3.25.0",
|
|
108
|
+
"@graphorin/core": "0.5.0",
|
|
109
|
+
"@graphorin/observability": "0.5.0",
|
|
110
|
+
"@graphorin/protocol": "0.5.0",
|
|
111
|
+
"@graphorin/security": "0.5.0",
|
|
112
|
+
"@graphorin/store-sqlite": "0.5.0",
|
|
113
|
+
"@graphorin/triggers": "0.5.0"
|
|
114
|
+
},
|
|
115
|
+
"peerDependencies": {
|
|
116
|
+
"@graphorin/agent": "0.5.0",
|
|
117
|
+
"@graphorin/memory": "0.5.0",
|
|
118
|
+
"@graphorin/sessions": "0.5.0",
|
|
119
|
+
"@graphorin/workflow": "0.5.0"
|
|
120
|
+
},
|
|
121
|
+
"peerDependenciesMeta": {
|
|
122
|
+
"@graphorin/agent": {
|
|
123
|
+
"optional": true
|
|
124
|
+
},
|
|
125
|
+
"@graphorin/memory": {
|
|
126
|
+
"optional": true
|
|
127
|
+
},
|
|
128
|
+
"@graphorin/sessions": {
|
|
129
|
+
"optional": true
|
|
130
|
+
},
|
|
131
|
+
"@graphorin/workflow": {
|
|
132
|
+
"optional": true
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"publishConfig": {
|
|
136
|
+
"access": "public",
|
|
137
|
+
"provenance": true
|
|
138
|
+
},
|
|
139
|
+
"devDependencies": {
|
|
140
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
141
|
+
"@types/ws": "^8.18.1",
|
|
142
|
+
"better-sqlite3": "^12.9.0",
|
|
143
|
+
"ws": "^8.20.1",
|
|
144
|
+
"@graphorin/agent": "0.5.0",
|
|
145
|
+
"@graphorin/memory": "0.5.0",
|
|
146
|
+
"@graphorin/sessions": "0.5.0",
|
|
147
|
+
"@graphorin/workflow": "0.5.0"
|
|
148
|
+
},
|
|
149
|
+
"scripts": {
|
|
150
|
+
"build": "tsdown",
|
|
151
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
|
|
152
|
+
"test": "vitest run",
|
|
153
|
+
"lint": "biome check .",
|
|
154
|
+
"clean": "rimraf dist .turbo *.tsbuildinfo"
|
|
155
|
+
}
|
|
156
|
+
}
|