@1presence/bridge 0.9.0 → 0.12.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/auth.js +52 -53
- package/dist/index.js +10 -4
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -4,19 +4,17 @@ exports.AuthCancelledError = void 0;
|
|
|
4
4
|
exports.isTokenValid = isTokenValid;
|
|
5
5
|
exports.ensureFreshToken = ensureFreshToken;
|
|
6
6
|
exports.getValidAuth = getValidAuth;
|
|
7
|
-
exports.refreshAuth = refreshAuth;
|
|
8
|
-
exports.clearAuth = clearAuth;
|
|
9
7
|
const http_1 = require("http");
|
|
10
8
|
const fs_1 = require("fs");
|
|
11
9
|
const os_1 = require("os");
|
|
12
10
|
const path_1 = require("path");
|
|
13
11
|
const child_process_1 = require("child_process");
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
// Auth lives only in process memory. Earlier versions persisted tokens to
|
|
14
|
+
// ~/.1presence/auth.json; remove any leftover file on startup so a stale,
|
|
15
|
+
// permission-bearing token can't survive a bridge restart.
|
|
16
|
+
const LEGACY_AUTH_FILE = (0, path_1.join)((0, os_1.homedir)(), '.1presence', 'auth.json');
|
|
17
|
+
(0, fs_1.rmSync)(LEGACY_AUTH_FILE, { force: true });
|
|
20
18
|
// ─── JWT helpers ──────────────────────────────────────────────────────────────
|
|
21
19
|
function parseJwt(token) {
|
|
22
20
|
try {
|
|
@@ -41,24 +39,6 @@ function uidFromToken(token) {
|
|
|
41
39
|
function emailFromToken(token) {
|
|
42
40
|
return parseJwt(token).email;
|
|
43
41
|
}
|
|
44
|
-
// ─── Cache read/write ─────────────────────────────────────────────────────────
|
|
45
|
-
function loadCachedAuth() {
|
|
46
|
-
try {
|
|
47
|
-
const raw = (0, fs_1.readFileSync)(AUTH_FILE, 'utf-8');
|
|
48
|
-
const data = JSON.parse(raw);
|
|
49
|
-
if (!data.token || !isTokenValid(data.token))
|
|
50
|
-
return null;
|
|
51
|
-
return { token: data.token, uid: data.uid, email: data.email, refreshToken: data.refreshToken };
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function saveCachedAuth(auth) {
|
|
58
|
-
ensureConfigDir();
|
|
59
|
-
const data = { ...auth, savedAt: Date.now() };
|
|
60
|
-
(0, fs_1.writeFileSync)(AUTH_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
61
|
-
}
|
|
62
42
|
// ─── Browser auth flow ────────────────────────────────────────────────────────
|
|
63
43
|
function openBrowser(url) {
|
|
64
44
|
const platform = process.platform;
|
|
@@ -77,9 +57,35 @@ exports.AuthCancelledError = AuthCancelledError;
|
|
|
77
57
|
function runBrowserAuthFlow(gatewayUrl, pwaUrl) {
|
|
78
58
|
return new Promise((resolve, reject) => {
|
|
79
59
|
let resolved = false;
|
|
60
|
+
// Per-launch nonce. Embedded in the auth URL the bridge prints/opens and
|
|
61
|
+
// required on every request to this localhost server. Without it, a
|
|
62
|
+
// malicious page in the user's browser could scan ephemeral ports during
|
|
63
|
+
// the auth window and POST a forged token to hijack the bridge.
|
|
64
|
+
const nonce = (0, crypto_1.randomBytes)(32).toString('base64url');
|
|
65
|
+
const nonceBuf = Buffer.from(nonce, 'utf-8');
|
|
66
|
+
function checkNonce(provided) {
|
|
67
|
+
if (!provided)
|
|
68
|
+
return false;
|
|
69
|
+
const provBuf = Buffer.from(provided, 'utf-8');
|
|
70
|
+
if (provBuf.length !== nonceBuf.length)
|
|
71
|
+
return false;
|
|
72
|
+
return (0, crypto_1.timingSafeEqual)(provBuf, nonceBuf);
|
|
73
|
+
}
|
|
74
|
+
// CORS allowlist scoped to the legitimate PWA origin only.
|
|
75
|
+
const pwaOrigin = (() => {
|
|
76
|
+
try {
|
|
77
|
+
return new URL(pwaUrl).origin;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
80
83
|
const server = (0, http_1.createServer)((req, res) => {
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
const reqOrigin = req.headers['origin'] ?? '';
|
|
85
|
+
if (pwaOrigin && reqOrigin === pwaOrigin) {
|
|
86
|
+
res.setHeader('Access-Control-Allow-Origin', pwaOrigin);
|
|
87
|
+
}
|
|
88
|
+
res.setHeader('Vary', 'Origin');
|
|
83
89
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
84
90
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
85
91
|
if (req.method === 'OPTIONS') {
|
|
@@ -92,12 +98,18 @@ function runBrowserAuthFlow(gatewayUrl, pwaUrl) {
|
|
|
92
98
|
res.end();
|
|
93
99
|
return;
|
|
94
100
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
const reqUrl = new URL(req.url ?? '/', 'http://localhost');
|
|
102
|
+
const path = reqUrl.pathname;
|
|
103
|
+
// Every POST must carry the launch-specific nonce.
|
|
104
|
+
if (!checkNonce(reqUrl.searchParams.get('nonce'))) {
|
|
105
|
+
res.writeHead(403);
|
|
106
|
+
res.end();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Status beacon from the PWA — used so we exit early when the user closes
|
|
110
|
+
// the auth tab before signing in (sendBeacon path).
|
|
98
111
|
if (path === '/status') {
|
|
99
|
-
const
|
|
100
|
-
const event = params.get('event');
|
|
112
|
+
const event = reqUrl.searchParams.get('event');
|
|
101
113
|
res.writeHead(204);
|
|
102
114
|
res.end();
|
|
103
115
|
if (event === 'closed' && !resolved) {
|
|
@@ -138,7 +150,7 @@ function runBrowserAuthFlow(gatewayUrl, pwaUrl) {
|
|
|
138
150
|
});
|
|
139
151
|
server.listen(0, '127.0.0.1', () => {
|
|
140
152
|
const { port } = server.address();
|
|
141
|
-
const authUrl = `${pwaUrl.replace(/\/$/, '')}/cli-auth?port=${port}`;
|
|
153
|
+
const authUrl = `${pwaUrl.replace(/\/$/, '')}/cli-auth?port=${port}&nonce=${encodeURIComponent(nonce)}`;
|
|
142
154
|
console.log('\nOpening browser for sign-in…');
|
|
143
155
|
console.log(`If the browser doesn't open, visit:\n ${authUrl}\n`);
|
|
144
156
|
openBrowser(authUrl);
|
|
@@ -168,7 +180,7 @@ async function refreshIdToken(refreshToken) {
|
|
|
168
180
|
throw new Error('Token refresh returned no id_token');
|
|
169
181
|
return data.id_token;
|
|
170
182
|
}
|
|
171
|
-
/** Returns auth with a fresh ID token. Refreshes if <10 minutes remain. */
|
|
183
|
+
/** Returns auth with a fresh ID token. Refreshes in-memory if <10 minutes remain. */
|
|
172
184
|
async function ensureFreshToken(auth) {
|
|
173
185
|
const { exp } = parseJwt(auth.token);
|
|
174
186
|
const tenMinutes = 10 * 60;
|
|
@@ -177,26 +189,13 @@ async function ensureFreshToken(auth) {
|
|
|
177
189
|
if (!auth.refreshToken)
|
|
178
190
|
return auth; // no refresh token, use as-is
|
|
179
191
|
const newToken = await refreshIdToken(auth.refreshToken);
|
|
180
|
-
|
|
181
|
-
saveCachedAuth(updated);
|
|
182
|
-
return updated;
|
|
192
|
+
return { ...auth, token: newToken };
|
|
183
193
|
}
|
|
184
194
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
195
|
+
// No cache — every bridge launch goes through the browser flow. This means
|
|
196
|
+
// permission revocations take effect on the next restart, and the PWA's
|
|
197
|
+
// CliAuthPage no-permission screen is what users see if access is denied.
|
|
185
198
|
async function getValidAuth(gatewayUrl, pwaUrl) {
|
|
186
|
-
const cached = loadCachedAuth();
|
|
187
|
-
if (cached)
|
|
188
|
-
return cached;
|
|
189
199
|
console.log('Sign-in required.');
|
|
190
|
-
|
|
191
|
-
saveCachedAuth(auth);
|
|
192
|
-
return auth;
|
|
193
|
-
}
|
|
194
|
-
function refreshAuth(auth) {
|
|
195
|
-
saveCachedAuth(auth);
|
|
196
|
-
}
|
|
197
|
-
function clearAuth() {
|
|
198
|
-
try {
|
|
199
|
-
(0, fs_1.writeFileSync)(AUTH_FILE, '{}', 'utf-8');
|
|
200
|
-
}
|
|
201
|
-
catch { /* ignore */ }
|
|
200
|
+
return runBrowserAuthFlow(gatewayUrl, pwaUrl);
|
|
202
201
|
}
|
package/dist/index.js
CHANGED
|
@@ -72,7 +72,7 @@ async function writeSetupFiles(auth) {
|
|
|
72
72
|
?? (await fetchVaultFile('AGENT.md', token))
|
|
73
73
|
?? (await fetchVaultFile('CLAUDE.md', token))
|
|
74
74
|
?? '';
|
|
75
|
-
(
|
|
75
|
+
writeRestricted(tmpFile(`agent-${uid}.md`), systemPrompt);
|
|
76
76
|
if (VERBOSE) {
|
|
77
77
|
console.log('\n[bridge:verbose] ─── system prompt ───────────────────────');
|
|
78
78
|
console.log(systemPrompt);
|
|
@@ -88,7 +88,14 @@ async function writeSetupFiles(auth) {
|
|
|
88
88
|
},
|
|
89
89
|
},
|
|
90
90
|
};
|
|
91
|
-
(
|
|
91
|
+
writeRestricted(tmpFile(`mcp-${uid}.json`), JSON.stringify(mcpConfig, null, 2));
|
|
92
|
+
}
|
|
93
|
+
// The MCP config embeds a Bearer JWT and the system prompt may contain vault
|
|
94
|
+
// state. writeFileSync's mode only takes effect on file creation — chmodSync
|
|
95
|
+
// covers the overwrite case so a legacy 0644 file gets tightened on next run.
|
|
96
|
+
function writeRestricted(path, data) {
|
|
97
|
+
(0, fs_1.writeFileSync)(path, data, { mode: 0o600 });
|
|
98
|
+
(0, fs_1.chmodSync)(path, 0o600);
|
|
92
99
|
}
|
|
93
100
|
// ─── Handle a single incoming message (token refresh + spawn) ─────────────────
|
|
94
101
|
async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpen, clientCapabilities, syncedFolders) {
|
|
@@ -245,8 +252,7 @@ function connect(auth, retryDelay = 1000) {
|
|
|
245
252
|
ws.on('close', (code) => {
|
|
246
253
|
stopPing();
|
|
247
254
|
if (code === 4001) {
|
|
248
|
-
console.error('Authentication failed
|
|
249
|
-
(0, auth_1.clearAuth)();
|
|
255
|
+
console.error('Authentication failed. Please restart the bridge to sign in again.');
|
|
250
256
|
process.exit(1);
|
|
251
257
|
}
|
|
252
258
|
if (code === 4003) {
|