@node-agent/local 0.0.0-stage → 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/dist/proxy.js ADDED
@@ -0,0 +1,336 @@
1
+ // node-agent: the optional built-in proxy — one token, one login page, one door in front of the page.
2
+ //
3
+ // Off unless serve.ts is given NODE_AGENT_ACCESS_TOKEN: with it unset this module is never called, no
4
+ // port is opened and the loopback behaviour of the entry is exactly what it was.
5
+ //
6
+ // GET /login the form (inline HTML, so the package carries no extra asset)
7
+ // POST /login the token; on a match, one HttpOnly cookie holding sha256(token)
8
+ // GET /healthz liveness, always open, never proxied
9
+ // everything else the cookie or `Authorization: Bearer <token>`, then piped to the upstream
10
+ //
11
+ // The cookie never carries the token, comparisons are constant time, `/bus` (WebSocket upgrade) and
12
+ // `/events` (SSE) are piped rather than buffered, and the upstream is fixed — this cannot become an
13
+ // open proxy. TLS is not here: put your own terminator in front (a reverse proxy, a tunnel, or the
14
+ // Caddy of deploy/docker-compose.yml's `public` profile) and set NODE_AGENT_PROXY_SECURE=1 so the
15
+ // cookie is marked Secure. One token is one user.
16
+ import { createHash, timingSafeEqual } from 'node:crypto';
17
+ import { connect } from 'node:net';
18
+ import { createServer, request as sendRequest, } from 'node:http';
19
+ /** The cookie a successful login hands out. Its value is sha256(token) in hex, never the token. */
20
+ export const COOKIE_NAME = 'node_agent_access';
21
+ /** How long a login lasts, in seconds (30 days), the same as the gateway's cookie. */
22
+ export const COOKIE_MAX_AGE = 2_592_000;
23
+ /**
24
+ * Headers that belong to one connection and must not be forwarded. `cookie` and `host` are handled
25
+ * separately: the browser's cookie is this door's business, not the stack's.
26
+ */
27
+ const HOP_BY_HOP = new Set([
28
+ 'connection',
29
+ 'keep-alive',
30
+ 'proxy-authenticate',
31
+ 'proxy-authorization',
32
+ 'te',
33
+ 'trailer',
34
+ 'transfer-encoding',
35
+ 'upgrade',
36
+ ]);
37
+ const LOGIN_PAGE = `<!doctype html>
38
+ <html lang="en">
39
+ <head>
40
+ <meta charset="utf-8" />
41
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
42
+ <title>node-agent</title>
43
+ <style>
44
+ :root { color-scheme: light dark; }
45
+ body {
46
+ margin: 0;
47
+ min-height: 100vh;
48
+ display: grid;
49
+ place-items: center;
50
+ font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
51
+ background: Canvas;
52
+ color: CanvasText;
53
+ }
54
+ form { width: min(22rem, 90vw); }
55
+ h1 { font-size: 1.1rem; margin: 0 0 0.25rem; }
56
+ p { margin: 0 0 1.25rem; opacity: 0.7; }
57
+ label { display: block; margin-bottom: 0.35rem; }
58
+ input {
59
+ width: 100%;
60
+ box-sizing: border-box;
61
+ padding: 0.6rem 0.7rem;
62
+ border: 1px solid color-mix(in srgb, CanvasText 25%, transparent);
63
+ border-radius: 0.5rem;
64
+ background: Canvas;
65
+ color: inherit;
66
+ font: inherit;
67
+ }
68
+ button {
69
+ margin-top: 0.75rem;
70
+ width: 100%;
71
+ padding: 0.6rem 0.7rem;
72
+ border: 0;
73
+ border-radius: 0.5rem;
74
+ background: color-mix(in srgb, CanvasText 88%, transparent);
75
+ color: Canvas;
76
+ font: inherit;
77
+ font-weight: 600;
78
+ cursor: pointer;
79
+ }
80
+ .error:empty { display: none; }
81
+ .error { margin-top: 0.75rem; color: #c0392b; }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <form method="post" action="/login">
86
+ <h1>node-agent</h1>
87
+ <p>This stack belongs to one person. Enter its token to open it.</p>
88
+ <label for="token">Token</label>
89
+ <input id="token" name="token" type="password" autocomplete="current-password"
90
+ autofocus required />
91
+ <button type="submit">Open</button>
92
+ <div class="error"><!--error--></div>
93
+ </form>
94
+ </body>
95
+ </html>
96
+ `;
97
+ function page(error = '') {
98
+ return LOGIN_PAGE.replace('<!--error-->', error);
99
+ }
100
+ /** The one comparison both the cookie and the bearer token go through. */
101
+ function sameDigest(expected, presented) {
102
+ const digest = createHash('sha256').update(presented).digest();
103
+ return digest.length === expected.length && timingSafeEqual(digest, expected);
104
+ }
105
+ function sameHex(expected, presentedHex) {
106
+ const presented = Buffer.from(presentedHex, 'hex');
107
+ return presented.length === expected.length && timingSafeEqual(presented, expected);
108
+ }
109
+ function cookieOf(req) {
110
+ for (const part of (req.headers.cookie ?? '').split(';')) {
111
+ const [name, ...rest] = part.trim().split('=');
112
+ if (name === COOKIE_NAME)
113
+ return rest.join('=');
114
+ }
115
+ return '';
116
+ }
117
+ function bearerOf(req) {
118
+ const header = req.headers.authorization ?? '';
119
+ const [scheme, ...rest] = header.trim().split(/\s+/);
120
+ if (scheme?.toLowerCase() !== 'bearer')
121
+ return '';
122
+ return rest.join(' ');
123
+ }
124
+ /** A request may prove itself with the cookie (browsers) or a bearer token (scripts). */
125
+ function allowed(req, expected) {
126
+ const cookie = cookieOf(req);
127
+ if (cookie !== '' && sameHex(expected, cookie))
128
+ return true;
129
+ const bearer = bearerOf(req);
130
+ return bearer !== '' && sameDigest(expected, bearer);
131
+ }
132
+ function body(req, limit = 8_192) {
133
+ return new Promise((resolve, reject) => {
134
+ let text = '';
135
+ req.setEncoding('utf8');
136
+ req.on('data', (chunk) => {
137
+ text += chunk;
138
+ if (text.length > limit)
139
+ reject(new Error('body too large'));
140
+ });
141
+ req.on('end', () => resolve(text));
142
+ req.on('error', reject);
143
+ });
144
+ }
145
+ /** Whatever the request carried, the stack behind the door sees only its own headers. */
146
+ function forwardHeaders(req, host) {
147
+ const named = (req.headers.connection ?? '')
148
+ .split(',')
149
+ .map((one) => one.trim().toLowerCase())
150
+ .filter((one) => one !== '');
151
+ const drop = new Set([...HOP_BY_HOP, ...named, 'cookie', 'host']);
152
+ const headers = { host };
153
+ for (const [name, value] of Object.entries(req.headers)) {
154
+ if (value === undefined || drop.has(name.toLowerCase()))
155
+ continue;
156
+ headers[name] = value;
157
+ }
158
+ return headers;
159
+ }
160
+ /** Response headers travel as they are, minus what belongs to this hop, so pipes stay unbuffered. */
161
+ function replyHeaders(answer) {
162
+ const headers = {};
163
+ for (const [name, value] of Object.entries(answer.headers)) {
164
+ if (value === undefined || HOP_BY_HOP.has(name.toLowerCase()))
165
+ continue;
166
+ headers[name] = value;
167
+ }
168
+ return headers;
169
+ }
170
+ /**
171
+ * Start the door. The upstream is fixed at start time; nothing in a request can move it, and an
172
+ * absolute-form request line (`GET http://elsewhere/`) is refused rather than resolved.
173
+ */
174
+ export async function startProxy(options) {
175
+ const token = options.token.trim();
176
+ if (token === '')
177
+ throw new Error('startProxy needs a token');
178
+ const bind = options.bind?.trim() || '127.0.0.1';
179
+ const port = options.port ?? 8080;
180
+ const secure = options.secure === true;
181
+ const upstream = new URL(options.upstream);
182
+ const upstreamHost = upstream.hostname;
183
+ const upstreamPort = Number(upstream.port || (upstream.protocol === 'https:' ? 443 : 80));
184
+ const expected = createHash('sha256').update(token).digest();
185
+ async function login(req, res) {
186
+ if (req.method === 'GET' || req.method === 'HEAD') {
187
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }).end(page());
188
+ return;
189
+ }
190
+ if (req.method !== 'POST') {
191
+ res.writeHead(405, { allow: 'GET, HEAD, POST' }).end();
192
+ return;
193
+ }
194
+ const form = new URLSearchParams(await body(req));
195
+ if (!sameDigest(expected, form.get('token') ?? '')) {
196
+ res
197
+ .writeHead(401, { 'content-type': 'text/html; charset=utf-8' })
198
+ .end(page('That token was not the one.'));
199
+ return;
200
+ }
201
+ const attributes = ['Path=/', 'HttpOnly', 'SameSite=Lax', `Max-Age=${COOKIE_MAX_AGE}`];
202
+ if (secure)
203
+ attributes.push('Secure');
204
+ res
205
+ .writeHead(303, {
206
+ 'set-cookie': `${COOKIE_NAME}=${expected.toString('hex')}; ${attributes.join('; ')}`,
207
+ location: '/',
208
+ 'content-type': 'text/plain; charset=utf-8',
209
+ 'cache-control': 'no-store',
210
+ })
211
+ .end('ok\n');
212
+ }
213
+ function proxy(req, res) {
214
+ const outbound = sendRequest({
215
+ protocol: upstream.protocol,
216
+ host: upstreamHost,
217
+ port: upstreamPort,
218
+ method: req.method,
219
+ path: req.url,
220
+ headers: forwardHeaders(req, upstream.host),
221
+ }, (answer) => {
222
+ res.writeHead(answer.statusCode ?? 502, replyHeaders(answer));
223
+ // pipe, never buffer: /events is a long-lived connection and must reach the browser as it
224
+ // is written, not when it ends.
225
+ answer.pipe(res);
226
+ answer.on('error', () => res.destroy());
227
+ });
228
+ outbound.on('error', () => {
229
+ if (!res.headersSent)
230
+ res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
231
+ res.end('the stack behind this door is not answering\n');
232
+ });
233
+ res.on('close', () => {
234
+ if (!res.writableFinished)
235
+ outbound.destroy();
236
+ });
237
+ req.pipe(outbound);
238
+ }
239
+ /** A WebSocket (or any upgrade) is piped byte for byte: the gate is the cookie, not the frames. */
240
+ function tunnel(req, socket, head) {
241
+ const upstreamSocket = connect(upstreamPort, upstreamHost, () => {
242
+ const lines = [`${req.method} ${req.url} HTTP/1.1`];
243
+ for (const [name, value] of Object.entries(req.headers)) {
244
+ // The upgrade's own hop-by-hop headers are the protocol here, so only the cookie goes.
245
+ if (name === 'cookie' || name === 'host')
246
+ continue;
247
+ for (const one of Array.isArray(value) ? value : [value ?? '']) {
248
+ lines.push(`${name}: ${one}`);
249
+ }
250
+ }
251
+ lines.push(`host: ${upstream.host}`);
252
+ upstreamSocket.write(`${lines.join('\r\n')}\r\n\r\n`);
253
+ if (head.length > 0)
254
+ upstreamSocket.write(head);
255
+ socket.pipe(upstreamSocket).pipe(socket);
256
+ });
257
+ upstreamSocket.on('error', () => socket.destroy());
258
+ socket.on('error', () => upstreamSocket.destroy());
259
+ // An upgraded socket is no longer the server's to close, so the pair is closed here.
260
+ socket.on('close', () => upstreamSocket.destroy());
261
+ upstreamSocket.on('close', () => socket.destroy());
262
+ }
263
+ // Every socket is kept so close() can end the long-lived ones (SSE, WebSocket) instead of waiting
264
+ // for a stream that by design never ends.
265
+ const sockets = new Set();
266
+ const server = createServer((req, res) => {
267
+ const path = (req.url ?? '/').split('?')[0] ?? '/';
268
+ // A door, not a proxy: `GET http://elsewhere/` would otherwise be forwarded as written. The
269
+ // client would have to know the token first, but a fixed upstream is the promise here.
270
+ if (path.startsWith('//') || /^https?:/i.test(req.url ?? '')) {
271
+ res.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' }).end('fixed upstream\n');
272
+ req.resume();
273
+ return;
274
+ }
275
+ if (path === '/login') {
276
+ login(req, res).catch(() => {
277
+ if (!res.headersSent)
278
+ res.writeHead(400, { 'content-type': 'text/plain' });
279
+ res.end('bad request\n');
280
+ });
281
+ return;
282
+ }
283
+ if (path === '/healthz') {
284
+ res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }).end('ok\n');
285
+ req.resume();
286
+ return;
287
+ }
288
+ if (!allowed(req, expected)) {
289
+ // Browsers get the page; anything else (a script, a fetch) gets the status instead of a
290
+ // redirect it cannot follow.
291
+ if (req.method === 'GET' || req.method === 'HEAD') {
292
+ res.writeHead(303, { location: '/login', 'cache-control': 'no-store' }).end();
293
+ }
294
+ else {
295
+ res.writeHead(401, { 'content-type': 'text/plain; charset=utf-8' }).end('unauthorized\n');
296
+ }
297
+ req.resume();
298
+ return;
299
+ }
300
+ proxy(req, res);
301
+ });
302
+ server.on('connection', (socket) => {
303
+ sockets.add(socket);
304
+ socket.on('close', () => sockets.delete(socket));
305
+ });
306
+ server.on('upgrade', (req, socket, head) => {
307
+ if (!allowed(req, expected)) {
308
+ socket.end('HTTP/1.1 401 Unauthorized\r\nconnection: close\r\n\r\n');
309
+ return;
310
+ }
311
+ tunnel(req, socket, head);
312
+ });
313
+ return new Promise((resolve, reject) => {
314
+ server.once('error', reject);
315
+ server.listen(port, bind, () => {
316
+ server.removeListener('error', reject);
317
+ const address = server.address();
318
+ const bound = address?.port ?? port;
319
+ const host = bind.includes(':') ? `[${bind}]` : bind;
320
+ resolve({
321
+ url: `http://${host}:${bound}`,
322
+ bind,
323
+ port: bound,
324
+ close: () => new Promise((done, fail) => {
325
+ // An SSE stream never ends on its own, so closing must end it rather than wait for it.
326
+ for (const socket of sockets)
327
+ socket.destroy();
328
+ sockets.clear();
329
+ server.closeAllConnections();
330
+ server.close((error) => (error === undefined ? done() : fail(error)));
331
+ }),
332
+ });
333
+ });
334
+ });
335
+ }
336
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LKJacky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,101 @@
1
+ export type RuntimeState = 'starting' | 'running' | 'stopped' | 'failed';
2
+ export type Env = Record<string, string | undefined>;
3
+ /** A small logger, so a runtime can be used from a process that already has one. */
4
+ export interface RuntimeLogger {
5
+ info(message: string): void;
6
+ warn(message: string): void;
7
+ error(message: string): void;
8
+ }
9
+ export interface RuntimeServerOptions {
10
+ /** Runtime-owned state (users, packages). Defaults to `<NODE_AGENT_HOME>/runtime`. */
11
+ home?: string;
12
+ /** Address the user's bus listens on. Defaults to loopback. */
13
+ host?: string;
14
+ /** Base port for allocation. 0 (the default) asks the OS for a free port. */
15
+ basePort?: number;
16
+ /** How many ports above basePort may be tried. Defaults to 100. */
17
+ portSpan?: number;
18
+ /** Token used for the bus. A runtime-generated token is used when unset. */
19
+ token?: string;
20
+ /** Extra environment for every child, after the runtime's own defaults. */
21
+ env?: Env;
22
+ /** Node binary. Defaults to process.execPath. */
23
+ nodeBinary?: string;
24
+ /** How a node is isolated: a worker thread in this runtime process, or a child process.
25
+ * Worker is the default, so a personal stack is one OS process: bus + every default node. */
26
+ isolation?: 'worker' | 'process';
27
+ logger?: RuntimeLogger;
28
+ }
29
+ export interface BusOptions {
30
+ host?: string;
31
+ port?: number;
32
+ token?: string;
33
+ env?: Env;
34
+ }
35
+ export interface NodeOptions {
36
+ /** Package root or package name. Defaults to `node` itself. */
37
+ dir?: string;
38
+ /** Bus name. Defaults to the package's `node.id`. */
39
+ name?: string;
40
+ /** Extra environment for this node only. */
41
+ env?: Env;
42
+ /** Extra command line arguments. */
43
+ args?: readonly string[];
44
+ }
45
+ export interface UserHandle {
46
+ readonly user: string;
47
+ readonly home: string;
48
+ state: 'ready' | 'removed';
49
+ }
50
+ export interface BusHandle {
51
+ readonly kind: 'bus';
52
+ readonly host: string;
53
+ readonly port: number;
54
+ readonly url: string;
55
+ readonly token: string;
56
+ readonly pid: number | undefined;
57
+ state: RuntimeState;
58
+ /** Stop this bus. Nodes on it are stopped first. */
59
+ stop(): Promise<void>;
60
+ }
61
+ export interface NodeHandle {
62
+ readonly kind: 'node';
63
+ readonly name: string;
64
+ readonly version: string;
65
+ readonly dir: string;
66
+ readonly pid: number | undefined;
67
+ state: RuntimeState;
68
+ /** Stop this node. */
69
+ stop(): Promise<void>;
70
+ }
71
+ /** The runtime server interface. Every method is async; there is no synchronous twin. */
72
+ export interface Runtime {
73
+ /** Create a user's persistent home. Idempotent. */
74
+ createUser(user: string): Promise<UserHandle>;
75
+ /** Stop the user's bus and nodes, then remove the user's home. Idempotent. */
76
+ destroyUser(user: string): Promise<void>;
77
+ /** Start the shared bus; returns the existing one when it is already running. */
78
+ launchBus(options?: BusOptions): Promise<BusHandle>;
79
+ /** Stop the shared bus and every node still on it. Idempotent. */
80
+ stopBus(): Promise<void>;
81
+ /** Start one shared node against the bus, creating the bus when needed. */
82
+ launchNode(node: string, options?: NodeOptions): Promise<NodeHandle>;
83
+ /** Stop one shared node. Idempotent. */
84
+ stopNode(node: string): Promise<void>;
85
+ /** Compose: ensure the bus and node, then connect that user to the node. */
86
+ connect(user: string, node: string, options?: NodeOptions): Promise<NodeHandle>;
87
+ /** Compose: remove the user's access. The shared node and bus stay up. */
88
+ disconnect(user: string, node: string): Promise<void>;
89
+ /** One user's table, or every user's table when no user is named. */
90
+ sessions(user?: string): Promise<Record<string, unknown>>;
91
+ }
92
+ export type RuntimeErrorCode = 'invalid_user' | 'invalid_node' | 'bus_not_running' | 'bus_start_failed' | 'node_not_running' | 'node_start_failed' | 'package_failed' | 'spawn_failed';
93
+ /** A refusal a caller can show: code is stable, message is for a human. */
94
+ export declare class RuntimeError extends Error {
95
+ readonly code: RuntimeErrorCode;
96
+ constructor(code: RuntimeErrorCode, message: string);
97
+ }
98
+ /** A user name is also a directory name: refuse anything that could escape one. */
99
+ export declare function userName(value: string): string;
100
+ /** A node name is a bus name: same rule the package manifest is held to. */
101
+ export declare function nodeName(value: string): string;
@@ -0,0 +1,32 @@
1
+ // The runtime contract: a server that owns one shared bus and the nodes on it.
2
+ //
3
+ // The interface is deliberately small. A runtime creates and stops two kinds of process — a bus and
4
+ // shared nodes — while a user is only an access relation: connect(user, node) ensures both exist and
5
+ // adds the node to that user's table; disconnect(user, node) removes only the relation.
6
+ //
7
+ // Everything here is transport-neutral. LocalRuntime uses worker threads by default; a remote
8
+ // implementation can implement the same methods over HTTP without changing a caller.
9
+ /** A refusal a caller can show: code is stable, message is for a human. */
10
+ export class RuntimeError extends Error {
11
+ code;
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.name = 'RuntimeError';
15
+ this.code = code;
16
+ }
17
+ }
18
+ const USER = /^[a-z][a-z0-9_-]{0,62}$/;
19
+ const NODE = /^[a-z][a-z0-9-]{0,62}$/;
20
+ /** A user name is also a directory name: refuse anything that could escape one. */
21
+ export function userName(value) {
22
+ if (!USER.test(value))
23
+ throw new RuntimeError('invalid_user', `not a user name: ${JSON.stringify(value)}`);
24
+ return value;
25
+ }
26
+ /** A node name is a bus name: same rule the package manifest is held to. */
27
+ export function nodeName(value) {
28
+ if (!NODE.test(value))
29
+ throw new RuntimeError('invalid_node', `not a node name: ${JSON.stringify(value)}`);
30
+ return value;
31
+ }
32
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.js","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,EAAE;AACF,oGAAoG;AACpG,qGAAqG;AACrG,wFAAwF;AACxF,EAAE;AACF,8FAA8F;AAC9F,qFAAqF;AAkHrF,2EAA2E;AAC3E,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC5B,IAAI,CAAmB;IAEhC,YAAY,IAAsB,EAAE,OAAe;QACjD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,IAAI,GAAG,yBAAyB,CAAC;AACvC,MAAM,IAAI,GAAG,wBAAwB,CAAC;AAEtC,mFAAmF;AACnF,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QACnB,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,oBAAoB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACtF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QACnB,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,oBAAoB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACtF,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './base.js';
2
+ export * from './server.js';
@@ -0,0 +1,9 @@
1
+ // @node-agent/runtime: the runtime contract and its HTTP surface.
2
+ //
3
+ // base.ts the interface (a bus, nodes, users as access relations)
4
+ // server.ts RuntimeServer: the same interface over HTTP, so a runtime can live elsewhere
5
+ //
6
+ // The local, single-user implementation lives in the `node-agent` package.
7
+ export * from './base.js';
8
+ export * from './server.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,uEAAuE;AACvE,4FAA4F;AAC5F,EAAE;AACF,2EAA2E;AAC3E,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
@@ -0,0 +1,44 @@
1
+ import { Bus } from 'node-agent-bus';
2
+ import { type BusHandle, type BusOptions, type NodeHandle, type NodeOptions, type Runtime, type RuntimeServerOptions, type UserHandle } from './base.js';
3
+ export declare class LocalRuntime implements Runtime {
4
+ /** One node launch at a time: two users asking at startup must not spawn the same shared node twice. */
5
+ private launchQueue;
6
+ private readonly options;
7
+ private readonly logger?;
8
+ private busRecord;
9
+ private readonly nodes;
10
+ private readonly knownUsers;
11
+ constructor(options?: RuntimeServerOptions);
12
+ /** The shared bus, once it is listening. Used by the process entry to print its user table. */
13
+ get bus(): Bus | undefined;
14
+ private home;
15
+ private nodeHome;
16
+ /** The bus never sees a display name: it sees a stable digest of the name. */
17
+ private busId;
18
+ busUser(user: string): string;
19
+ private tokenFor;
20
+ launchBus(options?: BusOptions): Promise<BusHandle>;
21
+ stopBus(): Promise<void>;
22
+ launchNode(node: string, options?: NodeOptions): Promise<NodeHandle>;
23
+ private launchNodeNow;
24
+ stopNode(node: string): Promise<void>;
25
+ createUser(user: string): Promise<UserHandle>;
26
+ destroyUser(user: string): Promise<void>;
27
+ connect(user: string, node: string, options?: NodeOptions): Promise<NodeHandle>;
28
+ disconnect(user: string, node: string): Promise<void>;
29
+ /** Every user this runtime has connected, and the shared nodes each one may reach. */
30
+ sessions(user?: string): Promise<Record<string, unknown>>;
31
+ /** The newest package the installer materialized for this user, if there is one. */
32
+ private installedPackage;
33
+ private nodeRunning;
34
+ /** Where the shared page node printed its address, if it has started yet. */
35
+ private webuiTarget;
36
+ /** The shared bus, for a trusted caller that has to proxy an outside node. */
37
+ busTarget(): {
38
+ url: string;
39
+ token: string;
40
+ } | undefined;
41
+ /** Recent stdout/stderr of one running node, for the entry process to surface its address. */
42
+ nodeOutput(node: string): string | undefined;
43
+ stopAll(): Promise<void>;
44
+ }