thinkpool-pair 0.7.163 → 0.7.164
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/bridge.mjs +77 -6
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -42,7 +42,7 @@ import { reapTerminalRow as _reapTerminalRow } from './reap-terminal.mjs'
|
|
|
42
42
|
// resolveProviderEnv(id) → {ANTHROPIC_BASE_URL,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_MODEL} for a
|
|
43
43
|
// registered custom provider, or null for the built-in/unknown (leave the default env intact).
|
|
44
44
|
// Multi-provider BYOK slice 1: a lane spawned with a `provider` id runs on that endpoint.
|
|
45
|
-
import { resolveProviderEnv, providerNameMap } from './providers.mjs'
|
|
45
|
+
import { resolveProviderEnv, providerNameMap, publicKeyB64, announceProviders, listProviders, addProvider, removeProvider, unseal } from './providers.mjs'
|
|
46
46
|
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
|
|
47
47
|
import { z } from 'zod'
|
|
48
48
|
import { startClaudeSession } from './claude-session.mjs'
|
|
@@ -635,11 +635,15 @@ let yielding = false // set once when standing down, so reclaim + revoke
|
|
|
635
635
|
const { authedClient } = await import('./account.mjs')
|
|
636
636
|
const refreshed = await authedClient(SUPABASE_URL, SUPABASE_ANON)
|
|
637
637
|
if (refreshed?.session?.access_token) codeAuthToken = refreshed.session.access_token
|
|
638
|
-
// If
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
} catch {
|
|
638
|
+
// If refresh fails, DO NOT clear the token yet. Let the serve-consent gate below
|
|
639
|
+
// (lines 648-676) validate the token via getUser and handle auth-shaped errors
|
|
640
|
+
// vs network errors correctly. This fixes GAP 2 where a transient network error
|
|
641
|
+
// refused serving when the existing token might still be valid.
|
|
642
|
+
} catch {
|
|
643
|
+
// Refresh failed — but keep the existing token. Let serve-consent gate validate it.
|
|
644
|
+
// If it's an auth error, the gate will refuse (exit 0). If it's a network error,
|
|
645
|
+
// the gate will fail open and we can serve with the existing token.
|
|
646
|
+
}
|
|
643
647
|
}
|
|
644
648
|
try {
|
|
645
649
|
if (codeAuthToken) {
|
|
@@ -920,6 +924,13 @@ const announce = () => {
|
|
|
920
924
|
// host (e.g. openrouter.ai). Older web clients ignore unknown fields.
|
|
921
925
|
provider: PROVIDER_INFO.provider,
|
|
922
926
|
providerBaseUrl: PROVIDER_INFO.baseUrl || undefined,
|
|
927
|
+
// providerPubKey: the bridge's RSA-OAEP public key (SPKI base64), used by the
|
|
928
|
+
// dashboard to seal a provider's API key so it reaches the bridge without ever
|
|
929
|
+
// crossing the wire in the clear. Computed once (the keypair is stable).
|
|
930
|
+
// providers: the name-only registry (id+name+model, NEVER the key or baseUrl)
|
|
931
|
+
// so the dashboard can list what's available. Both additive; older clients ignore.
|
|
932
|
+
providerPubKey: publicKeyB64(),
|
|
933
|
+
providers: announceProviders(),
|
|
923
934
|
// pendingUpdate: a newer thinkpool-pair is out — drives the room's "update ready"
|
|
924
935
|
// chip; re-sent per announce so reloads/late-joiners see it (Slice 3).
|
|
925
936
|
pendingUpdate,
|
|
@@ -2628,6 +2639,66 @@ channel
|
|
|
2628
2639
|
// Peer bridge announces (owner reclaim) — a grantee yields when the owner's bridge
|
|
2629
2640
|
// reappears. broadcast self:false, so this only fires for OTHER bridges on the topic.
|
|
2630
2641
|
.on('broadcast', { event: 'bridge' }, ({ payload }) => onPeerBridgeAnnounce(payload))
|
|
2642
|
+
// ── Provider registry — the multi-BYOK wire contract (room mode). ─────────────
|
|
2643
|
+
// AUTH: room mode serves a room the user may not own (post-#208 grant). We verify
|
|
2644
|
+
// the sender is a participant of THIS room before ANY operation. List is open to
|
|
2645
|
+
// any participant; add/remove mutate the HOST's registry, so gate to the BRIDGE
|
|
2646
|
+
// HOST's own uid (the authed login). Anon is refused for all three.
|
|
2647
|
+
.on('broadcast', { event: 'provider-add' }, async ({ payload }) => {
|
|
2648
|
+
const nonce = payload?.nonce
|
|
2649
|
+
// Auth: must be the bridge host (the authed login running this bridge)
|
|
2650
|
+
// Use the same pattern as providers-list-req: getUser + uid comparison, not byte comparison
|
|
2651
|
+
try {
|
|
2652
|
+
const { data } = await supabase.auth.getUser(payload?.jwt)
|
|
2653
|
+
if (!data?.user || data.user.id !== myServeUid) {
|
|
2654
|
+
return channel.send({ type: 'broadcast', event: 'provider-add-res', payload: { nonce, ok: false, error: 'unauthorized' } })
|
|
2655
|
+
}
|
|
2656
|
+
} catch {
|
|
2657
|
+
return channel.send({ type: 'broadcast', event: 'provider-add-res', payload: { nonce, ok: false, error: 'unauthorized' } })
|
|
2658
|
+
}
|
|
2659
|
+
let fields
|
|
2660
|
+
try { fields = unseal(payload?.sealed) } catch { return channel.send({ type: 'broadcast', event: 'provider-add-res', payload: { nonce, ok: false, error: 'could not decrypt the sealed payload' } }) }
|
|
2661
|
+
const r = addProvider(fields)
|
|
2662
|
+
if (r.ok) { announce(); process.stderr.write(`\n ◆ provider added (${String(fields?.name || '').slice(0, 40)}) — re-announced.\n`) }
|
|
2663
|
+
channel.send({ type: 'broadcast', event: 'provider-add-res', payload: { nonce, ok: r.ok, error: r.ok ? undefined : r.error, id: r.ok ? r.id : undefined } })
|
|
2664
|
+
})
|
|
2665
|
+
// provider-remove {id, nonce, jwt} → remove, re-announce.
|
|
2666
|
+
.on('broadcast', { event: 'provider-remove' }, async ({ payload }) => {
|
|
2667
|
+
const nonce = payload?.nonce
|
|
2668
|
+
// Auth: must be the bridge host (the authed login running this bridge)
|
|
2669
|
+
// Use the same pattern as providers-list-req: getUser + uid comparison, not byte comparison
|
|
2670
|
+
try {
|
|
2671
|
+
const { data } = await supabase.auth.getUser(payload?.jwt)
|
|
2672
|
+
if (!data?.user || data.user.id !== myServeUid) {
|
|
2673
|
+
return channel.send({ type: 'broadcast', event: 'provider-remove-res', payload: { nonce, ok: false, error: 'unauthorized' } })
|
|
2674
|
+
}
|
|
2675
|
+
} catch {
|
|
2676
|
+
return channel.send({ type: 'broadcast', event: 'provider-remove-res', payload: { nonce, ok: false, error: 'unauthorized' } })
|
|
2677
|
+
}
|
|
2678
|
+
const r = removeProvider(payload?.id)
|
|
2679
|
+
if (r.ok) { announce(); process.stderr.write('\n ◆ provider removed — re-announced.\n') }
|
|
2680
|
+
channel.send({ type: 'broadcast', event: 'provider-remove-res', payload: { nonce, ok: r.ok, error: r.ok ? undefined : r.error } })
|
|
2681
|
+
})
|
|
2682
|
+
// providers-list-req {nonce, jwt} → the masked list (keyHint = last 4 chars only).
|
|
2683
|
+
.on('broadcast', { event: 'providers-list-req' }, async ({ payload }) => {
|
|
2684
|
+
const nonce = payload?.nonce
|
|
2685
|
+
const isParticipant = async (jwt) => {
|
|
2686
|
+
if (!jwt || typeof jwt !== 'string') return false
|
|
2687
|
+
try {
|
|
2688
|
+
const { data } = await supabase.auth.getUser(jwt)
|
|
2689
|
+
if (!data?.user) return false
|
|
2690
|
+
const uid = data.user.id
|
|
2691
|
+
// Verify the user is a participant of THIS room (owner OR granted OR joined).
|
|
2692
|
+
// Room mode serves the room, so RLS returns participants; we just need to confirm
|
|
2693
|
+
// the jwt's user is one of them. A simple existence check on code_sessions
|
|
2694
|
+
// with the room code + the user's id in the participants array is enough.
|
|
2695
|
+
const r = await fetch(`${SUPABASE_URL}/rest/v1/code_sessions?code=eq.${encodeURIComponent(room)}&participants=cs.${uid}`, { headers: { apikey: SUPABASE_ANON, Authorization: `Bearer ${codeAuthToken}` } })
|
|
2696
|
+
return r.ok
|
|
2697
|
+
} catch { return false }
|
|
2698
|
+
}
|
|
2699
|
+
if (!(await isParticipant(payload?.jwt))) return channel.send({ type: 'broadcast', event: 'providers-list-res', payload: { nonce, ok: false, error: 'unauthorized', providers: [] } })
|
|
2700
|
+
channel.send({ type: 'broadcast', event: 'providers-list-res', payload: { nonce, ok: true, providers: listProviders() } })
|
|
2701
|
+
})
|
|
2631
2702
|
.subscribe(status => {
|
|
2632
2703
|
if (status === 'SUBSCRIBED') {
|
|
2633
2704
|
realtimeHealthy = true; brokenSince = 0
|