@gotcos/glasses-server 7.0.29 → 7.0.31
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/CHANGELOG.md +76 -0
- package/docs/web-key-verifier-contract.md +33 -7
- package/package.json +1 -1
- package/server/lib/provider-failure-text.ts +4 -1
- package/server/lib/web-key-authority.ts +1 -1
- package/server/lib/web-key-registry.ts +1 -1
- package/server/lib/web-key-signer-tls.ts +17 -7
- package/server/web-control/.manifest.json +15 -15
- package/server/web-control/app.css +6 -4
- package/server/web-control/app.js +309 -17
- package/server/web-control/control.css +32 -2
- package/server/web-control/index.html +2 -0
- package/server/web-control/panes/cos-chat.css +26 -1
- package/server/web-control/panes/cos-chat.js +9 -3
- package/server/web-control/panes/meetings.css +4 -0
- package/server/web-control/panes/meetings.js +25 -6
- package/server/web-control/panes/memories.js +2 -2
- package/server/web-control/panes/messages.js +7 -2
- package/server/web-control/panes/settings.js +1 -1
- package/server/web-control/panes/speakers.js +28 -3
- package/server/web-control/panes/today-actions.js +2 -2
- package/server/web-control/panes/work.js +1 -1
- package/server/web-control/shell.js +2 -1
- package/server/web-signer/.manifest.json +2 -2
- package/server/web-signer/signer-proof.js +18 -3
- package/server/web-signer/signer.js +4 -2
- package/shared/web-turn-envelope.ts +33 -8
|
@@ -525,10 +525,12 @@
|
|
|
525
525
|
for (var w = 0; w < 7; w++) heads += '<span class="mt-wd" aria-hidden="true">' + esc(new Date(2023, 0, 1 + (ws + w) % 7).toLocaleDateString(undefined, { weekday: 'narrow' })) + '</span>';
|
|
526
526
|
var cells = '', n = 0;
|
|
527
527
|
for (var i = 0; i < lead; i++) { cells += '<span class="mt-blank"></span>'; n++; }
|
|
528
|
+
// 7.0.30 item 4: today is marked (a ring, aria-current) and can be picked even with no meetings.
|
|
529
|
+
var todayKey = dayKey(new Date());
|
|
528
530
|
for (var day = 1; day <= dim; day++) {
|
|
529
|
-
var key = S.month + '-' + pad2(day), c = counts[key] || 0, on = S.day === key;
|
|
530
|
-
cells += '<button type="button" class="mt-day" data-mt="day" data-day="' + key + '" aria-pressed="' + on + '"' + (c === 0 ? ' disabled' : '') +
|
|
531
|
-
' aria-label="' + esc(key + (c === 0 ? ', no meetings' : ', ' + c + ' meetings')) + '"><span class="mt-dnum">' + day + '</span><span class="mt-dot' + (c > 0 ? ' on' : '') + '"></span></button>';
|
|
531
|
+
var key = S.month + '-' + pad2(day), c = counts[key] || 0, on = S.day === key, isToday = key === todayKey;
|
|
532
|
+
cells += '<button type="button" class="mt-day' + (isToday ? ' mt-today' : '') + '" data-mt="day" data-day="' + key + '" aria-pressed="' + on + '"' + (isToday ? ' aria-current="date"' : '') + (c === 0 && !isToday ? ' disabled' : '') +
|
|
533
|
+
' aria-label="' + esc(key + (isToday ? ', today' : '') + (c === 0 ? ', no meetings' : ', ' + c + ' meetings')) + '"><span class="mt-dnum">' + day + '</span><span class="mt-dot' + (c > 0 ? ' on' : '') + '"></span></button>';
|
|
532
534
|
n++;
|
|
533
535
|
}
|
|
534
536
|
while (n % 7 !== 0) { cells += '<span class="mt-blank"></span>'; n++; }
|
|
@@ -594,6 +596,20 @@
|
|
|
594
596
|
(m.sessionId ? pillsHtml(m, isNew(m.sessionId)) : '') + '</span><span class="chev"></span></button>';
|
|
595
597
|
}
|
|
596
598
|
function emptyBox(text) { return '<div class="cx-list"><p class="cx-empty mt-empty">' + esc(text) + '</p></div>'; }
|
|
599
|
+
// 7.0.30 item 4: today with no meetings says so in plain words, with one tap to the latest earlier day that has some
|
|
600
|
+
// (this month), or to last month when this month has none. The calendar stays as it is.
|
|
601
|
+
function latestDayBefore(key) {
|
|
602
|
+
var best = '';
|
|
603
|
+
(S.days || []).forEach(function (d) { if (num(d.count) > 0 && d.date < key && d.date > best) best = d.date; });
|
|
604
|
+
return best;
|
|
605
|
+
}
|
|
606
|
+
function emptyToday() {
|
|
607
|
+
var last = latestDayBefore(dayKey(new Date()));
|
|
608
|
+
var jump = last
|
|
609
|
+
? '<button type="button" class="cx-btn quiet mt-jump" data-mt="jump-day" data-day="' + esc(last) + '">Go to ' + esc(W.plainDay ? W.plainDay(last) : last) + '</button>'
|
|
610
|
+
: '<button type="button" class="cx-btn quiet mt-jump" data-mt="month" data-delta="-1">Go to last month</button>';
|
|
611
|
+
return '<div class="cx-list"><p class="cx-empty mt-empty">No meetings today.</p><p class="mt-jump-row">' + jump + '</p></div>';
|
|
612
|
+
}
|
|
597
613
|
function paintList() {
|
|
598
614
|
var el = $('mt-list'); if (!el) return;
|
|
599
615
|
var html;
|
|
@@ -611,6 +627,7 @@
|
|
|
611
627
|
var rows = visibleRows();
|
|
612
628
|
html = rows.length ? '<div class="cx-list">' + rows.map(function (m) { return rowHtml(m, false); }).join('') + '</div>' +
|
|
613
629
|
(monthCapped() ? '<p class="mt-capnote">Showing the newest ' + S.rows.length + ' of ' + monthTotal() + ' in ' + esc(monthTitle(S.month)) + '. Pick a day in the calendar to see all of its meetings.</p>' : '')
|
|
630
|
+
: S.day && S.day === dayKey(new Date()) ? emptyToday()
|
|
614
631
|
: emptyBox(S.day ? 'No meetings stored on ' + S.day + '. Pick another day, or show the whole month.' : 'No meetings stored in ' + monthTitle(S.month) + '.');
|
|
615
632
|
}
|
|
616
633
|
if (S.listHtml === html) return;
|
|
@@ -643,9 +660,10 @@
|
|
|
643
660
|
S.days = days;
|
|
644
661
|
var kept = S.dayRows; S.dayRows = {};
|
|
645
662
|
if (S.day && kept[S.day]) S.dayRows[S.day] = kept[S.day];
|
|
663
|
+
// 7.0.30 item 4 (Miles 10:15: "Meetings should SHOW the current day"): the current month opens on today, with
|
|
664
|
+
// meetings or without (the list then says so and offers the latest day that has some).
|
|
646
665
|
if (!S.autoDay && S.month === monthKey(new Date())) {
|
|
647
|
-
|
|
648
|
-
if (days.some(function (x) { return x.date === today && num(x.count) > 0; })) S.day = today;
|
|
666
|
+
S.day = dayKey(new Date());
|
|
649
667
|
S.autoDay = true;
|
|
650
668
|
}
|
|
651
669
|
S.error = ''; S.loading = false;
|
|
@@ -2089,7 +2107,7 @@
|
|
|
2089
2107
|
var hit = lists.reduce(function (all, l) { return all.concat(l); }, []).filter(function (m) { return m.sessionId === f.sid || (m.g2SessionIds || []).indexOf(f.sid) >= 0; })[0];
|
|
2090
2108
|
if (hit && S.view === 'live' && S.liveSid === f.sid) {
|
|
2091
2109
|
S.rowsStale = true;
|
|
2092
|
-
|
|
2110
|
+
W.replace('#/meetings/' + [hit.domain, hit.month, hit.filename].map(encodeURIComponent).join('/'));
|
|
2093
2111
|
} else if (!hit && f.endedAt && now - (f.retryAt || f.endedAt) >= FINAL_DEADLINE_MS + (f.start ? Math.max(0, f.endedAt - f.start) / 12 : 0)) {
|
|
2094
2112
|
f.notFound = true; paintLiveHead();
|
|
2095
2113
|
}
|
|
@@ -2667,6 +2685,7 @@
|
|
|
2667
2685
|
case 'month': shiftMonth(Number(t.getAttribute('data-delta')) || 0); return;
|
|
2668
2686
|
case 'day': { var d = t.getAttribute('data-day'); S.day = S.day === d ? null : d; S.autoDay = true; paintBrowse(); ensureDay(); return; }
|
|
2669
2687
|
case 'all-month': S.day = null; S.autoDay = true; paintBrowse(); return;
|
|
2688
|
+
case 'jump-day': { S.day = t.getAttribute('data-day'); S.autoDay = true; paintBrowse(); ensureDay(); return; }
|
|
2670
2689
|
case 'clear': {
|
|
2671
2690
|
S.q = ''; S.hits = null; S.ranFor = ''; S.searching = false; S.searchError = ''; clearTimeout(S.qTimer);
|
|
2672
2691
|
var q = $('mt-q'); if (q) { q.value = ''; q.focus(); }
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
function readerArgs(r) { return r.pane === 'memories' && !tabRoute(r.args) ? r.args : []; }
|
|
106
106
|
function showTab(v) {
|
|
107
107
|
var h = '#/memories/t/' + encodeURIComponent(v);
|
|
108
|
-
if (location.hash === h) paint(); else
|
|
108
|
+
if (location.hash === h) paint(); else W.replace(h);
|
|
109
109
|
}
|
|
110
110
|
function isWide() { return !!(window.matchMedia && window.matchMedia('(min-width: 760px)').matches); }
|
|
111
111
|
function el(id) { return document.getElementById(id); }
|
|
@@ -2262,7 +2262,7 @@
|
|
|
2262
2262
|
// #/memories/view/<view> (older links) and a bare #/memories become the tab's own address; an unknown tab, the stored one.
|
|
2263
2263
|
if (args[0] === 'view' || !args.length || (args[0] === 't' && !knownView(args[1]))) {
|
|
2264
2264
|
if (args[0] === 'view' && knownView(args[1])) { S.view = args[1]; persist(); }
|
|
2265
|
-
|
|
2265
|
+
W.replace('#/memories/t/' + encodeURIComponent(S.view)); return;
|
|
2266
2266
|
}
|
|
2267
2267
|
if (tabRoute(args) && S.view !== args[1]) { S.view = args[1]; persist(); }
|
|
2268
2268
|
if (args[0] === 'k') { S.view = 'knowledge'; if (KTABS.indexOf(args[1]) >= 0) { S.ktab = args[1]; S.ktabChosen = true; } persist(); }
|
|
@@ -469,9 +469,14 @@
|
|
|
469
469
|
}
|
|
470
470
|
function turnCards(t, msgKey) {
|
|
471
471
|
var a = atts(t);
|
|
472
|
-
|
|
472
|
+
// 7.0.30, Miles's pick A: who said what by side, in the COS chat's own shapes. His words on the right in the chat's
|
|
473
|
+
// gold bubble with "You" under it; COS on the left beside its mark, on the page. A screen reader hears "You said"
|
|
474
|
+
// and "COS said" first, since the side and the mark are visual only.
|
|
475
|
+
return '<div class="cx-said you"><span class="sr-only">You said</span><div class="cx-said-bubble cx-card-body" data-hl>' + esc(String(t.query || '').trim() || '(empty query)').replace(/\n/g, '<br>') + '</div>' +
|
|
476
|
+
'<span class="cx-said-who" aria-hidden="true">You</span></div>' +
|
|
473
477
|
stripHtml(a.filter(isUserAtt), true, msgKey) +
|
|
474
|
-
'<div class="cx-
|
|
478
|
+
'<div class="cx-said cos"><span class="cc-avatar" aria-hidden="true"><svg class="icon"><use href="#i-mark"/></svg></span><div class="cx-said-text"><span class="sr-only">COS said</span>' +
|
|
479
|
+
'<div class="cx-card-body md" data-hl>' + W.md(W.structureLensAnswer(String(t.text || ''))) + '</div></div></div>' +
|
|
475
480
|
stripHtml(a.filter(function (x) { return !isUserAtt(x); }), false, msgKey);
|
|
476
481
|
}
|
|
477
482
|
function copyTurnText(t) {
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
return '<section class="cx-set-cat"><h2 class="cx-col-title">This browser</h2>' +
|
|
124
124
|
group('Appearance', '', themeBtn('system', 'Match this device') + themeBtn('light', 'Light') + themeBtn('dark', 'Dark')) +
|
|
125
125
|
group('Sign-in', '',
|
|
126
|
-
'<button type="button" class="cx-set-row" id="device-code-btn"><span class="cx-set-text"><span class="cx-set-name">Use COS on another phone or browser</span><span class="cx-set-desc">A QR for your
|
|
126
|
+
'<button type="button" class="cx-set-row" id="device-code-btn"><span class="cx-set-text"><span class="cx-set-name">Use COS on another phone or browser</span><span class="cx-set-desc">A QR for your phone’s camera, and a code to type. This is not glasses pairing; pair glasses from COS Control on your Mac.</span></span><span class="chev"></span></button>' +
|
|
127
127
|
'<div id="device-code-panel" class="device-code hidden" aria-live="polite"></div>' +
|
|
128
128
|
'<button type="button" class="cx-set-row" id="copy-link"><span class="cx-set-text"><span class="cx-set-name">Copy link for your phone</span><span class="cx-set-desc" id="tailnet-url">' + esc(W.state.tailnetUrl || 'Checking Tailscale') + '</span></span></button>' +
|
|
129
129
|
'<button type="button" class="cx-set-row" id="sign-out"><span class="cx-set-text"><span class="cx-set-name">Sign out of this browser</span></span></button>' +
|
|
@@ -2363,8 +2363,11 @@
|
|
|
2363
2363
|
var acts = '';
|
|
2364
2364
|
if (rv && rv.recordId) acts += '<button type="button" class="cx-btn quiet" data-sp="open-meeting" title="Open this meeting’s transcript, summary and files">' + icon('meetings') + 'Open meeting</button>';
|
|
2365
2365
|
if (R.content && R.content.clipboardSummary) {
|
|
2366
|
-
|
|
2367
|
-
|
|
2366
|
+
// 7.0.30 item 3 (Anand: Summary and Full did not say they copy): the buttons say Copy, and the one tapped says
|
|
2367
|
+
// Copied for COPIED_MS (spCopied), while a status line the screen reader hears says what was copied.
|
|
2368
|
+
var cp = R.copied && Date.now() - R.copied.at < COPIED_MS ? R.copied.which : '';
|
|
2369
|
+
acts += '<button type="button" class="cx-btn quiet" id="sp-copy-summary" data-sp="copy-summary" title="Copy the meeting without the transcript">' + (cp === 'summary' ? 'Copied' : 'Copy summary') + '</button>' +
|
|
2370
|
+
'<button type="button" class="cx-btn quiet" id="sp-copy-full" data-sp="copy-full" title="Copy the meeting including the full transcript">' + (cp === 'full' ? 'Copied' : 'Copy full (' + Math.max(1, Math.floor(R.content.fullChars / 1024)) + ' KB)') + '</button>';
|
|
2368
2371
|
}
|
|
2369
2372
|
var next = S.meetings ? nextUnnamed(R.sid) : null;
|
|
2370
2373
|
acts += '<button type="button" class="cx-btn quiet" data-sp="next-unnamed"' + (next ? '' : ' disabled') + ' title="' +
|
|
@@ -2424,6 +2427,27 @@
|
|
|
2424
2427
|
|
|
2425
2428
|
// ── render dispatch ────────────────────────────────────
|
|
2426
2429
|
var expiryTimer = null;
|
|
2430
|
+
// 7.0.30 item 3: a copy confirms on its own button, then the button goes back to Copy; the words go to a status region
|
|
2431
|
+
// that lives outside the rebuilt pane, so a screen reader hears them (a region made in the same rebuild may not be).
|
|
2432
|
+
var COPIED_MS = 2500;
|
|
2433
|
+
function spAnnounce(text) {
|
|
2434
|
+
var live = document.getElementById('sp-copy-live');
|
|
2435
|
+
if (!live) {
|
|
2436
|
+
live = document.createElement('div');
|
|
2437
|
+
live.id = 'sp-copy-live'; live.className = 'sr-only';
|
|
2438
|
+
live.setAttribute('role', 'status'); live.setAttribute('aria-live', 'polite');
|
|
2439
|
+
document.body.appendChild(live);
|
|
2440
|
+
}
|
|
2441
|
+
live.textContent = '';
|
|
2442
|
+
setTimeout(function () { live.textContent = text; }, 30);
|
|
2443
|
+
}
|
|
2444
|
+
function spCopied(R, which, words) {
|
|
2445
|
+
var at = Date.now();
|
|
2446
|
+
R.copied = { which: which, at: at };
|
|
2447
|
+
render();
|
|
2448
|
+
spAnnounce(words);
|
|
2449
|
+
setTimeout(function () { if (R.copied && R.copied.at === at) { R.copied = null; render(); } }, COPIED_MS);
|
|
2450
|
+
}
|
|
2427
2451
|
function render() {
|
|
2428
2452
|
if (W.parseRoute().pane !== 'speakers') return;
|
|
2429
2453
|
var rt = route();
|
|
@@ -2641,7 +2665,8 @@
|
|
|
2641
2665
|
if (!text) { R.copyNote = 'Nothing to copy'; render(); return; }
|
|
2642
2666
|
W.copyText(text).then(function () {
|
|
2643
2667
|
var kb = Math.max(1, Math.floor((full ? c.fullChars : c.summaryChars) / 1024));
|
|
2644
|
-
R.copyNote = full ? 'Copied full meeting (' + kb + ' KB)' : 'Copied summary (' + kb + ' KB)';
|
|
2668
|
+
R.copyNote = full ? 'Copied full meeting (' + kb + ' KB)' : 'Copied summary (' + kb + ' KB)';
|
|
2669
|
+
spCopied(R, full ? 'full' : 'summary', R.copyNote);
|
|
2645
2670
|
}, function (err) { W.toast(err.message); });
|
|
2646
2671
|
return;
|
|
2647
2672
|
}
|
|
@@ -471,8 +471,8 @@
|
|
|
471
471
|
close('leave');
|
|
472
472
|
O.depth = 1;
|
|
473
473
|
if (!replace) { W.go(pane, args || []); return; }
|
|
474
|
-
if (extra) { quietBack(extra, function () {
|
|
475
|
-
|
|
474
|
+
if (extra) { quietBack(extra, function () { W.replace(hash); }); return; }
|
|
475
|
+
W.replace(hash);
|
|
476
476
|
}
|
|
477
477
|
// A Back step the overlay takes itself (X, Back to sessions, leaving for a tab): its popstate is ours, not the phone's
|
|
478
478
|
// Back, and `then` runs once it lands. If it never arrives (history.go with nothing to go back to, or a browser that
|
|
@@ -2813,7 +2813,7 @@
|
|
|
2813
2813
|
if (args[0] === 'sort') {
|
|
2814
2814
|
S.tab = 'sort'; S.sortGroupId = ''; S.sortIndex = 0; S.moreOpen = false;
|
|
2815
2815
|
if (!S.intake && !S.intakeLoading) loadIntake(true);
|
|
2816
|
-
|
|
2816
|
+
W.replace('#/work');
|
|
2817
2817
|
return;
|
|
2818
2818
|
}
|
|
2819
2819
|
startPoll(); resumeHandoffPolls();
|
|
@@ -263,7 +263,8 @@
|
|
|
263
263
|
}
|
|
264
264
|
crumbs.innerHTML = parts.join('');
|
|
265
265
|
$('nav-back').disabled = onToday;
|
|
266
|
-
//
|
|
266
|
+
// Below 600 px the section name gets the detail too. On a reader that label is hidden (the section button gives way
|
|
267
|
+
// to Back, which names where it goes), so this shows only on a list that has a detail.
|
|
267
268
|
var label = $('section-name');
|
|
268
269
|
if (label && detail && railHidden()) label.textContent = detail;
|
|
269
270
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"algorithm": "sha256",
|
|
4
4
|
"files": {
|
|
5
5
|
"/index.html": "3bea8ab14118e48c8bbf5f09de2e8b65ca1225b7e7b564982a84477bafc8341e",
|
|
6
|
-
"/signer-proof.js": "
|
|
6
|
+
"/signer-proof.js": "13b2d3f2709501b3bacff63b5a041282cacdf5bce6be168779c59ba89cf2e0c3",
|
|
7
7
|
"/signer-store.js": "ae123328f922a509eea4d8f9e8440c9ef6972eeb14bf90b9e8bde3c84f7132bc",
|
|
8
8
|
"/signer.css": "e19f5ec70c9dae28347ad40cd08742842e964b3d0b973a5c39f48f9cafdd319b",
|
|
9
|
-
"/signer.js": "
|
|
9
|
+
"/signer.js": "098f484b452c0c2b8ff33c9ca7764fb179e3e5f52c77bebf47eb540e73e7a566"
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -129,13 +129,28 @@
|
|
|
129
129
|
if (!Object.prototype.hasOwnProperty.call(channels, s.action) || channels[s.action].indexOf(s.channel) < 0
|
|
130
130
|
|| !/^[a-z0-9@][a-z0-9@._/-]{0,127}$/.test(s.artifact) || !/^[0-9]{1,5}\.[0-9]{1,5}\.[0-9]{1,6}(-[0-9A-Za-z.-]{1,32})?$/.test(s.version)
|
|
131
131
|
|| !/^(sha256:[0-9a-f]{64}|sha512:[0-9a-f]{128}|sha512-[A-Za-z0-9+/]{86}==)$/.test(s.digest) || !HEX64.test(s.packetSha256)) throw fail('scope_not_allowed')
|
|
132
|
-
var
|
|
133
|
-
if (
|
|
132
|
+
var manifest = manifestArtifact(s.channel)
|
|
133
|
+
if (manifest && (s.artifact !== manifest || !/^sha256:[0-9a-f]{64}$/.test(s.digest))) throw fail('scope_not_allowed')
|
|
134
134
|
return { kind: 'approve', action: s.action, artifact: s.artifact, version: s.version, digest: s.digest, channel: s.channel, packetSha256: s.packetSha256 }
|
|
135
135
|
}
|
|
136
136
|
throw fail('scope_not_allowed')
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// What an approval on each channel does, one fixed line per channel (Miles, 2026-11-12: a publish approval covers
|
|
140
|
+
// its own push). A copy of APPROVE_CHANNEL_EFFECT in shared/web-turn-envelope.ts; signer-proof.test.ts pins the two.
|
|
141
|
+
var CHANNEL_EFFECT = { 'mac-local': 'Installs on this Mac. No git push.', 'npm-next': 'Publishes to npm under the next tag, from the tested package. No git push.', 'npm-latest': 'Publishes to npm under the latest tag, from the tested package. No git push.', 'control-appcast': 'Pushes ukaoma/cos-starter main (control/appcast.json): the COS Control update feed on gotcos.com.', 'hub-alias': 'Pushes ukaoma/evenhub-g2-cos main: moves the Even Hub alias to this package.', 'gotcos-site': 'Pushes ukaoma/cos-starter main: republishes www.gotcos.com.', 'control-downloads': 'Creates a GitHub Release on ukaoma/cos-starter (a tag, no branch push) with the ZIP and its SHA-256 file.', 'release-docs': 'Pushes ukaoma/cos-starter main (docs/): republishes the public COS docs.' }
|
|
142
|
+
// Channels that approve one canonical deployment manifest, and the artifact each must name. A copy of
|
|
143
|
+
// PUBLISH_MANIFEST_ARTIFACTS in shared/web-turn-envelope.ts; signer-proof.test.ts pins the two (Q1, 2026-11-12).
|
|
144
|
+
var MANIFEST_ARTIFACTS = { 'gotcos-site': 'gotcos-site-manifest', 'control-downloads': 'cos-control-downloads-manifest', 'release-docs': 'cos-release-docs-manifest' }
|
|
145
|
+
/** The manifest artifact a channel requires, or '' when the channel is not a manifest channel. */
|
|
146
|
+
function manifestArtifact(channel) {
|
|
147
|
+
return typeof channel === 'string' && Object.prototype.hasOwnProperty.call(MANIFEST_ARTIFACTS, channel) ? MANIFEST_ARTIFACTS[channel] : ''
|
|
148
|
+
}
|
|
149
|
+
/** The fixed line for a channel this page can sign, or '' for anything else. Never built from packet text. */
|
|
150
|
+
function channelEffect(channel) {
|
|
151
|
+
return typeof channel === 'string' && Object.prototype.hasOwnProperty.call(CHANNEL_EFFECT, channel) ? CHANNEL_EFFECT[channel] : ''
|
|
152
|
+
}
|
|
153
|
+
|
|
139
154
|
/** Verify the full packet before display AND again before signing. A packet is still only a request. */
|
|
140
155
|
function validateHandoffPacket(h, nowMs) {
|
|
141
156
|
return Promise.resolve().then(function () {
|
|
@@ -340,7 +355,7 @@
|
|
|
340
355
|
canonicalJson: canonicalJson, jcsBytes: jcsBytes,
|
|
341
356
|
signedWords: signedWords, wordsDigest: wordsDigest,
|
|
342
357
|
buildEnvelope: buildEnvelope, passkeyChallenge: passkeyChallenge, passkeyHeader: passkeyHeader,
|
|
343
|
-
assembleTurnProof: assembleTurnProof, validateHandoffPacket: validateHandoffPacket,
|
|
358
|
+
assembleTurnProof: assembleTurnProof, validateHandoffPacket: validateHandoffPacket, channelEffect: channelEffect, manifestArtifact: manifestArtifact,
|
|
344
359
|
sasCommit: sasCommit, sasCode: sasCode, kidOf: kidOf, fingerprintOf: fingerprintOf, enrollProofMessage: enrollProofMessage,
|
|
345
360
|
spkiFromXY: spkiFromXY, xyFromSpki: xyFromSpki, coseFromXY: coseFromXY, xyFromCose: xyFromCose,
|
|
346
361
|
xyFromAttestation: xyFromAttestation, passkeyXY: passkeyXY,
|
|
@@ -124,8 +124,10 @@
|
|
|
124
124
|
return 'Unknown'
|
|
125
125
|
}
|
|
126
126
|
function deploymentMeaning(scope) {
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
// The fixed line for the channel (P.channelEffect), never text from the packet's requester (2026-11-12).
|
|
128
|
+
var effect = scope && scope.kind === 'approve' ? P.channelEffect(scope.channel) : ''
|
|
129
|
+
if (!effect) return ''
|
|
130
|
+
return '\nWhat this approves: ' + effect + (P.manifestArtifact(scope.channel) ? '\nApproval is limited to this exact manifest SHA-256. The receiving release tool checks its files and destination.' : '')
|
|
129
131
|
}
|
|
130
132
|
function claim(id) {
|
|
131
133
|
show('loading')
|
|
@@ -37,8 +37,15 @@ export const WEB_TURN_MAX_LIFETIME_S = 15 * 60
|
|
|
37
37
|
/** How far in the future `iat` may sit (clock skew between the phone and this Mac), in seconds. */
|
|
38
38
|
export const WEB_TURN_IAT_SKEW_S = 60
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The only actions an `approve` scope can name. Anything else, including every never-list item, is unexpressible.
|
|
42
|
+
* Miles, 2026-11-12 11:11: a phone approval may sign everything the build allows except pushing main, which stays
|
|
43
|
+
* typed at the desk ("Keep allow for all action but the push to main. That way we can resolve when at the desk").
|
|
44
|
+
*/
|
|
45
|
+
export const APPROVE_ACTIONS: ReadonlySet<string> = new Set(['install', 'publish'])
|
|
46
|
+
|
|
47
|
+
/** Actions a phone can never sign. Not expressible, and still named in every `notAuthorized` list. */
|
|
48
|
+
export const APPROVE_ACTIONS_REMOVED: readonly string[] = Object.freeze(['push-main'])
|
|
42
49
|
|
|
43
50
|
/** What a `message` scope means: conversation only (review B4). */
|
|
44
51
|
export const MESSAGE_SCOPE_MEANING = 'conversation'
|
|
@@ -55,10 +62,28 @@ export const MESSAGE_SCOPE_NOT_AUTHORIZED: readonly string[] = Object.freeze([
|
|
|
55
62
|
])
|
|
56
63
|
|
|
57
64
|
/** The channels each approve action may name. Nothing else is signable (review B4). */
|
|
58
|
-
export const APPROVE_ACTION_CHANNELS: Readonly<Record<'install' | 'publish'
|
|
65
|
+
export const APPROVE_ACTION_CHANNELS: Readonly<Record<'install' | 'publish', readonly string[]>> = Object.freeze({
|
|
59
66
|
install: Object.freeze(['mac-local']),
|
|
60
67
|
publish: Object.freeze(['npm-next', 'npm-latest', 'control-appcast', 'hub-alias', 'gotcos-site', 'control-downloads', 'release-docs']),
|
|
61
|
-
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What an approval on each channel does, in one fixed line the phone shows (Miles, 2026-11-12: "Publish includes its
|
|
72
|
+
* own push"). A publish approval covers the push that publish consists of, and only that: cos-starter main for the
|
|
73
|
+
* site, the appcast and the docs; evenhub-g2-cos main for the Hub alias; npm goes out from the tested package with no
|
|
74
|
+
* push. Pushing the code main of any repo stays typed at the desk. The line is derived from the CHANNEL alone, never
|
|
75
|
+
* from text the requester sent. The phone's page keeps a copy (server/web-signer/signer-proof.js, CHANNEL_EFFECT),
|
|
76
|
+
* pinned to this one by signer-proof.test.ts.
|
|
77
|
+
*/
|
|
78
|
+
export const APPROVE_CHANNEL_EFFECT: Readonly<Record<string, string>> = Object.freeze({
|
|
79
|
+
'mac-local': 'Installs on this Mac. No git push.',
|
|
80
|
+
'npm-next': 'Publishes to npm under the next tag, from the tested package. No git push.',
|
|
81
|
+
'npm-latest': 'Publishes to npm under the latest tag, from the tested package. No git push.',
|
|
82
|
+
'control-appcast': 'Pushes ukaoma/cos-starter main (control/appcast.json): the COS Control update feed on gotcos.com.',
|
|
83
|
+
'hub-alias': 'Pushes ukaoma/evenhub-g2-cos main: moves the Even Hub alias to this package.',
|
|
84
|
+
'gotcos-site': 'Pushes ukaoma/cos-starter main: republishes www.gotcos.com.',
|
|
85
|
+
'control-downloads': 'Creates a GitHub Release on ukaoma/cos-starter (a tag, no branch push) with the ZIP and its SHA-256 file.',
|
|
86
|
+
'release-docs': 'Pushes ukaoma/cos-starter main (docs/): republishes the public COS docs.',
|
|
62
87
|
})
|
|
63
88
|
|
|
64
89
|
export const APPROVE_ARTIFACT_RE = /^[a-z0-9@][a-z0-9@._/-]{0,127}$/
|
|
@@ -88,13 +113,13 @@ export function approveFieldsValid(action: unknown, artifact: unknown, version:
|
|
|
88
113
|
/** The verifier's `notAuthorized` list for a scope: the never-list plus what this scope does not cover. */
|
|
89
114
|
export function notAuthorizedFor(scope: WebTurnScope): string[] {
|
|
90
115
|
if (scope.kind === 'message') return [...WEB_KEY_NEVER_AUTHORIZED, ...MESSAGE_SCOPE_NOT_AUTHORIZED]
|
|
91
|
-
const others = [...APPROVE_ACTIONS].filter(a => a !== scope.action)
|
|
116
|
+
const others = [...APPROVE_ACTIONS, ...APPROVE_ACTIONS_REMOVED].filter(a => a !== scope.action)
|
|
92
117
|
return [...WEB_KEY_NEVER_AUTHORIZED, ...others, 'push-branch', 'hub-file', 'other-artifact-version-digest-or-channel']
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
export type WebTurnScope =
|
|
96
121
|
| { kind: 'message' }
|
|
97
|
-
| { kind: 'approve'; action: 'install' | 'publish'
|
|
122
|
+
| { kind: 'approve'; action: 'install' | 'publish'; artifact: string; version: string; digest: string; channel: string; packetSha256: string }
|
|
98
123
|
|
|
99
124
|
export interface WebTurnAttachment { id: string; sha256: string; bytes: number; name: string }
|
|
100
125
|
|
|
@@ -194,7 +219,7 @@ export function parseWebTurnScope(v: unknown): WebTurnScope | null {
|
|
|
194
219
|
// The closed action list, the strict field patterns and the channel table (approveFieldsValid).
|
|
195
220
|
if (!approveFieldsValid(v.action, v.artifact, v.version, v.digest, v.channel)) return null
|
|
196
221
|
if (typeof v.packetSha256 !== 'string' || !HEX64.test(v.packetSha256)) return null
|
|
197
|
-
return { kind: 'approve', action: v.action as 'install' | 'publish'
|
|
222
|
+
return { kind: 'approve', action: v.action as 'install' | 'publish', artifact: v.artifact as string, version: v.version as string, digest: v.digest as string, channel: v.channel as string, packetSha256: v.packetSha256 }
|
|
198
223
|
}
|
|
199
224
|
return null
|
|
200
225
|
}
|
|
@@ -249,7 +274,7 @@ export function parseWebTurnEnvelope(v: unknown): { ok: true; envelope: WebTurnE
|
|
|
249
274
|
export interface WebApprovalPacket {
|
|
250
275
|
v: typeof WEB_APPROVAL_PACKET_VERSION
|
|
251
276
|
packetId: string
|
|
252
|
-
action: 'install' | 'publish'
|
|
277
|
+
action: 'install' | 'publish'
|
|
253
278
|
artifact: string
|
|
254
279
|
version: string
|
|
255
280
|
digest: string
|