@1presence/bridge 0.65.0 → 0.67.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/README.md CHANGED
@@ -22,6 +22,27 @@ On first run, a browser window will open to sign in to your 1Presence account. Y
22
22
 
23
23
  Once connected, your 1Presence app on any device automatically routes to your local Claude Code session. When you stop the bridge, 1Presence falls back to platform mode.
24
24
 
25
+ ## Admin: running the bridge as another user
26
+
27
+ If your 1Presence account is an **admin**, the sign-in screen shows a user picker
28
+ instead of connecting immediately. Choose which 1Presence user this bridge runs
29
+ as — it defaults to **you** — and click Connect. Picking another user lets you
30
+ drive their account (their vault, connectors, and history) through your local
31
+ Claude Code, which is handy for onboarding testers who don't run the bridge
32
+ themselves. You never need their password; the picker mints a scoped token for
33
+ the selected user server-side.
34
+
35
+ The target user must have **Local Claude Code access** granted in the admin panel
36
+ (the same permission that gates your own bridge and also lets bridge turns run
37
+ free on your Claude subscription). The picker flags anyone who isn't eligible yet.
38
+
39
+ ### Multiple sessions at once
40
+
41
+ Because every session is keyed by the connected user, you can run several bridges
42
+ side by side — each in its own terminal, each impersonating a **different** user —
43
+ and they won't interfere. Running two bridges for the *same* user at once is not
44
+ supported (the second displaces the first).
45
+
25
46
  ## How it works
26
47
 
27
48
  The bridge connects outbound to the 1Presence gateway as a persistent WebSocket. When you send a message from the app, the gateway relays it to the bridge, which spawns a `claude` subprocess with your personal system prompt and all 1Presence tools (vault, Gmail, Drive, Calendar, MemPalace, and more) wired in via MCP. Responses stream back in real time.
package/dist/auth.js CHANGED
@@ -197,11 +197,22 @@ async function refreshIdToken(refreshToken) {
197
197
  throw new Error('Token refresh returned no id_token');
198
198
  return data.id_token;
199
199
  }
200
- /** Returns auth with a fresh ID token. Refreshes in-memory if <10 minutes remain. */
200
+ /** Headroom below which ensureFreshToken mints a new ID token at turn start.
201
+ * Firebase ID tokens live 60 min; the token is baked into the MCP SSE config
202
+ * header at spawn (writeMcpConfig) and reused by that connection for the whole
203
+ * turn — it can't be re-tokened mid-turn without reconnecting the MCP server.
204
+ * So a turn must START with enough headroom to outlive itself. At 10 min a turn
205
+ * that began with ~12 min of life and ran 17 min lost its MCP OAuth AND its
206
+ * save-turn token mid-flight ("MCP OAuth not configured" + save-turn 401 — see
207
+ * the 2026-07-07 stuck-run bug). 30 min comfortably covers the 10-min bridge
208
+ * stage wall-clock and any normal chat turn; near-full-hour refresh would churn
209
+ * a token every turn for little gain. */
210
+ const TOKEN_REFRESH_HEADROOM_SEC = 30 * 60;
211
+ /** Returns auth with a fresh ID token. Refreshes in-memory if less than
212
+ * TOKEN_REFRESH_HEADROOM_SEC of validity remains. */
201
213
  export async function ensureFreshToken(auth) {
202
214
  const { exp } = parseJwt(auth.token);
203
- const tenMinutes = 10 * 60;
204
- if (exp && exp > Math.floor(Date.now() / 1000) + tenMinutes)
215
+ if (exp && exp > Math.floor(Date.now() / 1000) + TOKEN_REFRESH_HEADROOM_SEC)
205
216
  return auth;
206
217
  if (!auth.refreshToken)
207
218
  return auth; // no refresh token, use as-is
package/dist/index.js CHANGED
@@ -433,6 +433,22 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
433
433
  catch (err) {
434
434
  console.warn(`[bridge] spool write failed: ${err.message}`);
435
435
  }
436
+ // Refresh the token right before the POST — this fires at turn END, when the
437
+ // most time has elapsed. A long turn (started with a token that was fresh
438
+ // then) must not 401 on save-turn with a since-expired token — the failure
439
+ // that left a run stuck at 'running' on 2026-07-07. Best-effort: on refresh
440
+ // failure fall back to the current token; the spool file is retried on the
441
+ // next successful POST or on the next bridge startup.
442
+ try {
443
+ const fresh = await ensureFreshToken(activeAuth);
444
+ if (fresh.token !== activeAuth.token) {
445
+ activeAuth = fresh;
446
+ currentAuth = fresh;
447
+ }
448
+ }
449
+ catch (err) {
450
+ console.warn(`[bridge] pre-save-turn token refresh failed (using current token): ${err.message}`);
451
+ }
436
452
  const result = await postSaveTurn(GATEWAY_HTTP, activeAuth.token, record);
437
453
  if (result.ok) {
438
454
  deleteSpool(record.conversationId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.65.0",
3
+ "version": "0.67.0",
4
4
  "description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",