thinkpool-pair 0.7.3 → 0.7.5

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.
Files changed (2) hide show
  1. package/account.mjs +32 -27
  2. package/package.json +1 -1
package/account.mjs CHANGED
@@ -19,27 +19,35 @@ const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./pack
19
19
 
20
20
  const BRIDGE = fileURLToPath(new URL('./bridge.mjs', import.meta.url))
21
21
 
22
- // ── login: web hands the bridge a session over an ephemeral realtime channel ──
23
- // The bridge prints a one-time URL with a high-entropy code; the signed-in web
24
- // page broadcasts {refresh_token,…} to tplink:<code> after the user approves.
25
- // (Phase-1 handoff an edge-function device-code flow can harden this later.)
22
+ // ── login: OAuth-style DEVICE-CODE flow (hardened) ──────────────────────────
23
+ // The bridge asks Postgres (start_device_code) for a secret device_code + a short
24
+ // human user_code, prints the user_code + URL, then POLLS claim_device_code with
25
+ // the secret until the signed-in web has approved (approve_device_code stores the
26
+ // browser's session). The session is delivered only over authenticated TLS RPCs
27
+ // + a single-use secret — never broadcast on a public channel.
26
28
  export async function runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) {
27
29
  const sb = createClient(SUPABASE_URL, SUPABASE_ANON, { auth: { persistSession: false } })
28
- const code = randomUUID().replace(/-/g, '').slice(0, 12)
29
- const ch = sb.channel(`tplink:${code}`, { config: { broadcast: { self: false } } })
30
- let done = false
31
- ch.on('broadcast', { event: 'session' }, async ({ payload }) => {
32
- if (done || !payload?.refresh_token) return
33
- done = true
34
- saveAuth(payload)
35
- process.stderr.write(`\n linked as ${payload.email || 'your account'}. You can close the browser tab.\n Now run: npx thinkpool-pair\n`)
36
- try { await sb.removeChannel(ch) } catch { /* noop */ }
37
- process.exit(0)
38
- })
39
- await new Promise((res) => ch.subscribe((s) => { if (s === 'SUBSCRIBED') res() }))
40
- const url = `${WEB_BASE}/code/link?c=${code}`
41
- process.stderr.write(`\n ◆ Link this device to your ThinkPool account.\n Open (signed in) and approve:\n\n ${url}\n\n Code: ${code} — waiting up to 5 min…\n`)
42
- setTimeout(() => { if (!done) { process.stderr.write('\n ◇ link timed out — run `npx thinkpool-pair login` again.\n'); process.exit(1) } }, 5 * 60 * 1000)
30
+ const { data, error } = await sb.rpc('start_device_code')
31
+ const row = Array.isArray(data) ? data[0] : data
32
+ if (error || !row?.device_code) { process.stderr.write(`\n ✗ couldn't start login: ${error?.message || 'no response'}\n`); process.exit(1) }
33
+ const { device_code, user_code } = row
34
+ const url = `${WEB_BASE}/code/link?c=${encodeURIComponent(user_code)}`
35
+ process.stderr.write(`\n ◆ Link this device to your ThinkPool account.\n Open (signed in) and approve:\n\n ${url}\n\n code: ${user_code}\n\n Waiting…\n`)
36
+ const started = Date.now()
37
+ const poll = async () => {
38
+ if (Date.now() - started > 5 * 60 * 1000) { process.stderr.write('\n ◇ link timed out — run `npx thinkpool-pair login` again.\n'); process.exit(1) }
39
+ try {
40
+ const { data: r } = await sb.rpc('claim_device_code', { p_device_code: device_code })
41
+ if (r?.status === 'approved' && r.session?.refresh_token) {
42
+ saveAuth(r.session)
43
+ process.stderr.write(`\n ◆ linked as ${r.session.email || 'your account'}. You can close the browser tab.\n Now run: npx thinkpool-pair\n`)
44
+ process.exit(0)
45
+ }
46
+ if (r?.status === 'expired' || r?.status === 'not_found') { process.stderr.write('\n ◇ link expired — run `npx thinkpool-pair login` again.\n'); process.exit(1) }
47
+ } catch { /* transient — keep polling */ }
48
+ setTimeout(poll, 3000)
49
+ }
50
+ poll()
43
51
  await new Promise(() => {})
44
52
  }
45
53
 
@@ -91,7 +99,8 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
91
99
  process.exit(1)
92
100
  }
93
101
 
94
- process.stderr.write(`\n ◆ thinkpool-pair account mode · ${email}\n Serving your code sessions. Add a session's directory with \`bind\`.\n`)
102
+ const DEFAULT_DIR = process.cwd() // unbound sessions auto-serve here; `bind` overrides per session
103
+ process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${email}\n Auto-serving your sessions from ${DEFAULT_DIR}\n (point one at another repo: npx thinkpool-pair bind <code> <dir>)\n`)
95
104
 
96
105
  const children = new Map() // room -> child process
97
106
  const warned = new Set()
@@ -107,24 +116,20 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
107
116
  const tick = async () => {
108
117
  let rooms = []
109
118
  try {
110
- const { data } = await sb.from('code_sessions').select('code,name,last_active_at').order('last_active_at', { ascending: false })
119
+ const { data } = await sb.from('code_sessions').select('code,name,last_active_at').order('last_active_at', { ascending: false }).limit(20)
111
120
  rooms = data || []
112
121
  } catch { /* transient — keep existing children, retry next tick */ }
113
122
  const dirs = loadDirs()
114
123
  for (const r of rooms) {
115
124
  const room = r.code
116
125
  if (!room || children.has(room)) continue
117
- const dir = dirs[room]
118
- if (!dir) {
119
- if (!warned.has(room)) { warned.add(room); process.stderr.write(`\n ◇ session ${r.name ? `"${r.name}" ` : ''}(${room}) needs a directory:\n npx thinkpool-pair bind ${room} <path>\n`) }
120
- continue
121
- }
126
+ const dir = dirs[room] || DEFAULT_DIR // auto-serve unbound sessions in the launch dir
122
127
  if (!fs.existsSync(dir)) {
123
128
  if (!warned.has(room)) { warned.add(room); process.stderr.write(`\n ◇ ${room}: bound directory is gone (${dir}) — re-bind it.\n`) }
124
129
  continue
125
130
  }
126
131
  warned.delete(room)
127
- process.stderr.write(`\n ◆ serving ${room}${r.name ? ` "${r.name}"` : ''} → ${dir}\n`)
132
+ process.stderr.write(`\n ◆ serving ${room}${r.name ? ` "${r.name}"` : ''} → ${dir}${dirs[room] ? '' : ' (default)'}\n`)
128
133
  const child = spawn(process.execPath, [BRIDGE, room, '--headless'], { cwd: dir, stdio: 'inherit' })
129
134
  children.set(room, child)
130
135
  child.on('exit', () => { children.delete(room) }) // re-served on the next tick
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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": {