@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.
- package/README.md +34 -0
- package/dist/audio/capture-upload.d.ts +116 -0
- package/dist/audio/capture-upload.js +347 -0
- package/dist/audio/clip-registry.d.ts +20 -0
- package/dist/audio/clip-registry.js +47 -0
- package/dist/audio/ddc-delegation.d.ts +18 -0
- package/dist/audio/ddc-delegation.js +64 -0
- package/dist/audio/mic.d.ts +1 -0
- package/dist/audio/mic.js +23 -0
- package/dist/audio/segment.d.ts +3 -0
- package/dist/audio/segment.js +24 -0
- package/dist/audio/wav.d.ts +2 -0
- package/dist/audio/wav.js +27 -0
- package/dist/bridge-wallet.d.ts +46 -0
- package/dist/bridge-wallet.js +80 -0
- package/dist/bridge.d.ts +6 -0
- package/dist/bridge.js +15 -0
- package/dist/build-tools.d.ts +4 -0
- package/dist/build-tools.js +12 -0
- package/dist/entry.d.ts +11 -0
- package/dist/entry.js +159 -0
- package/dist/host-bridge.d.ts +19 -0
- package/dist/host-bridge.js +214 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +21 -0
- package/dist/kinds/composite.d.ts +7 -0
- package/dist/kinds/composite.js +206 -0
- package/dist/kinds/conversation.d.ts +7 -0
- package/dist/kinds/conversation.js +667 -0
- package/dist/kinds/dashboard.d.ts +2 -0
- package/dist/kinds/dashboard.js +53 -0
- package/dist/kinds/list.d.ts +2 -0
- package/dist/kinds/list.js +22 -0
- package/dist/kinds/record.d.ts +2 -0
- package/dist/kinds/record.js +14 -0
- package/dist/kinds/submit.d.ts +6 -0
- package/dist/kinds/submit.js +481 -0
- package/dist/manifest.d.ts +32 -0
- package/dist/manifest.js +47 -0
- package/dist/mount.d.ts +19 -0
- package/dist/mount.js +48 -0
- package/dist/render-helpers.d.ts +9 -0
- package/dist/render-helpers.js +42 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +36 -0
- package/dist/standalone-wallet.d.ts +21 -0
- package/dist/standalone-wallet.js +102 -0
- package/dist/types.d.ts +245 -0
- package/dist/types.js +7 -0
- package/dist/validate-config.d.ts +2 -0
- package/dist/validate-config.js +74 -0
- package/dist/vault-client.d.ts +45 -0
- package/dist/vault-client.js +122 -0
- package/dist/widget-runtime.js +348 -0
- package/dist/widget.html +33 -0
- package/package.json +43 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// packages/widget-runtime/src/audio/mic.ts
|
|
2
|
+
// One place to acquire a microphone stream that captures the FULL acoustic scene.
|
|
3
|
+
//
|
|
4
|
+
// getUserMedia({ audio: true }) applies the browser's voice-call DSP by default:
|
|
5
|
+
// • noiseSuppression — treats music and a second/far speaker as "noise" → removes them
|
|
6
|
+
// • echoCancellation — drops audio that correlates with speaker output (far-end/system bleed)
|
|
7
|
+
// • autoGainControl — pumps levels up/down
|
|
8
|
+
// For interview / transcription capture we want everything the device hears, so
|
|
9
|
+
// we opt all three OFF. Some browsers reject the explicit constraints, so we fall
|
|
10
|
+
// back to plain { audio: true }.
|
|
11
|
+
const RAW_AUDIO = { echoCancellation: false, noiseSuppression: false, autoGainControl: false };
|
|
12
|
+
export async function requestMicStream() {
|
|
13
|
+
try {
|
|
14
|
+
return await navigator.mediaDevices.getUserMedia({ audio: RAW_AUDIO });
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const name = err?.name;
|
|
18
|
+
if (name === 'OverconstrainedError' || name === 'TypeError') {
|
|
19
|
+
return navigator.mediaDevices.getUserMedia({ audio: true });
|
|
20
|
+
}
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function segmentPcm(pcm: Float32Array, sampleRate: number, segmentSeconds: number): Float32Array[];
|
|
2
|
+
/** Decode an audio Blob to mono Float32 PCM resampled to 16 kHz (browser only). */
|
|
3
|
+
export declare function decodeToMono16k(blob: Blob): Promise<Float32Array>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// packages/widget-runtime/src/audio/segment.ts
|
|
2
|
+
export function segmentPcm(pcm, sampleRate, segmentSeconds) {
|
|
3
|
+
const per = sampleRate * segmentSeconds;
|
|
4
|
+
const out = [];
|
|
5
|
+
for (let i = 0; i < pcm.length; i += per)
|
|
6
|
+
out.push(pcm.subarray(i, Math.min(i + per, pcm.length)));
|
|
7
|
+
return out.length ? out : [pcm];
|
|
8
|
+
}
|
|
9
|
+
/** Decode an audio Blob to mono Float32 PCM resampled to 16 kHz (browser only). */
|
|
10
|
+
export async function decodeToMono16k(blob) {
|
|
11
|
+
const bytes = await blob.arrayBuffer();
|
|
12
|
+
const Ctx = (window.OfflineAudioContext || window.webkitOfflineAudioContext);
|
|
13
|
+
const tmp = new (window.AudioContext || window.webkitAudioContext)();
|
|
14
|
+
const decoded = await tmp.decodeAudioData(bytes.slice(0));
|
|
15
|
+
void tmp.close();
|
|
16
|
+
const durationFrames = Math.ceil(decoded.duration * 16000);
|
|
17
|
+
const off = new Ctx(1, durationFrames, 16000);
|
|
18
|
+
const src = off.createBufferSource();
|
|
19
|
+
src.buffer = decoded;
|
|
20
|
+
src.connect(off.destination);
|
|
21
|
+
src.start();
|
|
22
|
+
const rendered = await off.startRendering();
|
|
23
|
+
return rendered.getChannelData(0).slice();
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// packages/widget-runtime/src/audio/wav.ts
|
|
2
|
+
/** Encode mono Float32 PCM [-1,1] as a 16-bit PCM WAV ArrayBuffer. */
|
|
3
|
+
export function encodeWav(samples, sampleRate) {
|
|
4
|
+
const buf = new ArrayBuffer(44 + samples.length * 2);
|
|
5
|
+
const v = new DataView(buf);
|
|
6
|
+
const str = (off, s) => { for (let i = 0; i < s.length; i++)
|
|
7
|
+
v.setUint8(off + i, s.charCodeAt(i)); };
|
|
8
|
+
str(0, 'RIFF');
|
|
9
|
+
v.setUint32(4, 36 + samples.length * 2, true);
|
|
10
|
+
str(8, 'WAVE');
|
|
11
|
+
str(12, 'fmt ');
|
|
12
|
+
v.setUint32(16, 16, true);
|
|
13
|
+
v.setUint16(20, 1, true);
|
|
14
|
+
v.setUint16(22, 1, true);
|
|
15
|
+
v.setUint32(24, sampleRate, true);
|
|
16
|
+
v.setUint32(28, sampleRate * 2, true);
|
|
17
|
+
v.setUint16(32, 2, true);
|
|
18
|
+
v.setUint16(34, 16, true);
|
|
19
|
+
str(36, 'data');
|
|
20
|
+
v.setUint32(40, samples.length * 2, true);
|
|
21
|
+
let off = 44;
|
|
22
|
+
for (let i = 0; i < samples.length; i++, off += 2) {
|
|
23
|
+
const s = Math.max(-1, Math.min(1, samples[i]));
|
|
24
|
+
v.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
|
25
|
+
}
|
|
26
|
+
return buf;
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Signer } from '@cef-ai/vault-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Wallet shape required by @cef-ai/client-sdk's Vault constructor.
|
|
4
|
+
* sign/signRawBytes return hex WITHOUT a `0x` prefix.
|
|
5
|
+
*
|
|
6
|
+
* Formerly in embed-adapter.ts — moved here since the bridge is now the
|
|
7
|
+
* sole wallet source.
|
|
8
|
+
*/
|
|
9
|
+
export interface VaultSignedWallet {
|
|
10
|
+
type: 'ed25519' | 'sr25519';
|
|
11
|
+
address: string;
|
|
12
|
+
publicKey: string;
|
|
13
|
+
isReady: () => Promise<boolean>;
|
|
14
|
+
sign: (data: string) => Promise<string>;
|
|
15
|
+
signRawBytes: (bytes: Uint8Array) => Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build a vault-sdk `Signer` (`@cef-ai/signer`) from a bridge identity + sign
|
|
19
|
+
* function. vault-sdk 3.0.0 consumes a `Signer` directly (no `Wallet` adapter).
|
|
20
|
+
*
|
|
21
|
+
* @param pubkey Hex public key (with or without 0x). Decoded to the `publicKey`
|
|
22
|
+
* bytes the SDK reads for `X-Public-Key`.
|
|
23
|
+
* @param sigType Signature scheme reported by the host ('ed25519' | 'sr25519').
|
|
24
|
+
* @param signFn Raw-bytes signer from the host bridge. The optional `intent`
|
|
25
|
+
* is ignored — the host bridge signs raw bytes regardless.
|
|
26
|
+
*/
|
|
27
|
+
export declare function toVaultSdkSigner(pubkey: string, sigType: 'ed25519' | 'sr25519', signFn: (bytes: Uint8Array) => Promise<Uint8Array>): Signer;
|
|
28
|
+
/**
|
|
29
|
+
* Build a VaultSignedWallet (shape expected by @cef-ai/client-sdk's Vault)
|
|
30
|
+
* from a bridge identity + sign function.
|
|
31
|
+
*
|
|
32
|
+
* sign(data: string) encodes the string as UTF-8 bytes and signs them.
|
|
33
|
+
* signRawBytes signs the bytes directly.
|
|
34
|
+
* Both return hex WITHOUT `0x`.
|
|
35
|
+
*
|
|
36
|
+
* @param sigType Signature scheme reported by the host ('ed25519' | 'sr25519').
|
|
37
|
+
*/
|
|
38
|
+
export declare function toClientSdkWallet(pubkey: string, sigType: 'ed25519' | 'sr25519', signFn: (bytes: Uint8Array) => Promise<Uint8Array>): VaultSignedWallet;
|
|
39
|
+
/**
|
|
40
|
+
* Build the signerPair shape that `createVaultUploader` (and `mintDelegationToken`)
|
|
41
|
+
* accept, from a bridge pubkey + sign function.
|
|
42
|
+
*/
|
|
43
|
+
export declare function toUploaderSignerPair(pubkey: string, signFn: (bytes: Uint8Array) => Promise<Uint8Array>): {
|
|
44
|
+
publicKey: Uint8Array;
|
|
45
|
+
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
46
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// packages/widget-runtime/src/bridge-wallet.ts
|
|
2
|
+
//
|
|
3
|
+
// Adapters that expose the Vault host-bridge signing surface in the three
|
|
4
|
+
// shapes the rest of the runtime expects:
|
|
5
|
+
//
|
|
6
|
+
// toVaultSdkSigner → @cef-ai/signer `Signer` (type/address/publicKey/isReady/sign)
|
|
7
|
+
// toClientSdkWallet → @cef-ai/client-sdk VaultSignedWallet
|
|
8
|
+
// toUploaderSignerPair → { publicKey: Uint8Array; sign } for createVaultUploader
|
|
9
|
+
//
|
|
10
|
+
// All hex values in the client-sdk shape are WITHOUT a 0x prefix, matching
|
|
11
|
+
// the proven embed-wallet behaviour (see embed-adapter.ts history).
|
|
12
|
+
import { hexToU8a } from '@polkadot/util';
|
|
13
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
14
|
+
/** Encode bytes as lowercase hex WITHOUT a `0x` prefix. */
|
|
15
|
+
function bytesToHexNo0x(bytes) {
|
|
16
|
+
let s = '';
|
|
17
|
+
for (const b of bytes)
|
|
18
|
+
s += b.toString(16).padStart(2, '0');
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
// ── Adapters ──────────────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Build a vault-sdk `Signer` (`@cef-ai/signer`) from a bridge identity + sign
|
|
24
|
+
* function. vault-sdk 3.0.0 consumes a `Signer` directly (no `Wallet` adapter).
|
|
25
|
+
*
|
|
26
|
+
* @param pubkey Hex public key (with or without 0x). Decoded to the `publicKey`
|
|
27
|
+
* bytes the SDK reads for `X-Public-Key`.
|
|
28
|
+
* @param sigType Signature scheme reported by the host ('ed25519' | 'sr25519').
|
|
29
|
+
* @param signFn Raw-bytes signer from the host bridge. The optional `intent`
|
|
30
|
+
* is ignored — the host bridge signs raw bytes regardless.
|
|
31
|
+
*/
|
|
32
|
+
export function toVaultSdkSigner(pubkey, sigType, signFn) {
|
|
33
|
+
return {
|
|
34
|
+
type: sigType,
|
|
35
|
+
// `address` is set to the hex pubkey (NOT an SS58 address). vault-sdk's
|
|
36
|
+
// WalletAuthProvider reads `publicKey`/`type`, not `address`, to build the
|
|
37
|
+
// auth headers — matching the toClientSdkWallet note below.
|
|
38
|
+
address: pubkey,
|
|
39
|
+
publicKey: hexToU8a(pubkey),
|
|
40
|
+
isReady: async () => true,
|
|
41
|
+
sign: (bytes) => signFn(bytes),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build a VaultSignedWallet (shape expected by @cef-ai/client-sdk's Vault)
|
|
46
|
+
* from a bridge identity + sign function.
|
|
47
|
+
*
|
|
48
|
+
* sign(data: string) encodes the string as UTF-8 bytes and signs them.
|
|
49
|
+
* signRawBytes signs the bytes directly.
|
|
50
|
+
* Both return hex WITHOUT `0x`.
|
|
51
|
+
*
|
|
52
|
+
* @param sigType Signature scheme reported by the host ('ed25519' | 'sr25519').
|
|
53
|
+
*/
|
|
54
|
+
export function toClientSdkWallet(pubkey, sigType, signFn) {
|
|
55
|
+
return {
|
|
56
|
+
type: sigType,
|
|
57
|
+
// `address` is set to the hex pubkey (NOT an SS58 address). This is safe
|
|
58
|
+
// because current @cef-ai/client-sdk consumers read `publicKey`, not `address`.
|
|
59
|
+
address: pubkey,
|
|
60
|
+
publicKey: pubkey,
|
|
61
|
+
isReady: async () => true,
|
|
62
|
+
sign: async (data) => {
|
|
63
|
+
const bytes = new TextEncoder().encode(data);
|
|
64
|
+
return bytesToHexNo0x(await signFn(bytes));
|
|
65
|
+
},
|
|
66
|
+
signRawBytes: async (bytes) => {
|
|
67
|
+
return bytesToHexNo0x(await signFn(bytes));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build the signerPair shape that `createVaultUploader` (and `mintDelegationToken`)
|
|
73
|
+
* accept, from a bridge pubkey + sign function.
|
|
74
|
+
*/
|
|
75
|
+
export function toUploaderSignerPair(pubkey, signFn) {
|
|
76
|
+
return {
|
|
77
|
+
publicKey: hexToU8a(pubkey),
|
|
78
|
+
sign: signFn,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/dist/bridge.d.ts
ADDED
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const MARKER = '/* cere-widget-bridge */';
|
|
2
|
+
function escapeForScript(json) {
|
|
3
|
+
return json.replace(/<\/script/gi, '<\\/script');
|
|
4
|
+
}
|
|
5
|
+
function escapeAttr(value) {
|
|
6
|
+
return value.replace(/[&"<>]/g, (c) => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]));
|
|
7
|
+
}
|
|
8
|
+
export function injectWidgetBridge(html, input) {
|
|
9
|
+
if (html.includes(MARKER))
|
|
10
|
+
return html;
|
|
11
|
+
const manifestJson = escapeForScript(JSON.stringify(input.manifest, null, 2));
|
|
12
|
+
const head = `<script>${MARKER}\nwindow.WidgetSandbox = { manifest: ${manifestJson} };\n</script>\n` +
|
|
13
|
+
`<script src="${escapeAttr(input.runtimeUrl)}"></script>\n`;
|
|
14
|
+
return html.includes('</head>') ? html.replace('</head>', `${head}</head>`) : head + html;
|
|
15
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { buildWidgetManifest, type BuildManifestInput } from './manifest.js';
|
|
2
|
+
export { injectWidgetBridge, type BridgeInput } from './bridge.js';
|
|
3
|
+
export { validateConfig } from './validate-config.js';
|
|
4
|
+
export type { WidgetManifest, WidgetKind, WidgetConfig, RecordConfig, ListConfig, DashboardConfig, SubmitConfig, ConversationConfig, CustomPanelConfig, CompositePanel, CompositeConfig, } from './types.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// packages/widget-runtime/src/build-tools.ts
|
|
2
|
+
//
|
|
3
|
+
// Build-time surface for tooling (e.g. `@cef-ai/cli`) that needs to BUILD a
|
|
4
|
+
// manifest and inject the bridge WITHOUT loading the browser runtime. The main
|
|
5
|
+
// entry (`index.ts`) re-exports the interactive standalone wallet, which pulls
|
|
6
|
+
// in `@cere/embed-wallet` (a browser UMD bundle that references `self`) — that
|
|
7
|
+
// cannot be imported in Node. This entry re-exports only the pure,
|
|
8
|
+
// environment-agnostic pieces: the manifest builder, the HTML bridge injector,
|
|
9
|
+
// and the manifest/config types.
|
|
10
|
+
export { buildWidgetManifest } from './manifest.js';
|
|
11
|
+
export { injectWidgetBridge } from './bridge.js';
|
|
12
|
+
export { validateConfig } from './validate-config.js';
|
package/dist/entry.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WidgetManifest } from './types.js';
|
|
2
|
+
import { createWidgetRuntime } from './runtime.js';
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
WidgetSandbox?: {
|
|
6
|
+
manifest: WidgetManifest;
|
|
7
|
+
};
|
|
8
|
+
WidgetRuntime?: ReturnType<typeof createWidgetRuntime>;
|
|
9
|
+
registerCompositePanel?: (name: string, fn: (root: HTMLElement, runtime: unknown, deps?: unknown) => void | Promise<void>) => void;
|
|
10
|
+
}
|
|
11
|
+
}
|
package/dist/entry.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// packages/widget-runtime/src/entry.ts
|
|
2
|
+
import { VaultSDK } from '@cef-ai/vault-sdk';
|
|
3
|
+
import { createHostBridge } from './host-bridge.js';
|
|
4
|
+
import { toVaultSdkSigner, toUploaderSignerPair } from './bridge-wallet.js';
|
|
5
|
+
import { connectStandaloneWallet } from './standalone-wallet.js';
|
|
6
|
+
import { createVaultClient } from './vault-client.js';
|
|
7
|
+
import { createWidgetRuntime } from './runtime.js';
|
|
8
|
+
import { captureAndUploadAudio, createVaultUploader } from './audio/capture-upload.js';
|
|
9
|
+
import { decodeToMono16k } from './audio/segment.js';
|
|
10
|
+
import { registerCompositePanel } from './kinds/composite.js';
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` once the DOM is parsed. The runtime <script> is injected into
|
|
13
|
+
* <head>, so boot() executes BEFORE <body>/#app is parsed — any
|
|
14
|
+
* document.getElementById('app') at boot time would be null.
|
|
15
|
+
*/
|
|
16
|
+
function onDomReady(fn) {
|
|
17
|
+
if (typeof document !== 'undefined' && document.readyState === 'loading') {
|
|
18
|
+
document.addEventListener('DOMContentLoaded', fn, { once: true });
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
fn();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function boot() {
|
|
25
|
+
const manifest = window.WidgetSandbox?.manifest;
|
|
26
|
+
if (!manifest)
|
|
27
|
+
return;
|
|
28
|
+
const bridge = createHostBridge();
|
|
29
|
+
// ── Multi-source identity ─────────────────────────────────────────────────
|
|
30
|
+
// Tier 1: the Vault host iframes us → use the postMessage sign-bridge.
|
|
31
|
+
// Tier 2/3: no responding host (top-level link, or the bridge times out
|
|
32
|
+
// returning null) → connect an interactive Cere embed-wallet built from
|
|
33
|
+
// `manifest.wallet.{appId,env}` (opens the wallet UI). Both paths yield a
|
|
34
|
+
// pubkey + raw-bytes signer that feed the SAME vault-sdk client.
|
|
35
|
+
//
|
|
36
|
+
// Memoized so the (possibly interactive) resolution runs at most once — the
|
|
37
|
+
// wallet popup must not open on every query/connect/upload.
|
|
38
|
+
let identityP = null;
|
|
39
|
+
const resolveSigningIdentity = () => {
|
|
40
|
+
if (identityP)
|
|
41
|
+
return identityP;
|
|
42
|
+
identityP = (async () => {
|
|
43
|
+
const hostId = await bridge.requestIdentity();
|
|
44
|
+
if (hostId) {
|
|
45
|
+
return {
|
|
46
|
+
source: 'host',
|
|
47
|
+
publicKey: hostId.pubkey,
|
|
48
|
+
sigType: hostId.sigType,
|
|
49
|
+
sign: (bytes) => bridge.sign(bytes),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// No host → interactive wallet (standalone / direct-link boot).
|
|
53
|
+
const wallet = await connectStandaloneWallet(manifest.wallet);
|
|
54
|
+
return {
|
|
55
|
+
source: 'wallet',
|
|
56
|
+
publicKey: wallet.pubkey(),
|
|
57
|
+
sigType: wallet.sigType,
|
|
58
|
+
sign: (bytes) => wallet.signRawBytes(bytes),
|
|
59
|
+
};
|
|
60
|
+
})().catch((err) => {
|
|
61
|
+
identityP = null; // let a later action retry (e.g. user reopens the wallet)
|
|
62
|
+
throw err;
|
|
63
|
+
});
|
|
64
|
+
return identityP;
|
|
65
|
+
};
|
|
66
|
+
const deps = {
|
|
67
|
+
resolveIdentity: async () => {
|
|
68
|
+
const id = await resolveSigningIdentity();
|
|
69
|
+
return { source: id.source, publicKey: id.publicKey, sigType: id.sigType };
|
|
70
|
+
},
|
|
71
|
+
buildVaultClient: (m, id) => {
|
|
72
|
+
const sigType = id.sigType ?? 'ed25519';
|
|
73
|
+
// Defer to the memoized identity for signing — resolveIdentity() has
|
|
74
|
+
// already run by the time buildVaultClient is called, so this resolves
|
|
75
|
+
// to the same (host or wallet) signer without a second handshake/popup.
|
|
76
|
+
const signFn = (bytes) => resolveSigningIdentity().then((rid) => rid.sign(bytes));
|
|
77
|
+
const vaultSdkSigner = toVaultSdkSigner(id.publicKey, sigType, signFn);
|
|
78
|
+
// vault-sdk SDK instance: used for vault.ensure(), cubby reads, publish,
|
|
79
|
+
// connectAgent, agentStatus — the single SDK for the whole runtime.
|
|
80
|
+
const makeSdk = () => new VaultSDK({
|
|
81
|
+
endpoint: m.endpoints.vault,
|
|
82
|
+
garEndpoint: m.endpoints.gar,
|
|
83
|
+
marketplaceEndpoint: m.endpoints.marketplace,
|
|
84
|
+
s3GatewayAuthInfoUrl: m.endpoints.s3GatewayAuthInfo,
|
|
85
|
+
signer: vaultSdkSigner,
|
|
86
|
+
});
|
|
87
|
+
return createVaultClient(m, makeSdk);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
// Build the mount deps (audio uploader) for config-driven audio kinds.
|
|
91
|
+
// The uploader resolves its identity lazily via the shared (memoized)
|
|
92
|
+
// resolver on the first upload — it never blocks WidgetRuntime exposure and
|
|
93
|
+
// reuses the same host-or-wallet signer as the vault client.
|
|
94
|
+
let mountDeps;
|
|
95
|
+
if (manifest.kind === 'submit' || manifest.kind === 'conversation' || manifest.kind === 'composite') {
|
|
96
|
+
// Memoize: build uploader once, reuse across multiple uploads in same session.
|
|
97
|
+
let uploaderP = null;
|
|
98
|
+
const getUploader = () => {
|
|
99
|
+
if (uploaderP)
|
|
100
|
+
return uploaderP;
|
|
101
|
+
uploaderP = (async () => {
|
|
102
|
+
const id = await resolveSigningIdentity();
|
|
103
|
+
const signerPair = toUploaderSignerPair(id.publicKey, id.sign);
|
|
104
|
+
const vaultSdkSigner = toVaultSdkSigner(id.publicKey, id.sigType, id.sign);
|
|
105
|
+
const sdk = new VaultSDK({
|
|
106
|
+
endpoint: manifest.endpoints.vault,
|
|
107
|
+
garEndpoint: manifest.endpoints.gar,
|
|
108
|
+
marketplaceEndpoint: manifest.endpoints.marketplace,
|
|
109
|
+
s3GatewayAuthInfoUrl: manifest.endpoints.s3GatewayAuthInfo,
|
|
110
|
+
signer: vaultSdkSigner,
|
|
111
|
+
});
|
|
112
|
+
const ensuredVault = await sdk.vault.ensure({ onboard: false });
|
|
113
|
+
return createVaultUploader({
|
|
114
|
+
signerPair,
|
|
115
|
+
sigType: id.sigType,
|
|
116
|
+
gatewayUrl: manifest.endpoints.s3GatewayAuthInfo.replace(/\/auth\/info\/?$/, ''),
|
|
117
|
+
bucketId: ensuredVault.bucketId,
|
|
118
|
+
region: ensuredVault.region,
|
|
119
|
+
});
|
|
120
|
+
})().catch((err) => { uploaderP = null; throw err; });
|
|
121
|
+
return uploaderP;
|
|
122
|
+
};
|
|
123
|
+
const uploadAudio = (blob, opts) => getUploader().then((upload) => captureAndUploadAudio({
|
|
124
|
+
blob,
|
|
125
|
+
segmentSeconds: opts?.segmentSeconds,
|
|
126
|
+
softCapSeconds: opts?.softCapSeconds,
|
|
127
|
+
onProgress: opts?.onProgress,
|
|
128
|
+
onWarn: opts?.onWarn,
|
|
129
|
+
}, { decode: decodeToMono16k, upload, sampleRate: 16000 }));
|
|
130
|
+
mountDeps = { uploadAudio };
|
|
131
|
+
}
|
|
132
|
+
// Expose WidgetRuntime SYNCHRONOUSLY at boot. Custom widgets read
|
|
133
|
+
// window.WidgetRuntime in their own inline boot() at page load and would
|
|
134
|
+
// otherwise find it undefined (race) and render a fallback. The identity
|
|
135
|
+
// resolution runs lazily inside createWidgetRuntime's resolveIdentity on the
|
|
136
|
+
// first query/publish/connect (host handshake or interactive wallet).
|
|
137
|
+
window.WidgetRuntime = createWidgetRuntime(manifest, deps);
|
|
138
|
+
// Expose the composite panel registry SYNCHRONOUSLY so a host app bundle
|
|
139
|
+
// (loaded in <head> as a classic <script> right after widget-runtime.js) can
|
|
140
|
+
// call registerCompositePanel before the deferred mountKind runs. A dynamic
|
|
141
|
+
// import would settle on a later microtask and race that synchronous call.
|
|
142
|
+
// The static import is safe: entry.ts is a leaf importer, so it does not add
|
|
143
|
+
// to the existing composite↔mount module cycle (both export only functions).
|
|
144
|
+
window.registerCompositePanel = registerCompositePanel;
|
|
145
|
+
// Config-driven kinds: mount automatically. Custom widgets (custom/no kind)
|
|
146
|
+
// self-boot and now find window.WidgetRuntime present.
|
|
147
|
+
if (manifest.kind && manifest.kind !== 'custom' && manifest.config) {
|
|
148
|
+
// Defer until the DOM is ready: the runtime <script> is in <head>, so
|
|
149
|
+
// #app isn't parsed yet at boot. (Custom widgets self-boot from a <body>
|
|
150
|
+
// script and are unaffected.)
|
|
151
|
+
onDomReady(() => {
|
|
152
|
+
const app = document.getElementById('app');
|
|
153
|
+
if (app) {
|
|
154
|
+
void import('./mount.js').then(({ mountKind }) => mountKind(app, manifest.config, window.WidgetRuntime, mountDeps));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
boot();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const IDENTITY_REQUEST_TYPE = "cef-widget:identity-request";
|
|
2
|
+
export declare const IDENTITY_RESPONSE_TYPE = "cef-widget:identity-response";
|
|
3
|
+
export declare const SIGN_REQUEST_TYPE = "cef-widget:sign-request";
|
|
4
|
+
export declare const SIGN_RESPONSE_TYPE = "cef-widget:sign-response";
|
|
5
|
+
export interface HostBridge {
|
|
6
|
+
requestIdentity: () => Promise<{
|
|
7
|
+
pubkey: string;
|
|
8
|
+
sigType: 'ed25519' | 'sr25519';
|
|
9
|
+
} | null>;
|
|
10
|
+
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns the host bridge client. The bridge is stateless across calls
|
|
14
|
+
* (identity is cached internally after the first successful handshake).
|
|
15
|
+
* Safe to call multiple times — returns the same singleton behaviour.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createHostBridge(): HostBridge;
|
|
18
|
+
/** Reset module-level state (for tests only). */
|
|
19
|
+
export declare function _resetBridgeForTest(): void;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// packages/widget-runtime/src/host-bridge.ts
|
|
2
|
+
//
|
|
3
|
+
// postMessage client that talks to the Vault host's widget-sign-bridge.
|
|
4
|
+
// Protocol types MUST match the host repo exactly (protocol.ts in vault):
|
|
5
|
+
// cef-widget:identity-request / cef-widget:identity-response
|
|
6
|
+
// cef-widget:sign-request / cef-widget:sign-response
|
|
7
|
+
//
|
|
8
|
+
// Wire bytes are always number[] (not Uint8Array) — postMessage does not
|
|
9
|
+
// structured-clone Uint8Array reliably across sandboxed origin pairs.
|
|
10
|
+
export const IDENTITY_REQUEST_TYPE = 'cef-widget:identity-request';
|
|
11
|
+
export const IDENTITY_RESPONSE_TYPE = 'cef-widget:identity-response';
|
|
12
|
+
export const SIGN_REQUEST_TYPE = 'cef-widget:sign-request';
|
|
13
|
+
export const SIGN_RESPONSE_TYPE = 'cef-widget:sign-response';
|
|
14
|
+
/** Singleton message listener + pending-request map; set up once on first use. */
|
|
15
|
+
let listenerAttached = false;
|
|
16
|
+
const pending = new Map();
|
|
17
|
+
function ensureListener() {
|
|
18
|
+
if (listenerAttached)
|
|
19
|
+
return;
|
|
20
|
+
listenerAttached = true;
|
|
21
|
+
window.addEventListener('message', (event) => {
|
|
22
|
+
// Security: only accept replies from window.parent.
|
|
23
|
+
if (event.source !== window.parent)
|
|
24
|
+
return;
|
|
25
|
+
const data = event.data;
|
|
26
|
+
if (!data || typeof data !== 'object')
|
|
27
|
+
return;
|
|
28
|
+
const { type, requestId } = data;
|
|
29
|
+
if (typeof requestId !== 'string')
|
|
30
|
+
return;
|
|
31
|
+
if (type !== IDENTITY_RESPONSE_TYPE &&
|
|
32
|
+
type !== SIGN_RESPONSE_TYPE)
|
|
33
|
+
return;
|
|
34
|
+
const entry = pending.get(requestId);
|
|
35
|
+
if (entry) {
|
|
36
|
+
pending.delete(requestId);
|
|
37
|
+
entry.resolve(data);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function generateId() {
|
|
42
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
43
|
+
}
|
|
44
|
+
/** Identity result cached after first successful handshake. */
|
|
45
|
+
let cachedIdentity = undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Request the connected-wallet identity from the Vault host.
|
|
48
|
+
*
|
|
49
|
+
* Returns null when:
|
|
50
|
+
* - Not running inside an iframe (window.parent === window).
|
|
51
|
+
* - Host doesn't reply within ~8 s (attach-vs-load race exhausted).
|
|
52
|
+
* - Host replies with an error on every attempt.
|
|
53
|
+
*
|
|
54
|
+
* Resolves immediately (from cache) on subsequent calls after a success.
|
|
55
|
+
*/
|
|
56
|
+
async function requestIdentity() {
|
|
57
|
+
// Not inside an iframe.
|
|
58
|
+
if (window.parent === window)
|
|
59
|
+
return null;
|
|
60
|
+
// Return cached value if we already succeeded.
|
|
61
|
+
if (cachedIdentity !== undefined)
|
|
62
|
+
return cachedIdentity;
|
|
63
|
+
ensureListener();
|
|
64
|
+
const RETRY_INTERVAL_MS = 400;
|
|
65
|
+
const TOTAL_TIMEOUT_MS = 8000;
|
|
66
|
+
const deadline = Date.now() + TOTAL_TIMEOUT_MS;
|
|
67
|
+
return new Promise((outerResolve) => {
|
|
68
|
+
let settled = false;
|
|
69
|
+
let retryTimer;
|
|
70
|
+
let guardTimer;
|
|
71
|
+
let currentRequestId;
|
|
72
|
+
/** Clear both the retry timer and the outer deadline guard. */
|
|
73
|
+
function clearTimers() {
|
|
74
|
+
if (retryTimer !== undefined) {
|
|
75
|
+
clearTimeout(retryTimer);
|
|
76
|
+
retryTimer = undefined;
|
|
77
|
+
}
|
|
78
|
+
if (guardTimer !== undefined) {
|
|
79
|
+
clearTimeout(guardTimer);
|
|
80
|
+
guardTimer = undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function attempt() {
|
|
84
|
+
if (settled)
|
|
85
|
+
return;
|
|
86
|
+
// Remove old pending entry (the host reply arrives on a per-requestId
|
|
87
|
+
// basis; stale IDs just get GC'd from the map when we replace them).
|
|
88
|
+
if (currentRequestId)
|
|
89
|
+
pending.delete(currentRequestId);
|
|
90
|
+
currentRequestId = generateId();
|
|
91
|
+
pending.set(currentRequestId, {
|
|
92
|
+
resolve: (data) => {
|
|
93
|
+
if (settled)
|
|
94
|
+
return;
|
|
95
|
+
const d = data;
|
|
96
|
+
if (d.error || !d.pubkey) {
|
|
97
|
+
// Error response — keep retrying until deadline.
|
|
98
|
+
scheduleRetryOrTimeout();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
settled = true;
|
|
102
|
+
clearTimers();
|
|
103
|
+
const result = { pubkey: d.pubkey, sigType: (d.sigType ?? 'ed25519') };
|
|
104
|
+
cachedIdentity = result;
|
|
105
|
+
outerResolve(result);
|
|
106
|
+
},
|
|
107
|
+
reject: () => {
|
|
108
|
+
if (settled)
|
|
109
|
+
return;
|
|
110
|
+
scheduleRetryOrTimeout();
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
// targetOrigin '*' — same trust assumption as the sign request (see the
|
|
114
|
+
// note below): host origin unknown to the widget; replies are source-gated.
|
|
115
|
+
window.parent.postMessage({ type: IDENTITY_REQUEST_TYPE, requestId: currentRequestId }, '*');
|
|
116
|
+
// Schedule an automatic retry in case no reply arrives within the interval.
|
|
117
|
+
scheduleRetryOrTimeout();
|
|
118
|
+
}
|
|
119
|
+
function scheduleRetryOrTimeout() {
|
|
120
|
+
if (settled)
|
|
121
|
+
return;
|
|
122
|
+
// Cancel any existing retry timer before scheduling a fresh one.
|
|
123
|
+
if (retryTimer !== undefined) {
|
|
124
|
+
clearTimeout(retryTimer);
|
|
125
|
+
retryTimer = undefined;
|
|
126
|
+
}
|
|
127
|
+
const remaining = deadline - Date.now();
|
|
128
|
+
if (remaining <= 0) {
|
|
129
|
+
settled = true;
|
|
130
|
+
clearTimers();
|
|
131
|
+
if (currentRequestId)
|
|
132
|
+
pending.delete(currentRequestId);
|
|
133
|
+
cachedIdentity = null;
|
|
134
|
+
outerResolve(null);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
retryTimer = setTimeout(() => {
|
|
138
|
+
if (!settled)
|
|
139
|
+
attempt();
|
|
140
|
+
}, Math.min(RETRY_INTERVAL_MS, remaining));
|
|
141
|
+
}
|
|
142
|
+
attempt();
|
|
143
|
+
// Also set an outer deadline guard in case no reply ever arrives.
|
|
144
|
+
guardTimer = setTimeout(() => {
|
|
145
|
+
if (!settled) {
|
|
146
|
+
settled = true;
|
|
147
|
+
clearTimers();
|
|
148
|
+
if (currentRequestId)
|
|
149
|
+
pending.delete(currentRequestId);
|
|
150
|
+
cachedIdentity = null;
|
|
151
|
+
outerResolve(null);
|
|
152
|
+
}
|
|
153
|
+
}, TOTAL_TIMEOUT_MS + RETRY_INTERVAL_MS);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Ask the Vault host to sign `bytes` with the connected wallet.
|
|
158
|
+
*
|
|
159
|
+
* Rejects on:
|
|
160
|
+
* - `signature === null` (user declined).
|
|
161
|
+
* - `error` string in the response.
|
|
162
|
+
* - No response within ~60 s (modal closed / stale-reply race).
|
|
163
|
+
*/
|
|
164
|
+
async function sign(bytes) {
|
|
165
|
+
if (window.parent === window)
|
|
166
|
+
throw new Error('sign called outside of an iframe');
|
|
167
|
+
ensureListener();
|
|
168
|
+
const SIGN_TIMEOUT_MS = 60000;
|
|
169
|
+
const requestId = generateId();
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
pending.delete(requestId);
|
|
173
|
+
reject(new Error('widget signing timed out — the vault modal may have closed'));
|
|
174
|
+
}, SIGN_TIMEOUT_MS);
|
|
175
|
+
pending.set(requestId, {
|
|
176
|
+
resolve: (data) => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
const d = data;
|
|
179
|
+
if (d.error) {
|
|
180
|
+
reject(new Error(d.error));
|
|
181
|
+
}
|
|
182
|
+
else if (d.signature === null) {
|
|
183
|
+
reject(new Error('widget signing was declined'));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
resolve(Uint8Array.from(d.signature));
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
reject: (reason) => {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
reject(reason);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
// targetOrigin '*': the widget doesn't know the Vault host's origin (it's
|
|
195
|
+
// not in the manifest), and the host is trusted. Safe because replies are
|
|
196
|
+
// gated on `event.source === window.parent` — only the real parent can
|
|
197
|
+
// answer. If the manifest ever carries the host origin, pin it here.
|
|
198
|
+
window.parent.postMessage({ type: SIGN_REQUEST_TYPE, requestId, bytes: Array.from(bytes) }, '*');
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Returns the host bridge client. The bridge is stateless across calls
|
|
203
|
+
* (identity is cached internally after the first successful handshake).
|
|
204
|
+
* Safe to call multiple times — returns the same singleton behaviour.
|
|
205
|
+
*/
|
|
206
|
+
export function createHostBridge() {
|
|
207
|
+
return { requestIdentity, sign };
|
|
208
|
+
}
|
|
209
|
+
/** Reset module-level state (for tests only). */
|
|
210
|
+
export function _resetBridgeForTest() {
|
|
211
|
+
listenerAttached = false;
|
|
212
|
+
pending.clear();
|
|
213
|
+
cachedIdentity = undefined;
|
|
214
|
+
}
|