@cortexkit/common-auth 0.9.0 → 0.9.1
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/dist/rpc/rpc-server.d.ts +16 -3
- package/dist/rpc/rpc-server.js +66 -7
- package/package.json +1 -1
package/dist/rpc/rpc-server.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface RpcServerOptions {
|
|
|
12
12
|
secureDir?: boolean;
|
|
13
13
|
sweepRoot?: string;
|
|
14
14
|
drain: (lastReceivedId: number, sessionId?: string) => RpcNotification[];
|
|
15
|
+
drainAsync?: never;
|
|
16
|
+
/** Replace default cursor parsing with plugin policy over the raw JSON value. */
|
|
17
|
+
parsePending?: (params: unknown) => {
|
|
18
|
+
lastReceivedId: number;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
};
|
|
15
21
|
apply: (request: ApplyRequest) => Promise<ApplyResult>;
|
|
16
22
|
/**
|
|
17
23
|
* Refuse a `pending-notifications` drain whose `sessionId` is absent, not a
|
|
@@ -32,14 +38,21 @@ export interface RpcServerOptions {
|
|
|
32
38
|
*/
|
|
33
39
|
applyDeadlineMs?: number;
|
|
34
40
|
}
|
|
41
|
+
/** Async-only options keep the existing synchronous drain type unchanged. */
|
|
42
|
+
export interface RpcServerAsyncOptions extends Omit<RpcServerOptions, 'drain' | 'drainAsync'> {
|
|
43
|
+
drain?: never;
|
|
44
|
+
drainAsync: (lastReceivedId: number, sessionId?: string) => Promise<RpcNotification[]>;
|
|
45
|
+
}
|
|
35
46
|
/**
|
|
36
|
-
* Thrown by an `apply` or `
|
|
47
|
+
* Thrown by an `apply`, `drain`, or `parsePending` handler to refuse a request with a 4xx
|
|
37
48
|
* status. Its message is sent on the wire as `{error: message}`, so it must
|
|
38
|
-
* be written for the client and never quote a credential. Any other error
|
|
39
|
-
* handler throws answers 500 with a fixed code
|
|
49
|
+
* be written for the client and never quote a credential. Any other error an
|
|
50
|
+
* apply/drain handler throws answers 500 with a fixed code; other parser
|
|
51
|
+
* errors answer 400 invalid params. Async drain failures always answer 500.
|
|
40
52
|
*/
|
|
41
53
|
export declare class RpcRequestError extends Error {
|
|
42
54
|
readonly status: number;
|
|
43
55
|
constructor(status: number, message: string);
|
|
44
56
|
}
|
|
57
|
+
export declare function startRpcServer(options: RpcServerAsyncOptions): Promise<RpcServerHandle>;
|
|
45
58
|
export declare function startRpcServer(options: RpcServerOptions): Promise<RpcServerHandle>;
|
package/dist/rpc/rpc-server.js
CHANGED
|
@@ -5,10 +5,11 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { isSessionId, } from './notifications.js';
|
|
6
6
|
import { sweepRpcState, writePortFile } from './port-file.js';
|
|
7
7
|
/**
|
|
8
|
-
* Thrown by an `apply` or `
|
|
8
|
+
* Thrown by an `apply`, `drain`, or `parsePending` handler to refuse a request with a 4xx
|
|
9
9
|
* status. Its message is sent on the wire as `{error: message}`, so it must
|
|
10
|
-
* be written for the client and never quote a credential. Any other error
|
|
11
|
-
* handler throws answers 500 with a fixed code
|
|
10
|
+
* be written for the client and never quote a credential. Any other error an
|
|
11
|
+
* apply/drain handler throws answers 500 with a fixed code; other parser
|
|
12
|
+
* errors answer 400 invalid params. Async drain failures always answer 500.
|
|
12
13
|
*/
|
|
13
14
|
export class RpcRequestError extends Error {
|
|
14
15
|
status;
|
|
@@ -78,6 +79,9 @@ function tokenOk(header, token) {
|
|
|
78
79
|
return got.length === want.length && timingSafeEqual(got, want);
|
|
79
80
|
}
|
|
80
81
|
export async function startRpcServer(options) {
|
|
82
|
+
if ((typeof options.drain === 'function') ===
|
|
83
|
+
(typeof options.drainAsync === 'function'))
|
|
84
|
+
throw new TypeError('exactly one of drain or drainAsync must be provided');
|
|
81
85
|
const log = options.log ?? { warn() { }, debug() { } };
|
|
82
86
|
const token = randomBytes(32).toString('hex');
|
|
83
87
|
// The receipt timeout limits request delivery, not handler execution.
|
|
@@ -85,12 +89,17 @@ export async function startRpcServer(options) {
|
|
|
85
89
|
const handlerTimeoutMs = options.timeoutMs ?? 90_000;
|
|
86
90
|
const receiptTimeoutMs = options.receiptTimeoutMs ?? 2_000;
|
|
87
91
|
let warnedMissingNotificationSession = false;
|
|
92
|
+
const connections = new Set();
|
|
88
93
|
const server = createServer((req, res) => {
|
|
89
94
|
req.setTimeout(handlerTimeoutMs, () => {
|
|
90
95
|
req.socket.destroy();
|
|
91
96
|
});
|
|
92
97
|
void dispatch(req, res);
|
|
93
98
|
});
|
|
99
|
+
server.on('connection', (socket) => {
|
|
100
|
+
connections.add(socket);
|
|
101
|
+
socket.once('close', () => connections.delete(socket));
|
|
102
|
+
});
|
|
94
103
|
server.requestTimeout = receiptTimeoutMs;
|
|
95
104
|
server.headersTimeout = receiptTimeoutMs;
|
|
96
105
|
async function dispatch(req, res) {
|
|
@@ -132,16 +141,59 @@ export async function startRpcServer(options) {
|
|
|
132
141
|
return json(400, { error: 'invalid json' });
|
|
133
142
|
}
|
|
134
143
|
if (method === 'pending-notifications') {
|
|
135
|
-
|
|
144
|
+
let pending;
|
|
145
|
+
try {
|
|
146
|
+
if (options.parsePending) {
|
|
147
|
+
pending = options.parsePending(params);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (!params || typeof params !== 'object' || Array.isArray(params))
|
|
151
|
+
throw new Error('invalid params');
|
|
152
|
+
const raw = params;
|
|
153
|
+
if (options.requireSession === true && !isSessionId(raw.sessionId))
|
|
154
|
+
return json(400, { error: 'session required' });
|
|
155
|
+
if (('lastReceivedId' in raw &&
|
|
156
|
+
(typeof raw.lastReceivedId !== 'number' ||
|
|
157
|
+
!Number.isSafeInteger(raw.lastReceivedId) ||
|
|
158
|
+
raw.lastReceivedId < 0)) ||
|
|
159
|
+
('sessionId' in raw && typeof raw.sessionId !== 'string'))
|
|
160
|
+
throw new Error('invalid params');
|
|
161
|
+
pending = {
|
|
162
|
+
lastReceivedId: Number(raw.lastReceivedId ?? 0),
|
|
163
|
+
sessionId: typeof raw.sessionId === 'string' ? raw.sessionId : undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof RpcRequestError)
|
|
169
|
+
throw error;
|
|
170
|
+
return json(400, { error: 'invalid params' });
|
|
171
|
+
}
|
|
172
|
+
const { lastReceivedId, sessionId } = pending;
|
|
173
|
+
if (options.requireSession === true && !isSessionId(sessionId))
|
|
136
174
|
return json(400, { error: 'session required' });
|
|
137
|
-
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : undefined;
|
|
138
175
|
if (sessionId === undefined && !warnedMissingNotificationSession) {
|
|
139
176
|
warnedMissingNotificationSession = true;
|
|
140
177
|
log.warn('rpc notification drain missing session id', {
|
|
141
178
|
pid: process.pid,
|
|
142
179
|
});
|
|
143
180
|
}
|
|
144
|
-
|
|
181
|
+
let messages;
|
|
182
|
+
if (options.drainAsync) {
|
|
183
|
+
try {
|
|
184
|
+
messages = await options.drainAsync(lastReceivedId, sessionId);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
log.warn('rpc notification drain failed', {
|
|
188
|
+
pid: process.pid,
|
|
189
|
+
error: error instanceof Error ? error.message : String(error),
|
|
190
|
+
});
|
|
191
|
+
return json(500, { error: 'drain failed' });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
messages = options.drain?.(lastReceivedId, sessionId);
|
|
196
|
+
}
|
|
145
197
|
return json(200, { messages });
|
|
146
198
|
}
|
|
147
199
|
if (method === 'apply') {
|
|
@@ -214,7 +266,14 @@ export async function startRpcServer(options) {
|
|
|
214
266
|
port,
|
|
215
267
|
token,
|
|
216
268
|
async stop() {
|
|
217
|
-
await new Promise((resolve) =>
|
|
269
|
+
await new Promise((resolve) => {
|
|
270
|
+
server.close(() => resolve());
|
|
271
|
+
// Stop accepting connections before ending requests that may never finish.
|
|
272
|
+
server.closeAllConnections?.();
|
|
273
|
+
// Bun exposes closeAllConnections but leaves partial requests open.
|
|
274
|
+
for (const socket of connections)
|
|
275
|
+
socket.destroy();
|
|
276
|
+
});
|
|
218
277
|
const portFile = join(options.dir, `port-${process.pid}.json`);
|
|
219
278
|
const current = await readFile(portFile, 'utf8')
|
|
220
279
|
.then((raw) => JSON.parse(raw))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/common-auth",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Shared code for the CortexKit auth plugins: account pool, quota and routing, commands and auth menu, OpenCode 2 hooks, Claustrum custody, and plumbing (loopback RPC, file locks, logger, sidebar state, TUI preferences and build).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|