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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />);
|
||||
Reference in New Issue
Block a user