@gotcos/glasses-server 6.98.1 → 6.99.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/package.json +2 -1
  3. package/server/index.ts +170 -11
  4. package/server/lib/api-auth.ts +31 -14
  5. package/server/lib/claude-bridge.ts +21 -2
  6. package/server/lib/codex-bridge.ts +21 -2
  7. package/server/lib/cos-thread.ts +582 -0
  8. package/server/lib/display-bus.ts +3 -0
  9. package/server/lib/drop-attachments.ts +55 -0
  10. package/server/lib/drop-destinations.ts +134 -0
  11. package/server/lib/drop-history-search.ts +97 -0
  12. package/server/lib/drop-p2-boot.ts +60 -0
  13. package/server/lib/drop-p2-runtime.ts +237 -0
  14. package/server/lib/drop-p2-settings.ts +174 -0
  15. package/server/lib/drop-resolution.ts +168 -0
  16. package/server/lib/drop-router.ts +325 -0
  17. package/server/lib/drop-service.ts +830 -0
  18. package/server/lib/drop-settings.ts +56 -0
  19. package/server/lib/drop-store.ts +450 -0
  20. package/server/lib/drop-when.ts +265 -0
  21. package/server/lib/glasses-pairing.ts +15 -0
  22. package/server/lib/learning-needs-you.ts +111 -0
  23. package/server/lib/live-cues-v2-settings.ts +2 -1
  24. package/server/lib/live-cues-v2.ts +6 -4
  25. package/server/lib/media-store.ts +80 -0
  26. package/server/lib/meeting-batch-transcribe.ts +15 -2
  27. package/server/lib/meeting-consolidation-run.ts +169 -0
  28. package/server/lib/meeting-store.ts +61 -0
  29. package/server/lib/meeting-title-steer.ts +200 -0
  30. package/server/lib/network-policy.ts +84 -6
  31. package/server/lib/permission-broker.ts +13 -0
  32. package/server/lib/phone-recording.ts +1 -1
  33. package/server/lib/ping-budget.ts +149 -0
  34. package/server/lib/ping-engine.ts +245 -0
  35. package/server/lib/ping-feed-sink.ts +33 -0
  36. package/server/lib/ping-sources.ts +192 -0
  37. package/server/lib/query-job-coordinator.ts +5 -2
  38. package/server/lib/query-job-runtime.ts +45 -4
  39. package/server/lib/query-job-types.ts +10 -1
  40. package/server/lib/reminder-scheduler.ts +153 -0
  41. package/server/lib/tailnet-self-name.ts +103 -0
  42. package/server/lib/tailscale-nudge.ts +40 -2
  43. package/server/lib/task-store.ts +32 -0
  44. package/server/lib/web-ask-admission.ts +56 -0
  45. package/server/lib/web-live-chunks.ts +273 -0
  46. package/server/lib/web-memories-bridge.ts +323 -0
  47. package/server/lib/web-session-auth.ts +492 -0
  48. package/server/lib/web-usage-bridge.ts +154 -0
  49. package/server/lib/work-search.ts +14 -1
  50. package/server/lib/your-move-count.ts +58 -0
  51. package/server/routes/cos-thread.ts +79 -0
  52. package/server/routes/display.ts +11 -1
  53. package/server/routes/drops.ts +262 -0
  54. package/server/routes/health.ts +16 -0
  55. package/server/routes/live-cues.ts +5 -4
  56. package/server/routes/meeting.ts +61 -5
  57. package/server/routes/permission-broker.ts +23 -3
  58. package/server/routes/transcribe-stream.ts +43 -4
  59. package/server/routes/web-control.ts +569 -0
  60. package/server/routes/welcome-context.ts +2 -1
  61. package/server/scripts/web-live-cls/cdp.mjs +57 -0
  62. package/server/scripts/web-live-cls/measure.mjs +80 -0
  63. package/server/scripts/web-live-cls/mock.mjs +97 -0
  64. package/server/scripts/web-live-cls/render.mjs +26 -0
  65. package/server/web-control/app.css +519 -0
  66. package/server/web-control/app.js +3643 -0
  67. package/server/web-control/control.css +325 -0
  68. package/server/web-control/fonts/DMSans.ttf +0 -0
  69. package/server/web-control/fonts/Fraunces.ttf +0 -0
  70. package/server/web-control/fonts/JetBrainsMono.ttf +0 -0
  71. package/server/web-control/index.html +275 -0
  72. package/server/web-control/mark.svg +1 -0
  73. package/server/web-control/memories/d3.min.js +2 -0
  74. package/server/web-control/memories/graph-explorer.css +200 -0
  75. package/server/web-control/memories/graph-explorer.js +748 -0
  76. package/server/web-control/memories/memories-app.js +1561 -0
  77. package/server/web-control/memories/memories-theme.css +73 -0
  78. package/server/web-control/memories/memories.html +72 -0
  79. package/server/web-control/memories/memory-stewardship.js +63 -0
  80. package/server/web-control/memories/memory-workspace.js +216 -0
  81. package/server/web-control/panes/cos-chat.css +174 -0
  82. package/server/web-control/panes/cos-chat.js +1118 -0
  83. package/server/web-control/panes/dictation.js +173 -0
  84. package/server/web-control/panes/meetings.css +314 -0
  85. package/server/web-control/panes/meetings.js +2568 -0
  86. package/server/web-control/panes/messages.js +718 -0
  87. package/server/web-control/panes/questions.css +32 -0
  88. package/server/web-control/panes/questions.js +275 -0
  89. package/server/web-control/panes/sessions.css +452 -0
  90. package/server/web-control/panes/settings.js +292 -0
  91. package/server/web-control/panes/speakers.css +269 -0
  92. package/server/web-control/panes/speakers.js +2755 -0
  93. package/server/web-control/panes/threads.js +200 -0
  94. package/server/web-control/panes/work.css +261 -0
  95. package/server/web-control/panes/work.js +1667 -0
  96. package/server/web-control/shell.js +294 -0
  97. package/server/web-control/theme.js +25 -0
  98. package/server/web-control/vendor/qrcode-encoder.js +10 -0
@@ -1,6 +1,7 @@
1
1
  import type { RequestHandler } from 'express'
2
2
  import { timingSafeTokenEqual } from './token-auth.js'
3
3
  import { PAIRING_NONCE_BODY } from './glasses-pairing.js'
4
+ import { isWebSessionAuthorized } from './web-session-auth.js'
4
5
 
5
6
  // Recovery/setup clients need these availability surfaces before they have a
6
7
  // usable token. Keep private provider state and every mutation route out.
@@ -47,6 +48,11 @@ export function isPublicApiRequest(method: string, path: string): boolean {
47
48
  if (PUBLIC_API_PATHS.has(path)) return true
48
49
  if (method === 'POST' && path === '/pairing/claim') return true
49
50
  if (method === 'GET' && PAIRING_POLL_PATH.test(path)) return true
51
+ // COS Control Web (browser on the tailnet). Login takes the token or a one-time handoff
52
+ // ticket and answers with the HttpOnly cookie; it is rate limited in the route. Status
53
+ // says only whether this browser is signed in. Everything else stays behind the gate.
54
+ if (method === 'POST' && (path === '/web-session/login' || path === '/web-session/logout')) return true
55
+ if (method === 'GET' && path === '/web-session/status') return true
50
56
  if (method !== 'GET' && method !== 'HEAD') return false
51
57
  return TTS_PLAYBACK_CAPABILITY_PATH.test(path)
52
58
  || DISPLAY_STREAM_CAPABILITY_PATH.test(path)
@@ -54,29 +60,40 @@ export function isPublicApiRequest(method: string, path: string): boolean {
54
60
 
55
61
  export interface RequireApiTokenOptions {
56
62
  /**
57
- * Called once per request that passed the token check (never for a public path).
58
- * Pairing uses it to stamp `firstAuthAfterClaimAt`. A throw here is swallowed: an
59
- * observer must never turn an authenticated request into a failure.
63
+ * Called once per request that passed the X-COS-Token header check (never for a
64
+ * public path and never for a browser cookie session so web login cannot disturb
65
+ * the glasses pairing state machine).
60
66
  */
61
67
  onAuthenticated?: (req: Parameters<RequestHandler>[0]) => void
68
+ /** Verified Tailscale `Self.DNSName` for validating browser cookie Origins. */
69
+ selfDnsName?: () => string | null
70
+ /** Allowed browser-facing server ports (e.g. `[3141, 3143]`) for cookie Origin port binding. */
71
+ allowedPorts?: () => readonly number[]
62
72
  }
63
73
 
64
74
  /** Global /api authentication boundary. Mount before all body parsers. */
65
75
  export function requireApiToken(apiToken: string, options: RequireApiTokenOptions = {}): RequestHandler {
66
76
  return (req, res, next) => {
67
77
  if (isPublicApiRequest(req.method, req.path)) return next()
68
- if (!timingSafeTokenEqual(req.headers['x-cos-token'], apiToken)) {
69
- return res.status(401).json({
70
- error: 'unauthorized',
71
- // Stable on purpose: clients key off this value, not the message.
72
- reason: 'pairing_token_rejected',
73
- // Older COS Glasses builds, which cannot scan, show this message word for word.
74
- message: 'Paste the pairing token from COS Control, or scan its code with COS Glasses 6.10.621 or newer.',
75
- })
78
+ if (timingSafeTokenEqual(req.headers['x-cos-token'], apiToken)) {
79
+ if (options.onAuthenticated) {
80
+ try { options.onAuthenticated(req) } catch { /* observer only */ }
81
+ }
82
+ return next()
76
83
  }
77
- if (options.onAuthenticated) {
78
- try { options.onAuthenticated(req) } catch { /* observer only */ }
84
+ const cookieAuth = isWebSessionAuthorized(req, apiToken, {
85
+ selfDnsName: options.selfDnsName?.() ?? null,
86
+ allowedPorts: options.allowedPorts?.(),
87
+ })
88
+ if (cookieAuth) {
89
+ return next()
79
90
  }
80
- next()
91
+ return res.status(401).json({
92
+ error: 'unauthorized',
93
+ // Stable on purpose: clients key off this value, not the message.
94
+ reason: 'pairing_token_rejected',
95
+ // Older COS Glasses builds, which cannot scan, show this message word for word.
96
+ message: 'Paste the pairing token from COS Control, or scan its code with COS Glasses 6.10.621 or newer.',
97
+ })
81
98
  }
82
99
  }
@@ -418,6 +418,8 @@ const PHASE_LABELS: Record<Phase, string> = {
418
418
  * Returns the session ID for multi-turn tracking.
419
419
  */
420
420
  export interface CallOptions {
421
+ /** Internal checkpoint runner only; timeout is never a successful answer. */
422
+ maintenancePass?: true
421
423
  lightweight?: boolean // Skip async context fetch — use cached context instantly (G2 speed path)
422
424
  abortSignal?: AbortSignal
423
425
  effort?: EffortPreference
@@ -700,7 +702,7 @@ export async function callClaudeStreaming(
700
702
  const effortWallMax = resolvedEffort === 'max' || resolvedEffort === 'ultracode'
701
703
  ? Math.max(defaultWallMax, WALL_MAX_DEEP_EFFORT_MS)
702
704
  : defaultWallMax
703
- const wallMax = isExtendedQuery(query) ? Math.max(WALL_MAX_EXTENDED_MS, effortWallMax) : effortWallMax
705
+ const wallMax = options?.maintenancePass ? 900_000 : isExtendedQuery(query) ? Math.max(WALL_MAX_EXTENDED_MS, effortWallMax) : effortWallMax
704
706
  // Key lanes: one nonce per turn from the relay (P2 contract), then the child env points at it.
705
707
  // Only the child's copy is written. A relay that cannot issue ends the turn before spawn.
706
708
  let keyTurn: RelayTurnHandle | null = null
@@ -850,8 +852,16 @@ export async function callClaudeStreaming(
850
852
  })
851
853
  }
852
854
 
855
+ let maintenanceClosed = false
853
856
  async function finalize(text: string) {
854
857
  if (finalized) return
858
+ // A next maintenance pass may start only after the entire provider tree
859
+ // has closed. Normal chat retains its existing result-event behavior.
860
+ if (options?.maintenancePass && !maintenanceClosed) {
861
+ if (terminationRequested) return
862
+ await terminateForTerminal('maintenance checkpoint', () => finalize(text))
863
+ return
864
+ }
855
865
  const responseAuthFailure = terminalProviderAuthFailure('claude', text)
856
866
  const authFailure = responseAuthFailure
857
867
  ?? (!text.trim() ? terminalTextError ?? terminalProviderAuthFailure('claude', stderr) : null)
@@ -1010,6 +1020,11 @@ export async function callClaudeStreaming(
1010
1020
  status: Exclude<ClaudeRunStatus, 'running'> = 'failed',
1011
1021
  ) {
1012
1022
  if (finalized) return
1023
+ if (options?.maintenancePass && !maintenanceClosed) {
1024
+ if (terminationRequested) return
1025
+ await terminateForTerminal('maintenance error', () => finalizeError(msg, exitCode, status))
1026
+ return
1027
+ }
1013
1028
  finalized = true
1014
1029
  cleanup()
1015
1030
  armSteerReaper()
@@ -1041,6 +1056,7 @@ export async function callClaudeStreaming(
1041
1056
  console.error(`[claude-bridge] provider did not close after SIGKILL (${reason}); retaining lifecycle ownership`)
1042
1057
  return
1043
1058
  }
1059
+ maintenanceClosed = true
1044
1060
  await onClosed(result)
1045
1061
  }
1046
1062
 
@@ -1117,6 +1133,7 @@ export async function callClaudeStreaming(
1117
1133
 
1118
1134
  const wallTimer = setTimeout(() => {
1119
1135
  void terminateForTerminal('wall timeout', result => {
1136
+ if (options?.maintenancePass) return finalizeError('maintenance_pass_timeout', result.code)
1120
1137
  // 6.78.0 steer: a steered run past its first whole answer completes with that answer.
1121
1138
  if (steerCheckpoint !== null) return finalize(steerCheckpoint)
1122
1139
  if (fullText) {
@@ -1338,7 +1355,9 @@ export async function callClaudeStreaming(
1338
1355
  } catch { /* ignore */ }
1339
1356
  }
1340
1357
 
1341
- if (fullText) {
1358
+ if (options?.maintenancePass) {
1359
+ void finalizeError('maintenance_pass_incomplete_exit', code)
1360
+ } else if (fullText) {
1342
1361
  // If we got text but no explicit result event, still finalize. A sole,
1343
1362
  // machine-shaped auth response is classified inside finalize.
1344
1363
  void finalize(fullText)
@@ -722,8 +722,16 @@ export async function callCodexStreaming(
722
722
  callbacks.onChunk(text)
723
723
  }
724
724
 
725
+ let maintenanceClosed = false
725
726
  async function finalize(text: string) {
726
727
  if (finalized) return
728
+ // A next maintenance pass may start only after the entire provider tree
729
+ // has closed. Normal chat retains its existing result-event behavior.
730
+ if (options?.maintenancePass && !maintenanceClosed) {
731
+ if (terminationRequested) return
732
+ await terminateForTerminal('maintenance checkpoint', () => finalize(text))
733
+ return
734
+ }
727
735
  const responseAuthFailure = terminalProviderAuthFailure('codex', text)
728
736
  const authFailure = responseAuthFailure
729
737
  ?? (!text.trim() ? terminalTextError ?? terminalProviderAuthFailure('codex', stderr) : null)
@@ -861,6 +869,11 @@ export async function callCodexStreaming(
861
869
 
862
870
  async function finalizeError(msg: string, exitCode?: number | null, status: Exclude<CodexRunStatus, 'running'> = 'failed') {
863
871
  if (finalized) return
872
+ if (options?.maintenancePass && !maintenanceClosed) {
873
+ if (terminationRequested) return
874
+ await terminateForTerminal('maintenance error', () => finalizeError(msg, exitCode, status))
875
+ return
876
+ }
864
877
  finalized = true
865
878
  // Canary R14: a Codex child keeps retrying after the bridge ends the turn; its nonce dies now.
866
879
  keyTurn?.revoke('cancel')
@@ -876,7 +889,7 @@ export async function callCodexStreaming(
876
889
  exitCode,
877
890
  })
878
891
  try {
879
- await callbacks.onError(safeCodexUserError(msg))
892
+ await callbacks.onError(options?.maintenancePass && msg === 'maintenance_pass_timeout' ? msg : safeCodexUserError(msg))
880
893
  } catch (error) {
881
894
  console.error('[codex-bridge] terminal error callback failed:', error)
882
895
  }
@@ -910,6 +923,7 @@ export async function callCodexStreaming(
910
923
  console.error(`[codex-bridge] provider did not close after SIGKILL (${reason}); retaining lifecycle ownership`)
911
924
  return
912
925
  }
926
+ maintenanceClosed = true
913
927
  await onClosed(result)
914
928
  }
915
929
 
@@ -949,6 +963,7 @@ export async function callCodexStreaming(
949
963
 
950
964
  const wallTimer = setTimeout(() => {
951
965
  void terminateForTerminal('wall timeout', result => {
966
+ if (options?.maintenancePass) return finalizeError('maintenance_pass_timeout', result.code)
952
967
  if (fullText) return finalize(fullText)
953
968
  return finalizeError(`Wall clock limit reached (${WALL_MAX_MS / 1000}s). Codex process killed.`, result.code)
954
969
  })
@@ -1046,7 +1061,11 @@ export async function callCodexStreaming(
1046
1061
  try { handleEvent(JSON.parse(buffer.trim())) } catch { /* ignore */ }
1047
1062
  }
1048
1063
  runUsage.settle()
1049
- if (finalized) return
1064
+ if (finalized || terminationRequested) return
1065
+ if (options?.maintenancePass) {
1066
+ finalizeError('maintenance_pass_incomplete_exit', code)
1067
+ return
1068
+ }
1050
1069
  if (fullText) {
1051
1070
  void finalize(fullText)
1052
1071
  return