@1presence/bridge 0.1.12 → 0.1.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.
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: { ...process.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,13 +57,95 @@ 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 ─────────────────────────────────────────────────────
111
+ const PING_INTERVAL_MS = 30_000;
112
+ const PONG_TIMEOUT_MS = 10_000;
61
113
  function connect(auth, retryDelay = 1000) {
62
114
  const ws = new ws_1.default(GATEWAY_WS, {
63
115
  headers: { Authorization: `Bearer ${auth.token}` },
64
116
  });
117
+ let pingTimer = null;
118
+ let pongTimer = null;
119
+ function startPing() {
120
+ pingTimer = setInterval(() => {
121
+ if (ws.readyState !== ws_1.default.OPEN)
122
+ return;
123
+ ws.ping();
124
+ pongTimer = setTimeout(() => {
125
+ console.log('[bridge] pong timeout — reconnecting…');
126
+ ws.terminate();
127
+ }, PONG_TIMEOUT_MS);
128
+ }, PING_INTERVAL_MS);
129
+ }
130
+ function stopPing() {
131
+ if (pingTimer) {
132
+ clearInterval(pingTimer);
133
+ pingTimer = null;
134
+ }
135
+ if (pongTimer) {
136
+ clearTimeout(pongTimer);
137
+ pongTimer = null;
138
+ }
139
+ }
140
+ ws.on('pong', () => {
141
+ if (pongTimer) {
142
+ clearTimeout(pongTimer);
143
+ pongTimer = null;
144
+ }
145
+ });
65
146
  ws.on('open', () => {
66
147
  console.log('✓ Bridge connected. Local Mode active on all your devices.\n');
148
+ startPing();
67
149
  });
68
150
  ws.on('message', (raw) => {
69
151
  let msg;
@@ -79,42 +161,12 @@ function connect(auth, retryDelay = 1000) {
79
161
  const ts = new Date().toLocaleTimeString();
80
162
  const preview = text.length > 80 ? text.slice(0, 80) + '…' : text;
81
163
  console.log(`[${ts}] ▶ ${preview}`);
82
- let responding = false;
83
- (0, claude_1.spawnClaude)({
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
- },
164
+ handleMessage(conversationId, text, sessionId ?? null, ws, auth).catch((err) => {
165
+ console.error(`[bridge] handleMessage error: ${err.message}`);
115
166
  });
116
167
  });
117
- ws.on('close', (code, reason) => {
168
+ ws.on('close', (code) => {
169
+ stopPing();
118
170
  if (code === 4001) {
119
171
  console.error('Authentication failed — clearing cached credentials. Please restart the bridge.');
120
172
  (0, auth_1.clearAuth)();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
5
5
  "bin": {
6
6
  "1presence-bridge": "dist/index.js"
@@ -11,6 +11,7 @@
11
11
  "README.md"
12
12
  ],
13
13
  "scripts": {
14
+ "prepack": "tsc",
14
15
  "build": "tsc",
15
16
  "dev": "tsx src/index.ts",
16
17
  "start": "node dist/index.js"