instar 1.3.623 → 1.3.625
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/dashboard/index.html +22 -0
- package/dashboard/subscriptions.js +398 -5
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +2 -3
- package/dist/commands/server.js.map +1 -1
- package/dist/core/FrameworkLoginDriver.d.ts +10 -0
- package/dist/core/FrameworkLoginDriver.d.ts.map +1 -1
- package/dist/core/FrameworkLoginDriver.js +13 -0
- package/dist/core/FrameworkLoginDriver.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +352 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/1.3.624.md +23 -0
- package/upgrades/1.3.625.md +23 -0
- package/upgrades/side-effects/account-machine-matrix.md +39 -0
- package/upgrades/side-effects/ws52-code-paste-back.md +105 -0
package/dashboard/index.html
CHANGED
|
@@ -3393,6 +3393,19 @@
|
|
|
3393
3393
|
.sub-pending-url { font-size:12px; opacity:.7; word-break:break-all; margin-top:4px; }
|
|
3394
3394
|
.sub-pending-reissue { font-size:11px; opacity:.55; margin-top:4px; }
|
|
3395
3395
|
.sub-empty, .sub-disabled { font-size:13px; opacity:.7; padding:8px 0; }
|
|
3396
|
+
/* account-machine-matrix grid */
|
|
3397
|
+
.sub-matrix { border-collapse:collapse; width:100%; font-size:13px; }
|
|
3398
|
+
.sub-matrix th, .sub-matrix td { border:1px solid var(--border,#2a2a2a); padding:8px 10px; text-align:center; vertical-align:top; }
|
|
3399
|
+
.sub-matrix-acct { text-align:left; font-weight:600; word-break:break-all; }
|
|
3400
|
+
.sub-matrix-mach { font-weight:600; }
|
|
3401
|
+
.sub-matrix-off { opacity:.55; }
|
|
3402
|
+
.sub-matrix-cell.sub-matrix-offline { opacity:.5; }
|
|
3403
|
+
.sub-matrix-glyph { font-size:13px; }
|
|
3404
|
+
.sub-matrix-setup, .sub-matrix-confirm, .sub-matrix-code-submit { margin-top:4px; padding:4px 10px; cursor:pointer; }
|
|
3405
|
+
.sub-matrix-pin, .sub-matrix-code-input { display:block; margin:4px auto; padding:3px 6px; width:90%; box-sizing:border-box; }
|
|
3406
|
+
.sub-matrix-status { font-size:11px; opacity:.8; margin-top:4px; }
|
|
3407
|
+
.sub-matrix-signin { display:inline-block; margin-top:4px; }
|
|
3408
|
+
.sub-matrix-url { font-size:11px; opacity:.7; word-break:break-all; margin-top:4px; }
|
|
3396
3409
|
</style>
|
|
3397
3410
|
<div id="subscriptionsPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
|
|
3398
3411
|
<h2 class="ph-title">Subscriptions</h2>
|
|
@@ -3401,6 +3414,14 @@
|
|
|
3401
3414
|
your subscriptions (ws52-operator-tap-not-text Part A). Leads the panel as the primary
|
|
3402
3415
|
action when present; silent otherwise. -->
|
|
3403
3416
|
<div id="subFollowMe"></div>
|
|
3417
|
+
<!-- Account × Machine matrix (account-machine-matrix) — at-a-glance "which account is set up
|
|
3418
|
+
on which machine," with a one-tap "Set up" per empty cell that runs the whole sign-in in
|
|
3419
|
+
the dashboard (PIN → sign-in link → paste the code). Hidden when there's nothing to show. -->
|
|
3420
|
+
<section class="ph-section">
|
|
3421
|
+
<h3 class="ph-h">Accounts on each machine</h3>
|
|
3422
|
+
<p class="ph-h-sub">Which subscription is set up on which machine. Tap “Set up” on an empty cell to add one — you’ll enter your PIN, open the sign-in link, and paste the code right here.</p>
|
|
3423
|
+
<div id="subMatrix"></div>
|
|
3424
|
+
</section>
|
|
3404
3425
|
<section class="ph-section">
|
|
3405
3426
|
<h3 class="ph-h">Accounts</h3>
|
|
3406
3427
|
<p class="ph-h-sub">Each account’s live usage (5-hour and weekly) and when it resets.</p>
|
|
@@ -9157,6 +9178,7 @@
|
|
|
9157
9178
|
accounts: document.getElementById('subAccounts'),
|
|
9158
9179
|
pending: document.getElementById('subPending'),
|
|
9159
9180
|
followMe: document.getElementById('subFollowMe'),
|
|
9181
|
+
matrix: document.getElementById('subMatrix'),
|
|
9160
9182
|
};
|
|
9161
9183
|
// Pass opts through (method/body/signal) so the controller can POST the follow-me
|
|
9162
9184
|
// scan + the PIN-gated issue-for-machine, not just GET.
|
|
@@ -271,6 +271,9 @@ export function renderPendingLogins(doc, target, logins, now = Date.now()) {
|
|
|
271
271
|
}
|
|
272
272
|
for (const l of logins) {
|
|
273
273
|
const row = el(doc, 'div', 'sub-pending');
|
|
274
|
+
// Non-sensitive identifiers for the code-submit handler (never a credential).
|
|
275
|
+
row.setAttribute('data-login-id', sanitizeForDisplay(l && l.id, 'label'));
|
|
276
|
+
if (l && (l.machineId || l.machineNickname)) row.setAttribute('data-machine-id', sanitizeForDisplay(l.machineId, 'label'));
|
|
274
277
|
|
|
275
278
|
// Lead with a plain-language headline naming what to do + where (machine).
|
|
276
279
|
const machine = sanitizeForDisplay(l && (l.machineNickname || l.machineId), 'label');
|
|
@@ -300,6 +303,22 @@ export function renderPendingLogins(doc, target, logins, now = Date.now()) {
|
|
|
300
303
|
row.appendChild(el(doc, 'div', 'sub-pending-code', `Code: ${sanitizeForDisplay(l.userCode, 'code')}`));
|
|
301
304
|
}
|
|
302
305
|
|
|
306
|
+
// url-code-paste flow (Claude): after signing in, the provider hands the operator a
|
|
307
|
+
// CODE to paste back. Give them a field for it right here — so it goes straight to the
|
|
308
|
+
// machine doing the login (off-chat), not relayed by hand. (ws52-code-paste-back)
|
|
309
|
+
if (l && l.kind === 'url-code-paste') {
|
|
310
|
+
row.appendChild(el(doc, 'div', 'sub-pending-codehint', 'After you sign in, paste the code the page gives you here:'));
|
|
311
|
+
const input = doc.createElement('input');
|
|
312
|
+
input.setAttribute('type', 'text');
|
|
313
|
+
input.setAttribute('class', 'sub-pending-code-input');
|
|
314
|
+
input.setAttribute('placeholder', 'Paste your sign-in code');
|
|
315
|
+
input.setAttribute('autocomplete', 'off');
|
|
316
|
+
row.appendChild(input);
|
|
317
|
+
const submit = el(doc, 'button', 'sub-pending-code-submit', 'Submit code');
|
|
318
|
+
submit.setAttribute('data-submit-code', '1');
|
|
319
|
+
row.appendChild(submit);
|
|
320
|
+
}
|
|
321
|
+
|
|
303
322
|
// One short secondary line: the TTL, and the flow notice only if present (trimmed).
|
|
304
323
|
const ttl = l && l.ttlExpiresAt ? countdown(l.ttlExpiresAt, now) : '';
|
|
305
324
|
if (ttl) row.appendChild(el(doc, 'div', 'sub-pending-ttl', `Link expires in ${ttl}`));
|
|
@@ -311,6 +330,156 @@ export function renderPendingLogins(doc, target, logins, now = Date.now()) {
|
|
|
311
330
|
}
|
|
312
331
|
}
|
|
313
332
|
|
|
333
|
+
// ── Account × Machine matrix (account-machine-matrix spec) ─────────────────
|
|
334
|
+
// At-a-glance "which account is set up on which machine," with a one-tap "Set up"
|
|
335
|
+
// per empty cell that runs the whole sign-in IN the dashboard (PIN → mandate →
|
|
336
|
+
// enroll-start → paste the code). Pure renderer so the jsdom tests exercise the
|
|
337
|
+
// SHIPPED grid. Built ENTIRELY from already-shipped pool-scope reads (FD1): the
|
|
338
|
+
// matrix invents no account key — it pivots `(accountId, machineId)` rows.
|
|
339
|
+
//
|
|
340
|
+
// Inputs:
|
|
341
|
+
// poolScope = GET /subscription-pool?scope=pool body
|
|
342
|
+
// { accounts:[{id,email,status,machineId,machineNickname,remote}],
|
|
343
|
+
// pool:{ selfMachineId, failed:[{machineId,error}] } }
|
|
344
|
+
// pendingScope = GET /subscription-pool/pending-logins?scope=pool body
|
|
345
|
+
// { logins:[{id,machineId,...}] } (id === accountId for matrix enrollments)
|
|
346
|
+
// transient = optional client-side last-attempt map keyed `${accountId}::${machineId}`
|
|
347
|
+
// → { state:'held'|'cant-resolve' } (FD6 — known only to the client)
|
|
348
|
+
|
|
349
|
+
/** Pivot the pool-scope + pending-scope bodies into a grid model. Pure + testable. */
|
|
350
|
+
export function buildMatrixModel(poolScope, pendingScope, transient = {}) {
|
|
351
|
+
const accountRows = (poolScope && Array.isArray(poolScope.accounts)) ? poolScope.accounts : [];
|
|
352
|
+
const pendingRows = (pendingScope && Array.isArray(pendingScope.logins)) ? pendingScope.logins : [];
|
|
353
|
+
const failed = (poolScope && poolScope.pool && Array.isArray(poolScope.pool.failed)) ? poolScope.pool.failed : [];
|
|
354
|
+
const selfMachineId = (poolScope && poolScope.pool && poolScope.pool.selfMachineId) || null;
|
|
355
|
+
const offlineMachineIds = new Set(failed.map((f) => f && f.machineId).filter(Boolean));
|
|
356
|
+
|
|
357
|
+
// Columns = union of machines from account rows + failed (offline) machines. A failed
|
|
358
|
+
// machine has NO account rows (pool-scope queries live, codex r3 #1) — so its column is
|
|
359
|
+
// discovered from the failed list and rendered offline (never a fabricated per-account ✓).
|
|
360
|
+
const machines = new Map(); // machineId → { machineId, nickname, offline }
|
|
361
|
+
for (const a of accountRows) {
|
|
362
|
+
const mid = a && a.machineId;
|
|
363
|
+
if (!mid || offlineMachineIds.has(mid)) continue;
|
|
364
|
+
if (!machines.has(mid)) machines.set(mid, { machineId: mid, nickname: (a.machineNickname || mid), offline: false });
|
|
365
|
+
}
|
|
366
|
+
for (const f of failed) {
|
|
367
|
+
const mid = f && f.machineId;
|
|
368
|
+
if (!mid) continue;
|
|
369
|
+
if (!machines.has(mid)) machines.set(mid, { machineId: mid, nickname: mid, offline: true });
|
|
370
|
+
else machines.get(mid).offline = true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Rows = union of account ids, displayed by email (FD8 — keyed by pool id, shown by email).
|
|
374
|
+
const accounts = new Map(); // accountId → { accountId, email }
|
|
375
|
+
for (const a of accountRows) {
|
|
376
|
+
const id = a && a.id;
|
|
377
|
+
if (!id) continue;
|
|
378
|
+
if (!accounts.has(id)) accounts.set(id, { accountId: id, email: a.email || id });
|
|
379
|
+
else if (!accounts.get(id).email && a.email) accounts.get(id).email = a.email;
|
|
380
|
+
}
|
|
381
|
+
// A pending matrix login can reference an account not yet in any pool row — surface its row too.
|
|
382
|
+
for (const l of pendingRows) {
|
|
383
|
+
const id = l && l.id;
|
|
384
|
+
if (id && !accounts.has(id)) accounts.set(id, { accountId: id, email: id });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// (accountId, machineId) → active|needs-reauth, from a CURRENTLY-REACHABLE machine only.
|
|
388
|
+
const cellStatus = new Map();
|
|
389
|
+
for (const a of accountRows) {
|
|
390
|
+
const mid = a && a.machineId;
|
|
391
|
+
if (!a || !a.id || !mid || offlineMachineIds.has(mid)) continue;
|
|
392
|
+
cellStatus.set(`${a.id}::${mid}`, a.status === 'needs-reauth' ? 'needs-reauth' : (a.status === 'active' ? 'active' : 'other'));
|
|
393
|
+
}
|
|
394
|
+
// (accountId, machineId) in-progress, correlated on (login.id === accountId, machineId) (FD6 r3 #2).
|
|
395
|
+
const inProgress = new Set();
|
|
396
|
+
for (const l of pendingRows) {
|
|
397
|
+
if (l && l.id && l.machineId) inProgress.add(`${l.id}::${l.machineId}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const machineList = Array.from(machines.values());
|
|
401
|
+
const accountList = Array.from(accounts.values());
|
|
402
|
+
const cells = [];
|
|
403
|
+
for (const acct of accountList) {
|
|
404
|
+
const rowCells = [];
|
|
405
|
+
for (const m of machineList) {
|
|
406
|
+
const key = `${acct.accountId}::${m.machineId}`;
|
|
407
|
+
let state;
|
|
408
|
+
if (m.offline) state = 'offline'; // whole column offline (FD6)
|
|
409
|
+
else if (transient[key] && transient[key].state === 'held') state = 'held';
|
|
410
|
+
else if (transient[key] && transient[key].state === 'cant-resolve') state = 'cant-resolve';
|
|
411
|
+
else if (inProgress.has(key)) state = 'in-progress';
|
|
412
|
+
else if (cellStatus.get(key) === 'active') state = 'active';
|
|
413
|
+
else if (cellStatus.get(key) === 'needs-reauth') state = 'needs-reauth';
|
|
414
|
+
else state = 'empty'; // → "Set up" button
|
|
415
|
+
rowCells.push({ accountId: acct.accountId, machineId: m.machineId, state });
|
|
416
|
+
}
|
|
417
|
+
cells.push({ account: acct, cells: rowCells });
|
|
418
|
+
}
|
|
419
|
+
return { machines: machineList, accounts: accountList, rows: cells, selfMachineId };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const MATRIX_CELL_GLYPH = {
|
|
423
|
+
active: '✓', 'needs-reauth': '⟳', 'in-progress': '◷', offline: '—', held: '⚠', 'cant-resolve': '✗',
|
|
424
|
+
};
|
|
425
|
+
const MATRIX_CELL_WORD = {
|
|
426
|
+
active: 'Active', 'needs-reauth': 'Needs sign-in', 'in-progress': 'Signing in…',
|
|
427
|
+
offline: 'Machine offline', held: 'Didn’t match — re-try', 'cant-resolve': 'Can’t set up', other: 'Set up',
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/** Render the account × machine grid. `target` is replaced. Each empty (reachable) cell
|
|
431
|
+
* gets a "Set up" button carrying its (accountId, machineId) as data-* attributes for the
|
|
432
|
+
* controller's delegated tap handler. Offline columns are disabled; no state is fabricated. */
|
|
433
|
+
export function renderAccountMatrix(doc, target, poolScope, pendingScope, transient = {}) {
|
|
434
|
+
if (!target) return;
|
|
435
|
+
target.replaceChildren();
|
|
436
|
+
const model = buildMatrixModel(poolScope, pendingScope, transient);
|
|
437
|
+
if (model.accounts.length === 0 || model.machines.length === 0) {
|
|
438
|
+
target.appendChild(el(doc, 'div', 'sub-empty', 'No accounts or machines to show yet.'));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const table = el(doc, 'table', 'sub-matrix');
|
|
442
|
+
// Header row: blank corner + one column per machine.
|
|
443
|
+
const thead = doc.createElement('thead');
|
|
444
|
+
const hr = doc.createElement('tr');
|
|
445
|
+
hr.appendChild(el(doc, 'th', 'sub-matrix-corner', ''));
|
|
446
|
+
for (const m of model.machines) {
|
|
447
|
+
const th = el(doc, 'th', m.offline ? 'sub-matrix-mach sub-matrix-off' : 'sub-matrix-mach',
|
|
448
|
+
sanitizeForDisplay(m.nickname, 'label') + (m.offline ? ' (offline)' : ''));
|
|
449
|
+
hr.appendChild(th);
|
|
450
|
+
}
|
|
451
|
+
thead.appendChild(hr);
|
|
452
|
+
table.appendChild(thead);
|
|
453
|
+
|
|
454
|
+
const tbody = doc.createElement('tbody');
|
|
455
|
+
for (const row of model.rows) {
|
|
456
|
+
const tr = doc.createElement('tr');
|
|
457
|
+
tr.appendChild(el(doc, 'th', 'sub-matrix-acct', sanitizeForDisplay(row.account.email, 'label')));
|
|
458
|
+
for (const c of row.cells) {
|
|
459
|
+
const td = el(doc, 'td', `sub-matrix-cell sub-matrix-${c.state}`);
|
|
460
|
+
if (c.state === 'empty' || c.state === 'held' || c.state === 'cant-resolve') {
|
|
461
|
+
// An actionable cell → a "Set up" button (held/cant-resolve let the operator retry).
|
|
462
|
+
const btn = el(doc, 'button', 'sub-matrix-setup', c.state === 'empty' ? 'Set up' : 'Retry');
|
|
463
|
+
btn.setAttribute('data-matrix-setup', '1');
|
|
464
|
+
btn.setAttribute('data-account-id', sanitizeForDisplay(c.accountId, 'label'));
|
|
465
|
+
btn.setAttribute('data-machine-id', sanitizeForDisplay(c.machineId, 'label'));
|
|
466
|
+
if (c.state !== 'empty') {
|
|
467
|
+
td.appendChild(el(doc, 'div', 'sub-matrix-glyph', `${MATRIX_CELL_GLYPH[c.state]} ${MATRIX_CELL_WORD[c.state]}`));
|
|
468
|
+
}
|
|
469
|
+
td.appendChild(btn);
|
|
470
|
+
} else {
|
|
471
|
+
const word = c.state === 'offline' ? 'unknown' : MATRIX_CELL_WORD[c.state];
|
|
472
|
+
const glyph = MATRIX_CELL_GLYPH[c.state] || '';
|
|
473
|
+
td.appendChild(el(doc, 'span', 'sub-matrix-glyph', `${glyph} ${word}`.trim()));
|
|
474
|
+
}
|
|
475
|
+
tr.appendChild(td);
|
|
476
|
+
}
|
|
477
|
+
tbody.appendChild(tr);
|
|
478
|
+
}
|
|
479
|
+
table.appendChild(tbody);
|
|
480
|
+
target.appendChild(table);
|
|
481
|
+
}
|
|
482
|
+
|
|
314
483
|
export function renderDisabled(doc, els) {
|
|
315
484
|
if (els && els.accounts) {
|
|
316
485
|
els.accounts.replaceChildren(
|
|
@@ -327,8 +496,13 @@ const URLS = {
|
|
|
327
496
|
// operator's single dashboard (WS5.2 seam #3) — without it the device-code link never appears here.
|
|
328
497
|
pending: '/subscription-pool/pending-logins?scope=pool',
|
|
329
498
|
inUse: '/subscription-pool/in-use',
|
|
499
|
+
submitCode: '/subscription-pool/follow-me/submit-code', // POST — paste-back the sign-in code (ws52-code-paste-back)
|
|
330
500
|
scan: '/subscription-pool/follow-me/scan', // POST — follow-me consent offers (one-tap card)
|
|
331
501
|
issue: '/mandate/issue-for-machine', // POST (PIN-gated) — Approve issues the mandate
|
|
502
|
+
// account-machine-matrix: pool-scope accounts feed the grid (the SAME read the accounts list
|
|
503
|
+
// uses, with peers merged); start-cell is the PIN-gated "Set up" orchestrator over the chain.
|
|
504
|
+
accountsPool: '/subscription-pool?scope=pool',
|
|
505
|
+
startCell: '/subscription-pool/matrix/start-cell', // POST (PIN-gated) — start a cell's sign-in
|
|
332
506
|
};
|
|
333
507
|
|
|
334
508
|
export function createController(opts) {
|
|
@@ -342,7 +516,10 @@ export function createController(opts) {
|
|
|
342
516
|
cancel = (id) => clearTimeout(id),
|
|
343
517
|
} = opts;
|
|
344
518
|
|
|
345
|
-
|
|
519
|
+
// matrixTransient: client-side last-attempt state per `${accountId}::${machineId}` cell (FD6 —
|
|
520
|
+
// held / cant-resolve are known only to the client from the response it just got).
|
|
521
|
+
const state = { timerId: null, active: false, inFlight: null, offers: [], approveWired: false,
|
|
522
|
+
matrixWired: false, matrixTransient: {}, lastPoolBody: null, lastPendingBody: null };
|
|
346
523
|
|
|
347
524
|
async function fetchJson(url, controller) {
|
|
348
525
|
const resp = await fetchImpl(url, { signal: controller.signal });
|
|
@@ -369,15 +546,17 @@ export function createController(opts) {
|
|
|
369
546
|
if (state.inFlight) { try { state.inFlight.abort(); } catch { /* superseded */ } }
|
|
370
547
|
const controller = typeof AbortController !== 'undefined' ? new AbortController() : { signal: undefined, abort() {} };
|
|
371
548
|
state.inFlight = controller;
|
|
372
|
-
let accountsBody, pendingBody, inUseBody, scanBody;
|
|
549
|
+
let accountsBody, pendingBody, inUseBody, scanBody, poolBody;
|
|
373
550
|
try {
|
|
374
551
|
// in-use AND the follow-me scan are best-effort — their failure must not blank the
|
|
375
552
|
// accounts list, so each is caught independently (in-use → "unknown"; scan → no card).
|
|
376
|
-
|
|
553
|
+
// poolBody (scope=pool) feeds the account×machine matrix; best-effort (matrix is hidden if absent).
|
|
554
|
+
[accountsBody, pendingBody, inUseBody, scanBody, poolBody] = await Promise.all([
|
|
377
555
|
fetchJson(URLS.accounts, controller),
|
|
378
556
|
fetchJson(URLS.pending, controller),
|
|
379
557
|
fetchJson(URLS.inUse, controller).catch(() => null),
|
|
380
558
|
postJson(URLS.scan, {}, controller).then((r) => (r.ok ? r.json : null)).catch(() => null),
|
|
559
|
+
fetchJson(URLS.accountsPool, controller).catch(() => null),
|
|
381
560
|
]);
|
|
382
561
|
} catch {
|
|
383
562
|
if (controller.signal && controller.signal.aborted) return;
|
|
@@ -394,16 +573,25 @@ export function createController(opts) {
|
|
|
394
573
|
return;
|
|
395
574
|
}
|
|
396
575
|
state.offers = scanBody && Array.isArray(scanBody.offered) ? scanBody.offered : [];
|
|
397
|
-
render(accountsBody, pendingBody, inUseBody);
|
|
576
|
+
render(accountsBody, pendingBody, inUseBody, poolBody);
|
|
398
577
|
reschedule();
|
|
399
578
|
}
|
|
400
579
|
|
|
401
|
-
function render(accountsBody, pendingBody, inUseBody) {
|
|
580
|
+
function render(accountsBody, pendingBody, inUseBody, poolBody) {
|
|
402
581
|
const accounts = accountsBody && Array.isArray(accountsBody.accounts) ? accountsBody.accounts : [];
|
|
403
582
|
const logins = pendingBody && Array.isArray(pendingBody.logins) ? pendingBody.logins : [];
|
|
404
583
|
const inUseAccountId = inUseBody && inUseBody.activeAccountId ? inUseBody.activeAccountId : null;
|
|
405
584
|
renderAccounts(doc, els.accounts, accounts, now(), inUseAccountId);
|
|
406
585
|
renderPendingLogins(doc, els.pending, logins, now());
|
|
586
|
+
wireCodeSubmit();
|
|
587
|
+
// The account × machine matrix (account-machine-matrix) — built from the pool-scope read +
|
|
588
|
+
// the (already pool-scope) pending logins. Hidden when the pool-scope read is unavailable.
|
|
589
|
+
if (els.matrix) {
|
|
590
|
+
state.lastPoolBody = poolBody || null;
|
|
591
|
+
state.lastPendingBody = pendingBody || null;
|
|
592
|
+
renderAccountMatrix(doc, els.matrix, poolBody, pendingBody, state.matrixTransient);
|
|
593
|
+
wireMatrixSetup();
|
|
594
|
+
}
|
|
407
595
|
// The one-tap follow-me Approve card(s) — rendered into els.followMe from the scan offers
|
|
408
596
|
// (ws52-operator-tap-not-text Part A). Silent when there are none. The Approve click is wired
|
|
409
597
|
// once (delegated) so re-renders never stack listeners.
|
|
@@ -456,6 +644,210 @@ export function createController(opts) {
|
|
|
456
644
|
s.textContent = text;
|
|
457
645
|
}
|
|
458
646
|
|
|
647
|
+
// Delegated, wired ONCE: a "Submit code" tap on a pending-login row reads the pasted
|
|
648
|
+
// sign-in code and POSTs it (with the login's id + machineId) to the fronting relay,
|
|
649
|
+
// which carries it to the machine doing the login (off-chat). The code is never stored
|
|
650
|
+
// client-side beyond the input; it's cleared on success. (ws52-code-paste-back)
|
|
651
|
+
function wireCodeSubmit() {
|
|
652
|
+
if (state.codeSubmitWired || !els.pending) return;
|
|
653
|
+
state.codeSubmitWired = true;
|
|
654
|
+
els.pending.addEventListener('click', (ev) => {
|
|
655
|
+
const btn = ev.target && ev.target.closest ? ev.target.closest('[data-submit-code]') : null;
|
|
656
|
+
if (!btn || !els.pending.contains(btn)) return;
|
|
657
|
+
const row = btn.closest('.sub-pending');
|
|
658
|
+
if (!row) return;
|
|
659
|
+
const input = row.querySelector('.sub-pending-code-input');
|
|
660
|
+
const code = input ? input.value.trim() : '';
|
|
661
|
+
const id = row.getAttribute('data-login-id');
|
|
662
|
+
const machineId = row.getAttribute('data-machine-id') || undefined;
|
|
663
|
+
if (!code) { setRowStatus(row, 'Paste the code the sign-in page gave you, then tap Submit.'); return; }
|
|
664
|
+
if (!id) { setRowStatus(row, 'Couldn’t identify this login — please refresh.'); return; }
|
|
665
|
+
setRowStatus(row, 'Sending your code…');
|
|
666
|
+
btn.setAttribute('disabled', '1');
|
|
667
|
+
void (async () => {
|
|
668
|
+
try {
|
|
669
|
+
const r = await postJson(URLS.submitCode, { machineId, id, code });
|
|
670
|
+
if (r.ok && r.json && r.json.outcome === 'validated') {
|
|
671
|
+
setRowStatus(row, 'Done — this machine is set up with the account.');
|
|
672
|
+
if (input) input.value = '';
|
|
673
|
+
} else if (r.ok && r.json && r.json.outcome === 'submitted') {
|
|
674
|
+
setRowStatus(row, 'Code sent — finishing sign-in…');
|
|
675
|
+
if (input) input.value = '';
|
|
676
|
+
} else if (r.ok && r.json && r.json.outcome === 'held') {
|
|
677
|
+
setRowStatus(row, 'Signed in, but the account didn’t match what was approved — check with the operator.');
|
|
678
|
+
btn.removeAttribute('disabled');
|
|
679
|
+
} else {
|
|
680
|
+
const msg = (r.json && (r.json.error || r.json.reason)) ? (r.json.error || r.json.reason) : `failed (${r.status})`;
|
|
681
|
+
setRowStatus(row, `Couldn’t submit the code: ${msg}`);
|
|
682
|
+
btn.removeAttribute('disabled');
|
|
683
|
+
}
|
|
684
|
+
} catch (e) {
|
|
685
|
+
setRowStatus(row, 'Couldn’t reach the server — try again.');
|
|
686
|
+
btn.removeAttribute('disabled');
|
|
687
|
+
}
|
|
688
|
+
})();
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function setRowStatus(row, text) {
|
|
693
|
+
let s = row.querySelector('.sub-pending-status');
|
|
694
|
+
if (!s) { s = el(doc, 'div', 'sub-pending-status', ''); row.appendChild(s); }
|
|
695
|
+
s.textContent = text;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Delegated, wired ONCE: a "Set up" tap on a matrix cell expands an inline PIN input +
|
|
699
|
+
// Confirm. Confirm POSTs the PIN-gated start-cell; on success the cell shows the auth link
|
|
700
|
+
// (operator opens it) + a code input + Submit, which POSTs the SHIPPED submit-code relay.
|
|
701
|
+
// The PIN + code are memory-only (read from the input on tap, cleared after use; never cached).
|
|
702
|
+
function wireMatrixSetup() {
|
|
703
|
+
if (state.matrixWired || !els.matrix) return;
|
|
704
|
+
state.matrixWired = true;
|
|
705
|
+
els.matrix.addEventListener('click', (ev) => {
|
|
706
|
+
const t = ev.target;
|
|
707
|
+
if (!t || typeof t.closest !== 'function') return;
|
|
708
|
+
const setupBtn = t.closest('[data-matrix-setup]');
|
|
709
|
+
if (setupBtn && els.matrix.contains(setupBtn)) { onSetupTap(setupBtn); return; }
|
|
710
|
+
const confirmBtn = t.closest('[data-matrix-confirm]');
|
|
711
|
+
if (confirmBtn && els.matrix.contains(confirmBtn)) { onConfirmTap(confirmBtn); return; }
|
|
712
|
+
const codeBtn = t.closest('[data-matrix-code-submit]');
|
|
713
|
+
if (codeBtn && els.matrix.contains(codeBtn)) { onCodeTap(codeBtn); return; }
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function matrixCellOf(node) {
|
|
718
|
+
return node && typeof node.closest === 'function' ? node.closest('.sub-matrix-cell') : null;
|
|
719
|
+
}
|
|
720
|
+
function setCellStatus(cell, text) {
|
|
721
|
+
let s = cell.querySelector('.sub-matrix-status');
|
|
722
|
+
if (!s) { s = el(doc, 'div', 'sub-matrix-status', ''); cell.appendChild(s); }
|
|
723
|
+
s.textContent = text;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Expand the cell into a PIN input + Confirm (replacing the "Set up"/"Retry" button).
|
|
727
|
+
function onSetupTap(btn) {
|
|
728
|
+
const cell = matrixCellOf(btn);
|
|
729
|
+
if (!cell) return;
|
|
730
|
+
const accountId = btn.getAttribute('data-account-id');
|
|
731
|
+
const machineId = btn.getAttribute('data-machine-id');
|
|
732
|
+
if (!accountId || !machineId) return;
|
|
733
|
+
btn.remove();
|
|
734
|
+
const pin = doc.createElement('input');
|
|
735
|
+
pin.setAttribute('type', 'password');
|
|
736
|
+
pin.setAttribute('class', 'sub-matrix-pin');
|
|
737
|
+
pin.setAttribute('placeholder', 'Your PIN');
|
|
738
|
+
pin.setAttribute('autocomplete', 'off');
|
|
739
|
+
cell.appendChild(pin);
|
|
740
|
+
const confirm = el(doc, 'button', 'sub-matrix-confirm', 'Confirm');
|
|
741
|
+
confirm.setAttribute('data-matrix-confirm', '1');
|
|
742
|
+
confirm.setAttribute('data-account-id', accountId);
|
|
743
|
+
confirm.setAttribute('data-machine-id', machineId);
|
|
744
|
+
cell.appendChild(confirm);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Confirm → POST the PIN-gated start-cell; render the auth link + code input on success.
|
|
748
|
+
function onConfirmTap(btn) {
|
|
749
|
+
const cell = matrixCellOf(btn);
|
|
750
|
+
if (!cell) return;
|
|
751
|
+
const accountId = btn.getAttribute('data-account-id');
|
|
752
|
+
const machineId = btn.getAttribute('data-machine-id');
|
|
753
|
+
const pinInput = cell.querySelector('.sub-matrix-pin');
|
|
754
|
+
const pin = pinInput ? pinInput.value.trim() : '';
|
|
755
|
+
if (!accountId || !machineId) { setCellStatus(cell, 'Couldn’t prepare this — please refresh.'); return; }
|
|
756
|
+
if (!pin) { setCellStatus(cell, 'Enter your PIN to set this up.'); return; }
|
|
757
|
+
setCellStatus(cell, 'Starting sign-in…');
|
|
758
|
+
btn.setAttribute('disabled', '1');
|
|
759
|
+
void (async () => {
|
|
760
|
+
try {
|
|
761
|
+
const r = await postJson(URLS.startCell, { accountId, machineId, pin });
|
|
762
|
+
if (pinInput) pinInput.value = ''; // PIN is memory-only — clear it immediately
|
|
763
|
+
if (r.ok && r.json && r.json.verificationUrl) {
|
|
764
|
+
renderCellSignIn(cell, accountId, machineId, r.json.verificationUrl, r.json.loginId || accountId);
|
|
765
|
+
} else if (r.status === 409) {
|
|
766
|
+
state.matrixTransient[`${accountId}::${machineId}`] = { state: 'cant-resolve' };
|
|
767
|
+
setCellStatus(cell, 'Can’t set this account up here — its details couldn’t be resolved.');
|
|
768
|
+
btn.removeAttribute('disabled');
|
|
769
|
+
} else {
|
|
770
|
+
const msg = (r.json && (r.json.error || r.json.reason)) ? (r.json.error || r.json.reason) : `failed (${r.status})`;
|
|
771
|
+
setCellStatus(cell, `Couldn’t start: ${msg}`);
|
|
772
|
+
btn.removeAttribute('disabled');
|
|
773
|
+
}
|
|
774
|
+
} catch (e) {
|
|
775
|
+
setCellStatus(cell, 'Couldn’t reach the server — try again.');
|
|
776
|
+
btn.removeAttribute('disabled');
|
|
777
|
+
}
|
|
778
|
+
})();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// After start-cell: show the sign-in link (operator opens it) + a code input + Submit.
|
|
782
|
+
function renderCellSignIn(cell, accountId, machineId, verificationUrl, loginId) {
|
|
783
|
+
// Clear the PIN/Confirm UI, keep the status line.
|
|
784
|
+
const pin = cell.querySelector('.sub-matrix-pin'); if (pin) pin.remove();
|
|
785
|
+
const confirm = cell.querySelector('[data-matrix-confirm]'); if (confirm) confirm.remove();
|
|
786
|
+
setCellStatus(cell, 'Open the sign-in link, then paste the code below.');
|
|
787
|
+
const href = trustedLoginUrl(verificationUrl);
|
|
788
|
+
if (href) {
|
|
789
|
+
const a = doc.createElement('a');
|
|
790
|
+
a.setAttribute('href', href);
|
|
791
|
+
a.setAttribute('target', '_blank');
|
|
792
|
+
a.setAttribute('rel', 'noopener noreferrer');
|
|
793
|
+
a.setAttribute('class', 'sub-matrix-signin');
|
|
794
|
+
a.textContent = 'Sign in';
|
|
795
|
+
cell.appendChild(a);
|
|
796
|
+
} else {
|
|
797
|
+
cell.appendChild(el(doc, 'div', 'sub-matrix-url', sanitizeForDisplay(verificationUrl, 'url')));
|
|
798
|
+
}
|
|
799
|
+
const code = doc.createElement('input');
|
|
800
|
+
code.setAttribute('type', 'text');
|
|
801
|
+
code.setAttribute('class', 'sub-matrix-code-input');
|
|
802
|
+
code.setAttribute('placeholder', 'Paste your sign-in code');
|
|
803
|
+
code.setAttribute('autocomplete', 'off');
|
|
804
|
+
cell.appendChild(code);
|
|
805
|
+
const submit = el(doc, 'button', 'sub-matrix-code-submit', 'Submit');
|
|
806
|
+
submit.setAttribute('data-matrix-code-submit', '1');
|
|
807
|
+
submit.setAttribute('data-account-id', accountId);
|
|
808
|
+
submit.setAttribute('data-machine-id', machineId);
|
|
809
|
+
submit.setAttribute('data-login-id', loginId);
|
|
810
|
+
cell.appendChild(submit);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Submit the pasted code via the SHIPPED submit-code relay (unchanged contract).
|
|
814
|
+
function onCodeTap(btn) {
|
|
815
|
+
const cell = matrixCellOf(btn);
|
|
816
|
+
if (!cell) return;
|
|
817
|
+
const accountId = btn.getAttribute('data-account-id');
|
|
818
|
+
const machineId = btn.getAttribute('data-machine-id');
|
|
819
|
+
const loginId = btn.getAttribute('data-login-id') || accountId;
|
|
820
|
+
const input = cell.querySelector('.sub-matrix-code-input');
|
|
821
|
+
const code = input ? input.value.trim() : '';
|
|
822
|
+
if (!code) { setCellStatus(cell, 'Paste the code the sign-in page gave you, then tap Submit.'); return; }
|
|
823
|
+
setCellStatus(cell, 'Sending your code…');
|
|
824
|
+
btn.setAttribute('disabled', '1');
|
|
825
|
+
void (async () => {
|
|
826
|
+
try {
|
|
827
|
+
const r = await postJson(URLS.submitCode, { machineId, id: loginId, code });
|
|
828
|
+
if (input) input.value = ''; // code is memory-only — cleared after use
|
|
829
|
+
const key = `${accountId}::${machineId}`;
|
|
830
|
+
if (r.ok && r.json && r.json.outcome === 'validated') {
|
|
831
|
+
delete state.matrixTransient[key];
|
|
832
|
+
setCellStatus(cell, 'Done — this machine is set up with the account.');
|
|
833
|
+
} else if (r.ok && r.json && r.json.outcome === 'submitted') {
|
|
834
|
+
setCellStatus(cell, 'Code sent — finishing sign-in…');
|
|
835
|
+
} else if (r.ok && r.json && r.json.outcome === 'held') {
|
|
836
|
+
state.matrixTransient[key] = { state: 'held' };
|
|
837
|
+
setCellStatus(cell, 'Signed in, but the account didn’t match what was approved — re-try with the right account.');
|
|
838
|
+
btn.removeAttribute('disabled');
|
|
839
|
+
} else {
|
|
840
|
+
const msg = (r.json && (r.json.error || r.json.reason)) ? (r.json.error || r.json.reason) : `failed (${r.status})`;
|
|
841
|
+
setCellStatus(cell, `Couldn’t submit the code: ${msg}`);
|
|
842
|
+
btn.removeAttribute('disabled');
|
|
843
|
+
}
|
|
844
|
+
} catch (e) {
|
|
845
|
+
setCellStatus(cell, 'Couldn’t reach the server — try again.');
|
|
846
|
+
btn.removeAttribute('disabled');
|
|
847
|
+
}
|
|
848
|
+
})();
|
|
849
|
+
}
|
|
850
|
+
|
|
459
851
|
function reschedule() {
|
|
460
852
|
if (!state.active) return;
|
|
461
853
|
if (state.timerId != null) cancel(state.timerId);
|
|
@@ -478,5 +870,6 @@ if (typeof window !== 'undefined') {
|
|
|
478
870
|
window.Subscriptions = {
|
|
479
871
|
createController, sanitizeForDisplay, renderAccounts, renderPendingLogins,
|
|
480
872
|
renderFollowMeOffers, renderFollowMeApproveCard, buildFollowMeIssuePayload,
|
|
873
|
+
renderAccountMatrix, buildMatrixModel,
|
|
481
874
|
};
|
|
482
875
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAwBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAkH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAy4CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAwBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAkH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAy4CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAi8etE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
|
package/dist/commands/server.js
CHANGED
|
@@ -9874,7 +9874,7 @@ export async function startServer(options) {
|
|
|
9874
9874
|
// a 2nd account never clobbers the 1st.
|
|
9875
9875
|
const { PendingLoginStore } = await import('../core/PendingLoginStore.js');
|
|
9876
9876
|
const { EnrollmentWizard } = await import('../core/EnrollmentWizard.js');
|
|
9877
|
-
const { FrameworkLoginDriver } = await import('../core/FrameworkLoginDriver.js');
|
|
9877
|
+
const { FrameworkLoginDriver, enrollPaneSessionName } = await import('../core/FrameworkLoginDriver.js');
|
|
9878
9878
|
const DEFAULT_ENROLL_LOGIN_COMMANDS = {
|
|
9879
9879
|
'claude-code': 'claude auth login',
|
|
9880
9880
|
'codex-cli': 'codex login',
|
|
@@ -9933,8 +9933,7 @@ export async function startServer(options) {
|
|
|
9933
9933
|
const cmd = configHome
|
|
9934
9934
|
? `env CLAUDE_CONFIG_DIR=${JSON.stringify(configHome)} ${baseCmd}`
|
|
9935
9935
|
: baseCmd;
|
|
9936
|
-
const
|
|
9937
|
-
const session = `instar-enroll-${framework}-${slug}`;
|
|
9936
|
+
const session = enrollPaneSessionName(framework, configHome);
|
|
9938
9937
|
try {
|
|
9939
9938
|
execFileSync(tmuxPath, ['kill-session', '-t', `=${session}`], { stdio: 'ignore' });
|
|
9940
9939
|
}
|