@ereo/rpc 0.1.6
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 +357 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +433 -0
- package/dist/src/client.d.ts +56 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/context-bridge.d.ts +79 -0
- package/dist/src/context-bridge.d.ts.map +1 -0
- package/dist/src/hooks.d.ts +80 -0
- package/dist/src/hooks.d.ts.map +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +616 -0
- package/dist/src/middleware.d.ts +123 -0
- package/dist/src/middleware.d.ts.map +1 -0
- package/dist/src/plugin.d.ts +62 -0
- package/dist/src/plugin.d.ts.map +1 -0
- package/dist/src/procedure.d.ts +98 -0
- package/dist/src/procedure.d.ts.map +1 -0
- package/dist/src/router.d.ts +66 -0
- package/dist/src/router.d.ts.map +1 -0
- package/dist/src/types.d.ts +166 -0
- package/dist/src/types.d.ts.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/procedure.ts
|
|
3
|
+
function createProcedureBuilder(middlewares = []) {
|
|
4
|
+
return {
|
|
5
|
+
use(middleware) {
|
|
6
|
+
return createProcedureBuilder([
|
|
7
|
+
...middlewares,
|
|
8
|
+
{ fn: middleware }
|
|
9
|
+
]);
|
|
10
|
+
},
|
|
11
|
+
query(schemaOrHandler, maybeHandler) {
|
|
12
|
+
if (typeof schemaOrHandler === "function") {
|
|
13
|
+
return {
|
|
14
|
+
_type: "query",
|
|
15
|
+
_ctx: undefined,
|
|
16
|
+
_input: undefined,
|
|
17
|
+
_output: undefined,
|
|
18
|
+
middlewares: [...middlewares],
|
|
19
|
+
handler: schemaOrHandler
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
_type: "query",
|
|
24
|
+
_ctx: undefined,
|
|
25
|
+
_input: undefined,
|
|
26
|
+
_output: undefined,
|
|
27
|
+
middlewares: [...middlewares],
|
|
28
|
+
inputSchema: schemaOrHandler,
|
|
29
|
+
handler: maybeHandler
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
mutation(schemaOrHandler, maybeHandler) {
|
|
33
|
+
if (typeof schemaOrHandler === "function") {
|
|
34
|
+
return {
|
|
35
|
+
_type: "mutation",
|
|
36
|
+
_ctx: undefined,
|
|
37
|
+
_input: undefined,
|
|
38
|
+
_output: undefined,
|
|
39
|
+
middlewares: [...middlewares],
|
|
40
|
+
handler: schemaOrHandler
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
_type: "mutation",
|
|
45
|
+
_ctx: undefined,
|
|
46
|
+
_input: undefined,
|
|
47
|
+
_output: undefined,
|
|
48
|
+
middlewares: [...middlewares],
|
|
49
|
+
inputSchema: schemaOrHandler,
|
|
50
|
+
handler: maybeHandler
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
subscription(schemaOrHandler, maybeHandler) {
|
|
54
|
+
if (typeof schemaOrHandler === "function") {
|
|
55
|
+
return {
|
|
56
|
+
_type: "subscription",
|
|
57
|
+
_ctx: undefined,
|
|
58
|
+
_input: undefined,
|
|
59
|
+
_output: undefined,
|
|
60
|
+
middlewares: [...middlewares],
|
|
61
|
+
handler: schemaOrHandler
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
_type: "subscription",
|
|
66
|
+
_ctx: undefined,
|
|
67
|
+
_input: undefined,
|
|
68
|
+
_output: undefined,
|
|
69
|
+
middlewares: [...middlewares],
|
|
70
|
+
inputSchema: schemaOrHandler,
|
|
71
|
+
handler: maybeHandler
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
var procedure = createProcedureBuilder();
|
|
77
|
+
async function executeMiddleware(middlewares, initialCtx) {
|
|
78
|
+
let currentCtx = initialCtx;
|
|
79
|
+
for (const middleware of middlewares) {
|
|
80
|
+
const result = await middleware.fn({
|
|
81
|
+
ctx: currentCtx,
|
|
82
|
+
next: (ctx) => ({ ok: true, ctx })
|
|
83
|
+
});
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
currentCtx = result.ctx;
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, ctx: currentCtx };
|
|
90
|
+
}
|
|
91
|
+
function query(schemaOrHandler, maybeHandler) {
|
|
92
|
+
if (typeof schemaOrHandler === "function") {
|
|
93
|
+
return procedure.query(schemaOrHandler);
|
|
94
|
+
}
|
|
95
|
+
return procedure.query(schemaOrHandler, maybeHandler);
|
|
96
|
+
}
|
|
97
|
+
function mutation(schemaOrHandler, maybeHandler) {
|
|
98
|
+
if (typeof schemaOrHandler === "function") {
|
|
99
|
+
return procedure.mutation(schemaOrHandler);
|
|
100
|
+
}
|
|
101
|
+
return procedure.mutation(schemaOrHandler, maybeHandler);
|
|
102
|
+
}
|
|
103
|
+
function subscription(schemaOrHandler, maybeHandler) {
|
|
104
|
+
if (typeof schemaOrHandler === "function") {
|
|
105
|
+
return procedure.subscription(schemaOrHandler);
|
|
106
|
+
}
|
|
107
|
+
return procedure.subscription(schemaOrHandler, maybeHandler);
|
|
108
|
+
}
|
|
109
|
+
// src/router.ts
|
|
110
|
+
function createRouter(def) {
|
|
111
|
+
return {
|
|
112
|
+
_def: def,
|
|
113
|
+
handler: createHttpHandler(def),
|
|
114
|
+
websocket: createWebSocketHandler(def)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function createHttpHandler(def) {
|
|
118
|
+
return async (request, ctx) => {
|
|
119
|
+
let rpcRequest;
|
|
120
|
+
try {
|
|
121
|
+
if (request.method === "GET") {
|
|
122
|
+
const url = new URL(request.url);
|
|
123
|
+
const path = url.searchParams.get("path")?.split(".") ?? [];
|
|
124
|
+
const inputRaw = url.searchParams.get("input");
|
|
125
|
+
rpcRequest = {
|
|
126
|
+
path,
|
|
127
|
+
type: "query",
|
|
128
|
+
input: inputRaw ? JSON.parse(inputRaw) : undefined
|
|
129
|
+
};
|
|
130
|
+
} else {
|
|
131
|
+
rpcRequest = await request.json();
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
return jsonResponse({
|
|
135
|
+
ok: false,
|
|
136
|
+
error: { code: "PARSE_ERROR", message: "Invalid RPC request" }
|
|
137
|
+
}, 400);
|
|
138
|
+
}
|
|
139
|
+
const procedure2 = resolveProcedure(def, rpcRequest.path);
|
|
140
|
+
if (!procedure2) {
|
|
141
|
+
return jsonResponse({
|
|
142
|
+
ok: false,
|
|
143
|
+
error: { code: "NOT_FOUND", message: `Procedure not found: ${rpcRequest.path.join(".")}` }
|
|
144
|
+
}, 404);
|
|
145
|
+
}
|
|
146
|
+
if (procedure2._type === "subscription") {
|
|
147
|
+
return jsonResponse({
|
|
148
|
+
ok: false,
|
|
149
|
+
error: { code: "METHOD_NOT_ALLOWED", message: "Subscriptions must use WebSocket" }
|
|
150
|
+
}, 400);
|
|
151
|
+
}
|
|
152
|
+
if (procedure2._type !== rpcRequest.type) {
|
|
153
|
+
return jsonResponse({
|
|
154
|
+
ok: false,
|
|
155
|
+
error: { code: "METHOD_MISMATCH", message: `Expected ${procedure2._type}, got ${rpcRequest.type}` }
|
|
156
|
+
}, 400);
|
|
157
|
+
}
|
|
158
|
+
const baseContext = { ctx, request };
|
|
159
|
+
const middlewareResult = await executeMiddleware(procedure2.middlewares, baseContext);
|
|
160
|
+
if (!middlewareResult.ok) {
|
|
161
|
+
return jsonResponse({ ok: false, error: middlewareResult.error }, 400);
|
|
162
|
+
}
|
|
163
|
+
let input = rpcRequest.input;
|
|
164
|
+
if (procedure2.inputSchema) {
|
|
165
|
+
try {
|
|
166
|
+
input = procedure2.inputSchema.parse(rpcRequest.input);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const sanitizedError = sanitizeValidationError(error);
|
|
169
|
+
return jsonResponse({
|
|
170
|
+
ok: false,
|
|
171
|
+
error: { code: "VALIDATION_ERROR", message: "Input validation failed", details: sanitizedError }
|
|
172
|
+
}, 400);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const result = await procedure2.handler({ ...middlewareResult.ctx, input });
|
|
177
|
+
return jsonResponse({ ok: true, data: result });
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof RPCError) {
|
|
180
|
+
return jsonResponse({ ok: false, error: { code: error.code, message: error.message } }, error.status);
|
|
181
|
+
}
|
|
182
|
+
console.error(`RPC error [${rpcRequest.path.join(".")}]:`, error);
|
|
183
|
+
return jsonResponse({
|
|
184
|
+
ok: false,
|
|
185
|
+
error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred" }
|
|
186
|
+
}, 500);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function createWebSocketHandler(def) {
|
|
191
|
+
return {
|
|
192
|
+
open(ws) {
|
|
193
|
+
ws.data.subscriptions = new Map;
|
|
194
|
+
},
|
|
195
|
+
async message(ws, message) {
|
|
196
|
+
let msg;
|
|
197
|
+
try {
|
|
198
|
+
msg = JSON.parse(typeof message === "string" ? message : message.toString());
|
|
199
|
+
} catch {
|
|
200
|
+
sendError(ws, "", "PARSE_ERROR", "Invalid message format");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (msg.type === "ping") {
|
|
204
|
+
const pongMsg = { type: "pong" };
|
|
205
|
+
ws.send(JSON.stringify(pongMsg));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (msg.type === "subscribe") {
|
|
209
|
+
await handleSubscribe(ws, def, msg);
|
|
210
|
+
} else if (msg.type === "unsubscribe") {
|
|
211
|
+
handleUnsubscribe(ws, msg.id);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
close(ws) {
|
|
215
|
+
for (const [, controller] of ws.data.subscriptions) {
|
|
216
|
+
controller.abort();
|
|
217
|
+
}
|
|
218
|
+
ws.data.subscriptions.clear();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async function handleSubscribe(ws, def, msg) {
|
|
223
|
+
const { id, path, input } = msg;
|
|
224
|
+
if (ws.data.subscriptions.has(id)) {
|
|
225
|
+
sendError(ws, id, "DUPLICATE_ID", "Subscription ID already in use");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const procedure2 = resolveProcedure(def, path);
|
|
229
|
+
if (!procedure2) {
|
|
230
|
+
sendError(ws, id, "NOT_FOUND", `Procedure not found: ${path.join(".")}`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (procedure2._type !== "subscription") {
|
|
234
|
+
sendError(ws, id, "METHOD_MISMATCH", "Procedure is not a subscription");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const baseContext = {
|
|
238
|
+
ctx: ws.data.ctx,
|
|
239
|
+
request: ws.data.originalRequest ?? new Request("ws://localhost")
|
|
240
|
+
};
|
|
241
|
+
const middlewareResult = await executeMiddleware(procedure2.middlewares, baseContext);
|
|
242
|
+
if (!middlewareResult.ok) {
|
|
243
|
+
sendError(ws, id, middlewareResult.error.code, middlewareResult.error.message);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
let validatedInput = input;
|
|
247
|
+
if (procedure2.inputSchema) {
|
|
248
|
+
try {
|
|
249
|
+
validatedInput = procedure2.inputSchema.parse(input);
|
|
250
|
+
} catch {
|
|
251
|
+
sendError(ws, id, "VALIDATION_ERROR", "Input validation failed");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const controller = new AbortController;
|
|
256
|
+
ws.data.subscriptions.set(id, controller);
|
|
257
|
+
const sub = procedure2;
|
|
258
|
+
try {
|
|
259
|
+
const generator = sub.handler({ ...middlewareResult.ctx, input: validatedInput });
|
|
260
|
+
(async () => {
|
|
261
|
+
try {
|
|
262
|
+
for await (const value of generator) {
|
|
263
|
+
if (controller.signal.aborted)
|
|
264
|
+
break;
|
|
265
|
+
const msg2 = { type: "data", id, data: value };
|
|
266
|
+
ws.send(JSON.stringify(msg2));
|
|
267
|
+
}
|
|
268
|
+
if (!controller.signal.aborted) {
|
|
269
|
+
const msg2 = { type: "complete", id };
|
|
270
|
+
ws.send(JSON.stringify(msg2));
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (!controller.signal.aborted) {
|
|
274
|
+
const errorMsg = error instanceof Error ? error.message : "Subscription error";
|
|
275
|
+
sendError(ws, id, "SUBSCRIPTION_ERROR", errorMsg);
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
ws.data.subscriptions.delete(id);
|
|
279
|
+
}
|
|
280
|
+
})();
|
|
281
|
+
} catch (error) {
|
|
282
|
+
ws.data.subscriptions.delete(id);
|
|
283
|
+
const errorMsg = error instanceof Error ? error.message : "Failed to start subscription";
|
|
284
|
+
sendError(ws, id, "SUBSCRIPTION_ERROR", errorMsg);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function handleUnsubscribe(ws, id) {
|
|
288
|
+
const controller = ws.data.subscriptions.get(id);
|
|
289
|
+
if (controller) {
|
|
290
|
+
controller.abort();
|
|
291
|
+
ws.data.subscriptions.delete(id);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function sendError(ws, id, code, message) {
|
|
295
|
+
const msg = { type: "error", id, error: { code, message } };
|
|
296
|
+
ws.send(JSON.stringify(msg));
|
|
297
|
+
}
|
|
298
|
+
function sanitizeValidationError(error) {
|
|
299
|
+
if (error instanceof Error) {
|
|
300
|
+
const anyError = error;
|
|
301
|
+
if (Array.isArray(anyError.issues)) {
|
|
302
|
+
return anyError.issues.map((issue) => ({
|
|
303
|
+
path: issue.path,
|
|
304
|
+
message: issue.message,
|
|
305
|
+
code: issue.code
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
return { message: error.message };
|
|
309
|
+
}
|
|
310
|
+
return { message: "Validation failed" };
|
|
311
|
+
}
|
|
312
|
+
function resolveProcedure(def, path) {
|
|
313
|
+
let current = def;
|
|
314
|
+
for (const segment of path) {
|
|
315
|
+
if (!current || typeof current !== "object" || !(segment in current)) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
current = current[segment];
|
|
319
|
+
}
|
|
320
|
+
if (current && typeof current === "object" && "_type" in current) {
|
|
321
|
+
return current;
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
function jsonResponse(data, status = 200) {
|
|
326
|
+
return new Response(JSON.stringify(data), {
|
|
327
|
+
status,
|
|
328
|
+
headers: { "Content-Type": "application/json" }
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
class RPCError extends Error {
|
|
333
|
+
code;
|
|
334
|
+
status;
|
|
335
|
+
constructor(code, message, status = 400) {
|
|
336
|
+
super(message);
|
|
337
|
+
this.code = code;
|
|
338
|
+
this.status = status;
|
|
339
|
+
this.name = "RPCError";
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
var errors = {
|
|
343
|
+
unauthorized: (message = "Unauthorized") => new RPCError("UNAUTHORIZED", message, 401),
|
|
344
|
+
forbidden: (message = "Forbidden") => new RPCError("FORBIDDEN", message, 403),
|
|
345
|
+
notFound: (message = "Not found") => new RPCError("NOT_FOUND", message, 404),
|
|
346
|
+
badRequest: (message) => new RPCError("BAD_REQUEST", message, 400)
|
|
347
|
+
};
|
|
348
|
+
// src/plugin.ts
|
|
349
|
+
function rpcPlugin(options) {
|
|
350
|
+
const { router, endpoint = "/api/rpc" } = options;
|
|
351
|
+
const rpcMiddleware = async (request, context, next) => {
|
|
352
|
+
const url = new URL(request.url);
|
|
353
|
+
if (url.pathname === endpoint) {
|
|
354
|
+
const upgradeHeader = request.headers.get("Upgrade");
|
|
355
|
+
if (upgradeHeader?.toLowerCase() === "websocket") {
|
|
356
|
+
return new Response(null, {
|
|
357
|
+
status: 101,
|
|
358
|
+
headers: {
|
|
359
|
+
"X-Ereo-RPC-Upgrade": "websocket"
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return router.handler(request, context);
|
|
364
|
+
}
|
|
365
|
+
return next();
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
name: "@ereo/rpc",
|
|
369
|
+
runtimeMiddleware: [rpcMiddleware],
|
|
370
|
+
virtualModules: {
|
|
371
|
+
"virtual:ereo-rpc-client": `
|
|
372
|
+
import { createClient } from '@ereo/rpc/client';
|
|
373
|
+
|
|
374
|
+
const wsProtocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
375
|
+
const wsEndpoint = typeof window !== 'undefined'
|
|
376
|
+
? wsProtocol + '//' + window.location.host + '${endpoint}'
|
|
377
|
+
: 'ws://localhost:3000${endpoint}';
|
|
378
|
+
|
|
379
|
+
export const rpc = createClient({
|
|
380
|
+
httpEndpoint: '${endpoint}',
|
|
381
|
+
wsEndpoint,
|
|
382
|
+
});
|
|
383
|
+
`
|
|
384
|
+
},
|
|
385
|
+
getWebSocketConfig() {
|
|
386
|
+
return router.websocket;
|
|
387
|
+
},
|
|
388
|
+
upgradeToWebSocket(server, request, ctx) {
|
|
389
|
+
const url = new URL(request.url);
|
|
390
|
+
if (url.pathname !== endpoint) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
const upgradeHeader = request.headers.get("Upgrade");
|
|
394
|
+
if (upgradeHeader?.toLowerCase() !== "websocket") {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const data = {
|
|
398
|
+
subscriptions: new Map,
|
|
399
|
+
ctx,
|
|
400
|
+
originalRequest: request
|
|
401
|
+
};
|
|
402
|
+
const success = server.upgrade(request, { data });
|
|
403
|
+
return success;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// src/context-bridge.ts
|
|
408
|
+
var globalContextProvider = null;
|
|
409
|
+
function setContextProvider(provider) {
|
|
410
|
+
globalContextProvider = provider;
|
|
411
|
+
}
|
|
412
|
+
function getContextProvider() {
|
|
413
|
+
return globalContextProvider;
|
|
414
|
+
}
|
|
415
|
+
function clearContextProvider() {
|
|
416
|
+
globalContextProvider = null;
|
|
417
|
+
}
|
|
418
|
+
async function createSharedContext(request) {
|
|
419
|
+
if (globalContextProvider) {
|
|
420
|
+
return globalContextProvider(request);
|
|
421
|
+
}
|
|
422
|
+
return {};
|
|
423
|
+
}
|
|
424
|
+
function createContextProvider(provider) {
|
|
425
|
+
return provider;
|
|
426
|
+
}
|
|
427
|
+
function withSharedContext() {
|
|
428
|
+
return async ({ ctx, next }) => {
|
|
429
|
+
const sharedCtx = await createSharedContext(ctx.request);
|
|
430
|
+
return next({ ...ctx, ctx: { ...ctx.ctx, ...sharedCtx } });
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function useSharedContext() {
|
|
434
|
+
if (typeof window !== "undefined") {
|
|
435
|
+
return window.__EREO_SHARED_CONTEXT__ ?? null;
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
// src/middleware.ts
|
|
440
|
+
class RateLimitStore {
|
|
441
|
+
stores = new Map;
|
|
442
|
+
cleanupInterval = null;
|
|
443
|
+
CLEANUP_INTERVAL_MS = 60000;
|
|
444
|
+
getStore(windowMs) {
|
|
445
|
+
const key = String(windowMs);
|
|
446
|
+
let store = this.stores.get(key);
|
|
447
|
+
if (!store) {
|
|
448
|
+
store = new Map;
|
|
449
|
+
this.stores.set(key, store);
|
|
450
|
+
this.scheduleCleanup();
|
|
451
|
+
}
|
|
452
|
+
return store;
|
|
453
|
+
}
|
|
454
|
+
scheduleCleanup() {
|
|
455
|
+
if (this.cleanupInterval)
|
|
456
|
+
return;
|
|
457
|
+
this.cleanupInterval = setInterval(() => {
|
|
458
|
+
const now = Date.now();
|
|
459
|
+
for (const [windowMsStr, store] of this.stores) {
|
|
460
|
+
const windowMs = parseInt(windowMsStr, 10);
|
|
461
|
+
for (const [key, entry] of store) {
|
|
462
|
+
if (entry.resetAt < now) {
|
|
463
|
+
store.delete(key);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (store.size === 0) {
|
|
467
|
+
this.stores.delete(windowMsStr);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (this.stores.size === 0 && this.cleanupInterval) {
|
|
471
|
+
clearInterval(this.cleanupInterval);
|
|
472
|
+
this.cleanupInterval = null;
|
|
473
|
+
}
|
|
474
|
+
}, this.CLEANUP_INTERVAL_MS);
|
|
475
|
+
}
|
|
476
|
+
clear() {
|
|
477
|
+
if (this.cleanupInterval) {
|
|
478
|
+
clearInterval(this.cleanupInterval);
|
|
479
|
+
this.cleanupInterval = null;
|
|
480
|
+
}
|
|
481
|
+
this.stores.clear();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
var globalRateLimitStore = new RateLimitStore;
|
|
485
|
+
function logging(options = {}) {
|
|
486
|
+
const { log = console.log, timing = true } = options;
|
|
487
|
+
return async ({ ctx, next }) => {
|
|
488
|
+
const start = timing ? performance.now() : 0;
|
|
489
|
+
const result = await next(ctx);
|
|
490
|
+
if (timing) {
|
|
491
|
+
const duration = (performance.now() - start).toFixed(2);
|
|
492
|
+
log(`[RPC] ${duration}ms`);
|
|
493
|
+
} else {
|
|
494
|
+
log("[RPC] Request completed");
|
|
495
|
+
}
|
|
496
|
+
return result;
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function rateLimit(options) {
|
|
500
|
+
const {
|
|
501
|
+
limit,
|
|
502
|
+
windowMs,
|
|
503
|
+
keyFn = (ctx) => ctx.request.headers.get("x-forwarded-for") ?? "unknown",
|
|
504
|
+
message = "Too many requests"
|
|
505
|
+
} = options;
|
|
506
|
+
const store = globalRateLimitStore.getStore(windowMs);
|
|
507
|
+
return async ({ ctx, next }) => {
|
|
508
|
+
const key = keyFn(ctx);
|
|
509
|
+
const now = Date.now();
|
|
510
|
+
let entry = store.get(key);
|
|
511
|
+
if (!entry || entry.resetAt < now) {
|
|
512
|
+
entry = { count: 0, resetAt: now + windowMs };
|
|
513
|
+
store.set(key, entry);
|
|
514
|
+
}
|
|
515
|
+
entry.count++;
|
|
516
|
+
if (entry.count > limit) {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
error: { code: "RATE_LIMITED", message }
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return next(ctx);
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function clearRateLimitStore() {
|
|
526
|
+
globalRateLimitStore.clear();
|
|
527
|
+
}
|
|
528
|
+
function createAuthMiddleware(getUser, options = {}) {
|
|
529
|
+
const { message = "Unauthorized" } = options;
|
|
530
|
+
return async ({ ctx, next }) => {
|
|
531
|
+
const user = await getUser(ctx);
|
|
532
|
+
if (!user) {
|
|
533
|
+
return {
|
|
534
|
+
ok: false,
|
|
535
|
+
error: { code: "UNAUTHORIZED", message }
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return next({ ...ctx, user });
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function requireRoles(roles, options = {}) {
|
|
542
|
+
const { message = "Insufficient permissions" } = options;
|
|
543
|
+
return async ({ ctx, next }) => {
|
|
544
|
+
const userRole = ctx.user?.role;
|
|
545
|
+
if (!userRole || !roles.includes(userRole)) {
|
|
546
|
+
return {
|
|
547
|
+
ok: false,
|
|
548
|
+
error: { code: "FORBIDDEN", message }
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return next(ctx);
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function validate(validator) {
|
|
555
|
+
return async ({ ctx, next }) => {
|
|
556
|
+
const result = await validator(ctx);
|
|
557
|
+
if (!result.ok) {
|
|
558
|
+
return { ok: false, error: result.error };
|
|
559
|
+
}
|
|
560
|
+
return next(ctx);
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function extend(extender) {
|
|
564
|
+
return async ({ ctx, next }) => {
|
|
565
|
+
const extension = await extender(ctx);
|
|
566
|
+
return next({ ...ctx, ...extension });
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function timing() {
|
|
570
|
+
return async ({ ctx, next }) => {
|
|
571
|
+
const start = performance.now();
|
|
572
|
+
return next({
|
|
573
|
+
...ctx,
|
|
574
|
+
timing: {
|
|
575
|
+
start,
|
|
576
|
+
getDuration: () => performance.now() - start
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function catchErrors(handler) {
|
|
582
|
+
return async ({ ctx, next }) => {
|
|
583
|
+
try {
|
|
584
|
+
return await next(ctx);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
const errorResult = handler(error);
|
|
587
|
+
return { ok: false, error: errorResult };
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
export {
|
|
592
|
+
withSharedContext,
|
|
593
|
+
validate,
|
|
594
|
+
useSharedContext,
|
|
595
|
+
timing,
|
|
596
|
+
subscription,
|
|
597
|
+
setContextProvider,
|
|
598
|
+
rpcPlugin,
|
|
599
|
+
requireRoles,
|
|
600
|
+
rateLimit,
|
|
601
|
+
query,
|
|
602
|
+
procedure,
|
|
603
|
+
mutation,
|
|
604
|
+
logging,
|
|
605
|
+
getContextProvider,
|
|
606
|
+
extend,
|
|
607
|
+
errors,
|
|
608
|
+
createSharedContext,
|
|
609
|
+
createRouter,
|
|
610
|
+
createContextProvider,
|
|
611
|
+
createAuthMiddleware,
|
|
612
|
+
clearRateLimitStore,
|
|
613
|
+
clearContextProvider,
|
|
614
|
+
catchErrors,
|
|
615
|
+
RPCError
|
|
616
|
+
};
|