@pellux/goodvibes-daemon-sdk 0.30.3 → 0.33.1

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 (72) hide show
  1. package/README.md +2 -2
  2. package/dist/api-router.d.ts +4 -2
  3. package/dist/api-router.d.ts.map +1 -1
  4. package/dist/api-router.js +18 -10
  5. package/dist/artifact-upload.d.ts +9 -9
  6. package/dist/artifact-upload.d.ts.map +1 -1
  7. package/dist/artifact-upload.js +27 -19
  8. package/dist/auth-helpers.d.ts +9 -0
  9. package/dist/auth-helpers.d.ts.map +1 -0
  10. package/dist/auth-helpers.js +10 -0
  11. package/dist/automation.js +10 -10
  12. package/dist/channel-route-types.d.ts +22 -23
  13. package/dist/channel-route-types.d.ts.map +1 -1
  14. package/dist/channel-routes.d.ts.map +1 -1
  15. package/dist/channel-routes.js +37 -42
  16. package/dist/context.d.ts +27 -27
  17. package/dist/context.d.ts.map +1 -1
  18. package/dist/control-routes.d.ts +12 -13
  19. package/dist/control-routes.d.ts.map +1 -1
  20. package/dist/control-routes.js +55 -26
  21. package/dist/error-response.d.ts +10 -3
  22. package/dist/error-response.d.ts.map +1 -1
  23. package/dist/error-response.js +102 -11
  24. package/dist/http-policy.d.ts +3 -4
  25. package/dist/http-policy.d.ts.map +1 -1
  26. package/dist/http-policy.js +2 -1
  27. package/dist/index.d.ts +11 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -1
  30. package/dist/integration-routes.d.ts +1 -1
  31. package/dist/integration-routes.d.ts.map +1 -1
  32. package/dist/integration-routes.js +72 -33
  33. package/dist/knowledge-refinement-routes.d.ts.map +1 -1
  34. package/dist/knowledge-refinement-routes.js +22 -15
  35. package/dist/knowledge-route-types.d.ts +16 -15
  36. package/dist/knowledge-route-types.d.ts.map +1 -1
  37. package/dist/knowledge-routes.d.ts.map +1 -1
  38. package/dist/knowledge-routes.js +144 -89
  39. package/dist/media-route-types.d.ts +2 -1
  40. package/dist/media-route-types.d.ts.map +1 -1
  41. package/dist/media-routes.d.ts.map +1 -1
  42. package/dist/media-routes.js +119 -55
  43. package/dist/operator.d.ts +16 -0
  44. package/dist/operator.d.ts.map +1 -1
  45. package/dist/operator.js +105 -61
  46. package/dist/otlp-protobuf.d.ts.map +1 -1
  47. package/dist/otlp-protobuf.js +28 -10
  48. package/dist/remote-routes.d.ts +9 -3
  49. package/dist/remote-routes.d.ts.map +1 -1
  50. package/dist/remote-routes.js +259 -163
  51. package/dist/route-helpers.d.ts +13 -2
  52. package/dist/route-helpers.d.ts.map +1 -1
  53. package/dist/route-helpers.js +38 -1
  54. package/dist/runtime-automation-routes.d.ts.map +1 -1
  55. package/dist/runtime-automation-routes.js +88 -54
  56. package/dist/runtime-route-types.d.ts +87 -93
  57. package/dist/runtime-route-types.d.ts.map +1 -1
  58. package/dist/runtime-session-routes.d.ts +6 -4
  59. package/dist/runtime-session-routes.d.ts.map +1 -1
  60. package/dist/runtime-session-routes.js +132 -88
  61. package/dist/sessions.js +3 -3
  62. package/dist/system-route-types.d.ts +25 -24
  63. package/dist/system-route-types.d.ts.map +1 -1
  64. package/dist/system-routes.d.ts +1 -1
  65. package/dist/system-routes.d.ts.map +1 -1
  66. package/dist/system-routes.js +126 -92
  67. package/dist/tasks.d.ts.map +1 -1
  68. package/dist/tasks.js +2 -0
  69. package/dist/telemetry-routes.d.ts +14 -14
  70. package/dist/telemetry-routes.d.ts.map +1 -1
  71. package/dist/telemetry-routes.js +54 -29
  72. package/package.json +5 -4
@@ -1,7 +1,7 @@
1
1
  import { resolvePrivateHostFetchOptions } from './http-policy.js';
2
2
  import { jsonErrorResponse } from './error-response.js';
3
- import { DaemonErrorCategory } from '@pellux/goodvibes-errors';
4
3
  import { createArtifactFromUploadRequest, isArtifactUploadRequest } from './artifact-upload.js';
4
+ import { createRouteBodySchema, createRouteBodySchemaRegistry, readBoundedBodyInteger, readOptionalStringField, readStringArrayField, } from './route-helpers.js';
5
5
  function readErrorMessage(error) {
6
6
  if (typeof error === 'string')
7
7
  return error;
@@ -13,22 +13,16 @@ function readErrorMessage(error) {
13
13
  return 'Unknown error';
14
14
  }
15
15
  function isProviderNotConfiguredError(error) {
16
- const msg = readErrorMessage(error).toLowerCase();
17
- return (msg.includes('not configured')
18
- || msg.includes('no provider')
19
- || msg.includes('api key')
20
- || msg.includes('api_key')
21
- || msg.includes('missing key')
22
- || msg.includes('no api')
23
- || msg.includes('provider not')
24
- || msg.includes('unconfigured'));
16
+ return Boolean(error && typeof error === 'object' && error.code === 'PROVIDER_NOT_CONFIGURED');
25
17
  }
26
18
  export function createDaemonMediaRouteHandlers(context) {
27
19
  return {
28
20
  getVoiceStatus: async () => Response.json(await context.voiceService.getStatus(Boolean(context.configManager.get('ui.voiceEnabled')))),
29
- getVoiceProviders: async () => Response.json({
30
- providers: await context.voiceService.getStatus(Boolean(context.configManager.get('ui.voiceEnabled'))).then((status) => status.providers),
31
- }),
21
+ getVoiceProviders: async () => {
22
+ // reuse the same status snapshot instead of issuing a second provider-status probe.
23
+ const status = await context.voiceService.getStatus(Boolean(context.configManager.get('ui.voiceEnabled')));
24
+ return Response.json({ providers: status.providers });
25
+ },
32
26
  getVoiceVoices: async (url) => Response.json({ voices: await context.voiceService.listVoices(url.searchParams.get('providerId') ?? undefined) }),
33
27
  postVoiceTts: async (request) => handleVoiceTts(context, request),
34
28
  postVoiceTtsStream: async (request) => handleVoiceTtsStream(context, request),
@@ -42,7 +36,7 @@ export function createDaemonMediaRouteHandlers(context) {
42
36
  const artifact = context.artifactStore.get(artifactId);
43
37
  return artifact
44
38
  ? Response.json({ artifact })
45
- : Response.json({ error: 'Unknown artifact' }, { status: 404 });
39
+ : jsonErrorResponse({ error: 'Unknown artifact' }, { status: 404 });
46
40
  },
47
41
  getArtifactContent: async (artifactId, request) => handleArtifactContent(context, artifactId, request),
48
42
  getMediaProviders: async () => Response.json({ providers: await context.mediaProviders.status() }),
@@ -60,6 +54,12 @@ function readOptionalConfigString(context, key) {
60
54
  const value = context.configManager.get(key);
61
55
  return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
62
56
  }
57
+ const MAX_MEDIA_WRITEBACK_TAGS = 64;
58
+ function readOptionalBoundedNumber(value, min, max) {
59
+ if (typeof value !== 'number' || !Number.isFinite(value))
60
+ return undefined;
61
+ return Math.min(max, Math.max(min, value));
62
+ }
63
63
  function readVoiceSynthesisRequest(body, context) {
64
64
  const providerId = typeof body.providerId === 'string'
65
65
  ? body.providerId
@@ -73,7 +73,7 @@ function readVoiceSynthesisRequest(body, context) {
73
73
  : undefined;
74
74
  const modelId = typeof body.modelId === 'string' ? body.modelId : undefined;
75
75
  const format = typeof body.format === 'string' ? body.format : undefined;
76
- const speed = typeof body.speed === 'number' ? body.speed : undefined;
76
+ const speed = readOptionalBoundedNumber(body.speed, 0.25, 4);
77
77
  const input = {
78
78
  text: typeof body.text === 'string' ? body.text : '',
79
79
  metadata: typeof body.metadata === 'object' && body.metadata !== null ? body.metadata : {},
@@ -91,38 +91,83 @@ function readVoiceSynthesisRequest(body, context) {
91
91
  input,
92
92
  };
93
93
  }
94
+ const mediaBodySchemas = createRouteBodySchemaRegistry({
95
+ webSearch: createRouteBodySchema('POST /api/web-search', (body) => {
96
+ const query = readOptionalStringField(body, 'query');
97
+ if (!query)
98
+ return jsonErrorResponse({ error: 'Missing query' }, { status: 400 });
99
+ const providerId = readOptionalStringField(body, 'providerId');
100
+ const verbosity = readOptionalStringField(body, 'verbosity');
101
+ const region = readOptionalStringField(body, 'region');
102
+ const safeSearch = readOptionalStringField(body, 'safeSearch');
103
+ const timeRange = readOptionalStringField(body, 'timeRange');
104
+ const evidenceExtract = readOptionalStringField(body, 'evidenceExtract');
105
+ return {
106
+ query,
107
+ ...(providerId ? { providerId } : {}),
108
+ ...(Object.hasOwn(body, 'maxResults') ? { maxResults: readBoundedBodyInteger(body.maxResults, 5, 50) } : {}),
109
+ ...(verbosity ? { verbosity: verbosity } : {}),
110
+ ...(region ? { region } : {}),
111
+ ...(safeSearch ? { safeSearch: safeSearch } : {}),
112
+ ...(timeRange ? { timeRange: timeRange } : {}),
113
+ ...(typeof body.includeInstantAnswer === 'boolean' ? { includeInstantAnswer: body.includeInstantAnswer } : {}),
114
+ ...(typeof body.includeEvidence === 'boolean' ? { includeEvidence: body.includeEvidence } : {}),
115
+ ...(Object.hasOwn(body, 'evidenceTopN') ? { evidenceTopN: readBoundedBodyInteger(body.evidenceTopN, 3, 20) } : {}),
116
+ ...(evidenceExtract ? { evidenceExtract: evidenceExtract } : {}),
117
+ };
118
+ }),
119
+ multimodalPacket: createRouteBodySchema('POST /api/multimodal/packet', (body) => {
120
+ if (typeof body.analysis !== 'object' || body.analysis === null) {
121
+ return jsonErrorResponse({ error: 'Missing analysis payload' }, { status: 400 });
122
+ }
123
+ const detail = readOptionalStringField(body, 'detail');
124
+ return {
125
+ analysis: body.analysis,
126
+ detail: detail ?? 'standard',
127
+ ...(Object.hasOwn(body, 'budgetLimit') ? { budgetLimit: readBoundedBodyInteger(body.budgetLimit, 8, 100) } : {}),
128
+ };
129
+ }),
130
+ });
94
131
  async function handleVoiceTts(context, req) {
132
+ const admin = context.requireAdmin(req);
133
+ if (admin)
134
+ return admin;
95
135
  const body = await context.parseJsonBody(req);
96
136
  if (body instanceof Response)
97
137
  return body;
98
- const { providerId, input } = readVoiceSynthesisRequest(body);
138
+ // pass context so defaults from tts.provider/tts.voice config are applied
139
+ // consistently with handleVoiceTtsStream.
140
+ const { providerId, input } = readVoiceSynthesisRequest(body, context);
99
141
  if (!input.text.trim())
100
- return Response.json({ error: 'Missing text' }, { status: 400 });
142
+ return jsonErrorResponse({ error: 'Missing text' }, { status: 400 });
101
143
  try {
102
144
  const result = await context.voiceService.synthesize(providerId, input);
103
145
  return Response.json(result);
104
146
  }
105
147
  catch (error) {
106
148
  if (isProviderNotConfiguredError(error)) {
107
- return Response.json({ code: 'PROVIDER_NOT_CONFIGURED', error: readErrorMessage(error), category: DaemonErrorCategory.CONFIG, source: 'provider', recoverable: false, hint: 'Configure the voice provider API key or service credentials.' }, { status: 409 });
149
+ return jsonErrorResponse({ code: 'PROVIDER_NOT_CONFIGURED', error: readErrorMessage(error), source: 'provider', recoverable: false, hint: 'Configure the voice provider API key or service credentials.' }, { status: 409 });
108
150
  }
109
151
  return jsonErrorResponse(error, { status: 400 });
110
152
  }
111
153
  }
112
154
  async function handleVoiceTtsStream(context, req) {
155
+ const admin = context.requireAdmin(req);
156
+ if (admin)
157
+ return admin;
113
158
  const body = await context.parseJsonBody(req);
114
159
  if (body instanceof Response)
115
160
  return body;
116
161
  const { providerId, input } = readVoiceSynthesisRequest(body, context);
117
162
  if (!input.text.trim())
118
- return Response.json({ error: 'Missing text' }, { status: 400 });
163
+ return jsonErrorResponse({ error: 'Missing text' }, { status: 400 });
119
164
  try {
120
165
  const result = await context.voiceService.synthesizeStream(providerId, { ...input, signal: req.signal });
121
166
  return voiceStreamResponse(result);
122
167
  }
123
168
  catch (error) {
124
169
  if (isProviderNotConfiguredError(error)) {
125
- return Response.json({ code: 'PROVIDER_NOT_CONFIGURED', error: readErrorMessage(error), category: DaemonErrorCategory.CONFIG, source: 'provider', recoverable: false, hint: 'Configure the streaming TTS provider API key or service credentials.' }, { status: 409 });
170
+ return jsonErrorResponse({ code: 'PROVIDER_NOT_CONFIGURED', error: readErrorMessage(error), source: 'provider', recoverable: false, hint: 'Configure the streaming TTS provider API key or service credentials.' }, { status: 409 });
126
171
  }
127
172
  return jsonErrorResponse(error, { status: 400 });
128
173
  }
@@ -162,11 +207,14 @@ function voiceStreamResponse(result) {
162
207
  });
163
208
  }
164
209
  async function handleVoiceStt(context, req) {
210
+ const admin = context.requireAdmin(req);
211
+ if (admin)
212
+ return admin;
165
213
  const body = await context.parseJsonBody(req);
166
214
  if (body instanceof Response)
167
215
  return body;
168
216
  if (typeof body.audio !== 'object' || body.audio === null) {
169
- return Response.json({ error: 'Missing audio artifact' }, { status: 400 });
217
+ return jsonErrorResponse({ error: 'Missing audio artifact' }, { status: 400 });
170
218
  }
171
219
  try {
172
220
  const result = await context.voiceService.transcribe(typeof body.providerId === 'string' ? body.providerId : undefined, {
@@ -180,12 +228,15 @@ async function handleVoiceStt(context, req) {
180
228
  }
181
229
  catch (error) {
182
230
  if (isProviderNotConfiguredError(error)) {
183
- return Response.json({ code: 'PROVIDER_NOT_CONFIGURED', error: readErrorMessage(error), category: DaemonErrorCategory.CONFIG, source: 'provider', recoverable: false, hint: 'Configure the voice provider API key or service credentials.' }, { status: 409 });
231
+ return jsonErrorResponse({ code: 'PROVIDER_NOT_CONFIGURED', error: readErrorMessage(error), source: 'provider', recoverable: false, hint: 'Configure the voice provider API key or service credentials.' }, { status: 409 });
184
232
  }
185
233
  return jsonErrorResponse(error, { status: 400 });
186
234
  }
187
235
  }
188
236
  async function handleVoiceRealtimeSession(context, req) {
237
+ const admin = context.requireAdmin(req);
238
+ if (admin)
239
+ return admin;
189
240
  const body = await context.parseJsonBody(req);
190
241
  if (body instanceof Response)
191
242
  return body;
@@ -202,18 +253,21 @@ async function handleVoiceRealtimeSession(context, req) {
202
253
  }
203
254
  catch (error) {
204
255
  if (isProviderNotConfiguredError(error)) {
205
- return Response.json({ code: 'PROVIDER_NOT_CONFIGURED', error: readErrorMessage(error), category: DaemonErrorCategory.CONFIG, source: 'provider', recoverable: false, hint: 'Configure the voice provider API key or service credentials.' }, { status: 409 });
256
+ return jsonErrorResponse({ code: 'PROVIDER_NOT_CONFIGURED', error: readErrorMessage(error), source: 'provider', recoverable: false, hint: 'Configure the voice provider API key or service credentials.' }, { status: 409 });
206
257
  }
207
258
  return jsonErrorResponse(error, { status: 400 });
208
259
  }
209
260
  }
210
261
  async function handleMediaAnalyze(context, req) {
262
+ const admin = context.requireAdmin(req);
263
+ if (admin)
264
+ return admin;
211
265
  const body = await context.parseJsonBody(req);
212
266
  if (body instanceof Response)
213
267
  return body;
214
268
  const provider = context.mediaProviders.findProvider('understand', typeof body.providerId === 'string' ? body.providerId : undefined);
215
269
  if (!provider?.analyze)
216
- return Response.json({ error: 'No media analysis provider is registered' }, { status: 404 });
270
+ return jsonErrorResponse({ error: 'No media analysis provider is registered' }, { status: 404 });
217
271
  const artifact = typeof body.artifact === 'object' && body.artifact !== null
218
272
  ? body.artifact
219
273
  : typeof body.artifactId === 'string' && body.artifactId.trim().length > 0
@@ -224,7 +278,7 @@ async function handleMediaAnalyze(context, req) {
224
278
  }
225
279
  : null;
226
280
  if (!artifact) {
227
- return Response.json({ error: 'Missing media artifact' }, { status: 400 });
281
+ return jsonErrorResponse({ error: 'Missing media artifact' }, { status: 400 });
228
282
  }
229
283
  return Response.json(await provider.analyze({
230
284
  artifact,
@@ -234,6 +288,9 @@ async function handleMediaAnalyze(context, req) {
234
288
  }));
235
289
  }
236
290
  async function handleArtifactCreate(context, req) {
291
+ const admin = context.requireAdmin(req);
292
+ if (admin)
293
+ return admin;
237
294
  if (isArtifactUploadRequest(req)) {
238
295
  const uploaded = await createArtifactFromUploadRequest(context.artifactStore, req);
239
296
  if (uploaded instanceof Response)
@@ -280,7 +337,9 @@ async function handleArtifactContent(context, artifactId, req) {
280
337
  });
281
338
  const download = new URL(req.url).searchParams.get('download');
282
339
  if (record.filename && download !== '0') {
283
- headers.set('Content-Disposition', `attachment; filename="${record.filename.replace(/"/g, '\\"')}"`);
340
+ // Strip non-ASCII bytes so the ASCII filename parameter stays valid.
341
+ const asciiFallback = record.filename.replace(/[^\x20-\x7E]/g, '_').replace(/[\r\n"]/g, '_');
342
+ headers.set('Content-Disposition', `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodeURIComponent(record.filename)}`);
284
343
  }
285
344
  return new Response(bytes, { status: 200, headers });
286
345
  }
@@ -289,44 +348,38 @@ async function handleArtifactContent(context, artifactId, req) {
289
348
  }
290
349
  }
291
350
  async function handleWebSearch(context, req) {
351
+ const admin = context.requireAdmin(req);
352
+ if (admin)
353
+ return admin;
292
354
  const body = await context.parseJsonBody(req);
293
355
  if (body instanceof Response)
294
356
  return body;
295
- const query = typeof body.query === 'string' ? body.query.trim() : '';
296
- if (!query)
297
- return Response.json({ error: 'Missing query' }, { status: 400 });
357
+ const input = mediaBodySchemas.webSearch.parse(body);
358
+ if (input instanceof Response)
359
+ return input;
298
360
  try {
299
- return Response.json(await context.webSearchService.search({
300
- query,
301
- ...(typeof body.providerId === 'string' ? { providerId: body.providerId } : {}),
302
- ...(typeof body.maxResults === 'number' ? { maxResults: body.maxResults } : {}),
303
- ...(typeof body.verbosity === 'string' ? { verbosity: body.verbosity } : {}),
304
- ...(typeof body.region === 'string' ? { region: body.region } : {}),
305
- ...(typeof body.safeSearch === 'string' ? { safeSearch: body.safeSearch } : {}),
306
- ...(typeof body.timeRange === 'string' ? { timeRange: body.timeRange } : {}),
307
- ...(typeof body.includeInstantAnswer === 'boolean' ? { includeInstantAnswer: body.includeInstantAnswer } : {}),
308
- ...(typeof body.includeEvidence === 'boolean' ? { includeEvidence: body.includeEvidence } : {}),
309
- ...(typeof body.evidenceTopN === 'number' ? { evidenceTopN: body.evidenceTopN } : {}),
310
- ...(typeof body.evidenceExtract === 'string' ? { evidenceExtract: body.evidenceExtract } : {}),
311
- }));
361
+ return Response.json(await context.webSearchService.search(input));
312
362
  }
313
363
  catch (error) {
314
364
  return jsonErrorResponse(error, { status: 400 });
315
365
  }
316
366
  }
317
367
  async function handleMediaTransform(context, req) {
368
+ const admin = context.requireAdmin(req);
369
+ if (admin)
370
+ return admin;
318
371
  const body = await context.parseJsonBody(req);
319
372
  if (body instanceof Response)
320
373
  return body;
321
374
  const provider = context.mediaProviders.findProvider('transform', typeof body.providerId === 'string' ? body.providerId : undefined);
322
375
  if (!provider?.transform)
323
- return Response.json({ error: 'No media transform provider is registered' }, { status: 404 });
376
+ return jsonErrorResponse({ error: 'No media transform provider is registered' }, { status: 404 });
324
377
  if (typeof body.artifact !== 'object' || body.artifact === null) {
325
- return Response.json({ error: 'Missing media artifact' }, { status: 400 });
378
+ return jsonErrorResponse({ error: 'Missing media artifact' }, { status: 400 });
326
379
  }
327
380
  const operation = typeof body.operation === 'string' ? body.operation : '';
328
381
  if (!operation)
329
- return Response.json({ error: 'Missing media transform operation' }, { status: 400 });
382
+ return jsonErrorResponse({ error: 'Missing media transform operation' }, { status: 400 });
330
383
  return Response.json(await provider.transform({
331
384
  artifact: body.artifact,
332
385
  operation,
@@ -336,15 +389,18 @@ async function handleMediaTransform(context, req) {
336
389
  }));
337
390
  }
338
391
  async function handleMediaGenerate(context, req) {
392
+ const admin = context.requireAdmin(req);
393
+ if (admin)
394
+ return admin;
339
395
  const body = await context.parseJsonBody(req);
340
396
  if (body instanceof Response)
341
397
  return body;
342
398
  const provider = context.mediaProviders.findProvider('generate', typeof body.providerId === 'string' ? body.providerId : undefined);
343
399
  if (!provider?.generate)
344
- return Response.json({ error: 'No media generation provider is registered' }, { status: 404 });
400
+ return jsonErrorResponse({ error: 'No media generation provider is registered' }, { status: 404 });
345
401
  const prompt = typeof body.prompt === 'string' ? body.prompt : '';
346
402
  if (!prompt.trim())
347
- return Response.json({ error: 'Missing media generation prompt' }, { status: 400 });
403
+ return jsonErrorResponse({ error: 'Missing media generation prompt' }, { status: 400 });
348
404
  return Response.json(await provider.generate({
349
405
  prompt,
350
406
  outputMimeType: typeof body.outputMimeType === 'string' ? body.outputMimeType : undefined,
@@ -354,6 +410,9 @@ async function handleMediaGenerate(context, req) {
354
410
  }));
355
411
  }
356
412
  async function handleMultimodalAnalyze(context, req) {
413
+ const admin = context.requireAdmin(req);
414
+ if (admin)
415
+ return admin;
357
416
  const body = await context.parseJsonBody(req);
358
417
  if (body instanceof Response)
359
418
  return body;
@@ -412,30 +471,35 @@ async function handleMultimodalAnalyze(context, req) {
412
471
  }
413
472
  }
414
473
  async function handleMultimodalPacket(context, req) {
474
+ const admin = context.requireAdmin(req);
475
+ if (admin)
476
+ return admin;
415
477
  const body = await context.parseJsonBody(req);
416
478
  if (body instanceof Response)
417
479
  return body;
418
- if (typeof body.analysis !== 'object' || body.analysis === null) {
419
- return Response.json({ error: 'Missing analysis payload' }, { status: 400 });
420
- }
421
- const detail = typeof body.detail === 'string' ? body.detail : 'standard';
422
- const budgetLimit = typeof body.budgetLimit === 'number' ? body.budgetLimit : undefined;
480
+ const input = mediaBodySchemas.multimodalPacket.parse(body);
481
+ if (input instanceof Response)
482
+ return input;
423
483
  return Response.json({
424
- packet: context.multimodalService.buildPacket(body.analysis, detail, budgetLimit),
484
+ packet: context.multimodalService.buildPacket(input.analysis, input.detail, input.budgetLimit),
425
485
  });
426
486
  }
427
487
  async function handleMultimodalWriteback(context, req) {
488
+ const admin = context.requireAdmin(req);
489
+ if (admin)
490
+ return admin;
428
491
  const body = await context.parseJsonBody(req);
429
492
  if (body instanceof Response)
430
493
  return body;
431
494
  if (typeof body.analysis !== 'object' || body.analysis === null) {
432
- return Response.json({ error: 'Missing analysis payload' }, { status: 400 });
495
+ return jsonErrorResponse({ error: 'Missing analysis payload' }, { status: 400 });
433
496
  }
497
+ const tags = readStringArrayField(body, 'tags', MAX_MEDIA_WRITEBACK_TAGS);
434
498
  try {
435
499
  const writeback = await context.multimodalService.writeBackAnalysis(body.analysis, {
436
500
  ...(typeof body.sessionId === 'string' ? { sessionId: body.sessionId } : {}),
437
501
  ...(typeof body.title === 'string' ? { title: body.title } : {}),
438
- ...(Array.isArray(body.tags) ? { tags: body.tags.filter((entry) => typeof entry === 'string') } : {}),
502
+ ...(tags ? { tags } : {}),
439
503
  ...(typeof body.folderPath === 'string' ? { folderPath: body.folderPath } : {}),
440
504
  ...(typeof body.metadata === 'object' && body.metadata !== null ? { metadata: body.metadata } : {}),
441
505
  });
@@ -1,3 +1,19 @@
1
1
  import type { DaemonOperatorRouteHandlers } from './context.js';
2
+ /**
3
+ * Route-level authentication is enforced inside each handler, not at the
4
+ * dispatcher level. Handler auth requirements by category:
5
+ *
6
+ * - READ-ONLY routes (status, providers, settings, continuity, intelligence,
7
+ * worktrees, watchers, approvals, telemetry snapshot) — require admin or
8
+ * authenticated session per handler implementation.
9
+ * - STATE-CHANGING routes (service install/start/stop, route bindings,
10
+ * automation jobs, knowledge ingest) — always `withAdmin(context, req, ...)`.
11
+ * - SCHEDULER/RUNTIME routes (getSchedulerCapacity, getRuntimeMetrics) —
12
+ * require admin per handler implementation.
13
+ *
14
+ * Dispatcher does not short-circuit unauthenticated requests; all auth
15
+ * enforcement lives in the handler factories (system-routes.ts,
16
+ * integration-routes.ts, runtime-automation-routes.ts, etc.).
17
+ */
2
18
  export declare function dispatchOperatorRoutes(req: Request, handlers: DaemonOperatorRouteHandlers): Promise<Response | null>;
3
19
  //# sourceMappingURL=operator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../src/operator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAGhE,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,2BAA2B,GACpC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA8S1B"}
1
+ {"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../src/operator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AA4BhE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,2BAA2B,GACpC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAO1B"}