thinkpool-pair 0.7.217 → 0.7.219

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 CHANGED
@@ -65,7 +65,7 @@ import { writeLaneArtifact, digestSlice, appendDigest, resumeLane } from './flow
65
65
  import { createFlowWorktree, worktreeSpec } from './flow-worktree.mjs'
66
66
  import { startPreview, stopAllPreviews, previews } from './flow-preview.mjs'
67
67
  import { ViewportManager, createViewportTools, sharedViewportBrowser } from './viewport.mjs'
68
- import { designPrompt, resolveDesignSource, validateDesignRequest } from './design-edit.mjs'
68
+ import { designPrompt, resolveDesignSource, restoreDesignSources, syncRealtimeAuth, validateDesignRequest } from './design-edit.mjs'
69
69
  // FL-M9 — per-lane preview servers leak (one per done lane, never stopped until shutdown).
70
70
  // Lane previews are keyed `lane:<flowId>:<laneId>`; stop a whole flow's set when it assembles
71
71
  // (the assembled preview supersedes them) or when a lane is reverted.
@@ -739,6 +739,13 @@ let myServeUid = null // this bridge's authed uid (null = anon)
739
739
  }
740
740
  }
741
741
 
742
+ // Private realtime topics authorize the JWT carried by the socket at join time.
743
+ // Supabase's REST auth helpers do not propagate codeAuthToken to Realtime, so pin
744
+ // it before constructing any channel. A direct/anonymous bridge stays on anon.
745
+ if (!await syncRealtimeAuth(supabase, codeAuthToken, SUPABASE_ANON) && codeAuthToken) {
746
+ process.stderr.write('\n ⚠ realtime auth could not be initialized; private bridge features are unavailable.\n')
747
+ }
748
+
742
749
  refreshOwnerPlan() // fire-and-forget on boot (codeAuthToken now set from env OR the stored login)
743
750
 
744
751
  // ── Pre-flight budget caps — spec docs/specs/2026-07-01-budget-caps.md (nexos steal #1).
@@ -1855,6 +1862,9 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
1855
1862
  // they land on this session even after other terminals open/close.
1856
1863
  const mockupOutbox = ownerOutbox('sessions', id)
1857
1864
  entry.mockupWatcher = watchOutbox(mockupOutbox, () => id, true)
1865
+ const restoredDesigns = restoreDesignSources(mockupOutbox, cwd || process.cwd(), id)
1866
+ for (const record of restoredDesigns) designArtifacts.set(record.previewId, record)
1867
+ if (restoredDesigns.length) process.stderr.write(`\n ◆ restored ${restoredDesigns.length} Thinkpool Design artifact${restoredDesigns.length === 1 ? '' : 's'} (${id.slice(0, 8)}).\n`)
1858
1868
  // Visual QA runs in the bridge process, outside either agent runtime's sandbox.
1859
1869
  // It remains scoped to this lane's cwd and private mockup outbox.
1860
1870
  entry.viewport = new ViewportManager({ workspaceRoot: cwd || process.cwd(), ownerId: id, outbox: mockupOutbox })
@@ -4014,7 +4024,13 @@ if (process.env.THINKPOOL_PAIR_AUTOUPDATE === '1' && VERSION) {
4014
4024
  if (m.t === 'update-available') surfaceUpdate(m.version)
4015
4025
  // Supervisor rotated the owner JWT (account.mjs scheduleTokenRefresh) — adopt
4016
4026
  // it so our authed writes (code-mockup) never go stale on a long session (L16).
4017
- else if (m.t === 'token') { codeAuthToken = m.accessToken || null; refreshOwnerPlan() }
4027
+ else if (m.t === 'token') {
4028
+ codeAuthToken = m.accessToken || null
4029
+ syncRealtimeAuth(supabase, codeAuthToken, SUPABASE_ANON).then((ok) => {
4030
+ if (!ok && codeAuthToken) process.stderr.write('\n ⚠ realtime auth refresh failed; private bridge features may need a reconnect.\n')
4031
+ })
4032
+ refreshOwnerPlan()
4033
+ }
4018
4034
  })
4019
4035
  setInterval(() => { try { process.send({ t: 'idle', idle: idleNow(), between: betweenTurns(), webPeer: webPeerPresent() }) } catch { /* parent gone */ } }, 5000).unref()
4020
4036
  } else if (VERSION) {
package/design-edit.mjs CHANGED
@@ -8,6 +8,20 @@ const MAX_TEXT = 4_000
8
8
 
9
9
  const clean = (value, max) => String(value ?? '').replace(/\0/g, '').trim().slice(0, max)
10
10
 
11
+ // supabase-js does not copy the Auth session into an already-created Realtime
12
+ // client. Private tpdesign:* joins therefore need the bridge owner's JWT pinned
13
+ // explicitly, both at boot and whenever the account supervisor rotates it.
14
+ export async function syncRealtimeAuth(client, accessToken, fallbackToken = null) {
15
+ const token = accessToken || fallbackToken
16
+ if (!token || typeof client?.realtime?.setAuth !== 'function') return false
17
+ try {
18
+ await client.realtime.setAuth(token)
19
+ return true
20
+ } catch {
21
+ return false
22
+ }
23
+ }
24
+
11
25
  export function sourceRevision(source) {
12
26
  return crypto.createHash('sha256').update(String(source)).digest('hex').slice(0, 24)
13
27
  }
@@ -34,6 +48,44 @@ export function resolveDesignSource(file, workspaceRoot) {
34
48
  return { previewId, revision, sourcePath, workspaceRoot: root, source }
35
49
  }
36
50
 
51
+ // Design provenance is intentionally host-only, but it must survive a bridge
52
+ // restart. Rebuild the in-memory registry from manifests already written by the
53
+ // trusted lane render workflow; do not re-upload or re-broadcast old cards.
54
+ export function restoreDesignSources(box, workspaceRoot, term, limit = 100) {
55
+ if (!box || !workspaceRoot || !term) return []
56
+ let files
57
+ try {
58
+ files = fs.readdirSync(box, { withFileTypes: true })
59
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json') && !entry.name.endsWith('.receipt.json'))
60
+ .map((entry) => {
61
+ const file = path.join(box, entry.name)
62
+ return { file, mtimeMs: fs.statSync(file).mtimeMs }
63
+ })
64
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
65
+ .slice(0, Math.max(0, limit))
66
+ } catch { return [] }
67
+ const restored = []
68
+ for (const { file } of files) {
69
+ try {
70
+ const manifest = JSON.parse(fs.readFileSync(file, 'utf8'))
71
+ if (!manifest?.source) continue
72
+ const record = resolveDesignSource(manifest.source, workspaceRoot)
73
+ if (!record) continue
74
+ Object.assign(record, {
75
+ term,
76
+ slug: String(manifest.slug || ''),
77
+ title: String(manifest.title || manifest.slug || ''),
78
+ desktop: manifest.desktop || null,
79
+ mobile: manifest.mobile || null,
80
+ })
81
+ const backupPath = path.join(box, '.design-revisions', `${record.revision}.html`)
82
+ if (fs.existsSync(backupPath)) record.backupPath = backupPath
83
+ restored.push(record)
84
+ } catch { /* stale or malformed manifests are ignored */ }
85
+ }
86
+ return restored
87
+ }
88
+
37
89
  function safeTarget(value) {
38
90
  if (!value || typeof value !== 'object') return null
39
91
  const rect = value.rect && typeof value.rect === 'object' ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.217",
3
+ "version": "0.7.219",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {