hiloop-sdk 0.6.0 → 0.8.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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Hiloop SDK client for agents to interact with the Hiloop platform. */
2
2
  import { CryptoContext, computeFingerprint, decryptAes, deriveKeyPairFromApiKey, deriveWrappingKey, encryptAes, generateKeyPair } from "./crypto.js";
3
- import { parseChannel, parseChannelMessage, parseChannelParticipant, parseConvSession, parseConvSessionMessage, parseGuestToken, parseInteraction, parseMessage, } from "./types.js";
3
+ import { parseChannel, parseChannelMessage, parseChannelParticipant, parseConvSession, parseConvSessionMessage, parseGuestToken, parseSessionMessage, } from "./types.js";
4
4
  export class HiloopError extends Error {
5
5
  constructor(statusCode, message) {
6
6
  super(`HTTP ${statusCode}: ${message}`);
@@ -201,242 +201,6 @@ export class HiloopClient {
201
201
  clearTimeout(timer);
202
202
  }
203
203
  }
204
- // -- Interactions -----------------------------------------------------------
205
- async createInteraction(opts) {
206
- // Validate form schema before encrypting
207
- if (opts.type === "form" && opts.formSchema === undefined) {
208
- throw new Error("Form interactions require a formSchema with at least one field.");
209
- }
210
- if (opts.formSchema !== undefined) {
211
- validateFormSchema(opts.formSchema);
212
- }
213
- // Encrypt all fields with a SINGLE shared content key to avoid
214
- // DB unique constraint on (interaction_id, scope, recipient_key_id).
215
- const { encrypted, contentWrappings } = await this.encryptFields({
216
- encryptedTitle: opts.title,
217
- encryptedDescription: opts.description,
218
- encryptedOptions: opts.options,
219
- encryptedContext: opts.context,
220
- encryptedFormSchema: opts.formSchema,
221
- });
222
- const body = {
223
- type: opts.type,
224
- priority: opts.priority ?? "normal",
225
- contentWrappings,
226
- ...encrypted,
227
- };
228
- if (opts.routeToUser !== undefined)
229
- body.routeToUser = opts.routeToUser;
230
- if (opts.routeToTeam !== undefined)
231
- body.routeToTeam = opts.routeToTeam;
232
- if (opts.deadlineMinutes !== undefined)
233
- body.deadlineMinutes = opts.deadlineMinutes;
234
- if (opts.callbackUrl !== undefined)
235
- body.callbackUrl = opts.callbackUrl;
236
- if (opts.convSessionId !== undefined)
237
- body.convSessionId = opts.convSessionId;
238
- if (opts.presentation !== undefined)
239
- body.presentation = opts.presentation;
240
- const data = await this.request("POST", "/agent/interactions", { body });
241
- return parseInteraction(data);
242
- }
243
- async getInteraction(interactionId) {
244
- const data = await this.request("GET", `/agent/interactions/${interactionId}`);
245
- return this.decryptInteractionFields(parseInteraction(data), data);
246
- }
247
- /** Try to decrypt encrypted interaction fields using content wrappings. */
248
- async decryptInteractionFields(interaction, raw) {
249
- const wrappings = raw.contentWrappings;
250
- if (!wrappings?.length)
251
- return interaction;
252
- const tryDecrypt = async (ciphertext) => {
253
- if (!ciphertext)
254
- return undefined;
255
- const plain = await this.crypto.decryptWrappedContent(ciphertext, wrappings);
256
- return plain ?? ciphertext;
257
- };
258
- return {
259
- ...interaction,
260
- title: (await tryDecrypt(interaction.title)) ?? "",
261
- description: await tryDecrypt(interaction.description),
262
- options: await tryDecrypt(interaction.options),
263
- context: await tryDecrypt(interaction.context),
264
- formSchema: await tryDecrypt(interaction.formSchema),
265
- response: await tryDecrypt(interaction.response),
266
- comment: await tryDecrypt(interaction.comment),
267
- };
268
- }
269
- async cancelInteraction(interactionId) {
270
- const data = await this.request("POST", `/agent/interactions/${interactionId}/cancel`);
271
- return parseInteraction(data);
272
- }
273
- /** Update description/context on an interaction. Accepts plaintext — auto-encrypts. */
274
- async updateInteractionContext(interactionId, opts) {
275
- const { encrypted, contentWrappings } = await this.encryptFields({
276
- encryptedDescription: opts.description,
277
- encryptedContext: opts.context,
278
- });
279
- return await this.request("PATCH", `/agent/interactions/${interactionId}/context`, { body: { contentWrappings, ...encrypted } });
280
- }
281
- async acknowledgeInteraction(interactionId) {
282
- const data = await this.request("POST", `/agent/interactions/${interactionId}/acknowledge`);
283
- return parseInteraction(data);
284
- }
285
- /** AG-018/019: Update priority, deadline, or context metadata on a pending interaction. */
286
- async updateInteractionMetadata(interactionId, opts) {
287
- return await this.request("PATCH", `/agent/interactions/${interactionId}/metadata`, { body: opts });
288
- }
289
- /** Get the transition log for an interaction. */
290
- async getInteractionTransitions(interactionId) {
291
- return await this.request("GET", `/agent/interactions/${interactionId}/transitions`);
292
- }
293
- /** HU-073 (agent side): Read pending task control signal (pause/resume/cancel). */
294
- async getTaskControl(interactionId) {
295
- return await this.request("GET", `/agent/interactions/${interactionId}/task-control`);
296
- }
297
- /** HU-073 (agent side): Acknowledge task control signal, clearing it. */
298
- async ackTaskControl(interactionId) {
299
- return await this.request("POST", `/agent/interactions/${interactionId}/task-control/ack`);
300
- }
301
- /** Single long-poll for a response (server-side timeout, max ~30s). */
302
- async awaitResponse(interactionId, timeout = 30) {
303
- const data = await this.request("GET", `/agent/interactions/${interactionId}/await`, { params: { timeout: String(timeout) }, timeout: (timeout + 5) * 1000 });
304
- return parseInteraction(data);
305
- }
306
- /**
307
- * Wait for a human to respond to an interaction, polling repeatedly.
308
- * Returns the interaction once it has a response (status is responded/completed),
309
- * or throws after the timeout expires.
310
- *
311
- * @param interactionId - The interaction to wait for
312
- * @param timeoutSeconds - Maximum wait time in seconds (default 300, max 3600)
313
- */
314
- async waitForResponse(interactionId, timeoutSeconds = 300) {
315
- const deadline = Date.now() + timeoutSeconds * 1000;
316
- const pollInterval = Math.min(30, timeoutSeconds); // server long-poll max 30s
317
- while (Date.now() < deadline) {
318
- const remaining = Math.ceil((deadline - Date.now()) / 1000);
319
- if (remaining <= 0)
320
- break;
321
- const pollTimeout = Math.min(pollInterval, remaining);
322
- const interaction = await this.awaitResponse(interactionId, pollTimeout);
323
- // Check if the human has responded
324
- if (interaction.status === "responded" ||
325
- interaction.status === "completed" ||
326
- interaction.status === "cancelled" ||
327
- interaction.status === "expired") {
328
- return interaction;
329
- }
330
- // Still pending — loop and poll again
331
- }
332
- // Final check before timeout
333
- const final = await this.getInteraction(interactionId);
334
- if (final.status !== "created" && final.status !== "pending" && final.status !== "viewed") {
335
- return final;
336
- }
337
- throw new HiloopError(408, `Timed out waiting for response after ${timeoutSeconds}s`);
338
- }
339
- async listInteractions(filters) {
340
- const params = {};
341
- if (filters?.status !== undefined)
342
- params.status = filters.status;
343
- if (filters?.type !== undefined)
344
- params.type = filters.type;
345
- params.page = String(filters?.page ?? 1);
346
- params.pageSize = String(filters?.pageSize ?? 20);
347
- const data = await this.request("GET", "/agent/interactions", { params });
348
- return {
349
- items: data.items.map((i) => parseInteraction(i)),
350
- total: data.total,
351
- page: data.page,
352
- pageSize: data.pageSize,
353
- };
354
- }
355
- // -- Messages ---------------------------------------------------------------
356
- async sendMessage(interactionId, content) {
357
- const { encrypted, contentWrappings } = await this.encryptFields({
358
- encryptedContent: content,
359
- });
360
- const data = await this.request("POST", `/agent/interactions/${interactionId}/messages`, { body: { contentWrappings, ...encrypted } });
361
- return parseMessage(data);
362
- }
363
- /** Push content blocks to an interaction. */
364
- async pushContentBlocks(interactionId, blocks) {
365
- await this.ensureInitialized();
366
- const encryptedBlocks = await Promise.all(blocks.map(async (block) => {
367
- const plaintext = JSON.stringify(block.data);
368
- const wrapped = await this.crypto.encryptWithWrapping(plaintext);
369
- return {
370
- blockType: block.blockType,
371
- encryptedData: wrapped.ciphertext,
372
- contentWrappings: wrapped.wrappings,
373
- position: block.position,
374
- };
375
- }));
376
- return await this.request("POST", `/agent/interactions/${interactionId}/blocks`, { body: { blocks: encryptedBlocks } });
377
- }
378
- /** List content blocks for an interaction. */
379
- async listContentBlocks(interactionId) {
380
- return await this.request("GET", `/agent/interactions/${interactionId}/blocks`);
381
- }
382
- /** Update a content block. */
383
- async updateContentBlock(interactionId, blockId, data) {
384
- return await this.request("PUT", `/agent/interactions/${interactionId}/blocks/${blockId}`, { body: { data } });
385
- }
386
- /** Delete a content block. */
387
- async deleteContentBlock(interactionId, blockId) {
388
- return await this.request("DELETE", `/agent/interactions/${interactionId}/blocks/${blockId}`);
389
- }
390
- /** Upload a file attachment to an interaction (base64-encoded). */
391
- async uploadAttachment(interactionId, opts) {
392
- return await this.request("POST", `/agent/interactions/${interactionId}/attachments`, { body: opts });
393
- }
394
- /** List attachments for an interaction, optionally filtered by messageId. */
395
- async listAttachments(interactionId, messageId) {
396
- const qs = messageId ? `?messageId=${encodeURIComponent(messageId)}` : "";
397
- return await this.request("GET", `/agent/interactions/${interactionId}/attachments${qs}`);
398
- }
399
- /** Download raw binary content of a specific attachment. */
400
- async getAttachment(interactionId, fileId) {
401
- return await this.requestBinary(`/agent/interactions/${interactionId}/attachments/${fileId}`);
402
- }
403
- /** List all reactions for messages in an interaction. */
404
- async listInteractionReactions(interactionId) {
405
- return await this.request("GET", `/agent/interactions/${interactionId}/reactions`);
406
- }
407
- /** Add a reaction emoji to a message in an interaction. */
408
- async addInteractionReaction(interactionId, messageId, emoji) {
409
- return await this.request("POST", `/agent/interactions/${interactionId}/messages/${messageId}/reactions`, { body: { emoji } });
410
- }
411
- /** Remove a reaction emoji from a message in an interaction. */
412
- async removeInteractionReaction(interactionId, messageId, emoji) {
413
- return await this.request("DELETE", `/agent/interactions/${interactionId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
414
- }
415
- /** Mark all messages in an interaction as read by this agent. */
416
- async markInteractionRead(interactionId) {
417
- return await this.request("POST", `/agent/interactions/${interactionId}/messages/read`);
418
- }
419
- /** Send a typing indicator for an interaction. */
420
- async sendInteractionTyping(interactionId, isTyping) {
421
- return await this.request("POST", `/agent/interactions/${interactionId}/typing`, { body: { isTyping } });
422
- }
423
- async listMessages(interactionId, page = 1, pageSize = 50) {
424
- const params = { page: String(page), pageSize: String(pageSize) };
425
- const data = await this.request("GET", `/agent/interactions/${interactionId}/messages`, { params });
426
- const rawItems = data.messages ?? data.items ?? [];
427
- const items = rawItems.map((m) => parseMessage(m));
428
- // Try to decrypt messages using content wrappings
429
- await Promise.all(items.map(async (msg, i) => {
430
- const raw = rawItems[i];
431
- const wrappings = raw.contentWrappings;
432
- if (wrappings?.length && msg.content) {
433
- const plain = await this.crypto.decryptWrappedContent(msg.content, wrappings);
434
- if (plain !== null)
435
- msg.content = plain;
436
- }
437
- }));
438
- return { items, total: data.total, page: data.page, pageSize: data.pageSize };
439
- }
440
204
  /** AD-060: Push agent telemetry activity status + plan to the platform. */
441
205
  async pushTelemetry(opts) {
442
206
  return await this.request("POST", "/agent/activity", { body: opts });
@@ -544,19 +308,60 @@ export class HiloopClient {
544
308
  const data = await this.request("POST", `/agent/sessions/${sessionId}/close`);
545
309
  return parseConvSession(data);
546
310
  }
311
+ // -- Unified Message API ----------------------------------------------------
547
312
  /**
548
- * Send a message to a conversation session.
549
- * The content must be pre-encrypted using encryptAes() with the shared session key.
550
- */
551
- /**
552
- * Send a message in a conv session. Accepts plaintext — encrypts automatically
553
- * using the session message key (`{agentPub}:{AES-GCM ciphertext}` format).
313
+ * Send a message to a session. Accepts plaintext content and optional
314
+ * rich components. Encrypts content and components automatically.
315
+ *
316
+ * This is the primary method for agent-to-human communication.
554
317
  */
555
- async sendConvSessionMessage(sessionId, content, opts) {
318
+ async sendMessage(sessionId, content, opts) {
556
319
  await this.ensureInitialized();
557
320
  const encryptedContent = await this.crypto.encryptSessionMessage(content);
558
- const data = await this.request("POST", `/agent/sessions/${sessionId}/messages`, { body: { encryptedContent }, agentName: opts?.agentName });
559
- return parseConvSessionMessage(data);
321
+ const body = { encryptedContent };
322
+ if (opts?.components && opts.components.length > 0) {
323
+ body.encryptedComponents = await this.crypto.encryptSessionMessage(JSON.stringify(opts.components));
324
+ // Extract metadata from components for server-side routing
325
+ const hasResponseRequired = opts.components.some((n) => n.type === "button_group" &&
326
+ n.data?.responseRequired === true);
327
+ if (hasResponseRequired) {
328
+ body.responseRequired = true;
329
+ body.category = "decide";
330
+ }
331
+ }
332
+ if (opts?.priority)
333
+ body.priority = opts.priority;
334
+ if (opts?.deadlineMinutes)
335
+ body.deadlineMinutes = opts.deadlineMinutes;
336
+ if (opts?.replyToMessageId)
337
+ body.replyToMessageId = opts.replyToMessageId;
338
+ const data = await this.request("POST", `/agent/sessions/${sessionId}/messages`, { body, agentName: opts?.agentName });
339
+ return parseSessionMessage(data);
340
+ }
341
+ /**
342
+ * Long-poll for a response to a specific message.
343
+ * Returns the updated message once the human has responded, or after timeout.
344
+ */
345
+ async awaitResponse(sessionId, messageId, timeoutSeconds = 300) {
346
+ const data = await this.request("GET", `/agent/sessions/${sessionId}/messages/${messageId}/await`, {
347
+ params: { timeout: String(Math.min(timeoutSeconds, 30)) },
348
+ timeout: (Math.min(timeoutSeconds, 30) + 5) * 1000,
349
+ });
350
+ return parseSessionMessage(data);
351
+ }
352
+ /**
353
+ * Acknowledge a message (mark it as seen/handled by the agent).
354
+ */
355
+ async acknowledge(sessionId, messageId) {
356
+ const data = await this.request("POST", `/agent/sessions/${sessionId}/messages/${messageId}/acknowledge`);
357
+ return parseSessionMessage(data);
358
+ }
359
+ /**
360
+ * Cancel a previously sent message (e.g. withdraw an approval request).
361
+ */
362
+ async cancel(sessionId, messageId) {
363
+ const data = await this.request("POST", `/agent/sessions/${sessionId}/messages/${messageId}/cancel`);
364
+ return parseSessionMessage(data);
560
365
  }
561
366
  /** Send a typing indicator to a conv session (HU-034). */
562
367
  async sendConvSessionTyping(sessionId, typing) {
@@ -580,7 +385,7 @@ export class HiloopClient {
580
385
  if (opts?.after !== undefined)
581
386
  params.after = opts.after;
582
387
  const result = await this.request("GET", `/agent/sessions/${sessionId}/timeline`, { params });
583
- // Decrypt timeline items (session messages + interaction fields)
388
+ // Decrypt timeline items (session messages)
584
389
  if (Array.isArray(result.items)) {
585
390
  await Promise.all(result.items.map(async (item) => {
586
391
  if (item.kind === "message" && typeof item.encryptedContent === "string") {
@@ -590,20 +395,6 @@ export class HiloopClient {
590
395
  else
591
396
  item.content = "[Encrypted]";
592
397
  }
593
- if (item.kind === "interaction") {
594
- const wrappings = item.contentWrappings;
595
- if (wrappings?.length) {
596
- for (const field of ["encryptedTitle", "encryptedDescription", "encryptedOptions", "encryptedFormSchema", "encryptedContext"]) {
597
- if (typeof item[field] === "string") {
598
- const plain = await this.crypto.decryptWrappedContent(item[field], wrappings);
599
- if (plain !== null) {
600
- const readableKey = field.replace(/^encrypted/, "").replace(/^./, (c) => c.toLowerCase());
601
- item[readableKey] = plain;
602
- }
603
- }
604
- }
605
- }
606
- }
607
398
  }));
608
399
  }
609
400
  return result;
@@ -634,117 +425,31 @@ export class HiloopClient {
634
425
  async revokeGuestToken(sessionId, tokenId) {
635
426
  await this.request("DELETE", `/human/sessions/${sessionId}/guest-tokens/${tokenId}`);
636
427
  }
637
- /** AG-060: Create multiple interactions in a single API call. */
638
- async createInteractionBatch(interactions) {
639
- const items = await Promise.all(interactions.map(async (opts) => {
640
- const { encrypted, contentWrappings } = await this.encryptFields({
641
- encryptedTitle: opts.title,
642
- encryptedDescription: opts.description,
643
- encryptedOptions: opts.options,
644
- encryptedContext: opts.context,
645
- });
646
- const body = {
647
- type: opts.type,
648
- priority: opts.priority ?? "normal",
649
- contentWrappings,
650
- ...encrypted,
651
- };
652
- if (opts.routeToUser !== undefined)
653
- body.routeToUser = opts.routeToUser;
654
- if (opts.routeToTeam !== undefined)
655
- body.routeToTeam = opts.routeToTeam;
656
- if (opts.deadlineMinutes !== undefined)
657
- body.deadlineMinutes = opts.deadlineMinutes;
658
- if (opts.convSessionId !== undefined)
659
- body.convSessionId = opts.convSessionId;
660
- return body;
661
- }));
662
- return this.request("POST", "/agent/interactions/batch", { body: { interactions: items } });
663
- }
664
428
  /** Archive a conversation session. */
665
429
  async archiveConvSession(sessionId) {
666
430
  const data = await this.request("POST", `/agent/sessions/${sessionId}/archive`);
667
431
  return parseConvSession(data);
668
432
  }
669
- /** Create a group interaction involving multiple participants (AG-061). */
670
- async createGroupInteraction(opts, participants) {
671
- const { encrypted, contentWrappings } = await this.encryptFields({
672
- encryptedTitle: opts.title,
673
- encryptedDescription: opts.description,
674
- encryptedContext: opts.context,
675
- });
676
- const body = {
677
- type: opts.type,
678
- priority: opts.priority ?? "normal",
679
- participants,
680
- contentWrappings,
681
- ...encrypted,
682
- };
683
- if (opts.routeToUser !== undefined)
684
- body.routeToUser = opts.routeToUser;
685
- if (opts.routeToTeam !== undefined)
686
- body.routeToTeam = opts.routeToTeam;
687
- if (opts.deadlineMinutes !== undefined)
688
- body.deadlineMinutes = opts.deadlineMinutes;
689
- if (opts.callbackUrl !== undefined)
690
- body.callbackUrl = opts.callbackUrl;
691
- return this.request("POST", "/agent/interactions/group", { body });
692
- }
693
- /** Add a reference from one interaction to another (AG-062). */
694
- async addInteractionReference(interactionId, targetInteractionId, opts) {
695
- return this.request("POST", `/agent/interactions/${interactionId}/references`, {
696
- body: { targetInteractionId, referenceType: opts?.referenceType, note: opts?.note },
697
- });
433
+ // -- Session attachments -----------------------------------------------------
434
+ /** Upload an attachment to a session. Content must be base64-encoded. */
435
+ async uploadSessionAttachment(sessionId, opts) {
436
+ const data = await this.request("POST", `/agent/sessions/${sessionId}/attachments`, { body: opts });
437
+ return data.attachment;
698
438
  }
699
- /** List all references for an interaction (AG-062). */
700
- async listInteractionReferences(interactionId) {
701
- return this.request("GET", `/agent/interactions/${interactionId}/references`);
439
+ /** List attachments for a session. */
440
+ async listSessionAttachments(sessionId) {
441
+ const data = await this.request("GET", `/agent/sessions/${sessionId}/attachments`);
442
+ return data.attachments;
702
443
  }
703
- /** Remove an interaction reference by its ID (AG-062). */
704
- async removeInteractionReference(interactionId, refId) {
705
- return this.request("DELETE", `/agent/interactions/${interactionId}/references/${refId}`);
444
+ /** Download an attachment's binary content. */
445
+ async downloadSessionAttachment(sessionId, fileId) {
446
+ return this.requestBinary(`/agent/sessions/${sessionId}/attachments/${fileId}`);
706
447
  }
707
448
  // -- Public agent routes (no auth required) ---------------------------------
708
449
  /** Get public metadata for a public agent by slug. */
709
450
  async getPublicAgentInfo(slug) {
710
451
  return this.request("GET", `/public/${slug}/info`);
711
452
  }
712
- /** Create an interaction as a public (anonymous) user on a public agent. */
713
- async createPublicInteraction(slug, opts) {
714
- return this.request("POST", `/public/${slug}/interactions`, { body: opts });
715
- }
716
- /** Get the status of a public interaction by slug + ID. */
717
- async getPublicInteraction(slug, interactionId) {
718
- return this.request("GET", `/public/${slug}/interactions/${interactionId}`);
719
- }
720
- /** Long-poll for a response on a public interaction (returns when responded/completed/cancelled/expired). */
721
- async awaitPublicInteraction(slug, interactionId, timeoutMs = 30000) {
722
- return this.request("GET", `/public/${slug}/interactions/${interactionId}/await`, {
723
- params: { timeout: String(timeoutMs) },
724
- timeout: timeoutMs + 5000,
725
- });
726
- }
727
- /** Send a message to a public interaction as an anonymous user. */
728
- async sendPublicMessage(slug, interactionId, encryptedContent) {
729
- return this.request("POST", `/public/${slug}/interactions/${interactionId}/messages`, { body: { encryptedContent } });
730
- }
731
- /** List interactions created on a public agent (paginated). */
732
- async listPublicInteractions(slug, opts) {
733
- const params = {};
734
- if (opts?.page !== undefined)
735
- params.page = String(opts.page);
736
- if (opts?.pageSize !== undefined)
737
- params.pageSize = String(opts.pageSize);
738
- return this.request("GET", `/public/${slug}/interactions`, Object.keys(params).length > 0 ? { params } : undefined);
739
- }
740
- /** Cancel a public interaction (anonymous user side). */
741
- async cancelPublicInteraction(slug, interactionId) {
742
- return this.request("DELETE", `/public/${slug}/interactions/${interactionId}`);
743
- }
744
- /** Get the transition log for a public interaction. */
745
- async getPublicInteractionTransitions(slug, interactionId) {
746
- return this.request("GET", `/public/${slug}/interactions/${interactionId}/transitions`);
747
- }
748
453
  /** Look up a public session by its public token (no auth required). */
749
454
  async getPublicSession(publicToken) {
750
455
  return this.request("GET", `/public/sessions/${publicToken}`);
@@ -753,298 +458,6 @@ export class HiloopClient {
753
458
  async joinPublicSession(publicToken, displayName) {
754
459
  return this.request("POST", `/public/sessions/${publicToken}/join`, { body: { displayName } });
755
460
  }
756
- // -- Human Interaction Detail -----------------------------------------------
757
- /** Get a human-facing interaction detail view. */
758
- async getHumanInteraction(interactionId) {
759
- return this.request("GET", `/human/interactions/${interactionId}`);
760
- }
761
- /** Mark an interaction as viewed by the authenticated user. */
762
- async viewInteraction(interactionId) {
763
- return this.request("POST", `/human/interactions/${interactionId}/view`);
764
- }
765
- /** Submit a response to an interaction. */
766
- async respondToHumanInteraction(interactionId, opts) {
767
- return this.request("POST", `/human/interactions/${interactionId}/respond`, { body: opts });
768
- }
769
- /** Snooze an interaction until a given ISO datetime. */
770
- async snoozeInteraction(interactionId, snoozedUntil) {
771
- return this.request("POST", `/human/interactions/${interactionId}/snooze`, { body: { snoozedUntil } });
772
- }
773
- /** Toggle the pinned state of an interaction. */
774
- async pinInteraction(interactionId, pinned) {
775
- return this.request("POST", `/human/interactions/${interactionId}/pin`, { body: { pinned } });
776
- }
777
- // -- Human Inbox / Feed ----------------------------------------------------
778
- // getInbox removed -- use getFeed with view param
779
- /** Get the human user's activity feed (all interactions). */
780
- async getFeed(opts) {
781
- const params = new URLSearchParams();
782
- if (opts?.page)
783
- params.set("page", String(opts.page));
784
- if (opts?.pageSize)
785
- params.set("pageSize", String(opts.pageSize));
786
- if (opts?.view)
787
- params.set("view", opts.view);
788
- if (opts?.q)
789
- params.set("q", opts.q);
790
- if (opts?.type)
791
- params.set("type", opts.type);
792
- if (opts?.priority)
793
- params.set("priority", opts.priority);
794
- if (opts?.pinned !== undefined)
795
- params.set("pinned", String(opts.pinned));
796
- if (opts?.agentId)
797
- params.set("agentId", opts.agentId);
798
- const qs = params.toString() ? `?${params.toString()}` : "";
799
- return this.request("GET", `/human/feed${qs}`);
800
- }
801
- // searchInteractions removed -- use getFeed with q param
802
- // getHistory removed -- use getFeed with view param
803
- /** Get badge counts (e.g. open interactions) for the authenticated user. */
804
- async getBadges() {
805
- return this.request("GET", "/human/badges");
806
- }
807
- // -- Account ----------------------------------------------------------------
808
- /** Get the authenticated user's current availability status. */
809
- async getMyAvailability() {
810
- return this.request("GET", "/human/availability");
811
- }
812
- /** Update the authenticated user's availability status. */
813
- async updateMyAvailability(status) {
814
- return this.request("PATCH", "/human/availability", { body: { status } });
815
- }
816
- /** List delegation rules for the authenticated user. */
817
- async listDelegations() {
818
- return this.request("GET", "/human/delegations");
819
- }
820
- /** Create a delegation rule for the authenticated user. */
821
- async createDelegation(opts) {
822
- return this.request("POST", "/human/delegations", { body: opts });
823
- }
824
- /** Remove a delegation rule by delegateId. */
825
- async deleteDelegation(delegateId) {
826
- return this.request("DELETE", `/human/delegations/${delegateId}`);
827
- }
828
- // -- Notifications ----------------------------------------------------------
829
- /** List sent notifications for the authenticated user. */
830
- async listNotifications() {
831
- return this.request("GET", "/human/notifications");
832
- }
833
- /** Get notification preferences for the authenticated user. */
834
- async getNotificationPreferences() {
835
- return this.request("GET", "/human/notifications/preferences");
836
- }
837
- /** Update notification preference for a specific channel. */
838
- async updateNotificationPreference(channel, opts) {
839
- return this.request("PUT", `/human/notifications/preferences/${channel}`, { body: opts });
840
- }
841
- /** Register a push token for the authenticated user's device. */
842
- async registerPushToken(opts) {
843
- return this.request("POST", "/human/notifications/push-tokens", { body: opts });
844
- }
845
- /** Remove the push token for the authenticated user's device. */
846
- async deletePushToken(opts) {
847
- return this.request("DELETE", "/human/notifications/push-tokens", { body: opts });
848
- }
849
- /** Save (upsert) a response draft for an interaction. */
850
- async saveDraft(interactionId, encryptedDraft) {
851
- return this.request("PUT", `/human/interactions/${interactionId}/draft`, { body: { encryptedDraft } });
852
- }
853
- /** Get the saved response draft for an interaction. */
854
- async getDraft(interactionId) {
855
- return this.request("GET", `/human/interactions/${interactionId}/draft`);
856
- }
857
- /** Delete the saved response draft for an interaction. */
858
- async deleteDraft(interactionId) {
859
- return this.request("DELETE", `/human/interactions/${interactionId}/draft`);
860
- }
861
- /** Unsnooze an interaction before its scheduled snooze time. */
862
- async unsnoozeInteraction(interactionId) {
863
- return this.request("POST", `/human/interactions/${interactionId}/unsnooze`);
864
- }
865
- // -- Collaboration (milestone alerts + approval chain) -----------------------
866
- /** Create a milestone alert for an interaction. */
867
- async createMilestoneAlert(interactionId, opts) {
868
- return this.request("POST", `/human/interactions/${interactionId}/milestone-alerts`, { body: opts });
869
- }
870
- /** List milestone alerts for an interaction. */
871
- async listMilestoneAlerts(interactionId) {
872
- return this.request("GET", `/human/interactions/${interactionId}/milestone-alerts`);
873
- }
874
- /** Delete a milestone alert. */
875
- async deleteMilestoneAlert(interactionId, alertId) {
876
- return this.request("DELETE", `/human/interactions/${interactionId}/milestone-alerts/${alertId}`);
877
- }
878
- /** Get the approval chain for an interaction. */
879
- async getApprovalChain(interactionId) {
880
- return this.request("GET", `/human/interactions/${interactionId}/approval-chain`);
881
- }
882
- /** Respond to an approval chain step. */
883
- async respondToApprovalChain(interactionId, opts) {
884
- return this.request("POST", `/human/interactions/${interactionId}/approval-chain/respond`, { body: opts });
885
- }
886
- // -- Phase 628: Comments + review feedback ------------------------------------
887
- /** List comments on an interaction. */
888
- async listInteractionComments(interactionId) {
889
- return this.request("GET", `/human/interactions/${interactionId}/comments`);
890
- }
891
- /** Post a comment on an interaction. */
892
- async createInteractionComment(interactionId, opts) {
893
- return this.request("POST", `/human/interactions/${interactionId}/comments`, { body: opts });
894
- }
895
- /** Mark a comment as resolved. */
896
- async resolveInteractionComment(interactionId, commentId) {
897
- return this.request("PATCH", `/human/interactions/${interactionId}/comments/${commentId}/resolve`);
898
- }
899
- /** Delete a comment. */
900
- async deleteInteractionComment(interactionId, commentId) {
901
- return this.request("DELETE", `/human/interactions/${interactionId}/comments/${commentId}`);
902
- }
903
- /** Submit review feedback for an interaction. */
904
- async submitReviewFeedback(interactionId, opts) {
905
- return this.request("POST", `/human/interactions/${interactionId}/review-feedback`, { body: opts });
906
- }
907
- // -- Phase 629: Voting + tags --------------------------------------------------
908
- /** Cast a vote on an interaction. */
909
- async voteOnInteraction(interactionId, opts) {
910
- return this.request("POST", `/human/interactions/${interactionId}/vote`, { body: opts });
911
- }
912
- /** Get aggregate vote results for an interaction. */
913
- async getVoteResults(interactionId) {
914
- return this.request("GET", `/human/interactions/${interactionId}/vote/results`);
915
- }
916
- /** Get the current user's vote on an interaction. */
917
- async getMyVote(interactionId) {
918
- return this.request("GET", `/human/interactions/${interactionId}/vote/mine`);
919
- }
920
- /** List tags on an interaction. */
921
- async getInteractionTags(interactionId) {
922
- return this.request("GET", `/human/interactions/${interactionId}/tags`);
923
- }
924
- /** Add a tag to an interaction. */
925
- async addInteractionTag(interactionId, tag) {
926
- return this.request("POST", `/human/interactions/${interactionId}/tags`, { body: { tag } });
927
- }
928
- // -- Phase 630: More tags + response templates ---------------------------------
929
- /** Remove a tag from an interaction. */
930
- async deleteInteractionTag(interactionId, tag) {
931
- return this.request("DELETE", `/human/interactions/${interactionId}/tags/${encodeURIComponent(tag)}`);
932
- }
933
- /** List all space-wide tags. */
934
- async listSpaceTags() {
935
- return this.request("GET", "/human/tags");
936
- }
937
- /** List response templates for the current user. */
938
- async listResponseTemplates() {
939
- return this.request("GET", "/human/response-templates");
940
- }
941
- /** Create a response template. */
942
- async createResponseTemplate(opts) {
943
- return this.request("POST", "/human/response-templates", { body: opts });
944
- }
945
- /** Delete a response template. */
946
- async deleteResponseTemplate(templateId) {
947
- return this.request("DELETE", `/human/response-templates/${templateId}`);
948
- }
949
- // -- Phase 631: Review feedback + template use + message routes ----------------
950
- /** Get review feedback for an interaction. */
951
- async getReviewFeedback(interactionId) {
952
- return this.request("GET", `/human/interactions/${interactionId}/review-feedback`);
953
- }
954
- /** Use a response template (increments usage count). */
955
- async useResponseTemplate(templateId) {
956
- return this.request("POST", `/human/response-templates/${templateId}/use`);
957
- }
958
- /** List messages for a human interaction. */
959
- async listInteractionMessages(interactionId, opts) {
960
- const params = {};
961
- if (opts?.before !== undefined)
962
- params.before = opts.before;
963
- if (opts?.limit !== undefined)
964
- params.limit = String(opts.limit);
965
- return this.request("GET", `/human/interactions/${interactionId}/messages`, { params });
966
- }
967
- /** Send a message to a human interaction thread. */
968
- async sendInteractionMessage(interactionId, opts) {
969
- return this.request("POST", `/human/interactions/${interactionId}/messages`, { body: opts });
970
- }
971
- /** Mark messages as read for an interaction. */
972
- async markMessagesRead(interactionId) {
973
- return this.request("POST", `/human/interactions/${interactionId}/messages/read`);
974
- }
975
- // -- Phase 632: Reactions + agent activity + references ------------------------
976
- /** Get aggregated reactions for all messages in an interaction. */
977
- async getInteractionReactions(interactionId) {
978
- return this.request("GET", `/human/interactions/${interactionId}/reactions`);
979
- }
980
- /** Add a reaction to a specific message. */
981
- async addMessageReaction(interactionId, messageId, emoji) {
982
- return this.request("POST", `/human/interactions/${interactionId}/messages/${messageId}/reactions`, { body: { emoji } });
983
- }
984
- /** Remove a reaction from a specific message. */
985
- async removeMessageReaction(interactionId, messageId, emoji) {
986
- return this.request("DELETE", `/human/interactions/${interactionId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
987
- }
988
- /** Get agent activity data for the space (for human dashboard). */
989
- async getAgentActivity() {
990
- return this.request("GET", "/human/agents/activity");
991
- }
992
- /** List interaction references for a human interaction. */
993
- async listHumanInteractionReferences(interactionId) {
994
- return this.request("GET", `/human/interactions/${interactionId}/references`);
995
- }
996
- // -- Phase 633: References + usage + feature gates + presence ------------------
997
- /** Add a reference link between two interactions. */
998
- async addHumanInteractionReference(interactionId, opts) {
999
- return this.request("POST", `/human/interactions/${interactionId}/references`, { body: opts });
1000
- }
1001
- /** Get current usage and quota for the space. */
1002
- async getUsage() {
1003
- return this.request("GET", "/human/usage");
1004
- }
1005
- /** Get feature gate flags for the current user. */
1006
- async getFeatureGates() {
1007
- return this.request("GET", "/human/feature-gates");
1008
- }
1009
- /** Track that the current user is viewing an interaction. */
1010
- async trackInteractionViewing(interactionId) {
1011
- return this.request("POST", `/human/interactions/${interactionId}/viewing`);
1012
- }
1013
- /** Get who is currently viewing an interaction. */
1014
- async getInteractionViewers(interactionId) {
1015
- return this.request("GET", `/human/interactions/${interactionId}/viewers`);
1016
- }
1017
- // -- Phase 634: More account routes + inbox grouped + members ------------------
1018
- /** Stop tracking viewing presence for an interaction. */
1019
- async stopTrackingViewing(interactionId) {
1020
- return this.request("DELETE", `/human/interactions/${interactionId}/viewing`);
1021
- }
1022
- /** Get human-assigned tasks (interactions requiring action). */
1023
- async getHumanTasks(opts) {
1024
- const params = {};
1025
- if (opts?.page !== undefined)
1026
- params.page = String(opts.page);
1027
- if (opts?.pageSize !== undefined)
1028
- params.pageSize = String(opts.pageSize);
1029
- return this.request("GET", "/human/tasks", { params });
1030
- }
1031
- // getInboxGrouped removed -- use getFeed with view param
1032
- /** List space members (humans). */
1033
- async getHumanMembers(opts) {
1034
- const params = {};
1035
- if (opts?.page !== undefined)
1036
- params.page = String(opts.page);
1037
- if (opts?.pageSize !== undefined)
1038
- params.pageSize = String(opts.pageSize);
1039
- return this.request("GET", "/human/members", { params });
1040
- }
1041
- /** Get aggregated notification digest for the current user. */
1042
- async getNotificationDigest(opts) {
1043
- const params = {};
1044
- if (opts?.since !== undefined)
1045
- params.since = opts.since;
1046
- return this.request("GET", "/human/notifications/digest", { params });
1047
- }
1048
461
  // -- Channels ---------------------------------------------------------------
1049
462
  /** Create a new agent-to-agent channel. */
1050
463
  async createChannel(opts) {
@@ -1237,6 +650,22 @@ export class HiloopClient {
1237
650
  await this.ensureInitialized();
1238
651
  return this.crypto.decryptSessionMessage(encryptedContent);
1239
652
  }
653
+ /**
654
+ * Decrypt a webhook payload in place.
655
+ * Decrypts encryptedContent from session messages.
656
+ * Returns the payload with an added `content` field.
657
+ */
658
+ async decryptWebhookPayload(payload) {
659
+ await this.ensureInitialized();
660
+ const result = { ...payload };
661
+ // Session message
662
+ if (typeof payload.encryptedContent === "string") {
663
+ const plain = await this.crypto.decryptSessionMessage(payload.encryptedContent);
664
+ if (plain !== null)
665
+ result.content = plain;
666
+ }
667
+ return result;
668
+ }
1240
669
  // -- Crypto -----------------------------------------------------------------
1241
670
  static generateKeyPair() {
1242
671
  return generateKeyPair();
@@ -1250,47 +679,3 @@ export class HiloopClient {
1250
679
  return decryptAes(ciphertextBase64, keyBase64);
1251
680
  }
1252
681
  }
1253
- /**
1254
- * Validate a form schema JSON string before encrypting.
1255
- * - Must be valid JSON with a non-empty `fields` array.
1256
- * - select / multi-select / radio fields must have at least 2 options.
1257
- */
1258
- function validateFormSchema(schemaJson) {
1259
- if (schemaJson === undefined || schemaJson.trim() === "") {
1260
- throw new Error("formSchema must not be empty.");
1261
- }
1262
- let parsed;
1263
- try {
1264
- parsed = JSON.parse(schemaJson);
1265
- }
1266
- catch {
1267
- throw new Error("formSchema must be valid JSON.");
1268
- }
1269
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1270
- throw new Error("formSchema must be a JSON object.");
1271
- }
1272
- const schema = parsed;
1273
- const fields = schema["fields"];
1274
- if (!Array.isArray(fields) || fields.length === 0) {
1275
- throw new Error("formSchema must contain a non-empty 'fields' array. " +
1276
- "A form requires at least one input field.");
1277
- }
1278
- for (const field of fields) {
1279
- if (typeof field !== "object" || field === null)
1280
- continue;
1281
- const f = field;
1282
- const type = f["type"];
1283
- if (type === "select" || type === "multi-select" || type === "radio") {
1284
- const options = f["options"];
1285
- if (!Array.isArray(options) || options.length < 2) {
1286
- const name = f["name"] ?? "(unnamed)";
1287
- throw new Error(`Form field '${name}' of type '${type}' must have at least 2 options.`);
1288
- }
1289
- }
1290
- }
1291
- }
1292
- /**
1293
- * Return all wrappings as-is. Each encrypted field has its own random content
1294
- * key, so every wrapping is unique even when scope + recipientKeyId match.
1295
- * The frontend tries each wrapping until one successfully decrypts.
1296
- */