@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
|
@@ -7,10 +7,8 @@ import {
|
|
|
7
7
|
getDirectLibraryMeetingDetail,
|
|
8
8
|
getCosOperationsMeetingDetail,
|
|
9
9
|
listDirectLibraryMeetings,
|
|
10
|
-
listDirectLibraryMeetingDays,
|
|
11
10
|
listDirectLibraryMeetingMonths,
|
|
12
11
|
listCosOperationsMeetings,
|
|
13
|
-
listCosOperationsMeetingDays,
|
|
14
12
|
listCosOperationsMeetingMonths,
|
|
15
13
|
resolveMeetingLibrary,
|
|
16
14
|
} from '../lib/cos-operations-meetings.js'
|
|
@@ -18,10 +16,38 @@ import type { MeetingMeta } from '../lib/meeting-store.js'
|
|
|
18
16
|
import { meetingListLimit } from '../lib/meeting-store.js'
|
|
19
17
|
import { searchMeetingLibrary } from '../lib/meeting-library-search.js'
|
|
20
18
|
import { g2RecordingsReachOperations } from '../lib/g2-ops-handoff.js'
|
|
19
|
+
import {
|
|
20
|
+
type ImportedMeetingLibrary,
|
|
21
|
+
IMPORTED_DOMAIN,
|
|
22
|
+
getImportedMeetingLibrary,
|
|
23
|
+
} from '../lib/imported-meeting-library.js'
|
|
24
|
+
import {
|
|
25
|
+
type DerivedRecordSummary,
|
|
26
|
+
derivedSourcesFor,
|
|
27
|
+
dropSupersededRows,
|
|
28
|
+
importedLibraryMonths,
|
|
29
|
+
listImportedLibraryRows,
|
|
30
|
+
readDerivedRecords,
|
|
31
|
+
supersededDayCounts,
|
|
32
|
+
supersededFromRows,
|
|
33
|
+
supersededInputsOf,
|
|
34
|
+
withDerivedIdentity,
|
|
35
|
+
} from '../lib/imported-library-rows.js'
|
|
21
36
|
|
|
22
37
|
const MONTH_QUERY = /^\d{4}-(0[1-9]|1[0-2])$/
|
|
23
38
|
const DAY_QUERY = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/
|
|
24
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The only filenames the imported branch of detail will answer for.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately stricter than "is it in the imports folder": a G2 recording whose
|
|
44
|
+
* TITLE contains the word "merged" produces a store filename that must keep
|
|
45
|
+
* resolving through the store, and a hand-dropped or iCloud-conflicted file in
|
|
46
|
+
* the imports root must not be served as a record. The branch falls through on a
|
|
47
|
+
* miss rather than 404ing, so a near-miss is answered by the next source.
|
|
48
|
+
*/
|
|
49
|
+
export const IMPORTED_DETAIL_FILENAME = /^\d{4}-\d{2}-\d{2}_(?:fireflies|merged|piece)_[0-9a-f]{16}\.md$/
|
|
50
|
+
|
|
25
51
|
function withStandaloneIdentity(meeting: MeetingMeta): MeetingMeta {
|
|
26
52
|
return {
|
|
27
53
|
...meeting,
|
|
@@ -74,29 +100,78 @@ function parseListFilters(query: { month?: unknown; day?: unknown }): {
|
|
|
74
100
|
return { month, day }
|
|
75
101
|
}
|
|
76
102
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
): Array<{ date: string; count: number }> {
|
|
80
|
-
const counts = new Map<string, number>()
|
|
81
|
-
for (const group of groups) {
|
|
82
|
-
for (const { date, count } of group) {
|
|
83
|
-
counts.set(date, (counts.get(date) ?? 0) + count)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return [...counts.entries()]
|
|
87
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
88
|
-
.map(([date, count]) => ({ date, count }))
|
|
103
|
+
function uniqueSortedMonths(groups: string[][]): string[] {
|
|
104
|
+
return [...new Set(groups.flat())].sort().reverse()
|
|
89
105
|
}
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Detail for one record of the imported library, or null to fall through.
|
|
109
|
+
*
|
|
110
|
+
* NULL, NEVER 404. The three detail sources share one URL shape, and a record
|
|
111
|
+
* that is not here may still be an operations meeting or a store recording, so a
|
|
112
|
+
* miss has to hand the request on. The two gates — the routing domain and the
|
|
113
|
+
* strict filename — are what keep an ordinary meeting out of this branch.
|
|
114
|
+
*
|
|
115
|
+
* A merged record answers with `sources[]`, each input naming the record that
|
|
116
|
+
* still holds it. Nothing is read from the client: the record's own sidecar is
|
|
117
|
+
* the only thing consulted.
|
|
118
|
+
*/
|
|
119
|
+
function importedMeetingDetail(
|
|
120
|
+
domain: string,
|
|
121
|
+
month: string,
|
|
122
|
+
filename: string,
|
|
123
|
+
store: MeetingStore,
|
|
124
|
+
library: ImportedMeetingLibrary,
|
|
125
|
+
): MeetingMeta | null {
|
|
126
|
+
if (domain !== IMPORTED_DOMAIN) return null
|
|
127
|
+
if (!IMPORTED_DETAIL_FILENAME.test(filename)) return null
|
|
128
|
+
const detail = library.detail(month, filename)
|
|
129
|
+
if (!detail) return null
|
|
130
|
+
if (detail.librarySource !== 'blended') return detail as MeetingMeta
|
|
131
|
+
const record = readDerivedRecords(library).find(entry => entry.recordId === detail.recordId)
|
|
132
|
+
if (!record) return detail as MeetingMeta
|
|
133
|
+
return {
|
|
134
|
+
...withDerivedIdentity(detail as MeetingMeta, record),
|
|
135
|
+
sources: derivedSourcesFor(record, store),
|
|
136
|
+
}
|
|
93
137
|
}
|
|
94
138
|
|
|
95
|
-
|
|
96
|
-
|
|
139
|
+
/**
|
|
140
|
+
* The imported library's contribution to one list request.
|
|
141
|
+
*
|
|
142
|
+
* Read ONCE per request: the derived sidecars decide both what the derived rows
|
|
143
|
+
* say about themselves and which G2, import and long-original rows they have
|
|
144
|
+
* taken over, and reading them twice would let a write between the two reads
|
|
145
|
+
* show a meeting and its merged replacement side by side.
|
|
146
|
+
*/
|
|
147
|
+
function importedContribution(
|
|
148
|
+
options: { limit: number; domain: string; month?: string; day?: string },
|
|
149
|
+
library: ImportedMeetingLibrary,
|
|
150
|
+
): {
|
|
151
|
+
imports: MeetingMeta[]
|
|
152
|
+
derived: MeetingMeta[]
|
|
153
|
+
records: DerivedRecordSummary[]
|
|
154
|
+
drop: (rows: MeetingMeta[]) => MeetingMeta[]
|
|
155
|
+
present: boolean
|
|
156
|
+
} {
|
|
157
|
+
const records = readDerivedRecords(library)
|
|
158
|
+
const { imports, derived } = listImportedLibraryRows(options, library, records)
|
|
159
|
+
const superseded = supersededInputsOf(records)
|
|
160
|
+
return {
|
|
161
|
+
imports,
|
|
162
|
+
derived,
|
|
163
|
+
records,
|
|
164
|
+
drop: rows => dropSupersededRows(rows, superseded),
|
|
165
|
+
present: imports.length > 0 || derived.length > 0,
|
|
166
|
+
}
|
|
97
167
|
}
|
|
98
168
|
|
|
99
|
-
export function createMeetingsRouter(
|
|
169
|
+
export function createMeetingsRouter(
|
|
170
|
+
store: MeetingStore = getMeetingStore(),
|
|
171
|
+
// Injectable so a test can drive a whole imports library without reaching the
|
|
172
|
+
// process-wide data home. Production passes nothing and gets the singleton.
|
|
173
|
+
importsLibrary: ImportedMeetingLibrary = getImportedMeetingLibrary(),
|
|
174
|
+
): Router {
|
|
100
175
|
const router = Router()
|
|
101
176
|
|
|
102
177
|
// GET /api/meetings?limit=20&domain=all
|
|
@@ -131,6 +206,7 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
131
206
|
const listOptions = { limit: sourceLimit, domain, month: filters.month, day: filters.day }
|
|
132
207
|
|
|
133
208
|
if (library.layout === 'direct') {
|
|
209
|
+
const imported = importedContribution(listOptions, importsLibrary)
|
|
134
210
|
const operations = cosOperationsMeetingsConfigured()
|
|
135
211
|
? listCosOperationsMeetings(listOptions)
|
|
136
212
|
: []
|
|
@@ -138,24 +214,30 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
138
214
|
? listDirectLibraryMeetings({ limit: sourceLimit, month: filters.month, day: filters.day })
|
|
139
215
|
: []
|
|
140
216
|
const standalone = store.list(listOptions).map(withStandaloneIdentity)
|
|
141
|
-
|
|
217
|
+
// Derived rows lead. They carry the sessionId of the earliest capture they
|
|
218
|
+
// hold, so leading makes a merged record win that session outright even if
|
|
219
|
+
// its sidecar became unreadable and the supersession filter went quiet.
|
|
220
|
+
const meetings = mergeMeetingSources([
|
|
221
|
+
imported.derived,
|
|
222
|
+
imported.drop(operations),
|
|
223
|
+
imported.drop(direct),
|
|
224
|
+
imported.drop(standalone),
|
|
225
|
+
imported.drop(imported.imports),
|
|
226
|
+
], limit, sourceLimit)
|
|
142
227
|
const months = uniqueSortedMonths([
|
|
143
228
|
...(cosOperationsMeetingsConfigured() ? [listCosOperationsMeetingMonths(domain)] : []),
|
|
144
229
|
...(domain === 'all' || domain === 'library' ? [listDirectLibraryMeetingMonths()] : []),
|
|
145
230
|
store.listMonths(),
|
|
231
|
+
importedLibraryMonths(importsLibrary),
|
|
146
232
|
])
|
|
147
233
|
const days = filters.month
|
|
148
|
-
?
|
|
149
|
-
...(cosOperationsMeetingsConfigured() ? [listCosOperationsMeetingDays(filters.month, domain)] : []),
|
|
150
|
-
...(domain === 'all' || domain === 'library' ? [listDirectLibraryMeetingDays(filters.month)] : []),
|
|
151
|
-
store.listDayCounts(filters.month),
|
|
152
|
-
])
|
|
234
|
+
? supersededDayCounts(filters.month, 'direct', { domain, store, library: importsLibrary })
|
|
153
235
|
: []
|
|
154
236
|
res.json({
|
|
155
237
|
meetings,
|
|
156
238
|
months,
|
|
157
239
|
days,
|
|
158
|
-
source: operations.length > 0 ? 'mixed_library' : 'direct_library',
|
|
240
|
+
source: operations.length > 0 || imported.present ? 'mixed_library' : 'direct_library',
|
|
159
241
|
layout: 'direct',
|
|
160
242
|
root: library.root,
|
|
161
243
|
rootFingerprint: library.rootFingerprint,
|
|
@@ -167,16 +249,38 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
167
249
|
|
|
168
250
|
if (library.layout === 'multi_domain') {
|
|
169
251
|
if (g2RecordingsReachOperations()) {
|
|
170
|
-
|
|
252
|
+
// The pipeline owns this tree, so the tree is the whole library: no
|
|
253
|
+
// imports, no derived records from `data/imports`. A merge here was
|
|
254
|
+
// spliced into the Fireflies scribe, which declares the captures it
|
|
255
|
+
// holds, so the capture's own row is dropped from its own tree.
|
|
256
|
+
const rows = listCosOperationsMeetings({
|
|
171
257
|
limit,
|
|
172
258
|
domain,
|
|
173
259
|
month: filters.month,
|
|
174
260
|
day: filters.day,
|
|
175
261
|
})
|
|
262
|
+
const declaredSessions = supersededFromRows(rows).g2Sessions
|
|
263
|
+
const meetings = dropSupersededRows(rows, { g2Sessions: declaredSessions, importRecordIds: new Set<string>(), isEmpty: declaredSessions.size === 0 })
|
|
264
|
+
// ONLY WHEN THE LIST SAW THE WHOLE MONTH. The row list is capped, and a capped
|
|
265
|
+
// list knows about fewer merged scribes than the month holds, so handing its
|
|
266
|
+
// sessions to an UNCAPPED day count would subtract too little and put the dot
|
|
267
|
+
// back above the row. A capped page pays for the scan instead.
|
|
268
|
+
const listSawWholeMonth = rows.length < limit
|
|
176
269
|
res.json({
|
|
177
270
|
meetings,
|
|
178
271
|
months: listCosOperationsMeetingMonths(domain),
|
|
179
|
-
|
|
272
|
+
// The rows above already read every scribe in this month and told us which
|
|
273
|
+
// sessions the merged ones hold. Handing that over is the difference between
|
|
274
|
+
// one read per file and two on every month request.
|
|
275
|
+
days: filters.month
|
|
276
|
+
? supersededDayCounts(filters.month, 'multi_domain', {
|
|
277
|
+
domain,
|
|
278
|
+
store,
|
|
279
|
+
library: importsLibrary,
|
|
280
|
+
pipeline: true,
|
|
281
|
+
...(listSawWholeMonth ? { declaredSessions } : {}),
|
|
282
|
+
})
|
|
283
|
+
: [],
|
|
180
284
|
source: 'cos_operations',
|
|
181
285
|
layout: 'multi_domain',
|
|
182
286
|
root: library.root,
|
|
@@ -191,28 +295,27 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
191
295
|
// without COS_SCRIPTS_DIR, 2026-09-14), so they live only in this server's store. List them beside the
|
|
192
296
|
// operations rows. Operations rows go first, so a copy that did reach operations wins by sessionId, and
|
|
193
297
|
// the day counts skip the store row it covers. Day counts always describe the whole month.
|
|
298
|
+
const imported = importedContribution(listOptions, importsLibrary)
|
|
194
299
|
const operations = listCosOperationsMeetings(listOptions)
|
|
195
300
|
const standalone = store.list(listOptions).map(withStandaloneIdentity)
|
|
196
|
-
const meetings = mergeMeetingSources([
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const covered = new Set(monthOperations.flatMap(row => (row.sessionId ? [row.sessionId] : [])))
|
|
206
|
-
days = mergeDayCounts([
|
|
207
|
-
listCosOperationsMeetingDays(filters.month, domain),
|
|
208
|
-
dayCountsOf(monthStore.filter(row => !row.sessionId || !covered.has(row.sessionId))),
|
|
209
|
-
])
|
|
210
|
-
}
|
|
301
|
+
const meetings = mergeMeetingSources([
|
|
302
|
+
imported.derived,
|
|
303
|
+
imported.drop(operations),
|
|
304
|
+
imported.drop(standalone),
|
|
305
|
+
imported.drop(imported.imports),
|
|
306
|
+
], limit, sourceLimit)
|
|
307
|
+
const days = filters.month
|
|
308
|
+
? supersededDayCounts(filters.month, 'multi_domain', { domain, store, library: importsLibrary, pipeline: false })
|
|
309
|
+
: []
|
|
211
310
|
res.json({
|
|
212
311
|
meetings,
|
|
213
|
-
months: uniqueSortedMonths([
|
|
312
|
+
months: uniqueSortedMonths([
|
|
313
|
+
listCosOperationsMeetingMonths(domain),
|
|
314
|
+
store.listMonths(),
|
|
315
|
+
importedLibraryMonths(importsLibrary),
|
|
316
|
+
]),
|
|
214
317
|
days,
|
|
215
|
-
source: standalone.length > 0 ? 'mixed_library' : 'cos_operations',
|
|
318
|
+
source: standalone.length > 0 || imported.present ? 'mixed_library' : 'cos_operations',
|
|
216
319
|
layout: 'multi_domain',
|
|
217
320
|
root: library.root,
|
|
218
321
|
rootFingerprint: library.rootFingerprint,
|
|
@@ -222,17 +325,23 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
222
325
|
return
|
|
223
326
|
}
|
|
224
327
|
|
|
225
|
-
const
|
|
226
|
-
|
|
328
|
+
const imported = importedContribution(listOptions, importsLibrary)
|
|
329
|
+
const standalone = store.list({
|
|
330
|
+
limit: sourceLimit,
|
|
227
331
|
domain,
|
|
228
332
|
month: filters.month,
|
|
229
333
|
day: filters.day,
|
|
230
334
|
}).map(withStandaloneIdentity)
|
|
335
|
+
const meetings = mergeMeetingSources([
|
|
336
|
+
imported.derived,
|
|
337
|
+
imported.drop(standalone),
|
|
338
|
+
imported.drop(imported.imports),
|
|
339
|
+
], limit, sourceLimit)
|
|
231
340
|
res.json({
|
|
232
341
|
meetings,
|
|
233
|
-
months: store.listMonths(),
|
|
234
|
-
days: filters.month ?
|
|
235
|
-
source: 'standalone_recordings',
|
|
342
|
+
months: uniqueSortedMonths([store.listMonths(), importedLibraryMonths(importsLibrary)]),
|
|
343
|
+
days: filters.month ? supersededDayCounts(filters.month, 'standalone', { domain, store, library: importsLibrary }) : [],
|
|
344
|
+
source: imported.present ? 'mixed_library' : 'standalone_recordings',
|
|
236
345
|
layout: 'standalone',
|
|
237
346
|
meetingCount: meetings.length,
|
|
238
347
|
})
|
|
@@ -284,6 +393,12 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
284
393
|
}
|
|
285
394
|
}
|
|
286
395
|
|
|
396
|
+
const imported = importedMeetingDetail(domain, month, filename, store, importsLibrary)
|
|
397
|
+
if (imported) {
|
|
398
|
+
res.json(imported)
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
287
402
|
if (cosOperationsMeetingsConfigured()) {
|
|
288
403
|
const detail = getCosOperationsMeetingDetail(domain, month, filename)
|
|
289
404
|
if (detail) {
|
|
@@ -314,6 +429,18 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
314
429
|
}
|
|
315
430
|
}
|
|
316
431
|
|
|
432
|
+
const imported = importedMeetingDetail(
|
|
433
|
+
req.params.domain,
|
|
434
|
+
req.params.month,
|
|
435
|
+
req.params.filename,
|
|
436
|
+
store,
|
|
437
|
+
importsLibrary,
|
|
438
|
+
)
|
|
439
|
+
if (imported) {
|
|
440
|
+
res.json(imported)
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
317
444
|
if (cosOperationsMeetingsConfigured()) {
|
|
318
445
|
const detail = getCosOperationsMeetingDetail(
|
|
319
446
|
req.params.domain,
|
package/server/routes/voice.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { fanOutSpeakerRename, type SpeakerRenameFanOut } from '../lib/speaker-re
|
|
|
20
20
|
import { HeldGroupError, discardHeldSamples, enrollHeldGroup, heldVoiceGroups, parseHeldMembers, previewDiscard } from '../lib/held-voice-groups.js'
|
|
21
21
|
import { previewHeldNaming, applyHeldNaming, undoHeldNaming, resumeHeldNaming, namingBatchList, resolveStoredName, NAMING_CAPABILITIES } from '../lib/held-naming-batches.js'
|
|
22
22
|
import { resolveCosOperationsDir } from '../lib/cos-operations-meetings.js'
|
|
23
|
+
import { assertVoiceEvidenceSource } from '../lib/voice-evidence-guard.js'
|
|
23
24
|
|
|
24
25
|
// These MUST match the writer in transcribe-stream.ts, which saves under
|
|
25
26
|
// dataPath(). They previously resolved relative to __dirname — i.e. inside the
|
|
@@ -364,6 +365,10 @@ voiceRouter.post('/voice/enroll-ext', async (req, res) => {
|
|
|
364
365
|
if (!name || typeof name !== 'string' || name.length < 2) {
|
|
365
366
|
return res.status(400).json({ error: 'name is required (min 2 chars)' })
|
|
366
367
|
}
|
|
368
|
+
// An imported or derived record never has audio to enrol from, and its
|
|
369
|
+
// speaker names came from a vendor rather than a voiceprint.
|
|
370
|
+
const evidenceRefusal = assertVoiceEvidenceSource([sessionId])
|
|
371
|
+
if (evidenceRefusal) return res.status(evidenceRefusal.status).json(evidenceRefusal.body)
|
|
367
372
|
|
|
368
373
|
if (!existsSync(EXT_AUDIO_DIR)) {
|
|
369
374
|
return res.json({ enrolled: 0, message: 'No ext-audio available' })
|
|
@@ -583,6 +588,19 @@ voiceRouter.post('/voice/held-groups/enroll', async (req, res) => {
|
|
|
583
588
|
const nameCheck = checkSpeakerName(req.body?.name, { ownerLabel: getOwnerSpeakerLabel() })
|
|
584
589
|
if (!nameCheck.ok) return res.status(400).json({ success:false,error:nameCheck.message,reason:nameCheck.reason })
|
|
585
590
|
const name = resolveStoredName(String(req.body.name).trim())
|
|
591
|
+
// Held samples are audio this Mac captured. A member pointing at an imported
|
|
592
|
+
// or derived record is not held audio, so it never reaches the preview.
|
|
593
|
+
//
|
|
594
|
+
// ON THE RAW BODY, BEFORE parseHeldMembers. That parser calls
|
|
595
|
+
// `normalizeSessionId`, which rewrites every colon to an underscore for the
|
|
596
|
+
// on-disk directory name, so `blended:<h16>` arrives here as
|
|
597
|
+
// `blended_<h16>` and an id-kind guard reading the parsed members never
|
|
598
|
+
// fires. Caught by the execution test, which is the only place it could be.
|
|
599
|
+
const rawMembers = Array.isArray(req.body?.members) ? req.body.members : []
|
|
600
|
+
const evidenceRefusal = assertVoiceEvidenceSource(
|
|
601
|
+
rawMembers.map((member: unknown) => (member as { sessionId?: unknown })?.sessionId),
|
|
602
|
+
)
|
|
603
|
+
if (evidenceRefusal) return res.status(evidenceRefusal.status).json(evidenceRefusal.body)
|
|
586
604
|
const members = parseHeldMembers(req.body?.members)
|
|
587
605
|
if (!req.body?.previewHash || req.body?.dryRun === true) {
|
|
588
606
|
if (name.owner && req.body?.confirm === true && req.body?.dryRun !== true) return res.status(400).json({success:false,error:'Owner confirmation requires a preview and ownerAck',reason:'owner_confirmation_required'})
|