@north-light/crouter 0.3.283 → 0.3.284

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.
@@ -28,7 +28,9 @@ export interface CrtrClientOptions {
28
28
  socketPath?: string;
29
29
  /** `http(s)://host:port` for TCP/remote transport. */
30
30
  baseUrl?: string;
31
- /** Extra headers (e.g. an edge auth token on TCP; crtrd ignores it). */
31
+ /** Extra headers, e.g. `{ authorization: 'Bearer <token>' }` when the target
32
+ * crtrd's TCP listener has `CRTRD_TOKEN` set (unix-socket transport is
33
+ * never checked, and a TCP daemon with no token set ignores this too). */
32
34
  headers?: Record<string, string>;
33
35
  /** Autostart on a cold socket (default true for socketPath, false for baseUrl). */
34
36
  autostart?: boolean;
@@ -3,10 +3,14 @@
3
3
  // Two `http.Server` listeners share one logical API (a Node server can bind one
4
4
  // handle only):
5
5
  // - the unix socket at `apiSocketPath()` — ALWAYS bound, owner-only (mode
6
- // 0600 = the fs-perms-are-auth model);
6
+ // 0600 = the fs-perms-are-auth model); NEVER token-checked, token set or
7
+ // not;
7
8
  // - an opt-in TCP listener, bound only when `--tcp <host:port>` / `CRTRD_TCP`
8
- // is set (a fronting edge owns auth there).
9
- // crtrd applies NO auth to either listener (hard constraint, §1).
9
+ // is set. When `CRTRD_TOKEN` is ALSO set, every TCP request and WS upgrade
10
+ // must carry `Authorization: Bearer <token>` or the daemon answers 401
11
+ // with the standard `ErrorBody` (errors.ts) — otherwise (the default) TCP
12
+ // gets no auth check at all, unchanged from before this guard existed (a
13
+ // fronting edge owns auth there instead).
10
14
  //
11
15
  // Non-upgrade requests flow through the hand-rolled `Router` (A-6) loaded with
12
16
  // every handler route table (A-7). `GET /v1/nodes/:id/attach` upgrades are
@@ -17,7 +21,7 @@ import { createServer } from 'node:http';
17
21
  import { chmodSync, existsSync, unlinkSync } from 'node:fs';
18
22
  import { apiSocketPath } from '../../core/canvas/paths.js';
19
23
  import { emitEvent } from '../../core/events/emit.js';
20
- import { envCrtrdTcp } from '../../shared/env.js';
24
+ import { envCrtrdTcp, envCrtrdToken } from '../../shared/env.js';
21
25
  import { Router } from './router.js';
22
26
  import { handleAttachUpgrade } from './bridge.js';
23
27
  import { attachRoutes } from './handlers/attach.js';
@@ -114,6 +118,46 @@ function parseTcp(spec) {
114
118
  return null;
115
119
  return { host: host === '' ? '0.0.0.0' : host, port };
116
120
  }
121
+ /** The standard `ErrorBody` (errors.ts) for a TCP request/upgrade missing or
122
+ * carrying a wrong `Authorization: Bearer <token>` header. Only ever written
123
+ * when `CRTRD_TOKEN` is set — an unset token never reaches this path. */
124
+ const UNAUTHORIZED_BODY = {
125
+ error: { code: 'unauthorized', message: 'missing or invalid bearer token' },
126
+ };
127
+ /** Does `req` carry the exact `Authorization: Bearer <token>` this TCP
128
+ * listener requires? */
129
+ function isAuthorized(req, token) {
130
+ return req.headers['authorization'] === `Bearer ${token}`;
131
+ }
132
+ /** Reject an unauthorized non-upgrade TCP request with the standard 401 body. */
133
+ function writeUnauthorized(res) {
134
+ const payload = JSON.stringify(UNAUTHORIZED_BODY);
135
+ res.writeHead(401, { 'content-type': 'application/json; charset=utf-8' });
136
+ res.end(payload);
137
+ }
138
+ /** Reject an unauthorized TCP WS-upgrade attempt: this is a raw pre-upgrade
139
+ * socket (no `ServerResponse`), so the 401 is written as a literal HTTP
140
+ * response — mirroring `bridge.ts`'s `refuseUpgrade` (`end()`, not
141
+ * `write()`+`destroy()`, so a slow peer can't truncate the body) — before the
142
+ * socket closes. */
143
+ function destroyUpgradeUnauthorized(socket) {
144
+ const payload = JSON.stringify(UNAUTHORIZED_BODY);
145
+ const response = `HTTP/1.1 401 Unauthorized\r\n` +
146
+ `content-type: application/json; charset=utf-8\r\n` +
147
+ `content-length: ${Buffer.byteLength(payload)}\r\n` +
148
+ `connection: close\r\n\r\n${payload}`;
149
+ try {
150
+ socket.end(response);
151
+ }
152
+ catch {
153
+ try {
154
+ socket.destroy();
155
+ }
156
+ catch {
157
+ /* already gone */
158
+ }
159
+ }
160
+ }
117
161
  /** Close one http.Server, dropping idle keep-alives so the callback fires. */
118
162
  function closeServer(server) {
119
163
  return new Promise((resolve) => {
@@ -244,7 +288,9 @@ export function createApiServer(opts = {}) {
244
288
  });
245
289
  }
246
290
  });
247
- // TCP — opt-in only. A SECOND http.Server sharing the same handlers.
291
+ // TCP — opt-in only. A SECOND http.Server sharing the same handlers, gated
292
+ // by CRTRD_TOKEN when set (never the unix socket, which shares onRequest/
293
+ // onUpgrade directly above with no wrapping at all).
248
294
  let tcpServer;
249
295
  const tcpSpec = opts.tcp ?? envCrtrdTcp();
250
296
  if (tcpSpec !== undefined && tcpSpec !== '') {
@@ -257,8 +303,27 @@ export function createApiServer(opts = {}) {
257
303
  });
258
304
  }
259
305
  else {
260
- tcpServer = createServer(onRequest);
261
- tcpServer.on('upgrade', onUpgrade);
306
+ const token = envCrtrdToken();
307
+ const tcpOnRequest = token === undefined
308
+ ? onRequest
309
+ : (req, res) => {
310
+ if (!isAuthorized(req, token)) {
311
+ writeUnauthorized(res);
312
+ return;
313
+ }
314
+ onRequest(req, res);
315
+ };
316
+ const tcpOnUpgrade = token === undefined
317
+ ? onUpgrade
318
+ : (req, socket, head) => {
319
+ if (!isAuthorized(req, token)) {
320
+ destroyUpgradeUnauthorized(socket);
321
+ return;
322
+ }
323
+ onUpgrade(req, socket, head);
324
+ };
325
+ tcpServer = createServer(tcpOnRequest);
326
+ tcpServer.on('upgrade', tcpOnUpgrade);
262
327
  bindErrorHandler(tcpServer);
263
328
  tcpServer.listen(parsed.port, parsed.host);
264
329
  }
@@ -56,6 +56,11 @@ export declare function envNoDaemonAutostart(): boolean;
56
56
  /** The `--tcp` fallback for crtrd's opt-in TCP listener (`CRTRD_TCP`), raw
57
57
  * (`host:port` or undefined — absent means unix socket only). */
58
58
  export declare function envCrtrdTcp(): string | undefined;
59
+ /** The opt-in bearer token guarding crtrd's TCP listener (`CRTRD_TOKEN`).
60
+ * Unset (or blank) → undefined, and the TCP listener performs NO auth check
61
+ * at all — exactly today's behavior. The unix socket never checks this,
62
+ * set or not. */
63
+ export declare function envCrtrdToken(): string | undefined;
59
64
  /** Kill switch for the host-exports writer/pruner (`CRTR_NO_EXPORTS=1`). */
60
65
  export declare function envNoExports(): boolean;
61
66
  /** Verbose-diagnostics gate for otherwise-silent best-effort catches
@@ -137,6 +137,14 @@ export function envNoDaemonAutostart() {
137
137
  export function envCrtrdTcp() {
138
138
  return process.env['CRTRD_TCP'];
139
139
  }
140
+ /** The opt-in bearer token guarding crtrd's TCP listener (`CRTRD_TOKEN`).
141
+ * Unset (or blank) → undefined, and the TCP listener performs NO auth check
142
+ * at all — exactly today's behavior. The unix socket never checks this,
143
+ * set or not. */
144
+ export function envCrtrdToken() {
145
+ const raw = process.env['CRTRD_TOKEN'];
146
+ return raw !== undefined && raw !== '' ? raw : undefined;
147
+ }
140
148
  /** Kill switch for the host-exports writer/pruner (`CRTR_NO_EXPORTS=1`). */
141
149
  export function envNoExports() {
142
150
  return process.env['CRTR_NO_EXPORTS'] === '1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.283",
3
+ "version": "0.3.284",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -73,6 +73,7 @@
73
73
  "activate": "npm run build && npm run install-runtime && crtr sys daemon restart",
74
74
  "activate:pull": "git pull && npm install && npm run activate",
75
75
  "build:api": "tsc -p packages/crouter-api/tsconfig.json",
76
+ "build:env-docker": "tsc -p packages/crouter-env-docker/tsconfig.json",
76
77
  "typecheck:pi-packages": "npx tsc -p src/builtin-pi-packages/pi-crtr-extensions/tsconfig.json",
77
78
  "test:build-output": "node scripts/build-concurrency-test.mjs",
78
79
  "check:cli-no-canvas": "node scripts/check-cli-no-canvas.mjs",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.283",
3
+ "version": "0.3.284",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.283",
9
+ "version": "0.3.284",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {