feat(p6): kernel-client route methods + @insignia/iios-community-web SDK

Task 6.5: add createBinding/listBindings/simulateRoute/listRouteDecisions/
approveDecision/denyDecision to RestClient + RouteBinding/RouteDecision types.
New @insignia/iios-community-web package (CommunityProvider, useBindings,
useRoutePreview, useRouteDecisions). Boundary allowlist: community-web ->
contracts + kernel-client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 15:12:50 +05:30
parent 095ea869aa
commit 1ee737c025
9 changed files with 210 additions and 1 deletions
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@insignia/iios-community-web",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist"],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@insignia/iios-kernel-client": "workspace:*"
},
"peerDependencies": {
"react": ">=18"
},
"devDependencies": {
"@types/react": "^19.0.0",
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
}
}
+2
View File
@@ -0,0 +1,2 @@
export { CommunityProvider, useBindings, useRoutePreview, useRouteDecisions } from './react';
export type { RouteBinding, RouteDecision } from '@insignia/iios-kernel-client';
+88
View File
@@ -0,0 +1,88 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { RestClient, type RouteBinding, type RouteDecision } from '@insignia/iios-kernel-client';
const CommunityContext = createContext<RestClient | null>(null);
export function CommunityProvider({
serviceUrl,
token,
children,
}: {
serviceUrl: string;
token: string;
children: React.ReactNode;
}): React.ReactElement {
const client = useMemo(() => new RestClient({ serviceUrl, token }), [serviceUrl, token]);
return <CommunityContext.Provider value={client}>{children}</CommunityContext.Provider>;
}
function useClient(): RestClient {
const c = useContext(CommunityContext);
if (!c) throw new Error('community hooks must be used within <CommunityProvider>');
return c;
}
/** Manage route bindings (origin→destination rules). */
export function useBindings(): {
bindings: RouteBinding[];
refresh: () => Promise<void>;
createBinding: (input: Partial<RouteBinding>) => Promise<RouteBinding>;
} {
const client = useClient();
const [bindings, setBindings] = useState<RouteBinding[]>([]);
const refresh = useCallback(async () => {
try {
setBindings(await client.listBindings());
} catch {
/* transient */
}
}, [client]);
useEffect(() => {
void refresh();
}, [refresh]);
const createBinding = async (input: Partial<RouteBinding>) => {
const b = await client.createBinding(input);
await refresh();
return b;
};
return { bindings, refresh, createBinding };
}
/** Preview-first: simulate an interaction against the bindings (no send). */
export function useRoutePreview(): (interactionId: string, originChannelType: string, originRef?: string) => Promise<RouteDecision[]> {
const client = useClient();
return async (interactionId, originChannelType, originRef) =>
(await client.simulateRoute({ interactionId, originChannelType, originRef })).decisions;
}
/** Pending decisions + admin approve/deny. */
export function useRouteDecisions(opts?: { state?: string; pollMs?: number }): {
decisions: RouteDecision[];
refresh: () => Promise<void>;
approve: (id: string) => Promise<void>;
deny: (id: string) => Promise<void>;
} {
const client = useClient();
const [decisions, setDecisions] = useState<RouteDecision[]>([]);
const refresh = useCallback(async () => {
try {
setDecisions(await client.listRouteDecisions(opts?.state));
} catch {
/* transient */
}
}, [client, opts?.state]);
useEffect(() => {
void refresh();
const t = setInterval(() => void refresh(), opts?.pollMs ?? 3000);
return () => clearInterval(t);
}, [refresh, opts?.pollMs]);
const approve = async (id: string) => {
await client.approveDecision(id);
await refresh();
};
const deny = async (id: string) => {
await client.denyDecision(id);
await refresh();
};
return { decisions, refresh, approve, deny };
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"jsx": "react-jsx",
"types": ["react"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
}
@@ -0,0 +1,9 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
external: ['react', 'react-dom', '@insignia/iios-kernel-client'],
});
+25 -1
View File
@@ -1,5 +1,5 @@
import type { IngestInteractionRequest } from '@insignia/iios-contracts';
import type { Message, InboxItem, InboxState, Ticket, TicketState, CallbackRequest } from './types';
import type { Message, InboxItem, InboxState, Ticket, TicketState, CallbackRequest, RouteBinding, RouteDecision } from './types';
export interface RestConfig {
serviceUrl: string;
@@ -100,6 +100,30 @@ export class RestClient {
return r.json();
}
// ─── routing (P6) ─────────────────────────────────────────────
async createBinding(input: Partial<RouteBinding>): Promise<RouteBinding> {
return this.post<RouteBinding>('/v1/routes/bindings', input);
}
async listBindings(): Promise<RouteBinding[]> {
const r = await fetch(this.url('/v1/routes/bindings'), { headers: this.headers() });
if (!r.ok) throw new Error(`listBindings ${r.status}`);
return (await r.json()) as RouteBinding[];
}
async simulateRoute(input: { interactionId: string; originChannelType: string; originRef?: string }): Promise<{ simulationId: string; decisions: RouteDecision[] }> {
return this.post('/v1/routes/simulate', input);
}
async listRouteDecisions(state?: string): Promise<RouteDecision[]> {
const r = await fetch(this.url(`/v1/routes/decisions${state ? `?state=${state}` : ''}`), { headers: this.headers() });
if (!r.ok) throw new Error(`listRouteDecisions ${r.status}`);
return (await r.json()) as RouteDecision[];
}
async approveDecision(id: string): Promise<RouteDecision> {
return this.post<RouteDecision>(`/v1/routes/decisions/${id}/approve`, {});
}
async denyDecision(id: string): Promise<RouteDecision> {
return this.post<RouteDecision>(`/v1/routes/decisions/${id}/deny`, {});
}
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}`);
+25
View File
@@ -77,6 +77,31 @@ export interface CallbackRequest {
preferChannel: string;
}
export interface RouteBinding {
id: string;
originChannelType: string;
originRef?: string | null;
destinationChannelType: string;
destinationRef?: string | null;
restrictionProfile?: string | null;
mode: string;
outputFormat: string;
requiresReview: boolean;
enabled: boolean;
}
export interface RouteDecision {
id: string;
interactionId: string;
routeBindingId: string;
decisionState: string;
reasonCodes: string[];
previewPayload: { text?: string; destinationRef?: string | null; format?: string } | unknown;
outputFormat: string;
executed: boolean;
routeBinding?: RouteBinding;
}
/** Minimal socket surface so the facade can be unit-tested with a fake. */
export interface SocketLike {
on(event: string, handler: (...args: unknown[]) => void): unknown;
+19
View File
@@ -120,6 +120,25 @@ importers:
specifier: workspace:*
version: link:../iios-contracts
packages/iios-community-web:
dependencies:
'@insignia/iios-kernel-client':
specifier: workspace:*
version: link:../iios-kernel-client
devDependencies:
'@types/react':
specifier: ^19.0.0
version: 19.2.17
react:
specifier: ^19.0.0
version: 19.2.7
tsup:
specifier: ^8.3.5
version: 8.5.1(jiti@2.7.0)(postcss@8.5.16)(typescript@5.9.3)
typescript:
specifier: ^5.7.3
version: 5.9.3
packages/iios-contracts: {}
packages/iios-inbox-web:
+1
View File
@@ -21,6 +21,7 @@ const ALLOWED = {
'@insignia/iios-service': ['@insignia/iios-contracts', '@insignia/iios-testkit', '@insignia/iios-adapter-sdk'],
'@insignia/iios-message-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
'@insignia/iios-inbox-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
'@insignia/iios-community-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
'@insignia/iios-support-web': [
'@insignia/iios-contracts',
'@insignia/iios-kernel-client',