feat(sdk): P3.4 @insignia/iios-inbox-web + kernel-client inbox REST

RestClient.listInboxItems/patchInboxItem + InboxItem type; iios-inbox-web
InboxProvider + useInbox (poll + snooze/done/reopen). Boundary allowlist extended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 04:30:42 +05:30
parent 20c8f15dff
commit 0a1631dbc0
9 changed files with 170 additions and 1 deletions
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@insignia/iios-inbox-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 { InboxProvider, useInbox } from './react';
export type { InboxItem, InboxState } from '@insignia/iios-kernel-client';
+63
View File
@@ -0,0 +1,63 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { RestClient, type InboxItem, type InboxState } from '@insignia/iios-kernel-client';
const InboxContext = createContext<RestClient | null>(null);
/** Provides an inbox REST client to the inbox hooks. */
export function InboxProvider({
serviceUrl,
token,
children,
}: {
serviceUrl: string;
token: string;
children: React.ReactNode;
}): React.ReactElement {
const client = useMemo(() => new RestClient({ serviceUrl, token }), [serviceUrl, token]);
return <InboxContext.Provider value={client}>{children}</InboxContext.Provider>;
}
function useClient(): RestClient {
const c = useContext(InboxContext);
if (!c) throw new Error('useInbox must be used within <InboxProvider>');
return c;
}
/** The caller's inbox items + actions. Polls on an interval (default 3s). */
export function useInbox(opts?: { state?: InboxState; pollMs?: number }): {
items: InboxItem[];
refresh: () => Promise<void>;
snooze: (id: string) => Promise<void>;
done: (id: string) => Promise<void>;
reopen: (id: string) => Promise<void>;
} {
const client = useClient();
const [items, setItems] = useState<InboxItem[]>([]);
const refresh = useCallback(async () => {
try {
setItems(await client.listInboxItems(opts?.state));
} catch {
/* transient — keep last items */
}
}, [client, opts?.state]);
useEffect(() => {
void refresh();
const t = setInterval(() => void refresh(), opts?.pollMs ?? 3000);
return () => clearInterval(t);
}, [refresh, opts?.pollMs]);
const patch = async (id: string, state: InboxState): Promise<void> => {
await client.patchInboxItem(id, { state });
await refresh();
};
return {
items,
refresh,
snooze: (id) => patch(id, 'SNOOZED'),
done: (id) => patch(id, 'DONE'),
reopen: (id) => patch(id, 'OPEN'),
};
}
+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"]
}
+9
View File
@@ -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'],
});
+18 -1
View File
@@ -1,5 +1,5 @@
import type { IngestInteractionRequest } from '@insignia/iios-contracts'; import type { IngestInteractionRequest } from '@insignia/iios-contracts';
import type { Message } from './types'; import type { Message, InboxItem, InboxState } from './types';
export interface RestConfig { export interface RestConfig {
serviceUrl: string; serviceUrl: string;
@@ -40,6 +40,23 @@ export class RestClient {
return (await r.json()) as Message; return (await r.json()) as Message;
} }
async listInboxItems(state?: InboxState): Promise<InboxItem[]> {
const q = state ? `?state=${encodeURIComponent(state)}` : '';
const r = await fetch(this.url(`/v1/inbox/items${q}`), { headers: this.headers() });
if (!r.ok) throw new Error(`listInboxItems ${r.status}`);
return (await r.json()) as InboxItem[];
}
async patchInboxItem(id: string, body: { state: InboxState; reason?: string }): Promise<InboxItem> {
const r = await fetch(this.url(`/v1/inbox/items/${id}`), {
method: 'PATCH',
headers: this.headers(),
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`patchInboxItem ${r.status}`);
return (await r.json()) as InboxItem;
}
async ingest(body: IngestInteractionRequest, idempotencyKey: string): Promise<unknown> { async ingest(body: IngestInteractionRequest, idempotencyKey: string): Promise<unknown> {
const r = await fetch(this.url('/v1/interactions/ingest'), { const r = await fetch(this.url('/v1/interactions/ingest'), {
method: 'POST', method: 'POST',
+17
View File
@@ -32,6 +32,23 @@ export interface MessageEvents {
typing: (e: TypingEvent) => void; typing: (e: TypingEvent) => void;
} }
export type InboxState = 'OPEN' | 'SNOOZED' | 'DONE' | 'ARCHIVED' | 'CANCELLED' | 'STALE';
export interface InboxItem {
id: string;
ownerActorId: string;
kind: string;
state: InboxState;
title: string;
summary?: string | null;
priority: string;
threadId?: string | null;
sourceInteractionId?: string | null;
traceId?: string | null;
createdAt: string;
updatedAt: string;
}
/** Minimal socket surface so the facade can be unit-tested with a fake. */ /** Minimal socket surface so the facade can be unit-tested with a fake. */
export interface SocketLike { export interface SocketLike {
on(event: string, handler: (...args: unknown[]) => void): unknown; on(event: string, handler: (...args: unknown[]) => void): unknown;
+19
View File
@@ -51,6 +51,25 @@ importers:
packages/iios-contracts: {} packages/iios-contracts: {}
packages/iios-inbox-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-kernel-client: packages/iios-kernel-client:
dependencies: dependencies:
'@insignia/iios-contracts': '@insignia/iios-contracts':
+1
View File
@@ -19,6 +19,7 @@ const ALLOWED = {
'@insignia/iios-kernel-client': ['@insignia/iios-contracts'], '@insignia/iios-kernel-client': ['@insignia/iios-contracts'],
'@insignia/iios-service': ['@insignia/iios-contracts', '@insignia/iios-testkit'], '@insignia/iios-service': ['@insignia/iios-contracts', '@insignia/iios-testkit'],
'@insignia/iios-message-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'], '@insignia/iios-message-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
'@insignia/iios-inbox-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
}; };
const KNOWN = Object.keys(ALLOWED); const KNOWN = Object.keys(ALLOWED);