@gotcos/glasses-server 6.46.0 → 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 (39) hide show
  1. package/CHANGELOG.md +21 -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/g2-ops-handoff.ts +15 -1
  8. package/server/lib/imported-library-rows.ts +616 -0
  9. package/server/lib/imported-meeting-library.ts +608 -0
  10. package/server/lib/maintenance-lifecycle.ts +14 -0
  11. package/server/lib/meeting-actions-store.ts +478 -0
  12. package/server/lib/meeting-actions.ts +2583 -0
  13. package/server/lib/meeting-corrections.ts +32 -1
  14. package/server/lib/meeting-decisions.ts +223 -0
  15. package/server/lib/meeting-engine/align.ts +167 -0
  16. package/server/lib/meeting-engine/attribute.ts +265 -0
  17. package/server/lib/meeting-engine/evidence.ts +428 -0
  18. package/server/lib/meeting-engine/pairing.ts +327 -0
  19. package/server/lib/meeting-engine/render.ts +694 -0
  20. package/server/lib/meeting-engine/split.ts +242 -0
  21. package/server/lib/meeting-engine/worker.ts +238 -0
  22. package/server/lib/meeting-engine-mode.ts +197 -0
  23. package/server/lib/meeting-file-guards.ts +141 -0
  24. package/server/lib/meeting-import.ts +763 -0
  25. package/server/lib/meeting-library-search.ts +146 -11
  26. package/server/lib/meeting-parse.ts +184 -0
  27. package/server/lib/meeting-store.ts +108 -275
  28. package/server/lib/meeting-suggestion-sides.ts +242 -0
  29. package/server/lib/morning-brief-runtime.ts +20 -8
  30. package/server/lib/pipeline-runner.ts +227 -0
  31. package/server/lib/voice-evidence-guard.ts +87 -0
  32. package/server/routes/fireflies-key.ts +102 -0
  33. package/server/routes/meeting-actions.ts +82 -0
  34. package/server/routes/meeting-engine.ts +52 -0
  35. package/server/routes/meeting-import.ts +67 -0
  36. package/server/routes/meeting-suggestions.ts +66 -0
  37. package/server/routes/meeting.ts +117 -10
  38. package/server/routes/meetings.ts +205 -37
  39. package/server/routes/voice.ts +18 -0
@@ -0,0 +1,141 @@
1
+ // Filesystem guards shared by every meeting library on this box.
2
+ //
3
+ // These were private methods on MeetingStore. 6.47.0 gives the imported library
4
+ // its own root under the data home, and it needs the SAME guards rather than a
5
+ // second, subtly different copy of them: a symlinked month folder, a meeting
6
+ // file that is really a link to ~/.ssh/id_ed25519, or a "markdown" file of
7
+ // 400 MB are the three ways a directory of user-named files becomes a read
8
+ // primitive, and each is closed here once.
9
+ //
10
+ // Every rule is carried over unchanged:
11
+ // - lstat before open, so a symlink is refused rather than followed;
12
+ // - realpath containment, and the parent must be the directory we meant, so
13
+ // `../` in a name cannot climb out;
14
+ // - O_NOFOLLOW on the open itself, which closes the window between the lstat
15
+ // and the open;
16
+ // - a byte ceiling, because the caller decides what "too big to read" means
17
+ // and the answer differs for markdown and for a sidecar.
18
+
19
+ import {
20
+ closeSync,
21
+ constants,
22
+ existsSync,
23
+ fstatSync,
24
+ lstatSync,
25
+ openSync,
26
+ readFileSync,
27
+ readSync,
28
+ realpathSync,
29
+ } from 'node:fs'
30
+ import { dirname, join, sep } from 'node:path'
31
+
32
+ /** Markdown ceiling. A meeting record past this is not a meeting record. */
33
+ export const MAX_MEETING_BYTES = 10 * 1024 * 1024
34
+
35
+ export class UnsafeMeetingDirectoryError extends Error {
36
+ constructor(message: string) {
37
+ super(message)
38
+ this.name = 'UnsafeMeetingDirectoryError'
39
+ }
40
+ }
41
+
42
+ export function isContained(parent: string, child: string): boolean {
43
+ return child === parent || child.startsWith(`${parent}${sep}`)
44
+ }
45
+
46
+ /**
47
+ * Realpath of an existing root, or null when it does not exist.
48
+ *
49
+ * `unsafe` builds the error thrown when the root is a symlink or not a
50
+ * directory, so each caller can keep its own error contract: MeetingStore
51
+ * throws a MeetingStoreError with a status and a code, and the imported library
52
+ * throws its own.
53
+ */
54
+ export function existingRootRealpath(
55
+ root: string,
56
+ unsafe: () => Error = () => new UnsafeMeetingDirectoryError(`Unsafe meeting directory: ${root}`),
57
+ ): string | null {
58
+ if (!existsSync(root)) return null
59
+ const stat = lstatSync(root)
60
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
61
+ throw unsafe()
62
+ }
63
+ return realpathSync(root)
64
+ }
65
+
66
+ export function safeDirectoryRealpath(path: string, parentReal: string): string | null {
67
+ try {
68
+ const stat = lstatSync(path)
69
+ if (stat.isSymbolicLink() || !stat.isDirectory()) return null
70
+ const real = realpathSync(path)
71
+ return isContained(parentReal, real) && dirname(real) === parentReal ? real : null
72
+ } catch {
73
+ return null
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Read a whole file inside a verified directory, or null.
79
+ *
80
+ * `maxBytes` defaults to the markdown ceiling. The imported library passes a
81
+ * larger one for its JSON sidecars, which legitimately run to megabytes.
82
+ */
83
+ export function safeReadFile(
84
+ directory: string,
85
+ directoryReal: string,
86
+ filename: string,
87
+ maxBytes: number = MAX_MEETING_BYTES,
88
+ ): string | null {
89
+ const filepath = join(directory, filename)
90
+ let fd: number | null = null
91
+ try {
92
+ const linkStat = lstatSync(filepath)
93
+ if (linkStat.isSymbolicLink() || !linkStat.isFile()) return null
94
+ const real = realpathSync(filepath)
95
+ if (!isContained(directoryReal, real) || dirname(real) !== directoryReal) return null
96
+ fd = openSync(filepath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0))
97
+ const stat = fstatSync(fd)
98
+ if (!stat.isFile() || stat.size > maxBytes) return null
99
+ return readFileSync(fd, 'utf8')
100
+ } catch {
101
+ return null
102
+ } finally {
103
+ if (fd !== null) {
104
+ try { closeSync(fd) } catch { /* already closed */ }
105
+ }
106
+ }
107
+ }
108
+
109
+ /** Read only the first `bytes`, with the same guards as safeReadFile.
110
+ *
111
+ * Exists so a list can lift one field out of a sidecar without reading it
112
+ * whole: sidecars run to megabytes (1.3 MB for a 32-minute meeting) and would
113
+ * also trip the markdown ceiling. */
114
+ export function safeReadFileHead(
115
+ directory: string,
116
+ directoryReal: string,
117
+ filename: string,
118
+ bytes: number,
119
+ ): string | null {
120
+ const filepath = join(directory, filename)
121
+ let fd: number | null = null
122
+ try {
123
+ const linkStat = lstatSync(filepath)
124
+ if (linkStat.isSymbolicLink() || !linkStat.isFile()) return null
125
+ const real = realpathSync(filepath)
126
+ if (!isContained(directoryReal, real) || dirname(real) !== directoryReal) return null
127
+ fd = openSync(filepath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0))
128
+ const stat = fstatSync(fd)
129
+ if (!stat.isFile()) return null
130
+ const buffer = Buffer.alloc(Math.min(bytes, stat.size))
131
+ if (buffer.length === 0) return ''
132
+ const read = readSync(fd, buffer, 0, buffer.length, 0)
133
+ return buffer.subarray(0, read).toString('utf8')
134
+ } catch {
135
+ return null
136
+ } finally {
137
+ if (fd !== null) {
138
+ try { closeSync(fd) } catch { /* already closed */ }
139
+ }
140
+ }
141
+ }