@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.
- package/CHANGELOG.md +13 -0
- package/package.json +6 -2
- package/server/index.ts +76 -0
- package/server/lib/cos-operations-meetings.ts +99 -8
- package/server/lib/fireflies-client.ts +862 -0
- package/server/lib/fireflies-key.ts +182 -0
- package/server/lib/imported-library-rows.ts +616 -0
- package/server/lib/imported-meeting-library.ts +608 -0
- package/server/lib/maintenance-lifecycle.ts +14 -0
- package/server/lib/meeting-actions-store.ts +478 -0
- package/server/lib/meeting-actions.ts +2583 -0
- package/server/lib/meeting-corrections.ts +32 -1
- package/server/lib/meeting-decisions.ts +223 -0
- package/server/lib/meeting-engine/align.ts +167 -0
- package/server/lib/meeting-engine/attribute.ts +265 -0
- package/server/lib/meeting-engine/evidence.ts +428 -0
- package/server/lib/meeting-engine/pairing.ts +327 -0
- package/server/lib/meeting-engine/render.ts +694 -0
- package/server/lib/meeting-engine/split.ts +242 -0
- package/server/lib/meeting-engine/worker.ts +238 -0
- package/server/lib/meeting-engine-mode.ts +197 -0
- package/server/lib/meeting-file-guards.ts +141 -0
- package/server/lib/meeting-import.ts +763 -0
- package/server/lib/meeting-library-search.ts +146 -11
- package/server/lib/meeting-parse.ts +184 -0
- package/server/lib/meeting-store.ts +108 -275
- package/server/lib/meeting-suggestion-sides.ts +242 -0
- package/server/lib/morning-brief-runtime.ts +20 -8
- package/server/lib/pipeline-runner.ts +227 -0
- package/server/lib/voice-evidence-guard.ts +87 -0
- package/server/routes/fireflies-key.ts +102 -0
- package/server/routes/meeting-actions.ts +82 -0
- package/server/routes/meeting-engine.ts +52 -0
- package/server/routes/meeting-import.ts +67 -0
- package/server/routes/meeting-suggestions.ts +66 -0
- package/server/routes/meeting.ts +117 -10
- package/server/routes/meetings.ts +177 -50
- 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
|
+
}
|