hiloop-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +823 -0
- package/dist/client.js +1181 -0
- package/dist/crypto.d.ts +113 -0
- package/dist/crypto.js +278 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +204 -0
- package/dist/types.js +122 -0
- package/package.json +23 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
/** Hiloop SDK client for agents to interact with the Hiloop platform. */
|
|
2
|
+
import { CryptoContext, decryptAes, deriveWrappingKey, encryptAes, generateKeyPair } from "./crypto.js";
|
|
3
|
+
import { parseChannel, parseChannelMessage, parseChannelParticipant, parseConvSession, parseConvSessionMessage, parseGuestToken, parseInteraction, parseMessage, } from "./types.js";
|
|
4
|
+
export class HiloopError extends Error {
|
|
5
|
+
constructor(statusCode, message) {
|
|
6
|
+
super(`HTTP ${statusCode}: ${message}`);
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
this.name = "HiloopError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class HiloopClient {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.apiKey = options.apiKey;
|
|
14
|
+
this.baseUrl = (options.baseUrl ?? "http://localhost:8000").replace(/\/$/, "");
|
|
15
|
+
this.timeout = options.timeout ?? 30000;
|
|
16
|
+
this.secretKeyB64 = options.secretKey ?? "";
|
|
17
|
+
this.publicKeyB64 = options.publicKey ?? "";
|
|
18
|
+
this.crypto = new CryptoContext(options.secretKey ?? "", options.publicKey ?? "", options.spacePublicKey ?? "", options.spaceKeyId ?? "", options.serverKeyId);
|
|
19
|
+
}
|
|
20
|
+
encryptField(text) {
|
|
21
|
+
return this.crypto.encryptForApi(text);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Encrypt multiple fields with a SINGLE shared content key (AES-256-GCM).
|
|
25
|
+
* The content key is wrapped once using ECDH + HKDF + AES-GCM.
|
|
26
|
+
*
|
|
27
|
+
* This avoids the DB unique constraint issue where per-field wrappings
|
|
28
|
+
* with the same (scope, recipientKeyId) are silently dropped.
|
|
29
|
+
*/
|
|
30
|
+
async encryptFields(fields) {
|
|
31
|
+
const encrypted = {};
|
|
32
|
+
const entries = Object.entries(fields).filter(([, v]) => v !== undefined);
|
|
33
|
+
if (entries.length === 0)
|
|
34
|
+
return { encrypted, contentWrappings: [] };
|
|
35
|
+
// 1. Generate ONE random content key for all fields
|
|
36
|
+
const contentKeyBytes = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
37
|
+
const contentKeyB64 = this.bytesToBase64(contentKeyBytes);
|
|
38
|
+
// 2. Encrypt each field with the shared content key
|
|
39
|
+
for (const [key, value] of entries) {
|
|
40
|
+
encrypted[key] = await encryptAes(value, contentKeyB64);
|
|
41
|
+
}
|
|
42
|
+
// 3. Wrap the content key using ECDH + HKDF (same derivation as MCP + frontend)
|
|
43
|
+
const sharedSecret = this.crypto.getSharedSecret();
|
|
44
|
+
const wrappingKey = await deriveWrappingKey(sharedSecret, "space");
|
|
45
|
+
const wrappedContentKey = await encryptAes(contentKeyB64, wrappingKey);
|
|
46
|
+
return {
|
|
47
|
+
encrypted,
|
|
48
|
+
contentWrappings: [{
|
|
49
|
+
scope: "space",
|
|
50
|
+
recipientKeyId: this.crypto.getSpaceKeyId(),
|
|
51
|
+
wrappedContentKey,
|
|
52
|
+
}],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
bytesToBase64(bytes) {
|
|
56
|
+
let binary = "";
|
|
57
|
+
for (let i = 0; i < bytes.length; i++)
|
|
58
|
+
binary += String.fromCharCode(bytes[i]);
|
|
59
|
+
return btoa(binary);
|
|
60
|
+
}
|
|
61
|
+
async request(method, path, options) {
|
|
62
|
+
let url = `${this.baseUrl}/v1${path}`;
|
|
63
|
+
if (options?.params !== undefined) {
|
|
64
|
+
const qs = new URLSearchParams(options.params).toString();
|
|
65
|
+
url += `?${qs}`;
|
|
66
|
+
}
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timeoutMs = options?.timeout ?? this.timeout;
|
|
69
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(url, {
|
|
72
|
+
method,
|
|
73
|
+
headers: {
|
|
74
|
+
"X-API-Key": this.apiKey,
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
},
|
|
77
|
+
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
78
|
+
signal: controller.signal,
|
|
79
|
+
});
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
throw new HiloopError(res.status, text);
|
|
83
|
+
}
|
|
84
|
+
return (await res.json());
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Fetch binary content (e.g. file download). Returns raw ArrayBuffer. */
|
|
91
|
+
async requestBinary(path) {
|
|
92
|
+
const url = `${this.baseUrl}/v1${path}`;
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(url, {
|
|
97
|
+
method: "GET",
|
|
98
|
+
headers: { "X-API-Key": this.apiKey },
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
throw new HiloopError(res.status, text);
|
|
104
|
+
}
|
|
105
|
+
return await res.arrayBuffer();
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// -- Interactions -----------------------------------------------------------
|
|
112
|
+
async createInteraction(opts) {
|
|
113
|
+
// Validate form schema before encrypting
|
|
114
|
+
if (opts.type === "form" && opts.formSchema === undefined) {
|
|
115
|
+
throw new Error("Form interactions require a formSchema with at least one field.");
|
|
116
|
+
}
|
|
117
|
+
if (opts.formSchema !== undefined) {
|
|
118
|
+
validateFormSchema(opts.formSchema);
|
|
119
|
+
}
|
|
120
|
+
// Encrypt all fields with a SINGLE shared content key to avoid
|
|
121
|
+
// DB unique constraint on (interaction_id, scope, recipient_key_id).
|
|
122
|
+
const { encrypted, contentWrappings } = await this.encryptFields({
|
|
123
|
+
encryptedTitle: opts.title,
|
|
124
|
+
encryptedDescription: opts.description,
|
|
125
|
+
encryptedOptions: opts.options,
|
|
126
|
+
encryptedContext: opts.context,
|
|
127
|
+
encryptedFormSchema: opts.formSchema,
|
|
128
|
+
});
|
|
129
|
+
const body = {
|
|
130
|
+
type: opts.type,
|
|
131
|
+
priority: opts.priority ?? "normal",
|
|
132
|
+
contentWrappings,
|
|
133
|
+
...encrypted,
|
|
134
|
+
};
|
|
135
|
+
if (opts.routeToUser !== undefined)
|
|
136
|
+
body.routeToUser = opts.routeToUser;
|
|
137
|
+
if (opts.routeToTeam !== undefined)
|
|
138
|
+
body.routeToTeam = opts.routeToTeam;
|
|
139
|
+
if (opts.deadlineMinutes !== undefined)
|
|
140
|
+
body.deadlineMinutes = opts.deadlineMinutes;
|
|
141
|
+
if (opts.callbackUrl !== undefined)
|
|
142
|
+
body.callbackUrl = opts.callbackUrl;
|
|
143
|
+
if (opts.convSessionId !== undefined)
|
|
144
|
+
body.convSessionId = opts.convSessionId;
|
|
145
|
+
if (opts.presentation !== undefined)
|
|
146
|
+
body.presentation = opts.presentation;
|
|
147
|
+
const data = await this.request("POST", "/agent/interactions", { body });
|
|
148
|
+
return parseInteraction(data);
|
|
149
|
+
}
|
|
150
|
+
async getInteraction(interactionId) {
|
|
151
|
+
const data = await this.request("GET", `/agent/interactions/${interactionId}`);
|
|
152
|
+
return this.decryptInteractionFields(parseInteraction(data), data);
|
|
153
|
+
}
|
|
154
|
+
/** Try to decrypt encrypted interaction fields using content wrappings. */
|
|
155
|
+
async decryptInteractionFields(interaction, raw) {
|
|
156
|
+
const wrappings = raw.contentWrappings;
|
|
157
|
+
if (!wrappings?.length)
|
|
158
|
+
return interaction;
|
|
159
|
+
const tryDecrypt = async (ciphertext) => {
|
|
160
|
+
if (!ciphertext)
|
|
161
|
+
return undefined;
|
|
162
|
+
const plain = await this.crypto.decryptWrappedContent(ciphertext, wrappings);
|
|
163
|
+
return plain ?? ciphertext;
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
...interaction,
|
|
167
|
+
title: (await tryDecrypt(interaction.title)) ?? "",
|
|
168
|
+
description: await tryDecrypt(interaction.description),
|
|
169
|
+
options: await tryDecrypt(interaction.options),
|
|
170
|
+
context: await tryDecrypt(interaction.context),
|
|
171
|
+
formSchema: await tryDecrypt(interaction.formSchema),
|
|
172
|
+
response: await tryDecrypt(interaction.response),
|
|
173
|
+
comment: await tryDecrypt(interaction.comment),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async cancelInteraction(interactionId) {
|
|
177
|
+
const data = await this.request("POST", `/agent/interactions/${interactionId}/cancel`);
|
|
178
|
+
return parseInteraction(data);
|
|
179
|
+
}
|
|
180
|
+
/** Update description/context on an interaction. Accepts plaintext — auto-encrypts. */
|
|
181
|
+
async updateInteractionContext(interactionId, opts) {
|
|
182
|
+
const { encrypted, contentWrappings } = await this.encryptFields({
|
|
183
|
+
encryptedDescription: opts.description,
|
|
184
|
+
encryptedContext: opts.context,
|
|
185
|
+
});
|
|
186
|
+
return await this.request("PATCH", `/agent/interactions/${interactionId}/context`, { body: { contentWrappings, ...encrypted } });
|
|
187
|
+
}
|
|
188
|
+
async acknowledgeInteraction(interactionId) {
|
|
189
|
+
const data = await this.request("POST", `/agent/interactions/${interactionId}/acknowledge`);
|
|
190
|
+
return parseInteraction(data);
|
|
191
|
+
}
|
|
192
|
+
/** AG-018/019: Update priority, deadline, or context metadata on a pending interaction. */
|
|
193
|
+
async updateInteractionMetadata(interactionId, opts) {
|
|
194
|
+
return await this.request("PATCH", `/agent/interactions/${interactionId}/metadata`, { body: opts });
|
|
195
|
+
}
|
|
196
|
+
/** Get the transition log for an interaction. */
|
|
197
|
+
async getInteractionTransitions(interactionId) {
|
|
198
|
+
return await this.request("GET", `/agent/interactions/${interactionId}/transitions`);
|
|
199
|
+
}
|
|
200
|
+
/** HU-073 (agent side): Read pending task control signal (pause/resume/cancel). */
|
|
201
|
+
async getTaskControl(interactionId) {
|
|
202
|
+
return await this.request("GET", `/agent/interactions/${interactionId}/task-control`);
|
|
203
|
+
}
|
|
204
|
+
/** HU-073 (agent side): Acknowledge task control signal, clearing it. */
|
|
205
|
+
async ackTaskControl(interactionId) {
|
|
206
|
+
return await this.request("POST", `/agent/interactions/${interactionId}/task-control/ack`);
|
|
207
|
+
}
|
|
208
|
+
/** Single long-poll for a response (server-side timeout, max ~30s). */
|
|
209
|
+
async awaitResponse(interactionId, timeout = 30) {
|
|
210
|
+
const data = await this.request("GET", `/agent/interactions/${interactionId}/await`, { params: { timeout: String(timeout) }, timeout: (timeout + 5) * 1000 });
|
|
211
|
+
return parseInteraction(data);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Wait for a human to respond to an interaction, polling repeatedly.
|
|
215
|
+
* Returns the interaction once it has a response (status is responded/completed),
|
|
216
|
+
* or throws after the timeout expires.
|
|
217
|
+
*
|
|
218
|
+
* @param interactionId - The interaction to wait for
|
|
219
|
+
* @param timeoutSeconds - Maximum wait time in seconds (default 300, max 3600)
|
|
220
|
+
*/
|
|
221
|
+
async waitForResponse(interactionId, timeoutSeconds = 300) {
|
|
222
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
223
|
+
const pollInterval = Math.min(30, timeoutSeconds); // server long-poll max 30s
|
|
224
|
+
while (Date.now() < deadline) {
|
|
225
|
+
const remaining = Math.ceil((deadline - Date.now()) / 1000);
|
|
226
|
+
if (remaining <= 0)
|
|
227
|
+
break;
|
|
228
|
+
const pollTimeout = Math.min(pollInterval, remaining);
|
|
229
|
+
const interaction = await this.awaitResponse(interactionId, pollTimeout);
|
|
230
|
+
// Check if the human has responded
|
|
231
|
+
if (interaction.status === "responded" ||
|
|
232
|
+
interaction.status === "completed" ||
|
|
233
|
+
interaction.status === "cancelled" ||
|
|
234
|
+
interaction.status === "expired") {
|
|
235
|
+
return interaction;
|
|
236
|
+
}
|
|
237
|
+
// Still pending — loop and poll again
|
|
238
|
+
}
|
|
239
|
+
// Final check before timeout
|
|
240
|
+
const final = await this.getInteraction(interactionId);
|
|
241
|
+
if (final.status !== "created" && final.status !== "pending" && final.status !== "viewed") {
|
|
242
|
+
return final;
|
|
243
|
+
}
|
|
244
|
+
throw new HiloopError(408, `Timed out waiting for response after ${timeoutSeconds}s`);
|
|
245
|
+
}
|
|
246
|
+
async listInteractions(filters) {
|
|
247
|
+
const params = {};
|
|
248
|
+
if (filters?.status !== undefined)
|
|
249
|
+
params.status = filters.status;
|
|
250
|
+
if (filters?.type !== undefined)
|
|
251
|
+
params.type = filters.type;
|
|
252
|
+
params.page = String(filters?.page ?? 1);
|
|
253
|
+
params.pageSize = String(filters?.pageSize ?? 20);
|
|
254
|
+
const data = await this.request("GET", "/agent/interactions", { params });
|
|
255
|
+
return {
|
|
256
|
+
items: data.items.map((i) => parseInteraction(i)),
|
|
257
|
+
total: data.total,
|
|
258
|
+
page: data.page,
|
|
259
|
+
pageSize: data.pageSize,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// -- Messages ---------------------------------------------------------------
|
|
263
|
+
async sendMessage(interactionId, content) {
|
|
264
|
+
const { encrypted, contentWrappings } = await this.encryptFields({
|
|
265
|
+
encryptedContent: content,
|
|
266
|
+
});
|
|
267
|
+
const data = await this.request("POST", `/agent/interactions/${interactionId}/messages`, { body: { contentWrappings, ...encrypted } });
|
|
268
|
+
return parseMessage(data);
|
|
269
|
+
}
|
|
270
|
+
/** Push content blocks to an interaction. */
|
|
271
|
+
async pushContentBlocks(interactionId, blocks) {
|
|
272
|
+
const encryptedBlocks = await Promise.all(blocks.map(async (block) => {
|
|
273
|
+
const plaintext = JSON.stringify(block.data);
|
|
274
|
+
const wrapped = await this.crypto.encryptWithWrapping(plaintext);
|
|
275
|
+
return {
|
|
276
|
+
blockType: block.blockType,
|
|
277
|
+
encryptedData: wrapped.ciphertext,
|
|
278
|
+
contentWrappings: wrapped.wrappings,
|
|
279
|
+
position: block.position,
|
|
280
|
+
};
|
|
281
|
+
}));
|
|
282
|
+
return await this.request("POST", `/agent/interactions/${interactionId}/blocks`, { body: { blocks: encryptedBlocks } });
|
|
283
|
+
}
|
|
284
|
+
/** List content blocks for an interaction. */
|
|
285
|
+
async listContentBlocks(interactionId) {
|
|
286
|
+
return await this.request("GET", `/agent/interactions/${interactionId}/blocks`);
|
|
287
|
+
}
|
|
288
|
+
/** Update a content block. */
|
|
289
|
+
async updateContentBlock(interactionId, blockId, data) {
|
|
290
|
+
return await this.request("PUT", `/agent/interactions/${interactionId}/blocks/${blockId}`, { body: { data } });
|
|
291
|
+
}
|
|
292
|
+
/** Delete a content block. */
|
|
293
|
+
async deleteContentBlock(interactionId, blockId) {
|
|
294
|
+
return await this.request("DELETE", `/agent/interactions/${interactionId}/blocks/${blockId}`);
|
|
295
|
+
}
|
|
296
|
+
/** Upload a file attachment to an interaction (base64-encoded). */
|
|
297
|
+
async uploadAttachment(interactionId, opts) {
|
|
298
|
+
return await this.request("POST", `/agent/interactions/${interactionId}/attachments`, { body: opts });
|
|
299
|
+
}
|
|
300
|
+
/** List attachments for an interaction, optionally filtered by messageId. */
|
|
301
|
+
async listAttachments(interactionId, messageId) {
|
|
302
|
+
const qs = messageId ? `?messageId=${encodeURIComponent(messageId)}` : "";
|
|
303
|
+
return await this.request("GET", `/agent/interactions/${interactionId}/attachments${qs}`);
|
|
304
|
+
}
|
|
305
|
+
/** Download raw binary content of a specific attachment. */
|
|
306
|
+
async getAttachment(interactionId, fileId) {
|
|
307
|
+
return await this.requestBinary(`/agent/interactions/${interactionId}/attachments/${fileId}`);
|
|
308
|
+
}
|
|
309
|
+
/** List all reactions for messages in an interaction. */
|
|
310
|
+
async listInteractionReactions(interactionId) {
|
|
311
|
+
return await this.request("GET", `/agent/interactions/${interactionId}/reactions`);
|
|
312
|
+
}
|
|
313
|
+
/** Add a reaction emoji to a message in an interaction. */
|
|
314
|
+
async addInteractionReaction(interactionId, messageId, emoji) {
|
|
315
|
+
return await this.request("POST", `/agent/interactions/${interactionId}/messages/${messageId}/reactions`, { body: { emoji } });
|
|
316
|
+
}
|
|
317
|
+
/** Remove a reaction emoji from a message in an interaction. */
|
|
318
|
+
async removeInteractionReaction(interactionId, messageId, emoji) {
|
|
319
|
+
return await this.request("DELETE", `/agent/interactions/${interactionId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
320
|
+
}
|
|
321
|
+
/** Mark all messages in an interaction as read by this agent. */
|
|
322
|
+
async markInteractionRead(interactionId) {
|
|
323
|
+
return await this.request("POST", `/agent/interactions/${interactionId}/messages/read`);
|
|
324
|
+
}
|
|
325
|
+
/** Send a typing indicator for an interaction. */
|
|
326
|
+
async sendInteractionTyping(interactionId, isTyping) {
|
|
327
|
+
return await this.request("POST", `/agent/interactions/${interactionId}/typing`, { body: { isTyping } });
|
|
328
|
+
}
|
|
329
|
+
async listMessages(interactionId, page = 1, pageSize = 50) {
|
|
330
|
+
const params = { page: String(page), pageSize: String(pageSize) };
|
|
331
|
+
const data = await this.request("GET", `/agent/interactions/${interactionId}/messages`, { params });
|
|
332
|
+
const rawItems = data.messages ?? data.items ?? [];
|
|
333
|
+
const items = rawItems.map((m) => parseMessage(m));
|
|
334
|
+
// Try to decrypt messages using content wrappings
|
|
335
|
+
await Promise.all(items.map(async (msg, i) => {
|
|
336
|
+
const raw = rawItems[i];
|
|
337
|
+
const wrappings = raw.contentWrappings;
|
|
338
|
+
if (wrappings?.length && msg.content) {
|
|
339
|
+
const plain = await this.crypto.decryptWrappedContent(msg.content, wrappings);
|
|
340
|
+
if (plain !== null)
|
|
341
|
+
msg.content = plain;
|
|
342
|
+
}
|
|
343
|
+
}));
|
|
344
|
+
return { items, total: data.total, page: data.page, pageSize: data.pageSize };
|
|
345
|
+
}
|
|
346
|
+
/** AD-060: Push agent telemetry activity status + plan to the platform. */
|
|
347
|
+
async pushTelemetry(opts) {
|
|
348
|
+
return await this.request("POST", "/agent/activity", { body: opts });
|
|
349
|
+
}
|
|
350
|
+
/** AG-041: List webhook delivery history for the agent. */
|
|
351
|
+
async listWebhookDeliveries(limit = 50) {
|
|
352
|
+
return await this.request("GET", `/agent/webhooks/deliveries?limit=${limit}`);
|
|
353
|
+
}
|
|
354
|
+
/** AG-041: Get webhook delivery statistics for the agent. */
|
|
355
|
+
async getWebhookDeliveryStats() {
|
|
356
|
+
return await this.request("GET", "/agent/webhooks/deliveries/stats");
|
|
357
|
+
}
|
|
358
|
+
/** AG-041: Manually retry a failed webhook delivery. */
|
|
359
|
+
async retryWebhookDelivery(deliveryId) {
|
|
360
|
+
return await this.request("POST", `/agent/webhooks/deliveries/${deliveryId}/retry`);
|
|
361
|
+
}
|
|
362
|
+
/** BL-042: Check remaining plan quota for this space. */
|
|
363
|
+
async getQuota() {
|
|
364
|
+
return await this.request("GET", "/agent/quota");
|
|
365
|
+
}
|
|
366
|
+
/** AD-060: Log an inter-agent communication event for monitoring. */
|
|
367
|
+
async logComm(opts) {
|
|
368
|
+
return await this.request("POST", "/agent/comms", { body: opts });
|
|
369
|
+
}
|
|
370
|
+
/** Get space encryption info (space public key). */
|
|
371
|
+
async getEncryptionInfo() {
|
|
372
|
+
return await this.request("GET", "/agent/encryption-info");
|
|
373
|
+
}
|
|
374
|
+
/** Get this agent's currently registered public key. */
|
|
375
|
+
async getAgentPublicKey() {
|
|
376
|
+
return await this.request("GET", "/agent/keys/me");
|
|
377
|
+
}
|
|
378
|
+
/** Register (or confirm) this agent's public key (idempotent). */
|
|
379
|
+
async putAgentPublicKey(publicKey, fingerprint) {
|
|
380
|
+
return await this.request("PUT", "/agent/keys/me", { body: { publicKey, fingerprint } });
|
|
381
|
+
}
|
|
382
|
+
/** List space teams for this agent's space. */
|
|
383
|
+
async listSpaceTeams() {
|
|
384
|
+
return await this.request("GET", "/agent/space/teams");
|
|
385
|
+
}
|
|
386
|
+
/** Get availability status for a specific space user. */
|
|
387
|
+
async getUserAvailability(userId) {
|
|
388
|
+
return await this.request("GET", `/agent/space/users/${userId}/availability`);
|
|
389
|
+
}
|
|
390
|
+
/** List space users visible to this agent. */
|
|
391
|
+
async listSpaceUsers(opts) {
|
|
392
|
+
const params = new URLSearchParams();
|
|
393
|
+
if (opts?.limit !== undefined)
|
|
394
|
+
params.set("limit", String(opts.limit));
|
|
395
|
+
if (opts?.offset !== undefined)
|
|
396
|
+
params.set("offset", String(opts.offset));
|
|
397
|
+
const qs = params.toString();
|
|
398
|
+
return await this.request("GET", `/agent/space/users${qs ? `?${qs}` : ""}`);
|
|
399
|
+
}
|
|
400
|
+
// -- Conversation Sessions -------------------------------------------------
|
|
401
|
+
async createConvSession(opts) {
|
|
402
|
+
const body = {
|
|
403
|
+
sessionType: opts.sessionType ?? "direct",
|
|
404
|
+
isPublic: opts.isPublic ?? false,
|
|
405
|
+
};
|
|
406
|
+
if (opts.maxGuests !== undefined)
|
|
407
|
+
body.maxGuests = opts.maxGuests;
|
|
408
|
+
if (opts.participantIds !== undefined)
|
|
409
|
+
body.participantIds = opts.participantIds;
|
|
410
|
+
if (opts.publicMode !== undefined)
|
|
411
|
+
body.publicMode = opts.publicMode;
|
|
412
|
+
// If a plaintext title is provided, encrypt it with content wrapping
|
|
413
|
+
if (opts.title !== undefined) {
|
|
414
|
+
const result = await this.crypto.encryptWithWrapping(opts.title, opts.spaceKeyId);
|
|
415
|
+
body.encryptedTitle = result.ciphertext;
|
|
416
|
+
if (result.wrappings.length > 0) {
|
|
417
|
+
body.titleContentWrappings = result.wrappings;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else if (opts.encryptedTitle !== undefined) {
|
|
421
|
+
body.encryptedTitle = opts.encryptedTitle;
|
|
422
|
+
if (opts.titleContentWrappings !== undefined) {
|
|
423
|
+
body.titleContentWrappings = opts.titleContentWrappings;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const data = await this.request("POST", "/agent/sessions", { body });
|
|
427
|
+
return parseConvSession(data);
|
|
428
|
+
}
|
|
429
|
+
async getConvSession(sessionId) {
|
|
430
|
+
const data = await this.request("GET", `/agent/sessions/${sessionId}`);
|
|
431
|
+
return parseConvSession(data);
|
|
432
|
+
}
|
|
433
|
+
async listConvSessions(opts) {
|
|
434
|
+
const params = {};
|
|
435
|
+
if (opts?.status !== undefined)
|
|
436
|
+
params.status = opts.status;
|
|
437
|
+
if (opts?.isPublic !== undefined)
|
|
438
|
+
params.isPublic = String(opts.isPublic);
|
|
439
|
+
if (opts?.limit !== undefined)
|
|
440
|
+
params.limit = String(opts.limit);
|
|
441
|
+
if (opts?.offset !== undefined)
|
|
442
|
+
params.offset = String(opts.offset);
|
|
443
|
+
if (opts?.page !== undefined)
|
|
444
|
+
params.page = String(opts.page);
|
|
445
|
+
const data = await this.request("GET", "/agent/sessions", { params });
|
|
446
|
+
return data.sessions.map((s) => parseConvSession(s));
|
|
447
|
+
}
|
|
448
|
+
async closeConvSession(sessionId) {
|
|
449
|
+
const data = await this.request("POST", `/agent/sessions/${sessionId}/close`);
|
|
450
|
+
return parseConvSession(data);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Send a message to a conversation session.
|
|
454
|
+
* The content must be pre-encrypted using encryptAes() with the shared session key.
|
|
455
|
+
*/
|
|
456
|
+
/**
|
|
457
|
+
* Send a message in a conv session. Accepts plaintext — encrypts automatically
|
|
458
|
+
* using the session message key (`{agentPub}:{AES-GCM ciphertext}` format).
|
|
459
|
+
*/
|
|
460
|
+
async sendConvSessionMessage(sessionId, content) {
|
|
461
|
+
const encryptedContent = await this.crypto.encryptSessionMessage(content);
|
|
462
|
+
const data = await this.request("POST", `/agent/sessions/${sessionId}/messages`, { body: { encryptedContent } });
|
|
463
|
+
return parseConvSessionMessage(data);
|
|
464
|
+
}
|
|
465
|
+
/** Send a typing indicator to a conv session (HU-034). */
|
|
466
|
+
async sendConvSessionTyping(sessionId, typing) {
|
|
467
|
+
await this.request("POST", `/agent/sessions/${sessionId}/typing`, { body: { typing } });
|
|
468
|
+
}
|
|
469
|
+
/** Edit a previously sent conv session message. */
|
|
470
|
+
async editConvSessionMessage(sessionId, msgId, encryptedContent) {
|
|
471
|
+
const data = await this.request("PATCH", `/agent/sessions/${sessionId}/messages/${msgId}`, { body: { encryptedContent } });
|
|
472
|
+
return parseConvSessionMessage(data);
|
|
473
|
+
}
|
|
474
|
+
/** Soft-delete a conv session message. */
|
|
475
|
+
async deleteConvSessionMessage(sessionId, msgId) {
|
|
476
|
+
return await this.request("DELETE", `/agent/sessions/${sessionId}/messages/${msgId}`);
|
|
477
|
+
}
|
|
478
|
+
async listConvSessionTimeline(sessionId, opts) {
|
|
479
|
+
const params = {};
|
|
480
|
+
if (opts?.limit !== undefined)
|
|
481
|
+
params.limit = String(opts.limit);
|
|
482
|
+
if (opts?.before !== undefined)
|
|
483
|
+
params.before = opts.before;
|
|
484
|
+
const result = await this.request("GET", `/agent/sessions/${sessionId}/timeline`, { params });
|
|
485
|
+
// Decrypt timeline items (session messages + interaction fields)
|
|
486
|
+
if (Array.isArray(result.items)) {
|
|
487
|
+
await Promise.all(result.items.map(async (item) => {
|
|
488
|
+
if (item.kind === "message" && typeof item.encryptedContent === "string") {
|
|
489
|
+
const plain = await this.crypto.decryptSessionMessage(item.encryptedContent);
|
|
490
|
+
if (plain !== null)
|
|
491
|
+
item.content = plain;
|
|
492
|
+
else
|
|
493
|
+
item.content = "[Encrypted]";
|
|
494
|
+
}
|
|
495
|
+
if (item.kind === "interaction") {
|
|
496
|
+
const wrappings = item.contentWrappings;
|
|
497
|
+
if (wrappings?.length) {
|
|
498
|
+
for (const field of ["encryptedTitle", "encryptedDescription", "encryptedOptions", "encryptedFormSchema", "encryptedContext"]) {
|
|
499
|
+
if (typeof item[field] === "string") {
|
|
500
|
+
const plain = await this.crypto.decryptWrappedContent(item[field], wrappings);
|
|
501
|
+
if (plain !== null) {
|
|
502
|
+
const readableKey = field.replace(/^encrypted/, "").replace(/^./, (c) => c.toLowerCase());
|
|
503
|
+
item[readableKey] = plain;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
async addConvSessionParticipant(sessionId, userId, role = "member") {
|
|
514
|
+
const data = await this.request("POST", `/agent/sessions/${sessionId}/participants`, { body: { userId, role } });
|
|
515
|
+
return data.participant;
|
|
516
|
+
}
|
|
517
|
+
async removeConvSessionParticipant(sessionId, userId) {
|
|
518
|
+
await this.request("DELETE", `/agent/sessions/${sessionId}/participants/${userId}`);
|
|
519
|
+
}
|
|
520
|
+
/** Update a participant's role in a conversation session. */
|
|
521
|
+
async updateConvSessionParticipantRole(sessionId, userId, role) {
|
|
522
|
+
return await this.request("PATCH", `/agent/sessions/${sessionId}/participants/${userId}`, { body: { role } });
|
|
523
|
+
}
|
|
524
|
+
async listConvSessionParticipants(sessionId) {
|
|
525
|
+
const data = await this.request("GET", `/agent/sessions/${sessionId}/participants`);
|
|
526
|
+
return data.participants;
|
|
527
|
+
}
|
|
528
|
+
async createGuestToken(sessionId, opts) {
|
|
529
|
+
const data = await this.request("POST", `/agent/sessions/${sessionId}/guest-tokens`, { body: opts ?? {} });
|
|
530
|
+
return parseGuestToken(data);
|
|
531
|
+
}
|
|
532
|
+
async listGuestTokens(sessionId) {
|
|
533
|
+
const data = await this.request("GET", `/agent/sessions/${sessionId}/guest-tokens`);
|
|
534
|
+
return data.tokens.map((t) => parseGuestToken(t));
|
|
535
|
+
}
|
|
536
|
+
async revokeGuestToken(sessionId, tokenId) {
|
|
537
|
+
await this.request("DELETE", `/human/sessions/${sessionId}/guest-tokens/${tokenId}`);
|
|
538
|
+
}
|
|
539
|
+
/** AG-060: Create multiple interactions in a single API call. */
|
|
540
|
+
async createInteractionBatch(interactions) {
|
|
541
|
+
const items = await Promise.all(interactions.map(async (opts) => {
|
|
542
|
+
const { encrypted, contentWrappings } = await this.encryptFields({
|
|
543
|
+
encryptedTitle: opts.title,
|
|
544
|
+
encryptedDescription: opts.description,
|
|
545
|
+
encryptedOptions: opts.options,
|
|
546
|
+
encryptedContext: opts.context,
|
|
547
|
+
});
|
|
548
|
+
const body = {
|
|
549
|
+
type: opts.type,
|
|
550
|
+
priority: opts.priority ?? "normal",
|
|
551
|
+
contentWrappings,
|
|
552
|
+
...encrypted,
|
|
553
|
+
};
|
|
554
|
+
if (opts.routeToUser !== undefined)
|
|
555
|
+
body.routeToUser = opts.routeToUser;
|
|
556
|
+
if (opts.routeToTeam !== undefined)
|
|
557
|
+
body.routeToTeam = opts.routeToTeam;
|
|
558
|
+
if (opts.deadlineMinutes !== undefined)
|
|
559
|
+
body.deadlineMinutes = opts.deadlineMinutes;
|
|
560
|
+
if (opts.convSessionId !== undefined)
|
|
561
|
+
body.convSessionId = opts.convSessionId;
|
|
562
|
+
return body;
|
|
563
|
+
}));
|
|
564
|
+
return this.request("POST", "/agent/interactions/batch", { body: { interactions: items } });
|
|
565
|
+
}
|
|
566
|
+
/** Archive a conversation session. */
|
|
567
|
+
async archiveConvSession(sessionId) {
|
|
568
|
+
const data = await this.request("POST", `/agent/sessions/${sessionId}/archive`);
|
|
569
|
+
return parseConvSession(data);
|
|
570
|
+
}
|
|
571
|
+
/** Create a group interaction involving multiple participants (AG-061). */
|
|
572
|
+
async createGroupInteraction(opts, participants) {
|
|
573
|
+
const { encrypted, contentWrappings } = await this.encryptFields({
|
|
574
|
+
encryptedTitle: opts.title,
|
|
575
|
+
encryptedDescription: opts.description,
|
|
576
|
+
encryptedContext: opts.context,
|
|
577
|
+
});
|
|
578
|
+
const body = {
|
|
579
|
+
type: opts.type,
|
|
580
|
+
priority: opts.priority ?? "normal",
|
|
581
|
+
participants,
|
|
582
|
+
contentWrappings,
|
|
583
|
+
...encrypted,
|
|
584
|
+
};
|
|
585
|
+
if (opts.routeToUser !== undefined)
|
|
586
|
+
body.routeToUser = opts.routeToUser;
|
|
587
|
+
if (opts.routeToTeam !== undefined)
|
|
588
|
+
body.routeToTeam = opts.routeToTeam;
|
|
589
|
+
if (opts.deadlineMinutes !== undefined)
|
|
590
|
+
body.deadlineMinutes = opts.deadlineMinutes;
|
|
591
|
+
if (opts.callbackUrl !== undefined)
|
|
592
|
+
body.callbackUrl = opts.callbackUrl;
|
|
593
|
+
return this.request("POST", "/agent/interactions/group", { body });
|
|
594
|
+
}
|
|
595
|
+
/** Add a reference from one interaction to another (AG-062). */
|
|
596
|
+
async addInteractionReference(interactionId, targetInteractionId, opts) {
|
|
597
|
+
return this.request("POST", `/agent/interactions/${interactionId}/references`, {
|
|
598
|
+
body: { targetInteractionId, referenceType: opts?.referenceType, note: opts?.note },
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
/** List all references for an interaction (AG-062). */
|
|
602
|
+
async listInteractionReferences(interactionId) {
|
|
603
|
+
return this.request("GET", `/agent/interactions/${interactionId}/references`);
|
|
604
|
+
}
|
|
605
|
+
/** Remove an interaction reference by its ID (AG-062). */
|
|
606
|
+
async removeInteractionReference(interactionId, refId) {
|
|
607
|
+
return this.request("DELETE", `/agent/interactions/${interactionId}/references/${refId}`);
|
|
608
|
+
}
|
|
609
|
+
// -- Public agent routes (no auth required) ---------------------------------
|
|
610
|
+
/** Get public metadata for a public agent by slug. */
|
|
611
|
+
async getPublicAgentInfo(slug) {
|
|
612
|
+
return this.request("GET", `/public/${slug}/info`);
|
|
613
|
+
}
|
|
614
|
+
/** Create an interaction as a public (anonymous) user on a public agent. */
|
|
615
|
+
async createPublicInteraction(slug, opts) {
|
|
616
|
+
return this.request("POST", `/public/${slug}/interactions`, { body: opts });
|
|
617
|
+
}
|
|
618
|
+
/** Get the status of a public interaction by slug + ID. */
|
|
619
|
+
async getPublicInteraction(slug, interactionId) {
|
|
620
|
+
return this.request("GET", `/public/${slug}/interactions/${interactionId}`);
|
|
621
|
+
}
|
|
622
|
+
/** Long-poll for a response on a public interaction (returns when responded/completed/cancelled/expired). */
|
|
623
|
+
async awaitPublicInteraction(slug, interactionId, timeoutMs = 30000) {
|
|
624
|
+
return this.request("GET", `/public/${slug}/interactions/${interactionId}/await`, {
|
|
625
|
+
params: { timeout: String(timeoutMs) },
|
|
626
|
+
timeout: timeoutMs + 5000,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
/** Send a message to a public interaction as an anonymous user. */
|
|
630
|
+
async sendPublicMessage(slug, interactionId, encryptedContent) {
|
|
631
|
+
return this.request("POST", `/public/${slug}/interactions/${interactionId}/messages`, { body: { encryptedContent } });
|
|
632
|
+
}
|
|
633
|
+
/** List interactions created on a public agent (paginated). */
|
|
634
|
+
async listPublicInteractions(slug, opts) {
|
|
635
|
+
const params = {};
|
|
636
|
+
if (opts?.page !== undefined)
|
|
637
|
+
params.page = String(opts.page);
|
|
638
|
+
if (opts?.pageSize !== undefined)
|
|
639
|
+
params.pageSize = String(opts.pageSize);
|
|
640
|
+
return this.request("GET", `/public/${slug}/interactions`, Object.keys(params).length > 0 ? { params } : undefined);
|
|
641
|
+
}
|
|
642
|
+
/** Cancel a public interaction (anonymous user side). */
|
|
643
|
+
async cancelPublicInteraction(slug, interactionId) {
|
|
644
|
+
return this.request("DELETE", `/public/${slug}/interactions/${interactionId}`);
|
|
645
|
+
}
|
|
646
|
+
/** Get the transition log for a public interaction. */
|
|
647
|
+
async getPublicInteractionTransitions(slug, interactionId) {
|
|
648
|
+
return this.request("GET", `/public/${slug}/interactions/${interactionId}/transitions`);
|
|
649
|
+
}
|
|
650
|
+
/** Look up a public session by its public token (no auth required). */
|
|
651
|
+
async getPublicSession(publicToken) {
|
|
652
|
+
return this.request("GET", `/public/sessions/${publicToken}`);
|
|
653
|
+
}
|
|
654
|
+
/** Join a public session as a guest using its public token. */
|
|
655
|
+
async joinPublicSession(publicToken, displayName) {
|
|
656
|
+
return this.request("POST", `/public/sessions/${publicToken}/join`, { body: { displayName } });
|
|
657
|
+
}
|
|
658
|
+
// -- Human Interaction Detail -----------------------------------------------
|
|
659
|
+
/** Get a human-facing interaction detail view. */
|
|
660
|
+
async getHumanInteraction(interactionId) {
|
|
661
|
+
return this.request("GET", `/human/interactions/${interactionId}`);
|
|
662
|
+
}
|
|
663
|
+
/** Mark an interaction as viewed by the authenticated user. */
|
|
664
|
+
async viewInteraction(interactionId) {
|
|
665
|
+
return this.request("POST", `/human/interactions/${interactionId}/view`);
|
|
666
|
+
}
|
|
667
|
+
/** Submit a response to an interaction. */
|
|
668
|
+
async respondToHumanInteraction(interactionId, opts) {
|
|
669
|
+
return this.request("POST", `/human/interactions/${interactionId}/respond`, { body: opts });
|
|
670
|
+
}
|
|
671
|
+
/** Snooze an interaction until a given ISO datetime. */
|
|
672
|
+
async snoozeInteraction(interactionId, snoozedUntil) {
|
|
673
|
+
return this.request("POST", `/human/interactions/${interactionId}/snooze`, { body: { snoozedUntil } });
|
|
674
|
+
}
|
|
675
|
+
/** Toggle the pinned state of an interaction. */
|
|
676
|
+
async pinInteraction(interactionId, pinned) {
|
|
677
|
+
return this.request("POST", `/human/interactions/${interactionId}/pin`, { body: { pinned } });
|
|
678
|
+
}
|
|
679
|
+
// -- Human Inbox / Feed ----------------------------------------------------
|
|
680
|
+
// getInbox removed -- use getFeed with view param
|
|
681
|
+
/** Get the human user's activity feed (all interactions). */
|
|
682
|
+
async getFeed(opts) {
|
|
683
|
+
const params = new URLSearchParams();
|
|
684
|
+
if (opts?.page)
|
|
685
|
+
params.set("page", String(opts.page));
|
|
686
|
+
if (opts?.pageSize)
|
|
687
|
+
params.set("pageSize", String(opts.pageSize));
|
|
688
|
+
if (opts?.view)
|
|
689
|
+
params.set("view", opts.view);
|
|
690
|
+
if (opts?.q)
|
|
691
|
+
params.set("q", opts.q);
|
|
692
|
+
if (opts?.type)
|
|
693
|
+
params.set("type", opts.type);
|
|
694
|
+
if (opts?.priority)
|
|
695
|
+
params.set("priority", opts.priority);
|
|
696
|
+
if (opts?.pinned !== undefined)
|
|
697
|
+
params.set("pinned", String(opts.pinned));
|
|
698
|
+
if (opts?.agentId)
|
|
699
|
+
params.set("agentId", opts.agentId);
|
|
700
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
701
|
+
return this.request("GET", `/human/feed${qs}`);
|
|
702
|
+
}
|
|
703
|
+
// searchInteractions removed -- use getFeed with q param
|
|
704
|
+
// getHistory removed -- use getFeed with view param
|
|
705
|
+
/** Get badge counts (e.g. open interactions) for the authenticated user. */
|
|
706
|
+
async getBadges() {
|
|
707
|
+
return this.request("GET", "/human/badges");
|
|
708
|
+
}
|
|
709
|
+
// -- Account ----------------------------------------------------------------
|
|
710
|
+
/** Get the authenticated user's current availability status. */
|
|
711
|
+
async getMyAvailability() {
|
|
712
|
+
return this.request("GET", "/human/availability");
|
|
713
|
+
}
|
|
714
|
+
/** Update the authenticated user's availability status. */
|
|
715
|
+
async updateMyAvailability(status) {
|
|
716
|
+
return this.request("PATCH", "/human/availability", { body: { status } });
|
|
717
|
+
}
|
|
718
|
+
/** List delegation rules for the authenticated user. */
|
|
719
|
+
async listDelegations() {
|
|
720
|
+
return this.request("GET", "/human/delegations");
|
|
721
|
+
}
|
|
722
|
+
/** Create a delegation rule for the authenticated user. */
|
|
723
|
+
async createDelegation(opts) {
|
|
724
|
+
return this.request("POST", "/human/delegations", { body: opts });
|
|
725
|
+
}
|
|
726
|
+
/** Remove a delegation rule by delegateId. */
|
|
727
|
+
async deleteDelegation(delegateId) {
|
|
728
|
+
return this.request("DELETE", `/human/delegations/${delegateId}`);
|
|
729
|
+
}
|
|
730
|
+
// -- Notifications ----------------------------------------------------------
|
|
731
|
+
/** List sent notifications for the authenticated user. */
|
|
732
|
+
async listNotifications() {
|
|
733
|
+
return this.request("GET", "/human/notifications");
|
|
734
|
+
}
|
|
735
|
+
/** Get notification preferences for the authenticated user. */
|
|
736
|
+
async getNotificationPreferences() {
|
|
737
|
+
return this.request("GET", "/human/notifications/preferences");
|
|
738
|
+
}
|
|
739
|
+
/** Update notification preference for a specific channel. */
|
|
740
|
+
async updateNotificationPreference(channel, opts) {
|
|
741
|
+
return this.request("PUT", `/human/notifications/preferences/${channel}`, { body: opts });
|
|
742
|
+
}
|
|
743
|
+
/** Register a push token for the authenticated user's device. */
|
|
744
|
+
async registerPushToken(opts) {
|
|
745
|
+
return this.request("POST", "/human/notifications/push-tokens", { body: opts });
|
|
746
|
+
}
|
|
747
|
+
/** Remove the push token for the authenticated user's device. */
|
|
748
|
+
async deletePushToken(opts) {
|
|
749
|
+
return this.request("DELETE", "/human/notifications/push-tokens", { body: opts });
|
|
750
|
+
}
|
|
751
|
+
/** Save (upsert) a response draft for an interaction. */
|
|
752
|
+
async saveDraft(interactionId, encryptedDraft) {
|
|
753
|
+
return this.request("PUT", `/human/interactions/${interactionId}/draft`, { body: { encryptedDraft } });
|
|
754
|
+
}
|
|
755
|
+
/** Get the saved response draft for an interaction. */
|
|
756
|
+
async getDraft(interactionId) {
|
|
757
|
+
return this.request("GET", `/human/interactions/${interactionId}/draft`);
|
|
758
|
+
}
|
|
759
|
+
/** Delete the saved response draft for an interaction. */
|
|
760
|
+
async deleteDraft(interactionId) {
|
|
761
|
+
return this.request("DELETE", `/human/interactions/${interactionId}/draft`);
|
|
762
|
+
}
|
|
763
|
+
/** Unsnooze an interaction before its scheduled snooze time. */
|
|
764
|
+
async unsnoozeInteraction(interactionId) {
|
|
765
|
+
return this.request("POST", `/human/interactions/${interactionId}/unsnooze`);
|
|
766
|
+
}
|
|
767
|
+
// -- Collaboration (milestone alerts + approval chain) -----------------------
|
|
768
|
+
/** Create a milestone alert for an interaction. */
|
|
769
|
+
async createMilestoneAlert(interactionId, opts) {
|
|
770
|
+
return this.request("POST", `/human/interactions/${interactionId}/milestone-alerts`, { body: opts });
|
|
771
|
+
}
|
|
772
|
+
/** List milestone alerts for an interaction. */
|
|
773
|
+
async listMilestoneAlerts(interactionId) {
|
|
774
|
+
return this.request("GET", `/human/interactions/${interactionId}/milestone-alerts`);
|
|
775
|
+
}
|
|
776
|
+
/** Delete a milestone alert. */
|
|
777
|
+
async deleteMilestoneAlert(interactionId, alertId) {
|
|
778
|
+
return this.request("DELETE", `/human/interactions/${interactionId}/milestone-alerts/${alertId}`);
|
|
779
|
+
}
|
|
780
|
+
/** Get the approval chain for an interaction. */
|
|
781
|
+
async getApprovalChain(interactionId) {
|
|
782
|
+
return this.request("GET", `/human/interactions/${interactionId}/approval-chain`);
|
|
783
|
+
}
|
|
784
|
+
/** Respond to an approval chain step. */
|
|
785
|
+
async respondToApprovalChain(interactionId, opts) {
|
|
786
|
+
return this.request("POST", `/human/interactions/${interactionId}/approval-chain/respond`, { body: opts });
|
|
787
|
+
}
|
|
788
|
+
// -- Phase 628: Comments + review feedback ------------------------------------
|
|
789
|
+
/** List comments on an interaction. */
|
|
790
|
+
async listInteractionComments(interactionId) {
|
|
791
|
+
return this.request("GET", `/human/interactions/${interactionId}/comments`);
|
|
792
|
+
}
|
|
793
|
+
/** Post a comment on an interaction. */
|
|
794
|
+
async createInteractionComment(interactionId, opts) {
|
|
795
|
+
return this.request("POST", `/human/interactions/${interactionId}/comments`, { body: opts });
|
|
796
|
+
}
|
|
797
|
+
/** Mark a comment as resolved. */
|
|
798
|
+
async resolveInteractionComment(interactionId, commentId) {
|
|
799
|
+
return this.request("PATCH", `/human/interactions/${interactionId}/comments/${commentId}/resolve`);
|
|
800
|
+
}
|
|
801
|
+
/** Delete a comment. */
|
|
802
|
+
async deleteInteractionComment(interactionId, commentId) {
|
|
803
|
+
return this.request("DELETE", `/human/interactions/${interactionId}/comments/${commentId}`);
|
|
804
|
+
}
|
|
805
|
+
/** Submit review feedback for an interaction. */
|
|
806
|
+
async submitReviewFeedback(interactionId, opts) {
|
|
807
|
+
return this.request("POST", `/human/interactions/${interactionId}/review-feedback`, { body: opts });
|
|
808
|
+
}
|
|
809
|
+
// -- Phase 629: Voting + tags --------------------------------------------------
|
|
810
|
+
/** Cast a vote on an interaction. */
|
|
811
|
+
async voteOnInteraction(interactionId, opts) {
|
|
812
|
+
return this.request("POST", `/human/interactions/${interactionId}/vote`, { body: opts });
|
|
813
|
+
}
|
|
814
|
+
/** Get aggregate vote results for an interaction. */
|
|
815
|
+
async getVoteResults(interactionId) {
|
|
816
|
+
return this.request("GET", `/human/interactions/${interactionId}/vote/results`);
|
|
817
|
+
}
|
|
818
|
+
/** Get the current user's vote on an interaction. */
|
|
819
|
+
async getMyVote(interactionId) {
|
|
820
|
+
return this.request("GET", `/human/interactions/${interactionId}/vote/mine`);
|
|
821
|
+
}
|
|
822
|
+
/** List tags on an interaction. */
|
|
823
|
+
async getInteractionTags(interactionId) {
|
|
824
|
+
return this.request("GET", `/human/interactions/${interactionId}/tags`);
|
|
825
|
+
}
|
|
826
|
+
/** Add a tag to an interaction. */
|
|
827
|
+
async addInteractionTag(interactionId, tag) {
|
|
828
|
+
return this.request("POST", `/human/interactions/${interactionId}/tags`, { body: { tag } });
|
|
829
|
+
}
|
|
830
|
+
// -- Phase 630: More tags + response templates ---------------------------------
|
|
831
|
+
/** Remove a tag from an interaction. */
|
|
832
|
+
async deleteInteractionTag(interactionId, tag) {
|
|
833
|
+
return this.request("DELETE", `/human/interactions/${interactionId}/tags/${encodeURIComponent(tag)}`);
|
|
834
|
+
}
|
|
835
|
+
/** List all space-wide tags. */
|
|
836
|
+
async listSpaceTags() {
|
|
837
|
+
return this.request("GET", "/human/tags");
|
|
838
|
+
}
|
|
839
|
+
/** List response templates for the current user. */
|
|
840
|
+
async listResponseTemplates() {
|
|
841
|
+
return this.request("GET", "/human/response-templates");
|
|
842
|
+
}
|
|
843
|
+
/** Create a response template. */
|
|
844
|
+
async createResponseTemplate(opts) {
|
|
845
|
+
return this.request("POST", "/human/response-templates", { body: opts });
|
|
846
|
+
}
|
|
847
|
+
/** Delete a response template. */
|
|
848
|
+
async deleteResponseTemplate(templateId) {
|
|
849
|
+
return this.request("DELETE", `/human/response-templates/${templateId}`);
|
|
850
|
+
}
|
|
851
|
+
// -- Phase 631: Review feedback + template use + message routes ----------------
|
|
852
|
+
/** Get review feedback for an interaction. */
|
|
853
|
+
async getReviewFeedback(interactionId) {
|
|
854
|
+
return this.request("GET", `/human/interactions/${interactionId}/review-feedback`);
|
|
855
|
+
}
|
|
856
|
+
/** Use a response template (increments usage count). */
|
|
857
|
+
async useResponseTemplate(templateId) {
|
|
858
|
+
return this.request("POST", `/human/response-templates/${templateId}/use`);
|
|
859
|
+
}
|
|
860
|
+
/** List messages for a human interaction. */
|
|
861
|
+
async listInteractionMessages(interactionId, opts) {
|
|
862
|
+
const params = {};
|
|
863
|
+
if (opts?.before !== undefined)
|
|
864
|
+
params.before = opts.before;
|
|
865
|
+
if (opts?.limit !== undefined)
|
|
866
|
+
params.limit = String(opts.limit);
|
|
867
|
+
return this.request("GET", `/human/interactions/${interactionId}/messages`, { params });
|
|
868
|
+
}
|
|
869
|
+
/** Send a message to a human interaction thread. */
|
|
870
|
+
async sendInteractionMessage(interactionId, opts) {
|
|
871
|
+
return this.request("POST", `/human/interactions/${interactionId}/messages`, { body: opts });
|
|
872
|
+
}
|
|
873
|
+
/** Mark messages as read for an interaction. */
|
|
874
|
+
async markMessagesRead(interactionId) {
|
|
875
|
+
return this.request("POST", `/human/interactions/${interactionId}/messages/read`);
|
|
876
|
+
}
|
|
877
|
+
// -- Phase 632: Reactions + agent activity + references ------------------------
|
|
878
|
+
/** Get aggregated reactions for all messages in an interaction. */
|
|
879
|
+
async getInteractionReactions(interactionId) {
|
|
880
|
+
return this.request("GET", `/human/interactions/${interactionId}/reactions`);
|
|
881
|
+
}
|
|
882
|
+
/** Add a reaction to a specific message. */
|
|
883
|
+
async addMessageReaction(interactionId, messageId, emoji) {
|
|
884
|
+
return this.request("POST", `/human/interactions/${interactionId}/messages/${messageId}/reactions`, { body: { emoji } });
|
|
885
|
+
}
|
|
886
|
+
/** Remove a reaction from a specific message. */
|
|
887
|
+
async removeMessageReaction(interactionId, messageId, emoji) {
|
|
888
|
+
return this.request("DELETE", `/human/interactions/${interactionId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
889
|
+
}
|
|
890
|
+
/** Get agent activity data for the space (for human dashboard). */
|
|
891
|
+
async getAgentActivity() {
|
|
892
|
+
return this.request("GET", "/human/agents/activity");
|
|
893
|
+
}
|
|
894
|
+
/** List interaction references for a human interaction. */
|
|
895
|
+
async listHumanInteractionReferences(interactionId) {
|
|
896
|
+
return this.request("GET", `/human/interactions/${interactionId}/references`);
|
|
897
|
+
}
|
|
898
|
+
// -- Phase 633: References + usage + feature gates + presence ------------------
|
|
899
|
+
/** Add a reference link between two interactions. */
|
|
900
|
+
async addHumanInteractionReference(interactionId, opts) {
|
|
901
|
+
return this.request("POST", `/human/interactions/${interactionId}/references`, { body: opts });
|
|
902
|
+
}
|
|
903
|
+
/** Get current usage and quota for the space. */
|
|
904
|
+
async getUsage() {
|
|
905
|
+
return this.request("GET", "/human/usage");
|
|
906
|
+
}
|
|
907
|
+
/** Get feature gate flags for the current user. */
|
|
908
|
+
async getFeatureGates() {
|
|
909
|
+
return this.request("GET", "/human/feature-gates");
|
|
910
|
+
}
|
|
911
|
+
/** Track that the current user is viewing an interaction. */
|
|
912
|
+
async trackInteractionViewing(interactionId) {
|
|
913
|
+
return this.request("POST", `/human/interactions/${interactionId}/viewing`);
|
|
914
|
+
}
|
|
915
|
+
/** Get who is currently viewing an interaction. */
|
|
916
|
+
async getInteractionViewers(interactionId) {
|
|
917
|
+
return this.request("GET", `/human/interactions/${interactionId}/viewers`);
|
|
918
|
+
}
|
|
919
|
+
// -- Phase 634: More account routes + inbox grouped + members ------------------
|
|
920
|
+
/** Stop tracking viewing presence for an interaction. */
|
|
921
|
+
async stopTrackingViewing(interactionId) {
|
|
922
|
+
return this.request("DELETE", `/human/interactions/${interactionId}/viewing`);
|
|
923
|
+
}
|
|
924
|
+
/** Get human-assigned tasks (interactions requiring action). */
|
|
925
|
+
async getHumanTasks(opts) {
|
|
926
|
+
const params = {};
|
|
927
|
+
if (opts?.page !== undefined)
|
|
928
|
+
params.page = String(opts.page);
|
|
929
|
+
if (opts?.pageSize !== undefined)
|
|
930
|
+
params.pageSize = String(opts.pageSize);
|
|
931
|
+
return this.request("GET", "/human/tasks", { params });
|
|
932
|
+
}
|
|
933
|
+
// getInboxGrouped removed -- use getFeed with view param
|
|
934
|
+
/** List space members (humans). */
|
|
935
|
+
async getHumanMembers(opts) {
|
|
936
|
+
const params = {};
|
|
937
|
+
if (opts?.page !== undefined)
|
|
938
|
+
params.page = String(opts.page);
|
|
939
|
+
if (opts?.pageSize !== undefined)
|
|
940
|
+
params.pageSize = String(opts.pageSize);
|
|
941
|
+
return this.request("GET", "/human/members", { params });
|
|
942
|
+
}
|
|
943
|
+
/** Get aggregated notification digest for the current user. */
|
|
944
|
+
async getNotificationDigest(opts) {
|
|
945
|
+
const params = {};
|
|
946
|
+
if (opts?.since !== undefined)
|
|
947
|
+
params.since = opts.since;
|
|
948
|
+
return this.request("GET", "/human/notifications/digest", { params });
|
|
949
|
+
}
|
|
950
|
+
// -- Channels ---------------------------------------------------------------
|
|
951
|
+
/** Create a new agent-to-agent channel. */
|
|
952
|
+
async createChannel(opts) {
|
|
953
|
+
const body = {};
|
|
954
|
+
if (opts.name !== undefined) {
|
|
955
|
+
const w = await this.crypto.encryptWithWrapping(opts.name);
|
|
956
|
+
body.encryptedName = w.ciphertext;
|
|
957
|
+
if (w.wrappings.length > 0)
|
|
958
|
+
body.nameContentWrappings = w.wrappings;
|
|
959
|
+
}
|
|
960
|
+
if (opts.description !== undefined) {
|
|
961
|
+
const w = await this.crypto.encryptWithWrapping(opts.description);
|
|
962
|
+
body.encryptedDescription = w.ciphertext;
|
|
963
|
+
if (w.wrappings.length > 0)
|
|
964
|
+
body.descriptionContentWrappings = w.wrappings;
|
|
965
|
+
}
|
|
966
|
+
if (opts.externalId !== undefined)
|
|
967
|
+
body.externalId = opts.externalId;
|
|
968
|
+
if (opts.participantAgentIds !== undefined)
|
|
969
|
+
body.participantAgentIds = opts.participantAgentIds;
|
|
970
|
+
const data = await this.request("POST", "/agent/channels", { body });
|
|
971
|
+
return parseChannel(data.channel);
|
|
972
|
+
}
|
|
973
|
+
/** Get a channel by ID. */
|
|
974
|
+
async getChannel(channelId) {
|
|
975
|
+
const data = await this.request("GET", `/agent/channels/${channelId}`);
|
|
976
|
+
return parseChannel(data.channel);
|
|
977
|
+
}
|
|
978
|
+
/** List channels with optional filters. */
|
|
979
|
+
async listChannels(filters) {
|
|
980
|
+
const params = {};
|
|
981
|
+
if (filters?.status !== undefined)
|
|
982
|
+
params.status = filters.status;
|
|
983
|
+
params.page = String(filters?.page ?? 1);
|
|
984
|
+
params.pageSize = String(filters?.pageSize ?? 20);
|
|
985
|
+
const data = await this.request("GET", "/agent/channels", { params });
|
|
986
|
+
return {
|
|
987
|
+
items: data.items.map((i) => parseChannel(i)),
|
|
988
|
+
total: data.total,
|
|
989
|
+
page: data.page,
|
|
990
|
+
pageSize: data.pageSize,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
/** Close a channel. */
|
|
994
|
+
async closeChannel(channelId) {
|
|
995
|
+
const data = await this.request("POST", `/agent/channels/${channelId}/close`);
|
|
996
|
+
return parseChannel(data.channel);
|
|
997
|
+
}
|
|
998
|
+
/** Add a participant to a channel. */
|
|
999
|
+
async addChannelParticipant(channelId, agentId) {
|
|
1000
|
+
const data = await this.request("POST", `/agent/channels/${channelId}/participants`, { body: { agentId } });
|
|
1001
|
+
return parseChannelParticipant(data.participant);
|
|
1002
|
+
}
|
|
1003
|
+
/** Remove a participant from a channel. */
|
|
1004
|
+
async removeChannelParticipant(channelId, agentId) {
|
|
1005
|
+
return this.request("DELETE", `/agent/channels/${channelId}/participants/${agentId}`);
|
|
1006
|
+
}
|
|
1007
|
+
/** Send a message in a channel. */
|
|
1008
|
+
async sendChannelMessage(channelId, content) {
|
|
1009
|
+
const wrapped = await this.crypto.encryptWithWrapping(content);
|
|
1010
|
+
const body = {
|
|
1011
|
+
encryptedContent: wrapped.ciphertext,
|
|
1012
|
+
};
|
|
1013
|
+
if (wrapped.wrappings.length > 0)
|
|
1014
|
+
body.contentWrappings = wrapped.wrappings;
|
|
1015
|
+
const data = await this.request("POST", `/agent/channels/${channelId}/messages`, { body });
|
|
1016
|
+
return parseChannelMessage(data.message);
|
|
1017
|
+
}
|
|
1018
|
+
/** List messages in a channel. */
|
|
1019
|
+
async listChannelMessages(channelId, opts) {
|
|
1020
|
+
const params = {};
|
|
1021
|
+
if (opts?.page !== undefined)
|
|
1022
|
+
params.page = String(opts.page);
|
|
1023
|
+
if (opts?.pageSize !== undefined)
|
|
1024
|
+
params.pageSize = String(opts.pageSize);
|
|
1025
|
+
if (opts?.before !== undefined)
|
|
1026
|
+
params.before = opts.before;
|
|
1027
|
+
if (opts?.after !== undefined)
|
|
1028
|
+
params.after = opts.after;
|
|
1029
|
+
const data = await this.request("GET", `/agent/channels/${channelId}/messages`, { params });
|
|
1030
|
+
const items = data.items.map((m) => parseChannelMessage(m));
|
|
1031
|
+
// Try to decrypt each message using content wrappings
|
|
1032
|
+
await Promise.all(items.map(async (msg) => {
|
|
1033
|
+
const raw = msg.raw;
|
|
1034
|
+
const wrappings = raw.contentWrappings;
|
|
1035
|
+
if (wrappings?.length && msg.encryptedContent) {
|
|
1036
|
+
const plain = await this.crypto.decryptWrappedContent(msg.encryptedContent, wrappings);
|
|
1037
|
+
if (plain !== null)
|
|
1038
|
+
msg.content = plain;
|
|
1039
|
+
}
|
|
1040
|
+
}));
|
|
1041
|
+
return { items, total: data.total, page: data.page, pageSize: data.pageSize };
|
|
1042
|
+
}
|
|
1043
|
+
/** Log a detailed activity entry visible to the human (thinking, tool calls, decisions). */
|
|
1044
|
+
async logActivity(entry) {
|
|
1045
|
+
return this.request("POST", "/agent/activity/log", { body: entry });
|
|
1046
|
+
}
|
|
1047
|
+
/** List participants in a channel. */
|
|
1048
|
+
async listChannelParticipants(channelId) {
|
|
1049
|
+
const data = await this.request("GET", `/agent/channels/${channelId}/participants`);
|
|
1050
|
+
return { participants: data.participants.map((p) => parseChannelParticipant(p)) };
|
|
1051
|
+
}
|
|
1052
|
+
// -- Agent Commands ----------------------------------------------------------
|
|
1053
|
+
/** Register or update commands for this agent (batch, max 50). */
|
|
1054
|
+
async registerCommands(commands) {
|
|
1055
|
+
const items = await Promise.all(commands.map(async (cmd) => {
|
|
1056
|
+
const { encrypted, contentWrappings } = await this.encryptFields({
|
|
1057
|
+
encryptedDescription: cmd.description,
|
|
1058
|
+
encryptedArgumentHint: cmd.argumentHint,
|
|
1059
|
+
encryptedParameters: cmd.parameters ? JSON.stringify(cmd.parameters) : undefined,
|
|
1060
|
+
});
|
|
1061
|
+
return { name: cmd.name, contentWrappings, annotations: cmd.annotations, ...encrypted };
|
|
1062
|
+
}));
|
|
1063
|
+
return this.request("POST", "/agent/commands", { body: { commands: items } });
|
|
1064
|
+
}
|
|
1065
|
+
/** List all commands registered by this agent. */
|
|
1066
|
+
async listCommands() {
|
|
1067
|
+
return this.request("GET", "/agent/commands");
|
|
1068
|
+
}
|
|
1069
|
+
/** Update a registered command by name. */
|
|
1070
|
+
async updateCommand(name, updates) {
|
|
1071
|
+
const fields = {};
|
|
1072
|
+
if (updates.description !== undefined)
|
|
1073
|
+
fields.encryptedDescription = updates.description;
|
|
1074
|
+
if (updates.argumentHint !== undefined)
|
|
1075
|
+
fields.encryptedArgumentHint = updates.argumentHint;
|
|
1076
|
+
if (updates.parameters !== undefined)
|
|
1077
|
+
fields.encryptedParameters = JSON.stringify(updates.parameters);
|
|
1078
|
+
const { encrypted, contentWrappings } = await this.encryptFields(fields);
|
|
1079
|
+
const body = { contentWrappings, ...encrypted };
|
|
1080
|
+
if (updates.annotations !== undefined)
|
|
1081
|
+
body.annotations = updates.annotations;
|
|
1082
|
+
return this.request("PUT", `/agent/commands/${name}`, { body });
|
|
1083
|
+
}
|
|
1084
|
+
/** Remove a registered command by name. */
|
|
1085
|
+
async removeCommand(name) {
|
|
1086
|
+
return this.request("DELETE", `/agent/commands/${name}`);
|
|
1087
|
+
}
|
|
1088
|
+
/** Poll for pending command invocations. */
|
|
1089
|
+
async getInvocations(opts) {
|
|
1090
|
+
const params = {};
|
|
1091
|
+
if (opts?.status)
|
|
1092
|
+
params.status = opts.status;
|
|
1093
|
+
if (opts?.convSessionId)
|
|
1094
|
+
params.convSessionId = opts.convSessionId;
|
|
1095
|
+
if (opts?.limit !== undefined)
|
|
1096
|
+
params.limit = String(opts.limit);
|
|
1097
|
+
if (opts?.offset !== undefined)
|
|
1098
|
+
params.offset = String(opts.offset);
|
|
1099
|
+
return this.request("GET", "/agent/commands/invocations", Object.keys(params).length > 0 ? { params } : undefined);
|
|
1100
|
+
}
|
|
1101
|
+
/** Acknowledge a command invocation (pending -> running). */
|
|
1102
|
+
async ackInvocation(invocationId) {
|
|
1103
|
+
return this.request("POST", `/agent/commands/invocations/${invocationId}/ack`);
|
|
1104
|
+
}
|
|
1105
|
+
/** Mark an invocation as completed. */
|
|
1106
|
+
async completeInvocation(invocationId, opts) {
|
|
1107
|
+
const body = {};
|
|
1108
|
+
if (opts?.result) {
|
|
1109
|
+
const { encrypted, contentWrappings } = await this.encryptFields({ encryptedResult: opts.result });
|
|
1110
|
+
Object.assign(body, encrypted);
|
|
1111
|
+
body.resultWrappings = contentWrappings;
|
|
1112
|
+
}
|
|
1113
|
+
return this.request("POST", `/agent/commands/invocations/${invocationId}/complete`, { body });
|
|
1114
|
+
}
|
|
1115
|
+
/** Mark an invocation as failed. */
|
|
1116
|
+
async failInvocation(invocationId, opts) {
|
|
1117
|
+
const body = {};
|
|
1118
|
+
if (opts?.error) {
|
|
1119
|
+
const { encrypted, contentWrappings } = await this.encryptFields({ encryptedResult: opts.error });
|
|
1120
|
+
Object.assign(body, encrypted);
|
|
1121
|
+
body.resultWrappings = contentWrappings;
|
|
1122
|
+
}
|
|
1123
|
+
return this.request("POST", `/agent/commands/invocations/${invocationId}/fail`, { body });
|
|
1124
|
+
}
|
|
1125
|
+
// -- Crypto -----------------------------------------------------------------
|
|
1126
|
+
static generateKeyPair() {
|
|
1127
|
+
return generateKeyPair();
|
|
1128
|
+
}
|
|
1129
|
+
/** Encrypt plaintext with AES-256-GCM for session messages. */
|
|
1130
|
+
static encryptAes(plaintext, keyBase64) {
|
|
1131
|
+
return encryptAes(plaintext, keyBase64);
|
|
1132
|
+
}
|
|
1133
|
+
/** Decrypt AES-256-GCM ciphertext from session messages. */
|
|
1134
|
+
static decryptAes(ciphertextBase64, keyBase64) {
|
|
1135
|
+
return decryptAes(ciphertextBase64, keyBase64);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Validate a form schema JSON string before encrypting.
|
|
1140
|
+
* - Must be valid JSON with a non-empty `fields` array.
|
|
1141
|
+
* - select / multi-select / radio fields must have at least 2 options.
|
|
1142
|
+
*/
|
|
1143
|
+
function validateFormSchema(schemaJson) {
|
|
1144
|
+
if (schemaJson === undefined || schemaJson.trim() === "") {
|
|
1145
|
+
throw new Error("formSchema must not be empty.");
|
|
1146
|
+
}
|
|
1147
|
+
let parsed;
|
|
1148
|
+
try {
|
|
1149
|
+
parsed = JSON.parse(schemaJson);
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
throw new Error("formSchema must be valid JSON.");
|
|
1153
|
+
}
|
|
1154
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1155
|
+
throw new Error("formSchema must be a JSON object.");
|
|
1156
|
+
}
|
|
1157
|
+
const schema = parsed;
|
|
1158
|
+
const fields = schema["fields"];
|
|
1159
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
1160
|
+
throw new Error("formSchema must contain a non-empty 'fields' array. " +
|
|
1161
|
+
"A form requires at least one input field.");
|
|
1162
|
+
}
|
|
1163
|
+
for (const field of fields) {
|
|
1164
|
+
if (typeof field !== "object" || field === null)
|
|
1165
|
+
continue;
|
|
1166
|
+
const f = field;
|
|
1167
|
+
const type = f["type"];
|
|
1168
|
+
if (type === "select" || type === "multi-select" || type === "radio") {
|
|
1169
|
+
const options = f["options"];
|
|
1170
|
+
if (!Array.isArray(options) || options.length < 2) {
|
|
1171
|
+
const name = f["name"] ?? "(unnamed)";
|
|
1172
|
+
throw new Error(`Form field '${name}' of type '${type}' must have at least 2 options.`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Return all wrappings as-is. Each encrypted field has its own random content
|
|
1179
|
+
* key, so every wrapping is unique even when scope + recipientKeyId match.
|
|
1180
|
+
* The frontend tries each wrapping until one successfully decrypts.
|
|
1181
|
+
*/
|