@addai/node 0.30.12 → 0.30.14

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.
Files changed (143) hide show
  1. package/dist/auto-update.d.ts +3 -0
  2. package/dist/auto-update.js +15 -0
  3. package/dist/capabilities.d.ts +31 -6
  4. package/dist/capabilities.js +48 -26
  5. package/dist/claude-models.d.ts +71 -0
  6. package/dist/claude-models.js +330 -0
  7. package/dist/claude-print.d.ts +15 -0
  8. package/dist/claude-print.js +16 -0
  9. package/dist/codex-models.d.ts +81 -0
  10. package/dist/codex-models.js +385 -0
  11. package/dist/codex-spawn.d.ts +14 -8
  12. package/dist/codex-spawn.js +30 -10
  13. package/dist/command-runner.js +31 -9
  14. package/dist/desktop/docker.js +2 -0
  15. package/dist/desktop/manager.d.ts +2 -1
  16. package/dist/desktop/manager.js +51 -1
  17. package/dist/desktop/provider.d.ts +1 -1
  18. package/dist/desktop/provider.js +2 -0
  19. package/dist/desktop/relay-client.js +7 -0
  20. package/dist/desktop/spec.d.ts +2 -0
  21. package/dist/direct/aios-bridge.d.ts +13 -0
  22. package/dist/direct/aios-bridge.js +177 -0
  23. package/dist/direct/aios-mcp.d.ts +2 -0
  24. package/dist/direct/aios-mcp.js +132 -0
  25. package/dist/direct/aios-tools.d.ts +351 -0
  26. package/dist/direct/aios-tools.js +120 -0
  27. package/dist/direct/board-chat-provider.d.ts +64 -0
  28. package/dist/direct/board-chat-provider.js +151 -0
  29. package/dist/direct/board-chat.d.ts +40 -0
  30. package/dist/direct/board-chat.js +414 -0
  31. package/dist/direct/config.d.ts +2 -0
  32. package/dist/direct/config.js +60 -0
  33. package/dist/direct/launch.d.ts +14 -0
  34. package/dist/direct/launch.js +121 -0
  35. package/dist/direct/protocol.d.ts +46 -0
  36. package/dist/direct/protocol.js +84 -0
  37. package/dist/direct/relay-main.d.ts +1 -0
  38. package/dist/direct/relay-main.js +21 -0
  39. package/dist/direct/relay.d.ts +26 -0
  40. package/dist/direct/relay.js +334 -0
  41. package/dist/direct/sessions.d.ts +61 -0
  42. package/dist/direct/sessions.js +368 -0
  43. package/dist/direct/supervisor.d.ts +9 -0
  44. package/dist/direct/supervisor.js +137 -0
  45. package/dist/direct/terminal.d.ts +1 -0
  46. package/dist/direct/terminal.js +106 -0
  47. package/dist/direct/worker.d.ts +1 -0
  48. package/dist/direct/worker.js +278 -0
  49. package/dist/entitybox/container-action.cjs +85 -0
  50. package/dist/entitybox/container-watchdog.cjs +70 -0
  51. package/dist/entitybox/model-entry.d.ts +3 -0
  52. package/dist/entitybox/model-entry.js +233 -0
  53. package/dist/entitybox/model-mcp-proxy.d.ts +3 -0
  54. package/dist/entitybox/model-mcp-proxy.js +85 -0
  55. package/dist/entitybox-application-boundary.d.ts +59 -0
  56. package/dist/entitybox-application-boundary.js +161 -0
  57. package/dist/entitybox-application-guard-core.d.ts +44 -0
  58. package/dist/entitybox-application-guard-core.js +164 -0
  59. package/dist/entitybox-application-guard-process.d.ts +1 -0
  60. package/dist/entitybox-application-guard-process.js +83 -0
  61. package/dist/entitybox-application-guard.d.ts +46 -0
  62. package/dist/entitybox-application-guard.js +239 -0
  63. package/dist/entitybox-capacity-health-host.d.ts +12 -0
  64. package/dist/entitybox-capacity-health-host.js +80 -0
  65. package/dist/entitybox-capacity-health-worker.d.ts +22 -0
  66. package/dist/entitybox-capacity-health-worker.js +168 -0
  67. package/dist/entitybox-capacity-host.d.ts +17 -0
  68. package/dist/entitybox-capacity-host.js +155 -0
  69. package/dist/entitybox-capacity-owner-process.d.ts +1 -0
  70. package/dist/entitybox-capacity-owner-process.js +32 -0
  71. package/dist/entitybox-capacity-owner.d.ts +16 -0
  72. package/dist/entitybox-capacity-owner.js +90 -0
  73. package/dist/entitybox-capacity-programs.d.ts +7 -0
  74. package/dist/entitybox-capacity-programs.js +49 -0
  75. package/dist/entitybox-capacity-provisioner.d.ts +82 -0
  76. package/dist/entitybox-capacity-provisioner.js +404 -0
  77. package/dist/entitybox-capacity-publication.d.ts +29 -0
  78. package/dist/entitybox-capacity-publication.js +199 -0
  79. package/dist/entitybox-capacity-registration.d.ts +36 -0
  80. package/dist/entitybox-capacity-registration.js +75 -0
  81. package/dist/entitybox-capacity-worker.d.ts +29 -0
  82. package/dist/entitybox-capacity-worker.js +113 -0
  83. package/dist/entitybox-computer-task.d.ts +69 -0
  84. package/dist/entitybox-computer-task.js +176 -0
  85. package/dist/entitybox-controller-recovery.d.ts +40 -0
  86. package/dist/entitybox-controller-recovery.js +156 -0
  87. package/dist/entitybox-desktop-action-server.d.ts +34 -0
  88. package/dist/entitybox-desktop-action-server.js +176 -0
  89. package/dist/entitybox-desktop-admission.d.ts +32 -0
  90. package/dist/entitybox-desktop-admission.js +79 -0
  91. package/dist/entitybox-display-binding.d.ts +13 -0
  92. package/dist/entitybox-display-binding.js +31 -0
  93. package/dist/entitybox-execution-container.d.ts +82 -0
  94. package/dist/entitybox-execution-container.js +450 -0
  95. package/dist/entitybox-execution-host.d.ts +40 -0
  96. package/dist/entitybox-execution-host.js +168 -0
  97. package/dist/entitybox-execution-lease.d.ts +81 -0
  98. package/dist/entitybox-execution-lease.js +332 -0
  99. package/dist/entitybox-file-inputs.d.ts +51 -0
  100. package/dist/entitybox-file-inputs.js +182 -0
  101. package/dist/entitybox-file-session.d.ts +38 -0
  102. package/dist/entitybox-file-session.js +82 -0
  103. package/dist/entitybox-host-process.d.ts +12 -0
  104. package/dist/entitybox-host-process.js +120 -0
  105. package/dist/entitybox-install.d.ts +10 -0
  106. package/dist/entitybox-install.js +40 -0
  107. package/dist/entitybox-mcp-broker.d.ts +77 -0
  108. package/dist/entitybox-mcp-broker.js +420 -0
  109. package/dist/entitybox-mcp-contract.d.ts +10 -0
  110. package/dist/entitybox-mcp-contract.js +24 -0
  111. package/dist/entitybox-mcp-guard.d.ts +3 -0
  112. package/dist/entitybox-mcp-guard.js +167 -0
  113. package/dist/entitybox-model-artifact.d.ts +9 -0
  114. package/dist/entitybox-model-artifact.js +83 -0
  115. package/dist/entitybox-native-services.d.ts +33 -0
  116. package/dist/entitybox-native-services.js +172 -0
  117. package/dist/entitybox-operation.d.ts +4 -0
  118. package/dist/entitybox-operation.js +34 -0
  119. package/dist/entitybox-provider-broker.d.ts +41 -0
  120. package/dist/entitybox-provider-broker.js +149 -0
  121. package/dist/entitybox-queue-request.d.ts +32 -0
  122. package/dist/entitybox-queue-request.js +141 -0
  123. package/dist/entitybox-recovery-host.d.ts +12 -0
  124. package/dist/entitybox-recovery-host.js +80 -0
  125. package/dist/entitybox-recovery-store.d.ts +33 -0
  126. package/dist/entitybox-recovery-store.js +150 -0
  127. package/dist/entitybox-recovery-worker.d.ts +30 -0
  128. package/dist/entitybox-recovery-worker.js +134 -0
  129. package/dist/entitybox-vault-provider.d.ts +27 -0
  130. package/dist/entitybox-vault-provider.js +127 -0
  131. package/dist/harness-registry.js +3 -1
  132. package/dist/index.js +72 -38
  133. package/dist/install.d.ts +14 -2
  134. package/dist/install.js +115 -10
  135. package/dist/model-cache.d.ts +38 -0
  136. package/dist/model-cache.js +137 -0
  137. package/dist/request-pump.d.ts +3 -0
  138. package/dist/request-pump.js +21 -7
  139. package/dist/session-runner.d.ts +8 -1
  140. package/dist/session-runner.js +48 -3
  141. package/dist/supabase-client.js +4 -1
  142. package/package.json +5 -1
  143. package/scripts/copy-assets.js +4 -2
@@ -0,0 +1,46 @@
1
+ /** A separate protocol: never sent through the entity request queue or desktop relay. */
2
+ export declare const DIRECT_PROTOCOL = 1;
3
+ export declare const MAX_FRAME: number;
4
+ export declare const MAX_INPUT: number;
5
+ export declare const UUID: RegExp;
6
+ export type Provider = 'claude' | 'codex';
7
+ export type DirectRequest = {
8
+ type: 'request';
9
+ id: string;
10
+ op: 'describe' | 'list' | 'create' | 'attach' | 'input' | 'resize' | 'detach' | 'stop' | 'resume' | 'ack' | 'aios-result' | 'aios-access' | 'chat';
11
+ sessionId?: string;
12
+ provider?: Provider;
13
+ cwd?: string;
14
+ title?: string;
15
+ cols?: number;
16
+ rows?: number;
17
+ data?: string;
18
+ seq?: number;
19
+ aios?: boolean;
20
+ attachmentId?: string;
21
+ callId?: string;
22
+ ok?: boolean;
23
+ result?: unknown;
24
+ error?: string;
25
+ chat?: {
26
+ action: 'open' | 'history' | 'conversations' | 'send' | 'reply' | 'cancel' | 'detach';
27
+ binding?: unknown;
28
+ fresh?: boolean;
29
+ before?: number;
30
+ messageId?: string;
31
+ text?: string;
32
+ callId?: string;
33
+ ok?: boolean;
34
+ result?: unknown;
35
+ error?: string;
36
+ decision?: 'accept' | 'decline';
37
+ answers?: {
38
+ [id: string]: {
39
+ answers: string[];
40
+ };
41
+ };
42
+ };
43
+ };
44
+ export declare function parseRequest(value: unknown): DirectRequest;
45
+ /** HTTPS in production; plaintext is limited to explicit loopback development. */
46
+ export declare function relayUrl(raw: string, route: string): string;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UUID = exports.MAX_INPUT = exports.MAX_FRAME = exports.DIRECT_PROTOCOL = void 0;
4
+ exports.parseRequest = parseRequest;
5
+ exports.relayUrl = relayUrl;
6
+ /** A separate protocol: never sent through the entity request queue or desktop relay. */
7
+ exports.DIRECT_PROTOCOL = 1;
8
+ exports.MAX_FRAME = 256 * 1024;
9
+ exports.MAX_INPUT = 32 * 1024;
10
+ exports.UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11
+ function parseRequest(value) {
12
+ if (!value || typeof value !== 'object')
13
+ throw new Error('Invalid request.');
14
+ const r = value;
15
+ if (r.type !== 'request' || !exports.UUID.test(r.id ?? ''))
16
+ throw new Error('Invalid request ID.');
17
+ if (!['describe', 'list', 'create', 'attach', 'input', 'resize', 'detach', 'stop', 'resume', 'ack', 'aios-result', 'aios-access', 'chat'].includes(r.op)) {
18
+ throw new Error('Unknown operation.');
19
+ }
20
+ if (!['describe', 'list'].includes(r.op) && !exports.UUID.test(r.sessionId ?? ''))
21
+ throw new Error('Invalid session ID.');
22
+ if (r.op === 'chat') {
23
+ const c = r.chat;
24
+ if (!c || !['open', 'history', 'conversations', 'send', 'reply', 'cancel', 'detach'].includes(c.action) || Buffer.byteLength(JSON.stringify(c)) > 64 * 1024)
25
+ throw new Error('Invalid chat request.');
26
+ if (c.action !== 'open' && !exports.UUID.test(r.attachmentId ?? ''))
27
+ throw new Error('Invalid chat attachment.');
28
+ if (c.action === 'open' && (c.fresh !== undefined && typeof c.fresh !== 'boolean'))
29
+ throw new Error('Invalid chat creation request.');
30
+ if (c.action === 'send' && (!exports.UUID.test(c.messageId ?? '') || typeof c.text !== 'string' || !c.text.trim() || c.text.length > exports.MAX_INPUT))
31
+ throw new Error('Invalid chat message.');
32
+ if (c.before !== undefined && (!Number.isSafeInteger(c.before) || c.before < 1))
33
+ throw new Error('Invalid chat history cursor.');
34
+ if (c.action === 'reply') {
35
+ if (!exports.UUID.test(c.callId ?? '') || (c.ok !== undefined && typeof c.ok !== 'boolean') || (c.decision !== undefined && !['accept', 'decline'].includes(c.decision)) ||
36
+ (c.error !== undefined && (typeof c.error !== 'string' || c.error.length > 1000)))
37
+ throw new Error('Invalid chat reply.');
38
+ if (c.answers !== undefined && (!c.answers || typeof c.answers !== 'object' || Array.isArray(c.answers) || Object.values(c.answers).some(a => !a || !Array.isArray(a.answers) || a.answers.some(t => typeof t !== 'string' || t.length > 4000))))
39
+ throw new Error('Invalid question answer.');
40
+ }
41
+ }
42
+ if (r.aios !== undefined && typeof r.aios !== 'boolean')
43
+ throw new Error('Invalid +Ai access setting.');
44
+ if (r.op === 'aios-access' && (typeof r.aios !== 'boolean' || !exports.UUID.test(r.attachmentId ?? '')))
45
+ throw new Error('Invalid +Ai access setting.');
46
+ if (r.op === 'aios-result') {
47
+ if (!exports.UUID.test(r.callId ?? '') || !exports.UUID.test(r.attachmentId ?? '') || typeof r.ok !== 'boolean' ||
48
+ (r.error !== undefined && (typeof r.error !== 'string' || r.error.length > 500)) ||
49
+ Buffer.byteLength(JSON.stringify(r.result) ?? '') > 64 * 1024)
50
+ throw new Error('Invalid +Ai result.');
51
+ }
52
+ if (r.op === 'create') {
53
+ if (r.provider !== 'claude' && r.provider !== 'codex')
54
+ throw new Error('Choose Claude Code or Codex.');
55
+ if (typeof r.cwd !== 'string' || !r.cwd.trim() || r.cwd.length > 4096 || /[\x00-\x1f]/.test(r.cwd)) {
56
+ throw new Error('Choose a project folder.');
57
+ }
58
+ if (r.title !== undefined && (typeof r.title !== 'string' || r.title.length > 100 || /[\x00-\x1f]/.test(r.title))) {
59
+ throw new Error('Invalid session title.');
60
+ }
61
+ }
62
+ if (r.op === 'input' && (typeof r.data !== 'string' || r.data.length > exports.MAX_INPUT))
63
+ throw new Error('Input is too large.');
64
+ if (r.op === 'ack' && (!Number.isSafeInteger(r.seq) || r.seq < 0))
65
+ throw new Error('Invalid output acknowledgment.');
66
+ if (r.op === 'resize' || r.op === 'create' || r.op === 'resume' || r.op === 'attach') {
67
+ if (!Number.isInteger(r.cols) || r.cols < 20 || r.cols > 400 ||
68
+ !Number.isInteger(r.rows) || r.rows < 5 || r.rows > 150)
69
+ throw new Error('Invalid terminal size.');
70
+ }
71
+ return r;
72
+ }
73
+ /** HTTPS in production; plaintext is limited to explicit loopback development. */
74
+ function relayUrl(raw, route) {
75
+ const url = new URL(raw);
76
+ const local = ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname);
77
+ if (url.protocol !== 'wss:' && !(local && url.protocol === 'ws:'))
78
+ throw new Error('Direct sessions require a secure relay URL.');
79
+ if (url.username || url.password || url.search || url.hash || (url.pathname !== '/' && url.pathname !== '')) {
80
+ throw new Error('Use the relay origin without credentials, paths or query parameters.');
81
+ }
82
+ url.pathname = route;
83
+ return url.toString();
84
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const relay_1 = require("./relay");
4
+ if (require.main === module) {
5
+ const { SUPABASE_URL, SUPABASE_ANON_KEY, DIRECT_ALLOWED_ORIGINS } = process.env;
6
+ if (!SUPABASE_URL || !SUPABASE_ANON_KEY || !DIRECT_ALLOWED_ORIGINS) {
7
+ throw new Error('Configure SUPABASE_URL, SUPABASE_ANON_KEY and DIRECT_ALLOWED_ORIGINS.');
8
+ }
9
+ const origins = DIRECT_ALLOWED_ORIGINS.split(',').map(s => s.trim()).filter(Boolean);
10
+ for (const origin of origins) {
11
+ const parsed = new URL(origin);
12
+ if (parsed.origin !== origin || (parsed.protocol !== 'https:' && !['localhost', '127.0.0.1'].includes(parsed.hostname))) {
13
+ throw new Error('Allowed origins must be exact HTTPS origins (or loopback development origins).');
14
+ }
15
+ }
16
+ const relay = (0, relay_1.createDirectRelay)({ auth: (0, relay_1.supabaseRelayAuth)(SUPABASE_URL, SUPABASE_ANON_KEY), origins });
17
+ const port = Number(process.env.PORT ?? 8788);
18
+ relay.server.listen(port, process.env.HOST ?? '127.0.0.1', () => console.log(`[direct-relay] listening on ${port}`));
19
+ process.once('SIGTERM', () => { void relay.close(); });
20
+ process.once('SIGINT', () => { void relay.close(); });
21
+ }
@@ -0,0 +1,26 @@
1
+ import { type IncomingMessage } from 'node:http';
2
+ export interface NodeIdentity {
3
+ id: string;
4
+ ownerId: string;
5
+ name: string;
6
+ }
7
+ export interface UserIdentity {
8
+ id: string;
9
+ expiresAt: number;
10
+ }
11
+ export interface RelayAuth {
12
+ node(token: string): Promise<NodeIdentity | null>;
13
+ user(token: string): Promise<UserIdentity | null>;
14
+ }
15
+ export interface RelayOptions {
16
+ auth: RelayAuth;
17
+ origins: string[];
18
+ recheckMs?: number;
19
+ }
20
+ /** Independent server and node registry; never imported into the desktop relay. */
21
+ export declare function createDirectRelay(options: RelayOptions): {
22
+ server: import("http").Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
23
+ close(): Promise<void>;
24
+ };
25
+ /** Uses existing auth and runtime tables. No new SQL or existing RPC replacements. */
26
+ export declare function supabaseRelayAuth(url: string, anonKey: string): RelayAuth;
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createDirectRelay = createDirectRelay;
37
+ exports.supabaseRelayAuth = supabaseRelayAuth;
38
+ const node_http_1 = require("node:http");
39
+ const node_crypto_1 = require("node:crypto");
40
+ const ws_1 = __importStar(require("ws"));
41
+ const protocol_1 = require("./protocol");
42
+ function send(ws, value) {
43
+ if (ws.readyState !== ws_1.default.OPEN)
44
+ return false;
45
+ if (ws.bufferedAmount > 4 * 1024 * 1024) {
46
+ ws.close(1013, 'Connection cannot keep up. Reconnect.');
47
+ return false;
48
+ }
49
+ ws.send(JSON.stringify(value));
50
+ return true;
51
+ }
52
+ function bearer(req) {
53
+ const header = req.headers.authorization;
54
+ return header?.startsWith('Bearer ') ? header.slice(7) : null;
55
+ }
56
+ /** Independent server and node registry; never imported into the desktop relay. */
57
+ function createDirectRelay(options) {
58
+ const relayId = (0, node_crypto_1.randomUUID)();
59
+ const nodes = new Map();
60
+ const viewers = new Map();
61
+ const sockets = new Set();
62
+ const origins = new Set(options.origins);
63
+ const wss = new ws_1.WebSocketServer({ noServer: true, maxPayload: protocol_1.MAX_FRAME, perMessageDeflate: false });
64
+ const server = (0, node_http_1.createServer)(async (req, res) => {
65
+ const pathname = new URL(req.url ?? '/', 'http://relay').pathname;
66
+ res.setHeader('Cache-Control', 'no-store');
67
+ if (pathname === '/health' && req.method === 'GET') {
68
+ res.setHeader('Content-Type', 'application/json');
69
+ res.end(JSON.stringify({ ok: true, protocol: protocol_1.DIRECT_PROTOCOL, relayId }));
70
+ return;
71
+ }
72
+ const origin = req.headers.origin;
73
+ if (!origin || !origins.has(origin)) {
74
+ res.writeHead(403);
75
+ res.end();
76
+ return;
77
+ }
78
+ res.setHeader('Access-Control-Allow-Origin', origin);
79
+ res.setHeader('Vary', 'Origin');
80
+ if (req.method === 'OPTIONS' && pathname === '/nodes') {
81
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization');
82
+ res.setHeader('Access-Control-Allow-Methods', 'GET');
83
+ res.writeHead(204);
84
+ res.end();
85
+ return;
86
+ }
87
+ if (req.method !== 'GET' || pathname !== '/nodes') {
88
+ res.writeHead(404);
89
+ res.end();
90
+ return;
91
+ }
92
+ try {
93
+ const token = bearer(req);
94
+ const user = token ? await options.auth.user(token) : null;
95
+ if (!user || user.expiresAt <= Date.now()) {
96
+ res.writeHead(401);
97
+ res.end();
98
+ return;
99
+ }
100
+ res.setHeader('Content-Type', 'application/json');
101
+ res.end(JSON.stringify([...nodes.values()].filter(n => n.identity.ownerId === user.id)
102
+ .map(n => ({ id: n.identity.id, name: n.identity.name }))));
103
+ }
104
+ catch {
105
+ res.writeHead(503);
106
+ res.end();
107
+ }
108
+ });
109
+ function dropNode(node) {
110
+ if (nodes.get(node.identity.id) !== node)
111
+ return;
112
+ nodes.delete(node.identity.id);
113
+ for (const viewer of viewers.values())
114
+ if (viewer.node === node)
115
+ viewer.ws.close(1012, 'Machine disconnected. Reconnecting preserves running sessions.');
116
+ }
117
+ function dropViewer(viewer) {
118
+ if (viewers.get(viewer.id) !== viewer)
119
+ return;
120
+ viewers.delete(viewer.id);
121
+ send(viewer.node.ws, { type: 'detach_viewer', viewerId: viewer.id });
122
+ }
123
+ server.on('upgrade', (req, socket, head) => {
124
+ const url = new URL(req.url ?? '/', 'http://relay');
125
+ const match = /^\/client\/([0-9a-f-]{36})$/i.exec(url.pathname);
126
+ const isNode = url.pathname === '/node';
127
+ // Tokens are accepted only in the first frame, never in access-log URLs.
128
+ if (url.search || (!isNode && (!match || !protocol_1.UUID.test(match[1]) || !origins.has(req.headers.origin ?? '')))) {
129
+ socket.destroy();
130
+ return;
131
+ }
132
+ wss.handleUpgrade(req, socket, head, ws => {
133
+ sockets.add(ws);
134
+ let authenticating = false;
135
+ let node;
136
+ let viewer;
137
+ let inFlight = 0;
138
+ let windowStart = Date.now();
139
+ let frames = 0;
140
+ const timeout = setTimeout(() => ws.close(4001, 'Authentication required.'), 8000);
141
+ ws.on('error', () => { });
142
+ ws.on('pong', () => { if (node)
143
+ node.alive = true; if (viewer)
144
+ viewer.alive = true; });
145
+ ws.on('close', () => {
146
+ clearTimeout(timeout);
147
+ sockets.delete(ws);
148
+ if (node)
149
+ dropNode(node);
150
+ if (viewer)
151
+ dropViewer(viewer);
152
+ });
153
+ ws.on('message', async (raw, binary) => {
154
+ try {
155
+ if (binary) {
156
+ ws.close(1003, 'JSON frames required.');
157
+ return;
158
+ }
159
+ const message = JSON.parse(raw.toString());
160
+ if (!node && !viewer) {
161
+ if (authenticating || message.type !== 'authenticate' || typeof message.token !== 'string' || message.token.length > 16_384) {
162
+ ws.close(4001, 'Authentication required.');
163
+ return;
164
+ }
165
+ authenticating = true;
166
+ if (isNode) {
167
+ const identity = await options.auth.node(message.token);
168
+ if (!identity || !protocol_1.UUID.test(identity.id) || !protocol_1.UUID.test(identity.ownerId)) {
169
+ ws.close(4001, 'Authentication failed.');
170
+ return;
171
+ }
172
+ if (ws.readyState !== ws_1.default.OPEN)
173
+ return;
174
+ const prior = nodes.get(identity.id);
175
+ if (prior) {
176
+ dropNode(prior);
177
+ prior.ws.close(1012, 'Direct-session worker replaced.');
178
+ }
179
+ node = { ws, identity, token: message.token, alive: true };
180
+ nodes.set(identity.id, node);
181
+ send(ws, { type: 'ready', protocol: protocol_1.DIRECT_PROTOCOL, runtimeId: identity.id, relayId });
182
+ }
183
+ else {
184
+ const user = await options.auth.user(message.token);
185
+ const target = nodes.get(match[1]);
186
+ // Raw machine access is owner-only. Workspace/entity grants never imply shell access.
187
+ if (!user || user.expiresAt <= Date.now() || !target || target.identity.ownerId !== user.id) {
188
+ ws.close(4003, 'Machine unavailable or access denied.');
189
+ return;
190
+ }
191
+ if (ws.readyState !== ws_1.default.OPEN)
192
+ return;
193
+ viewer = { ws, id: (0, node_crypto_1.randomUUID)(), node: target, user, token: message.token, alive: true };
194
+ viewers.set(viewer.id, viewer);
195
+ send(ws, { type: 'ready', protocol: protocol_1.DIRECT_PROTOCOL, chat: 1 });
196
+ }
197
+ clearTimeout(timeout);
198
+ return;
199
+ }
200
+ if (node) {
201
+ if (!['response', 'snapshot', 'event'].includes(message.type) || !protocol_1.UUID.test(message.viewerId ?? ''))
202
+ return;
203
+ const target = viewers.get(message.viewerId);
204
+ if (!target || target.node !== node || target.user.id !== node.identity.ownerId)
205
+ return;
206
+ const { viewerId: _viewerId, ...out } = message;
207
+ send(target.ws, out);
208
+ return;
209
+ }
210
+ if (!viewer || viewer.user.expiresAt <= Date.now() || nodes.get(viewer.node.identity.id) !== viewer.node) {
211
+ ws.close(4001, 'Reconnect to authenticate.');
212
+ return;
213
+ }
214
+ if (Date.now() - windowStart > 1000) {
215
+ frames = 0;
216
+ windowStart = Date.now();
217
+ }
218
+ if (++frames > 240 || ++inFlight > 128) {
219
+ ws.close(1008, 'Too many requests.');
220
+ return;
221
+ }
222
+ try {
223
+ const request = (0, protocol_1.parseRequest)(message);
224
+ send(viewer.node.ws, { type: 'request', viewerId: viewer.id, ownerId: viewer.user.id, request });
225
+ }
226
+ finally {
227
+ inFlight--;
228
+ }
229
+ }
230
+ catch {
231
+ ws.close(1008, 'Invalid direct-session message.');
232
+ }
233
+ });
234
+ });
235
+ });
236
+ let checking = false;
237
+ const interval = setInterval(async () => {
238
+ if (checking)
239
+ return;
240
+ checking = true;
241
+ try {
242
+ await Promise.all([...nodes.values()].map(async (node) => {
243
+ if (!node.alive) {
244
+ node.ws.terminate();
245
+ return;
246
+ }
247
+ node.alive = false;
248
+ node.ws.ping();
249
+ try {
250
+ const identity = await options.auth.node(node.token);
251
+ if (!identity || identity.id !== node.identity.id || identity.ownerId !== node.identity.ownerId)
252
+ node.ws.close(4001, 'Pairing changed.');
253
+ }
254
+ catch {
255
+ node.ws.close(1013, 'Unable to verify pairing.');
256
+ }
257
+ }));
258
+ await Promise.all([...viewers.values()].map(async (viewer) => {
259
+ if (!viewer.alive || viewer.user.expiresAt <= Date.now()) {
260
+ viewer.ws.close(4001, 'Reconnect to authenticate.');
261
+ return;
262
+ }
263
+ viewer.alive = false;
264
+ viewer.ws.ping();
265
+ try {
266
+ const user = await options.auth.user(viewer.token);
267
+ if (!user || user.id !== viewer.user.id || user.expiresAt <= Date.now())
268
+ viewer.ws.close(4001, 'Authentication expired.');
269
+ }
270
+ catch {
271
+ viewer.ws.close(1013, 'Unable to verify authentication.');
272
+ }
273
+ }));
274
+ }
275
+ finally {
276
+ checking = false;
277
+ }
278
+ }, options.recheckMs ?? 30_000);
279
+ interval.unref();
280
+ return {
281
+ server,
282
+ async close() {
283
+ clearInterval(interval);
284
+ for (const ws of sockets)
285
+ ws.terminate();
286
+ wss.close();
287
+ if (server.listening)
288
+ await new Promise(resolve => server.close(() => resolve()));
289
+ },
290
+ };
291
+ }
292
+ /** Uses existing auth and runtime tables. No new SQL or existing RPC replacements. */
293
+ function supabaseRelayAuth(url, anonKey) {
294
+ async function json(route, key, token, init = {}) {
295
+ const response = await fetch(`${url.replace(/\/$/, '')}${route}`, {
296
+ ...init, signal: AbortSignal.timeout(8000),
297
+ headers: { apikey: key, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
298
+ });
299
+ if (!response.ok)
300
+ throw new Error('Authentication service unavailable.');
301
+ return response.json();
302
+ }
303
+ return {
304
+ async node(token) {
305
+ // The existing SECURITY DEFINER RPC validates the pairing token and returns
306
+ // only its own runtime. No administrator key or unrestricted table read.
307
+ const r = await json('/rest/v1/rpc/runtime_self', anonKey, anonKey, { method: 'POST', body: JSON.stringify({ p_token: token }) });
308
+ if (!r || !protocol_1.UUID.test(r.id ?? '') || !protocol_1.UUID.test(r.owner_user_id ?? ''))
309
+ return null;
310
+ return { id: r.id, ownerId: r.owner_user_id, name: typeof r.name === 'string' && r.name ? r.name : 'Paired machine' };
311
+ },
312
+ async user(token) {
313
+ // Verify with Supabase before inspecting the already-verified token's expiration.
314
+ let user;
315
+ try {
316
+ user = await json('/auth/v1/user', anonKey, token);
317
+ }
318
+ catch {
319
+ return null;
320
+ }
321
+ if (!user.id || !protocol_1.UUID.test(user.id))
322
+ return null;
323
+ try {
324
+ const claims = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
325
+ if (claims.sub !== user.id || !Number.isFinite(claims.exp))
326
+ return null;
327
+ return { id: user.id, expiresAt: claims.exp * 1000 };
328
+ }
329
+ catch {
330
+ return null;
331
+ }
332
+ },
333
+ };
334
+ }
@@ -0,0 +1,61 @@
1
+ import { type Provider } from './protocol';
2
+ import { type DesktopCall } from './aios-tools';
3
+ export interface SessionRecord {
4
+ id: string;
5
+ ownerId: string;
6
+ provider: Provider;
7
+ cwd: string;
8
+ title: string;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ status: 'running' | 'exited';
12
+ nativeSessionId?: string;
13
+ exitCode?: number;
14
+ reason?: string;
15
+ aios?: boolean;
16
+ }
17
+ export interface TerminalProcess {
18
+ write(data: string): void;
19
+ resize(cols: number, rows: number): void;
20
+ kill(): void;
21
+ pause?(): void;
22
+ resume?(): void;
23
+ onData(cb: (data: string) => void): unknown;
24
+ onExit(cb: (event: {
25
+ exitCode: number;
26
+ }) => void): unknown;
27
+ }
28
+ export interface SessionDeps {
29
+ directory: string;
30
+ roots: string[];
31
+ maxActive?: number;
32
+ providers: () => Record<Provider, boolean>;
33
+ spawn: (record: SessionRecord, size: {
34
+ cols: number;
35
+ rows: number;
36
+ }, resume: boolean, desktop: DesktopCall) => TerminalProcess;
37
+ emit: (viewerId: string, event: Record<string, unknown>) => void;
38
+ onCount?: (count: number) => void;
39
+ canStart?: () => boolean;
40
+ }
41
+ export declare function isWithin(root: string, folder: string): boolean;
42
+ export declare class DirectSessions {
43
+ private deps;
44
+ private records;
45
+ private live;
46
+ private roots;
47
+ private shuttingDown;
48
+ constructor(deps: SessionDeps);
49
+ get count(): number;
50
+ private save;
51
+ private folder;
52
+ private owned;
53
+ private controlled;
54
+ private start;
55
+ request(viewerId: string, ownerId: string, value: unknown): Promise<unknown>;
56
+ detach(viewerId: string): void;
57
+ detachAll(): void;
58
+ stop(): void;
59
+ private cancelCalls;
60
+ callDesktop(id: string, name: string, args: Record<string, unknown>): Promise<unknown>;
61
+ }