@1presence/bridge 0.1.11 → 0.1.13
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 +33 -3
- package/dist/claude.js +9 -1
- package/dist/index.js +53 -34
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureFreshToken = ensureFreshToken;
|
|
3
4
|
exports.getValidAuth = getValidAuth;
|
|
4
5
|
exports.refreshAuth = refreshAuth;
|
|
5
6
|
exports.clearAuth = clearAuth;
|
|
@@ -45,7 +46,7 @@ function loadCachedAuth() {
|
|
|
45
46
|
const data = JSON.parse(raw);
|
|
46
47
|
if (!data.token || !isTokenValid(data.token))
|
|
47
48
|
return null;
|
|
48
|
-
return { token: data.token, uid: data.uid, email: data.email };
|
|
49
|
+
return { token: data.token, uid: data.uid, email: data.email, refreshToken: data.refreshToken };
|
|
49
50
|
}
|
|
50
51
|
catch {
|
|
51
52
|
return null;
|
|
@@ -88,7 +89,7 @@ function runBrowserAuthFlow(gatewayUrl, pwaUrl) {
|
|
|
88
89
|
req.on('data', (chunk) => { body += chunk.toString(); });
|
|
89
90
|
req.on('end', () => {
|
|
90
91
|
try {
|
|
91
|
-
const { token } = JSON.parse(body);
|
|
92
|
+
const { token, refreshToken } = JSON.parse(body);
|
|
92
93
|
if (!token) {
|
|
93
94
|
res.writeHead(400);
|
|
94
95
|
res.end();
|
|
@@ -103,7 +104,7 @@ function runBrowserAuthFlow(gatewayUrl, pwaUrl) {
|
|
|
103
104
|
reject(new Error('Invalid token — missing uid'));
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
106
|
-
resolve({ token, uid, email });
|
|
107
|
+
resolve({ token, uid, email, refreshToken: refreshToken ?? undefined });
|
|
107
108
|
}
|
|
108
109
|
catch (err) {
|
|
109
110
|
res.writeHead(400);
|
|
@@ -125,6 +126,35 @@ function runBrowserAuthFlow(gatewayUrl, pwaUrl) {
|
|
|
125
126
|
}, 5 * 60 * 1000);
|
|
126
127
|
});
|
|
127
128
|
}
|
|
129
|
+
// ─── Token refresh ────────────────────────────────────────────────────────────
|
|
130
|
+
// Firebase web API key — public, safe to embed
|
|
131
|
+
const FIREBASE_API_KEY = 'AIzaSyAz16A3eRIMhdLGF9ptsVsWZx9LjKeZwi8';
|
|
132
|
+
async function refreshIdToken(refreshToken) {
|
|
133
|
+
const res = await fetch(`https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify({ grant_type: 'refresh_token', refresh_token: refreshToken }),
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok)
|
|
139
|
+
throw new Error(`Token refresh failed: ${res.status}`);
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
if (!data.id_token)
|
|
142
|
+
throw new Error('Token refresh returned no id_token');
|
|
143
|
+
return data.id_token;
|
|
144
|
+
}
|
|
145
|
+
/** Returns auth with a fresh ID token. Refreshes if <10 minutes remain. */
|
|
146
|
+
async function ensureFreshToken(auth) {
|
|
147
|
+
const { exp } = parseJwt(auth.token);
|
|
148
|
+
const tenMinutes = 10 * 60;
|
|
149
|
+
if (exp && exp > Math.floor(Date.now() / 1000) + tenMinutes)
|
|
150
|
+
return auth;
|
|
151
|
+
if (!auth.refreshToken)
|
|
152
|
+
return auth; // no refresh token, use as-is
|
|
153
|
+
const newToken = await refreshIdToken(auth.refreshToken);
|
|
154
|
+
const updated = { ...auth, token: newToken };
|
|
155
|
+
saveCachedAuth(updated);
|
|
156
|
+
return updated;
|
|
157
|
+
}
|
|
128
158
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
129
159
|
async function getValidAuth(gatewayUrl, pwaUrl) {
|
|
130
160
|
const cached = loadCachedAuth();
|
package/dist/claude.js
CHANGED
|
@@ -21,12 +21,16 @@ function spawnClaude(params) {
|
|
|
21
21
|
'--allowedTools', 'mcp__1presence__*',
|
|
22
22
|
'--system-prompt', systemPromptPath,
|
|
23
23
|
'--mcp-config', mcpConfigPath,
|
|
24
|
+
'--strict-mcp-config',
|
|
24
25
|
];
|
|
25
26
|
if (claudeSessionId) {
|
|
26
27
|
args.push('--resume', claudeSessionId);
|
|
27
28
|
}
|
|
29
|
+
// Strip API key so Claude Code uses the user's claude.ai Pro subscription
|
|
30
|
+
// (OAuth credentials), not an API key that would bill to a separate account.
|
|
31
|
+
const { ANTHROPIC_API_KEY: _stripped, ...safeEnv } = process.env;
|
|
28
32
|
const proc = (0, child_process_1.spawn)('claude', args, {
|
|
29
|
-
env:
|
|
33
|
+
env: safeEnv,
|
|
30
34
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
35
|
});
|
|
32
36
|
active.set(conversationId, proc);
|
|
@@ -57,6 +61,10 @@ function spawnClaude(params) {
|
|
|
57
61
|
if (sid && presenceSessionId) {
|
|
58
62
|
(0, sessions_1.saveClaudeSession)(presenceSessionId, sid);
|
|
59
63
|
}
|
|
64
|
+
const keySource = event['apiKeySource'];
|
|
65
|
+
const model = event['model'];
|
|
66
|
+
const authLabel = keySource === 'oauth' || !keySource ? 'claude.ai plan' : `api key`;
|
|
67
|
+
process.stderr.write(`[bridge] model: ${model ?? 'unknown'} auth: ${authLabel}\n`);
|
|
60
68
|
sessionIdExtracted = true;
|
|
61
69
|
}
|
|
62
70
|
// Count complete assistant turns + accumulate token usage
|
package/dist/index.js
CHANGED
|
@@ -57,6 +57,56 @@ async function writeSetupFiles(auth) {
|
|
|
57
57
|
};
|
|
58
58
|
(0, fs_1.writeFileSync)(tmpFile(`mcp-${uid}.json`), JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
59
59
|
}
|
|
60
|
+
// ─── Handle a single incoming message (token refresh + spawn) ─────────────────
|
|
61
|
+
async function handleMessage(conversationId, text, sessionId, ws, auth) {
|
|
62
|
+
// Refresh JWT if <10 min remaining before spawning Claude
|
|
63
|
+
let activeAuth = auth;
|
|
64
|
+
try {
|
|
65
|
+
const freshAuth = await (0, auth_1.ensureFreshToken)(auth);
|
|
66
|
+
if (freshAuth.token !== auth.token) {
|
|
67
|
+
currentAuth = freshAuth;
|
|
68
|
+
activeAuth = freshAuth;
|
|
69
|
+
await writeSetupFiles(freshAuth);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.warn(`[bridge] token refresh failed: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
let responding = false;
|
|
76
|
+
(0, claude_1.spawnClaude)({
|
|
77
|
+
conversationId,
|
|
78
|
+
presenceSessionId: sessionId,
|
|
79
|
+
text,
|
|
80
|
+
uid: activeAuth.uid,
|
|
81
|
+
onEvent: (event) => {
|
|
82
|
+
if (!responding && event['type'] === 'assistant') {
|
|
83
|
+
responding = true;
|
|
84
|
+
console.log(`[${new Date().toLocaleTimeString()}] ◐ responding…`);
|
|
85
|
+
}
|
|
86
|
+
if (ws.readyState === ws_1.default.OPEN) {
|
|
87
|
+
ws.send(JSON.stringify({ type: 'stream', conversationId, event }));
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
onDone: (messageCount, costUsd, usage) => {
|
|
91
|
+
const parts = [];
|
|
92
|
+
if (usage)
|
|
93
|
+
parts.push(`in:${usage.input_tokens} out:${usage.output_tokens}`);
|
|
94
|
+
const costStr = costUsd === 0 ? '$0.0000 (plan usage)' : `$${costUsd.toFixed(4)}`;
|
|
95
|
+
parts.push(costStr);
|
|
96
|
+
const suffix = parts.length ? ` ${parts.join(' ')}` : '';
|
|
97
|
+
console.log(`[${new Date().toLocaleTimeString()}] ✓ done${suffix}`);
|
|
98
|
+
if (ws.readyState === ws_1.default.OPEN) {
|
|
99
|
+
ws.send(JSON.stringify({ type: 'done', conversationId, messageCount, costUsd }));
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
onError: (message) => {
|
|
103
|
+
console.error(`[${new Date().toLocaleTimeString()}] ✗ ${message}`);
|
|
104
|
+
if (ws.readyState === ws_1.default.OPEN) {
|
|
105
|
+
ws.send(JSON.stringify({ type: 'error', conversationId, message }));
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
60
110
|
// ─── WebSocket connection ─────────────────────────────────────────────────────
|
|
61
111
|
function connect(auth, retryDelay = 1000) {
|
|
62
112
|
const ws = new ws_1.default(GATEWAY_WS, {
|
|
@@ -79,42 +129,11 @@ function connect(auth, retryDelay = 1000) {
|
|
|
79
129
|
const ts = new Date().toLocaleTimeString();
|
|
80
130
|
const preview = text.length > 80 ? text.slice(0, 80) + '…' : text;
|
|
81
131
|
console.log(`[${ts}] ▶ ${preview}`);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
conversationId,
|
|
85
|
-
presenceSessionId: sessionId ?? null,
|
|
86
|
-
text,
|
|
87
|
-
uid: auth.uid,
|
|
88
|
-
onEvent: (event) => {
|
|
89
|
-
if (!responding && event['type'] === 'assistant') {
|
|
90
|
-
responding = true;
|
|
91
|
-
console.log(`[${new Date().toLocaleTimeString()}] ◐ responding…`);
|
|
92
|
-
}
|
|
93
|
-
if (ws.readyState === ws_1.default.OPEN) {
|
|
94
|
-
ws.send(JSON.stringify({ type: 'stream', conversationId, event }));
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
onDone: (messageCount, costUsd, usage) => {
|
|
98
|
-
const parts = [];
|
|
99
|
-
if (usage)
|
|
100
|
-
parts.push(`in:${usage.input_tokens} out:${usage.output_tokens}`);
|
|
101
|
-
const costStr = costUsd === 0 ? '$0.0000 (plan usage)' : `$${costUsd.toFixed(4)}`;
|
|
102
|
-
parts.push(costStr);
|
|
103
|
-
const suffix = parts.length ? ` ${parts.join(' ')}` : '';
|
|
104
|
-
console.log(`[${new Date().toLocaleTimeString()}] ✓ done${suffix}`);
|
|
105
|
-
if (ws.readyState === ws_1.default.OPEN) {
|
|
106
|
-
ws.send(JSON.stringify({ type: 'done', conversationId, messageCount, costUsd }));
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
onError: (message) => {
|
|
110
|
-
console.error(`[${new Date().toLocaleTimeString()}] ✗ ${message}`);
|
|
111
|
-
if (ws.readyState === ws_1.default.OPEN) {
|
|
112
|
-
ws.send(JSON.stringify({ type: 'error', conversationId, message }));
|
|
113
|
-
}
|
|
114
|
-
},
|
|
132
|
+
handleMessage(conversationId, text, sessionId ?? null, ws, auth).catch((err) => {
|
|
133
|
+
console.error(`[bridge] handleMessage error: ${err.message}`);
|
|
115
134
|
});
|
|
116
135
|
});
|
|
117
|
-
ws.on('close', (code
|
|
136
|
+
ws.on('close', (code) => {
|
|
118
137
|
if (code === 4001) {
|
|
119
138
|
console.error('Authentication failed — clearing cached credentials. Please restart the bridge.');
|
|
120
139
|
(0, auth_1.clearAuth)();
|