@gotcos/glasses-server 7.0.27 → 7.0.29
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 +63 -0
- package/docs/web-key-verifier-contract.md +1 -1
- package/package.json +1 -1
- package/server/index.ts +29 -6
- package/server/lib/tailnet-self-name.ts +57 -10
- package/server/lib/web-key-signer-app.ts +25 -6
- package/server/lib/web-key-signer-tls.ts +790 -0
- package/server/routes/web-key.ts +31 -2
- package/server/web-control/.manifest.json +4 -4
- package/server/web-control/app.css +54 -0
- package/server/web-control/app.js +317 -36
- package/server/web-control/index.html +44 -5
- package/server/web-control/theme.js +169 -2
package/server/routes/web-key.ts
CHANGED
|
@@ -43,9 +43,21 @@ export interface WebKeyRouterDeps {
|
|
|
43
43
|
isWebRequest: (req: Request) => boolean
|
|
44
44
|
apiToken: () => string
|
|
45
45
|
selfDnsName: () => string | null
|
|
46
|
+
/**
|
|
47
|
+
* 7.0.14: when selfDnsName() is null, GET status (after its auth check) awaits this before it
|
|
48
|
+
* answers, so a boot-time miss does not hand COS Control `origin: null` (lib/tailnet-self-name.ts
|
|
49
|
+
* refreshIfMissing: bounded, single-flight, rate-limited). Never called while a name is known.
|
|
50
|
+
*/
|
|
51
|
+
refreshSelfDnsName?: () => Promise<unknown>
|
|
46
52
|
expectedOrigin: () => string | null
|
|
47
53
|
/** Served-asset integrity (Phase 6) for Control's audit; absent: not reported. */
|
|
48
54
|
integrityStatus?: () => Record<string, unknown>
|
|
55
|
+
/**
|
|
56
|
+
* B2: whether 3144 presents a publicly trusted certificate for this Mac's tailnet name, and why not
|
|
57
|
+
* (lib/web-key-signer-tls.ts SignerTlsStatus, e.g. `tailnet_https_certs_off`). Authority mode only;
|
|
58
|
+
* absent or null (no 3144 listener): not reported.
|
|
59
|
+
*/
|
|
60
|
+
signerTls?: () => object | null
|
|
49
61
|
/**
|
|
50
62
|
* Phase 3: whether this request reached the 3144 signer listener (index.ts: the socket's local
|
|
51
63
|
* port). Given, the phone's key steps (enroll start and prove, nonce) and the handoff claim answer
|
|
@@ -97,13 +109,27 @@ export function createWebKeyRouter(deps: WebKeyRouterDeps): Router {
|
|
|
97
109
|
next()
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
|
|
112
|
+
// 7.0.14: a request that passed the status route's own auth check, while this Mac's tailnet name is
|
|
113
|
+
// unknown, waits for one bounded lookup instead of answering a stale miss. Known name: no wait.
|
|
114
|
+
const learnSelfName = async (): Promise<void> => {
|
|
115
|
+
if (!deps.refreshSelfDnsName || deps.selfDnsName() !== null) return
|
|
116
|
+
try { await deps.refreshSelfDnsName() } catch { /* answer with what is known */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const signerTlsField = (): { signerTls?: object } => {
|
|
120
|
+
let state: object | null = null
|
|
121
|
+
try { state = deps.signerTls?.() ?? null } catch { state = null }
|
|
122
|
+
return state ? { signerTls: state } : {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
router.get('/web-key/status', anchorRoutes, async (req, res) => {
|
|
101
126
|
const registry = deps.authority.registry
|
|
102
|
-
const selfDns = deps.selfDnsName()
|
|
103
127
|
if (deps.authority.mode() !== 'authority') {
|
|
104
128
|
// Bootstrap only: COS Control's anchor state, nothing that suggests approvals. Token only:
|
|
105
129
|
// no web page needs it while no phone can enroll. `enabled: false` keeps older Controls hidden.
|
|
106
130
|
if (!hasToken(req)) return refuse(res, 401, 'unauthorized')
|
|
131
|
+
await learnSelfName()
|
|
132
|
+
const selfDns = deps.selfDnsName()
|
|
107
133
|
const anchor = registry.anchor()
|
|
108
134
|
return void res.json({
|
|
109
135
|
ok: true,
|
|
@@ -118,6 +144,8 @@ export function createWebKeyRouter(deps: WebKeyRouterDeps): Router {
|
|
|
118
144
|
})
|
|
119
145
|
}
|
|
120
146
|
if (!hasToken(req) && !web(req)) return refuse(res, 401, 'unauthorized')
|
|
147
|
+
await learnSelfName()
|
|
148
|
+
const selfDns = deps.selfDnsName()
|
|
121
149
|
res.json({
|
|
122
150
|
ok: true,
|
|
123
151
|
enabled: true,
|
|
@@ -131,6 +159,7 @@ export function createWebKeyRouter(deps: WebKeyRouterDeps): Router {
|
|
|
131
159
|
epoch: registry.epoch(),
|
|
132
160
|
activeKeys: registry.activeCount(selfDns),
|
|
133
161
|
...(deps.integrityStatus ? { integrity: deps.integrityStatus() } : {}),
|
|
162
|
+
...signerTlsField(),
|
|
134
163
|
})
|
|
135
164
|
})
|
|
136
165
|
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"v": 1,
|
|
3
3
|
"algorithm": "sha256",
|
|
4
4
|
"files": {
|
|
5
|
-
"/app.css": "
|
|
6
|
-
"/app.js": "
|
|
5
|
+
"/app.css": "1204f3b57d312c94e96a1430ba036292873ecbe132073c8a4f03e37063172a70",
|
|
6
|
+
"/app.js": "1491f6dd957f47881e55a47caf98a3ce8fcb6df43a58b6e6877dd86807bebea6",
|
|
7
7
|
"/apple-touch-icon.png": "04ec7eca0bf6f53d89900d146ac32d4826759d6258bcdbd1ccdcc517f26f532c",
|
|
8
8
|
"/control.css": "e7b287aa4acc1b3895a9ce61512fe449c555b6335a7145684fd78108ae008c75",
|
|
9
9
|
"/fonts/DMSans.ttf": "8cd08d97e89c24d0aa92edd2f0f4c8ee6195eee9b7c9f154865a58b02f0c1c0d",
|
|
10
10
|
"/fonts/Fraunces.ttf": "177ff6c0f14e5550a3c624247cd1189611d4eb65d000b14944c63d967958abbb",
|
|
11
11
|
"/fonts/JetBrainsMono.ttf": "48715a42ec242c21e9f02692891e147d022299a52e48d5e413e1a942193ffeda",
|
|
12
|
-
"/index.html": "
|
|
12
|
+
"/index.html": "8664ebf9ae7635c1fcf5fce333b89d272b6ce0df9529120523b99a3d49eb5ebb",
|
|
13
13
|
"/mark.svg": "8e380d33d393027df192eb1afa08f15a380c2070c72d68f27311b49d9e94ca18",
|
|
14
14
|
"/memories/d3.min.js": "f2094bbf6141b359722c4fe454eb6c4b0f0e42cc10cc7af921fc158fceb86539",
|
|
15
15
|
"/memories/graph-explorer.css": "ec128de59811f45c71b6bededd737989309205e903951eb8211ade7664f87486",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"/panes/work.css": "da50355dac690f712d1f6331f70ee5eadbf5982d37508648ae7786afc99ac7b1",
|
|
39
39
|
"/panes/work.js": "1ef872b8c7e1a57bd7bd314e11eee7ac5fa9668871da7e644a1fbf5fd9c0d9e8",
|
|
40
40
|
"/shell.js": "f1cc6a52c78e01ddba89ff27c6bd0e8f8bd8f29bdfcbb5150ee4ed66a8128d9d",
|
|
41
|
-
"/theme.js": "
|
|
41
|
+
"/theme.js": "e3e6422eaac43979d03149da114f5b83757b7361dd7b309cdf76be36a596044b",
|
|
42
42
|
"/vendor/qrcode-encoder.js": "61d87cc7445cc66b862cff913de4571634746ea62bcd50d0f592be14f6d1ce46"
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -653,3 +653,57 @@ html { touch-action: manipulation; }
|
|
|
653
653
|
.sx-te-choices, .ta-te-choices { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 6px; }
|
|
654
654
|
.sx-te-choices .cx-btn, .ta-te-choices .cx-btn { flex: 1 1 12rem; min-height: 44px; white-space: normal; text-align: center; line-height: 1.25; }
|
|
655
655
|
.sx-mac-q .sx-note { margin: 6px 0 0; }
|
|
656
|
+
|
|
657
|
+
/* ── 7.0.29: the folded Duo's side rail (Miles 21:39 "These side buttons are a must on the duo"; 21:42 tall phones keep
|
|
658
|
+
the top menu). theme.js sets html.short-screen from the screen's shape; this draws it only under 600 px on a touch
|
|
659
|
+
screen, so nothing changes from 600 px, on a mouse, or on a tall phone. It is the same nav.rail as the wide rail, so
|
|
660
|
+
the panes, their order, icons, badges and aria-current are one list: here it moves to the right edge as icons only,
|
|
661
|
+
with the top bar's Back at its foot on a reader. --rail-hit is 44 points on the phone: app.js railHitSync sets it from
|
|
662
|
+
the zoom (52 px at Safari's 85 percent Page Zoom, 44 px at 100); 52 px until it runs. Colours are the rail's own
|
|
663
|
+
tokens; the current pane keeps the wide rail's look (rail-active fill and its small shadow, no border). */
|
|
664
|
+
.rail-back { display: none; }
|
|
665
|
+
@media (max-width: 599px) and (pointer: coarse) {
|
|
666
|
+
html.short-screen { --rail-w: calc(var(--rail-hit, 52px) + 6px); }
|
|
667
|
+
html.short-screen .app-shell { display: flex; flex-direction: row; align-items: flex-start; }
|
|
668
|
+
html.short-screen.sx-frame .app-shell { flex-direction: row; align-items: stretch; }
|
|
669
|
+
html.short-screen .main-col { flex: 1 1 auto; min-width: 0; --safe-right: 0px; }
|
|
670
|
+
html.short-screen .rail {
|
|
671
|
+
order: 2; flex: none; width: calc(var(--rail-w) + var(--safe-right)); box-sizing: border-box;
|
|
672
|
+
display: flex; flex-direction: column; align-items: center; gap: 0;
|
|
673
|
+
position: sticky; top: 0; height: 100dvh; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none;
|
|
674
|
+
padding: calc(var(--safe-top) + 4px) var(--safe-right) calc(var(--safe-bottom) + 6px) 0;
|
|
675
|
+
background: var(--rail); border-left: 1px solid var(--rail-border);
|
|
676
|
+
}
|
|
677
|
+
html.short-screen.sx-frame .rail { position: static; height: auto; min-height: 0; }
|
|
678
|
+
html.short-screen .rail::-webkit-scrollbar { display: none; }
|
|
679
|
+
html.short-screen .rail-head, html.short-screen .rail-text, html.short-screen .rail-settings .rail-label, html.short-screen .server-line { display: none; }
|
|
680
|
+
html.short-screen .rail-btn, html.short-screen .rail-settings {
|
|
681
|
+
position: relative; flex: none; display: grid; place-items: center;
|
|
682
|
+
width: var(--rail-hit, 52px); height: var(--rail-hit, 52px); min-height: 0; padding: 0;
|
|
683
|
+
border-radius: 12px; color: var(--grey);
|
|
684
|
+
}
|
|
685
|
+
html.short-screen .rail-btn .icon, html.short-screen .rail-settings .icon { width: 22px; height: 22px; }
|
|
686
|
+
html.short-screen .rail-btn[aria-current='page'], html.short-screen .rail-settings[aria-current='page'] { background: var(--rail-active); color: var(--ink); box-shadow: 0 1px 2px rgba(43, 33, 26, 0.08); }
|
|
687
|
+
html.short-screen .rail-btn:active:not([aria-current='page']), html.short-screen .rail-settings:active:not([aria-current='page']) { background: var(--rail-border); color: var(--ink); }
|
|
688
|
+
html.short-screen .rail-btn .badge-dot { top: 9px; right: 9px; }
|
|
689
|
+
html.short-screen .rail-btn.hot::after { content: ''; position: absolute; top: 9px; right: 9px; width: 8px; height: 8px; border-radius: 50%; background: var(--hot); }
|
|
690
|
+
html.short-screen .rail-btn .inbox-count { position: absolute; top: 4px; right: 2px; }
|
|
691
|
+
html.short-screen .rail-btn .inbox-dot { position: absolute; top: 9px; right: 9px; }
|
|
692
|
+
html.short-screen .rail-sep { flex: none; width: 24px; height: 1px; margin: 3px 0; background: var(--rail-border); }
|
|
693
|
+
html.short-screen .rail-spacer { display: none; }
|
|
694
|
+
/* Back: alone at the foot, outlined so it never reads as a pane; it stays on screen when the panes scroll. */
|
|
695
|
+
html.short-screen .rail-back:not([hidden]) {
|
|
696
|
+
display: grid; place-items: center; flex: none; margin-top: auto;
|
|
697
|
+
position: sticky; bottom: 0; z-index: 1;
|
|
698
|
+
width: var(--rail-hit, 52px); height: var(--rail-hit, 52px);
|
|
699
|
+
border: 1px solid var(--btn-border); border-radius: 12px; background: var(--rail); color: var(--ink);
|
|
700
|
+
}
|
|
701
|
+
html.short-screen .rail-back .icon { width: 22px; height: 22px; stroke-width: 2; }
|
|
702
|
+
html.short-screen .rail-back:active { background: var(--rail-border); }
|
|
703
|
+
/* The focus ring sits inside the button: the rail clips anything past its edge. */
|
|
704
|
+
html.short-screen .rail :focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; border-radius: 12px; }
|
|
705
|
+
/* One way to switch panes on a screen: the rail. The top bar keeps the pane's name, without the caret and its menu
|
|
706
|
+
(app.js makes the button inert), and its Back moves to the rail. */
|
|
707
|
+
html.short-screen .section-btn .caret, html.short-screen #topbar-back { display: none !important; }
|
|
708
|
+
html.short-screen .section-btn { cursor: default; }
|
|
709
|
+
}
|