Files
iios/packages/iios-kernel-client/src/rest.ts
T
maaz519 9c6e2c76e8 fix(support): agent-demo self-seeds (join default queue + go online); tickets always get a queue
Root cause of 'waiting for escalations': no queue/membership meant tickets were
never assigned. Now: createTicket find-or-creates a default queue; new joinDefault
endpoint + useGoOnline hook; agent-demo joins+goes-AVAILABLE on load (assignPending
picks up any waiting ticket). smoke-support self-seeds (no seed script needed).
Also: vitest singleFork to end cross-file DB races (45 tests deterministic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:39:14 +05:30

119 lines
4.9 KiB
TypeScript

import type { IngestInteractionRequest } from '@insignia/iios-contracts';
import type { Message, InboxItem, InboxState, Ticket, TicketState, CallbackRequest } from './types';
export interface RestConfig {
serviceUrl: string;
token?: string;
}
/** REST/polling client for kernel reads and the native-send fallback. */
export class RestClient {
constructor(private readonly config: RestConfig) {}
private url(path: string): string {
return `${this.config.serviceUrl.replace(/\/$/, '')}${path}`;
}
private headers(extra: Record<string, string> = {}): Record<string, string> {
const h: Record<string, string> = { 'content-type': 'application/json', ...extra };
if (this.config.token) h.authorization = `Bearer ${this.config.token}`;
return h;
}
async getThreadMessages(threadId: string): Promise<{ threadId: string; messages: unknown[] }> {
const r = await fetch(this.url(`/v1/threads/${threadId}/messages`), { headers: this.headers() });
if (!r.ok) throw new Error(`getThreadMessages ${r.status}`);
return (await r.json()) as { threadId: string; messages: unknown[] };
}
async sendMessage(
threadId: string,
content: string,
opts?: { contentRef?: string; idempotencyKey?: string },
): Promise<Message> {
const r = await fetch(this.url(`/v1/threads/${threadId}/messages`), {
method: 'POST',
headers: this.headers(opts?.idempotencyKey ? { 'idempotency-key': opts.idempotencyKey } : {}),
body: JSON.stringify({ content, contentRef: opts?.contentRef }),
});
if (!r.ok) throw new Error(`sendMessage ${r.status}`);
return (await r.json()) as Message;
}
async listInboxItems(state?: InboxState): Promise<InboxItem[]> {
const q = state ? `?state=${encodeURIComponent(state)}` : '';
const r = await fetch(this.url(`/v1/inbox/items${q}`), { headers: this.headers() });
if (!r.ok) throw new Error(`listInboxItems ${r.status}`);
return (await r.json()) as InboxItem[];
}
async patchInboxItem(id: string, body: { state: InboxState; reason?: string }): Promise<InboxItem> {
const r = await fetch(this.url(`/v1/inbox/items/${id}`), {
method: 'PATCH',
headers: this.headers(),
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`patchInboxItem ${r.status}`);
return (await r.json()) as InboxItem;
}
// ─── support ──────────────────────────────────────────────────
async createTicket(body: { subject: string; priority?: string; threadId?: string }): Promise<Ticket> {
return this.post<Ticket>('/v1/support/tickets', body);
}
async escalate(threadId: string, subject?: string): Promise<Ticket> {
return this.post<Ticket>('/v1/support/escalate', { threadId, subject });
}
async listTickets(scope: 'mine' | 'assigned' = 'mine'): Promise<Ticket[]> {
const r = await fetch(this.url(`/v1/support/tickets?scope=${scope}`), { headers: this.headers() });
if (!r.ok) throw new Error(`listTickets ${r.status}`);
return (await r.json()) as Ticket[];
}
async patchTicket(id: string, state: TicketState): Promise<Ticket> {
const r = await fetch(this.url(`/v1/support/tickets/${id}`), {
method: 'PATCH',
headers: this.headers(),
body: JSON.stringify({ state }),
});
if (!r.ok) throw new Error(`patchTicket ${r.status}`);
return (await r.json()) as Ticket;
}
async requestCallback(body: { preferChannel: string; preferredTime?: string; notes?: string; ticketId?: string }): Promise<CallbackRequest> {
return this.post<CallbackRequest>('/v1/support/callbacks', body);
}
async createQueue(name: string): Promise<{ id: string }> {
return this.post<{ id: string }>('/v1/support/queues', { name });
}
async joinQueue(queueId: string): Promise<unknown> {
return this.post('/v1/support/queues/' + queueId + '/members', {});
}
async joinDefaultQueue(): Promise<unknown> {
return this.post('/v1/support/agents/me/join', {});
}
async setAvailability(state: string): Promise<unknown> {
const r = await fetch(this.url('/v1/support/members/me/availability'), {
method: 'PATCH',
headers: this.headers(),
body: JSON.stringify({ state }),
});
if (!r.ok) throw new Error(`setAvailability ${r.status}`);
return r.json();
}
private async post<T>(path: string, body: unknown): Promise<T> {
const r = await fetch(this.url(path), { method: 'POST', headers: this.headers(), body: JSON.stringify(body) });
if (!r.ok) throw new Error(`POST ${path} ${r.status}`);
return (await r.json()) as T;
}
async ingest(body: IngestInteractionRequest, idempotencyKey: string): Promise<unknown> {
const r = await fetch(this.url('/v1/interactions/ingest'), {
method: 'POST',
headers: this.headers({ 'idempotency-key': idempotencyKey }),
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`ingest ${r.status}`);
return r.json();
}
}