@gotcos/glasses-server 6.21.19 → 6.21.20

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 CHANGED
@@ -1,3 +1,22 @@
1
+ ## 6.21.20
2
+
3
+ - Audio playback works at all. Every play button in the Control speaker review
4
+ returned a 404 and made no sound, on every default install, since playback
5
+ shipped. `res.sendFile` delegates to `send`, which defaults to
6
+ `dotfiles: 'ignore'` and — with no `root` set — applies that policy to the
7
+ WHOLE absolute path rather than to anything the request supplied. The default
8
+ data home is `~/.cos-glasses/data`, and `.cos-glasses` is a dot component, so
9
+ the file was found, confirmed to exist, then refused on the way out the door.
10
+ All three audio routes were affected: meeting chunks, speaker-profile samples,
11
+ and ext-audio samples.
12
+
13
+ The tests could not have caught this. They point `COS_DATA_DIR` at
14
+ `mktemp -d` — `/var/folders/...` — which cannot contain a dot component, so
15
+ the suite was structurally incapable of reproducing a default install and
16
+ stayed green while the feature was dead. The three routes now share
17
+ `sendAudioFile`, and `send-audio.test.ts` serves from a dot-directory on
18
+ purpose, over a real listener, asserting on the returned bytes.
19
+
1
20
  ## 6.21.19
2
21
 
3
22
  - Review playback falls back to ext-audio. The 7-day archive introduced in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.21.19",
3
+ "version": "6.21.20",
4
4
  "description": "COS Glasses — self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, or Cursor Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,42 @@
1
+ // Serving a retained WAV back to a reviewer.
2
+ //
3
+ // WHY THIS EXISTS AT ALL, because `res.sendFile(path)` looks like it would do.
4
+ //
5
+ // `send` (what Express delegates to) defaults to `dotfiles: 'ignore'`, and when
6
+ // no `root` option is given it applies that policy to the ENTIRE absolute path,
7
+ // not to the part a request supplied:
8
+ //
9
+ // parts = normalize(path).split(sep) // send/index.js — the whole path
10
+ // if (containsDotFile(parts)) ... error(404)
11
+ //
12
+ // The COS data home is `~/.cos-glasses/data` by default. `.cos-glasses` is a
13
+ // dot component, so every audio route 404'd on every default install — the play
14
+ // button rendered, the fetch failed, and nothing was heard. Verified directly
15
+ // against the installed module: `{}` -> 404, `{dotfiles:'allow'}` -> resolves.
16
+ //
17
+ // The tests never caught it because they point `COS_DATA_DIR` at
18
+ // `mktemp -d` (`/var/folders/...`), which structurally CANNOT contain a dot
19
+ // component. The suite was green and the feature was dead. `send-audio.test.ts`
20
+ // serves from a dot-directory on purpose.
21
+ //
22
+ // 'allow' is safe here rather than merely convenient: the dot component is ours
23
+ // (the data home), and nothing a caller supplies can introduce one. Every route
24
+ // resolves its path through a validator first — `sessionId` and speaker names
25
+ // match `[A-Za-z0-9:_-]`, chunk indices are integers, and the archive helpers
26
+ // re-check containment before returning. The path handed to this function is
27
+ // already known to exist and to live under the data dir.
28
+
29
+ import type { Response } from 'express'
30
+
31
+ /**
32
+ * Send a WAV that has already been resolved and containment-checked.
33
+ *
34
+ * Callers must have verified the file exists; a missing file here surfaces as
35
+ * send's own 404 HTML rather than the route's JSON, which is exactly the
36
+ * confusing failure this module documents.
37
+ */
38
+ export function sendAudioFile(res: Response, path: string): void {
39
+ res.type('audio/wav')
40
+ // See the file header: without this, any data home under a dot-directory 404s.
41
+ res.sendFile(path, { dotfiles: 'allow' })
42
+ }
@@ -10,6 +10,7 @@ import { cleanTranscriptLines } from '../lib/hallucination-filter.js'
10
10
  import { durableAtomicWriteFileSync } from '../lib/atomic-fs.js'
11
11
  import { appendCorrection, pendingCorrections } from '../lib/meeting-corrections.js'
12
12
  import { isSampleFromSession, untraceableSampleCount } from '../lib/training-audio-provenance.js'
13
+ import { sendAudioFile } from '../lib/send-audio.js'
13
14
  import {
14
15
  extAudioChunkPath,
15
16
  listExtAudioChunks,
@@ -1178,8 +1179,7 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
1178
1179
  })
1179
1180
  return
1180
1181
  }
1181
- res.type('audio/wav')
1182
- res.sendFile(path)
1182
+ sendAudioFile(res, path)
1183
1183
  })
1184
1184
 
1185
1185
  /** What audio a meeting still has, so the panel can show play buttons only
@@ -11,6 +11,7 @@ import { getOwnerSpeakerLabel } from '../lib/profile.js'
11
11
  import { dataPath } from '../lib/data-dir.js'
12
12
  import { purgeSpeakerCalibrationRows, relabelSpeakerCalibrationRows } from '../lib/speaker-calibration-log.js'
13
13
  import { trainingSourceFor } from '../lib/training-audio-provenance.js'
14
+ import { sendAudioFile } from '../lib/send-audio.js'
14
15
 
15
16
  // These MUST match the writer in transcribe-stream.ts, which saves under
16
17
  // dataPath(). They previously resolved relative to __dirname — i.e. inside the
@@ -471,8 +472,7 @@ voiceRouter.get('/voice/profiles/:name/sample', (req, res) => {
471
472
  })
472
473
  return
473
474
  }
474
- res.type('audio/wav')
475
- res.sendFile(wav)
475
+ sendAudioFile(res, wav)
476
476
  })
477
477
 
478
478
  // GET /api/voice/ext-audio/:sessionId/sample — hear an unidentified voice.
@@ -492,8 +492,7 @@ voiceRouter.get('/voice/ext-audio/:sessionId/sample', (req, res) => {
492
492
  })
493
493
  return
494
494
  }
495
- res.type('audio/wav')
496
- res.sendFile(wav)
495
+ sendAudioFile(res, wav)
497
496
  })
498
497
 
499
498
  // GET /api/voice/profiles — enrolled people with sample counts and provenance.