first commit
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Server-side client for the real IIOS service (see D:\iios\docs\IIOS_API_AND_SDK_GUIDE.md).
|
||||
* Runs only in BFF route handlers — never shipped to the browser. Auth uses a dev token
|
||||
* minted from /v1/dev/token (requires the IIOS service running with IIOS_DEV_TOKENS=1);
|
||||
* in production swap mintToken() for a real Session-Broker exchange.
|
||||
*/
|
||||
|
||||
const IIOS_URL = process.env.IIOS_URL ?? "http://localhost:3200";
|
||||
const APP_ID = process.env.IIOS_APP_ID ?? "portal-demo";
|
||||
const USER_ID = process.env.IIOS_USER_ID ?? "james";
|
||||
const ORG_ID = process.env.IIOS_ORG_ID ?? "org_A";
|
||||
|
||||
let cachedToken: { token: string; exp: number } | null = null;
|
||||
|
||||
async function mintToken(): Promise<string> {
|
||||
// reuse for ~100 min (IIOS dev tokens live 2h)
|
||||
if (cachedToken && cachedToken.exp > Date.now()) return cachedToken.token;
|
||||
const res = await fetch(`${IIOS_URL}/v1/dev/token`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ appId: APP_ID, userId: USER_ID, orgId: ORG_ID }),
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) throw new Error(`IIOS token mint failed: ${res.status} ${await res.text().catch(() => "")}`);
|
||||
const { token } = (await res.json()) as { token: string };
|
||||
cachedToken = { token, exp: Date.now() + 100 * 60_000 };
|
||||
return token;
|
||||
}
|
||||
|
||||
async function req<T>(path: string, init?: RequestInit & { idempotencyKey?: string }): Promise<T> {
|
||||
const token = await mintToken();
|
||||
const headers: Record<string, string> = {
|
||||
"content-type": "application/json",
|
||||
authorization: `Bearer ${token}`,
|
||||
...(init?.headers as Record<string, string> | undefined),
|
||||
};
|
||||
if (init?.idempotencyKey) headers["idempotency-key"] = init.idempotencyKey;
|
||||
const res = await fetch(`${IIOS_URL}${path}`, { ...init, headers, cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`IIOS ${res.status} ${path}: ${await res.text().catch(() => "")}`);
|
||||
if (res.status === 204) return undefined as T;
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
export const iios = {
|
||||
info: { url: IIOS_URL, appId: APP_ID, userId: USER_ID, orgId: ORG_ID },
|
||||
health: () => req<{ status: string; db: boolean }>("/health"),
|
||||
getThreadMessages: (threadId: string) => req<any>(`/v1/threads/${threadId}/messages`),
|
||||
sendMessage: (threadId: string, content: string, idem?: string) =>
|
||||
req<any>(`/v1/threads/${threadId}/messages`, { method: "POST", body: JSON.stringify({ content }), idempotencyKey: idem }),
|
||||
listInbox: (state?: string) => req<any>(`/v1/inbox/items${state ? `?state=${state}` : ""}`),
|
||||
patchInbox: (id: string, state: string, reason?: string) =>
|
||||
req<any>(`/v1/inbox/items/${id}`, { method: "PATCH", body: JSON.stringify({ state, reason }) }),
|
||||
listTickets: (scope?: string) => req<any>(`/v1/support/tickets${scope ? `?scope=${scope}` : ""}`),
|
||||
patchTicket: (id: string, state: string, reason?: string) =>
|
||||
req<any>(`/v1/support/tickets/${id}`, { method: "PATCH", body: JSON.stringify({ state, reason }) }),
|
||||
createTicket: (subject: string, priority?: string, threadId?: string) =>
|
||||
req<any>(`/v1/support/tickets`, { method: "POST", body: JSON.stringify({ subject, priority, threadId }) }),
|
||||
escalate: (threadId: string, subject?: string) =>
|
||||
req<any>(`/v1/support/escalate`, { method: "POST", body: JSON.stringify({ threadId, subject }) }),
|
||||
};
|
||||
|
||||
export async function iiosReachable(): Promise<boolean> {
|
||||
try {
|
||||
const h = await iios.health();
|
||||
return !!h?.status;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { InboxItem, InboxKind, InboxState, Message, Ticket, TicketPriority, TicketState } from "@lynkd/messaging-inbox-sdk";
|
||||
|
||||
/**
|
||||
* Map IIOS API shapes → this app's SDK types. Written defensively because the
|
||||
* exact field names live in the IIOS codebase; adjust as you wire real responses.
|
||||
* (Keep the UI contract = SDK types; do all translation here in the BFF.)
|
||||
*/
|
||||
|
||||
const ts = (v: any): number => {
|
||||
if (typeof v === "number") return v;
|
||||
if (typeof v === "string") { const t = Date.parse(v); if (!Number.isNaN(t)) return t; }
|
||||
return Date.now();
|
||||
};
|
||||
|
||||
export function toMessage(m: any, channelId: string): Message {
|
||||
const text =
|
||||
m.content ??
|
||||
m.body ??
|
||||
(Array.isArray(m.parts) ? m.parts.find((p: any) => (p.kind ?? p.partType) === "TEXT")?.bodyText : undefined) ??
|
||||
"";
|
||||
return {
|
||||
id: String(m.id ?? m.messageId ?? m.interactionId),
|
||||
channelId,
|
||||
authorId: String(m.senderActorId ?? m.actorId ?? m.authorId ?? m.source?.externalId ?? "unknown"),
|
||||
ts: ts(m.sentAt ?? m.occurredAt ?? m.createdAt ?? m.ts),
|
||||
body: String(text),
|
||||
parentId: m.parentInteractionId ?? m.parentId ?? null,
|
||||
reactions: [],
|
||||
deliveryState: "sent",
|
||||
};
|
||||
}
|
||||
|
||||
const inboxKind = (k: any): InboxKind => {
|
||||
const up = String(k ?? "").toUpperCase();
|
||||
const known: InboxKind[] = ["NEEDS_REPLY", "MENTION", "REACTION", "THREAD_REPLY", "SAVED", "APP", "SUPPORT_UPDATE", "MEETING_FOLLOWUP"];
|
||||
return (known.includes(up as InboxKind) ? up : "NEEDS_REPLY") as InboxKind;
|
||||
};
|
||||
|
||||
export function toInboxItem(i: any): InboxItem {
|
||||
return {
|
||||
id: String(i.id),
|
||||
kind: inboxKind(i.kind),
|
||||
state: (String(i.state ?? "OPEN").toUpperCase() as InboxState),
|
||||
title: String(i.title ?? i.subject ?? "Inbox item"),
|
||||
preview: String(i.preview ?? i.snippet ?? ""),
|
||||
channelId: i.conversationId ?? i.threadId ?? undefined,
|
||||
messageId: i.messageId ?? undefined,
|
||||
actorId: i.actorId ?? i.createdByActorId ?? undefined,
|
||||
ts: ts(i.createdAt ?? i.ts),
|
||||
dueAt: i.dueAt ? ts(i.dueAt) : undefined,
|
||||
priority: i.priority ? (String(i.priority).toLowerCase() as InboxItem["priority"]) : "normal",
|
||||
};
|
||||
}
|
||||
|
||||
export function toTicket(t: any): Ticket {
|
||||
return {
|
||||
id: String(t.id),
|
||||
ref: String(t.ref ?? t.id),
|
||||
subject: String(t.subject ?? ""),
|
||||
requesterId: String(t.requesterActorId ?? t.requesterId ?? "unknown"),
|
||||
assigneeId: t.assigneeActorId ?? t.assigneeId ?? undefined,
|
||||
state: (String(t.state ?? "NEW").toUpperCase() as TicketState),
|
||||
priority: (String(t.priority ?? "NORMAL").toUpperCase() as TicketPriority),
|
||||
channelId: t.threadId ?? t.conversationId ?? undefined,
|
||||
category: t.klass ?? t.category ?? undefined,
|
||||
createdTs: ts(t.createdAt),
|
||||
updatedTs: ts(t.updatedAt ?? t.createdAt),
|
||||
slaBreached: !!t.slaBreached,
|
||||
tags: t.tags ?? undefined,
|
||||
messages: Array.isArray(t.messages) ? t.messages.map((m: any) => toMessage(m, `t_${t.id}`)) : [],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"seeded": true,
|
||||
"generatedAt": "2026-07-12T07:10:38.714Z",
|
||||
"workspace": {
|
||||
"id": "ws_lynkd",
|
||||
"name": "LynkedUp Pro",
|
||||
"initials": "LP",
|
||||
"accent": "#FDA913",
|
||||
"plan": "Business+"
|
||||
},
|
||||
"workspaces": [
|
||||
{
|
||||
"id": "ws_lynkd",
|
||||
"name": "LynkedUp Pro",
|
||||
"initials": "LP",
|
||||
"accent": "#FDA913",
|
||||
"plan": "Business+"
|
||||
}
|
||||
],
|
||||
"me": {
|
||||
"id": "cmrgtgjsd0004vcsgilt81q5t",
|
||||
"name": "James Carter",
|
||||
"handle": "james",
|
||||
"avatarText": "JC",
|
||||
"avatarColor": "#FDA913",
|
||||
"title": "Property Owner",
|
||||
"presence": "active"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"id": "cmrhgf55k000wvcsg3r0v75yt",
|
||||
"name": "Ava Mitchell",
|
||||
"handle": "ava",
|
||||
"avatarText": "AM",
|
||||
"avatarColor": "#5b9dff",
|
||||
"title": "Ops Lead",
|
||||
"presence": "active"
|
||||
},
|
||||
{
|
||||
"id": "cmrhgf56a0018vcsgmfp78o4e",
|
||||
"name": "Sara Khan",
|
||||
"handle": "sara",
|
||||
"avatarText": "SK",
|
||||
"avatarColor": "#f5b544",
|
||||
"title": "Account Manager",
|
||||
"presence": "active"
|
||||
},
|
||||
{
|
||||
"id": "cmrgtgjsd0004vcsgilt81q5t",
|
||||
"name": "James Carter",
|
||||
"handle": "james",
|
||||
"avatarText": "JC",
|
||||
"avatarColor": "#FDA913",
|
||||
"title": "Property Owner",
|
||||
"presence": "active"
|
||||
},
|
||||
{
|
||||
"id": "cmrhgf56z001mvcsgni08yw4s",
|
||||
"name": "Noah Bennett",
|
||||
"handle": "noah",
|
||||
"avatarText": "NB",
|
||||
"avatarColor": "#3ecf8e",
|
||||
"title": "Field Supervisor",
|
||||
"presence": "away"
|
||||
},
|
||||
{
|
||||
"id": "cmrhgf57s0022vcsgd8cjjuwq",
|
||||
"name": "Mia Torres",
|
||||
"handle": "mia",
|
||||
"avatarText": "MT",
|
||||
"avatarColor": "#bb98ff",
|
||||
"title": "Estimator",
|
||||
"presence": "active"
|
||||
},
|
||||
{
|
||||
"id": "cmrhgf58d002gvcsga37m9tzz",
|
||||
"name": "Zoe Nguyen",
|
||||
"handle": "zoe",
|
||||
"avatarText": "ZN",
|
||||
"avatarColor": "#f2555a",
|
||||
"title": "Customer Success",
|
||||
"presence": "active"
|
||||
}
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
"id": "starred",
|
||||
"label": "Starred"
|
||||
},
|
||||
{
|
||||
"id": "channels",
|
||||
"label": "Channels"
|
||||
},
|
||||
{
|
||||
"id": "dms",
|
||||
"label": "Direct messages"
|
||||
}
|
||||
],
|
||||
"channels": [
|
||||
{
|
||||
"id": "cmrhgf55s0010vcsgm2fl4wnw",
|
||||
"kind": "channel",
|
||||
"name": "general",
|
||||
"topic": "Company-wide announcements and work-based matters",
|
||||
"memberIds": [
|
||||
"cmrhgf55k000wvcsg3r0v75yt",
|
||||
"cmrhgf56a0018vcsgmfp78o4e",
|
||||
"cmrgtgjsd0004vcsgilt81q5t",
|
||||
"cmrhgf56z001mvcsgni08yw4s",
|
||||
"cmrhgf57s0022vcsgd8cjjuwq",
|
||||
"cmrhgf58d002gvcsga37m9tzz"
|
||||
],
|
||||
"isStarred": true,
|
||||
"sectionId": "starred",
|
||||
"unreadCount": 0
|
||||
},
|
||||
{
|
||||
"id": "cmrhgf57i001uvcsg83xtxj23",
|
||||
"kind": "channel",
|
||||
"name": "field-ops",
|
||||
"topic": "Crews, schedules, and on-site coordination",
|
||||
"memberIds": [
|
||||
"cmrhgf55k000wvcsg3r0v75yt",
|
||||
"cmrhgf56a0018vcsgmfp78o4e",
|
||||
"cmrgtgjsd0004vcsgilt81q5t",
|
||||
"cmrhgf56z001mvcsgni08yw4s",
|
||||
"cmrhgf57s0022vcsgd8cjjuwq",
|
||||
"cmrhgf58d002gvcsga37m9tzz"
|
||||
],
|
||||
"isStarred": true,
|
||||
"sectionId": "starred",
|
||||
"unreadCount": 0
|
||||
},
|
||||
{
|
||||
"id": "cmrhgf58e002ivcsg0lsskcjr",
|
||||
"kind": "channel",
|
||||
"name": "leads-pipeline",
|
||||
"topic": "New leads and verification queue",
|
||||
"memberIds": [
|
||||
"cmrhgf55k000wvcsg3r0v75yt",
|
||||
"cmrhgf56a0018vcsgmfp78o4e",
|
||||
"cmrgtgjsd0004vcsgilt81q5t",
|
||||
"cmrhgf56z001mvcsgni08yw4s",
|
||||
"cmrhgf57s0022vcsgd8cjjuwq",
|
||||
"cmrhgf58d002gvcsga37m9tzz"
|
||||
],
|
||||
"isStarred": false,
|
||||
"sectionId": "channels",
|
||||
"unreadCount": 0
|
||||
},
|
||||
{
|
||||
"id": "cmrhgf590002wvcsg322e4w53",
|
||||
"kind": "channel",
|
||||
"name": "estimates",
|
||||
"topic": "ProCanvas estimates & approvals",
|
||||
"memberIds": [
|
||||
"cmrhgf55k000wvcsg3r0v75yt",
|
||||
"cmrhgf56a0018vcsgmfp78o4e",
|
||||
"cmrgtgjsd0004vcsgilt81q5t",
|
||||
"cmrhgf56z001mvcsgni08yw4s",
|
||||
"cmrhgf57s0022vcsgd8cjjuwq",
|
||||
"cmrhgf58d002gvcsga37m9tzz"
|
||||
],
|
||||
"isStarred": false,
|
||||
"sectionId": "channels",
|
||||
"unreadCount": 0
|
||||
},
|
||||
{
|
||||
"id": "cmrhgf59k003avcsgw3lvm6mt",
|
||||
"kind": "channel",
|
||||
"name": "random",
|
||||
"topic": "Non-work banter",
|
||||
"memberIds": [
|
||||
"cmrhgf55k000wvcsg3r0v75yt",
|
||||
"cmrhgf56a0018vcsgmfp78o4e",
|
||||
"cmrgtgjsd0004vcsgilt81q5t",
|
||||
"cmrhgf56z001mvcsgni08yw4s",
|
||||
"cmrhgf57s0022vcsgd8cjjuwq",
|
||||
"cmrhgf58d002gvcsga37m9tzz"
|
||||
],
|
||||
"isStarred": false,
|
||||
"sectionId": "channels",
|
||||
"unreadCount": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user