@pellux/goodvibes-daemon-sdk 0.30.2 → 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,7 +1,114 @@
1
1
  import { jsonErrorResponse } from './error-response.js';
2
- import { serializableJsonResponse } from './route-helpers.js';
2
+ import { createRouteBodySchema, createRouteBodySchemaRegistry, readOptionalStringField, readStringArrayField, serializableJsonResponse, } from './route-helpers.js';
3
3
  const MAX_REMOTE_RESULT_BYTES = 1_000_000;
4
4
  const MAX_REMOTE_PAYLOAD_BYTES = 1_000_000;
5
+ const MAX_REMOTE_CAPABILITIES = 128;
6
+ const MAX_REMOTE_COMMANDS = 128;
7
+ const MAX_REMOTE_SCOPES = 128;
8
+ const remoteBodySchemas = createRouteBodySchemaRegistry({
9
+ pairRequest: createRouteBodySchema('POST /api/remote/pair', (body) => {
10
+ const label = readOptionalStringField(body, 'label');
11
+ if (!label)
12
+ return jsonErrorResponse({ error: 'Missing remote peer label' }, { status: 400 });
13
+ const requestedId = readOptionalStringField(body, 'requestedId');
14
+ const platform = readOptionalStringField(body, 'platform');
15
+ const deviceFamily = readOptionalStringField(body, 'deviceFamily');
16
+ const version = readOptionalStringField(body, 'version');
17
+ const clientMode = readOptionalStringField(body, 'clientMode');
18
+ const ttlMs = boundedPositiveNumber(body.ttlMs, 1_000, 86_400_000);
19
+ return {
20
+ peerKind: body.peerKind === 'device' ? 'device' : 'node',
21
+ ...(requestedId ? { requestedId } : {}),
22
+ label,
23
+ ...(platform ? { platform } : {}),
24
+ ...(deviceFamily ? { deviceFamily } : {}),
25
+ ...(version ? { version } : {}),
26
+ ...(clientMode ? { clientMode } : {}),
27
+ capabilities: readStringArrayField(body, 'capabilities', MAX_REMOTE_CAPABILITIES) ?? [],
28
+ commands: readStringArrayField(body, 'commands', MAX_REMOTE_COMMANDS) ?? [],
29
+ metadata: readRemoteMetadata(body.metadata),
30
+ ...(ttlMs !== undefined ? { ttlMs } : {}),
31
+ };
32
+ }),
33
+ pairVerify: createRouteBodySchema('POST /api/remote/pair/verify', (body) => {
34
+ const requestId = readOptionalStringField(body, 'requestId');
35
+ const challenge = readOptionalStringField(body, 'challenge');
36
+ if (!requestId || !challenge) {
37
+ return jsonErrorResponse({ error: 'Missing requestId or challenge' }, { status: 400 });
38
+ }
39
+ return { requestId, challenge, metadata: readRemoteMetadata(body.metadata) };
40
+ }),
41
+ peerHeartbeat: createRouteBodySchema('POST /api/remote/heartbeat', (body) => {
42
+ const capabilities = readStringArrayField(body, 'capabilities', MAX_REMOTE_CAPABILITIES);
43
+ const commands = readStringArrayField(body, 'commands', MAX_REMOTE_COMMANDS);
44
+ const version = readOptionalStringField(body, 'version');
45
+ const clientMode = readOptionalStringField(body, 'clientMode');
46
+ return {
47
+ ...(capabilities ? { capabilities } : {}),
48
+ ...(commands ? { commands } : {}),
49
+ ...(version ? { version } : {}),
50
+ ...(clientMode ? { clientMode } : {}),
51
+ metadata: readRemoteMetadata(body.metadata),
52
+ };
53
+ }),
54
+ workPull: createRouteBodySchema('POST /api/remote/work/pull', (body) => {
55
+ const maxItems = boundedPositiveNumber(body.maxItems, 1, 100);
56
+ const leaseMs = boundedPositiveNumber(body.leaseMs, 1_000, 3_600_000);
57
+ return {
58
+ ...(maxItems !== undefined ? { maxItems } : {}),
59
+ ...(leaseMs !== undefined ? { leaseMs } : {}),
60
+ };
61
+ }),
62
+ workComplete: createRouteBodySchema('POST /api/remote/work/:workId/complete', (body) => {
63
+ const error = readOptionalStringField(body, 'error');
64
+ return {
65
+ ...(body.status === 'failed' || body.status === 'cancelled' || body.status === 'completed' ? { status: body.status } : {}),
66
+ result: body.result,
67
+ ...(error ? { error } : {}),
68
+ metadata: readRemoteMetadata(body.metadata),
69
+ };
70
+ }),
71
+ operatorNote: createRouteBodySchema('POST /api/remote/operator-action', (body) => {
72
+ const note = readOptionalStringField(body, 'note');
73
+ const reason = readOptionalStringField(body, 'reason');
74
+ const label = readOptionalStringField(body, 'label');
75
+ const tokenId = readOptionalStringField(body, 'tokenId');
76
+ const scopes = readStringArrayField(body, 'scopes', MAX_REMOTE_SCOPES);
77
+ return {
78
+ ...(note ? { note } : {}),
79
+ ...(reason ? { reason } : {}),
80
+ ...(label ? { label } : {}),
81
+ ...(tokenId ? { tokenId } : {}),
82
+ ...(typeof body.requeueClaimedWork === 'boolean' ? { requeueClaimedWork: body.requeueClaimedWork } : {}),
83
+ ...(scopes ? { scopes } : {}),
84
+ };
85
+ }),
86
+ invoke: createRouteBodySchema('POST /api/remote/peers/:peerId/invoke', (body) => {
87
+ const command = readOptionalStringField(body, 'command');
88
+ if (!command)
89
+ return jsonErrorResponse({ error: 'Missing remote invoke command' }, { status: 400 });
90
+ const waitMs = boundedPositiveNumber(body.waitMs, 0, 300_000);
91
+ const timeoutMs = boundedPositiveNumber(body.timeoutMs, 1_000, 300_000);
92
+ const sessionId = readOptionalStringField(body, 'sessionId');
93
+ const routeId = readOptionalStringField(body, 'routeId');
94
+ const automationRunId = readOptionalStringField(body, 'automationRunId');
95
+ const automationJobId = readOptionalStringField(body, 'automationJobId');
96
+ const approvalId = readOptionalStringField(body, 'approvalId');
97
+ return {
98
+ command,
99
+ payload: body.payload,
100
+ priority: body.priority === 'high' || body.priority === 'default' ? body.priority : 'normal',
101
+ ...(waitMs !== undefined ? { waitMs } : {}),
102
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
103
+ ...(sessionId ? { sessionId } : {}),
104
+ ...(routeId ? { routeId } : {}),
105
+ ...(automationRunId ? { automationRunId } : {}),
106
+ ...(automationJobId ? { automationJobId } : {}),
107
+ ...(approvalId ? { approvalId } : {}),
108
+ metadata: readRemoteMetadata(body.metadata),
109
+ };
110
+ }),
111
+ });
5
112
  export function createDaemonRemoteRouteHandlers(context) {
6
113
  return {
7
114
  getRemotePairRequests: () => Response.json({ requests: context.distributedRuntime.listPairRequests() }),
@@ -21,24 +128,13 @@ export async function handleRemotePairRequest(context, req) {
21
128
  const body = await context.parseJsonBody(req);
22
129
  if (body instanceof Response)
23
130
  return body;
24
- const peerKind = body.peerKind === 'device' ? 'device' : 'node';
25
- const label = typeof body.label === 'string' ? body.label.trim() : '';
26
- if (!label) {
27
- return Response.json({ error: 'Missing remote peer label' }, { status: 400 });
28
- }
131
+ const input = remoteBodySchemas.pairRequest.parse(body);
132
+ if (input instanceof Response)
133
+ return input;
29
134
  const created = await context.distributedRuntime.requestPairing({
30
- peerKind,
31
- requestedId: typeof body.requestedId === 'string' ? body.requestedId : undefined,
32
- label,
33
- platform: typeof body.platform === 'string' ? body.platform : undefined,
34
- deviceFamily: typeof body.deviceFamily === 'string' ? body.deviceFamily : undefined,
35
- version: typeof body.version === 'string' ? body.version : undefined,
36
- clientMode: typeof body.clientMode === 'string' ? body.clientMode : undefined,
37
- capabilities: Array.isArray(body.capabilities) ? body.capabilities.filter((value) => typeof value === 'string') : [],
38
- commands: Array.isArray(body.commands) ? body.commands.filter((value) => typeof value === 'string') : [],
39
- metadata: readMetadataWithClientHintForwardedFor(req, body.metadata),
135
+ ...input,
136
+ metadata: readMetadataWithClientHintForwardedFor(req, input.metadata),
40
137
  requestedBy: 'remote',
41
- ttlMs: boundedPositiveNumber(body.ttlMs, 1_000, 86_400_000),
42
138
  });
43
139
  return Response.json(created, { status: 201 });
44
140
  }
@@ -46,17 +142,15 @@ export async function handleRemotePairVerify(context, req) {
46
142
  const body = await context.parseJsonBody(req);
47
143
  if (body instanceof Response)
48
144
  return body;
49
- const requestId = typeof body.requestId === 'string' ? body.requestId : '';
50
- const challenge = typeof body.challenge === 'string' ? body.challenge : '';
51
- if (!requestId || !challenge) {
52
- return Response.json({ error: 'Missing requestId or challenge' }, { status: 400 });
53
- }
54
- const verified = await context.distributedRuntime.verifyPairRequest(requestId, challenge, {
55
- metadata: readMetadataWithClientHintForwardedFor(req, body.metadata),
145
+ const input = remoteBodySchemas.pairVerify.parse(body);
146
+ if (input instanceof Response)
147
+ return input;
148
+ const verified = await context.distributedRuntime.verifyPairRequest(input.requestId, input.challenge, {
149
+ metadata: readMetadataWithClientHintForwardedFor(req, input.metadata),
56
150
  });
57
151
  return verified
58
152
  ? Response.json(verified)
59
- : Response.json({ error: 'Pair request not approved, expired, or invalid' }, { status: 404 });
153
+ : jsonErrorResponse({ error: 'Pair request not approved, expired, or invalid' }, { status: 404 });
60
154
  }
61
155
  export async function handleRemotePeerHeartbeat(context, req) {
62
156
  const auth = await context.requireRemotePeer(req, 'remote:heartbeat');
@@ -65,12 +159,12 @@ export async function handleRemotePeerHeartbeat(context, req) {
65
159
  const body = await context.parseJsonBody(req);
66
160
  if (body instanceof Response)
67
161
  return body;
162
+ const input = remoteBodySchemas.peerHeartbeat.parse(body);
163
+ if (input instanceof Response)
164
+ return input;
68
165
  const peer = await context.distributedRuntime.heartbeatPeer(auth, {
69
- capabilities: Array.isArray(body.capabilities) ? body.capabilities.filter((value) => typeof value === 'string') : undefined,
70
- commands: Array.isArray(body.commands) ? body.commands.filter((value) => typeof value === 'string') : undefined,
71
- version: typeof body.version === 'string' ? body.version : undefined,
72
- clientMode: typeof body.clientMode === 'string' ? body.clientMode : undefined,
73
- metadata: readMetadataWithClientHintForwardedFor(req, body.metadata),
166
+ ...input,
167
+ metadata: readMetadataWithClientHintForwardedFor(req, input.metadata),
74
168
  });
75
169
  return Response.json({ peer });
76
170
  }
@@ -81,9 +175,12 @@ export async function handleRemotePeerWorkPull(context, req) {
81
175
  const body = await context.parseJsonBody(req);
82
176
  if (body instanceof Response)
83
177
  return body;
178
+ const input = remoteBodySchemas.workPull.parse(body);
179
+ if (input instanceof Response)
180
+ return input;
84
181
  const work = await context.distributedRuntime.claimWork(auth, {
85
- maxItems: boundedPositiveNumber(body.maxItems, 1, 100),
86
- leaseMs: boundedPositiveNumber(body.leaseMs, 1_000, 3_600_000),
182
+ maxItems: input.maxItems,
183
+ leaseMs: input.leaseMs,
87
184
  });
88
185
  return Response.json({ work });
89
186
  }
@@ -94,19 +191,24 @@ export async function handleRemotePeerWorkComplete(context, workId, req) {
94
191
  const body = await context.parseJsonBody(req);
95
192
  if (body instanceof Response)
96
193
  return body;
97
- const result = validateRemoteJsonPayload(body.result, MAX_REMOTE_RESULT_BYTES, 'result');
194
+ const input = remoteBodySchemas.workComplete.parse(body);
195
+ if (input instanceof Response)
196
+ return input;
197
+ const result = validateRemoteJsonPayload(input.result, MAX_REMOTE_RESULT_BYTES, 'result');
98
198
  if (result instanceof Response)
99
199
  return result;
100
200
  const work = await context.distributedRuntime.completeWork(auth, workId, {
101
- status: body.status === 'failed' || body.status === 'cancelled' ? body.status : body.status === 'completed' ? 'completed' : undefined,
201
+ status: input.status,
102
202
  result,
103
- error: typeof body.error === 'string' ? body.error : undefined,
104
- metadata: typeof body.metadata === 'object' && body.metadata !== null ? body.metadata : {},
203
+ error: input.error,
204
+ metadata: input.metadata,
105
205
  });
106
206
  return work
107
207
  ? Response.json({ work })
108
- : Response.json({ error: 'Unknown or unclaimed remote work item' }, { status: 404 });
208
+ : jsonErrorResponse({ error: 'Unknown or unclaimed remote work item' }, { status: 404 });
109
209
  }
210
+ // Non-numeric input is omitted from downstream calls so services can apply
211
+ // their own defaults.
110
212
  function boundedPositiveNumber(value, min, max) {
111
213
  if (typeof value !== 'number' || !Number.isFinite(value))
112
214
  return undefined;
@@ -115,118 +217,95 @@ function boundedPositiveNumber(value, min, max) {
115
217
  function validateRemoteJsonPayload(value, maxBytes, field) {
116
218
  const size = estimateJsonByteLengthWithinLimit(value ?? null, maxBytes);
117
219
  if (size.kind === 'invalid') {
118
- return Response.json({
220
+ return jsonErrorResponse({
119
221
  error: `Remote ${field} must be JSON-serializable`,
222
+ code: 'INVALID_REMOTE_JSON_PAYLOAD',
120
223
  }, { status: 400 });
121
224
  }
122
225
  if (size.byteLength <= maxBytes)
123
226
  return value;
124
- return Response.json({
125
- error: `Remote ${field} exceeds ${maxBytes} byte limit`,
227
+ return jsonErrorResponse({
228
+ error: `Remote ${field} exceeds ${maxBytes} bytes after JSON encoding. Reduce payload size before retrying.`,
229
+ code: 'REMOTE_PAYLOAD_TOO_LARGE',
126
230
  }, { status: 413 });
127
231
  }
128
- function estimateJsonByteLengthWithinLimit(value, maxBytes, seen = new Set()) {
129
- const byteLength = measureJsonByteLength(value, maxBytes, seen);
130
- return byteLength === undefined ? { kind: 'invalid' } : { kind: 'ok', byteLength };
131
- }
132
- function measureJsonByteLength(value, maxBytes, seen) {
133
- const valueType = typeof value;
134
- if (value === null)
135
- return 4;
136
- if (typeof value === 'string')
137
- return jsonStringByteLength(value, maxBytes);
138
- if (valueType === 'number')
139
- return Number.isFinite(value) ? String(value).length : 4;
140
- if (valueType === 'boolean')
141
- return value ? 4 : 5;
142
- if (valueType === 'undefined' || valueType === 'function' || valueType === 'symbol')
143
- return 4;
144
- if (valueType !== 'object')
145
- return undefined;
146
- const objectValue = value;
147
- if (seen.has(objectValue))
148
- return undefined;
149
- seen.add(objectValue);
150
- try {
151
- let total = 2;
152
- if (Array.isArray(value)) {
153
- for (let index = 0; index < value.length; index += 1) {
154
- if (index > 0)
155
- total += 1;
156
- if (total > maxBytes)
157
- return total;
158
- const entrySize = measureJsonByteLength(value[index], maxBytes - total, seen);
159
- if (entrySize === undefined)
160
- return undefined;
161
- total += entrySize;
162
- if (total > maxBytes) {
163
- return total;
164
- }
165
- }
166
- return total;
232
+ /**
233
+ * Compute the exact JSON-encoded byte length of a string without allocating the encoded form.
234
+ * Walks each code unit and counts the bytes JSON.stringify would emit:
235
+ * - 2 bytes for the surrounding quotes
236
+ * - 2 bytes for short-escape characters (", \, \b, \t, \n, \f, \r)
237
+ * - 6 bytes for other control chars (\uXXXX)
238
+ * - 1–4 bytes per non-control code point (UTF-8 encoding length)
239
+ */
240
+ function jsonStringByteLength(s) {
241
+ let len = 2; // surrounding quotes
242
+ for (let i = 0; i < s.length; i++) {
243
+ const c = s.charCodeAt(i);
244
+ if (c === 0x22 || c === 0x5c || c === 0x08 || c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d) {
245
+ len += 2; // \" \\ \b \t \n \f \r
167
246
  }
168
- let count = 0;
169
- for (const [key, entryValue] of Object.entries(value)) {
170
- const entryType = typeof entryValue;
171
- if (entryType === 'undefined' || entryType === 'function' || entryType === 'symbol')
172
- continue;
173
- if (count > 0)
174
- total += 1;
175
- total += jsonStringByteLength(key, maxBytes - total) + 1;
176
- if (total > maxBytes)
177
- return total;
178
- const entrySize = measureJsonByteLength(entryValue, maxBytes - total, seen);
179
- if (entrySize === undefined)
180
- return undefined;
181
- total += entrySize;
182
- count += 1;
183
- if (total > maxBytes) {
184
- return total;
185
- }
247
+ else if (c < 0x20) {
248
+ len += 6; // \uXXXX
186
249
  }
187
- return total;
188
- }
189
- finally {
190
- seen.delete(objectValue);
191
- }
192
- }
193
- function jsonStringByteLength(value, maxBytes = Number.POSITIVE_INFINITY) {
194
- let total = 2;
195
- for (let index = 0; index < value.length; index += 1) {
196
- const code = value.charCodeAt(index);
197
- if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x0c || code === 0x0a || code === 0x0d || code === 0x09) {
198
- total += 2;
250
+ else if (c < 0x80) {
251
+ len += 1;
199
252
  }
200
- else if (code <= 0x1f) {
201
- total += 6;
253
+ else if (c < 0x800) {
254
+ len += 2;
202
255
  }
203
- else if (code >= 0xd800 && code <= 0xdbff) {
204
- const next = value.charCodeAt(index + 1);
205
- if (next >= 0xdc00 && next <= 0xdfff) {
206
- total += 4;
207
- index += 1;
208
- }
209
- else {
210
- total += 6;
211
- }
212
- }
213
- else if (code >= 0xdc00 && code <= 0xdfff) {
214
- total += 6;
256
+ else if ((c & 0xfc00) === 0xd800 && i + 1 < s.length && (s.charCodeAt(i + 1) & 0xfc00) === 0xdc00) {
257
+ len += 4; // surrogate pair → 4-byte UTF-8
258
+ i++;
215
259
  }
216
260
  else {
217
- total += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : 3;
261
+ len += 3;
218
262
  }
219
- if (total > maxBytes)
220
- return total;
221
263
  }
222
- return total;
264
+ return len;
223
265
  }
224
- function readMetadataWithClientHintForwardedFor(req, value) {
225
- const metadata = typeof value === 'object' && value !== null && !Array.isArray(value)
226
- ? { ...value }
266
+ export function estimateJsonByteLengthWithinLimit(value, maxBytes) {
267
+ // walk the value with a counting replacer so we never allocate
268
+ // the full encoded string. The replacer throws a sentinel when the running
269
+ // byte total exceeds maxBytes, preventing peak allocation before the cap.
270
+ // switched from `val.length * 6` worst-case to exact
271
+ // jsonStringByteLength() count — same allocation profile, much tighter near the cap.
272
+ let byteCount = 0;
273
+ const OVER_LIMIT = Symbol('over-limit');
274
+ try {
275
+ JSON.stringify(value, (_key, val) => {
276
+ if (typeof val === 'string') {
277
+ byteCount += jsonStringByteLength(val);
278
+ }
279
+ else {
280
+ // null/number/boolean/object-open/array-open — add a small fixed cost per node.
281
+ byteCount += 16;
282
+ }
283
+ if (byteCount > maxBytes) {
284
+ // eslint-disable-next-line @typescript-eslint/no-throw-literal
285
+ throw OVER_LIMIT;
286
+ }
287
+ return val;
288
+ });
289
+ if (value === undefined)
290
+ return { kind: 'ok', byteLength: 4 }; // undefined → JSON undefined
291
+ // Result is an exact-count for strings; node-overhead remains a fixed approximation.
292
+ return { kind: 'ok', byteLength: Math.min(byteCount, maxBytes + 1) };
293
+ }
294
+ catch (err) {
295
+ if (err === OVER_LIMIT)
296
+ return { kind: 'ok', byteLength: maxBytes + 1 };
297
+ return { kind: 'invalid' };
298
+ }
299
+ }
300
+ function readRemoteMetadata(value) {
301
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
302
+ ? value
227
303
  : {};
304
+ }
305
+ function readMetadataWithClientHintForwardedFor(req, value) {
306
+ const metadata = { ...readRemoteMetadata(value) };
228
307
  const clientHintForwardedFor = readClientHintForwardedFor(req);
229
- return clientHintForwardedFor ? { ...metadata, clientHintForwardedFor } : metadata;
308
+ return clientHintForwardedFor ? { ...metadata, x_forwarded_for_untrusted: clientHintForwardedFor } : metadata;
230
309
  }
231
310
  function readClientHintForwardedFor(req) {
232
311
  // x-forwarded-for is caller-controlled unless the daemon is explicitly behind
@@ -245,15 +324,18 @@ async function handleApproveRemotePairRequest(context, requestId, req) {
245
324
  const body = await context.parseJsonBody(req);
246
325
  if (body instanceof Response)
247
326
  return body;
327
+ const input = remoteBodySchemas.operatorNote.parse(body);
328
+ if (input instanceof Response)
329
+ return input;
248
330
  const approved = await context.distributedRuntime.approvePairRequest(requestId, {
249
331
  actor: operatorActor(context, req),
250
- note: typeof body.note === 'string' ? body.note : undefined,
251
- label: typeof body.label === 'string' ? body.label : undefined,
252
- metadata: typeof body.metadata === 'object' && body.metadata !== null ? body.metadata : {},
332
+ note: input.note,
333
+ label: input.label,
334
+ metadata: readRemoteMetadata(body.metadata),
253
335
  });
254
336
  return approved
255
337
  ? Response.json(approved)
256
- : Response.json({ error: 'Unknown remote pair request' }, { status: 404 });
338
+ : jsonErrorResponse({ error: 'Unknown remote pair request' }, { status: 404 });
257
339
  }
258
340
  async function handleRejectRemotePairRequest(context, requestId, req) {
259
341
  const admin = context.requireAdmin(req);
@@ -262,13 +344,16 @@ async function handleRejectRemotePairRequest(context, requestId, req) {
262
344
  const body = await context.parseJsonBody(req);
263
345
  if (body instanceof Response)
264
346
  return body;
347
+ const input = remoteBodySchemas.operatorNote.parse(body);
348
+ if (input instanceof Response)
349
+ return input;
265
350
  const rejected = await context.distributedRuntime.rejectPairRequest(requestId, {
266
351
  actor: operatorActor(context, req),
267
- note: typeof body.note === 'string' ? body.note : undefined,
352
+ note: input.note,
268
353
  });
269
354
  return rejected
270
355
  ? Response.json(rejected)
271
- : Response.json({ error: 'Unknown remote pair request' }, { status: 404 });
356
+ : jsonErrorResponse({ error: 'Unknown remote pair request' }, { status: 404 });
272
357
  }
273
358
  async function handleRotateRemotePeerToken(context, peerId, req) {
274
359
  const admin = context.requireAdmin(req);
@@ -277,14 +362,17 @@ async function handleRotateRemotePeerToken(context, peerId, req) {
277
362
  const body = await context.parseJsonBody(req);
278
363
  if (body instanceof Response)
279
364
  return body;
365
+ const input = remoteBodySchemas.operatorNote.parse(body);
366
+ if (input instanceof Response)
367
+ return input;
280
368
  const rotated = await context.distributedRuntime.rotatePeerToken(peerId, {
281
369
  actor: operatorActor(context, req),
282
- label: typeof body.label === 'string' ? body.label : undefined,
283
- scopes: Array.isArray(body.scopes) ? body.scopes.filter((value) => typeof value === 'string') : undefined,
370
+ label: input.label,
371
+ scopes: input.scopes,
284
372
  });
285
373
  return rotated
286
374
  ? Response.json(rotated)
287
- : Response.json({ error: 'Unknown distributed peer' }, { status: 404 });
375
+ : jsonErrorResponse({ error: 'Unknown distributed peer' }, { status: 404 });
288
376
  }
289
377
  async function handleRevokeRemotePeerToken(context, peerId, req) {
290
378
  const admin = context.requireAdmin(req);
@@ -293,14 +381,17 @@ async function handleRevokeRemotePeerToken(context, peerId, req) {
293
381
  const body = await context.parseJsonBody(req);
294
382
  if (body instanceof Response)
295
383
  return body;
384
+ const input = remoteBodySchemas.operatorNote.parse(body);
385
+ if (input instanceof Response)
386
+ return input;
296
387
  const peer = await context.distributedRuntime.revokePeerToken(peerId, {
297
388
  actor: operatorActor(context, req),
298
- tokenId: typeof body.tokenId === 'string' ? body.tokenId : undefined,
299
- note: typeof body.note === 'string' ? body.note : undefined,
389
+ tokenId: input.tokenId,
390
+ note: input.note,
300
391
  });
301
392
  return peer
302
393
  ? Response.json({ peer })
303
- : Response.json({ error: 'Unknown distributed peer' }, { status: 404 });
394
+ : jsonErrorResponse({ error: 'Unknown distributed peer' }, { status: 404 });
304
395
  }
305
396
  async function handleDisconnectRemotePeer(context, peerId, req) {
306
397
  const admin = context.requireAdmin(req);
@@ -309,14 +400,17 @@ async function handleDisconnectRemotePeer(context, peerId, req) {
309
400
  const body = await context.parseJsonBody(req);
310
401
  if (body instanceof Response)
311
402
  return body;
403
+ const input = remoteBodySchemas.operatorNote.parse(body);
404
+ if (input instanceof Response)
405
+ return input;
312
406
  const peer = await context.distributedRuntime.disconnectPeer(peerId, {
313
407
  actor: operatorActor(context, req),
314
- note: typeof body.note === 'string' ? body.note : undefined,
315
- requeueClaimedWork: typeof body.requeueClaimedWork === 'boolean' ? body.requeueClaimedWork : undefined,
408
+ note: input.note,
409
+ requeueClaimedWork: input.requeueClaimedWork,
316
410
  });
317
411
  return peer
318
412
  ? Response.json({ peer })
319
- : Response.json({ error: 'Unknown distributed peer' }, { status: 404 });
413
+ : jsonErrorResponse({ error: 'Unknown distributed peer' }, { status: 404 });
320
414
  }
321
415
  async function handleInvokeRemotePeer(context, peerId, req) {
322
416
  const admin = context.requireAdmin(req);
@@ -325,28 +419,27 @@ async function handleInvokeRemotePeer(context, peerId, req) {
325
419
  const body = await context.parseJsonBody(req);
326
420
  if (body instanceof Response)
327
421
  return body;
328
- const command = typeof body.command === 'string' ? body.command.trim() : '';
329
- if (!command) {
330
- return Response.json({ error: 'Missing remote invoke command' }, { status: 400 });
331
- }
332
- const payload = validateRemoteJsonPayload(body.payload, MAX_REMOTE_PAYLOAD_BYTES, 'payload');
422
+ const input = remoteBodySchemas.invoke.parse(body);
423
+ if (input instanceof Response)
424
+ return input;
425
+ const payload = validateRemoteJsonPayload(input.payload, MAX_REMOTE_PAYLOAD_BYTES, 'payload');
333
426
  if (payload instanceof Response)
334
427
  return payload;
335
428
  try {
336
429
  const invoked = await context.distributedRuntime.invokePeer({
337
430
  peerId,
338
- command,
431
+ command: input.command,
339
432
  payload,
340
- priority: body.priority === 'high' || body.priority === 'default' ? body.priority : 'normal',
433
+ priority: input.priority,
341
434
  actor: operatorActor(context, req),
342
- waitMs: boundedPositiveNumber(body.waitMs, 0, 300_000),
343
- timeoutMs: boundedPositiveNumber(body.timeoutMs, 1_000, 300_000),
344
- sessionId: typeof body.sessionId === 'string' ? body.sessionId : undefined,
345
- routeId: typeof body.routeId === 'string' ? body.routeId : undefined,
346
- automationRunId: typeof body.automationRunId === 'string' ? body.automationRunId : undefined,
347
- automationJobId: typeof body.automationJobId === 'string' ? body.automationJobId : undefined,
348
- approvalId: typeof body.approvalId === 'string' ? body.approvalId : undefined,
349
- metadata: typeof body.metadata === 'object' && body.metadata !== null ? body.metadata : {},
435
+ waitMs: input.waitMs,
436
+ timeoutMs: input.timeoutMs,
437
+ sessionId: input.sessionId,
438
+ routeId: input.routeId,
439
+ automationRunId: input.automationRunId,
440
+ automationJobId: input.automationJobId,
441
+ approvalId: input.approvalId,
442
+ metadata: input.metadata,
350
443
  });
351
444
  return Response.json(invoked, { status: 202 });
352
445
  }
@@ -361,11 +454,14 @@ async function handleCancelRemoteWork(context, workId, req) {
361
454
  const body = await context.parseJsonBody(req);
362
455
  if (body instanceof Response)
363
456
  return body;
457
+ const input = remoteBodySchemas.operatorNote.parse(body);
458
+ if (input instanceof Response)
459
+ return input;
364
460
  const work = await context.distributedRuntime.cancelWork(workId, {
365
461
  actor: operatorActor(context, req),
366
- reason: typeof body.reason === 'string' ? body.reason : undefined,
462
+ reason: input.reason,
367
463
  });
368
464
  return work
369
465
  ? Response.json({ work })
370
- : Response.json({ error: 'Unknown remote work item' }, { status: 404 });
466
+ : jsonErrorResponse({ error: 'Unknown remote work item' }, { status: 404 });
371
467
  }
@@ -1,16 +1,27 @@
1
1
  export type JsonRecord = Record<string, unknown>;
2
+ export type JsonBody = JsonRecord;
3
+ export interface RouteBodySchema<T> {
4
+ readonly routeId: string;
5
+ readonly parse: (body: JsonRecord) => T | Response;
6
+ }
7
+ export declare function createRouteBodySchema<T>(routeId: string, parse: (body: JsonRecord) => T | Response): RouteBodySchema<T>;
8
+ export declare function createRouteBodySchemaRegistry<const TSchemaMap extends Record<string, RouteBodySchema<unknown>>>(schemas: TSchemaMap): TSchemaMap;
2
9
  export declare function isJsonRecord(value: unknown): value is JsonRecord;
3
10
  export declare function toSerializableJson(value: unknown, stack?: Map<object, string>, path?: string): unknown;
4
11
  export declare function serializableJsonResponse(body: unknown, init?: ResponseInit): Response;
5
12
  export interface BoundedIntegerOptions {
6
13
  readonly fallback: number;
7
- readonly min?: number;
8
- readonly max?: number;
14
+ readonly min?: number | undefined;
15
+ readonly max?: number | undefined;
9
16
  }
10
17
  export declare function readBoundedInteger(raw: string | null, options: BoundedIntegerOptions): number;
11
18
  export declare function readBoundedPositiveInteger(raw: string | null, fallback: number, max?: number): number;
19
+ export declare function readBoundedBodyInteger(value: unknown, fallback: number, max: number, min?: number): number;
12
20
  export declare function readOptionalBoundedInteger(raw: string | null, min: number, max: number): number | undefined;
21
+ export declare function readOptionalStringField(body: JsonRecord, key: string): string | undefined;
22
+ export declare function readStringArrayField(body: JsonRecord, key: string, max?: number): string[] | undefined;
13
23
  export declare function scopeMatches(granted: string, required: string): boolean;
24
+ export declare function hasAnyScope(grantedScopes: readonly string[] | undefined, requiredScopes: readonly string[]): boolean;
14
25
  export declare function missingScopes(grantedScopes: readonly string[] | undefined, requiredScopes: readonly string[]): string[];
15
26
  export type ChannelLifecycleAction = 'inspect' | 'setup' | 'retest' | 'connect' | 'disconnect' | 'start' | 'stop' | 'login' | 'logout' | 'wait_login';
16
27
  export type ChannelConversationKind = 'direct' | 'group' | 'channel' | 'thread' | 'service';
@@ -1 +1 @@
1
- {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../src/route-helpers.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,sBAA4B,EAAE,IAAI,SAAM,GAAG,OAAO,CAiBzG;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,QAAQ,CAErF;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAO7F;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAQ,GAAG,MAAM,CAEpG;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK3G;AAMD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMvE;AAED,wBAAgB,aAAa,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EAAE,cAAc,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAGvH;AAED,MAAM,MAAM,sBAAsB,GAC9B,SAAS,GACT,OAAO,GACP,QAAQ,GACR,SAAS,GACT,YAAY,GACZ,OAAO,GACP,MAAM,GACN,OAAO,GACP,QAAQ,GACR,YAAY,CAAC;AAEjB,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5F,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,GAAG,IAAI,CAaxF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAI1F"}
1
+ {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../src/route-helpers.ts"],"names":[],"mappings":"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"}