@addai/node 0.27.1 → 0.29.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/assets/vault-extension/assets/inter.woff2 +0 -0
- package/assets/vault-extension/background.js +45 -0
- package/assets/vault-extension/content.js +62 -0
- package/assets/vault-extension/icons/icon-128.png +0 -0
- package/assets/vault-extension/icons/icon-16.png +0 -0
- package/assets/vault-extension/icons/icon-32.png +0 -0
- package/assets/vault-extension/icons/icon-48.png +0 -0
- package/assets/vault-extension/manifest.json +39 -0
- package/dist/capabilities.js +2 -1
- package/dist/command-runner.js +119 -7
- package/dist/desktop/docker.d.ts +4 -1
- package/dist/desktop/docker.js +7 -3
- package/dist/desktop/engine.d.ts +13 -0
- package/dist/desktop/engine.js +35 -4
- package/dist/desktop/install-engine.d.ts +33 -2
- package/dist/desktop/install-engine.js +138 -12
- package/dist/desktop/learn/browser.d.ts +41 -0
- package/dist/desktop/learn/browser.js +136 -0
- package/dist/desktop/learn/injected.mjs +173 -0
- package/dist/desktop/learn/recorder.mjs +151 -0
- package/dist/desktop/learn/session.d.ts +51 -0
- package/dist/desktop/learn/session.js +224 -0
- package/dist/desktop/manager.d.ts +4 -0
- package/dist/desktop/manager.js +31 -2
- package/dist/desktop/provider.d.ts +4 -0
- package/dist/desktop/spec.d.ts +4 -1
- package/dist/desktop/start-engine.d.ts +7 -4
- package/dist/desktop/start-engine.js +50 -6
- package/dist/desktop/vault-extension.d.ts +21 -0
- package/dist/desktop/vault-extension.js +112 -0
- package/dist/desktop/vault-seed.mjs +112 -0
- package/dist/desktop/vault-session.d.ts +41 -0
- package/dist/desktop/vault-session.js +164 -0
- package/package.json +4 -3
- package/scripts/copy-assets.js +18 -0
- package/dist/tui.d.ts +0 -1
- package/dist/tui.js +0 -314
|
@@ -0,0 +1,112 @@
|
|
|
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.EXTENSION_FILES = exports.EXTENSION_DIR = void 0;
|
|
37
|
+
exports.packagedExtensionDir = packagedExtensionDir;
|
|
38
|
+
exports.ensureVaultExtension = ensureVaultExtension;
|
|
39
|
+
// The +Ai Vault, inside a desktop's browser.
|
|
40
|
+
//
|
|
41
|
+
// The extension already exists — it is the same MV3 build people run on their
|
|
42
|
+
// own machines (addai-credentials/extension). It signs in by reading the
|
|
43
|
+
// `auth` cookie on add.ai and asks the vault edge function what that session
|
|
44
|
+
// may see, so it needs no desktop-specific server work at all: whoever the
|
|
45
|
+
// desktop browser is signed in as, that is the vault it shows. An entity that
|
|
46
|
+
// has logged into add.ai as itself sees exactly what it has been granted, and
|
|
47
|
+
// a person watching over its shoulder sees what they have been granted.
|
|
48
|
+
//
|
|
49
|
+
// It is served straight out of /conf. That directory is already bind-mounted
|
|
50
|
+
// from the host, so the browser can load the extension from disk without the
|
|
51
|
+
// files ever being copied into the container, without a new image, and without
|
|
52
|
+
// the GHCR publish that everything else about desktops is waiting on. Updating
|
|
53
|
+
// the extension is rewriting host files.
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
/** Where the extension lives inside a desktop. /conf is the host's confDir. */
|
|
57
|
+
exports.EXTENSION_DIR = '/conf/vault/extension';
|
|
58
|
+
/** Every file the browser needs. Sourcemaps are deliberately absent: they are
|
|
59
|
+
* four times the size of the code and nobody debugs this from inside a
|
|
60
|
+
* container. */
|
|
61
|
+
exports.EXTENSION_FILES = [
|
|
62
|
+
'manifest.json',
|
|
63
|
+
'background.js',
|
|
64
|
+
'content.js',
|
|
65
|
+
'icons/icon-16.png',
|
|
66
|
+
'icons/icon-32.png',
|
|
67
|
+
'icons/icon-48.png',
|
|
68
|
+
'icons/icon-128.png',
|
|
69
|
+
'assets/inter.woff2',
|
|
70
|
+
];
|
|
71
|
+
/** The vendored copy that ships inside this package. */
|
|
72
|
+
function packagedExtensionDir() {
|
|
73
|
+
// dist/desktop/vault-extension.js -> ../../.. is the package root.
|
|
74
|
+
return path.resolve(__dirname, '..', '..', 'assets', 'vault-extension');
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Put the extension where the desktop's browser can load it.
|
|
78
|
+
*
|
|
79
|
+
* Copies only what differs, by size and mtime, so calling this on every
|
|
80
|
+
* desktop start is cheap after the first one.
|
|
81
|
+
*/
|
|
82
|
+
function ensureVaultExtension(confDir) {
|
|
83
|
+
const src = packagedExtensionDir();
|
|
84
|
+
if (!fs.existsSync(path.join(src, 'manifest.json'))) {
|
|
85
|
+
return { changed: false, available: false };
|
|
86
|
+
}
|
|
87
|
+
const dest = path.join(confDir, 'vault', 'extension');
|
|
88
|
+
let changed = false;
|
|
89
|
+
// The sign-in script travels with the extension: an extension with no
|
|
90
|
+
// session shows an empty vault, which reads as "broken" rather than "not
|
|
91
|
+
// signed in".
|
|
92
|
+
const seedFrom = path.join(__dirname, 'vault-seed.mjs');
|
|
93
|
+
const seedTo = path.join(confDir, 'vault', 'vault-seed.mjs');
|
|
94
|
+
if (fs.existsSync(seedFrom)
|
|
95
|
+
&& (!fs.existsSync(seedTo) || fs.statSync(seedTo).size !== fs.statSync(seedFrom).size)) {
|
|
96
|
+
fs.mkdirSync(path.dirname(seedTo), { recursive: true });
|
|
97
|
+
fs.copyFileSync(seedFrom, seedTo);
|
|
98
|
+
changed = true;
|
|
99
|
+
}
|
|
100
|
+
for (const rel of exports.EXTENSION_FILES) {
|
|
101
|
+
const from = path.join(src, rel);
|
|
102
|
+
if (!fs.existsSync(from))
|
|
103
|
+
continue;
|
|
104
|
+
const to = path.join(dest, rel);
|
|
105
|
+
if (fs.existsSync(to) && fs.statSync(to).size === fs.statSync(from).size)
|
|
106
|
+
continue;
|
|
107
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
108
|
+
fs.copyFileSync(from, to);
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
return { changed, available: true };
|
|
112
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Plants the add.ai session cookie in a desktop's browser, from inside it.
|
|
2
|
+
//
|
|
3
|
+
// Runs in the container, not the daemon: the debugging port is bound to
|
|
4
|
+
// loopback there on purpose (CDP is total control of a browser, and the host
|
|
5
|
+
// must not be able to reach it), so the only place that can talk to it is the
|
|
6
|
+
// container itself. Node 22 is in the image already — this is the same
|
|
7
|
+
// arrangement +Ai Learn's recorder uses.
|
|
8
|
+
//
|
|
9
|
+
// Launched by the browser wrapper, in the background, immediately before Chrome
|
|
10
|
+
// starts. It waits for the port rather than assuming, sets the cookie, and
|
|
11
|
+
// exits. Nothing depends on its exit code: a browser that comes up without a
|
|
12
|
+
// vault session is worse than one with, but it is still a working browser.
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
|
|
15
|
+
const PORT = Number(process.env.ADDAI_CDP_PORT || 9222);
|
|
16
|
+
const SESSION_FILE = process.env.ADDAI_VAULT_SESSION || '/conf/vault/session.json';
|
|
17
|
+
const DEADLINE_MS = 60_000;
|
|
18
|
+
const POLL_MS = 250;
|
|
19
|
+
|
|
20
|
+
const log = (m) => process.stdout.write(`[vault-seed] ${m}\n`);
|
|
21
|
+
|
|
22
|
+
/** The cookie the extension reads: URI-encoded JSON with the two tokens. */
|
|
23
|
+
export function cookieValue(session) {
|
|
24
|
+
return encodeURIComponent(JSON.stringify({
|
|
25
|
+
access_token: session.access_token,
|
|
26
|
+
refresh_token: session.refresh_token,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Both add.ai itself and every *.add.ai app, which is what the session is
|
|
31
|
+
* for — signing in at auth.add.ai signs you in across all of them. */
|
|
32
|
+
export function cookieParams(session) {
|
|
33
|
+
return {
|
|
34
|
+
name: 'auth',
|
|
35
|
+
value: cookieValue(session),
|
|
36
|
+
domain: '.add.ai',
|
|
37
|
+
path: '/',
|
|
38
|
+
secure: true,
|
|
39
|
+
httpOnly: false,
|
|
40
|
+
sameSite: 'Lax',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function waitForBrowser(deadline) {
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`http://127.0.0.1:${PORT}/json/version`);
|
|
48
|
+
if (res.ok) return await res.json();
|
|
49
|
+
} catch { /* not up yet */ }
|
|
50
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function setCookie(wsUrl, params) {
|
|
56
|
+
const ws = new WebSocket(wsUrl);
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
ws.addEventListener('open', resolve, { once: true });
|
|
59
|
+
ws.addEventListener('error', () => reject(new Error('could not open the debugger')), { once: true });
|
|
60
|
+
});
|
|
61
|
+
const done = new Promise((resolve, reject) => {
|
|
62
|
+
const timer = setTimeout(() => reject(new Error('the browser did not answer')), 10_000);
|
|
63
|
+
ws.addEventListener('message', (ev) => {
|
|
64
|
+
let msg;
|
|
65
|
+
try { msg = JSON.parse(ev.data); } catch { return; }
|
|
66
|
+
if (msg.id !== 1) return;
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
if (msg.error) reject(new Error(msg.error.message || 'setCookie failed'));
|
|
69
|
+
else resolve(msg.result);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
ws.send(JSON.stringify({ id: 1, method: 'Network.setCookie', params }));
|
|
73
|
+
// finally, not after the await: an error reply rejects, and a socket left
|
|
74
|
+
// open holds the event loop forever. The failure mode is a hung node process
|
|
75
|
+
// per browser launch, which is worse than the failure it is reporting.
|
|
76
|
+
try {
|
|
77
|
+
return await done;
|
|
78
|
+
} finally {
|
|
79
|
+
ws.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function main() {
|
|
84
|
+
let session;
|
|
85
|
+
try {
|
|
86
|
+
session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
87
|
+
} catch {
|
|
88
|
+
log('no session staged for this desktop — the vault will be empty until somebody signs in');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!session?.access_token || !session?.refresh_token) {
|
|
92
|
+
log('the staged session is not usable');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const version = await waitForBrowser(Date.now() + DEADLINE_MS);
|
|
97
|
+
if (!version?.webSocketDebuggerUrl) {
|
|
98
|
+
log('the browser never opened its debugging port');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const res = await setCookie(version.webSocketDebuggerUrl, cookieParams(session));
|
|
103
|
+
log(res?.success === false ? 'the browser refused the cookie' : 'signed in');
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log(`could not sign in: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Importable for tests; only runs when executed directly.
|
|
110
|
+
if (process.argv[1] && process.argv[1].endsWith('vault-seed.mjs')) {
|
|
111
|
+
main().catch((err) => log(`unexpected: ${err.message}`));
|
|
112
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** A GoTrue session, reduced to the two fields the cookie carries. */
|
|
2
|
+
export interface VaultSession {
|
|
3
|
+
access_token: string;
|
|
4
|
+
refresh_token: string;
|
|
5
|
+
/** Epoch ms. Used to decide whether to refresh before handing it over. */
|
|
6
|
+
expires_at: number;
|
|
7
|
+
email?: string;
|
|
8
|
+
entity_name?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function sessionPath(confDir: string): string;
|
|
11
|
+
export declare function readSession(confDir: string): VaultSession | null;
|
|
12
|
+
export declare function writeSession(confDir: string, s: VaultSession): void;
|
|
13
|
+
export declare function isFresh(s: VaultSession | null, now: number): boolean;
|
|
14
|
+
/** Exactly what the extension expects to find: the add.ai `auth` cookie is a
|
|
15
|
+
* URI-encoded JSON object with the two tokens in it. */
|
|
16
|
+
export declare function cookieValue(s: VaultSession): string;
|
|
17
|
+
type Fetcher = typeof fetch;
|
|
18
|
+
/** Trade a refresh token for a live one. Returns null when it has expired or
|
|
19
|
+
* been revoked — which is the normal way a rotation elsewhere reaches us. */
|
|
20
|
+
export declare function refreshSession(refreshToken: string, doFetch?: Fetcher): Promise<VaultSession | null>;
|
|
21
|
+
/** Sign in with a freshly rotated password. */
|
|
22
|
+
export declare function passwordSession(email: string, password: string, doFetch?: Fetcher): Promise<VaultSession | null>;
|
|
23
|
+
export interface LoginRpc {
|
|
24
|
+
(desktopId: string, rotate: boolean): Promise<any>;
|
|
25
|
+
}
|
|
26
|
+
export interface EnsureResult {
|
|
27
|
+
session: VaultSession | null;
|
|
28
|
+
/** Why there is no session, when there is none. */
|
|
29
|
+
reason?: string;
|
|
30
|
+
/** True when a rotation was performed — worth logging, it is destructive. */
|
|
31
|
+
rotated: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get a live session for this desktop, minting one only when necessary.
|
|
35
|
+
*
|
|
36
|
+
* Order matters: cached-and-fresh, then refresh, then rotate. Skipping to
|
|
37
|
+
* rotation because it is simpler would log the entity out of every other
|
|
38
|
+
* browser it is signed into, every time a desktop restarts.
|
|
39
|
+
*/
|
|
40
|
+
export declare function ensureVaultSession(confDir: string, login: LoginRpc, desktopId: string, doFetch?: Fetcher, now?: number): Promise<EnsureResult>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
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.sessionPath = sessionPath;
|
|
37
|
+
exports.readSession = readSession;
|
|
38
|
+
exports.writeSession = writeSession;
|
|
39
|
+
exports.isFresh = isFresh;
|
|
40
|
+
exports.cookieValue = cookieValue;
|
|
41
|
+
exports.refreshSession = refreshSession;
|
|
42
|
+
exports.passwordSession = passwordSession;
|
|
43
|
+
exports.ensureVaultSession = ensureVaultSession;
|
|
44
|
+
// Signing a desktop's browser in, so the vault has something in it.
|
|
45
|
+
//
|
|
46
|
+
// The +Ai Vault extension shows whatever the browser's add.ai session can see,
|
|
47
|
+
// so an installed extension and no session is an empty vault. This mints the
|
|
48
|
+
// session on the host and leaves it where the desktop can pick it up.
|
|
49
|
+
//
|
|
50
|
+
// Rotation is destructive — asking for a new password drops every other
|
|
51
|
+
// session that identity holds, which would sign an entity out of every browser
|
|
52
|
+
// it uses. So the order is always: use the cached refresh token, and only ask
|
|
53
|
+
// the server for a password when there is nothing to refresh.
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const config_1 = require("../config");
|
|
57
|
+
/** Refresh this long before expiry rather than at it: a browser that starts
|
|
58
|
+
* with a token about to die shows an empty vault for its first minute. */
|
|
59
|
+
const EARLY_REFRESH_MS = 5 * 60_000;
|
|
60
|
+
function sessionPath(confDir) {
|
|
61
|
+
return path.join(confDir, 'vault', 'session.json');
|
|
62
|
+
}
|
|
63
|
+
function readSession(confDir) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = fs.readFileSync(sessionPath(confDir), 'utf8');
|
|
66
|
+
const s = JSON.parse(raw);
|
|
67
|
+
if (!s?.access_token || !s?.refresh_token)
|
|
68
|
+
return null;
|
|
69
|
+
return s;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function writeSession(confDir, s) {
|
|
76
|
+
const p = sessionPath(confDir);
|
|
77
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
78
|
+
// 0600 on the host side. Inside the container the entity has root and can
|
|
79
|
+
// read it, which is fine: these are its OWN credentials, and it can already
|
|
80
|
+
// rotate them through its browser MCP. What matters is that the host does
|
|
81
|
+
// not leave them world-readable in a shared runtime directory.
|
|
82
|
+
fs.writeFileSync(p, JSON.stringify(s), { mode: 0o600 });
|
|
83
|
+
}
|
|
84
|
+
function isFresh(s, now) {
|
|
85
|
+
return !!s && s.expires_at - EARLY_REFRESH_MS > now;
|
|
86
|
+
}
|
|
87
|
+
/** Exactly what the extension expects to find: the add.ai `auth` cookie is a
|
|
88
|
+
* URI-encoded JSON object with the two tokens in it. */
|
|
89
|
+
function cookieValue(s) {
|
|
90
|
+
return encodeURIComponent(JSON.stringify({
|
|
91
|
+
access_token: s.access_token,
|
|
92
|
+
refresh_token: s.refresh_token,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
function toSession(body, extra = {}) {
|
|
96
|
+
if (!body?.access_token || !body?.refresh_token)
|
|
97
|
+
return null;
|
|
98
|
+
const ttl = Number(body.expires_in ?? 3600) * 1000;
|
|
99
|
+
return {
|
|
100
|
+
access_token: body.access_token,
|
|
101
|
+
refresh_token: body.refresh_token,
|
|
102
|
+
expires_at: Date.now() + ttl,
|
|
103
|
+
...extra,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Trade a refresh token for a live one. Returns null when it has expired or
|
|
107
|
+
* been revoked — which is the normal way a rotation elsewhere reaches us. */
|
|
108
|
+
async function refreshSession(refreshToken, doFetch = fetch) {
|
|
109
|
+
const res = await doFetch(`${config_1.SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'Content-Type': 'application/json', apikey: config_1.SUPABASE_ANON_KEY },
|
|
112
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok)
|
|
115
|
+
return null;
|
|
116
|
+
return toSession(await res.json());
|
|
117
|
+
}
|
|
118
|
+
/** Sign in with a freshly rotated password. */
|
|
119
|
+
async function passwordSession(email, password, doFetch = fetch) {
|
|
120
|
+
const res = await doFetch(`${config_1.SUPABASE_URL}/auth/v1/token?grant_type=password`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { 'Content-Type': 'application/json', apikey: config_1.SUPABASE_ANON_KEY },
|
|
123
|
+
body: JSON.stringify({ email, password }),
|
|
124
|
+
});
|
|
125
|
+
if (!res.ok)
|
|
126
|
+
return null;
|
|
127
|
+
return toSession(await res.json(), { email });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get a live session for this desktop, minting one only when necessary.
|
|
131
|
+
*
|
|
132
|
+
* Order matters: cached-and-fresh, then refresh, then rotate. Skipping to
|
|
133
|
+
* rotation because it is simpler would log the entity out of every other
|
|
134
|
+
* browser it is signed into, every time a desktop restarts.
|
|
135
|
+
*/
|
|
136
|
+
async function ensureVaultSession(confDir, login, desktopId, doFetch = fetch, now = Date.now()) {
|
|
137
|
+
const cached = readSession(confDir);
|
|
138
|
+
if (isFresh(cached, now))
|
|
139
|
+
return { session: cached, rotated: false };
|
|
140
|
+
if (cached?.refresh_token) {
|
|
141
|
+
const refreshed = await refreshSession(cached.refresh_token, doFetch);
|
|
142
|
+
if (refreshed) {
|
|
143
|
+
const merged = { ...refreshed, email: cached.email, entity_name: cached.entity_name };
|
|
144
|
+
writeSession(confDir, merged);
|
|
145
|
+
return { session: merged, rotated: false };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Ask the server who this desktop belongs to AND for a password in one call.
|
|
149
|
+
// It refuses to guess on a shared desktop with no default entity, which is
|
|
150
|
+
// the case where planting one entity's session would hand its vault to
|
|
151
|
+
// another.
|
|
152
|
+
const info = await login(desktopId, true);
|
|
153
|
+
if (!info?.ok)
|
|
154
|
+
return { session: null, reason: info?.reason ?? 'login_refused', rotated: false };
|
|
155
|
+
if (!info.password || !info.email) {
|
|
156
|
+
return { session: null, reason: 'no_credentials_returned', rotated: false };
|
|
157
|
+
}
|
|
158
|
+
const fresh = await passwordSession(info.email, info.password, doFetch);
|
|
159
|
+
if (!fresh)
|
|
160
|
+
return { session: null, reason: 'sign_in_failed', rotated: true };
|
|
161
|
+
const out = { ...fresh, entity_name: info.entity_name };
|
|
162
|
+
writeSession(confDir, out);
|
|
163
|
+
return { session: out, rotated: true };
|
|
164
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -34,10 +34,11 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"dist",
|
|
36
36
|
"scripts",
|
|
37
|
-
"README.md"
|
|
37
|
+
"README.md",
|
|
38
|
+
"assets"
|
|
38
39
|
],
|
|
39
40
|
"scripts": {
|
|
40
|
-
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-assets.js",
|
|
41
42
|
"start": "node dist/cli.js",
|
|
42
43
|
"dev": "tsc -p tsconfig.json && node dist/cli.js",
|
|
43
44
|
"test": "npm run build && node --test test/*.test.mjs",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// tsc compiles .ts and ignores everything else, but two files in this package
|
|
2
|
+
// are deliberately plain .mjs: they run inside a desktop container (node 22,
|
|
3
|
+
// no build step, no node_modules) rather than in the daemon. Ship them next to
|
|
4
|
+
// the compiled output so dist/ is self-contained.
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const ASSETS = [
|
|
9
|
+
['src/desktop/learn/recorder.mjs', 'dist/desktop/learn/recorder.mjs'],
|
|
10
|
+
['src/desktop/learn/injected.mjs', 'dist/desktop/learn/injected.mjs'],
|
|
11
|
+
['src/desktop/vault-seed.mjs', 'dist/desktop/vault-seed.mjs'],
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
for (const [from, to] of ASSETS) {
|
|
15
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
16
|
+
fs.copyFileSync(from, to);
|
|
17
|
+
}
|
|
18
|
+
console.log(`copied ${ASSETS.length} runtime assets into dist/`);
|
package/dist/tui.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runHarnessesTui(): Promise<void>;
|