@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 CHANGED
@@ -1,3 +1,79 @@
1
+ ## 7.0.31 - Control Web: a message's text no longer runs under and below Reply on a wide screen
2
+
3
+ 2026-11-12. A hotfix to 7.0.30, nothing else in it.
4
+
5
+ - **7.0.30 shipped this fault.** On a wide screen (the open Duo, an iPad, a desktop browser), a message's text could run behind and below the pinned Reply box. The top bar was gone, and the left rail started partway down its list and ended partway down the screen. Miles saw it on the open Duo at 17:25 and could not make it happen again. It came from 7.0.30's Reply staying at the bottom of the screen.
6
+ - **The cause.** The message reader was the whole page. Its Reply box was pinned to Safari's idea of the screen, and the rail was as tall as that idea. On his Duo, typing a reply moved Safari's view about 409 px down the page, as a keyboard does, so the field stays in sight. After the keyboard closed, he folded and unfolded the Duo five times. Safari then reported its visible area as 0 px tall, then as a large negative number, with its top still about 409 px down. The view stayed shifted, and Reply and the rail stopped about halfway down the screen, with the rest of the message painted below them. It needs that keyboard-then-fold sequence, which is why it came and went. No part of the page was too tall for its box.
7
+ - **The fix.** From 600 px wide, an open message is now one screen, the way Sessions already is. The rail and the area under the top bar each scroll by themselves, and the page itself no longer scrolls. The sign-in notice, a question card and the message still scroll together, as in 7.0.30, and Reply stays at the bottom of that area. At the end of a message, the Reply section sits just above the Reply box, with nothing under it. With the keyboard up, the box sits above the keyboard as before. The screen's height comes from the page's own measurement, which falls back to the window's height when Safari's reading is broken.
8
+ - **What 7.0.31 does and does not fix.**
9
+ - **Fixed:** it stops the message showing behind and below Reply.
10
+ - **Not fixed:** Safari leaving the page pushed down after the keyboard and a fold. If that happens again, the app can sit shifted, with a blank band at the bottom of the screen and the top bar out of sight. It returns to the full screen when Safari's view moves back, for example on the next fold, rotation or keyboard. In a browser test, the page refilled the screen after a resize, with no reload.
11
+ - **Not done:** the page does not follow Safari's reported position. On his Duo that reading was 40 to 85 px off while the height read 0.
12
+ - **What's next:** we now record Safari's readings on every message opened, every fold and every keyboard close, to find out how often it happens.
13
+ - **Seen once in the readings we have:** today, from 07:42, the only stranded push came after a keyboard in the message reader. After five keyboard uses in Sessions and one in the COS chat, which were already one screen, every following reading was back at the top, through folds that broke the height readings the same way. One day of readings proves nothing either way.
14
+ - **On the open Duo with a question card showing**, a short message's Reply box starts just below the bottom of the screen, as in 7.0.30. The page stays one screen, and scrolling the message brings Reply up.
15
+ - **Folding or unfolding while reading keeps your place.** A fold or an unfold moves the message between the page's own scroll (narrow) and the framed area (wide). On its own the new view would open at the top, so the line you were reading is put back at the top, once, after the fold. It waits until your finger is off the screen and the page is still. If you scroll first, your scroll wins and it does nothing. Nothing else moves the text: not the keyboard, not Safari's bars, not a question card or notice appearing.
16
+ - **Phones and the folded Duo otherwise unchanged.** Under 600 px, with no fold, the message reader works as in 7.0.30, with the side rail on the folded Duo.
17
+ - **The Reply section stays.** The "Reply. Continues this conversation" block above the box is where replies sent from that message appear. It is not a second Reply box.
18
+ - **The rest of the app in the same Safari state** (Today, the Messages list, Meetings, Speakers, Work and Settings, at 600 px and wider): no pinned part has text under it, but the rail can end partway down with the page continuing below it. Sessions already stays inside one screen. Those panes are not changed in this hotfix.
19
+ - **New numbers in the page's own layout reports**, numbers only, to the Mac's diagnostics log:
20
+ - the height of 100dvh at each fold;
21
+ - for every message opened, at any width: the visible area, the message's box, the scrolling area's height and content height, where Reply sits, the page's height and scroll, and 100dvh. The page already sends the window's size and Safari's visible area with each report, so a shifted view shows as its top.
22
+ The 17:25 frame could not be measured; the next one will be.
23
+ - **Tested in a browser (WebKit)**, in light and dark:
24
+ - **His two Safari states, recreated:** the view shifted 409 px, and a window 430 px tall drawn 787 px tall. 7.0.30 paints text below Reply in both; this build paints none.
25
+ - **A long message at seven sizes:** a tall iPhone, the folded Duo at 85% zoom, 1000x703, the open Duo at 1119x787 and 951x539, an iPad at 1180x820, and a desktop at 1440x900. 7.0.30 fails 20 of 48 of these checks; this build passes all 48.
26
+ - **The Sessions check list on the message reader:**
27
+ - typing with the keyboard up, with two files attached;
28
+ - folding and unfolding without a reload;
29
+ - Back while reading, and the how-you-got-here line on a phone;
30
+ - the browser's find, and selecting text;
31
+ - the question card;
32
+ - a short message and a 14,000 px message;
33
+ - the scroll position kept when the page refreshes the same message.
34
+
35
+ These ran at 951x539 and 1119x787 at 85%, 382x557, 449x797, 1180x820 and 1440x900. The keyboard was simulated by shrinking the window, the way an Android keyboard does; an iPhone keyboard shifts the view instead, which was not simulated. A reply arriving while you read was not driven.
36
+ - **Not yet:** a real phone. This counts as done only when Miles has looked at it on the Duo.
37
+
38
+ **Default-on for every install:** yes, with no switch, two things: on screens 600 px and wider, an open message is one screen and the area under the top bar scrolls instead of the page; and an open message keeps your reading place across a fold or an unfold. Nothing else changes under 600 px.
39
+
40
+ ### If you roll back
41
+
42
+ - To 7.0.30: an open message is the whole page again, and after the keyboard and folds its Reply box and the rail can stop partway down the screen with the message below them. A fold or an unfold moves you a few paragraphs back.
43
+ - Further back: as in 7.0.30 below.
44
+
45
+ ## 7.0.30 - Control Web: Back goes the way you came, a phone shows how you got here, a message shows who said what, Reply stays at the bottom, Meetings opens on today, and clearer copy buttons
46
+
47
+ 2026-11-12. 7.0.29 plus eight Control Web changes from a tester's phone feedback, one clearer sign-in error, and phone-approval changes that stay off unless switched on.
48
+
49
+ - **Back goes the way you came.** Back now returns to the page you came from, not a fixed parent. Open a meeting from Speakers, and Back takes you to Speakers, not to Meetings. It is the browser's own Back, so the two always agree, and it adds nothing to the browser's history. Opened straight from a link or after a reload, Back goes one level up as before. Back names where it goes ("Speakers", "Meetings"). On screens 600 px and wider, the navigation bar's Back keeps the word "Back" and says where it goes in its name and tooltip ("Back to Speakers").
50
+ - **Back stays in reach on a tall phone.** On a page you are reading (a session, a message, a meeting), the top bar with Back no longer slides away as you scroll down.
51
+ - **Reply stays at the bottom of a message.** Opening a message from Messages, the Reply box now sits at the bottom of the screen while you read, and above the keyboard while you type, as the COS chat already did.
52
+ - **Speakers: the copy buttons say they copy.** "Summary" and "Full (N KB)" now read "Copy summary" and "Copy full (N KB)". The one you tap says "Copied" for a moment, and a screen reader hears what was copied.
53
+ - **Meetings opens on today.** The calendar opens on today, marked with a ring, even when today has no meetings. Then the list says "No meetings today." with one tap to the latest earlier day this month that has meetings (or to last month when this month has none).
54
+ - **Settings says "your phone's camera"**, not "your iPhone Camera". The page is not only for iPhones.
55
+ - **A message shows who said what by side.** Your words sit on the right in the COS chat's gold bubble, with "You" under it; COS's answer sits on the left beside its mark. The two cards marked YOU and COS are gone. The bubble's edge is darker in light mode, so it reads against the page (3.48 to 1; it was 1.52 to 1). A screen reader hears "You said" and "COS said". The same applies to a chat opened from the archive.
56
+ - **A phone shows how you got here.** On a page you opened (a meeting, a message, a session), one line under the top bar names the steps that led there, for example "Today / Speakers / Naming voices / Meeting". Each earlier step is a link that goes back to it, the same way Back does; the page you are on is last and is not a link. The line never wraps: a long path drops steps from the middle and shows "…" in their place. There is no line on a pane's list, on a page opened from a link, or after a reload. Screens 600 px and wider are unchanged; they keep the navigation bar.
57
+ - Tested in a browser (WebKit) at a tall iPhone size, the folded Duo with its side rail, and the open Duo, in light and dark. Before this build the same checks fail. Not yet tested on a real phone; Android was not tested.
58
+
59
+ Server:
60
+
61
+ - **A sign-in that ran out says so.** When Claude Code reports "Failed to authenticate: OAuth session expired and could not be refreshed", COS now names it as a sign-in problem, not "provider process exited". The fork check and the provider check read the same words, so they say so too.
62
+ - **Phone approvals, off by default, unchanged for everyone until switched on:**
63
+ - A phone can sign every action this build supports except pushing main, which stays typed at the desk (Miles, 2026-11-12 11:11).
64
+ - The phone names what each approval includes, from one fixed line per channel (for example, the push that a publish makes).
65
+ - The phone-approval signer refuses a stored key or folder that other users can read (stored_readable). It runs only when the phone-approval switch is on.
66
+ - The web-key mutation gate's two dead targets now match. The script ships in the npm pack, so pack bytes change; server behaviour does not.
67
+
68
+ **Default-on for every install:** yes, all eight, with no switch, at every screen size (the how-you-got-here line below 600 px only). The sign-in line is on for everyone. The phone-approval items do nothing while the switch is off, which is the default everywhere.
69
+
70
+ **Known fault, fixed in 7.0.31:** on a wide screen, after the keyboard and then folds on the Duo, a message's text could run behind and below the pinned Reply box. 7.0.31 stops that. Safari can still leave the page pushed down after the keyboard and a fold; 7.0.31 does not fix that and records it when it happens.
71
+
72
+ ### If you roll back
73
+
74
+ - To 7.0.29: Back goes to a fixed parent again, the top bar tucks away on a reader, the message reader's Reply is at the end of the page, the copy buttons say Summary and Full, Meetings opens on today only when today has meetings, Settings says iPhone, a message shows the YOU and COS cards, the how-you-got-here line is gone, and an expired Claude sign-in reads "provider process exited" again. Nothing is stored.
75
+ - Further back: as in 7.0.29 below.
76
+
1
77
  ## 7.0.29 - Control Web: on the folded Duo, the panes sit in a side rail at the right edge, and Back is at thumb height
2
78
 
3
79
  2026-11-12. 7.0.28 plus Control Web changes: a side rail for the folded Duo only, and small Sessions list changes for every screen.
@@ -84,8 +84,9 @@ bumps the epoch.
84
84
  - `WEB_ANCHOR_MATURITY_MS = 86_400_000` (24 h). An anchor younger than that vouches nothing
85
85
  (`anchor_too_new`).
86
86
  - `setAt` is file-backed, so a same-user process can backdate it. The 24 h rule binds the server's
87
- own route; **the verifier's committed pin is the real check.** The verifier must refuse when the
88
- pin file is missing, uncommitted, or its `spkiSha256` differs from `anchor.spkiSha256` in the answer.
87
+ own route, and the verifier refuses when the pin file is missing, uncommitted, or its `spkiSha256`
88
+ differs from `anchor.spkiSha256` in the answer. Accepted limit (Miles, 2026-11-12): a process running
89
+ as the owner on this Mac can forge the whole chain, pin included.
89
90
 
90
91
  ## 3. Enrollment (anchor-signed approval)
91
92
 
@@ -176,7 +177,7 @@ footer as received (CRLF to LF only) and needs no trimming rule of its own.
176
177
 
177
178
  | Field | Pattern |
178
179
  | --- | --- |
179
- | action | `install` \| `publish` \| `push-main` |
180
+ | action | `install` \| `publish` |
180
181
  | artifact | `^[a-z0-9@][a-z0-9@._/-]{0,127}$` |
181
182
  | version | `^[0-9]{1,5}\.[0-9]{1,5}\.[0-9]{1,6}(-[0-9A-Za-z.-]{1,32})?$` |
182
183
  | digest | `^sha256:[0-9a-f]{64}$` or `^sha512:[0-9a-f]{128}$` or `^sha512-[A-Za-z0-9+/]{86}==$` (npm SRI) |
@@ -187,9 +188,34 @@ footer as received (CRLF to LF only) and needs no trimming rule of its own.
187
188
  | --- | --- |
188
189
  | install | `mac-local` |
189
190
  | publish | `npm-next`, `npm-latest`, `control-appcast`, `hub-alias`, `gotcos-site`, `control-downloads`, `release-docs` |
190
- | push-main | `git-main` |
191
191
 
192
- In this candidate `install` and `publish` are accepted only through exact scoped proof (`WEB_KEY_ENABLED_APPROVE_ACTIONS`). `push-main` remains refused with `approve_action_not_enabled`. A message scope never authorizes either action.
192
+ **Pushing main is never signable by phone** (Miles, typed 2026-11-12 11:11: "allow for all action but the push to
193
+ main. That way we can resolve when at the desk"). It stays typed at the desk. The `push-main` action and its `git-main`
194
+ channel are not expressible: a scope naming them is refused at the grammar (`scope_not_allowed`), and the packet route
195
+ refuses to create one (`action_not_allowed`). Pushing a feature branch and adding a versioned Hub file have no
196
+ `approve` form. `install` and `publish` are accepted only through exact scoped proof (`WEB_KEY_ENABLED_APPROVE_ACTIONS`).
197
+ A message scope never authorizes either action. The phone's signer page keeps its own copy of this table
198
+ (`server/web-signer/signer-proof.js`); a test fails if the two differ.
199
+
200
+ **What each channel's approval includes** (Miles, 2026-11-12: "Publish includes its own push"). A publish approval
201
+ covers the push that the publish consists of, and nothing else. Pushing the code `main` of any repo (the server, the
202
+ COS repo) stays typed at the desk. The phone shows the line below for the signed channel, taken from the channel alone
203
+ (`APPROVE_CHANNEL_EFFECT` in `shared/web-turn-envelope.ts`, copied as `CHANNEL_EFFECT` in the signer page and pinned to
204
+ it by test), never from text the requester sent. A channel not in the table shows nothing and is refused.
205
+
206
+ | Channel | The push or upload it includes | Line the phone shows |
207
+ | --- | --- | --- |
208
+ | `mac-local` | none | Installs on this Mac. No git push. |
209
+ | `npm-next` | none (npm registry only, from the tested package) | Publishes to npm under the next tag, from the tested package. No git push. |
210
+ | `npm-latest` | none (npm registry only, from the tested package) | Publishes to npm under the latest tag, from the tested package. No git push. |
211
+ | `control-appcast` | `ukaoma/cos-starter` `main` (`control/appcast.json`, served by GitHub Pages) | Pushes ukaoma/cos-starter main (control/appcast.json): the COS Control update feed on gotcos.com. |
212
+ | `hub-alias` | `ukaoma/evenhub-g2-cos` `main` (the alias file) | Pushes ukaoma/evenhub-g2-cos main: moves the Even Hub alias to this package. |
213
+ | `gotcos-site` | `ukaoma/cos-starter` `main` (GitHub Pages) | Pushes ukaoma/cos-starter main: republishes www.gotcos.com. |
214
+ | `control-downloads` | a GitHub Release on `ukaoma/cos-starter` (tag `control-v<version>`, the ZIP and its `.sha256`); no branch push. The site's links move only with `gotcos-site` or `control-appcast`, each its own approval | Creates a GitHub Release on ukaoma/cos-starter (a tag, no branch push) with the ZIP and its SHA-256 file. |
215
+ | `release-docs` | `ukaoma/cos-starter` `main` (`docs/`, GitHub Pages) | Pushes ukaoma/cos-starter main (docs/): republishes the public COS docs. |
216
+
217
+ The npm release flow as practised also commits the release files to the server repo and pushes its `main`
218
+ (cos-glasses skill, step 5). That push is NOT part of an npm approval: it is a code `main`, typed at the desk.
193
219
 
194
220
  ### Handoff from Control Web (3143) to the signer (3144), Phase 3
195
221
 
@@ -324,8 +350,8 @@ mirror) and refuses on difference; `approval.epoch` and `envelope.epoch` must eq
324
350
  `credentials`, `payment-details`, `moving-money`, `deleting-data`, `sending-messages`,
325
351
  `security-or-permission-settings`, `harness-permission-prompts`, `authority-rule-changes`.
326
352
  For `message` scope it also includes `install`, `publish`, `push-main`, `push-branch`, `hub-file`.
327
- For `approve` scope it also includes every action other than the signed one, and
328
- `other-artifact-version-digest-or-channel`.
353
+ For `approve` scope it also includes every action other than the signed one (`push-main` always, though it is not
354
+ signable), and `other-artifact-version-digest-or-channel`.
329
355
 
330
356
  **Attachments:** until `shared/turn-files-block.ts` exists, the verifier refuses any non-empty
331
357
  `envelope.attachments`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "7.0.29",
3
+ "version": "7.0.31",
4
4
  "description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,7 +6,10 @@
6
6
  * process machinery. Leaf module: no imports.
7
7
  */
8
8
  export const PROVIDER_QUOTA_RE = /usage[ _]limit|hit your (?:usage |session )?limit|session limit|rate.?limit|too many requests|\b429\b|\bquota\b|resets? (?:at|in) |over capacity|overloaded|\b529\b|plan limit/i
9
- export const PROVIDER_AUTH_RE = /not logged in|please (?:log ?in|sign in)|invalid api key|authentication|unauthori[sz]ed|\b401\b|\b403\b|login required|token (?:has )?expired|no credentials/i
9
+ export const PROVIDER_AUTH_RE = /not logged in|failed to authenticate|please (?:log ?in|sign in)|invalid api key|authentication|unauthori[sz]ed|\b401\b|\b403\b|login required|token (?:has )?expired|no credentials/i
10
+ // "Failed to authenticate" (7.0.30): claude -p says "Failed to authenticate: OAuth session expired and could not
11
+ // be refreshed" for an expired sign-in, which matched none of the words above, so the reason fell back to
12
+ // "provider process exited N". The four messages Claude Code documents are pinned in provider-failure-text.test.ts.
10
13
  export const PROVIDER_OVERFLOW_RE = /prompt is too long|context window|too many tokens|exceeds the (?:model|context)/i
11
14
 
12
15
  /** Codes a provider bridge raises when it has nothing better to say. Only these are upgraded. */
@@ -35,7 +35,7 @@ import {
35
35
 
36
36
  export const WEB_ENVELOPE_HEADER = 'x-cos-web-envelope'
37
37
  export const WEB_SIGNATURE_HEADER = 'x-cos-web-sig'
38
- /** Exact scoped install/publish only. Push-main remains refused (`approve_action_not_enabled`). */
38
+ /** Exact scoped install/publish only. Push-main is not expressible at all (APPROVE_ACTIONS, Miles 2026-11-12). */
39
39
  export const WEB_KEY_ENABLED_APPROVE_ACTIONS: ReadonlySet<string> = new Set(['install', 'publish'])
40
40
 
41
41
  /** The served-asset integrity state the authority reads (review W1). Null: integrity is not running. */
@@ -779,7 +779,7 @@ export class WebKeyPackets {
779
779
  * session that created its packet.
780
780
  */
781
781
  create(input: Record<string, unknown>, creator: { provider: unknown; sessionId: unknown } | null): { ok: true; packet: WebApprovalPacket; packetSha256: string } | { ok: false; reason: 'action_not_allowed' | 'invalid_packet' | 'packet_cap_reached' | 'session_from_context_only' | 'creator_unknown' } {
782
- // The closed action list: install, publish, push-main. Every never-list action is refused here.
782
+ // The closed action list: install and publish (push-main is never signable by phone, 2026-11-12). Every never-list action is refused here.
783
783
  if (typeof input.action !== 'string' || !APPROVE_ACTIONS.has(input.action)) return { ok: false, reason: 'action_not_allowed' }
784
784
  if ('provider' in input || 'sessionId' in input) return { ok: false, reason: 'session_from_context_only' }
785
785
  // Strict patterns plus the action-to-channel table (shared/web-turn-envelope.ts).
@@ -178,6 +178,7 @@ export const SIGNER_TLS_REASONS = Object.freeze([
178
178
  'stored_owner',
179
179
  'stored_writable',
180
180
  'stored_too_large',
181
+ 'stored_readable',
181
182
  ] as const)
182
183
 
183
184
  export type SignerTlsReason = typeof SIGNER_TLS_REASONS[number]
@@ -377,13 +378,22 @@ function firstLine(s: string): string {
377
378
  return (s.split('\n').find(l => l.trim()) ?? '').replace(/[^\x20-\x7e]/g, '').slice(0, 160)
378
379
  }
379
380
 
380
- type StoredRefusal = 'stored_symlink' | 'stored_not_file' | 'stored_owner' | 'stored_writable' | 'stored_too_large'
381
+ type StoredRefusal = 'stored_symlink' | 'stored_not_file' | 'stored_owner' | 'stored_writable' | 'stored_too_large' | 'stored_readable'
381
382
  type StoredRead = { kind: 'none' } | { kind: 'refused'; reason: StoredRefusal } | { kind: 'ok'; text: string }
382
383
 
383
- /** SR-08: the owner and mode rules shared by the folder and each file. */
384
- function ownerOrModeRefusal(uid: number, mode: number, ownerUid: number | null): StoredRefusal | null {
384
+ /**
385
+ * SR-08: the owner and mode rules shared by the folder and each file.
386
+ *
387
+ * `secret` (security review 2026-11-12, finding 5): the folder and the private key may carry no group or other
388
+ * bits at all, read and search included, so a key another user can read is never served. The certificate is
389
+ * public and the fetch state holds counts only, so there only write bits are refused. This server has written
390
+ * the folder 0700, the key 0600, the state 0600 and the certificate 0644 since the store was added (d4f3ee6,
391
+ * 2026-11-09), so no pair it wrote is refused by this rule.
392
+ */
393
+ function ownerOrModeRefusal(uid: number, mode: number, ownerUid: number | null, secret = false): StoredRefusal | null {
385
394
  if (ownerUid !== null && uid !== ownerUid) return 'stored_owner'
386
395
  if ((mode & 0o022) !== 0) return 'stored_writable'
396
+ if (secret && (mode & 0o077) !== 0) return 'stored_readable'
387
397
  return null
388
398
  }
389
399
 
@@ -396,7 +406,7 @@ function storedDirRefusal(dir: string, ownerUid: number | null): 'none' | Stored
396
406
  if (!st) return 'none'
397
407
  if (st.isSymbolicLink()) return 'stored_symlink'
398
408
  if (!st.isDirectory()) return 'stored_not_file'
399
- return ownerOrModeRefusal(st.uid, st.mode, ownerUid)
409
+ return ownerOrModeRefusal(st.uid, st.mode, ownerUid, true)
400
410
  }
401
411
 
402
412
  /**
@@ -404,7 +414,7 @@ function storedDirRefusal(dir: string, ownerUid: number | null): 'none' | Stored
404
414
  * (no swap between the check and the read): a regular file, owned by this server's user, not writable by
405
415
  * group or others, at most SIGNER_TLS_STORED_MAX_BYTES (read with that cap, never the whole file).
406
416
  */
407
- function readStoredFile(path: string, ownerUid: number | null): StoredRead {
417
+ function readStoredFile(path: string, ownerUid: number | null, secret = false): StoredRead {
408
418
  const pre = lstatSync(path, { throwIfNoEntry: false })
409
419
  if (!pre) return { kind: 'none' }
410
420
  if (pre.isSymbolicLink()) return { kind: 'refused', reason: 'stored_symlink' }
@@ -420,7 +430,7 @@ function readStoredFile(path: string, ownerUid: number | null): StoredRead {
420
430
  try {
421
431
  const st = fstatSync(fd)
422
432
  if (!st.isFile()) return { kind: 'refused', reason: 'stored_not_file' }
423
- const refusal = ownerOrModeRefusal(st.uid, st.mode, ownerUid)
433
+ const refusal = ownerOrModeRefusal(st.uid, st.mode, ownerUid, secret)
424
434
  if (refusal) return { kind: 'refused', reason: refusal }
425
435
  if (st.size > SIGNER_TLS_STORED_MAX_BYTES) return { kind: 'refused', reason: 'stored_too_large' }
426
436
  const buf = Buffer.alloc(SIGNER_TLS_STORED_MAX_BYTES + 1)
@@ -761,7 +771,7 @@ export class SignerTls {
761
771
  if (dir === 'none') { this.storedRefused = null; return }
762
772
  if (dir !== null) { this.storedRefused = dir; return }
763
773
  const cert = readStoredFile(join(this.deps.dir, SIGNER_TLS_CERT_FILE), this.ownerUid)
764
- const key = readStoredFile(join(this.deps.dir, SIGNER_TLS_KEY_FILE), this.ownerUid)
774
+ const key = readStoredFile(join(this.deps.dir, SIGNER_TLS_KEY_FILE), this.ownerUid, true)
765
775
  if (cert.kind === 'none' || key.kind === 'none') { this.storedRefused = null; return }
766
776
  if (cert.kind === 'refused') { this.storedRefused = cert.reason; return }
767
777
  if (key.kind === 'refused') { this.storedRefused = key.reason; return }
@@ -2,42 +2,42 @@
2
2
  "v": 1,
3
3
  "algorithm": "sha256",
4
4
  "files": {
5
- "/app.css": "1204f3b57d312c94e96a1430ba036292873ecbe132073c8a4f03e37063172a70",
6
- "/app.js": "1491f6dd957f47881e55a47caf98a3ce8fcb6df43a58b6e6877dd86807bebea6",
5
+ "/app.css": "bc5e29be841629261889aebfd439a3b126a50041c4e079c9866eac93a3552fe1",
6
+ "/app.js": "50ac6ccfcd483496a411ae6569217e5cf9574305642b628ec703aa4a6ee8b797",
7
7
  "/apple-touch-icon.png": "04ec7eca0bf6f53d89900d146ac32d4826759d6258bcdbd1ccdcc517f26f532c",
8
- "/control.css": "e7b287aa4acc1b3895a9ce61512fe449c555b6335a7145684fd78108ae008c75",
8
+ "/control.css": "f5b7a2e0f800cc07072581b7e817fcd2b54bdb1d91f4662a3cdc9d80a7bc32ff",
9
9
  "/fonts/DMSans.ttf": "8cd08d97e89c24d0aa92edd2f0f4c8ee6195eee9b7c9f154865a58b02f0c1c0d",
10
10
  "/fonts/Fraunces.ttf": "177ff6c0f14e5550a3c624247cd1189611d4eb65d000b14944c63d967958abbb",
11
11
  "/fonts/JetBrainsMono.ttf": "48715a42ec242c21e9f02692891e147d022299a52e48d5e413e1a942193ffeda",
12
- "/index.html": "8664ebf9ae7635c1fcf5fce333b89d272b6ce0df9529120523b99a3d49eb5ebb",
12
+ "/index.html": "c3a88479e5b9efa323129c5a425dada70dc7f0f27a0c787c1bc225c661f20a7b",
13
13
  "/mark.svg": "8e380d33d393027df192eb1afa08f15a380c2070c72d68f27311b49d9e94ca18",
14
14
  "/memories/d3.min.js": "f2094bbf6141b359722c4fe454eb6c4b0f0e42cc10cc7af921fc158fceb86539",
15
15
  "/memories/graph-explorer.css": "ec128de59811f45c71b6bededd737989309205e903951eb8211ade7664f87486",
16
16
  "/memories/graph-explorer.js": "98bde3df25f42022b098d047d2c8e7a22b972c1d817a18a24a0ab075d77871d3",
17
17
  "/memories/memory-workspace.js": "013f6c8c72c3474768f8c374edccde0e3656fc243e56528e258c3c29acca64f5",
18
18
  "/memory-review.js": "f27fe519f7ad7eccc3b44124fae724aa51e2eb3cb060d2f1795600e0e41b074f",
19
- "/panes/cos-chat.css": "29a5d22d0f02d0707e65711d151ad7c8ac0ab4bfeb30ab04b1f9d9fcf7d457b8",
20
- "/panes/cos-chat.js": "0005562525cab6353ef8fc002d7960b30a8bf9fc9a9a1059c0123c6cf343366e",
19
+ "/panes/cos-chat.css": "6edd123904869ff1d04093ffa8e45cf40863ac532054ce703eb44a59a2f395c4",
20
+ "/panes/cos-chat.js": "b94fca907500be21f777c5d5fa23b7415be48dcbd41d9fa30a4295ed1e3415d3",
21
21
  "/panes/dictation.js": "e9434b353a1a3ab3f21d4cef5b2d5f927a733f6af5f58285fda6a78472761f91",
22
- "/panes/meetings.css": "3ad4d0c5d59bd9f21c5ac9e234437cc0c81b381bd98ca4634b147efba5105a53",
23
- "/panes/meetings.js": "e3839cd7d52c98510ca232e41d3a336806d740b4a1381eab1a367d56a765f925",
22
+ "/panes/meetings.css": "635470e4d5f5665b046eab7382734c9772df569069c9b1eb10cfde6151014bfa",
23
+ "/panes/meetings.js": "e057f4dae697138111a4c07e8ec6c2a0502b5d93023cf491460bc7bf20172c85",
24
24
  "/panes/memories.css": "36731d6fd21ad1516dfeccf34fe5b2d3d267d21e40b47704055a0685334b579e",
25
- "/panes/memories.js": "28461f953ad0bf9d13287039594f2ce7ea29a46366e249240fa50f4b0f3b8c31",
26
- "/panes/messages.js": "be7a7b294f67a43fcb4cc518da987d2da2990484cb677ada810afa93af25a8a5",
25
+ "/panes/memories.js": "66341dd5126c40275979d1c40f8db72e1f8229ac9634a4bb72b70140db277f2a",
26
+ "/panes/messages.js": "7262ad82b6337a610b11ffff0677663e9317d1fe537f9e4161ae3af1ddef35ef",
27
27
  "/panes/preview.js": "79f1aecb27f48d584ed4cf37c7f4f423f0f9b45dd6c2fb24a9d88c72625d8680",
28
28
  "/panes/questions.css": "5c5a9b32ce32386b508ce326b17d40ecacbb0f2e049df7f4fe2a0009bd3b7908",
29
29
  "/panes/questions.js": "228e8cd0a9651a290cbb380db487af677a2fc7e93a64b1e33c9411cd8b013478",
30
30
  "/panes/reader-media.js": "4cf892a855aa2d0dce6978e69296cd49b8036d8fc62f12773b3fcadc210b2ef0",
31
31
  "/panes/sessions.css": "654e0509ab2b699b9ea92b6d9e2f7522146df1f0b6355915bef03caaf63addc4",
32
- "/panes/settings.js": "5c46c951fdcc2fc942486ec4b468b7e0bb0350a0a56a38d429948fc5147ab2f4",
32
+ "/panes/settings.js": "315458c5911f31592853800418dde6d87e9e61bbe96f90c77450c94f87d01951",
33
33
  "/panes/speakers.css": "49e70dae4266151b14b0057f2e43de169de225b8f3ac30343c81cf532735a487",
34
- "/panes/speakers.js": "8132c9244fb3965de802680840d2985978571988b9b4462b347d079330251bc5",
34
+ "/panes/speakers.js": "e4fbcc147c44f8bf798e0694e0c8b152d5cde82dc46823641c8e8d37714b8fba",
35
35
  "/panes/threads.js": "45fd7b56480be53225e91193cfa4c41a4ee7167890f10560e439bc7d2461747f",
36
36
  "/panes/today-actions.css": "c82f95b2aaf09802b15d47b24eccdb5c974afcab38ff5f861b3a8d8630b60869",
37
- "/panes/today-actions.js": "57ae8b378b1061060ae2a3f61d227d440d7806538085c1d5b310239c036e4bdc",
37
+ "/panes/today-actions.js": "c767b62b21a9f1debffd6e4ff4558891222b573535de1d78849b602030bb09bd",
38
38
  "/panes/work.css": "da50355dac690f712d1f6331f70ee5eadbf5982d37508648ae7786afc99ac7b1",
39
- "/panes/work.js": "1ef872b8c7e1a57bd7bd314e11eee7ac5fa9668871da7e644a1fbf5fd9c0d9e8",
40
- "/shell.js": "f1cc6a52c78e01ddba89ff27c6bd0e8f8bd8f29bdfcbb5150ee4ed66a8128d9d",
39
+ "/panes/work.js": "913c56084a6df463cc83358ce360da30d529466f1c583683fd3a4fd5eafe2d30",
40
+ "/shell.js": "cc7d0d771712607d4ba49eb1cfc83c50c7fcae2203f1c386778fee43d1803a81",
41
41
  "/theme.js": "e3e6422eaac43979d03149da114f5b83757b7361dd7b309cdf76be36a596044b",
42
42
  "/vendor/qrcode-encoder.js": "61d87cc7445cc66b862cff913de4571634746ea62bcd50d0f592be14f6d1ce46"
43
43
  }
@@ -206,8 +206,9 @@ select.field {
206
206
 
207
207
  /* No bottom bar, on purpose. On a phone the browser owns the bottom edge (Safari's floating
208
208
  address pill, Comet's and Chrome's own bars), and iOS 26 WebKit stops drawing the page under
209
- its bar while any fixed element sits within ~60px of it. Navigation lives in the top bar
210
- (folded screen) or the left rail (unfolded screen and up). */
209
+ its bar while any fixed element sits within ~60px of it. Navigation lives in the top bar (phones),
210
+ the side rail at the right edge (a short screen under 600 px: the folded Duo, html.short-screen),
211
+ or the left rail (600 px and up). */
211
212
  .badge-dot { position: absolute; width: 7px; height: 7px; border-radius: 50%; background: var(--hot); }
212
213
 
213
214
  .rail { display: none; }
@@ -702,8 +703,9 @@ html { touch-action: manipulation; }
702
703
  html.short-screen .rail-back:active { background: var(--rail-border); }
703
704
  /* The focus ring sits inside the button: the rail clips anything past its edge. */
704
705
  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. */
706
+ /* One way to switch panes on a screen: the rail. On a list the top bar keeps the pane's name, without the caret and
707
+ its menu (app.js makes the button inert); on a reader its Back moves to the rail and the left of the top bar is
708
+ empty. */
707
709
  html.short-screen .section-btn .caret, html.short-screen #topbar-back { display: none !important; }
708
710
  html.short-screen .section-btn { cursor: default; }
709
711
  }