@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.
- package/CHANGELOG.md +21 -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/g2-ops-handoff.ts +15 -1
- 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 +205 -37
- package/server/routes/voice.ts +18 -0
|
@@ -7,20 +7,47 @@ 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'
|
|
17
15
|
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'
|
|
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'
|
|
20
36
|
|
|
21
37
|
const MONTH_QUERY = /^\d{4}-(0[1-9]|1[0-2])$/
|
|
22
38
|
const DAY_QUERY = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/
|
|
23
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
|
+
|
|
24
51
|
function withStandaloneIdentity(meeting: MeetingMeta): MeetingMeta {
|
|
25
52
|
return {
|
|
26
53
|
...meeting,
|
|
@@ -73,25 +100,78 @@ function parseListFilters(query: { month?: unknown; day?: unknown }): {
|
|
|
73
100
|
return { month, day }
|
|
74
101
|
}
|
|
75
102
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
function uniqueSortedMonths(groups: string[][]): string[] {
|
|
104
|
+
return [...new Set(groups.flat())].sort().reverse()
|
|
105
|
+
}
|
|
106
|
+
|
|
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),
|
|
84
136
|
}
|
|
85
|
-
return [...counts.entries()]
|
|
86
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
87
|
-
.map(([date, count]) => ({ date, count }))
|
|
88
137
|
}
|
|
89
138
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
|
92
167
|
}
|
|
93
168
|
|
|
94
|
-
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 {
|
|
95
175
|
const router = Router()
|
|
96
176
|
|
|
97
177
|
// GET /api/meetings?limit=20&domain=all
|
|
@@ -126,6 +206,7 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
126
206
|
const listOptions = { limit: sourceLimit, domain, month: filters.month, day: filters.day }
|
|
127
207
|
|
|
128
208
|
if (library.layout === 'direct') {
|
|
209
|
+
const imported = importedContribution(listOptions, importsLibrary)
|
|
129
210
|
const operations = cosOperationsMeetingsConfigured()
|
|
130
211
|
? listCosOperationsMeetings(listOptions)
|
|
131
212
|
: []
|
|
@@ -133,24 +214,30 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
133
214
|
? listDirectLibraryMeetings({ limit: sourceLimit, month: filters.month, day: filters.day })
|
|
134
215
|
: []
|
|
135
216
|
const standalone = store.list(listOptions).map(withStandaloneIdentity)
|
|
136
|
-
|
|
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)
|
|
137
227
|
const months = uniqueSortedMonths([
|
|
138
228
|
...(cosOperationsMeetingsConfigured() ? [listCosOperationsMeetingMonths(domain)] : []),
|
|
139
229
|
...(domain === 'all' || domain === 'library' ? [listDirectLibraryMeetingMonths()] : []),
|
|
140
230
|
store.listMonths(),
|
|
231
|
+
importedLibraryMonths(importsLibrary),
|
|
141
232
|
])
|
|
142
233
|
const days = filters.month
|
|
143
|
-
?
|
|
144
|
-
...(cosOperationsMeetingsConfigured() ? [listCosOperationsMeetingDays(filters.month, domain)] : []),
|
|
145
|
-
...(domain === 'all' || domain === 'library' ? [listDirectLibraryMeetingDays(filters.month)] : []),
|
|
146
|
-
store.listDayCounts(filters.month),
|
|
147
|
-
])
|
|
234
|
+
? supersededDayCounts(filters.month, 'direct', { domain, store, library: importsLibrary })
|
|
148
235
|
: []
|
|
149
236
|
res.json({
|
|
150
237
|
meetings,
|
|
151
238
|
months,
|
|
152
239
|
days,
|
|
153
|
-
source: operations.length > 0 ? 'mixed_library' : 'direct_library',
|
|
240
|
+
source: operations.length > 0 || imported.present ? 'mixed_library' : 'direct_library',
|
|
154
241
|
layout: 'direct',
|
|
155
242
|
root: library.root,
|
|
156
243
|
rootFingerprint: library.rootFingerprint,
|
|
@@ -161,17 +248,74 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
161
248
|
}
|
|
162
249
|
|
|
163
250
|
if (library.layout === 'multi_domain') {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
251
|
+
if (g2RecordingsReachOperations()) {
|
|
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({
|
|
257
|
+
limit,
|
|
258
|
+
domain,
|
|
259
|
+
month: filters.month,
|
|
260
|
+
day: filters.day,
|
|
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
|
|
269
|
+
res.json({
|
|
270
|
+
meetings,
|
|
271
|
+
months: listCosOperationsMeetingMonths(domain),
|
|
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
|
+
: [],
|
|
284
|
+
source: 'cos_operations',
|
|
285
|
+
layout: 'multi_domain',
|
|
286
|
+
root: library.root,
|
|
287
|
+
rootFingerprint: library.rootFingerprint,
|
|
288
|
+
meetingCount: meetings.length,
|
|
289
|
+
warnings: library.warnings,
|
|
290
|
+
})
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// No COS pipeline can file this Mac's G2 recordings into the operations folder (a Starter Kit library
|
|
295
|
+
// without COS_SCRIPTS_DIR, 2026-09-14), so they live only in this server's store. List them beside the
|
|
296
|
+
// operations rows. Operations rows go first, so a copy that did reach operations wins by sessionId, and
|
|
297
|
+
// the day counts skip the store row it covers. Day counts always describe the whole month.
|
|
298
|
+
const imported = importedContribution(listOptions, importsLibrary)
|
|
299
|
+
const operations = listCosOperationsMeetings(listOptions)
|
|
300
|
+
const standalone = store.list(listOptions).map(withStandaloneIdentity)
|
|
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
|
+
: []
|
|
170
310
|
res.json({
|
|
171
311
|
meetings,
|
|
172
|
-
months:
|
|
173
|
-
|
|
174
|
-
|
|
312
|
+
months: uniqueSortedMonths([
|
|
313
|
+
listCosOperationsMeetingMonths(domain),
|
|
314
|
+
store.listMonths(),
|
|
315
|
+
importedLibraryMonths(importsLibrary),
|
|
316
|
+
]),
|
|
317
|
+
days,
|
|
318
|
+
source: standalone.length > 0 || imported.present ? 'mixed_library' : 'cos_operations',
|
|
175
319
|
layout: 'multi_domain',
|
|
176
320
|
root: library.root,
|
|
177
321
|
rootFingerprint: library.rootFingerprint,
|
|
@@ -181,17 +325,23 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
181
325
|
return
|
|
182
326
|
}
|
|
183
327
|
|
|
184
|
-
const
|
|
185
|
-
|
|
328
|
+
const imported = importedContribution(listOptions, importsLibrary)
|
|
329
|
+
const standalone = store.list({
|
|
330
|
+
limit: sourceLimit,
|
|
186
331
|
domain,
|
|
187
332
|
month: filters.month,
|
|
188
333
|
day: filters.day,
|
|
189
334
|
}).map(withStandaloneIdentity)
|
|
335
|
+
const meetings = mergeMeetingSources([
|
|
336
|
+
imported.derived,
|
|
337
|
+
imported.drop(standalone),
|
|
338
|
+
imported.drop(imported.imports),
|
|
339
|
+
], limit, sourceLimit)
|
|
190
340
|
res.json({
|
|
191
341
|
meetings,
|
|
192
|
-
months: store.listMonths(),
|
|
193
|
-
days: filters.month ?
|
|
194
|
-
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',
|
|
195
345
|
layout: 'standalone',
|
|
196
346
|
meetingCount: meetings.length,
|
|
197
347
|
})
|
|
@@ -243,6 +393,12 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
243
393
|
}
|
|
244
394
|
}
|
|
245
395
|
|
|
396
|
+
const imported = importedMeetingDetail(domain, month, filename, store, importsLibrary)
|
|
397
|
+
if (imported) {
|
|
398
|
+
res.json(imported)
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
246
402
|
if (cosOperationsMeetingsConfigured()) {
|
|
247
403
|
const detail = getCosOperationsMeetingDetail(domain, month, filename)
|
|
248
404
|
if (detail) {
|
|
@@ -273,6 +429,18 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
|
|
|
273
429
|
}
|
|
274
430
|
}
|
|
275
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
|
+
|
|
276
444
|
if (cosOperationsMeetingsConfigured()) {
|
|
277
445
|
const detail = getCosOperationsMeetingDetail(
|
|
278
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'})
|