@gotcos/glasses-server 7.0.7 → 7.0.27

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 (97) hide show
  1. package/.env.example +19 -0
  2. package/CHANGELOG.md +305 -0
  3. package/docs/web-key-verifier-contract.md +378 -0
  4. package/managed-runtime-contract.json +9 -1
  5. package/package.json +10 -3
  6. package/server/bootstrap.ts +10 -0
  7. package/server/index.ts +129 -23
  8. package/server/lib/agent-session-older.ts +218 -0
  9. package/server/lib/agent-session-recent.ts +321 -0
  10. package/server/lib/agent-session-store.ts +102 -20
  11. package/server/lib/api-auth.ts +37 -1
  12. package/server/lib/calendar-meetings.ts +70 -0
  13. package/server/lib/calendar-source.ts +54 -9
  14. package/server/lib/claude-hooks-installer.ts +111 -12
  15. package/server/lib/conversation.ts +56 -28
  16. package/server/lib/cors-reject-log.ts +51 -0
  17. package/server/lib/cos-started-sessions.ts +101 -0
  18. package/server/lib/disk-io-meter.ts +202 -0
  19. package/server/lib/drop-service.ts +47 -12
  20. package/server/lib/drop-store.ts +21 -1
  21. package/server/lib/event-loop-stall.ts +62 -0
  22. package/server/lib/full-hold-engine.ts +114 -0
  23. package/server/lib/full-hold.ts +124 -0
  24. package/server/lib/g2-turn-provenance.ts +128 -9
  25. package/server/lib/list-model-memo.ts +85 -0
  26. package/server/lib/listener-startup.ts +18 -0
  27. package/server/lib/meeting-batch-transcribe.ts +32 -5
  28. package/server/lib/model-efforts.ts +40 -0
  29. package/server/lib/permission-broker.ts +443 -11
  30. package/server/lib/secret-path-guard.ts +4 -1
  31. package/server/lib/session-cache-writer.ts +8 -1
  32. package/server/lib/session-hook-events.ts +23 -1
  33. package/server/lib/session-hooks-runtime.ts +34 -0
  34. package/server/lib/session-log.ts +313 -4
  35. package/server/lib/session-organizer.ts +305 -0
  36. package/server/lib/session-state-derive.ts +36 -0
  37. package/server/lib/session-stream-events.ts +39 -3
  38. package/server/lib/thread-turn-queue.ts +24 -0
  39. package/server/lib/today-brief.ts +522 -58
  40. package/server/lib/turn-end-ask.ts +320 -0
  41. package/server/lib/turn-footer.ts +3 -3
  42. package/server/lib/web-control-integrity.ts +160 -0
  43. package/server/lib/web-control-offnet-page.ts +58 -0
  44. package/server/lib/web-control-static.ts +53 -9
  45. package/server/lib/web-key-authority.ts +372 -0
  46. package/server/lib/web-key-envelope.ts +98 -0
  47. package/server/lib/web-key-handoff.ts +135 -0
  48. package/server/lib/web-key-origin.ts +21 -0
  49. package/server/lib/web-key-passkey.ts +155 -0
  50. package/server/lib/web-key-registry.ts +825 -0
  51. package/server/lib/web-key-signer-app.ts +152 -0
  52. package/server/lib/web-session-auth.ts +133 -15
  53. package/server/lib/whisper-local.ts +93 -7
  54. package/server/lib/whisper-preview.ts +5 -3
  55. package/server/lib/work-orchestrator/snapshot.ts +17 -3
  56. package/server/lib/work-review-runtime.ts +17 -4
  57. package/server/routes/agent-session-bindings.ts +16 -1
  58. package/server/routes/agent-session-stream.ts +7 -0
  59. package/server/routes/agent-sessions.ts +106 -2
  60. package/server/routes/drops.ts +7 -2
  61. package/server/routes/g2-turns.ts +35 -0
  62. package/server/routes/health.ts +14 -1
  63. package/server/routes/live-events.ts +6 -1
  64. package/server/routes/permission-broker.ts +26 -0
  65. package/server/routes/query-jobs.ts +3 -0
  66. package/server/routes/session-organizer.ts +149 -0
  67. package/server/routes/sessions.ts +15 -24
  68. package/server/routes/thread-turn-queue.ts +50 -14
  69. package/server/routes/today-brief.ts +91 -36
  70. package/server/routes/web-control.ts +37 -4
  71. package/server/routes/web-key.ts +284 -0
  72. package/server/routes/welcome-context.ts +6 -2
  73. package/server/routes/work-orchestrator.ts +12 -7
  74. package/server/scripts/write-web-control-manifest.mjs +43 -0
  75. package/server/web-control/.manifest.json +44 -0
  76. package/server/web-control/app.css +45 -0
  77. package/server/web-control/app.js +1680 -108
  78. package/server/web-control/apple-touch-icon.png +0 -0
  79. package/server/web-control/index.html +54 -17
  80. package/server/web-control/panes/cos-chat.css +52 -6
  81. package/server/web-control/panes/cos-chat.js +364 -32
  82. package/server/web-control/panes/questions.js +135 -14
  83. package/server/web-control/panes/sessions.css +280 -0
  84. package/server/web-control/panes/settings.js +1 -1
  85. package/server/web-control/panes/today-actions.css +35 -0
  86. package/server/web-control/panes/today-actions.js +121 -6
  87. package/server/web-control/panes/work.css +3 -0
  88. package/server/web-control/panes/work.js +185 -38
  89. package/server/web-control/theme.js +33 -0
  90. package/server/web-signer/.manifest.json +11 -0
  91. package/server/web-signer/index.html +74 -0
  92. package/server/web-signer/signer-proof.js +348 -0
  93. package/server/web-signer/signer-store.js +39 -0
  94. package/server/web-signer/signer.css +23 -0
  95. package/server/web-signer/signer.js +341 -0
  96. package/shared/web-approval-packets.golden.json +124 -0
  97. package/shared/web-turn-envelope.ts +271 -0
@@ -0,0 +1,341 @@
1
+ // The phone key signer page on https://<this Mac's MagicDNS name>:3144 (6.99.31, plan Phase 3, review
2
+ // B2 and W2). It holds the key and nothing else: no transcripts, no previews, no other app data.
3
+ //
4
+ // What it never does:
5
+ // - run inside a frame (CSP frame-ancestors 'none' plus the check below, before anything is read)
6
+ // - sign because another page asked: there is no postMessage listener, the words come from the Mac
7
+ // by a one-use handoff id, and a signature happens only after a real tap (event.isTrusted) here
8
+ // - take its origin from anyone: the envelope names location.origin, so a copy of this page served
9
+ // from any other origin can never produce a signature this server accepts
10
+ //
11
+ // The proof itself is assembled by signer-proof.js (one module, the seam a companion app signer will
12
+ // reuse). This file is the page: storage, WebAuthn, the screen, the network.
13
+ (function (W) {
14
+ 'use strict'
15
+ var P = W.COSSignerProof, S = W.COSSignerStore, D = W.document
16
+ var SECTIONS = ['loading', 'framed', 'enroll', 'pending', 'sign', 'done', 'idle', 'problem']
17
+ var POLL_MS = 2000
18
+ var state = { record: null, handoff: null, shownWords: null, nonce: null, enrolling: null, busy: false, revokeArmed: false }
19
+
20
+ var COPY = {
21
+ device_not_allowed: 'This phone is not allowed to set up a key. Add its Tailscale address to COS_WEB_KEY_DEVICES on the Mac, then try again.',
22
+ enrollment_pending: 'Another phone is setting up a key right now. Wait five minutes or finish that one first.',
23
+ key_cap_reached: 'Two phones already have keys. Revoke one in COS Control first.',
24
+ anchor_not_registered: 'COS Control on the Mac has not set up its approval key yet.',
25
+ anchor_unregistered: 'COS Control on the Mac has not set up its approval key yet.',
26
+ web_key_authority_off: 'Phone approval is turned off on the Mac.',
27
+ web_origin_required: 'Open this page from your phone over Tailscale, signed in to COS Control Web.',
28
+ signer_origin_required: 'Open this page from the approval address on port 3144.',
29
+ unauthorized: 'This browser is not signed in to COS Control Web. Sign in there first.',
30
+ handoff_unknown: 'This approval link is not known. Start again from Control.',
31
+ handoff_expired: 'This approval link has expired. Start again from Control.',
32
+ handoff_claimed: 'This approval link was already opened. Start again from Control.',
33
+ commit_mismatch: 'The setup check failed. Start again.',
34
+ enroll_burned: 'Setup failed three times and was stopped. Start again.',
35
+ enroll_unknown: 'That setup request has ended. Start again.',
36
+ passkey_invalid: 'Your passkey did not confirm the setup. Start again.',
37
+ passkey_malformed: 'The passkey could not be read. Start again.',
38
+ passkey_cancelled: 'Passkey confirmation was cancelled. Nothing was signed.',
39
+ packet_mismatch: 'The release request does not match its signed scope. Nothing was signed. Return to Control and open the request again.',
40
+ packet_expired: 'The release request expired. Ask the session for a fresh request. Nothing was signed.',
41
+ nonce_store_unavailable: 'The Mac cannot issue one-time codes right now. Nothing was signed.',
42
+ }
43
+ function reasonCopy(reason) { return COPY[reason] || ('COS said no (' + String(reason || 'unknown') + '). Nothing was signed.') }
44
+ function refusal(reason) { var e = new Error(reason); e.reason = reason; return e }
45
+
46
+ function $(id) { return D.getElementById(id) }
47
+ function show(id) { SECTIONS.forEach(function (s) { var el = $(s); if (el) el.hidden = s !== id }) }
48
+ function setText(id, s) { var el = $(id); if (el) el.textContent = s == null ? '' : String(s) }
49
+ function problem(copy) { setText('problem-text', copy); show('problem') }
50
+ function framed() { try { return W.top !== W.self } catch (_e) { return true } }
51
+ function spacedCode(code) { return String(code).slice(0, 3) + ' ' + String(code).slice(3) }
52
+ function controlUrl() { return 'https://' + W.location.hostname + ':3143/control/' }
53
+
54
+ function api(path, opts) {
55
+ var o = opts || {}
56
+ var headers = { 'X-COS-Web': '1' }
57
+ if (o.body !== undefined) headers['Content-Type'] = 'application/json'
58
+ if (o.headers) Object.keys(o.headers).forEach(function (k) { headers[k] = o.headers[k] })
59
+ return W.fetch(path, {
60
+ method: o.method || 'GET', headers: headers, credentials: 'same-origin', cache: 'no-store', redirect: 'error',
61
+ body: o.body !== undefined ? JSON.stringify(o.body) : undefined,
62
+ }).then(function (res) {
63
+ return res.text().then(function (t) { var b = {}; try { b = t ? JSON.parse(t) : {} } catch (_e) { b = {} } return { status: res.status, body: b || {} } })
64
+ })
65
+ }
66
+
67
+ // ── the page's signer: the WebCrypto key in IndexedDB plus the passkey (signer-proof.js interface) ──
68
+ function assertWith(credentialId, challenge) {
69
+ return W.navigator.credentials.get({
70
+ publicKey: {
71
+ challenge: challenge, rpId: W.location.hostname, userVerification: 'required', timeout: 120000,
72
+ allowCredentials: [{ type: 'public-key', id: P.fromB64url(credentialId) }],
73
+ },
74
+ }).then(function (cred) {
75
+ if (!cred || !cred.response) throw refusal('passkey_cancelled')
76
+ return {
77
+ credentialId: P.b64url(cred.rawId),
78
+ authenticatorData: P.b64url(cred.response.authenticatorData),
79
+ clientDataJSON: P.b64url(cred.response.clientDataJSON),
80
+ signature: P.b64url(cred.response.signature),
81
+ }
82
+ }, function () { throw refusal('passkey_cancelled') })
83
+ }
84
+ function pageSigner(record) {
85
+ return {
86
+ kid: record.kid,
87
+ signEnvelope: function (bytes) {
88
+ return W.crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, record.keyPair.privateKey, bytes).then(function (b) { return new Uint8Array(b) })
89
+ },
90
+ assert: function (challenge) { return assertWith(record.credentialId, challenge) },
91
+ }
92
+ }
93
+
94
+ // ── boot ──
95
+ function handoffIdFromHash() {
96
+ var m = /^#h=([A-Za-z0-9_-]{16,64})$/.exec(String(W.location.hash || ''))
97
+ return m ? m[1] : null
98
+ }
99
+ function paintKey(rec) {
100
+ setText('key-label', rec.label || 'This phone')
101
+ setText('key-fingerprint', rec.fingerprint || '')
102
+ }
103
+ function boot() {
104
+ // Framed: stop before the key store is opened or the handoff is claimed.
105
+ if (framed()) { show('framed'); return Promise.resolve() }
106
+ var id = handoffIdFromHash()
107
+ // The id is single use; drop it from the address so a reload or a shared screenshot cannot reuse it.
108
+ try { if (W.location.hash) W.history.replaceState(null, '', W.location.pathname) } catch (_e) { /* not fatal */ }
109
+ return S.get().then(function (rec) {
110
+ state.record = rec
111
+ if (!rec) { show('enroll'); return undefined }
112
+ paintKey(rec)
113
+ if (rec.state === 'pending') return watchEnrollment(rec)
114
+ if (!id) { show('idle'); return undefined }
115
+ return claim(id)
116
+ }).catch(function (err) { problem(reasonCopy(err && err.reason ? err.reason : 'storage_unavailable')) })
117
+ }
118
+
119
+ // ── per-turn signing ──
120
+ function providerName(p) { return p === 'claude' ? 'Claude' : p === 'codex' ? 'Codex' : p === 'cursor' ? 'Cursor' : String(p) }
121
+ function scopeLine(scope) {
122
+ if (!scope || scope.kind === 'message') return 'Message only. It approves no install, publish or push.'
123
+ if (scope.kind === 'approve') return 'Approve ' + scope.action + ': ' + scope.artifact + ' ' + scope.version + ' on ' + scope.channel + ' (' + scope.digest + ')'
124
+ return 'Unknown'
125
+ }
126
+ function deploymentMeaning(scope) {
127
+ var targets = { 'gotcos-site': 'gotcos.com website (cos-starter Pages)', 'control-downloads': 'COS Control GitHub release downloads and approved latest aliases', 'release-docs': 'Public COS release documentation' }
128
+ return scope && Object.prototype.hasOwnProperty.call(targets, scope.channel) ? '\nDeployment target: ' + targets[scope.channel] + '\nApproval is limited to this exact manifest SHA-256. The receiving release tool checks its files and destination.' : ''
129
+ }
130
+ function claim(id) {
131
+ show('loading')
132
+ return api('/api/web-key/handoff/claim', { method: 'POST', body: { handoffId: id } }).then(function (a) {
133
+ if (a.status !== 200 || a.body.ok !== true || !a.body.handoff) return problem(reasonCopy(a.body.error || 'handoff_unknown'))
134
+ return render(a.body.handoff)
135
+ })
136
+ }
137
+ function render(h) {
138
+ return P.validateHandoffPacket(h, Date.now()).then(function () { return renderChecked(h) }, function (err) {
139
+ return problem(reasonCopy(err && err.reason ? err.reason : 'packet_mismatch'))
140
+ })
141
+ }
142
+ function renderChecked(h) {
143
+ var words = P.signedWords(h.words)
144
+ if (!words.trim()) return problem('There is nothing to approve: the message is empty.')
145
+ if (h.attachments && h.attachments.length) return problem('Files cannot be approved on this phone yet. Nothing was signed.')
146
+ state.handoff = h
147
+ state.shownWords = words
148
+ setText('sign-words', words)
149
+ setText('sign-provider', providerName(h.provider))
150
+ setText('sign-session', h.sessionId)
151
+ setText('sign-scope', scopeLine(h.scope))
152
+ state.shownPacket = h.packet ? P.canonicalJson(h.packet) + '\nPacket SHA-256: ' + h.scope.packetSha256 + '\nExpires: ' + new Date(h.packet.exp).toISOString() + deploymentMeaning(h.scope) : ''
153
+ setText('sign-packet', state.shownPacket)
154
+ var packetBox = $('sign-packet'); if (packetBox) packetBox.hidden = !state.shownPacket
155
+ setText('sign-status', '')
156
+ show('sign')
157
+ // Fetched now, so the tap goes straight to signing and the phone passkey with no network wait in between
158
+ // (Safari allows a passkey prompt only close to the tap). A nonce is single use and harmless unused.
159
+ state.nonce = api('/api/web-key/nonce', { method: 'POST', body: {} }).then(null, function () { return { status: 0, body: {} } })
160
+ return state.nonce
161
+ }
162
+ function onApprove(ev) {
163
+ // Only a real tap on this page signs. A script-made click (isTrusted false) does nothing.
164
+ if (!ev || ev.isTrusted !== true) return undefined
165
+ if (state.busy || !state.handoff || !state.record || state.record.state !== 'enrolled') return undefined
166
+ var h = state.handoff, rec = state.record
167
+ // The words signed are the words on the screen, read back from it.
168
+ var shown = $('sign-words').textContent
169
+ if (shown !== state.shownWords) { problem('The words on screen changed. Nothing was signed.'); return undefined }
170
+ if (($('sign-packet') ? $('sign-packet').textContent : '') !== state.shownPacket) { problem('The release request on screen changed. Nothing was signed.'); return undefined }
171
+ state.busy = true
172
+ var btn = $('sign-approve'); if (btn) btn.disabled = true
173
+ var nonce = state.nonce || api('/api/web-key/nonce', { method: 'POST', body: {} })
174
+ state.nonce = null
175
+ return nonce.then(function (n) {
176
+ if (n.status !== 201 || n.body.ok !== true) throw refusal(n.body.error || 'nonce_failed')
177
+ setText('sign-status', 'Confirm with your phone passkey.')
178
+ return P.assembleTurnProof({
179
+ handoff: { words: shown, provider: h.provider, sessionId: h.sessionId, bindingId: h.bindingId, scope: h.scope, packet: h.packet, attachments: [], turn: h.turn },
180
+ nonce: n.body, signer: pageSigner(rec), origin: W.location.origin, nowMs: Date.now(),
181
+ })
182
+ }).then(function (proof) {
183
+ if (proof.words !== shown) throw refusal('words_changed')
184
+ setText('sign-status', 'Sending.')
185
+ return api('/api/agent-sessions/bindings/' + encodeURIComponent(h.bindingId) + '/turns', { method: 'POST', headers: proof.headers, body: proof.body })
186
+ }).then(function (t) {
187
+ var b = t.body || {}
188
+ if (t.status >= 200 && t.status < 300 && (b.outcome === 'queued' || b.outcome === 'completed')) {
189
+ setText('done-text', b.replayed ? 'This message was already sent.' : 'Sent to the ' + providerName(h.provider) + ' session with your approval.')
190
+ show('done')
191
+ return undefined
192
+ }
193
+ problem(b.reasonCopy || reasonCopy(b.reason || b.error || 'turn_refused'))
194
+ return undefined
195
+ }).catch(function (err) {
196
+ problem(reasonCopy(err && err.reason ? err.reason : 'signing_failed'))
197
+ }).then(function () {
198
+ state.busy = false
199
+ state.handoff = null
200
+ if (btn) btn.disabled = false
201
+ })
202
+ }
203
+
204
+ // ── enrollment (contract section 3) ──
205
+ function enrollFail(copy) { return S.clear().then(function () { state.record = null; problem(copy) }, function () { problem(copy) }) }
206
+ // Two taps, and each Face ID prompt is the FIRST thing its tap does: Safari allows a passkey call only
207
+ // inside a fresh user gesture, so no network wait ever sits between a tap and its prompt.
208
+ // Tap 1: create the passkey, then make the device key (no prompt), commit to r, start, sign the proof.
209
+ // Tap 2: Face ID over the proof, then prove; the code on screen is the one this phone worked out.
210
+ function onEnroll(ev) {
211
+ if (!ev || ev.isTrusted !== true || state.busy) return undefined
212
+ state.busy = true
213
+ var subtle = W.crypto.subtle
214
+ var labelEl = $('enroll-label')
215
+ var label = (labelEl && String(labelEl.value || '').trim()) || 'iPhone'
216
+ var c = {}
217
+ setText('enroll-status', 'Create this phone’s approval passkey.')
218
+ return W.navigator.credentials.create({
219
+ publicKey: {
220
+ rp: { id: W.location.hostname, name: 'COS Control' },
221
+ user: { id: W.crypto.getRandomValues(new Uint8Array(16)), name: label, displayName: label },
222
+ challenge: W.crypto.getRandomValues(new Uint8Array(32)),
223
+ pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
224
+ authenticatorSelection: { residentKey: 'preferred', requireResidentKey: false, userVerification: 'required' },
225
+ attestation: 'none', timeout: 120000,
226
+ },
227
+ }).then(null, function () { throw refusal('passkey_cancelled') }).then(function (cred) {
228
+ var xy = cred && cred.response ? P.passkeyXY(cred.response) : null
229
+ if (!xy) throw refusal('passkey_malformed')
230
+ c.xy = xy
231
+ c.credentialId = P.b64url(cred.rawId)
232
+ setText('enroll-status', 'Making a key on this phone.')
233
+ // The device key: non-extractable, made with no prompt of its own.
234
+ return subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign', 'verify'])
235
+ }).then(function (pair) {
236
+ c.pair = pair
237
+ return subtle.exportKey('spki', pair.publicKey)
238
+ }).then(function (spki) {
239
+ c.spki = new Uint8Array(spki)
240
+ c.r = W.crypto.getRandomValues(new Uint8Array(32))
241
+ return P.sasCommit(c.r)
242
+ }).then(function (commit) {
243
+ return api('/api/web-key/enroll/start', { method: 'POST', body: { spki: P.b64url(c.spki), label: label, sasCommit: commit } })
244
+ }).then(function (a) {
245
+ if (a.status !== 201 || a.body.ok !== true) throw refusal(a.body.error || 'enroll_start_failed')
246
+ c.start = a.body
247
+ if (c.start.origin !== W.location.origin || c.start.rpId !== W.location.hostname) throw refusal('origin_mismatch')
248
+ c.challenge = P.fromB64url(c.start.challenge)
249
+ if (!c.challenge || c.challenge.length !== 32) throw refusal('challenge_invalid')
250
+ return Promise.all([P.kidOf(c.spki), P.sasCode(c.challenge, c.spki, c.r), P.fingerprintOf(c.spki), P.sha256(P.spkiFromXY(c.xy.x, c.xy.y))])
251
+ }).then(function (v) {
252
+ if (v[0] !== c.start.kid) throw refusal('kid_mismatch')
253
+ c.kid = v[0]; c.code = v[1]; c.fingerprint = v[2]
254
+ c.record = { kid: c.kid, keyPair: c.pair, spki: P.b64url(c.spki), credentialId: c.credentialId, label: label, enrollId: c.start.enrollId, exp: c.start.exp, state: 'pending', code: c.code, fingerprint: c.fingerprint }
255
+ c.msg = P.jcsBytes(P.enrollProofMessage({
256
+ enrollId: c.start.enrollId, challenge: c.start.challenge, aud: c.start.aud, origin: W.location.origin, kid: c.kid,
257
+ phoneNonce: P.b64url(c.r), passkeyCredentialId: c.credentialId, passkeySpkiSha256: P.hex(v[3]),
258
+ }))
259
+ return S.put(c.record)
260
+ }).then(function () {
261
+ return subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, c.pair.privateKey, c.msg)
262
+ }).then(function (sig) {
263
+ c.sig = new Uint8Array(sig)
264
+ return P.sha256(c.msg)
265
+ }).then(function (proofChallenge) {
266
+ c.proofChallenge = proofChallenge
267
+ state.enrolling = c
268
+ setText('enroll-status', 'One more Face ID to finish.')
269
+ var go = $('enroll-confirm'); if (go) go.hidden = false
270
+ var first = $('enroll-start'); if (first) first.hidden = true
271
+ return undefined
272
+ }).catch(function (err) {
273
+ return enrollFail(reasonCopy(err && err.reason ? err.reason : 'enroll_failed'))
274
+ }).then(function () { state.busy = false })
275
+ }
276
+ function onEnrollConfirm(ev) {
277
+ var c = state.enrolling
278
+ if (!ev || ev.isTrusted !== true || state.busy || !c) return undefined
279
+ state.busy = true
280
+ state.enrolling = null
281
+ var go = $('enroll-confirm'); if (go) go.hidden = true
282
+ return assertWith(c.credentialId, c.proofChallenge).then(function (assertion) {
283
+ setText('enroll-status', 'Checking with your Mac.')
284
+ return api('/api/web-key/enroll/prove', { method: 'POST', body: {
285
+ enrollId: c.start.enrollId, signature: P.b64url(c.sig), phoneNonce: P.b64url(c.r),
286
+ passkey: { publicKey: P.b64url(P.coseFromXY(c.xy.x, c.xy.y)), credentialId: c.credentialId, assertion: assertion },
287
+ } })
288
+ }).then(function (a) {
289
+ if (a.status !== 200 || a.body.ok !== true) throw refusal(a.body.error || 'enroll_prove_failed')
290
+ // The code on this screen is the one this phone worked out. The Mac's copy must agree.
291
+ if (a.body.sas !== c.code) return enrollFail('The code from your Mac does not match the code this phone worked out. Do not approve on the Mac. Start again.')
292
+ state.record = c.record
293
+ paintKey(c.record)
294
+ state.busy = false
295
+ return watchEnrollment(c.record)
296
+ }).catch(function (err) {
297
+ return enrollFail(reasonCopy(err && err.reason ? err.reason : 'enroll_failed'))
298
+ }).then(function () { state.busy = false })
299
+ }
300
+ function watchEnrollment(rec) {
301
+ setText('pending-code', spacedCode(rec.code))
302
+ setText('pending-fingerprint', rec.fingerprint)
303
+ show('pending')
304
+ return new Promise(function (resolve) {
305
+ var tick = function () {
306
+ api('/api/web-key/enroll/status?enrollId=' + encodeURIComponent(rec.enrollId)).then(function (a) {
307
+ var s = a.body && a.body.status
308
+ if (s === 'enrolled') {
309
+ rec.state = 'enrolled'
310
+ return S.put(rec).then(function () { state.record = rec; setText('idle-text', 'This phone can approve messages now.'); show('idle'); resolve() })
311
+ }
312
+ if (s === 'started' || s === 'proven') { W.setTimeout(tick, POLL_MS); return undefined }
313
+ return enrollFail(s === 'expired' ? 'Setup expired before the Mac approved it. Start again.' : 'Setup ended without approval. Start again.').then(resolve)
314
+ }, function () { W.setTimeout(tick, POLL_MS) })
315
+ }
316
+ tick()
317
+ })
318
+ }
319
+
320
+ // ── revoke this phone's key (no Touch ID needed, plan section 4.7) ──
321
+ function onRevoke(ev) {
322
+ if (!ev || ev.isTrusted !== true || !state.record) return undefined
323
+ if (!state.revokeArmed) { state.revokeArmed = true; setText('key-revoke', 'Tap again to remove this phone’s key'); return undefined }
324
+ state.revokeArmed = false
325
+ var kid = state.record.kid
326
+ return api('/api/web-key/revoke', { method: 'POST', body: { kid: kid, reason: 'revoked on the phone' } }).then(function (a) {
327
+ if (a.status !== 200 && a.status !== 404) throw refusal(a.body.error || 'revoke_failed')
328
+ return S.clear()
329
+ }).then(function () { state.record = null; show('enroll') }, function (err) { problem(reasonCopy(err && err.reason)) })
330
+ }
331
+
332
+ function on(id, fn) { var el = $(id); if (el) el.addEventListener('click', fn) }
333
+ on('enroll-start', onEnroll)
334
+ on('enroll-confirm', onEnrollConfirm)
335
+ on('sign-approve', onApprove)
336
+ on('sign-cancel', function (ev) { if (!ev || ev.isTrusted !== true || state.busy) return; state.handoff = null; state.nonce = null; problem('Approval cancelled. Nothing was signed or sent.'); })
337
+ on('key-revoke', onRevoke)
338
+ var back = $('back'); if (back) back.href = controlUrl()
339
+ var back2 = $('done-back'); if (back2) back2.href = controlUrl()
340
+ W.COSSigner = { ready: boot() }
341
+ })(window)
@@ -0,0 +1,124 @@
1
+ {
2
+ "description": "Synthetic public fixtures only; no production authorization. Unix milliseconds, maximum lifetime1800000ms.",
3
+ "packets": [
4
+ {
5
+ "packet": {
6
+ "v": "cos-web-packet/1",
7
+ "packetId": "AAECAwQFBgcICQoL",
8
+ "action": "publish",
9
+ "artifact": "@gotcos/glasses-server",
10
+ "version": "7.0.6",
11
+ "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
12
+ "channel": "npm-next",
13
+ "provider": "codex",
14
+ "sessionId": "cos-phone-approval-fixture",
15
+ "createdAt": 1786000000000,
16
+ "exp": 1786001800000
17
+ },
18
+ "canonical": "{\"action\":\"publish\",\"artifact\":\"@gotcos/glasses-server\",\"channel\":\"npm-next\",\"createdAt\":1786000000000,\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"exp\":1786001800000,\"packetId\":\"AAECAwQFBgcICQoL\",\"provider\":\"codex\",\"sessionId\":\"cos-phone-approval-fixture\",\"v\":\"cos-web-packet/1\",\"version\":\"7.0.6\"}",
19
+ "packetSha256": "f006fcd8f6d18f45a0d5608ba4e63b56d970f95c97752611b271bf8a3829b142"
20
+ },
21
+ {
22
+ "packet": {
23
+ "v": "cos-web-packet/1",
24
+ "packetId": "AAECAwQFBgcICQoL",
25
+ "action": "publish",
26
+ "artifact": "@gotcos/glasses-server",
27
+ "version": "7.0.6",
28
+ "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
29
+ "channel": "npm-latest",
30
+ "provider": "codex",
31
+ "sessionId": "cos-phone-approval-fixture",
32
+ "createdAt": 1786000000000,
33
+ "exp": 1786001800000
34
+ },
35
+ "canonical": "{\"action\":\"publish\",\"artifact\":\"@gotcos/glasses-server\",\"channel\":\"npm-latest\",\"createdAt\":1786000000000,\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"exp\":1786001800000,\"packetId\":\"AAECAwQFBgcICQoL\",\"provider\":\"codex\",\"sessionId\":\"cos-phone-approval-fixture\",\"v\":\"cos-web-packet/1\",\"version\":\"7.0.6\"}",
36
+ "packetSha256": "9a2cbe9e575a2db320afe8dce0e5675f073da1c11632d1accb9dba4bed65a24e"
37
+ },
38
+ {
39
+ "packet": {
40
+ "v": "cos-web-packet/1",
41
+ "packetId": "AAECAwQFBgcICQoL",
42
+ "action": "publish",
43
+ "artifact": "cos-control",
44
+ "version": "0.5.381",
45
+ "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
46
+ "channel": "control-appcast",
47
+ "provider": "codex",
48
+ "sessionId": "cos-phone-approval-fixture",
49
+ "createdAt": 1786000000000,
50
+ "exp": 1786001800000
51
+ },
52
+ "canonical": "{\"action\":\"publish\",\"artifact\":\"cos-control\",\"channel\":\"control-appcast\",\"createdAt\":1786000000000,\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"exp\":1786001800000,\"packetId\":\"AAECAwQFBgcICQoL\",\"provider\":\"codex\",\"sessionId\":\"cos-phone-approval-fixture\",\"v\":\"cos-web-packet/1\",\"version\":\"0.5.381\"}",
53
+ "packetSha256": "c21781d2c2b2977ce6b60cf0d71611f43776b4cd4f0979298a34b2ffae31a81d"
54
+ },
55
+ {
56
+ "packet": {
57
+ "v": "cos-web-packet/1",
58
+ "packetId": "AAECAwQFBgcICQoL",
59
+ "action": "publish",
60
+ "artifact": "cos-glasses-hub",
61
+ "version": "7.0.6",
62
+ "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
63
+ "channel": "hub-alias",
64
+ "provider": "codex",
65
+ "sessionId": "cos-phone-approval-fixture",
66
+ "createdAt": 1786000000000,
67
+ "exp": 1786001800000
68
+ },
69
+ "canonical": "{\"action\":\"publish\",\"artifact\":\"cos-glasses-hub\",\"channel\":\"hub-alias\",\"createdAt\":1786000000000,\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"exp\":1786001800000,\"packetId\":\"AAECAwQFBgcICQoL\",\"provider\":\"codex\",\"sessionId\":\"cos-phone-approval-fixture\",\"v\":\"cos-web-packet/1\",\"version\":\"7.0.6\"}",
70
+ "packetSha256": "4a95b8091a2f739b2e3d2e966adcff41719e0c8a678f8084aeeceffdd0bb2cba"
71
+ },
72
+ {
73
+ "packet": {
74
+ "v": "cos-web-packet/1",
75
+ "packetId": "AAECAwQFBgcICQoL",
76
+ "action": "publish",
77
+ "artifact": "gotcos-site-manifest",
78
+ "version": "7.0.6",
79
+ "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
80
+ "channel": "gotcos-site",
81
+ "provider": "codex",
82
+ "sessionId": "cos-phone-approval-fixture",
83
+ "createdAt": 1786000000000,
84
+ "exp": 1786001800000
85
+ },
86
+ "canonical": "{\"action\":\"publish\",\"artifact\":\"gotcos-site-manifest\",\"channel\":\"gotcos-site\",\"createdAt\":1786000000000,\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"exp\":1786001800000,\"packetId\":\"AAECAwQFBgcICQoL\",\"provider\":\"codex\",\"sessionId\":\"cos-phone-approval-fixture\",\"v\":\"cos-web-packet/1\",\"version\":\"7.0.6\"}",
87
+ "packetSha256": "2c518894bc24e58bc6b07d54d567bbfd23a8fa81dc2285da5a0a6b11e7d79e7c"
88
+ },
89
+ {
90
+ "packet": {
91
+ "v": "cos-web-packet/1",
92
+ "packetId": "AAECAwQFBgcICQoL",
93
+ "action": "publish",
94
+ "artifact": "cos-control-downloads-manifest",
95
+ "version": "0.5.381",
96
+ "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
97
+ "channel": "control-downloads",
98
+ "provider": "codex",
99
+ "sessionId": "cos-phone-approval-fixture",
100
+ "createdAt": 1786000000000,
101
+ "exp": 1786001800000
102
+ },
103
+ "canonical": "{\"action\":\"publish\",\"artifact\":\"cos-control-downloads-manifest\",\"channel\":\"control-downloads\",\"createdAt\":1786000000000,\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"exp\":1786001800000,\"packetId\":\"AAECAwQFBgcICQoL\",\"provider\":\"codex\",\"sessionId\":\"cos-phone-approval-fixture\",\"v\":\"cos-web-packet/1\",\"version\":\"0.5.381\"}",
104
+ "packetSha256": "4826414b715f1408923616bbbf58af71417fee2d1903d44729488825e6e9fdec"
105
+ },
106
+ {
107
+ "packet": {
108
+ "v": "cos-web-packet/1",
109
+ "packetId": "AAECAwQFBgcICQoL",
110
+ "action": "publish",
111
+ "artifact": "cos-release-docs-manifest",
112
+ "version": "7.0.6",
113
+ "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
114
+ "channel": "release-docs",
115
+ "provider": "codex",
116
+ "sessionId": "cos-phone-approval-fixture",
117
+ "createdAt": 1786000000000,
118
+ "exp": 1786001800000
119
+ },
120
+ "canonical": "{\"action\":\"publish\",\"artifact\":\"cos-release-docs-manifest\",\"channel\":\"release-docs\",\"createdAt\":1786000000000,\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"exp\":1786001800000,\"packetId\":\"AAECAwQFBgcICQoL\",\"provider\":\"codex\",\"sessionId\":\"cos-phone-approval-fixture\",\"v\":\"cos-web-packet/1\",\"version\":\"7.0.6\"}",
121
+ "packetSha256": "cf37caaab37467c2f832de167504b56348feda530604a2e65ed9f54cb40288f9"
122
+ }
123
+ ]
124
+ }