@gotcos/glasses-server 6.99.33 → 7.0.18
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/.env.example +37 -0
- package/CHANGELOG.md +587 -0
- package/README.md +3 -1
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +12 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +155 -30
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +27 -14
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +43 -1
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +536 -0
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/conversation.ts +56 -28
- package/server/lib/cors-reject-log.ts +51 -0
- package/server/lib/disk-io-meter.ts +202 -0
- package/server/lib/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +31 -5
- package/server/lib/event-loop-stall.ts +62 -0
- package/server/lib/full-hold-engine.ts +114 -0
- package/server/lib/full-hold.ts +124 -0
- package/server/lib/g2-turn-provenance.ts +128 -9
- package/server/lib/learning-needs-you.ts +24 -7
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +83 -0
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/session-log.ts +313 -4
- package/server/lib/session-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +25 -1
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +640 -85
- package/server/lib/turn-end-ask.ts +320 -0
- package/server/lib/turn-footer.ts +3 -3
- package/server/lib/web-control-integrity.ts +160 -0
- package/server/lib/web-control-offnet-page.ts +58 -0
- package/server/lib/web-control-static.ts +53 -9
- package/server/lib/web-key-authority.ts +372 -0
- package/server/lib/web-key-envelope.ts +98 -0
- package/server/lib/web-key-handoff.ts +135 -0
- package/server/lib/web-key-origin.ts +21 -0
- package/server/lib/web-key-passkey.ts +155 -0
- package/server/lib/web-key-registry.ts +825 -0
- package/server/lib/web-key-signer-app.ts +152 -0
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/web-session-auth.ts +133 -15
- package/server/lib/whisper-local.ts +93 -7
- package/server/lib/whisper-preview.ts +5 -3
- package/server/lib/work-activity.ts +56 -6
- package/server/lib/work-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +29 -1
- package/server/routes/agent-session-stream.ts +23 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +46 -1
- package/server/routes/live-events.ts +334 -0
- package/server/routes/permission-broker.ts +95 -5
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +223 -12
- package/server/routes/today-brief.ts +134 -44
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +28 -4
- package/server/routes/work-board.ts +9 -2
- package/server/routes/work-orchestrator.ts +12 -7
- package/server/scripts/write-web-control-manifest.mjs +43 -0
- package/server/web-control/.manifest.json +44 -0
- package/server/web-control/app.css +76 -1
- package/server/web-control/app.js +2247 -98
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/control.css +25 -0
- package/server/web-control/index.html +21 -3
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +94 -9
- package/server/web-control/panes/cos-chat.js +628 -44
- package/server/web-control/panes/meetings.css +18 -2
- package/server/web-control/panes/meetings.js +24 -8
- package/server/web-control/panes/memories.css +17 -2
- package/server/web-control/panes/memories.js +127 -42
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.js +162 -21
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +32 -3
- package/server/web-control/panes/settings.js +97 -6
- package/server/web-control/panes/today-actions.css +46 -0
- package/server/web-control/panes/today-actions.js +119 -17
- package/server/web-control/panes/work.css +13 -0
- package/server/web-control/panes/work.js +188 -37
- package/server/web-control/shell.js +17 -14
- package/server/web-signer/.manifest.json +11 -0
- package/server/web-signer/index.html +74 -0
- package/server/web-signer/signer-proof.js +348 -0
- package/server/web-signer/signer-store.js +39 -0
- package/server/web-signer/signer.css +23 -0
- package/server/web-signer/signer.js +341 -0
- package/shared/web-approval-packets.golden.json +124 -0
- 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
|
+
}
|