@1presence/bridge 0.9.0 → 0.10.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 +51 -11
- package/dist/index.js +9 -2
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -11,11 +11,19 @@ const fs_1 = require("fs");
|
|
|
11
11
|
const os_1 = require("os");
|
|
12
12
|
const path_1 = require("path");
|
|
13
13
|
const child_process_1 = require("child_process");
|
|
14
|
+
const crypto_1 = require("crypto");
|
|
14
15
|
// ─── Paths ────────────────────────────────────────────────────────────────────
|
|
15
16
|
const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.1presence');
|
|
16
17
|
const AUTH_FILE = (0, path_1.join)(CONFIG_DIR, 'auth.json');
|
|
17
18
|
function ensureConfigDir() {
|
|
18
|
-
|
|
19
|
+
// 0o700: refresh token + session map live here; deny traversal to other local accounts.
|
|
20
|
+
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
21
|
+
}
|
|
22
|
+
// writeFileSync's `mode` only applies when the file is newly created. chmodSync
|
|
23
|
+
// covers the overwrite case so a legacy 0644 auth.json gets tightened on next save.
|
|
24
|
+
function writeRestricted(path, data) {
|
|
25
|
+
(0, fs_1.writeFileSync)(path, data, { mode: 0o600 });
|
|
26
|
+
(0, fs_1.chmodSync)(path, 0o600);
|
|
19
27
|
}
|
|
20
28
|
// ─── JWT helpers ──────────────────────────────────────────────────────────────
|
|
21
29
|
function parseJwt(token) {
|
|
@@ -57,7 +65,7 @@ function loadCachedAuth() {
|
|
|
57
65
|
function saveCachedAuth(auth) {
|
|
58
66
|
ensureConfigDir();
|
|
59
67
|
const data = { ...auth, savedAt: Date.now() };
|
|
60
|
-
(
|
|
68
|
+
writeRestricted(AUTH_FILE, JSON.stringify(data, null, 2));
|
|
61
69
|
}
|
|
62
70
|
// ─── Browser auth flow ────────────────────────────────────────────────────────
|
|
63
71
|
function openBrowser(url) {
|
|
@@ -77,9 +85,35 @@ exports.AuthCancelledError = AuthCancelledError;
|
|
|
77
85
|
function runBrowserAuthFlow(gatewayUrl, pwaUrl) {
|
|
78
86
|
return new Promise((resolve, reject) => {
|
|
79
87
|
let resolved = false;
|
|
88
|
+
// Per-launch nonce. Embedded in the auth URL the bridge prints/opens and
|
|
89
|
+
// required on every request to this localhost server. Without it, a
|
|
90
|
+
// malicious page in the user's browser could scan ephemeral ports during
|
|
91
|
+
// the auth window and POST a forged token to hijack the bridge.
|
|
92
|
+
const nonce = (0, crypto_1.randomBytes)(32).toString('base64url');
|
|
93
|
+
const nonceBuf = Buffer.from(nonce, 'utf-8');
|
|
94
|
+
function checkNonce(provided) {
|
|
95
|
+
if (!provided)
|
|
96
|
+
return false;
|
|
97
|
+
const provBuf = Buffer.from(provided, 'utf-8');
|
|
98
|
+
if (provBuf.length !== nonceBuf.length)
|
|
99
|
+
return false;
|
|
100
|
+
return (0, crypto_1.timingSafeEqual)(provBuf, nonceBuf);
|
|
101
|
+
}
|
|
102
|
+
// CORS allowlist scoped to the legitimate PWA origin only.
|
|
103
|
+
const pwaOrigin = (() => {
|
|
104
|
+
try {
|
|
105
|
+
return new URL(pwaUrl).origin;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
80
111
|
const server = (0, http_1.createServer)((req, res) => {
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
const reqOrigin = req.headers['origin'] ?? '';
|
|
113
|
+
if (pwaOrigin && reqOrigin === pwaOrigin) {
|
|
114
|
+
res.setHeader('Access-Control-Allow-Origin', pwaOrigin);
|
|
115
|
+
}
|
|
116
|
+
res.setHeader('Vary', 'Origin');
|
|
83
117
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
84
118
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
85
119
|
if (req.method === 'OPTIONS') {
|
|
@@ -92,12 +126,18 @@ function runBrowserAuthFlow(gatewayUrl, pwaUrl) {
|
|
|
92
126
|
res.end();
|
|
93
127
|
return;
|
|
94
128
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
129
|
+
const reqUrl = new URL(req.url ?? '/', 'http://localhost');
|
|
130
|
+
const path = reqUrl.pathname;
|
|
131
|
+
// Every POST must carry the launch-specific nonce.
|
|
132
|
+
if (!checkNonce(reqUrl.searchParams.get('nonce'))) {
|
|
133
|
+
res.writeHead(403);
|
|
134
|
+
res.end();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Status beacon from the PWA — used so we exit early when the user closes
|
|
138
|
+
// the auth tab before signing in (sendBeacon path).
|
|
98
139
|
if (path === '/status') {
|
|
99
|
-
const
|
|
100
|
-
const event = params.get('event');
|
|
140
|
+
const event = reqUrl.searchParams.get('event');
|
|
101
141
|
res.writeHead(204);
|
|
102
142
|
res.end();
|
|
103
143
|
if (event === 'closed' && !resolved) {
|
|
@@ -138,7 +178,7 @@ function runBrowserAuthFlow(gatewayUrl, pwaUrl) {
|
|
|
138
178
|
});
|
|
139
179
|
server.listen(0, '127.0.0.1', () => {
|
|
140
180
|
const { port } = server.address();
|
|
141
|
-
const authUrl = `${pwaUrl.replace(/\/$/, '')}/cli-auth?port=${port}`;
|
|
181
|
+
const authUrl = `${pwaUrl.replace(/\/$/, '')}/cli-auth?port=${port}&nonce=${encodeURIComponent(nonce)}`;
|
|
142
182
|
console.log('\nOpening browser for sign-in…');
|
|
143
183
|
console.log(`If the browser doesn't open, visit:\n ${authUrl}\n`);
|
|
144
184
|
openBrowser(authUrl);
|
|
@@ -196,7 +236,7 @@ function refreshAuth(auth) {
|
|
|
196
236
|
}
|
|
197
237
|
function clearAuth() {
|
|
198
238
|
try {
|
|
199
|
-
(
|
|
239
|
+
writeRestricted(AUTH_FILE, '{}');
|
|
200
240
|
}
|
|
201
241
|
catch { /* ignore */ }
|
|
202
242
|
}
|
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) {
|