@pellux/goodvibes-daemon-sdk 0.30.3 → 0.33.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 (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,5 +1,40 @@
1
+ import { createRouteBodySchema, createRouteBodySchemaRegistry, readBoundedBodyInteger, readOptionalStringField, } from './route-helpers.js';
1
2
  import { jsonErrorResponse } from './error-response.js';
2
- export function createDaemonSystemRouteHandlers(context, request) {
3
+ import { withAdmin } from './auth-helpers.js';
4
+ const WATCHER_KIND_VALUES = ['filesystem', 'webhook', 'socket', 'integration', 'polling'];
5
+ const WATCHER_SOURCE_KIND_MAP = {
6
+ webhook: 'webhook',
7
+ file: 'filesystem',
8
+ stream: 'socket',
9
+ api: 'integration',
10
+ };
11
+ const WATCHER_SOURCE_RECORD_KIND_MAP = {
12
+ webhook: 'webhook',
13
+ file: 'hook',
14
+ stream: 'hook',
15
+ api: 'hook',
16
+ };
17
+ const DEFAULT_WATCHER_INTERVAL_MS = 60_000;
18
+ const MIN_WATCHER_INTERVAL_MS = 1_000;
19
+ const MAX_WATCHER_INTERVAL_MS = 86_400_000;
20
+ const systemBodySchemas = createRouteBodySchemaRegistry({
21
+ routeBindingCreate: createRouteBodySchema('POST /api/system/route-bindings', (body) => {
22
+ const surfaceKind = readOptionalStringField(body, 'surfaceKind');
23
+ const kind = readOptionalStringField(body, 'kind');
24
+ const surfaceId = readOptionalStringField(body, 'surfaceId');
25
+ const externalId = readOptionalStringField(body, 'externalId');
26
+ if (!surfaceKind || !kind || !surfaceId || !externalId) {
27
+ return jsonErrorResponse({ error: 'Missing required route binding fields' }, { status: 400 });
28
+ }
29
+ return {
30
+ surfaceKind: surfaceKind,
31
+ kind: kind,
32
+ surfaceId,
33
+ externalId,
34
+ };
35
+ }),
36
+ });
37
+ export function createDaemonSystemRouteHandlers(context) {
3
38
  return {
4
39
  getWatchers: () => Response.json({ watchers: context.watcherRegistry.list() }),
5
40
  postWatcher: (req) => {
@@ -14,20 +49,20 @@ export function createDaemonSystemRouteHandlers(context, request) {
14
49
  return admin;
15
50
  return handleUpdateWatcher(context, watcherId, req);
16
51
  },
17
- watcherAction: (watcherId, action) => {
18
- const admin = context.requireAdmin(request);
52
+ watcherAction: (watcherId, action, req) => {
53
+ const admin = context.requireAdmin(req);
19
54
  if (admin)
20
55
  return admin;
21
- return handleWatcherAction(context, watcherId, action);
56
+ return handleWatcherAction(context, watcherId, action, req);
22
57
  },
23
- deleteWatcher: (watcherId) => {
24
- const admin = context.requireAdmin(request);
58
+ deleteWatcher: (watcherId, req) => {
59
+ const admin = context.requireAdmin(req);
25
60
  if (admin)
26
61
  return admin;
27
62
  const removed = context.watcherRegistry.removeWatcher(watcherId);
28
63
  return removed
29
64
  ? Response.json({ removed: true, id: watcherId })
30
- : Response.json({ error: 'Unknown watcher' }, { status: 404 });
65
+ : jsonErrorResponse({ error: 'Unknown watcher' }, { status: 404 });
31
66
  },
32
67
  getServiceStatus: () => Response.json({
33
68
  ...context.platformServiceManager.status(),
@@ -37,37 +72,42 @@ export function createDaemonSystemRouteHandlers(context, request) {
37
72
  outbound: context.inspectOutboundTls(),
38
73
  },
39
74
  }),
40
- installService: () => {
41
- const admin = context.requireAdmin(request);
75
+ installService: (req) => {
76
+ const admin = context.requireAdmin(req);
42
77
  if (admin)
43
78
  return admin;
44
79
  return Response.json(context.platformServiceManager.install());
45
80
  },
46
- startService: () => {
47
- const admin = context.requireAdmin(request);
81
+ startService: (req) => {
82
+ const admin = context.requireAdmin(req);
48
83
  if (admin)
49
84
  return admin;
50
85
  return Response.json(context.platformServiceManager.start());
51
86
  },
52
- stopService: () => {
53
- const admin = context.requireAdmin(request);
87
+ stopService: (req) => {
88
+ const admin = context.requireAdmin(req);
54
89
  if (admin)
55
90
  return admin;
56
91
  return Response.json(context.platformServiceManager.stop());
57
92
  },
58
- restartService: () => {
59
- const admin = context.requireAdmin(request);
93
+ restartService: (req) => {
94
+ const admin = context.requireAdmin(req);
60
95
  if (admin)
61
96
  return admin;
62
97
  return Response.json(context.platformServiceManager.restart());
63
98
  },
64
- uninstallService: () => {
65
- const admin = context.requireAdmin(request);
99
+ uninstallService: (req) => {
100
+ const admin = context.requireAdmin(req);
66
101
  if (admin)
67
102
  return admin;
68
103
  return Response.json(context.platformServiceManager.uninstall());
69
104
  },
70
- getRouteBindings: () => Response.json({ bindings: context.routeBindings.listBindings() }),
105
+ getRouteBindings: (req) => {
106
+ const admin = context.requireAdmin(req);
107
+ if (admin)
108
+ return admin;
109
+ return Response.json({ bindings: context.routeBindings.listBindings() });
110
+ },
71
111
  postRouteBinding: async (req) => {
72
112
  const admin = context.requireAdmin(req);
73
113
  if (admin)
@@ -75,19 +115,15 @@ export function createDaemonSystemRouteHandlers(context, request) {
75
115
  const body = await context.parseJsonBody(req);
76
116
  if (body instanceof Response)
77
117
  return body;
78
- const surfaceKind = typeof body.surfaceKind === 'string' ? body.surfaceKind : '';
79
- const kind = typeof body.kind === 'string' ? body.kind : '';
80
- const surfaceId = typeof body.surfaceId === 'string' ? body.surfaceId : '';
81
- const externalId = typeof body.externalId === 'string' ? body.externalId : '';
82
- if (!surfaceKind || !kind || !surfaceId || !externalId) {
83
- return Response.json({ error: 'Missing required route binding fields' }, { status: 400 });
84
- }
118
+ const input = systemBodySchemas.routeBindingCreate.parse(body);
119
+ if (input instanceof Response)
120
+ return input;
85
121
  const binding = await context.routeBindings.upsertBinding({
86
122
  id: typeof body.id === 'string' ? body.id : undefined,
87
- kind: kind,
88
- surfaceKind: surfaceKind,
89
- surfaceId,
90
- externalId,
123
+ kind: input.kind,
124
+ surfaceKind: input.surfaceKind,
125
+ surfaceId: input.surfaceId,
126
+ externalId: input.externalId,
91
127
  sessionPolicy: typeof body.sessionPolicy === 'string' ? body.sessionPolicy : undefined,
92
128
  threadPolicy: typeof body.threadPolicy === 'string' ? body.threadPolicy : undefined,
93
129
  deliveryGuarantee: typeof body.deliveryGuarantee === 'string' ? body.deliveryGuarantee : undefined,
@@ -122,26 +158,26 @@ export function createDaemonSystemRouteHandlers(context, request) {
122
158
  });
123
159
  return updated
124
160
  ? Response.json(updated)
125
- : Response.json({ error: 'Unknown route binding' }, { status: 404 });
161
+ : jsonErrorResponse({ error: 'Unknown route binding' }, { status: 404 });
126
162
  },
127
- deleteRouteBinding: async (bindingId) => {
128
- const admin = context.requireAdmin(request);
163
+ deleteRouteBinding: async (bindingId, req) => {
164
+ const admin = context.requireAdmin(req);
129
165
  if (admin)
130
166
  return admin;
131
167
  const removed = await context.routeBindings.removeBinding(bindingId);
132
168
  return removed
133
169
  ? Response.json({ removed: true, id: bindingId })
134
- : Response.json({ error: 'Unknown route binding' }, { status: 404 });
170
+ : jsonErrorResponse({ error: 'Unknown route binding' }, { status: 404 });
135
171
  },
136
172
  getApprovals: () => {
137
173
  if (!context.integrationHelpers) {
138
- return Response.json({ error: 'Integration helper service unavailable' }, { status: 503 });
174
+ return jsonErrorResponse({ error: 'Integration helper service unavailable' }, { status: 503 });
139
175
  }
140
176
  return Response.json(context.integrationHelpers.getApprovalSnapshot());
141
177
  },
142
178
  approvalAction: (approvalId, action, req) => handleApprovalAction(context, approvalId, action, req),
143
- getConfig: () => {
144
- const admin = context.requireAdmin(request);
179
+ getConfig: (req) => {
180
+ const admin = context.requireAdmin(req);
145
181
  if (admin)
146
182
  return admin;
147
183
  return Response.json(context.configManager.getAll());
@@ -155,28 +191,13 @@ export function createDaemonSystemRouteHandlers(context, request) {
155
191
  return payload;
156
192
  const { key, value } = payload;
157
193
  if (!key || typeof key !== 'string') {
158
- return Response.json({ error: 'Missing or invalid key' }, { status: 400 });
194
+ return jsonErrorResponse({ error: 'Missing or invalid key' }, { status: 400 });
159
195
  }
160
- // Special-case: runtime.workingDir is handled by the workspace swap manager.
161
196
  if (key === 'runtime.workingDir') {
162
- if (!context.swapManager) {
163
- return Response.json({ error: 'Workspace swapping is not available in this daemon configuration.' }, { status: 400 });
164
- }
165
- if (typeof value !== 'string' || !value.trim()) {
166
- return Response.json({ error: 'runtime.workingDir value must be a non-empty string path.', code: 'INVALID_PATH' }, { status: 400 });
167
- }
168
- const result = await context.swapManager.requestSwap(value);
169
- if (result.ok) {
170
- return Response.json({ success: true, key, value: result.current, previous: result.previous });
171
- }
172
- if (result.code === 'WORKSPACE_BUSY') {
173
- return Response.json({ error: result.reason, code: 'WORKSPACE_BUSY', retryAfter: result.retryAfter }, { status: 409 });
174
- }
175
- // INVALID_PATH
176
- return Response.json({ error: result.reason, code: 'INVALID_PATH' }, { status: 400 });
197
+ return handleWorkingDirectoryConfig(context, key, value);
177
198
  }
178
199
  if (!context.isValidConfigKey(key)) {
179
- return Response.json({ error: 'Invalid config key' }, { status: 400 });
200
+ return jsonErrorResponse({ error: 'Invalid config key' }, { status: 400 });
180
201
  }
181
202
  try {
182
203
  context.configManager.setDynamic(key, value);
@@ -196,24 +217,16 @@ async function handleRegisterWatcher(context, req) {
196
217
  const id = typeof body.id === 'string' && body.id.trim().length > 0
197
218
  ? body.id.trim()
198
219
  : label
199
- ? `watcher-${label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')}`
220
+ ? (() => {
221
+ const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
222
+ // If the label consists entirely of special chars the slug will be empty.
223
+ return slug ? `watcher-${slug}` : `watcher-${Date.now()}`;
224
+ })()
200
225
  : `watcher-${Date.now()}`;
201
- const kind = typeof body.kind === 'string'
202
- ? body.kind
203
- : typeof body.sourceKind === 'string'
204
- ? body.sourceKind === 'webhook'
205
- ? 'webhook'
206
- : body.sourceKind === 'file'
207
- ? 'filesystem'
208
- : body.sourceKind === 'stream'
209
- ? 'socket'
210
- : body.sourceKind === 'api'
211
- ? 'integration'
212
- : 'polling'
213
- : 'polling';
214
- const intervalMs = Number(body.intervalMs ?? context.configManager.get('watchers.pollIntervalMs') ?? 60_000);
226
+ const kind = readWatcherKind(body.kind, body.sourceKind, 'polling');
227
+ const intervalMs = readWatcherIntervalMs(body.intervalMs, context.configManager.get('watchers.pollIntervalMs'));
215
228
  if (!label) {
216
- return Response.json({ error: 'Missing watcher label' }, { status: 400 });
229
+ return jsonErrorResponse({ error: 'Missing watcher label' }, { status: 400 });
217
230
  }
218
231
  const metadata = typeof body.metadata === 'object' && body.metadata !== null
219
232
  ? body.metadata
@@ -234,11 +247,7 @@ async function handleRegisterWatcher(context, req) {
234
247
  source: {
235
248
  id: typeof body.sourceId === 'string' && body.sourceId.trim() ? body.sourceId.trim() : `source:${id}`,
236
249
  kind: typeof body.sourceKind === 'string'
237
- ? body.sourceKind === 'webhook'
238
- ? 'webhook'
239
- : body.sourceKind === 'file' || body.sourceKind === 'stream' || body.sourceKind === 'api'
240
- ? 'hook'
241
- : 'watcher'
250
+ ? WATCHER_SOURCE_RECORD_KIND_MAP[body.sourceKind] ?? 'watcher'
242
251
  : 'watcher',
243
252
  label,
244
253
  enabled: true,
@@ -252,27 +261,52 @@ async function handleRegisterWatcher(context, req) {
252
261
  });
253
262
  return Response.json(record, { status: 201 });
254
263
  }
264
+ async function handleWorkingDirectoryConfig(context, key, value) {
265
+ if (!context.swapManager) {
266
+ return jsonErrorResponse({ error: 'Workspace swapping is not available in this daemon configuration.' }, { status: 400 });
267
+ }
268
+ if (typeof value !== 'string' || !value.trim()) {
269
+ return jsonErrorResponse({ error: 'runtime.workingDir value must be a non-empty string path.', code: 'INVALID_PATH' }, { status: 400 });
270
+ }
271
+ const result = await context.swapManager.requestSwap(value);
272
+ if (result.ok) {
273
+ return Response.json({ success: true, key, value: result.current, previous: result.previous });
274
+ }
275
+ return jsonErrorResponse({
276
+ error: result.reason,
277
+ code: result.code,
278
+ ...(result.code === 'WORKSPACE_BUSY' ? { retryAfter: result.retryAfter } : {}),
279
+ }, { status: result.code === 'WORKSPACE_BUSY' ? 409 : 400 });
280
+ }
281
+ function readWatcherKind(kind, sourceKind, fallback) {
282
+ if (typeof kind === 'string' && WATCHER_KIND_VALUES.includes(kind)) {
283
+ return kind;
284
+ }
285
+ return typeof sourceKind === 'string'
286
+ ? WATCHER_SOURCE_KIND_MAP[sourceKind] ?? fallback
287
+ : fallback;
288
+ }
289
+ function readWatcherIntervalMs(value, fallbackValue) {
290
+ const fallback = typeof fallbackValue === 'number' && Number.isFinite(fallbackValue)
291
+ ? fallbackValue
292
+ : DEFAULT_WATCHER_INTERVAL_MS;
293
+ return readBoundedBodyInteger(value, fallback, MAX_WATCHER_INTERVAL_MS, MIN_WATCHER_INTERVAL_MS);
294
+ }
255
295
  async function handleUpdateWatcher(context, watcherId, req) {
256
296
  const body = await context.parseJsonBody(req);
257
297
  if (body instanceof Response)
258
298
  return body;
259
299
  const current = context.watcherRegistry.getWatcher(watcherId);
260
300
  if (!current) {
261
- return Response.json({ error: 'Unknown watcher' }, { status: 404 });
301
+ return jsonErrorResponse({ error: 'Unknown watcher' }, { status: 404 });
262
302
  }
263
303
  const nextSourceKind = typeof body.sourceKind === 'string'
264
- ? body.sourceKind === 'webhook'
265
- ? 'webhook'
266
- : body.sourceKind === 'file' || body.sourceKind === 'stream' || body.sourceKind === 'api'
267
- ? 'hook'
268
- : 'watcher'
304
+ ? WATCHER_SOURCE_RECORD_KIND_MAP[body.sourceKind] ?? 'watcher'
269
305
  : current.source.kind;
270
306
  const updated = context.watcherRegistry.registerWatcher({
271
307
  id: watcherId,
272
308
  label: typeof body.label === 'string' ? body.label : current.label,
273
- kind: typeof body.kind === 'string'
274
- ? body.kind
275
- : current.kind,
309
+ kind: readWatcherKind(body.kind, undefined, current.kind),
276
310
  source: {
277
311
  ...current.source,
278
312
  ...(typeof body.source === 'object' && body.source !== null ? body.source : {}),
@@ -292,28 +326,28 @@ async function handleUpdateWatcher(context, watcherId, req) {
292
326
  },
293
327
  updatedAt: Date.now(),
294
328
  },
295
- intervalMs: typeof body.intervalMs === 'number' ? body.intervalMs : (current.intervalMs ?? 60_000),
329
+ intervalMs: body.intervalMs !== undefined ? readWatcherIntervalMs(body.intervalMs, current.intervalMs) : (current.intervalMs ?? DEFAULT_WATCHER_INTERVAL_MS),
296
330
  metadata: typeof body.metadata === 'object' && body.metadata !== null ? body.metadata : current.metadata,
297
331
  });
298
332
  return Response.json(updated);
299
333
  }
300
- async function handleWatcherAction(context, watcherId, action) {
334
+ async function handleWatcherAction(context, watcherId, action, _req) {
301
335
  if (action === 'start') {
302
336
  const watcher = context.watcherRegistry.startWatcher(watcherId);
303
337
  return watcher
304
338
  ? Response.json(watcher)
305
- : Response.json({ error: 'Unknown watcher' }, { status: 404 });
339
+ : jsonErrorResponse({ error: 'Unknown watcher' }, { status: 404 });
306
340
  }
307
341
  if (action === 'stop') {
308
342
  const watcher = context.watcherRegistry.stopWatcher(watcherId, 'operator-stop');
309
343
  return watcher
310
344
  ? Response.json(watcher)
311
- : Response.json({ error: 'Unknown watcher' }, { status: 404 });
345
+ : jsonErrorResponse({ error: 'Unknown watcher' }, { status: 404 });
312
346
  }
313
347
  const watcher = await context.watcherRegistry.runWatcherNow(watcherId);
314
348
  return watcher
315
349
  ? Response.json(watcher)
316
- : Response.json({ error: 'Unknown watcher' }, { status: 404 });
350
+ : jsonErrorResponse({ error: 'Unknown watcher' }, { status: 404 });
317
351
  }
318
352
  async function handleApprovalAction(context, approvalId, action, req) {
319
353
  const body = await context.parseOptionalJsonBody(req);
@@ -324,13 +358,13 @@ async function handleApprovalAction(context, approvalId, action, req) {
324
358
  const approval = await context.approvalBroker.claimApproval(approvalId, actor, 'web', note);
325
359
  return approval
326
360
  ? context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, Response.json({ approval }))
327
- : context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, Response.json({ error: 'Unknown approval' }, { status: 404 }));
361
+ : context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, jsonErrorResponse({ error: 'Unknown approval' }, { status: 404 }));
328
362
  }
329
363
  if (action === 'cancel') {
330
364
  const approval = await context.approvalBroker.cancelApproval(approvalId, actor, 'web', note);
331
365
  return approval
332
366
  ? context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, Response.json({ approval }))
333
- : context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, Response.json({ error: 'Unknown approval' }, { status: 404 }));
367
+ : context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, jsonErrorResponse({ error: 'Unknown approval' }, { status: 404 }));
334
368
  }
335
369
  const approval = await context.approvalBroker.resolveApproval(approvalId, {
336
370
  approved: action === 'approve',
@@ -341,5 +375,5 @@ async function handleApprovalAction(context, approvalId, action, req) {
341
375
  });
342
376
  return approval
343
377
  ? context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, Response.json({ approval }))
344
- : context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, Response.json({ error: 'Unknown approval' }, { status: 404 }));
378
+ : context.recordApiResponse(req, `/api/approvals/${approvalId}/${action}`, jsonErrorResponse({ error: 'Unknown approval' }, { status: 404 }));
345
379
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAE5D,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAqB1B"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAE5D,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAuB1B"}
package/dist/tasks.js CHANGED
@@ -4,6 +4,8 @@ export async function dispatchTaskRoutes(req, handlers) {
4
4
  const method = req.method;
5
5
  if (pathname === '/task' && method === 'POST')
6
6
  return handlers.postTask(req);
7
+ // GET /task is intentionally left to extension routers that expose
8
+ // surface-specific task status pages.
7
9
  if (pathname === '/task' && method === 'GET')
8
10
  return null;
9
11
  if (pathname === '/api/tasks' && method === 'GET')
@@ -4,19 +4,19 @@ import type { RuntimeEventDomain } from '@pellux/goodvibes-contracts';
4
4
  type TelemetrySeverity = 'debug' | 'info' | 'warn' | 'error';
5
5
  type TelemetryViewMode = 'safe' | 'raw';
6
6
  interface TelemetryFilter {
7
- readonly limit?: number;
8
- readonly since?: number;
9
- readonly until?: number;
10
- readonly domains?: readonly RuntimeEventDomain[];
11
- readonly eventTypes?: readonly string[];
12
- readonly severity?: TelemetrySeverity;
13
- readonly traceId?: string;
14
- readonly sessionId?: string;
15
- readonly turnId?: string;
16
- readonly agentId?: string;
17
- readonly taskId?: string;
18
- readonly cursor?: string;
19
- readonly view?: TelemetryViewMode;
7
+ readonly limit?: number | undefined;
8
+ readonly since?: number | undefined;
9
+ readonly until?: number | undefined;
10
+ readonly domains?: readonly RuntimeEventDomain[] | undefined;
11
+ readonly eventTypes?: readonly string[] | undefined;
12
+ readonly severity?: TelemetrySeverity | undefined;
13
+ readonly traceId?: string | undefined;
14
+ readonly sessionId?: string | undefined;
15
+ readonly turnId?: string | undefined;
16
+ readonly agentId?: string | undefined;
17
+ readonly taskId?: string | undefined;
18
+ readonly cursor?: string | undefined;
19
+ readonly view?: TelemetryViewMode | undefined;
20
20
  }
21
21
  interface TelemetryApiLike {
22
22
  getSnapshot(filter: TelemetryFilter, view: TelemetryViewMode, rawAccessible: boolean): {
@@ -56,7 +56,7 @@ interface TelemetryRouteContext {
56
56
  * In production `DaemonHttpRouter` this is the `TelemetryApiService` instance
57
57
  * which stores ingested records in its bounded event buffer (default 500
58
58
  * records) and makes them observable via GET /api/v1/telemetry/events.
59
- * Pass `null` only in test/stub contexts where ingestion is intentionally
59
+ * Pass `null` only in test fixtures where ingestion is intentionally
60
60
  * a no-op — the receivers still return 200 to keep OTLP exporters happy but
61
61
  * discard the payload.
62
62
  */
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry-routes.d.ts","sourceRoot":"","sources":["../src/telemetry-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"telemetry-routes.d.ts","sourceRoot":"","sources":["../src/telemetry-routes.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
+ import { jsonErrorResponse } from './error-response.js';
1
2
  import { buildMissingScopeBody } from './http-policy.js';
2
3
  import { decodeOtlpProtobuf } from './otlp-protobuf.js';
3
4
  import { DaemonErrorCategory } from '@pellux/goodvibes-errors';
4
- import { readBoundedPositiveInteger, readOptionalBoundedInteger } from './route-helpers.js';
5
+ import { hasAnyScope, readBoundedPositiveInteger, readOptionalBoundedInteger } from './route-helpers.js';
5
6
  const MAX_TELEMETRY_TIMESTAMP_MS = Date.UTC(2100, 0, 1);
6
7
  function parseLimit(value) {
7
8
  if (value === null || value.trim().length === 0)
@@ -65,6 +66,7 @@ const OTLP_PARTIAL_SUCCESS_KEYS = {
65
66
  traces: 'partialSuccess',
66
67
  metrics: 'partialSuccess',
67
68
  };
69
+ const OTLP_INGEST_SCOPES = ['ingest:telemetry', 'write:telemetry'];
68
70
  /**
69
71
  * Validate and parse an OTLP HTTP ingest request body.
70
72
  * Returns a parsed JSON Record on success, or a Response (error) on failure.
@@ -73,20 +75,29 @@ const OTLP_PARTIAL_SUCCESS_KEYS = {
73
75
  * requests for logs, traces, and metrics.
74
76
  */
75
77
  async function parseOtlpBody(req, kind) {
76
- const contentType = (req.headers.get('content-type') ?? '').toLowerCase().split(';')[0].trim();
78
+ const contentType = ((req.headers.get('content-type') ?? '').toLowerCase().split(';')[0] ?? '').trim();
77
79
  const acceptsJson = contentType === OTLP_JSON_CONTENT_TYPE;
78
80
  const acceptsProtobuf = OTLP_PROTOBUF_CONTENT_TYPES.has(contentType);
79
81
  if (!acceptsJson && !acceptsProtobuf) {
80
- return Response.json({
82
+ return jsonErrorResponse({
81
83
  error: `Unsupported Content-Type '${contentType}' for OTLP ingest`,
82
84
  code: 'UNSUPPORTED_MEDIA_TYPE',
83
85
  category: DaemonErrorCategory.BAD_REQUEST,
84
86
  hint: `Use '${OTLP_JSON_CONTENT_TYPE}' or 'application/x-protobuf'.`,
85
87
  }, { status: 415 });
86
88
  }
89
+ // reject oversized requests before buffering the body into memory.
90
+ const contentLength = Number(req.headers.get('content-length'));
91
+ if (!Number.isNaN(contentLength) && contentLength > OTLP_INGEST_MAX_BODY_BYTES) {
92
+ return jsonErrorResponse({
93
+ error: `OTLP ingest payload too large (Content-Length: ${contentLength} > ${OTLP_INGEST_MAX_BODY_BYTES} bytes)`,
94
+ code: 'PAYLOAD_TOO_LARGE',
95
+ category: DaemonErrorCategory.BAD_REQUEST,
96
+ }, { status: 413 });
97
+ }
87
98
  const raw = await req.arrayBuffer();
88
99
  if (raw.byteLength > OTLP_INGEST_MAX_BODY_BYTES) {
89
- return Response.json({
100
+ return jsonErrorResponse({
90
101
  error: `OTLP ingest payload too large (${raw.byteLength} > ${OTLP_INGEST_MAX_BODY_BYTES} bytes)`,
91
102
  code: 'PAYLOAD_TOO_LARGE',
92
103
  category: DaemonErrorCategory.BAD_REQUEST,
@@ -96,22 +107,20 @@ async function parseOtlpBody(req, kind) {
96
107
  try {
97
108
  return decodeOtlpProtobuf(kind, new Uint8Array(raw));
98
109
  }
99
- catch (error) {
100
- void error;
101
- return Response.json({ error: 'OTLP ingest body is not valid protobuf', code: 'INVALID_PAYLOAD', category: DaemonErrorCategory.BAD_REQUEST }, { status: 400 });
110
+ catch {
111
+ return jsonErrorResponse({ error: 'OTLP ingest body is not valid protobuf', code: 'INVALID_PAYLOAD', category: DaemonErrorCategory.BAD_REQUEST }, { status: 400 });
102
112
  }
103
113
  }
104
114
  try {
105
115
  const text = new TextDecoder().decode(raw);
106
116
  const parsed = JSON.parse(text);
107
117
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
108
- return Response.json({ error: 'OTLP ingest body must be a JSON object', code: 'INVALID_PAYLOAD', category: DaemonErrorCategory.BAD_REQUEST }, { status: 400 });
118
+ return jsonErrorResponse({ error: 'OTLP ingest body must be a JSON object', code: 'INVALID_PAYLOAD', category: DaemonErrorCategory.BAD_REQUEST }, { status: 400 });
109
119
  }
110
120
  return parsed;
111
121
  }
112
- catch (error) {
113
- void error;
114
- return Response.json({ error: 'OTLP ingest body is not valid JSON', code: 'INVALID_PAYLOAD', category: DaemonErrorCategory.BAD_REQUEST }, { status: 400 });
122
+ catch {
123
+ return jsonErrorResponse({ error: 'OTLP ingest body is not valid JSON', code: 'INVALID_PAYLOAD', category: DaemonErrorCategory.BAD_REQUEST }, { status: 400 });
115
124
  }
116
125
  }
117
126
  function otlpIngestSuccess(kind) {
@@ -119,8 +128,27 @@ function otlpIngestSuccess(kind) {
119
128
  // partialSuccess omitted when empty (all records accepted).
120
129
  return Response.json({ [OTLP_PARTIAL_SUCCESS_KEYS[kind]]: {} });
121
130
  }
131
+ function authenticateTelemetryIngest(context, req) {
132
+ const principal = context.resolveAuthenticatedPrincipal(req);
133
+ if (!principal) {
134
+ return jsonErrorResponse({ error: 'Authentication required for OTLP ingest', code: 'AUTH_REQUIRED', category: DaemonErrorCategory.AUTHENTICATION, status: 401 }, { status: 401 });
135
+ }
136
+ if (!principal.admin && !hasAnyScope(principal.scopes, OTLP_INGEST_SCOPES)) {
137
+ return jsonErrorResponse({
138
+ error: `Missing required telemetry ingest scope: ${OTLP_INGEST_SCOPES.join(' or ')}`,
139
+ code: 'MISSING_SCOPE',
140
+ category: DaemonErrorCategory.AUTHORIZATION,
141
+ source: 'permission',
142
+ recoverable: false,
143
+ requiredScopes: [...OTLP_INGEST_SCOPES],
144
+ grantedScopes: [...principal.scopes],
145
+ status: 403,
146
+ }, { status: 403 });
147
+ }
148
+ return principal;
149
+ }
122
150
  function unavailable() {
123
- return Response.json({
151
+ return jsonErrorResponse({
124
152
  error: 'Telemetry API unavailable',
125
153
  code: 'TELEMETRY_UNAVAILABLE',
126
154
  category: DaemonErrorCategory.SERVICE,
@@ -131,7 +159,7 @@ function unavailable() {
131
159
  }, { status: 503 });
132
160
  }
133
161
  function invalidCursor(error) {
134
- return Response.json({
162
+ return jsonErrorResponse({
135
163
  error: error instanceof Error ? error.message : 'Invalid telemetry cursor',
136
164
  code: 'INVALID_CURSOR',
137
165
  category: DaemonErrorCategory.BAD_REQUEST,
@@ -144,7 +172,7 @@ function invalidCursor(error) {
144
172
  function authenticateTelemetryRequest(context, req, requestedView) {
145
173
  const principal = context.resolveAuthenticatedPrincipal(req);
146
174
  if (!principal) {
147
- return Response.json({
175
+ return jsonErrorResponse({
148
176
  error: 'Authentication required for telemetry access',
149
177
  code: 'AUTH_REQUIRED',
150
178
  category: DaemonErrorCategory.AUTHENTICATION,
@@ -156,7 +184,7 @@ function authenticateTelemetryRequest(context, req, requestedView) {
156
184
  }
157
185
  const missingRead = buildMissingScopeBody('telemetry access', ['read:telemetry'], principal.scopes);
158
186
  if (missingRead) {
159
- return Response.json({
187
+ return jsonErrorResponse({
160
188
  error: missingRead.error,
161
189
  code: 'MISSING_SCOPE',
162
190
  category: DaemonErrorCategory.AUTHORIZATION,
@@ -164,12 +192,12 @@ function authenticateTelemetryRequest(context, req, requestedView) {
164
192
  recoverable: false,
165
193
  hint: 'Use a token or session with the read:telemetry scope, or elevate to an admin/shared-token session.',
166
194
  status: 403,
167
- detail: JSON.stringify(missingRead),
195
+ detail: missingRead,
168
196
  }, { status: 403 });
169
197
  }
170
198
  const rawAccessible = principal.admin || principal.scopes.includes('read:telemetry-sensitive');
171
199
  if (requestedView === 'raw' && !rawAccessible) {
172
- return Response.json({
200
+ return jsonErrorResponse({
173
201
  error: 'Raw telemetry view requires elevated telemetry scope',
174
202
  code: 'MISSING_SCOPE',
175
203
  category: DaemonErrorCategory.AUTHORIZATION,
@@ -307,10 +335,9 @@ export function createDaemonTelemetryRouteHandlers(context) {
307
335
  // OTLP POST ingest receivers
308
336
  // -------------------------------------------------------------------------
309
337
  postTelemetryOtlpLogs: async (req) => {
310
- const auth = context.resolveAuthenticatedPrincipal(req);
311
- if (!auth) {
312
- return Response.json({ error: 'Authentication required for OTLP ingest', code: 'AUTH_REQUIRED', category: DaemonErrorCategory.AUTHENTICATION, status: 401 }, { status: 401 });
313
- }
338
+ const auth = authenticateTelemetryIngest(context, req);
339
+ if (auth instanceof Response)
340
+ return auth;
314
341
  const bodyOrErr = await parseOtlpBody(req, 'logs');
315
342
  if (bodyOrErr instanceof Response)
316
343
  return bodyOrErr;
@@ -318,10 +345,9 @@ export function createDaemonTelemetryRouteHandlers(context) {
318
345
  return otlpIngestSuccess('logs');
319
346
  },
320
347
  postTelemetryOtlpTraces: async (req) => {
321
- const auth = context.resolveAuthenticatedPrincipal(req);
322
- if (!auth) {
323
- return Response.json({ error: 'Authentication required for OTLP ingest', code: 'AUTH_REQUIRED', category: DaemonErrorCategory.AUTHENTICATION, status: 401 }, { status: 401 });
324
- }
348
+ const auth = authenticateTelemetryIngest(context, req);
349
+ if (auth instanceof Response)
350
+ return auth;
325
351
  const bodyOrErr = await parseOtlpBody(req, 'traces');
326
352
  if (bodyOrErr instanceof Response)
327
353
  return bodyOrErr;
@@ -329,10 +355,9 @@ export function createDaemonTelemetryRouteHandlers(context) {
329
355
  return otlpIngestSuccess('traces');
330
356
  },
331
357
  postTelemetryOtlpMetrics: async (req) => {
332
- const auth = context.resolveAuthenticatedPrincipal(req);
333
- if (!auth) {
334
- return Response.json({ error: 'Authentication required for OTLP ingest', code: 'AUTH_REQUIRED', category: DaemonErrorCategory.AUTHENTICATION, status: 401 }, { status: 401 });
335
- }
358
+ const auth = authenticateTelemetryIngest(context, req);
359
+ if (auth instanceof Response)
360
+ return auth;
336
361
  const bodyOrErr = await parseOtlpBody(req, 'metrics');
337
362
  if (bodyOrErr instanceof Response)
338
363
  return bodyOrErr;