@cef-ai/widget-runtime 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +34 -0
  2. package/dist/audio/capture-upload.d.ts +116 -0
  3. package/dist/audio/capture-upload.js +347 -0
  4. package/dist/audio/clip-registry.d.ts +20 -0
  5. package/dist/audio/clip-registry.js +47 -0
  6. package/dist/audio/ddc-delegation.d.ts +18 -0
  7. package/dist/audio/ddc-delegation.js +64 -0
  8. package/dist/audio/mic.d.ts +1 -0
  9. package/dist/audio/mic.js +23 -0
  10. package/dist/audio/segment.d.ts +3 -0
  11. package/dist/audio/segment.js +24 -0
  12. package/dist/audio/wav.d.ts +2 -0
  13. package/dist/audio/wav.js +27 -0
  14. package/dist/bridge-wallet.d.ts +46 -0
  15. package/dist/bridge-wallet.js +80 -0
  16. package/dist/bridge.d.ts +6 -0
  17. package/dist/bridge.js +15 -0
  18. package/dist/build-tools.d.ts +4 -0
  19. package/dist/build-tools.js +12 -0
  20. package/dist/entry.d.ts +11 -0
  21. package/dist/entry.js +159 -0
  22. package/dist/host-bridge.d.ts +19 -0
  23. package/dist/host-bridge.js +214 -0
  24. package/dist/index.d.ts +25 -0
  25. package/dist/index.js +21 -0
  26. package/dist/kinds/composite.d.ts +7 -0
  27. package/dist/kinds/composite.js +206 -0
  28. package/dist/kinds/conversation.d.ts +7 -0
  29. package/dist/kinds/conversation.js +667 -0
  30. package/dist/kinds/dashboard.d.ts +2 -0
  31. package/dist/kinds/dashboard.js +53 -0
  32. package/dist/kinds/list.d.ts +2 -0
  33. package/dist/kinds/list.js +22 -0
  34. package/dist/kinds/record.d.ts +2 -0
  35. package/dist/kinds/record.js +14 -0
  36. package/dist/kinds/submit.d.ts +6 -0
  37. package/dist/kinds/submit.js +481 -0
  38. package/dist/manifest.d.ts +32 -0
  39. package/dist/manifest.js +47 -0
  40. package/dist/mount.d.ts +19 -0
  41. package/dist/mount.js +48 -0
  42. package/dist/render-helpers.d.ts +9 -0
  43. package/dist/render-helpers.js +42 -0
  44. package/dist/runtime.d.ts +12 -0
  45. package/dist/runtime.js +36 -0
  46. package/dist/standalone-wallet.d.ts +21 -0
  47. package/dist/standalone-wallet.js +102 -0
  48. package/dist/types.d.ts +245 -0
  49. package/dist/types.js +7 -0
  50. package/dist/validate-config.d.ts +2 -0
  51. package/dist/validate-config.js +74 -0
  52. package/dist/vault-client.d.ts +45 -0
  53. package/dist/vault-client.js +122 -0
  54. package/dist/widget-runtime.js +348 -0
  55. package/dist/widget.html +33 -0
  56. package/package.json +43 -0
@@ -0,0 +1,19 @@
1
+ import type { WidgetConfig, WidgetRuntimeApi } from './types.js';
2
+ export type UploadAudioFn = (blob: Blob, opts?: {
3
+ onProgress?: (done: number, total: number) => void;
4
+ onWarn?: (m: string) => void;
5
+ segmentSeconds?: number;
6
+ softCapSeconds?: number;
7
+ }) => Promise<string[]>;
8
+ export interface MountDeps {
9
+ uploadAudio?: UploadAudioFn;
10
+ /** When provided, the renderer tears down (stops timers/recording) on abort. */
11
+ signal?: AbortSignal;
12
+ /**
13
+ * Fired when a panel reaches a terminal "done" state (e.g. the intake
14
+ * conversation completes). The composite shell uses this to re-check the
15
+ * gate and unlock panels without a page reload.
16
+ */
17
+ onDone?: () => void;
18
+ }
19
+ export declare function mountKind(root: HTMLElement, config: WidgetConfig, runtime: WidgetRuntimeApi, deps?: MountDeps): Promise<void>;
package/dist/mount.js ADDED
@@ -0,0 +1,48 @@
1
+ import { esc } from './render-helpers.js';
2
+ import { renderRecord } from './kinds/record.js';
3
+ import { renderList } from './kinds/list.js';
4
+ import { renderDashboard } from './kinds/dashboard.js';
5
+ import { renderSubmit } from './kinds/submit.js';
6
+ import { renderConversation } from './kinds/conversation.js';
7
+ import { renderComposite } from './kinds/composite.js';
8
+ const RENDERERS = {
9
+ record: renderRecord,
10
+ list: renderList,
11
+ dashboard: renderDashboard,
12
+ submit: renderSubmit,
13
+ conversation: renderConversation,
14
+ composite: renderComposite,
15
+ };
16
+ export async function mountKind(root, config, runtime, deps) {
17
+ const renderer = RENDERERS[config.kind];
18
+ if (!renderer) {
19
+ const p = document.createElement('p');
20
+ p.className = 'error';
21
+ p.textContent = `Unsupported widget kind: ${esc(config.kind)}`;
22
+ root.replaceChildren(p);
23
+ return;
24
+ }
25
+ const render = async () => {
26
+ try {
27
+ await renderer(root, config, runtime, deps);
28
+ }
29
+ catch (err) {
30
+ if (err && err.name === 'AgentNotConnectedError') {
31
+ root.innerHTML = `<p class="muted">Connect your account to view this.</p><button id="cta">Connect</button>`;
32
+ root.querySelector('#cta').addEventListener('click', async () => {
33
+ root.textContent = 'Connecting…';
34
+ try {
35
+ await runtime.connectAgent();
36
+ await render();
37
+ }
38
+ catch (e) {
39
+ root.innerHTML = `<p class="error">${esc(e?.message ?? String(e))}</p>`;
40
+ }
41
+ });
42
+ return;
43
+ }
44
+ root.innerHTML = `<p class="error">Something went wrong: ${esc(err?.message ?? String(err))}</p>`;
45
+ }
46
+ };
47
+ await render();
48
+ }
@@ -0,0 +1,9 @@
1
+ import type { FieldFormat } from './types.js';
2
+ export declare function esc(v: unknown): string;
3
+ export declare function colIndex(columns: string[], name: string): number;
4
+ export declare function relTime(iso: string): string;
5
+ export declare function formatValue(value: unknown, format?: FieldFormat): string;
6
+ export declare function parseColumnSpec(spec: string): {
7
+ column: string;
8
+ format?: FieldFormat;
9
+ };
@@ -0,0 +1,42 @@
1
+ export function esc(v) {
2
+ return String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
3
+ }
4
+ export function colIndex(columns, name) {
5
+ const lower = columns.map((c) => String(c).toLowerCase());
6
+ return lower.indexOf(name.toLowerCase());
7
+ }
8
+ export function relTime(iso) {
9
+ const t = Date.parse(iso);
10
+ if (Number.isNaN(t))
11
+ return esc(iso);
12
+ const sec = Math.floor((Date.now() - t) / 1000);
13
+ if (sec < 60)
14
+ return 'just now';
15
+ const min = Math.floor(sec / 60);
16
+ if (min < 60)
17
+ return `${min}m ago`;
18
+ const hr = Math.floor(min / 60);
19
+ if (hr < 24)
20
+ return `${hr}h ago`;
21
+ return `${Math.floor(hr / 24)}d ago`;
22
+ }
23
+ export function formatValue(value, format) {
24
+ if (value == null || String(value).trim() === '')
25
+ return '—';
26
+ switch (format) {
27
+ case 'reltime': return relTime(String(value));
28
+ case 'date': {
29
+ const t = Date.parse(String(value));
30
+ return Number.isNaN(t) ? String(value) : new Date(t).toLocaleDateString();
31
+ }
32
+ case 'number': {
33
+ const n = Number(value);
34
+ return Number.isNaN(n) ? String(value) : String(n);
35
+ }
36
+ default: return String(value);
37
+ }
38
+ }
39
+ export function parseColumnSpec(spec) {
40
+ const [column, format] = spec.split(':');
41
+ return { column, format: format || undefined };
42
+ }
@@ -0,0 +1,12 @@
1
+ import type { WidgetManifest, WidgetRuntimeApi } from './types.js';
2
+ import type { VaultClient } from './vault-client.js';
3
+ export interface ResolvedRuntimeIdentity {
4
+ source: string;
5
+ publicKey: string;
6
+ sigType?: 'ed25519' | 'sr25519';
7
+ }
8
+ export interface RuntimeDeps {
9
+ resolveIdentity: (manifest: WidgetManifest) => Promise<ResolvedRuntimeIdentity>;
10
+ buildVaultClient: (manifest: WidgetManifest, identity: ResolvedRuntimeIdentity) => VaultClient;
11
+ }
12
+ export declare function createWidgetRuntime(manifest: WidgetManifest, deps: RuntimeDeps): WidgetRuntimeApi;
@@ -0,0 +1,36 @@
1
+ export function createWidgetRuntime(manifest, deps) {
2
+ let status = 'anon';
3
+ let publicKey = '';
4
+ let clientP;
5
+ const subs = new Set();
6
+ const emit = (s) => {
7
+ status = s;
8
+ const snapshot = { publicKey, status };
9
+ for (const cb of subs)
10
+ cb(snapshot);
11
+ };
12
+ function ensureClient() {
13
+ clientP ?? (clientP = (async () => {
14
+ emit('connecting');
15
+ const id = await deps.resolveIdentity(manifest);
16
+ publicKey = id.publicKey;
17
+ const client = deps.buildVaultClient(manifest, id);
18
+ emit('ready');
19
+ return client;
20
+ })().catch((err) => {
21
+ clientP = undefined;
22
+ emit('anon');
23
+ throw err;
24
+ }));
25
+ return clientP;
26
+ }
27
+ return {
28
+ async query(ref, params) { return (await ensureClient()).query(ref, params); },
29
+ async publish(type, payload, context) { return (await ensureClient()).publish(type, payload, context); },
30
+ async identity() { await ensureClient(); return { publicKey, status }; },
31
+ async connect() { await ensureClient(); return { publicKey, status }; },
32
+ async connectAgent() { await (await ensureClient()).connectAgent(); },
33
+ async agentStatus() { return (await ensureClient()).agentStatus(); },
34
+ onIdentityChange(cb) { subs.add(cb); return () => subs.delete(cb); },
35
+ };
36
+ }
@@ -0,0 +1,21 @@
1
+ /** Raw-bytes signer produced by a connected standalone wallet. */
2
+ export interface StandaloneWallet {
3
+ /** 0x-hex ed25519 public key. */
4
+ pubkey(): string;
5
+ /** Signature scheme — embed-wallet raw-signing is ed25519. */
6
+ sigType: 'ed25519';
7
+ /** Sign the given bytes; returns a 64-byte ed25519 signature. */
8
+ signRawBytes(bytes: Uint8Array): Promise<Uint8Array>;
9
+ }
10
+ /**
11
+ * Initialise + connect an interactive Cere embed-wallet and return a
12
+ * raw-bytes signer.
13
+ *
14
+ * Opens the wallet UI (modal) so the user can authenticate; resolves once the
15
+ * account is connected and the local signing keypair is ready. Rejects if the
16
+ * user declines the connection or the seed cannot be extracted.
17
+ */
18
+ export declare function connectStandaloneWallet(cfg: {
19
+ appId: string;
20
+ env: string;
21
+ }): Promise<StandaloneWallet>;
@@ -0,0 +1,102 @@
1
+ // packages/widget-runtime/src/standalone-wallet.ts
2
+ //
3
+ // Standalone (interactive-wallet) identity source. When a widget runs
4
+ // top-level — opened via a direct link, not iframed by the Cere Vault host —
5
+ // there is no postMessage sign-bridge. This module builds an interactive Cere
6
+ // embed-wallet from the manifest's `wallet.{appId,env}`, opens the wallet UI so
7
+ // the user can connect, and exposes a raw-bytes signer.
8
+ //
9
+ // The result is fed through the EXISTING `toVaultSdkSigner(pubkey, sigType,
10
+ // signFn)` adapter, so the rest of the runtime (vault.ensure, cubby reads,
11
+ // connect, publish) is identical to the embedded/host path.
12
+ //
13
+ // ── Why sign locally instead of `Signer.signMessage()` ──────────────────────
14
+ // `@cere/embed-wallet`'s `Signer.signMessage(hex)` does NOT produce a
15
+ // signature over the raw bytes encoded in the hex string — the wallet iframe
16
+ // wraps/encodes the payload before signing, so the resulting signature does
17
+ // not verify against the canonical bytes that GAR (and the DDC gateway) hash.
18
+ // To sign raw bytes correctly we extract the wallet's ed25519 seed once at
19
+ // connect time via the wallet's EIP-1193-style `provider.request({ method })`
20
+ // surface, rebuild the keypair in-process with `@polkadot/keyring`, and sign
21
+ // locally. The user sees the wallet popup once (at connect); signatures after
22
+ // that do not round-trip through the iframe. This mirrors the proven
23
+ // `EmbedWalletAdapter` used by the Cere chat app.
24
+ import { EmbedWallet } from '@cere/embed-wallet';
25
+ import { Keyring } from '@polkadot/keyring';
26
+ import { cryptoWaitReady } from '@polkadot/util-crypto';
27
+ import { hexToU8a, u8aToHex } from '@polkadot/util';
28
+ // Different wallet versions expose the private key under different method
29
+ // names, so we probe all three and take whichever returns a hex string of at
30
+ // least 64 chars. The first 32 bytes (64 hex chars) are the ed25519 seed.
31
+ const PRIVATE_KEY_METHODS = [
32
+ 'private_key',
33
+ 'ed25519_private_key',
34
+ 'eth_private_key',
35
+ ];
36
+ function seedFromSecret(raw) {
37
+ const hex = raw.trim().replace(/^0x/i, '');
38
+ if (!/^[0-9a-fA-F]{64,}$/.test(hex))
39
+ return null;
40
+ return '0x' + hex.slice(0, 64).toLowerCase();
41
+ }
42
+ async function extractSeedHex(provider) {
43
+ for (const method of PRIVATE_KEY_METHODS) {
44
+ try {
45
+ const result = await provider.request({ method });
46
+ if (typeof result !== 'string')
47
+ continue;
48
+ const seed = seedFromSecret(result);
49
+ if (seed)
50
+ return seed;
51
+ }
52
+ catch {
53
+ // Wallet may expose only one of these methods — keep probing.
54
+ }
55
+ }
56
+ throw new Error('standalone wallet: could not extract embed-wallet seed. Tried: '
57
+ + PRIVATE_KEY_METHODS.join(', '));
58
+ }
59
+ function ensure0xHex(s) {
60
+ return s.startsWith('0x') ? s : `0x${s}`;
61
+ }
62
+ /**
63
+ * Initialise + connect an interactive Cere embed-wallet and return a
64
+ * raw-bytes signer.
65
+ *
66
+ * Opens the wallet UI (modal) so the user can authenticate; resolves once the
67
+ * account is connected and the local signing keypair is ready. Rejects if the
68
+ * user declines the connection or the seed cannot be extracted.
69
+ */
70
+ export async function connectStandaloneWallet(cfg) {
71
+ await cryptoWaitReady();
72
+ const wallet = new EmbedWallet({
73
+ appId: cfg.appId,
74
+ env: cfg.env,
75
+ });
76
+ // Render the connect flow as an in-page modal rather than a redirect so the
77
+ // widget page is not navigated away from.
78
+ await wallet.init({ appId: cfg.appId, env: cfg.env, popupMode: 'modal' });
79
+ await wallet.connect();
80
+ const signer = wallet.getSigner({ type: 'ed25519' });
81
+ const account = await signer.getAccount();
82
+ const expectedPub = ensure0xHex(account.publicKey).toLowerCase();
83
+ // `EmbedWallet.provider` is a public EIP-1193-style provider exposing
84
+ // `request({ method })`. The cast narrows its `Promise<any>` return to the
85
+ // typed shape our extractor wants — no private surface is reached for.
86
+ const seed = await extractSeedHex(wallet.provider);
87
+ const keyring = new Keyring({ type: 'ed25519' });
88
+ const pair = keyring.addFromSeed(hexToU8a(seed));
89
+ const derivedPub = u8aToHex(pair.publicKey);
90
+ if (derivedPub !== expectedPub) {
91
+ // Fail closed: the local keypair we'd sign with doesn't match the pubkey we
92
+ // advertise via pubkey(), so any signature would be unverifiable against
93
+ // the published identity. Refuse rather than sign with a mismatched key.
94
+ throw new Error(`[widget-runtime] standalone wallet: derived pubkey (${derivedPub}) does not ` +
95
+ `match the wallet's pubkey (${expectedPub}) — refusing to sign.`);
96
+ }
97
+ return {
98
+ pubkey: () => expectedPub,
99
+ sigType: 'ed25519',
100
+ signRawBytes: async (bytes) => pair.sign(bytes),
101
+ };
102
+ }
@@ -0,0 +1,245 @@
1
+ export interface WidgetManifest {
2
+ schemaVersion: 1;
3
+ widgetId: string;
4
+ name: string;
5
+ agentId: string;
6
+ scope: string;
7
+ cubbyAlias: string;
8
+ wallet: {
9
+ appId: string;
10
+ env: string;
11
+ };
12
+ endpoints: {
13
+ vault: string;
14
+ gar: string;
15
+ marketplace: string;
16
+ s3GatewayAuthInfo: string;
17
+ rpc: string;
18
+ };
19
+ queries: Array<{
20
+ id: string;
21
+ label: string;
22
+ sql: string;
23
+ timeoutMs?: number;
24
+ }>;
25
+ events?: Array<{
26
+ type: string;
27
+ schemaRef?: string;
28
+ }>;
29
+ kind?: WidgetKind;
30
+ config?: WidgetConfig;
31
+ }
32
+ export type IdentityStatus = 'anon' | 'connecting' | 'ready';
33
+ export interface Identity {
34
+ publicKey: string;
35
+ status: IdentityStatus;
36
+ }
37
+ export interface QueryResult {
38
+ columns: string[];
39
+ rows: unknown[][];
40
+ meta: {
41
+ rowCount: number;
42
+ };
43
+ }
44
+ export interface WidgetRuntimeApi {
45
+ query(ref: string, params?: unknown[]): Promise<QueryResult>;
46
+ publish(type: string, payload: unknown, context?: string): Promise<{
47
+ eventId: string;
48
+ }>;
49
+ identity(): Promise<Identity>;
50
+ connect(): Promise<Identity>;
51
+ connectAgent(): Promise<void>;
52
+ agentStatus(): Promise<'connected' | 'disconnected'>;
53
+ onIdentityChange(cb: (id: Identity) => void): () => void;
54
+ }
55
+ export declare class AgentNotConnectedError extends Error {
56
+ readonly agentId: string;
57
+ constructor(agentId: string);
58
+ }
59
+ export type WidgetKind = 'record' | 'list' | 'dashboard' | 'submit' | 'conversation' | 'custom' | 'composite';
60
+ export type FieldFormat = 'text' | 'multiline' | 'date' | 'reltime' | 'number';
61
+ export interface RecordConfig {
62
+ kind: 'record';
63
+ query: string;
64
+ empty?: string;
65
+ fields: Array<{
66
+ label: string;
67
+ column: string;
68
+ format?: FieldFormat;
69
+ }>;
70
+ }
71
+ export interface ListConfig {
72
+ kind: 'list';
73
+ query: string;
74
+ empty?: string;
75
+ /** Values are column names, optionally `column:format`. */
76
+ item: {
77
+ title: string;
78
+ subtitle?: string;
79
+ meta?: string;
80
+ };
81
+ limit?: number;
82
+ }
83
+ export type PanelRender = 'metric' | 'bar' | 'table' | 'list';
84
+ export interface DashboardPanel {
85
+ title: string;
86
+ query: string;
87
+ render: PanelRender;
88
+ value?: string;
89
+ label?: string;
90
+ columns?: string[];
91
+ item?: {
92
+ title: string;
93
+ subtitle?: string;
94
+ };
95
+ }
96
+ export interface DashboardConfig {
97
+ kind: 'dashboard';
98
+ panels: DashboardPanel[];
99
+ }
100
+ export interface SubmitConfig {
101
+ kind: 'submit';
102
+ title?: string;
103
+ intro?: string;
104
+ /** Label for the primary submit button (default 'Submit'). */
105
+ submitLabel?: string;
106
+ /** In-progress copy shown while the submission is processed (default 'Processing…'). */
107
+ processingLabel?: string;
108
+ /** Prompt shown on the not-connected Connect CTA (default 'Connect your account to continue.'). */
109
+ connectPrompt?: string;
110
+ form?: Array<{
111
+ name: string;
112
+ label: string;
113
+ type: 'text' | 'number' | 'select';
114
+ options?: string[];
115
+ required?: boolean;
116
+ }>;
117
+ audio: {
118
+ mode: 'upload' | 'record' | 'both';
119
+ required?: boolean;
120
+ softCapSeconds?: number;
121
+ segmentSeconds?: number;
122
+ };
123
+ event: {
124
+ type: string;
125
+ audioField: string;
126
+ formEnvelope?: string;
127
+ };
128
+ result: {
129
+ query: string;
130
+ poll: {
131
+ intervalMs: number;
132
+ timeoutMs: number;
133
+ doneWhen: {
134
+ column: string;
135
+ equals: string;
136
+ };
137
+ };
138
+ render: 'message' | 'summary';
139
+ summary?: Array<{
140
+ label: string;
141
+ path: string;
142
+ }>;
143
+ };
144
+ }
145
+ export interface ConversationConfig {
146
+ kind: 'conversation';
147
+ /** Prompt shown on the not-connected Connect CTA (default 'Connect your account to continue.'). */
148
+ connectPrompt?: string;
149
+ startEvent?: {
150
+ type: string;
151
+ };
152
+ turnEvent: {
153
+ type: string;
154
+ audioField: string;
155
+ turnIdxField?: string;
156
+ };
157
+ confirmEvent?: {
158
+ type: string;
159
+ };
160
+ turnsQuery: string;
161
+ roleColumn: string;
162
+ textColumn: string;
163
+ turnIdxColumn: string;
164
+ /** Named query that returns session status by id (used with confirmWhen/doneWhen). */
165
+ statusQuery?: string;
166
+ /** Transition to awaiting-confirm state when this column/value matches. */
167
+ confirmWhen?: {
168
+ column: string;
169
+ equals: string;
170
+ };
171
+ /** Transition to done state when this column/value matches. */
172
+ doneWhen?: {
173
+ column: string;
174
+ equals: string;
175
+ };
176
+ poll: {
177
+ intervalMs: number;
178
+ timeoutMs: number;
179
+ };
180
+ audio: {
181
+ mode: 'record' | 'upload' | 'both';
182
+ segmentSeconds?: number;
183
+ };
184
+ }
185
+ /** A custom panel inside a composite: rendered by a renderer registered at boot. */
186
+ export interface CustomPanelConfig {
187
+ kind: 'custom';
188
+ /** Name the host bundle registered via registerCompositePanel(). */
189
+ renderer: string;
190
+ /** Optional query id the custom renderer reads (passed through; renderer decides params). */
191
+ query?: string;
192
+ }
193
+ export interface CompositePanel {
194
+ id: string;
195
+ label: string;
196
+ icon?: string;
197
+ group: string;
198
+ /** A panel body of any widget kind except a nested composite. */
199
+ config: Exclude<WidgetConfig, CompositeConfig>;
200
+ /** Locked until gating.unlockWhen is satisfied. Default false. */
201
+ gated?: boolean;
202
+ badgeWhen?: {
203
+ query: string;
204
+ column: string;
205
+ equals: string;
206
+ };
207
+ }
208
+ export interface CompositeConfig {
209
+ kind: 'composite';
210
+ groups: Array<{
211
+ id: string;
212
+ label: string;
213
+ }>;
214
+ panels: CompositePanel[];
215
+ /** Panel id shown once the user is set up (gating satisfied). */
216
+ defaultPanel: string;
217
+ gating: {
218
+ /** MUST be a parameterless query. */
219
+ statusQuery: string;
220
+ unlockWhen: {
221
+ column: string;
222
+ equals: string;
223
+ };
224
+ firstRunPanel: string;
225
+ hero?: {
226
+ title: string;
227
+ body: string;
228
+ cta: string;
229
+ };
230
+ };
231
+ menu?: Array<{
232
+ kind: 'event';
233
+ label: string;
234
+ event: {
235
+ type: string;
236
+ payload?: unknown;
237
+ };
238
+ confirm?: string;
239
+ } | {
240
+ kind: 'goto';
241
+ label: string;
242
+ panel: string;
243
+ }>;
244
+ }
245
+ export type WidgetConfig = RecordConfig | ListConfig | DashboardConfig | SubmitConfig | ConversationConfig | CustomPanelConfig | CompositeConfig;
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ export class AgentNotConnectedError extends Error {
2
+ constructor(agentId) {
3
+ super(`Agent ${agentId} is not connected for this user.`);
4
+ this.name = 'AgentNotConnectedError';
5
+ this.agentId = agentId;
6
+ }
7
+ }
@@ -0,0 +1,2 @@
1
+ import type { WidgetKind, WidgetConfig } from './types.js';
2
+ export declare function validateConfig(kind: WidgetKind, config: unknown): asserts config is WidgetConfig;
@@ -0,0 +1,74 @@
1
+ const PANEL_RENDERS = ['metric', 'bar', 'table', 'list'];
2
+ function assert(cond, msg) {
3
+ if (!cond)
4
+ throw new Error(`Invalid widget config: ${msg}`);
5
+ }
6
+ export function validateConfig(kind, config) {
7
+ assert(config && typeof config === 'object', 'config must be an object');
8
+ const c = config;
9
+ assert(c.kind === kind, `config.kind (${String(c.kind)}) must match widget kind (${kind})`);
10
+ switch (kind) {
11
+ case 'record':
12
+ assert(typeof c.query === 'string' && c.query, 'record.query is required');
13
+ assert(Array.isArray(c.fields) && c.fields.length > 0, 'record.fields must be a non-empty array');
14
+ break;
15
+ case 'list':
16
+ assert(typeof c.query === 'string' && c.query, 'list.query is required');
17
+ assert(c.item && typeof c.item.title === 'string', 'list.item.title is required');
18
+ break;
19
+ case 'dashboard':
20
+ assert(Array.isArray(c.panels) && c.panels.length > 0, 'dashboard.panels must be non-empty');
21
+ for (const p of c.panels) {
22
+ assert(typeof p.query === 'string' && p.query, 'each panel needs a query');
23
+ assert(PANEL_RENDERS.includes(p.render), `panel.render must be one of ${PANEL_RENDERS.join('|')}`);
24
+ }
25
+ break;
26
+ case 'submit': {
27
+ const ev = c.event;
28
+ assert(ev && typeof ev.type === 'string', 'submit.event.type is required');
29
+ assert(ev && typeof ev.audioField === 'string', 'submit.event.audioField is required');
30
+ assert(c.audio && typeof c.audio === 'object', 'submit.audio is required');
31
+ assert(c.result && typeof c.result === 'object', 'submit.result is required');
32
+ break;
33
+ }
34
+ case 'conversation': {
35
+ const te = c.turnEvent;
36
+ assert(te && typeof te.type === 'string', 'conversation.turnEvent.type is required');
37
+ assert(typeof c.turnsQuery === 'string' && c.turnsQuery, 'conversation.turnsQuery is required');
38
+ assert(typeof c.roleColumn === 'string' && typeof c.textColumn === 'string', 'conversation role/text columns required');
39
+ assert(typeof c.turnIdxColumn === 'string' && c.turnIdxColumn, 'conversation.turnIdxColumn is required');
40
+ const poll = c.poll;
41
+ assert(poll && typeof poll.intervalMs === 'number' && typeof poll.timeoutMs === 'number', 'conversation.poll.intervalMs/timeoutMs required');
42
+ break;
43
+ }
44
+ case 'composite': {
45
+ assert(Array.isArray(c.groups) && c.groups.length > 0, 'composite.groups must be non-empty');
46
+ assert(Array.isArray(c.panels) && c.panels.length > 0, 'composite.panels must be non-empty');
47
+ const groupIds = new Set(c.groups.map((g) => g.id));
48
+ const panelIds = new Set();
49
+ for (const p of c.panels) {
50
+ assert(typeof p.id === 'string' && p.id, 'each panel needs an id');
51
+ assert(typeof p.label === 'string' && p.label, `panel ${String(p.id)} needs a label`);
52
+ assert(groupIds.has(p.group), `panel ${String(p.id)} group '${String(p.group)}' is not a declared group`);
53
+ panelIds.add(p.id);
54
+ const pc = p.config;
55
+ assert(pc && typeof pc.kind === 'string', `panel ${String(p.id)} needs a config with a kind`);
56
+ if (pc.kind === 'custom') {
57
+ assert(typeof pc.renderer === 'string', `custom panel ${String(p.id)} needs a renderer name`);
58
+ }
59
+ else {
60
+ validateConfig(pc.kind, pc);
61
+ }
62
+ }
63
+ const gating = c.gating;
64
+ assert(gating && typeof gating.statusQuery === 'string', 'composite.gating.statusQuery is required');
65
+ assert(panelIds.has(c.defaultPanel), `composite.defaultPanel '${String(c.defaultPanel)}' is not a panel id`);
66
+ assert(panelIds.has(gating.firstRunPanel), `composite.gating.firstRunPanel '${String(gating.firstRunPanel)}' is not a panel id`);
67
+ break;
68
+ }
69
+ case 'custom':
70
+ break; // custom widgets validate themselves
71
+ default:
72
+ assert(false, `unknown kind ${String(kind)}`);
73
+ }
74
+ }
@@ -0,0 +1,45 @@
1
+ import { type WidgetManifest, type QueryResult } from './types.js';
2
+ export interface VaultSdkLike {
3
+ vault: {
4
+ ensure: (opts?: {
5
+ onboard?: boolean;
6
+ }) => Promise<VaultLike>;
7
+ };
8
+ }
9
+ interface VaultLike {
10
+ id: string;
11
+ agents: {
12
+ get: (id: string) => Promise<AgentHandleLike>;
13
+ connect: (input: {
14
+ agentId: string;
15
+ scope: string;
16
+ settings?: Record<string, unknown>;
17
+ }) => Promise<AgentHandleLike>;
18
+ };
19
+ scope: (name: string) => {
20
+ publish: (input: {
21
+ type: string;
22
+ context: string;
23
+ payload: unknown;
24
+ }) => Promise<{
25
+ eventId: string;
26
+ }>;
27
+ };
28
+ }
29
+ interface CubbyHandleLike {
30
+ query: (sql: string, params?: unknown[]) => Promise<unknown>;
31
+ }
32
+ interface AgentHandleLike {
33
+ status: string;
34
+ cubby: (alias: string) => CubbyHandleLike;
35
+ }
36
+ export interface VaultClient {
37
+ query(ref: string, params?: unknown[]): Promise<QueryResult>;
38
+ publish(type: string, payload: unknown, context?: string): Promise<{
39
+ eventId: string;
40
+ }>;
41
+ connectAgent(): Promise<void>;
42
+ agentStatus(): Promise<'connected' | 'disconnected'>;
43
+ }
44
+ export declare function createVaultClient(manifest: WidgetManifest, makeSdk: () => VaultSdkLike): VaultClient;
45
+ export {};