feat: P5.5 dev inject + adapter-inspector demo + adapter smoke (P5 complete)

Dev-only POST /v1/dev/webhook/:type signs+injects a sample payload. apps/adapter-
inspector (Vite): inbound raw events + outbound commands/attempts, simulate-inbound
+ test-send buttons. smoke-adapter proves signed->normalized->interaction, bad-sig
401, sandboxed outbound (no network). 55 tests; all 4 smokes pass; boundary enforced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 13:54:18 +05:30
parent 4f661f4c81
commit b879c9eba2
9 changed files with 288 additions and 5 deletions
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>IIOS P5 — Adapter Inspector</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
{
"name": "adapter-inspector",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 5175",
"build": "vite build"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.3",
"vite": "^6.0.7"
}
}
+102
View File
@@ -0,0 +1,102 @@
import { useEffect, useState } from 'react';
const SERVICE = 'http://localhost:3200';
const APP_ID = 'portal-demo';
interface RawEvent {
id: string;
channelType: string;
externalEventId?: string | null;
signatureStatus: string;
status: string;
interactionId?: string | null;
}
interface Command {
id: string;
channelType: string;
target: string;
status: string;
providerRef?: string | null;
attempts?: Array<{ id: string; status: string }>;
}
async function devToken(userId: string): Promise<string> {
const r = await fetch(`${SERVICE}/v1/dev/token`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ appId: APP_ID, userId, name: userId }),
});
if (!r.ok) throw new Error(`devToken ${r.status} (service running with IIOS_DEV_TOKENS=1?)`);
return ((await r.json()) as { token: string }).token;
}
async function api<T>(token: string, path: string, method = 'GET', body?: unknown): Promise<T> {
const r = await fetch(`${SERVICE}${path}`, {
method,
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
body: body ? JSON.stringify(body) : undefined,
});
return (await r.json()) as T;
}
export function App() {
const [token, setToken] = useState<string | null>(null);
const [inbound, setInbound] = useState<RawEvent[]>([]);
const [outbound, setOutbound] = useState<Command[]>([]);
useEffect(() => {
void devToken('inspector').then(setToken);
}, []);
useEffect(() => {
if (!token) return;
const load = async () => {
setInbound(await api<RawEvent[]>(token, '/v1/adapters/inbound'));
setOutbound(await api<Command[]>(token, '/v1/adapters/outbound'));
};
void load();
const t = setInterval(() => void load(), 2000);
return () => clearInterval(t);
}, [token]);
if (!token) return <div style={{ padding: 16, fontFamily: 'sans-serif' }}>loading</div>;
const cell: React.CSSProperties = { padding: '4px 8px', borderBottom: '1px solid #eee' };
return (
<div style={{ padding: 16, fontFamily: 'sans-serif' }}>
<h2>IIOS P5 Adapter Inspector</h2>
<p style={{ color: '#666' }}>All simulated no real provider is contacted.</p>
<button onClick={() => void api(token, '/v1/dev/webhook/WEBHOOK', 'POST', { text: 'simulated inbound' })}>
simulate inbound
</button>
<button
style={{ marginLeft: 8 }}
onClick={() => void api(token, '/v1/adapters/WEBHOOK/send', 'POST', { target: 'user@x', payload: { text: 'test send' } })}
>
test send
</button>
<div style={{ display: 'flex', gap: 24, marginTop: 16 }}>
<div style={{ flex: 1 }}>
<h3>Inbound raw events ({inbound.length})</h3>
{inbound.map((e) => (
<div key={e.id} style={cell}>
<b>{e.channelType}</b> · sig:{e.signatureStatus} · {e.status}
{e.interactionId ? ` · → ${e.interactionId.slice(0, 8)}` : ''}
</div>
))}
</div>
<div style={{ flex: 1 }}>
<h3>Outbound commands ({outbound.length})</h3>
{outbound.map((c) => (
<div key={c.id} style={cell}>
<b>{c.channelType}</b> {c.target} · {c.status}
{c.providerRef ? ` · ${c.providerRef}` : ''} · attempts:{c.attempts?.length ?? 0}
</div>
))}
</div>
</div>
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { createRoot } from 'react-dom/client';
import { App } from './App';
const el = document.getElementById('root');
if (el) createRoot(el).render(<App />);
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"jsx": "react-jsx",
"types": ["react", "react-dom"],
"noEmit": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 5175 },
});