instar 1.3.1645 → 1.3.1646

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 (67) hide show
  1. package/dashboard/console/console.js +4 -4
  2. package/dashboard/console/index.html +1 -1
  3. package/dashboard/console/nav-model.js +7 -2
  4. package/dashboard-pair/pair-model.js +66 -0
  5. package/dashboard-pair/pair.css +30 -0
  6. package/dashboard-pair/pair.html +17 -0
  7. package/dashboard-pair/pair.js +332 -0
  8. package/dist/commands/install.d.ts.map +1 -1
  9. package/dist/commands/install.js +2 -1
  10. package/dist/commands/install.js.map +1 -1
  11. package/dist/commands/server.d.ts.map +1 -1
  12. package/dist/commands/server.js +2 -2
  13. package/dist/commands/server.js.map +1 -1
  14. package/dist/core/CodexLifecycleProductionComposition.d.ts +1 -1
  15. package/dist/core/CodexLifecycleProductionComposition.d.ts.map +1 -1
  16. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  17. package/dist/core/PostUpdateMigrator.js +36 -4
  18. package/dist/core/PostUpdateMigrator.js.map +1 -1
  19. package/dist/data/standards-guard-index.json +1 -1
  20. package/dist/data/standards-guard-index.meta.json +2 -2
  21. package/dist/data/standards-registry.meta.json +1 -1
  22. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  23. package/dist/messaging/TelegramAdapter.js +5 -2
  24. package/dist/messaging/TelegramAdapter.js.map +1 -1
  25. package/dist/messaging/slack/threadHistoryLabel.d.ts +16 -0
  26. package/dist/messaging/slack/threadHistoryLabel.d.ts.map +1 -0
  27. package/dist/messaging/slack/threadHistoryLabel.js +19 -0
  28. package/dist/messaging/slack/threadHistoryLabel.js.map +1 -0
  29. package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
  30. package/dist/native/instar-desktop-host +0 -0
  31. package/dist/scaffold/templates.d.ts +5 -3
  32. package/dist/scaffold/templates.d.ts.map +1 -1
  33. package/dist/scaffold/templates.js +5 -3
  34. package/dist/scaffold/templates.js.map +1 -1
  35. package/dist/server/AgentServer.d.ts.map +1 -1
  36. package/dist/server/AgentServer.js +6 -10
  37. package/dist/server/AgentServer.js.map +1 -1
  38. package/dist/server/dashboardEntry.d.ts +32 -0
  39. package/dist/server/dashboardEntry.d.ts.map +1 -0
  40. package/dist/server/dashboardEntry.js +92 -0
  41. package/dist/server/dashboardEntry.js.map +1 -0
  42. package/dist/server/dashboardPaths.d.ts +17 -0
  43. package/dist/server/dashboardPaths.d.ts.map +1 -0
  44. package/dist/server/dashboardPaths.js +23 -0
  45. package/dist/server/dashboardPaths.js.map +1 -0
  46. package/dist/server/middleware.d.ts.map +1 -1
  47. package/dist/server/middleware.js +5 -2
  48. package/dist/server/middleware.js.map +1 -1
  49. package/dist/server/mobilePairingRoutes.d.ts.map +1 -1
  50. package/dist/server/mobilePairingRoutes.js +8 -2
  51. package/dist/server/mobilePairingRoutes.js.map +1 -1
  52. package/dist/server/routes.d.ts.map +1 -1
  53. package/dist/server/routes.js +4 -3
  54. package/dist/server/routes.js.map +1 -1
  55. package/package.json +2 -1
  56. package/scripts/blocking-decision-surfaces-baseline.json +2 -2
  57. package/src/data/builtin-manifest.json +64 -64
  58. package/src/data/standards-guard-index.json +1 -1
  59. package/src/data/standards-guard-index.meta.json +2 -2
  60. package/src/data/standards-registry.meta.json +1 -1
  61. package/src/scaffold/templates.ts +5 -3
  62. package/src/templates/scripts/browser/chrome-launch.sh +7 -2
  63. package/upgrades/1.3.1646.md +40 -0
  64. package/upgrades/side-effects/chrome-keep-drawing-when-locked.eli16.md +9 -0
  65. package/upgrades/side-effects/chrome-keep-drawing-when-locked.md +18 -0
  66. package/upgrades/side-effects/dashboard-console-default-and-pair-page.md +99 -0
  67. package/upgrades/side-effects/slack-history-sender-label.md +36 -0
@@ -8,7 +8,7 @@
8
8
  import {
9
9
  failure, buildNeeds, buildDoing, buildPromised, buildRunning, buildAllowed, headline, ago, combineNeeds, machineNames,
10
10
  } from './home-model.js';
11
- import { GROUPS, legacyHref, icon, tabHref } from './nav-model.js';
11
+ import { GROUPS, legacyHref, icon, tabHref, LEGACY_DASHBOARD } from './nav-model.js';
12
12
  import { fillPartial } from './home-model.js';
13
13
  import { renderPromises, topicLink } from './promises.js';
14
14
  import { renderMachines } from './machines.js';
@@ -210,7 +210,7 @@ function navItems(active) {
210
210
  nav.append(a);
211
211
  }
212
212
  nav.append(el('div', { class: 'nav-group-label', text: 'Elsewhere' }));
213
- const legacy = el('a', { href: '/dashboard', icon: 'external' });
213
+ const legacy = el('a', { href: LEGACY_DASHBOARD, icon: 'external' });
214
214
  legacy.append(el('span', { text: 'Legacy dashboard' }));
215
215
  legacy.classList.add('nav-sub-legacy');
216
216
  nav.append(legacy);
@@ -218,7 +218,7 @@ function navItems(active) {
218
218
  }
219
219
 
220
220
  function renderShell(active) {
221
- const legacyChip = el('a', { class: 'chip chip-legacy', href: '/dashboard', text: 'Legacy' });
221
+ const legacyChip = el('a', { class: 'chip chip-legacy', href: LEGACY_DASHBOARD, text: 'Legacy' });
222
222
  const brand = el('a', { class: 'brand', href: '#/home' },
223
223
  el('img', { src: '/dashboard/logo.png', alt: '' }),
224
224
  el('span', { text: 'instar' }),
@@ -607,7 +607,7 @@ function renderGroup(main, g) {
607
607
  function footNote() {
608
608
  return el('p', { class: 'foot-note' },
609
609
  document.createTextNode('The older dashboard is still available: '),
610
- el('a', { href: '/dashboard', text: 'open the legacy dashboard' }),
610
+ el('a', { href: LEGACY_DASHBOARD, text: 'open the legacy dashboard' }),
611
611
  document.createTextNode('.'),
612
612
  );
613
613
  }
@@ -12,6 +12,6 @@
12
12
  <script type="module" src="console.js"></script>
13
13
  </head>
14
14
  <body>
15
- <noscript>The Instar console needs JavaScript. The <a href="/dashboard">legacy dashboard</a> is here.</noscript>
15
+ <noscript>The Instar console needs JavaScript. The <a href="/dashboard/legacy">legacy dashboard</a> is here.</noscript>
16
16
  </body>
17
17
  </html>
@@ -64,7 +64,7 @@ export const GROUPS = [
64
64
  { id: 'threadline', label: 'Other agents', icon: 'chat', desc: 'Who is waiting on you, whether each agent I work with is actually there, pairing new ones, and our conversations.' },
65
65
  { id: 'origins', label: 'Messages to you', icon: 'chat', desc: 'Who wrote each message I sent you, and the who-wrote-this footer.' },
66
66
  { id: 'mandates', label: 'Permission slips', icon: 'stamp', desc: 'What you have let agents do together, who is waiting for your approval, and every allow and refusal.' },
67
- { id: 'phones', label: 'Phones', icon: 'phone', desc: 'Pair the Instar iPhone app (the code shows here after a PIN unlock when there is no Telegram), and the phones already paired.' },
67
+ { id: 'phones', label: 'Phones', icon: 'phone', desc: 'Pair the Instar iPhone app: a waiting phone’s code, large with a Copy button, after your PIN (when there is no Telegram), and the phones already paired.' },
68
68
  { id: 'secrets', label: 'Passwords & keys', icon: 'lock', desc: 'Whether the agent is waiting on a password, a safe one-time link to hand one over, and the keys it already holds.' },
69
69
  ],
70
70
  },
@@ -118,6 +118,8 @@ export const NATIVE_PAGES = {
118
118
  features: '#/settings/allowed',
119
119
  'preferences-learning': '#/settings/learned',
120
120
  files: '#/settings/files',
121
+ // The pairing code page: its own strict page, outside the console (PIN, then the code + Copy).
122
+ phones: '/dashboard/pair',
121
123
  'pr-pipeline': '#/work/development?s=shipping',
122
124
  throughput: '#/work/development?s=pace',
123
125
  'feedback-drain': '#/work/development?s=feedback',
@@ -131,9 +133,12 @@ export function tabHref(tabId) {
131
133
  return NATIVE_PAGES[tabId] || legacyHref(tabId);
132
134
  }
133
135
 
136
+ /** The legacy dashboard's own address (/dashboard itself opens this console). */
137
+ export const LEGACY_DASHBOARD = '/dashboard/legacy';
138
+
134
139
  /** The legacy URL for a tab. Ordinary link; nothing is embedded. */
135
140
  export function legacyHref(tabId) {
136
- return `/dashboard?tab=${encodeURIComponent(tabId)}`;
141
+ return `${LEGACY_DASHBOARD}?tab=${encodeURIComponent(tabId)}`;
137
142
  }
138
143
 
139
144
  export function icon(name) {
@@ -0,0 +1,66 @@
1
+ // Pure helpers for the pairing code page (docs/specs/dashboard-console-default-and-pair-page.md §3.2).
2
+ // No DOM, no network: unit-tested directly.
3
+
4
+ /** The device id named in the page's fragment (`#d=<id>`), or null. Never sent to the server. */
5
+ export function deviceFromHash(hash) {
6
+ const raw = String(hash || '').replace(/^#/, '');
7
+ if (!raw) return null;
8
+ let id = null;
9
+ try { id = new URLSearchParams(raw).get('d'); } catch { return null; }
10
+ if (!id || id.length > 200 || !/^[A-Za-z0-9_.:-]+$/.test(id)) return null;
11
+ return id;
12
+ }
13
+
14
+ /**
15
+ * Which waiting phones to show. With a named device: only it, or `missing: true` when it is not
16
+ * waiting any more. Without one: every waiting phone.
17
+ */
18
+ export function shownApprovals(approvals, deviceId) {
19
+ const list = Array.isArray(approvals) ? approvals.filter((a) => a && typeof a === 'object' && a.deviceId) : [];
20
+ if (!deviceId) return { list, missing: false };
21
+ const one = list.filter((a) => a.deviceId === deviceId);
22
+ return { list: one, missing: one.length === 0 };
23
+ }
24
+
25
+ /** The code as shown: a word code as sent (`KITE-4172`), six digits as `123 456`. */
26
+ export function prettyCode(code) {
27
+ const raw = String(code ?? '');
28
+ if (/[A-Za-z]/.test(raw)) return raw;
29
+ const c = raw.replace(/\D/g, '');
30
+ return c.length === 6 ? `${c.slice(0, 3)} ${c.slice(3)}` : c;
31
+ }
32
+
33
+ /** The code as copied: what the app accepts, without display spacing (`KITE-4172`, `123456`). */
34
+ export function copyText(code) {
35
+ const raw = String(code ?? '').trim();
36
+ if (/[A-Za-z]/.test(raw)) return raw;
37
+ return raw.replace(/\D/g, '');
38
+ }
39
+
40
+ /** Minutes left, in words. */
41
+ export function minutesLeft(iso, now = Date.now()) {
42
+ const ms = Date.parse(iso) - now;
43
+ if (!(ms > 0)) return 'expired';
44
+ return `${Math.max(1, Math.round(ms / 60_000))} min left`;
45
+ }
46
+
47
+ /** Whose phone: " — Ann (owner)", " — Sam (MSP)", or a removed person. */
48
+ export function personText(d) {
49
+ if (!d) return '';
50
+ if (d.unassigned) return ' — no longer assigned to anyone';
51
+ if (!d.personName) return '';
52
+ return ` — ${d.personName}${d.role === 'msp' ? ' (MSP)' : ' (owner)'}`;
53
+ }
54
+
55
+ /** One line naming a waiting phone. */
56
+ export function waitingLine(a, now = Date.now()) {
57
+ return `${a.deviceName || 'A phone'}${a.label ? ` (${a.label})` : ''}${personText(a)} · ${minutesLeft(a.expiresAt, now)}`;
58
+ }
59
+
60
+ /** What the page says above the waiting list, from the approvals answer. */
61
+ export function waitingHeadline({ approval, shown, missing }) {
62
+ if (approval === 'telegram') return 'This agent sends pairing codes to your private Telegram chat, not to this page.';
63
+ if (missing) return 'That phone is no longer waiting: it was paired, declined, or its link expired.';
64
+ if (!shown.length) return 'No phone is waiting. Open the pairing link on the phone; its code appears here by itself.';
65
+ return shown.length === 1 ? 'A phone is waiting to pair.' : `${shown.length} phones are waiting to pair.`;
66
+ }
@@ -0,0 +1,30 @@
1
+ :root {
2
+ --bg: #0a0e17; --card: #121826; --text: #e8ecf3; --muted: #9aa4b5; --accent: #5b8cff; --line: #243049; --warn: #f2b84b;
3
+ color-scheme: dark light;
4
+ }
5
+ @media (prefers-color-scheme: light) {
6
+ :root { --bg: #f6f7fb; --card: #ffffff; --text: #141a26; --muted: #5b6577; --accent: #2f5fe0; --line: #d9deea; --warn: #9a6a00; }
7
+ }
8
+ * { box-sizing: border-box; }
9
+ body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
10
+ #pair-root { max-width: 560px; margin: 0 auto; padding: 24px 16px 48px; }
11
+ .pair-head h1 { font-size: 1.3rem; margin: 0 0 4px; }
12
+ .pair-host { font-size: 1.6rem; font-weight: 700; margin: 0; word-break: break-all; }
13
+ .pair-hint { color: var(--muted); margin: 4px 0 20px; font-size: .9rem; }
14
+ .pair-pin { display: grid; gap: 10px; }
15
+ .pair-pin input { font-size: 1.4rem; padding: 12px; border-radius: 10px; border: 1px solid var(--line); background: var(--card); color: var(--text); letter-spacing: .2em; }
16
+ .pair-button { font: inherit; font-weight: 600; padding: 12px 18px; border-radius: 10px; border: 0; background: var(--accent); color: #fff; cursor: pointer; text-decoration: none; display: inline-block; text-align: center; }
17
+ .pair-button:disabled { opacity: .6; }
18
+ .pair-link-button { font: inherit; background: none; border: 0; color: var(--accent); padding: 8px 0; cursor: pointer; text-decoration: underline; }
19
+ .pair-error { color: var(--warn); min-height: 1.5em; margin: 0; }
20
+ .pair-card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px; margin: 12px 0; }
21
+ .pair-device { margin: 0 0 8px; color: var(--muted); }
22
+ .pair-code { font: 700 2.6rem/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .06em; margin: 8px 0 12px; user-select: all; -webkit-user-select: all; word-break: break-all; }
23
+ .pair-copied { margin-left: 12px; color: var(--muted); }
24
+ .pair-fp { color: var(--muted); font-size: .85rem; word-break: break-all; }
25
+ .pair-paired { margin-top: 32px; }
26
+ .pair-paired h2, section h2 { font-size: 1.05rem; }
27
+ .pair-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--line); }
28
+ .pair-status { min-height: 1.5em; color: var(--muted); }
29
+ .pair-foot { margin-top: 32px; font-size: .9rem; }
30
+ .pair-foot a, section a { color: var(--accent); }
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <meta name="color-scheme" content="dark light">
7
+ <meta name="referrer" content="no-referrer">
8
+ <title>Pair a phone</title>
9
+ <link rel="icon" href="/dashboard/favicon.png">
10
+ <link rel="stylesheet" href="/dashboard/pair.css">
11
+ <script type="module" src="/dashboard/pair.js"></script>
12
+ </head>
13
+ <body>
14
+ <div id="pair-root"></div>
15
+ <noscript>This page needs JavaScript. The <a href="/dashboard/legacy?tab=phones">legacy dashboard's Phones tab</a> shows the same code.</noscript>
16
+ </body>
17
+ </html>
@@ -0,0 +1,332 @@
1
+ // The pairing code page (docs/specs/dashboard-console-default-and-pair-page.md §3.2).
2
+ //
3
+ // SAFETY CONTRACT
4
+ // - A code is shown only after a PIN unlock made on THIS page load. The Bearer token and the
5
+ // operator session live in page memory only; nothing is written to localStorage or
6
+ // sessionStorage, and a refused PIN, a 403 or session expiry wipes every code from the page.
7
+ // - Every dynamic value reaches the DOM through textContent. No innerHTML anywhere.
8
+ // - The page is served with a strict Content-Security-Policy (no inline script, nothing
9
+ // third-party); keep it that way: no inline handlers, no style attributes, no external URLs.
10
+
11
+ import { deviceFromHash, shownApprovals, prettyCode, copyText, waitingLine, waitingHeadline } from './pair-model.js';
12
+
13
+ const POLL_MS = 10_000;
14
+ const MAX_FAILURES = 5;
15
+ const REQUEST_MS = 15_000; // one authenticated call never holds the page longer than this
16
+ const CLOCK_MARGIN_MS = 5_000; // network transit and the Date header's one-second precision
17
+ const STATUS_WORDS = { pending: 'Waiting to confirm', active: 'Paired' };
18
+
19
+ export function mountPairPage(doc, {
20
+ fetchFn = (...a) => fetch(...a),
21
+ location: loc = doc.defaultView.location,
22
+ // Monotonic where available, so a clock change on the phone moves no deadline.
23
+ now = () => { const p = doc.defaultView.performance; return p && typeof p.now === 'function' && p.timeOrigin ? p.timeOrigin + p.now() : Date.now(); },
24
+ clipboard = doc.defaultView.navigator && doc.defaultView.navigator.clipboard,
25
+ confirmFn = (q) => doc.defaultView.confirm(q),
26
+ setTimer = (fn, ms) => doc.defaultView.setInterval(fn, ms),
27
+ clearTimer = (t) => doc.defaultView.clearInterval(t),
28
+ setDeadline = (fn, ms) => doc.defaultView.setTimeout(fn, ms),
29
+ clearDeadline = (t) => doc.defaultView.clearTimeout(t),
30
+ setRequestTimer = (fn, ms) => doc.defaultView.setTimeout(fn, ms),
31
+ clearRequestTimer = (t) => doc.defaultView.clearTimeout(t),
32
+ } = {}) {
33
+ const root = doc.getElementById('pair-root') || doc.body;
34
+ const make = (tag, opts = {}, parent) => {
35
+ const el = doc.createElement(tag);
36
+ if (opts.text !== undefined) el.textContent = String(opts.text);
37
+ if (opts.cls) el.className = opts.cls;
38
+ if (opts.type) el.type = opts.type;
39
+ for (const [k, v] of Object.entries(opts.attrs || {})) el.setAttribute(k, v);
40
+ if (parent) parent.append(el);
41
+ return el;
42
+ };
43
+
44
+ let auth = null; // { token, session, expiresAt } — page memory only
45
+ let timer = null;
46
+ let deadline = null; // one-shot: wipes the page when the operator session runs out
47
+ let failures = 0;
48
+ let busy = false;
49
+ let again = false;
50
+ let gen = 0; // bumped by every reset // a refresh asked for while one was running: run once more after it
51
+ let wanted = deviceFromHash(loc.hash);
52
+ const SESSION_MS = 15 * 60_000;
53
+
54
+ root.replaceChildren();
55
+ const head = make('header', { cls: 'pair-head' }, root);
56
+ make('h1', { text: 'Pair a phone with this agent' }, head);
57
+ make('p', { cls: 'pair-host', text: loc.host, attrs: { 'aria-label': 'This page’s address' } }, head);
58
+ make('p', { cls: 'pair-hint', text: 'Only type your PIN here if this is your agent’s usual address.' }, head);
59
+ const body = make('main', { cls: 'pair-body' }, root);
60
+ const status = make('p', { cls: 'pair-status', attrs: { role: 'status', 'aria-live': 'polite' } }, root);
61
+ const foot = make('p', { cls: 'pair-foot' }, root);
62
+ const back = make('a', { text: 'Back to the dashboard', attrs: { href: '/dashboard' } }, foot);
63
+ void back;
64
+
65
+ // The two calls made before there is a session (unlock-check, unlock): same time limit, body included.
66
+ async function plain(url, options = {}) {
67
+ let fail = () => {};
68
+ const timedOut = new Promise((_, reject) => { fail = () => reject(new Error('timeout')); });
69
+ const t = setRequestTimer(() => fail(), REQUEST_MS);
70
+ try {
71
+ return await Promise.race([(async () => {
72
+ const r = await fetchFn(url, { cache: 'no-store', ...options });
73
+ return { ok: r.ok, status: r.status, headers: r.headers, data: await r.json().catch(() => ({})) };
74
+ })(), timedOut]);
75
+ } finally { clearRequestTimer(t); }
76
+ }
77
+
78
+ function stopPolling() { if (timer !== null) { clearTimer(timer); timer = null; } }
79
+
80
+ // The one reset: credentials, every rendered code and copy button, timers. Runs on a refused
81
+ // session (403), at the session deadline (even while hidden or not polling), and on return to a
82
+ // page whose session ran out meanwhile.
83
+ function forget(message) {
84
+ auth = null; stopPolling(); failures = 0; again = false;
85
+ gen++; busy = false; pairedBusy = false; // a refresh still unwinding from the old session can no longer hold the page
86
+ for (const e of [...inflight]) e.abort();
87
+ inflight.clear();
88
+ if (deadline !== null) { clearDeadline(deadline); deadline = null; }
89
+ body.replaceChildren(); status.textContent = '';
90
+ showPin(message);
91
+ }
92
+ const EXPIRED = 'Your unlock ran out. Type your PIN again.';
93
+ doc.addEventListener('visibilitychange', () => {
94
+ if (auth && now() >= auth.expiresAt) forget(EXPIRED);
95
+ });
96
+
97
+ async function showPin(message) {
98
+ body.replaceChildren();
99
+ let check = { accepted: true };
100
+ try {
101
+ const r = await plain('/dashboard/unlock-check');
102
+ if (r.ok && r.data && r.data.accepted === false) check = r.data;
103
+ } catch { /* unreadable or slow check: offer the box; the server still decides */ }
104
+ if (auth) return; // unlocked meanwhile
105
+ body.replaceChildren();
106
+ if (check.accepted === false) {
107
+ make('p', { text: typeof check.reason === 'string' ? check.reason : 'Your PIN cannot be accepted on this address.' }, body);
108
+ if (typeof check.secureUrl === 'string' && /^https:\/\//i.test(check.secureUrl)) {
109
+ try {
110
+ const u = new URL(check.secureUrl);
111
+ make('a', { cls: 'pair-button', text: 'Open the secure address', attrs: { href: `${u.origin}/dashboard/pair${loc.hash || ''}` } }, body);
112
+ } catch { /* malformed: no link */ }
113
+ }
114
+ return;
115
+ }
116
+ const form = make('form', { cls: 'pair-pin' }, body);
117
+ make('label', { text: 'Dashboard PIN', attrs: { for: 'pair-pin-input' } }, form);
118
+ const input = make('input', { attrs: { id: 'pair-pin-input', type: 'password', inputmode: 'numeric', autocomplete: 'off', maxlength: '12' } }, form);
119
+ const go = make('button', { cls: 'pair-button', text: 'Show the code', type: 'submit' }, form);
120
+ const err = make('p', { cls: 'pair-error', text: message || '', attrs: { role: 'alert' } }, form);
121
+ form.addEventListener('submit', async (ev) => {
122
+ ev.preventDefault();
123
+ const pin = input.value;
124
+ if (!pin) { err.textContent = 'Type your PIN.'; return; }
125
+ go.disabled = true; err.textContent = '';
126
+ try {
127
+ const r = await plain('/dashboard/unlock', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pin }) });
128
+ input.value = '';
129
+ const d = r.data || {};
130
+ if (!r.ok || typeof d.token !== 'string' || typeof d.operatorSessionToken !== 'string') {
131
+ err.textContent = d && d.error ? `${d.error}${typeof d.attemptsRemaining === 'number' ? ` · ${d.attemptsRemaining} tries left` : ''}` : 'That did not work. Try again.';
132
+ go.disabled = false; return;
133
+ }
134
+ // The session's remaining life, measured on the server's clock (its Date header), so a
135
+ // phone clock that runs fast or slow neither locks the page nor keeps a code too long.
136
+ const serverExp = Date.parse(d.operatorSessionExpiresAt);
137
+ const serverNow = Date.parse(r.headers && typeof r.headers.get === 'function' ? r.headers.get('Date') : '');
138
+ const life = Number.isFinite(serverExp) && Number.isFinite(serverNow) ? serverExp - serverNow : SESSION_MS;
139
+ auth = { token: d.token, session: d.operatorSessionToken, expiresAt: now() + Math.min(SESSION_MS, Math.max(0, life - CLOCK_MARGIN_MS)) };
140
+ if (deadline !== null) clearDeadline(deadline);
141
+ deadline = setDeadline(() => { deadline = null; if (auth) forget(EXPIRED); }, Math.max(0, auth.expiresAt - now()));
142
+ await refresh();
143
+ stopPolling();
144
+ if (auth) timer = setTimer(() => { if (doc.visibilityState !== 'hidden') void refresh(); }, POLL_MS);
145
+ } catch { input.value = ''; err.textContent = 'The agent did not answer. Try again.'; go.disabled = false; }
146
+ });
147
+ try { input.focus(); } catch { /* not focusable in this environment */ }
148
+ }
149
+
150
+ // ── Authenticated calls ──────────────────────────────────────────────────────────────────
151
+ // One rule for every call made with the session: bound to the session it was made under,
152
+ // bounded in time, cancelled by the reset, and any 403 runs the reset.
153
+ const inflight = new Set();
154
+ class SessionGone extends Error {}
155
+ async function request(url, options = {}) {
156
+ const mine = auth;
157
+ if (!mine) throw new SessionGone();
158
+ let abort = () => {};
159
+ const aborted = new Promise((_, reject) => { abort = () => reject(new Error('aborted')); });
160
+ const ctl = typeof AbortController === 'function' ? new AbortController() : null;
161
+ const entry = { abort: () => { if (ctl) ctl.abort(); abort(); } };
162
+ inflight.add(entry);
163
+ const t = setRequestTimer(entry.abort, REQUEST_MS);
164
+ try {
165
+ // Headers AND body inside the same time limit and cancellation; the caller gets parsed data.
166
+ const out = await Promise.race([(async () => {
167
+ const r = await fetchFn(url, {
168
+ ...options, cache: 'no-store', ...(ctl ? { signal: ctl.signal } : {}),
169
+ headers: { Authorization: `Bearer ${mine.token}`, 'X-Instar-Operator-Session': mine.session, ...(options.headers || {}) },
170
+ });
171
+ const data = r.status === 403 ? null : await r.json().catch(() => null);
172
+ return { status: r.status, ok: r.ok, data };
173
+ })(), aborted]);
174
+ // Only the live session that made the call, before its deadline, may use the answer.
175
+ if (auth !== mine || now() >= mine.expiresAt) throw new SessionGone();
176
+ if (out.status === 403) { forget(EXPIRED); throw new SessionGone(); }
177
+ return out;
178
+ } finally { clearRequestTimer(t); inflight.delete(entry); }
179
+ }
180
+ const gone = (e) => e instanceof SessionGone || !auth;
181
+
182
+ // ── Layout: the waiting phones first, the paired phones below, filled independently ────────
183
+ const waitingBox = doc.createElement('section'); waitingBox.setAttribute('aria-label', 'Phones waiting to pair');
184
+ const pairedBox = doc.createElement('section'); pairedBox.className = 'pair-paired'; pairedBox.setAttribute('aria-label', 'Paired phones');
185
+ let lastData = null;
186
+ function ensureLayout() {
187
+ if (body.contains(waitingBox)) return;
188
+ waitingBox.replaceChildren(); pairedBox.replaceChildren();
189
+ make('h2', { text: 'Paired phones' }, pairedBox);
190
+ make('p', { text: 'Reading the paired phones…' }, pairedBox);
191
+ body.replaceChildren(waitingBox, pairedBox);
192
+ }
193
+
194
+ async function refresh() {
195
+ if (!auth) return;
196
+ if (busy) { again = true; return; }
197
+ if (now() >= auth.expiresAt) { forget(EXPIRED); return; }
198
+ busy = true;
199
+ const myGen = gen; // a refresh belongs to the unlock that started it
200
+ try {
201
+ const a = await request('/mobile/pairing-approvals');
202
+ if (a.status === 503) { body.replaceChildren(); make('p', { text: 'Phone pairing is turned off on this agent.' }, body); failures = 0; return; }
203
+ if (!a.ok || !a.data) throw new Error(`approvals ${a.status}`);
204
+ const data = a.data;
205
+ failures = 0; status.textContent = '';
206
+ lastData = data;
207
+ renderWaiting(data); // the code shows now; the paired-phones read runs on its own
208
+ void refreshPaired();
209
+ } catch (e) {
210
+ if (gone(e)) return;
211
+ failures++;
212
+ if (failures >= MAX_FAILURES) {
213
+ stopPolling();
214
+ status.replaceChildren();
215
+ make('span', { text: 'Lost contact with the agent. ' }, status);
216
+ const retry = make('button', { cls: 'pair-link-button', text: 'Retry', type: 'button' }, status);
217
+ retry.addEventListener('click', () => {
218
+ failures = 0; status.textContent = '';
219
+ void refresh();
220
+ stopPolling();
221
+ timer = setTimer(() => { if (doc.visibilityState !== 'hidden') void refresh(); }, POLL_MS);
222
+ });
223
+ } else status.textContent = 'Could not reach the agent; trying again.';
224
+ } finally {
225
+ if (gen === myGen) {
226
+ busy = false;
227
+ if (again) { again = false; if (auth) void refresh(); }
228
+ }
229
+ }
230
+ }
231
+
232
+ // The paired-phones list: its own read, never holding up the codes above it.
233
+ let pairedBusy = false;
234
+ async function refreshPaired() {
235
+ if (!auth || pairedBusy) return;
236
+ pairedBusy = true;
237
+ const myGen = gen;
238
+ try {
239
+ const d = await request('/mobile/devices');
240
+ renderPaired(d.ok && d.data && Array.isArray(d.data.devices) ? d.data.devices : null);
241
+ } catch (e) { if (!gone(e)) renderPaired(null); }
242
+ finally { if (gen === myGen) pairedBusy = false; }
243
+ }
244
+
245
+ // Taps on the page's buttons: a reset deadline first, then the call; the result is shown only
246
+ // while the session that made it is still the live one.
247
+ async function act(path, done, failed, onDone) {
248
+ if (!auth || now() >= auth.expiresAt) { forget(EXPIRED); return; }
249
+ try {
250
+ const r = await request(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
251
+ status.textContent = r.ok ? done : failed;
252
+ if (r.ok && onDone) onDone();
253
+ } catch (e) { if (gone(e)) return; status.textContent = 'Could not reach the agent.'; }
254
+ await refresh();
255
+ }
256
+ // Phones declined on this page: never drawn again, even by a read that started before the decline.
257
+ const declined = new Set();
258
+
259
+ function renderWaiting(data) {
260
+ ensureLayout();
261
+ const approvals = Array.isArray(data && data.approvals) ? data.approvals : [];
262
+ const { list, missing } = shownApprovals(approvals.filter((x) => x && !declined.has(x.deviceId)), wanted);
263
+ waitingBox.replaceChildren();
264
+ make('h2', { text: waitingHeadline({ approval: data && data.approval, shown: list, missing }) }, waitingBox);
265
+ if (missing && approvals.length) {
266
+ // Same page, same unlock: drop the fragment in place instead of reloading (a reload would ask for the PIN again).
267
+ const all = make('button', { cls: 'pair-link-button', text: 'Show every waiting phone', type: 'button' }, waitingBox);
268
+ all.addEventListener('click', () => {
269
+ wanted = null;
270
+ try { doc.defaultView.history.replaceState(null, '', loc.pathname); } catch { /* history unavailable */ }
271
+ if (lastData && auth) renderWaiting(lastData);
272
+ });
273
+ }
274
+ for (const a of list) {
275
+ const card = make('div', { cls: 'pair-card' }, waitingBox);
276
+ make('p', { cls: 'pair-device', text: waitingLine(a, now()) }, card);
277
+ const code = make('p', { cls: 'pair-code', text: prettyCode(a.code), attrs: { 'aria-label': 'Confirmation code' } }, card);
278
+ const copy = make('button', { cls: 'pair-button', text: 'Copy', type: 'button' }, card);
279
+ const copied = make('span', { cls: 'pair-copied', attrs: { role: 'status' } }, card);
280
+ copy.addEventListener('click', async () => {
281
+ if (!auth || now() >= auth.expiresAt) { forget(EXPIRED); return; }
282
+ try {
283
+ if (!clipboard || typeof clipboard.writeText !== 'function') throw new Error('no clipboard');
284
+ await clipboard.writeText(copyText(a.code));
285
+ copied.textContent = 'Copied';
286
+ } catch {
287
+ try {
288
+ const range = doc.createRange(); range.selectNodeContents(code);
289
+ const sel = doc.defaultView.getSelection(); sel.removeAllRanges(); sel.addRange(range);
290
+ } catch { /* selection unavailable */ }
291
+ copied.textContent = 'Press and hold the code to copy it.';
292
+ }
293
+ });
294
+ make('p', { text: 'Paste it into the Instar app on that phone. Never send it to anyone, including me.' }, card);
295
+ if (a.fingerprint) make('p', { cls: 'pair-fp', text: `Phone key: ${a.fingerprint}. The app shows the same key; check they match.` }, card);
296
+ const decline = make('button', { cls: 'pair-link-button', text: 'Not my phone — decline', type: 'button' }, card);
297
+ decline.addEventListener('click', () => {
298
+ decline.disabled = true;
299
+ void act(`/mobile/pairing-approvals/${encodeURIComponent(a.deviceId)}/decline`, 'Declined. That phone cannot pair with this code.', 'Could not decline. Try again.',
300
+ () => { declined.add(a.deviceId); if (lastData && auth) renderWaiting(lastData); });
301
+ });
302
+ }
303
+ }
304
+
305
+ function renderPaired(devices) {
306
+ ensureLayout();
307
+ pairedBox.replaceChildren();
308
+ make('h2', { text: 'Paired phones' }, pairedBox);
309
+ if (devices === null) { make('p', { text: 'Could not read the paired phones.' }, pairedBox); return; }
310
+ const shown = devices.filter((d) => d && (d.status === 'active' || d.status === 'pending'));
311
+ if (!shown.length) { make('p', { text: 'No phones are paired with this machine.' }, pairedBox); return; }
312
+ for (const d of shown) {
313
+ const row = make('div', { cls: 'pair-row' }, pairedBox);
314
+ make('span', { text: `${d.deviceName || 'Unnamed phone'} · ${STATUS_WORDS[d.status] || d.status}` }, row);
315
+ if (d.status !== 'active') continue;
316
+ const remove = make('button', { cls: 'pair-link-button', text: 'Remove this phone', type: 'button' }, row);
317
+ remove.addEventListener('click', () => {
318
+ if (!confirmFn('Remove this phone? It will stop reaching this agent at once.')) return;
319
+ remove.disabled = true;
320
+ void act(`/mobile/devices/${encodeURIComponent(d.id)}/revoke`, 'Removed.', 'Could not remove that phone. Try again.', () => { void refreshPaired(); });
321
+ });
322
+ }
323
+ }
324
+
325
+ void showPin();
326
+ return { refresh, forget, stop: stopPolling, isUnlocked: () => Boolean(auth) };
327
+ }
328
+
329
+ // Boot in a browser; tests import mountPairPage and drive it themselves.
330
+ if (typeof document !== 'undefined' && document.getElementById && document.getElementById('pair-root')) {
331
+ mountPairPage(document);
332
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,EAAE,qBAAqB,EAAE,CAAC;AACjC,OAAO,EAKL,KAAK,WAAW,EAEhB,KAAK,aAAa,EACnB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAkC,KAAK,WAAW,EAAmB,MAAM,qBAAqB,CAAC;AACxG,OAAO,EASL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,uBAAuB,CAAC;AAE/B,yGAAyG;AACzG,eAAO,MAAM,cAAc,QAAkB,CAAC;AAU9C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,4GAA4G;AAC5G,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0BpG;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,iEAAiE;AACjE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQhE;AAED,4GAA4G;AAC5G,wBAAgB,QAAQ,IAAI,OAAO,CAWlC;AAMD,mGAAmG;AACnG,wBAAgB,kBAAkB,CAChC,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,MAAM,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAuB,GACnD,MAAM,EAAE,GAAG,IAAI,CAEjB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,MAAM,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAuB,GACnD,MAAM,EAAE,GAAG,IAAI,CAOjB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,MAAM,GAAG,IAAI,EAC9B,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,GAAG,GAAE,MAAM,GAAG,IAAqE,GAClF,MAAM,EAAE,GAAG,IAAI,CAcjB;AAYD,kHAAkH;AAClH,wBAAgB,aAAa,CAAC,IAAI,GAAE,MAAqB,GAAG,MAAM,CAEjE;AAYD,sGAAsG;AACtG,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,EAAE,CAoBpE;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IAC/D,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,MAAM,GAAG,IAAI,CAAC;IAC/C,wEAAwE;IACxE,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAClF,4DAA4D;IAC5D,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,uDAAuD;IACvD,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,IAAI,CAAC;IAChE,+FAA+F;IAC/F,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,IAAI,CAAC;IAClE,iGAAiG;IACjG,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAC7C,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChF;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C,gEAAgE;IAChE,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAClF,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,2EAA2E;IAC3E,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAUD;;;GAGG;AACH,eAAO,MAAM,8BAA8B;IACzC,OAAO;IACP,UAAU;QAAI,OAAO;;IACrB,MAAM;QAAI,OAAO;QAAQ,KAAK;YAAI,OAAO;;;CAC1C,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAC9D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,KAAK,GAAG,aAAa,CAAA;CAAE,CAAC;AAEhE,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU7D;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAW1D;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAS3D;AAED,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMlD;AAED,yHAAyH;AACzH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAUxE;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,iCAAiC,EAAE,gBAAgB,EAAE,WAAW,CAAC,CAAC,CAuN9I;AAmGD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa;IAErD,UAAU;IACV,IAAI;IACJ,SAAS;IACT,IAAI;IACJ,WAAW;IACX,eAAe;IACf,WAAW;EAEd;AAwBD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAsBtE;AAkHD,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOhG;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAQ5D,CAAC;AAEF,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI,CAQ1E;AAuBD,4GAA4G;AAC5G,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAEvF;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC,CAkF9F"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,7 @@
5
5
  * output goes to a log. The ledger in ./install/progress.ts lets a harness poll it and `--resume`
6
6
  * continue it.
7
7
  */
8
+ import { codePageUrlForOrigin } from '../server/dashboardPaths.js';
8
9
  import { execFileSync, spawn, spawnSync } from 'node:child_process';
9
10
  import { randomInt } from 'node:crypto';
10
11
  import fs from 'node:fs';
@@ -489,7 +490,7 @@ export function buildInstallHandlers(env) {
489
490
  env.print(' Your iPhone needs Tailscale on, signed in to the same account as this computer.');
490
491
  env.print(telegram
491
492
  ? ' The app then asks for a code. Your agent sends it to you privately in Telegram.'
492
- : ` The app then asks for a code. To see it, open ${origin}/dashboard on your phone, enter your PIN, and open Phones.`);
493
+ : ` The app then asks for a code. To see it, open ${codePageUrlForOrigin(origin)} on your phone and enter your PIN. Tap Copy, then paste it into the app.`);
493
494
  }
494
495
  env.print('');
495
496
  return { kind: 'done' };