@gotcos/glasses-server 6.46.1 → 6.47.0

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 (38) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/package.json +6 -2
  3. package/server/index.ts +76 -0
  4. package/server/lib/cos-operations-meetings.ts +99 -8
  5. package/server/lib/fireflies-client.ts +862 -0
  6. package/server/lib/fireflies-key.ts +182 -0
  7. package/server/lib/imported-library-rows.ts +616 -0
  8. package/server/lib/imported-meeting-library.ts +608 -0
  9. package/server/lib/maintenance-lifecycle.ts +14 -0
  10. package/server/lib/meeting-actions-store.ts +478 -0
  11. package/server/lib/meeting-actions.ts +2583 -0
  12. package/server/lib/meeting-corrections.ts +32 -1
  13. package/server/lib/meeting-decisions.ts +223 -0
  14. package/server/lib/meeting-engine/align.ts +167 -0
  15. package/server/lib/meeting-engine/attribute.ts +265 -0
  16. package/server/lib/meeting-engine/evidence.ts +428 -0
  17. package/server/lib/meeting-engine/pairing.ts +327 -0
  18. package/server/lib/meeting-engine/render.ts +694 -0
  19. package/server/lib/meeting-engine/split.ts +242 -0
  20. package/server/lib/meeting-engine/worker.ts +238 -0
  21. package/server/lib/meeting-engine-mode.ts +197 -0
  22. package/server/lib/meeting-file-guards.ts +141 -0
  23. package/server/lib/meeting-import.ts +763 -0
  24. package/server/lib/meeting-library-search.ts +146 -11
  25. package/server/lib/meeting-parse.ts +184 -0
  26. package/server/lib/meeting-store.ts +108 -275
  27. package/server/lib/meeting-suggestion-sides.ts +242 -0
  28. package/server/lib/morning-brief-runtime.ts +20 -8
  29. package/server/lib/pipeline-runner.ts +227 -0
  30. package/server/lib/voice-evidence-guard.ts +87 -0
  31. package/server/routes/fireflies-key.ts +102 -0
  32. package/server/routes/meeting-actions.ts +82 -0
  33. package/server/routes/meeting-engine.ts +52 -0
  34. package/server/routes/meeting-import.ts +67 -0
  35. package/server/routes/meeting-suggestions.ts +66 -0
  36. package/server/routes/meeting.ts +117 -10
  37. package/server/routes/meetings.ts +177 -50
  38. package/server/routes/voice.ts +18 -0
@@ -0,0 +1,182 @@
1
+ // Fireflies API key storage.
2
+ //
3
+ // PUSH-ONLY, like the OpenAI key before it: COS Control posts the key, the
4
+ // server stores it at 0600 under the data home, and from then on every surface
5
+ // gets metadata only. No route, log line, status payload or error message ever
6
+ // returns the key.
7
+ //
8
+ // The env override exists for managed installs that provision secrets outside
9
+ // the app. It WINS over the stored file, and `status()` says so, so a user who
10
+ // saved a key into a box that already has one in its environment is told which
11
+ // one is live instead of being quietly ignored.
12
+ //
13
+ // DELIBERATELY NOT A SECOND READER OF THE COS PIPELINE .env. speaker-trainer.ts
14
+ // reads FIREFLIES_API_KEY out of the COS scripts directory for its own
15
+ // (unrelated, pipeline-only) training path. Reading that file here would mean
16
+ // saving a key in Control silently enabled a different feature on a pipeline
17
+ // Mac; keeping the two sources separate is what makes "a key file alone does
18
+ // not enable /api/voice/train" true and testable.
19
+
20
+ import { lstatSync, readFileSync, unlinkSync } from 'node:fs'
21
+ import { dirname } from 'node:path'
22
+ import { durableAtomicWriteFileSync } from './atomic-fs.js'
23
+ import { dataPath } from './data-dir.js'
24
+ import { FirefliesBudget, FirefliesClient, createFetchTransport, type FirefliesKeyCheck } from './fireflies-client.js'
25
+ import { importsRoot } from './imported-meeting-library.js'
26
+ import { securePrivateDirectory, secureExistingPrivateFile } from './secure-user-config.js'
27
+
28
+ export const FIREFLIES_KEY_ENV = 'FIREFLIES_API_KEY'
29
+ export const FIREFLIES_KEY_MIN_LENGTH = 16
30
+ export const FIREFLIES_KEY_MAX_LENGTH = 512
31
+ /** Printable ASCII with no spaces. A key with whitespace is a paste accident. */
32
+ export const FIREFLIES_KEY_PATTERN = /^[\x21-\x7e]+$/
33
+
34
+ export type FirefliesKeySource = 'env' | 'config' | 'none'
35
+
36
+ export interface FirefliesKeyStatus {
37
+ configured: boolean
38
+ source: FirefliesKeySource
39
+ savedAt?: string
40
+ validatedAt?: string
41
+ lastCheck?: Omit<FirefliesKeyCheck, 'cached'>
42
+ }
43
+
44
+ export class FirefliesKeyError extends Error {
45
+ constructor(readonly status: number, readonly code: string, message: string) {
46
+ super(message)
47
+ this.name = 'FirefliesKeyError'
48
+ }
49
+ }
50
+
51
+ interface KeyFile {
52
+ version: 1
53
+ key: string
54
+ savedAt: string
55
+ validatedAt?: string
56
+ lastCheck?: Omit<FirefliesKeyCheck, 'cached'>
57
+ }
58
+
59
+ export class FirefliesKeyStore {
60
+ readonly path: string
61
+
62
+ constructor(options: { path?: string } = {}) {
63
+ this.path = options.path ?? dataPath('fireflies-key.json')
64
+ }
65
+
66
+ private read(): KeyFile | null {
67
+ let raw: string
68
+ try {
69
+ secureExistingPrivateFile(this.path)
70
+ raw = readFileSync(this.path, 'utf8')
71
+ } catch {
72
+ return null
73
+ }
74
+ try {
75
+ const parsed = JSON.parse(raw) as Partial<KeyFile>
76
+ if (typeof parsed?.key !== 'string' || parsed.key.trim().length === 0) return null
77
+ return {
78
+ version: 1,
79
+ key: parsed.key.trim(),
80
+ savedAt: typeof parsed.savedAt === 'string' ? parsed.savedAt : new Date().toISOString(),
81
+ ...(typeof parsed.validatedAt === 'string' ? { validatedAt: parsed.validatedAt } : {}),
82
+ ...(parsed.lastCheck && typeof parsed.lastCheck === 'object' ? { lastCheck: parsed.lastCheck } : {}),
83
+ }
84
+ } catch {
85
+ return null
86
+ }
87
+ }
88
+
89
+ private write(file: KeyFile): void {
90
+ securePrivateDirectory(dirname(this.path))
91
+ durableAtomicWriteFileSync(this.path, JSON.stringify(file, null, 2), { mode: 0o600 })
92
+ }
93
+
94
+ /** The live key and where it came from, or null when none is reachable. */
95
+ resolve(): { key: string; source: Exclude<FirefliesKeySource, 'none'> } | null {
96
+ const fromEnv = process.env[FIREFLIES_KEY_ENV]?.trim()
97
+ if (fromEnv) return { key: fromEnv, source: 'env' }
98
+ const file = this.read()
99
+ return file ? { key: file.key, source: 'config' } : null
100
+ }
101
+
102
+ key(): string | null {
103
+ return this.resolve()?.key ?? null
104
+ }
105
+
106
+ save(rawKey: unknown): { savedAt: string } {
107
+ if (typeof rawKey !== 'string') {
108
+ throw new FirefliesKeyError(400, 'key_required', 'A Fireflies API key is required.')
109
+ }
110
+ const key = rawKey.trim()
111
+ if (key.length < FIREFLIES_KEY_MIN_LENGTH || key.length > FIREFLIES_KEY_MAX_LENGTH) {
112
+ throw new FirefliesKeyError(400, 'key_length_invalid', `The key should be ${FIREFLIES_KEY_MIN_LENGTH} to ${FIREFLIES_KEY_MAX_LENGTH} characters.`)
113
+ }
114
+ if (!FIREFLIES_KEY_PATTERN.test(key)) {
115
+ throw new FirefliesKeyError(400, 'key_shape_invalid', 'The key contains spaces or characters a Fireflies key never has.')
116
+ }
117
+ const savedAt = new Date().toISOString()
118
+ this.write({ version: 1, key, savedAt })
119
+ return { savedAt }
120
+ }
121
+
122
+ delete(): void {
123
+ try {
124
+ const stat = lstatSync(this.path)
125
+ if (stat.isSymbolicLink() || !stat.isFile()) {
126
+ throw new FirefliesKeyError(500, 'key_path_unsafe', 'The stored key path is not a regular file.')
127
+ }
128
+ unlinkSync(this.path)
129
+ } catch (error) {
130
+ if (error instanceof FirefliesKeyError) throw error
131
+ // Absent is the desired end state.
132
+ }
133
+ }
134
+
135
+ /** Remember what the vendor said, so status does not have to spend a call. */
136
+ recordCheck(check: FirefliesKeyCheck): void {
137
+ const file = this.read()
138
+ if (!file) return
139
+ const { cached: _cached, ...rest } = check
140
+ this.write({
141
+ ...file,
142
+ ...(check.state === 'ok' ? { validatedAt: check.checkedAt } : {}),
143
+ lastCheck: rest,
144
+ })
145
+ }
146
+
147
+ status(): FirefliesKeyStatus {
148
+ const file = this.read()
149
+ const resolved = this.resolve()
150
+ if (!resolved) return { configured: false, source: 'none' }
151
+ return {
152
+ configured: true,
153
+ source: resolved.source,
154
+ ...(file?.savedAt ? { savedAt: file.savedAt } : {}),
155
+ ...(file?.validatedAt ? { validatedAt: file.validatedAt } : {}),
156
+ ...(file?.lastCheck ? { lastCheck: file.lastCheck } : {}),
157
+ }
158
+ }
159
+ }
160
+
161
+ let keyStore: FirefliesKeyStore | null = null
162
+ let budget: FirefliesBudget | null = null
163
+ let client: FirefliesClient | null = null
164
+
165
+ export function getFirefliesKeyStore(): FirefliesKeyStore {
166
+ keyStore ??= new FirefliesKeyStore()
167
+ return keyStore
168
+ }
169
+
170
+ export function getFirefliesBudget(): FirefliesBudget {
171
+ budget ??= new FirefliesBudget({ path: importsRoot('.fireflies-budget.json') })
172
+ return budget
173
+ }
174
+
175
+ export function getFirefliesClient(): FirefliesClient {
176
+ client ??= new FirefliesClient({
177
+ transport: createFetchTransport(),
178
+ key: () => getFirefliesKeyStore().key(),
179
+ budget: getFirefliesBudget(),
180
+ })
181
+ return client
182
+ }