@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,271 @@
1
+ // The signed Control Web turn envelope, `cos-web-turn/1` (6.99.31, plan §4.4). PURE: no I/O, no
2
+ // crypto, no imports, so the phone signer, the server and the tests share one definition, and
3
+ // the Python verifier (MU-Chief-Staff operations/scripts/verify_g2_turn.py) mirrors it with golden
4
+ // fixtures.
5
+ //
6
+ // CANONICAL BYTES. The phone signs the RFC 8785 (JCS) encoding of the envelope: object keys
7
+ // sorted by UTF-16 code units, no whitespace, strings escaped as ES JSON.stringify escapes them,
8
+ // numbers as ES Number-to-string. Every number this protocol uses is a safe integer, so the
9
+ // Python mirror (json.dumps(sort_keys=True, separators=(',', ':'), ensure_ascii=False)) agrees
10
+ // byte for byte. The server refuses an envelope whose bytes are not already canonical: one
11
+ // message has exactly one signable form.
12
+ //
13
+ // CLOSED SHAPES. Every object here has an exact key set. An unknown key, an unknown `v`, an
14
+ // unknown scope kind or an approve action outside APPROVE_ACTIONS is refused, never ignored. The
15
+ // `approve` grammar has no spelling for the never-list (credentials, money, deleting data, sending
16
+ // messages, security or permission settings, the harness's permission prompts), and its free
17
+ // fields have strict patterns plus an action-to-channel table (review B4).
18
+ //
19
+ // `message` scope is CONVERSATION ONLY (MESSAGE_SCOPE_MEANING). Its words are free text, so a
20
+ // message can SAY "send the email"; that is not authority. Enforcement of what a message may
21
+ // cause is the receiving agent's policy, read from the verifier's `notAuthorized` list, exactly as
22
+ // for G2. The contract is docs/web-key-verifier-contract.md.
23
+
24
+ export const WEB_TURN_ENVELOPE_VERSION = 'cos-web-turn/1'
25
+ export const WEB_ENROLL_PROOF_VERSION = 'cos-web-enroll/2'
26
+ export const WEB_ENROLL_APPROVE_VERSION = 'cos-web-enroll-approve/2'
27
+ export const WEB_APPROVAL_PACKET_VERSION = 'cos-web-packet/1'
28
+ /** Approval packet timestamps are Unix milliseconds; a packet lives at most 30 minutes. */
29
+ export const WEB_APPROVAL_PACKET_MAX_LIFETIME_MS = 30 * 60_000
30
+ /** Canonical base64url encoding of the server's 12 random packet-id bytes. */
31
+ export const WEB_APPROVAL_PACKET_ID_RE = /^[A-Za-z0-9_-]{16}$/
32
+ export const WEB_PASSKEY_ASSERTION_VERSION = 'cos-web-passkey/1'
33
+ export const WEB_ANCHOR_STATEMENT_VERSION = 'cos-web-anchor/1'
34
+
35
+ /** The longest an envelope may live (exp - iat), in seconds. */
36
+ export const WEB_TURN_MAX_LIFETIME_S = 15 * 60
37
+ /** How far in the future `iat` may sit (clock skew between the phone and this Mac), in seconds. */
38
+ export const WEB_TURN_IAT_SKEW_S = 60
39
+
40
+ /** The only actions an `approve` scope can name. Anything else, including every never-list item, is unexpressible. */
41
+ export const APPROVE_ACTIONS: ReadonlySet<string> = new Set(['install', 'publish', 'push-main'])
42
+
43
+ /** What a `message` scope means: conversation only (review B4). */
44
+ export const MESSAGE_SCOPE_MEANING = 'conversation'
45
+
46
+ /** Never authorized by a web turn, whatever its scope or words (CLAUDE.md G2 Authority never-list). */
47
+ export const WEB_KEY_NEVER_AUTHORIZED: readonly string[] = Object.freeze([
48
+ 'credentials', 'payment-details', 'moving-money', 'deleting-data', 'sending-messages',
49
+ 'security-or-permission-settings', 'harness-permission-prompts', 'authority-rule-changes',
50
+ ])
51
+
52
+ /** What `message` scope never authorizes on top of the never-list: it is conversation only. */
53
+ export const MESSAGE_SCOPE_NOT_AUTHORIZED: readonly string[] = Object.freeze([
54
+ 'install', 'publish', 'push-main', 'push-branch', 'hub-file',
55
+ ])
56
+
57
+ /** The channels each approve action may name. Nothing else is signable (review B4). */
58
+ export const APPROVE_ACTION_CHANNELS: Readonly<Record<'install' | 'publish' | 'push-main', readonly string[]>> = Object.freeze({
59
+ install: Object.freeze(['mac-local']),
60
+ publish: Object.freeze(['npm-next', 'npm-latest', 'control-appcast', 'hub-alias', 'gotcos-site', 'control-downloads', 'release-docs']),
61
+ 'push-main': Object.freeze(['git-main']),
62
+ })
63
+
64
+ export const APPROVE_ARTIFACT_RE = /^[a-z0-9@][a-z0-9@._/-]{0,127}$/
65
+ export const APPROVE_VERSION_RE = /^[0-9]{1,5}\.[0-9]{1,5}\.[0-9]{1,6}(-[0-9A-Za-z.-]{1,32})?$/
66
+ /** sha256:<hex64>, sha512:<hex128>, or an npm SRI sha512-<base64>. Nothing else. */
67
+ export const APPROVE_DIGEST_RE = /^(sha256:[0-9a-f]{64}|sha512:[0-9a-f]{128}|sha512-[A-Za-z0-9+/]{86}==)$/
68
+ /** These channels approve one reviewed canonical deployment manifest, never arbitrary URLs. */
69
+ export const PUBLISH_MANIFEST_ARTIFACTS: Readonly<Record<string, string>> = Object.freeze({
70
+ 'gotcos-site': 'gotcos-site-manifest',
71
+ 'control-downloads': 'cos-control-downloads-manifest',
72
+ 'release-docs': 'cos-release-docs-manifest',
73
+ })
74
+
75
+ /** Whether the four free approve fields match their patterns and the channel belongs to the action. */
76
+ export function approveFieldsValid(action: unknown, artifact: unknown, version: unknown, digest: unknown, channel: unknown): boolean {
77
+ if (typeof action !== 'string' || !APPROVE_ACTIONS.has(action)) return false
78
+ if (typeof artifact !== 'string' || !APPROVE_ARTIFACT_RE.test(artifact)) return false
79
+ if (typeof version !== 'string' || !APPROVE_VERSION_RE.test(version)) return false
80
+ if (typeof digest !== 'string' || !APPROVE_DIGEST_RE.test(digest)) return false
81
+ const channels = APPROVE_ACTION_CHANNELS[action as keyof typeof APPROVE_ACTION_CHANNELS]
82
+ if (typeof channel !== 'string' || !channels.includes(channel)) return false
83
+ if (Object.prototype.hasOwnProperty.call(PUBLISH_MANIFEST_ARTIFACTS, channel)
84
+ && (artifact !== PUBLISH_MANIFEST_ARTIFACTS[channel] || !/^sha256:[0-9a-f]{64}$/.test(digest))) return false
85
+ return true
86
+ }
87
+
88
+ /** The verifier's `notAuthorized` list for a scope: the never-list plus what this scope does not cover. */
89
+ export function notAuthorizedFor(scope: WebTurnScope): string[] {
90
+ if (scope.kind === 'message') return [...WEB_KEY_NEVER_AUTHORIZED, ...MESSAGE_SCOPE_NOT_AUTHORIZED]
91
+ const others = [...APPROVE_ACTIONS].filter(a => a !== scope.action)
92
+ return [...WEB_KEY_NEVER_AUTHORIZED, ...others, 'push-branch', 'hub-file', 'other-artifact-version-digest-or-channel']
93
+ }
94
+
95
+ export type WebTurnScope =
96
+ | { kind: 'message' }
97
+ | { kind: 'approve'; action: 'install' | 'publish' | 'push-main'; artifact: string; version: string; digest: string; channel: string; packetSha256: string }
98
+
99
+ export interface WebTurnAttachment { id: string; sha256: string; bytes: number; name: string }
100
+
101
+ export interface WebTurnEnvelope {
102
+ v: typeof WEB_TURN_ENVELOPE_VERSION
103
+ aud: string
104
+ origin: string
105
+ kid: string
106
+ kver: number
107
+ epoch: number
108
+ turnId: string
109
+ nonce: string
110
+ iat: number
111
+ exp: number
112
+ provider: string
113
+ sessionId: string
114
+ bindingId: string | null
115
+ fork: boolean
116
+ wordsSha256: string
117
+ wordsLen: number
118
+ attachments: WebTurnAttachment[]
119
+ scope: WebTurnScope
120
+ }
121
+
122
+ export class CanonicalJsonError extends Error {}
123
+
124
+ function hasLoneSurrogate(s: string): boolean {
125
+ for (let i = 0; i < s.length; i += 1) {
126
+ const c = s.charCodeAt(i)
127
+ if (c >= 0xd800 && c <= 0xdbff) {
128
+ const n = s.charCodeAt(i + 1)
129
+ if (!(n >= 0xdc00 && n <= 0xdfff)) return true
130
+ i += 1
131
+ } else if (c >= 0xdc00 && c <= 0xdfff) return true
132
+ }
133
+ return false
134
+ }
135
+
136
+ /**
137
+ * RFC 8785 JSON Canonicalization Scheme for the values this protocol carries: plain objects,
138
+ * arrays, strings, safe integers, booleans and null. Anything else throws (a float, NaN, a lone
139
+ * surrogate, undefined, a class instance): the protocol never signs it, so it never encodes it.
140
+ */
141
+ export function canonicalJson(value: unknown): string {
142
+ if (value === null) return 'null'
143
+ if (typeof value === 'boolean') return value ? 'true' : 'false'
144
+ if (typeof value === 'number') {
145
+ if (!Number.isSafeInteger(value)) throw new CanonicalJsonError('only safe integers are signable')
146
+ return JSON.stringify(value)
147
+ }
148
+ if (typeof value === 'string') {
149
+ if (hasLoneSurrogate(value)) throw new CanonicalJsonError('lone surrogate')
150
+ return JSON.stringify(value)
151
+ }
152
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
153
+ if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
154
+ const obj = value as Record<string, unknown>
155
+ const keys = Object.keys(obj).sort()
156
+ return `{${keys.map((k) => {
157
+ if (hasLoneSurrogate(k)) throw new CanonicalJsonError('lone surrogate in key')
158
+ return `${JSON.stringify(k)}:${canonicalJson(obj[k])}`
159
+ }).join(',')}}`
160
+ }
161
+ throw new CanonicalJsonError(`not signable: ${typeof value}`)
162
+ }
163
+
164
+ /** The exact words as signed: CRLF becomes LF, and nothing else changes (no trim, no collapse). */
165
+ export function signedWordsText(words: string): string {
166
+ return words.replace(/\r\n/g, '\n')
167
+ }
168
+
169
+ const HEX64 = /^[0-9a-f]{64}$/
170
+ const B64URL = /^[A-Za-z0-9_-]+$/
171
+ const TURN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{7,127}$/
172
+ const PROVIDERS: ReadonlySet<string> = new Set(['claude', 'codex', 'cursor'])
173
+ const SHORT = (s: unknown, max: number): s is string => typeof s === 'string' && s.length > 0 && s.length <= max
174
+
175
+ function exactKeys(obj: Record<string, unknown>, keys: readonly string[]): boolean {
176
+ const have = Object.keys(obj)
177
+ return have.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(obj, k))
178
+ }
179
+
180
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
181
+ return typeof v === 'object' && v !== null && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype
182
+ }
183
+
184
+ const nonNegInt = (v: unknown): v is number => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0
185
+
186
+ const APPROVE_SCOPE_KEYS = ['kind', 'action', 'artifact', 'version', 'digest', 'channel', 'packetSha256'] as const
187
+
188
+ /** The closed scope list. Null for anything outside it. */
189
+ export function parseWebTurnScope(v: unknown): WebTurnScope | null {
190
+ if (!isPlainObject(v)) return null
191
+ if (v.kind === 'message') return exactKeys(v, ['kind']) ? { kind: 'message' } : null
192
+ if (v.kind === 'approve') {
193
+ if (!exactKeys(v, APPROVE_SCOPE_KEYS)) return null
194
+ // The closed action list, the strict field patterns and the channel table (approveFieldsValid).
195
+ if (!approveFieldsValid(v.action, v.artifact, v.version, v.digest, v.channel)) return null
196
+ if (typeof v.packetSha256 !== 'string' || !HEX64.test(v.packetSha256)) return null
197
+ return { kind: 'approve', action: v.action as 'install' | 'publish' | 'push-main', artifact: v.artifact as string, version: v.version as string, digest: v.digest as string, channel: v.channel as string, packetSha256: v.packetSha256 }
198
+ }
199
+ return null
200
+ }
201
+
202
+ const ENVELOPE_KEYS = ['v', 'aud', 'origin', 'kid', 'kver', 'epoch', 'turnId', 'nonce', 'iat', 'exp', 'provider', 'sessionId', 'bindingId', 'fork', 'wordsSha256', 'wordsLen', 'attachments', 'scope'] as const
203
+ const ATTACHMENT_KEYS = ['id', 'sha256', 'bytes', 'name'] as const
204
+ export const WEB_TURN_MAX_ATTACHMENTS = 16
205
+
206
+ /**
207
+ * The envelope's exact shape, or the first reason it is not one. `scope_not_allowed` is kept
208
+ * apart from `envelope_malformed` so the phone can say which.
209
+ */
210
+ export function parseWebTurnEnvelope(v: unknown): { ok: true; envelope: WebTurnEnvelope } | { ok: false; reason: 'envelope_malformed' | 'envelope_version_unknown' | 'scope_not_allowed' } {
211
+ if (!isPlainObject(v)) return { ok: false, reason: 'envelope_malformed' }
212
+ if (v.v !== WEB_TURN_ENVELOPE_VERSION) return { ok: false, reason: 'envelope_version_unknown' }
213
+ if (!exactKeys(v, ENVELOPE_KEYS)) return { ok: false, reason: 'envelope_malformed' }
214
+ if (typeof v.aud !== 'string' || !HEX64.test(v.aud)) return { ok: false, reason: 'envelope_malformed' }
215
+ if (!SHORT(v.origin, 256) || !/^https?:\/\/[^/\s]+$/.test(v.origin)) return { ok: false, reason: 'envelope_malformed' }
216
+ if (typeof v.kid !== 'string' || v.kid.length !== 22 || !B64URL.test(v.kid)) return { ok: false, reason: 'envelope_malformed' }
217
+ if (!nonNegInt(v.kver) || v.kver < 1 || !nonNegInt(v.epoch)) return { ok: false, reason: 'envelope_malformed' }
218
+ if (typeof v.turnId !== 'string' || !TURN_ID.test(v.turnId)) return { ok: false, reason: 'envelope_malformed' }
219
+ if (typeof v.nonce !== 'string' || !/^[A-Za-z0-9_-]{22,64}$/.test(v.nonce)) return { ok: false, reason: 'envelope_malformed' }
220
+ if (!nonNegInt(v.iat) || !nonNegInt(v.exp)) return { ok: false, reason: 'envelope_malformed' }
221
+ if (typeof v.provider !== 'string' || !PROVIDERS.has(v.provider)) return { ok: false, reason: 'envelope_malformed' }
222
+ if (!SHORT(v.sessionId, 128)) return { ok: false, reason: 'envelope_malformed' }
223
+ if (v.bindingId !== null && !SHORT(v.bindingId, 128)) return { ok: false, reason: 'envelope_malformed' }
224
+ if (typeof v.fork !== 'boolean') return { ok: false, reason: 'envelope_malformed' }
225
+ if (typeof v.wordsSha256 !== 'string' || !HEX64.test(v.wordsSha256) || !nonNegInt(v.wordsLen)) return { ok: false, reason: 'envelope_malformed' }
226
+ if (!Array.isArray(v.attachments) || v.attachments.length > WEB_TURN_MAX_ATTACHMENTS) return { ok: false, reason: 'envelope_malformed' }
227
+ const attachments: WebTurnAttachment[] = []
228
+ for (const a of v.attachments) {
229
+ if (!isPlainObject(a) || !exactKeys(a, ATTACHMENT_KEYS)) return { ok: false, reason: 'envelope_malformed' }
230
+ if (!SHORT(a.id, 128) || typeof a.sha256 !== 'string' || !HEX64.test(a.sha256) || !nonNegInt(a.bytes) || typeof a.name !== 'string' || a.name.length > 255) {
231
+ return { ok: false, reason: 'envelope_malformed' }
232
+ }
233
+ attachments.push({ id: a.id, sha256: a.sha256, bytes: a.bytes, name: a.name })
234
+ }
235
+ const scope = parseWebTurnScope(v.scope)
236
+ if (scope === null) return { ok: false, reason: 'scope_not_allowed' }
237
+ return {
238
+ ok: true,
239
+ envelope: {
240
+ v: WEB_TURN_ENVELOPE_VERSION, aud: v.aud, origin: v.origin, kid: v.kid, kver: v.kver, epoch: v.epoch,
241
+ turnId: v.turnId, nonce: v.nonce, iat: v.iat, exp: v.exp, provider: v.provider, sessionId: v.sessionId,
242
+ bindingId: v.bindingId as string | null, fork: v.fork, wordsSha256: v.wordsSha256, wordsLen: v.wordsLen,
243
+ attachments, scope,
244
+ },
245
+ }
246
+ }
247
+
248
+ /** An approval packet a receiving session posts for the phone to sign against (plan §4.4 `approve`). */
249
+ export interface WebApprovalPacket {
250
+ v: typeof WEB_APPROVAL_PACKET_VERSION
251
+ packetId: string
252
+ action: 'install' | 'publish' | 'push-main'
253
+ artifact: string
254
+ version: string
255
+ digest: string
256
+ channel: string
257
+ provider: string
258
+ sessionId: string
259
+ createdAt: number
260
+ exp: number
261
+ }
262
+
263
+ /** The exact packet preimage, shared with the receiving verifier. No URL or wildcard authority. */
264
+ export function parseWebApprovalPacket(value: unknown): WebApprovalPacket | null {
265
+ if (!isPlainObject(value) || !exactKeys(value, ['v', 'packetId', 'action', 'artifact', 'version', 'digest', 'channel', 'provider', 'sessionId', 'createdAt', 'exp'])) return null
266
+ if (value.v !== WEB_APPROVAL_PACKET_VERSION || typeof value.packetId !== 'string' || !WEB_APPROVAL_PACKET_ID_RE.test(value.packetId)) return null
267
+ if (!approveFieldsValid(value.action, value.artifact, value.version, value.digest, value.channel)) return null
268
+ if (typeof value.provider !== 'string' || !PROVIDERS.has(value.provider) || !SHORT(value.sessionId, 128) || /[\u0000-\u001f\u007f]/.test(value.sessionId)) return null
269
+ if (!nonNegInt(value.createdAt) || !nonNegInt(value.exp) || value.exp <= value.createdAt || value.exp - value.createdAt > WEB_APPROVAL_PACKET_MAX_LIFETIME_MS) return null
270
+ return { ...value } as unknown as WebApprovalPacket
271
+ }