From 1ee737c0258b7511810abcbbfb8bc08d324131d3 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Wed, 1 Jul 2026 15:12:50 +0530 Subject: [PATCH] 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) --- packages/iios-community-web/package.json | 27 +++++++ packages/iios-community-web/src/index.ts | 2 + packages/iios-community-web/src/react.tsx | 88 ++++++++++++++++++++++ packages/iios-community-web/tsconfig.json | 14 ++++ packages/iios-community-web/tsup.config.ts | 9 +++ packages/iios-kernel-client/src/rest.ts | 26 ++++++- packages/iios-kernel-client/src/types.ts | 25 ++++++ pnpm-lock.yaml | 19 +++++ scripts/check-import-boundary.mjs | 1 + 9 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 packages/iios-community-web/package.json create mode 100644 packages/iios-community-web/src/index.ts create mode 100644 packages/iios-community-web/src/react.tsx create mode 100644 packages/iios-community-web/tsconfig.json create mode 100644 packages/iios-community-web/tsup.config.ts diff --git a/packages/iios-community-web/package.json b/packages/iios-community-web/package.json new file mode 100644 index 0000000..d336921 --- /dev/null +++ b/packages/iios-community-web/package.json @@ -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" + } +} diff --git a/packages/iios-community-web/src/index.ts b/packages/iios-community-web/src/index.ts new file mode 100644 index 0000000..b1cc3a3 --- /dev/null +++ b/packages/iios-community-web/src/index.ts @@ -0,0 +1,2 @@ +export { CommunityProvider, useBindings, useRoutePreview, useRouteDecisions } from './react'; +export type { RouteBinding, RouteDecision } from '@insignia/iios-kernel-client'; diff --git a/packages/iios-community-web/src/react.tsx b/packages/iios-community-web/src/react.tsx new file mode 100644 index 0000000..21f3283 --- /dev/null +++ b/packages/iios-community-web/src/react.tsx @@ -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(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 {children}; +} + +function useClient(): RestClient { + const c = useContext(CommunityContext); + if (!c) throw new Error('community hooks must be used within '); + return c; +} + +/** Manage route bindings (origin→destination rules). */ +export function useBindings(): { + bindings: RouteBinding[]; + refresh: () => Promise; + createBinding: (input: Partial) => Promise; +} { + const client = useClient(); + const [bindings, setBindings] = useState([]); + const refresh = useCallback(async () => { + try { + setBindings(await client.listBindings()); + } catch { + /* transient */ + } + }, [client]); + useEffect(() => { + void refresh(); + }, [refresh]); + const createBinding = async (input: Partial) => { + 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 { + 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; + approve: (id: string) => Promise; + deny: (id: string) => Promise; +} { + const client = useClient(); + const [decisions, setDecisions] = useState([]); + 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 }; +} diff --git a/packages/iios-community-web/tsconfig.json b/packages/iios-community-web/tsconfig.json new file mode 100644 index 0000000..4a4d5c4 --- /dev/null +++ b/packages/iios-community-web/tsconfig.json @@ -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"] +} diff --git a/packages/iios-community-web/tsup.config.ts b/packages/iios-community-web/tsup.config.ts new file mode 100644 index 0000000..1cf9cbf --- /dev/null +++ b/packages/iios-community-web/tsup.config.ts @@ -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'], +}); diff --git a/packages/iios-kernel-client/src/rest.ts b/packages/iios-kernel-client/src/rest.ts index 525c541..3fb1339 100644 --- a/packages/iios-kernel-client/src/rest.ts +++ b/packages/iios-kernel-client/src/rest.ts @@ -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): Promise { + return this.post('/v1/routes/bindings', input); + } + async listBindings(): Promise { + 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 { + 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 { + return this.post(`/v1/routes/decisions/${id}/approve`, {}); + } + async denyDecision(id: string): Promise { + return this.post(`/v1/routes/decisions/${id}/deny`, {}); + } + private async post(path: string, body: unknown): Promise { 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}`); diff --git a/packages/iios-kernel-client/src/types.ts b/packages/iios-kernel-client/src/types.ts index db64aab..dd93be0 100644 --- a/packages/iios-kernel-client/src/types.ts +++ b/packages/iios-kernel-client/src/types.ts @@ -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; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2542fcd..fda06b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/scripts/check-import-boundary.mjs b/scripts/check-import-boundary.mjs index 5082d14..79c58ce 100644 --- a/scripts/check-import-boundary.mjs +++ b/scripts/check-import-boundary.mjs @@ -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',