@ethisyscore/extension-runtime 1.33.0 → 1.35.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 (33) hide show
  1. package/dist/{bridge-client-UK3qcGoi.d.ts → bridge-client-DD99netz.d.ts} +2 -33
  2. package/dist/{bridge-client-CIThO7jZ.d.cts → bridge-client-DzRcKIJT.d.cts} +2 -33
  3. package/dist/bridge-envelopes-DA6vxbyb.d.cts +151 -0
  4. package/dist/bridge-envelopes-DA6vxbyb.d.ts +151 -0
  5. package/dist/host/index.cjs +76 -2
  6. package/dist/host/index.cjs.map +1 -1
  7. package/dist/host/index.d.cts +3 -3
  8. package/dist/host/index.d.ts +3 -3
  9. package/dist/host/index.js +76 -2
  10. package/dist/host/index.js.map +1 -1
  11. package/dist/mock-host/cli.cjs +87 -2
  12. package/dist/mock-host/cli.cjs.map +1 -1
  13. package/dist/mock-host/cli.d.cts +2 -2
  14. package/dist/mock-host/cli.d.ts +2 -2
  15. package/dist/mock-host/cli.js +87 -2
  16. package/dist/mock-host/cli.js.map +1 -1
  17. package/dist/mock-host/index.cjs +41 -1
  18. package/dist/mock-host/index.cjs.map +1 -1
  19. package/dist/mock-host/index.d.cts +19 -7
  20. package/dist/mock-host/index.d.ts +19 -7
  21. package/dist/mock-host/index.js +41 -1
  22. package/dist/mock-host/index.js.map +1 -1
  23. package/dist/plugin/index.cjs +15 -3
  24. package/dist/plugin/index.cjs.map +1 -1
  25. package/dist/plugin/index.d.cts +13 -5
  26. package/dist/plugin/index.d.ts +13 -5
  27. package/dist/plugin/index.js +15 -4
  28. package/dist/plugin/index.js.map +1 -1
  29. package/dist/{transport-BN9Mzn_m.d.cts → transport-CHa8ESUx.d.ts} +39 -4
  30. package/dist/{transport-Jfd9KXAh.d.ts → transport-DJE6ljme.d.cts} +39 -4
  31. package/package.json +1 -1
  32. package/dist/bridge-envelopes-BRKGSiSC.d.cts +0 -63
  33. package/dist/bridge-envelopes-BRKGSiSC.d.ts +0 -63
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/host/declarative/interpreter.ts","../../src/plugin/clientPush.ts","../../src/plugin/hostIdentity.ts","../../src/plugin/PluginRealtimeContext.ts","../../src/plugin/ExtensionRuntimeProvider.tsx","../../src/mock-host/InMemoryMcpTransport.ts","../../src/mock-host/DeclarativeMockHost.tsx","../../src/mock-host/InMemoryBridgeTransport.ts","../../src/plugin/BridgeClientContext.ts","../../src/mock-host/WorkerMockHost.tsx"],"names":["createElement","createContext","useMemo","jsx","jsxs"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createElement, type ReactElement, type ReactNode } from \"react\";\nimport type { ComponentRegistry } from \"./registry\";\nimport type { SduiNode } from \"./types\";\n\n/**\n * Walk a parsed SDUI tree and render it as a React element by looking up each\n * node's `type` in the supplied {@link ComponentRegistry}.\n *\n * The interpreter is purely structural:\n *\n * - It owns no UI styling, layout, or data fetching.\n * - It never reads `node.props` — props are forwarded opaquely to the host\n * component, which owns interpretation per primitive.\n * - It does not evaluate `node.bindings` — reactive rules are handled in a\n * separate task (E2.S2). For v1 the interpreter passes through the static\n * tree only.\n *\n * Each child is given a stable React `key` derived from its position so that\n * React's reconciler can identify list items across renders. The key is a\n * sibling-local index; the registry consumer is responsible for opting into a\n * stable identity if it has a domain-meaningful `props.key`.\n *\n * @throws Error when `node.type` is not present in the registry. This is the\n * fail-loud behaviour required by the closed v1 vocabulary — unknown\n * primitives must not silently degrade.\n */\nexport function interpret(node: SduiNode, registry: ComponentRegistry): ReactElement\n{\n const Component = registry[node.type];\n if (!Component)\n {\n throw new Error(`Unknown SDUI primitive: ${String(node.type)}`);\n }\n\n const children: ReactNode[] | undefined = node.children?.map(\n (child, index) => interpretChild(child, registry, index),\n );\n\n return createElement(Component, { props: node.props }, children);\n}\n\nfunction interpretChild(node: SduiNode, registry: ComponentRegistry, index: number): ReactElement\n{\n const Component = registry[node.type];\n if (!Component)\n {\n throw new Error(`Unknown SDUI primitive: ${String(node.type)}`);\n }\n\n const children: ReactNode[] | undefined = node.children?.map(\n (child, childIndex) => interpretChild(child, registry, childIndex),\n );\n\n return createElement(Component, { props: node.props, key: index }, children);\n}\n","import { createContext, useContext, useEffect, useRef } from \"react\";\n\n/**\n * A single client-push event delivered from the host to a plugin surface. The host\n * relays the plugin backend's `IClientPushPublisher` events over its realtime\n * channel (SignalR); the transport envelope's extension identity is bound by the\n * host at mount time, so the plugin sees only the event body.\n *\n * SCOPE + ORDERING: which channel/user/group an event concerns is carried INSIDE\n * `payloadJson` by the emitting plugin — the transport envelope intentionally has no\n * group field. Consumers therefore demultiplex + order by their own payload fields\n * (e.g. a per-channel sequence in the payload), NOT by {@link eventSequence}, which\n * is per-group at the host and would produce false gaps when multiple groups\n * multiplex over one connection.\n */\nexport interface ClientPushEvent\n{\n /** Plugin-defined discriminator, e.g. `\"chatMessageReceived\"`. */\n eventType: string;\n /** Raw JSON payload authored by the plugin backend. */\n payloadJson: string;\n /**\n * Host per-group monotonic sequence. Advisory only — do NOT use for\n * cross-group gap detection (see the scope note above).\n */\n eventSequence: number;\n}\n\nexport interface ClientPushSubscribeOptions\n{\n /**\n * Opaque group names to enrol in (e.g. `\"chat:channel:{id}\"`). The host\n * authorises each subscription via the plugin's `authorize-subscription` tool\n * and enforces org/extension isolation — a plugin cannot subscribe outside its\n * own extension + organisation.\n */\n groups: string[];\n /** Called for each delivered (non-resync) event for the subscribed groups. */\n onEvent: (event: ClientPushEvent) => void;\n /**\n * Called when the host signals a gap/resync for the subscribed groups (a\n * dropped-event backpressure signal, or a reconnect). The consumer should\n * re-fetch authoritative state (e.g. a delta/cold-load) rather than trusting\n * incremental events.\n */\n onResync?: () => void;\n}\n\n/**\n * Host-provided channel for realtime server-push. The channel is already scoped to\n * the mounted surface's extension + organisation (bound by the host from the trusted\n * mount descriptor — a plugin CANNOT widen it), so {@link subscribe} takes only\n * opaque group names and returns an unsubscribe function.\n */\nexport interface ClientPushChannel\n{\n subscribe(options: ClientPushSubscribeOptions): () => void;\n}\n\n/**\n * `null` = no host channel (standalone/mock, or a host that predates client-push) →\n * {@link useClientPushSubscription} is inert. Provided by\n * {@link ExtensionRuntimeProvider}'s optional `clientPush` prop.\n */\nexport const ClientPushContext = createContext<ClientPushChannel | null>(null);\n\nexport interface UseClientPushSubscriptionOptions\n{\n /** Opaque groups to subscribe. Changing the SET re-subscribes; identity/order changes alone do not. */\n groups: string[];\n onEvent: (event: ClientPushEvent) => void;\n onResync?: () => void;\n /** Gate the subscription (e.g. until an id is known). Default `true`. */\n enabled?: boolean;\n}\n\n/**\n * Subscribe a plugin surface to host client-push events for `groups`.\n *\n * Inert (no-op) when no host channel is present (standalone/mock), when `enabled` is\n * false, or when `groups` is empty. Re-subscribes when the group set changes and\n * unsubscribes on unmount. Callback identities are held in refs, so passing new\n * inline `onEvent`/`onResync` closures every render does NOT churn the subscription.\n */\nexport function useClientPushSubscription(options: UseClientPushSubscriptionOptions): void\n{\n const { groups, onEvent, onResync, enabled = true } = options;\n const channel = useContext(ClientPushContext);\n\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n const onResyncRef = useRef(onResync);\n onResyncRef.current = onResync;\n\n // Normalise (dedupe + sort) so the effect re-runs only when the group SET actually\n // changes — reordering the same groups, or passing a fresh array literal of the\n // same set each render, must NOT churn the subscription.\n const normalizedGroups = [...new Set(groups)].sort();\n // JSON-encode (not space-join) so group names containing a delimiter can't alias\n // distinct sets to the same key (e.g. [\"a b\"] vs [\"a\",\"b\"]).\n const groupsKey = JSON.stringify(normalizedGroups);\n\n useEffect(() =>\n {\n if (!channel || !enabled || normalizedGroups.length === 0)\n {\n return;\n }\n const unsubscribe = channel.subscribe({\n groups: normalizedGroups,\n onEvent: (event) => onEventRef.current(event),\n onResync: () => onResyncRef.current?.(),\n });\n return unsubscribe;\n // normalizedGroups is captured via groupsKey; callbacks via refs — intentionally excluded.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [channel, enabled, groupsKey]);\n}\n","/**\n * Host identity/permission seam for platform-react plugin surfaces (WI 5108, F-AUTH-SEAM).\n *\n * Mirrors the clientPush seam: the host binds a HostIdentity to the mounted surface and\n * provides it via ExtensionRuntimeProvider's `identity` prop; the plugin reads it with\n * `useHostIdentity`. Inert (returns null) when no host provides the context — standalone /\n * mock, or a host that predates this feature.\n *\n * SECURITY: this is presentation/UX data. The plugin BACKEND re-authorises every MCP call\n * from the server session; `permission` is NOT an authorization boundary. The host forwards\n * ONLY the caller's grant for the surface's own extension (least-privilege), never the full set.\n */\nimport { createContext, useContext } from \"react\";\n\n/** Current-user identity for a mounted plugin surface, bound by the host. */\nexport interface HostIdentityUser\n{\n id: string;\n firstName: string;\n lastName: string;\n fullName: string;\n isExternal: boolean;\n}\n\n/** One resolved permission grant (bitMask over the plugin's PermissionMask bits). */\nexport interface HostPermission\n{\n groupCode: string;\n bitMask: number;\n}\n\n/** Host-provided identity context for the mounted surface. */\nexport interface HostIdentity\n{\n /** null while host auth is still loading (see isLoading) OR when unauthenticated. */\n user: HostIdentityUser | null;\n /** true while the host's /auth/user resolution is in flight — disambiguates loading from unauthenticated. */\n isLoading: boolean;\n /** The mounted surface's OWN grant only, or null when the user has no grant for this extension. */\n permission: HostPermission | null;\n /** The mounted surface's own extension groupCode, host-bound from the trusted manifest. */\n extensionGroupCode: string;\n /**\n * The active organisation id for the mounted surface, or null while host auth is loading /\n * unauthenticated. Host-bound from the SPA's active-organisation context. Surfaces that scope\n * realtime subscriptions or org-keyed queries read this (e.g. the chat client-push gate); it is\n * NOT a security token — the plugin backend derives org from the server session independently.\n */\n organisationId: string | null;\n}\n\n/**\n * `null` = no host channel (standalone/mock, or a host that predates the identity seam) →\n * {@link useHostIdentity} returns null and the plugin falls back to its deny-by-default path.\n * Provided by {@link ExtensionRuntimeProvider}'s optional `identity` prop.\n */\nexport const HostIdentityContext = createContext<HostIdentity | null>(null);\n\n/** Returns the host identity for the mounted surface, or null when no host context is present. */\nexport function useHostIdentity(): HostIdentity | null\n{\n return useContext(HostIdentityContext);\n}\n","import { createContext, useContext } from \"react\";\n\n/**\n * A host-supplied realtime subscription source for a plugin.\n *\n * The SDK keeps this intentionally dumb — it only calls `source.subscribe`.\n * All SignalR wiring, extensionId filtering, and connection lifecycle management\n * live host-side (Task 6 in coreconnect-web). This lets the SDK be tested with\n * a simple fake source.\n */\nexport interface PluginRealtimeSource\n{\n /**\n * Subscribe to notifications whose `typeCode` matches the given value.\n *\n * @param typeCode The application-level event type code to filter on\n * (e.g. `\"HelpdeskTicketCreated\"`). Filtering by\n * extensionId is the host's responsibility.\n * @param handler Called with the raw notification payload whenever a\n * matching notification arrives.\n * @returns An unsubscribe function. Calling it removes this handler.\n */\n subscribe(typeCode: string, handler: (payload: unknown) => void): () => void;\n}\n\n/**\n * React context carrying the plugin's active {@link PluginRealtimeSource}.\n *\n * `null` is the explicit \"not provided\" sentinel — hooks must treat null as a\n * clean no-op (dev/mock/no-connection) rather than an error.\n *\n * Provided by {@link ExtensionRuntimeProvider} when the host passes a\n * `realtime` prop; consumed by `usePluginRealtimeSource()`.\n */\nexport const PluginRealtimeContext = createContext<PluginRealtimeSource | null>(null);\n\n/**\n * Returns the {@link PluginRealtimeSource} from context, or `null` when none\n * is wired (dev/mock environments, unit tests that only care about MCP).\n *\n * Hooks built on top of this (e.g. `usePluginRealtime` in `plugin-ui`) should\n * skip their subscription entirely when this returns `null`.\n */\nexport function usePluginRealtimeSource(): PluginRealtimeSource | null\n{\n return useContext(PluginRealtimeContext);\n}\n","import { createContext, useContext, useMemo, type ReactNode } from \"react\";\nimport type { McpTransport } from \"./transport\";\nimport { ClientPushContext, type ClientPushChannel } from \"./clientPush\";\nimport { HostIdentityContext, type HostIdentity } from \"./hostIdentity\";\nimport { PluginRealtimeContext, type PluginRealtimeSource } from \"./PluginRealtimeContext\";\n\n/**\n * React context carrying the {@link McpTransport} the plugin should use to\n * reach the host. `null` is the explicit \"not provided\" sentinel so the hooks\n * can disambiguate from a transport that was provided but is incidentally\n * falsy in some other dimension.\n */\nconst ExtensionRuntimeContext = createContext<McpTransport | null>(null);\n\nexport interface ExtensionRuntimeProviderProps\n{\n transport: McpTransport;\n /**\n * Optional host realtime channel consumed by {@link useClientPushSubscription}.\n * Absent (or `null`) in standalone/mock hosts and hosts that predate client-push,\n * in which case the hook is inert. The host binds this channel to the mounted\n * surface's trusted extension + organisation identity.\n */\n clientPush?: ClientPushChannel | null;\n /**\n * Optional host identity/permission context consumed by {@link useHostIdentity}.\n * Absent (or `null`) in standalone/mock hosts and hosts that predate this seam,\n * in which case the hook is inert. The host binds this to the mounted surface's\n * trusted extension identity and forwards only that extension's own grant.\n */\n identity?: HostIdentity | null;\n /**\n * Optional realtime subscription source supplied by the host.\n *\n * When provided, descendant components can call `usePluginRealtimeSource()`\n * to obtain it and subscribe to push notifications. When omitted (dev/mock\n * environments or plugins that don't need realtime), the context defaults\n * to `null` and consumers no-op cleanly.\n */\n realtime?: PluginRealtimeSource;\n children?: ReactNode;\n}\n\n/**\n * Wrap a plugin's React tree so descendant {@link useMcpResource} and\n * {@link useMcpTool} calls resolve a default transport without having to\n * thread it through every component.\n *\n * Hooks still accept a per-call `transport` override, which takes precedence\n * over the context value — useful for tests and for plugins that want to\n * shard work across multiple hosts.\n */\nexport function ExtensionRuntimeProvider({ transport, clientPush = null, identity = null, realtime, children }: ExtensionRuntimeProviderProps): ReactNode\n{\n // Memoise so swapping `children` doesn't churn the context identity.\n const value = useMemo(() => transport, [transport]);\n const push = useMemo(() => clientPush, [clientPush]);\n const id = useMemo(() => identity, [identity]);\n const realtimeValue = useMemo(() => realtime ?? null, [realtime]);\n return (\n <ExtensionRuntimeContext.Provider value={value}>\n <ClientPushContext.Provider value={push}>\n <HostIdentityContext.Provider value={id}>\n <PluginRealtimeContext.Provider value={realtimeValue}>\n {children}\n </PluginRealtimeContext.Provider>\n </HostIdentityContext.Provider>\n </ClientPushContext.Provider>\n </ExtensionRuntimeContext.Provider>\n );\n}\n\n/**\n * Internal helper used by the hooks. Returns the explicit override when\n * supplied, otherwise falls back to the context. Throws a deterministic\n * error if neither is available so misconfiguration fails loudly at the\n * first render rather than producing silent no-ops.\n */\nexport function useExtensionRuntimeTransport(override?: McpTransport): McpTransport\n{\n const fromContext = useContext(ExtensionRuntimeContext);\n const resolved = override ?? fromContext;\n if (!resolved)\n {\n throw new Error(\n \"No McpTransport available. Wrap your plugin in <ExtensionRuntimeProvider transport={...}> \"\n + \"or pass `transport` directly to the hook.\",\n );\n }\n return resolved;\n}\n","import type { McpTransport } from \"../plugin/transport\";\nimport type { SduiNode } from \"../host/declarative/types\";\n\n/**\n * Handler for a mocked tool invocation. The handler receives the request\n * payload supplied by the caller and may return synchronously or\n * asynchronously. The result is forwarded verbatim through\n * {@link InMemoryMcpTransport.invokeTool}.\n */\nexport type MockToolHandler = (args: unknown) => Promise<unknown> | unknown;\n\n/**\n * An in-memory {@link McpTransport} backed by a `{ resources, tools }` map.\n *\n * Used by {@link DeclarativeMockHost} so plugin authors can run their app\n * standalone for local development without a real host. The transport mirrors\n * the runtime contract exactly:\n *\n * - `getResource(uri)` resolves a `SduiNode` keyed by URI, or rejects with a\n * descriptive error if the URI is not registered.\n * - `invokeTool(name, args)` dispatches to a synchronous or async handler,\n * or rejects if the tool name is unknown.\n *\n * The transport intentionally does NOT honour the supplied `AbortSignal` —\n * mock handlers are synchronous from the caller's perspective and there is\n * no in-flight network call to abort. Hooks still work correctly because\n * they treat the `AbortSignal` as a one-way notification, not a contract.\n */\nexport class InMemoryMcpTransport implements McpTransport\n{\n private readonly resources: Record<string, SduiNode>;\n private readonly tools: Record<string, MockToolHandler>;\n\n public constructor(\n resources: Record<string, SduiNode>,\n tools: Record<string, MockToolHandler> = {},\n )\n {\n this.resources = resources;\n this.tools = tools;\n }\n\n public async getResource<T>(uri: string): Promise<{ uri: string; data: T }>\n {\n const data = this.resources[uri];\n if (data === undefined)\n {\n throw new Error(`Mock resource not found: ${uri}`);\n }\n return { uri, data: data as unknown as T };\n }\n\n public async invokeTool<TReq, TRes>(name: string, args: TReq): Promise<TRes>\n {\n const tool = this.tools[name];\n if (!tool)\n {\n throw new Error(`Mock tool not found: ${name}`);\n }\n const result = await tool(args);\n return result as TRes;\n }\n}\n","import { useMemo, type ReactElement, type ReactNode } from \"react\";\nimport { interpret } from \"../host/declarative/interpreter\";\nimport type { ComponentRegistry } from \"../host/declarative/registry\";\nimport type { SduiNode } from \"../host/declarative/types\";\nimport { ExtensionRuntimeProvider } from \"../plugin/ExtensionRuntimeProvider\";\nimport { InMemoryMcpTransport, type MockToolHandler } from \"./InMemoryMcpTransport\";\n\n/**\n * Props for {@link DeclarativeMockHost}.\n *\n * Plugin authors `npm link` the runtime and render `<DeclarativeMockHost>` in\n * their local dev app to exercise the same declarative pipeline the real host\n * uses, but backed by in-memory fakes instead of the platform.\n */\nexport interface DeclarativeMockHostProps\n{\n /**\n * In-memory resource map. Keys are MCP resource URIs; values are SDUI trees\n * that {@link interpret} will render against the supplied registry.\n */\n resources: Record<string, SduiNode>;\n\n /**\n * In-memory tool map. Keys are MCP tool names; values are handlers invoked\n * when a child component calls `useMcpTool(name).invoke(args)`.\n *\n * Handlers may be sync or async — the transport awaits the result before\n * forwarding it to the caller.\n */\n tools?: Record<string, MockToolHandler>;\n\n /**\n * URI of the resource rendered as the host's default tree. If the URI is\n * not present in `resources` the host renders the supplied `children`\n * instead — useful for stubs that exercise only tool invocations.\n */\n defaultResourceUri: string;\n\n /**\n * The same primitive → component registry the real host uses. Passed\n * verbatim to {@link interpret}; the mock host owns no UI of its own.\n */\n registry: ComponentRegistry;\n\n /**\n * Optional fallback content rendered when `defaultResourceUri` does not\n * resolve to a registered resource. Children also have access to the wired\n * transport via {@link ExtensionRuntimeProvider}, so they can invoke\n * mocked tools and resources directly through the React hooks.\n */\n children?: ReactNode;\n}\n\n/**\n * In-memory host for declarative (Contract A) plugin local-dev.\n *\n * Renders a plugin's SDUI resource against the supplied registry and wires an\n * {@link InMemoryMcpTransport} into context so descendant components that use\n * `useMcpResource` / `useMcpTool` resolve against the same fakes.\n *\n * The host is intentionally minimal: it does not simulate permissions, theme\n * propagation, or capability tokens. Its purpose is to exercise the\n * declarative pipeline end-to-end against deterministic in-memory data so\n * plugin authors can iterate without standing up the real platform.\n */\nexport function DeclarativeMockHost(props: DeclarativeMockHostProps): ReactElement\n{\n const { resources, tools, defaultResourceUri, registry, children } = props;\n\n // Memoise the transport so React doesn't churn the context identity every\n // render — re-rendering this host with stable inputs must not abort\n // in-flight hook calls.\n const transport = useMemo(\n () => new InMemoryMcpTransport(resources, tools ?? {}),\n [resources, tools],\n );\n\n const tree = resources[defaultResourceUri];\n const renderedTree: ReactNode = tree ? interpret(tree, registry) : children;\n\n return (\n <ExtensionRuntimeProvider transport={transport}>\n {renderedTree}\n {tree ? children : null}\n </ExtensionRuntimeProvider>\n );\n}\n","/**\n * In-realm bridge transport for plugin local-dev and contract testing.\n *\n * Calling `pushTheme(...)`, `pushLocale(...)`, etc. invokes the registered\n * subscriber callbacks **synchronously** — no serialisation, no port. This\n * lets Vitest + React Testing Library drive bridge state changes with `act()`\n * without a real MessageChannel.\n *\n * In production, the bridge client is `createPortBridgeClient` backed by a\n * real MessagePort. `InMemoryBridgeTransport` is the dev/test equivalent:\n * both expose the same `PortBridgeClient`-compatible subscriber API on the\n * consumer side, but `InMemoryBridgeTransport` also exposes the push-side\n * and the `onChromeRequest` handler for test assertions.\n */\nimport type { PortBridgeClient, ThemePayload, LocalePayload, DensityPayload, A11yPayload, NavPayload, SessionTokenPayload } from \"../plugin/bridge-client\";\n\ntype ChromeRequestHandler = (\n action: string,\n payload: Record<string, unknown>,\n) => Promise<unknown> | unknown;\n\nexport class InMemoryBridgeTransport implements PortBridgeClient {\n private _themeCb: ((p: ThemePayload) => void) | undefined;\n private _localeCb: ((p: LocalePayload) => void) | undefined;\n private _densityCb: ((p: DensityPayload) => void) | undefined;\n private _a11yCb: ((p: A11yPayload) => void) | undefined;\n private _navCb: ((p: NavPayload) => void) | undefined;\n private _tokenCb: ((p: SessionTokenPayload) => void) | undefined;\n private _chromeHandler: ChromeRequestHandler | undefined;\n\n // ── PortBridgeClient subscriber interface ──────────────────────────────────\n\n onTheme(cb: (p: ThemePayload) => void): void { this._themeCb = cb; }\n onLocale(cb: (p: LocalePayload) => void): void { this._localeCb = cb; }\n onDensity(cb: (p: DensityPayload) => void): void { this._densityCb = cb; }\n onA11y(cb: (p: A11yPayload) => void): void { this._a11yCb = cb; }\n onNav(cb: (p: NavPayload) => void): void { this._navCb = cb; }\n onSessionToken(cb: (p: SessionTokenPayload) => void): void { this._tokenCb = cb; }\n\n requestChrome(action: string, payload: Record<string, unknown>): Promise<unknown> {\n return this.simulateChromeRequest(action, payload);\n }\n\n announceA11y(_message: string, _politeness: \"polite\" | \"assertive\"): void {\n // No-op in the mock — tests assert via `onChromeRequest` or inspect DOM.\n }\n\n // ── Test / dev control surface ─────────────────────────────────────────────\n\n /** Push a theme update to the registered subscriber (synchronous). */\n pushTheme(payload: ThemePayload): void { this._themeCb?.(payload); }\n /** Push a locale update to the registered subscriber. */\n pushLocale(payload: LocalePayload): void { this._localeCb?.(payload); }\n /** Push a density update. */\n pushDensity(payload: DensityPayload): void { this._densityCb?.(payload); }\n /** Push a11y preference changes. */\n pushA11y(payload: A11yPayload): void { this._a11yCb?.(payload); }\n /** Push a nav state update. */\n pushNav(payload: NavPayload): void { this._navCb?.(payload); }\n /** Push a frontend-session token. */\n pushSessionToken(payload: SessionTokenPayload): void { this._tokenCb?.(payload); }\n\n /**\n * Register a handler for plugin→host chrome requests (toast, confirm, etc).\n * Called by `requestChrome` and by `simulateChromeRequest`.\n */\n onChromeRequest(handler: ChromeRequestHandler): void {\n this._chromeHandler = handler;\n }\n\n /**\n * Programmatically send a chrome request as if a plugin component called\n * `PortBridgeClient.requestChrome(...)`. Useful for test assertions.\n */\n async simulateChromeRequest(action: string, payload: Record<string, unknown>): Promise<unknown> {\n if (this._chromeHandler === undefined) {\n return null;\n }\n return this._chromeHandler(action, payload);\n }\n}\n","import { createContext, useContext } from \"react\";\nimport type { PortBridgeClient } from \"./bridge-client\";\n\n/**\n * React context carrying the plugin's active {@link PortBridgeClient}.\n * Provided by the host mount (WorkerMockHost in dev, real bridge in production)\n * and consumed by `useBridgeTheme`, `useBridgeLocale`, and the `plugin-ui` hooks.\n */\nexport const BridgeClientContext = createContext<PortBridgeClient | null>(null);\n\n/**\n * Returns the bridge client from context, throwing a clear error when missing.\n * Used by the `useBridge*` hooks to fail loudly on misconfiguration.\n */\nexport function useBridgeClient(): PortBridgeClient {\n const client = useContext(BridgeClientContext);\n if (client === null) {\n throw new Error(\n \"No PortBridgeClient available. Wrap your plugin in <BridgeClientProvider> \"\n + \"or ensure the host mount wires a BridgeClientContext.Provider.\",\n );\n }\n return client;\n}\n","/**\n * Mock host for plugins that use bridge hooks (`useBridgeTheme`,\n * `useBridgeLocale`, `useBridgeA11y`, etc.) during local-dev or contract\n * testing.\n *\n * Wraps {@link DeclarativeMockHost} and wires a {@link BridgeClientContext}\n * provider so any descendant bridge hook resolves against the supplied\n * `bridgeTransport` instead of throwing \"no bridge client available\".\n *\n * For tests where bridge state must be driven externally (push a new theme,\n * assert that a component re-renders), pass an {@link InMemoryBridgeTransport}\n * instance and call `transport.pushTheme(...)` wrapped in `act()`.\n */\nimport { type ReactElement } from \"react\";\nimport { DeclarativeMockHost, type DeclarativeMockHostProps } from \"./DeclarativeMockHost\";\nimport { BridgeClientContext } from \"../plugin/BridgeClientContext\";\nimport type { PortBridgeClient } from \"../plugin/bridge-client\";\n\nexport interface WorkerMockHostProps extends DeclarativeMockHostProps {\n /**\n * The bridge transport to wire into context. Pass an\n * {@link InMemoryBridgeTransport} for tests; pass a\n * `createPortBridgeClient(port)` instance for postMessage integration tests.\n */\n bridgeTransport: PortBridgeClient;\n}\n\n/**\n * Mock host that combines the SDUI declarative pipeline with bridge context.\n *\n * Rendering contract (same as DeclarativeMockHost):\n * - `defaultResourceUri` present in `resources` → renders the SDUI tree.\n * - URI absent → renders `children` instead (tool-invocation stubs, etc).\n *\n * Bridge contract:\n * - All `useBridgeTheme`, `useBridgeLocale`, etc. hooks in the subtree resolve\n * against `bridgeTransport`.\n */\nexport function WorkerMockHost(props: WorkerMockHostProps): ReactElement {\n const { bridgeTransport, ...declarativeProps } = props;\n\n return (\n <BridgeClientContext.Provider value={bridgeTransport}>\n <DeclarativeMockHost {...declarativeProps} />\n </BridgeClientContext.Provider>\n );\n}\n"]}
1
+ {"version":3,"sources":["../../src/host/declarative/interpreter.ts","../../src/plugin/clientPush.ts","../../src/plugin/hostIdentity.ts","../../src/plugin/PluginRealtimeContext.ts","../../src/plugin/ExtensionRuntimeProvider.tsx","../../src/mock-host/InMemoryMcpTransport.ts","../../src/mock-host/DeclarativeMockHost.tsx","../../src/mock-host/InMemoryBridgeTransport.ts","../../src/plugin/BridgeClientContext.ts","../../src/mock-host/WorkerMockHost.tsx"],"names":["createElement","createContext","useMemo","jsx","jsxs"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createElement, type ReactElement, type ReactNode } from \"react\";\nimport type { ComponentRegistry } from \"./registry\";\nimport type { SduiNode } from \"./types\";\n\n/**\n * Walk a parsed SDUI tree and render it as a React element by looking up each\n * node's `type` in the supplied {@link ComponentRegistry}.\n *\n * The interpreter is purely structural:\n *\n * - It owns no UI styling, layout, or data fetching.\n * - It never reads `node.props` — props are forwarded opaquely to the host\n * component, which owns interpretation per primitive.\n * - It does not evaluate `node.bindings` — reactive rules are handled in a\n * separate task (E2.S2). For v1 the interpreter passes through the static\n * tree only.\n *\n * Each child is given a stable React `key` derived from its position so that\n * React's reconciler can identify list items across renders. The key is a\n * sibling-local index; the registry consumer is responsible for opting into a\n * stable identity if it has a domain-meaningful `props.key`.\n *\n * @throws Error when `node.type` is not present in the registry. This is the\n * fail-loud behaviour required by the closed v1 vocabulary — unknown\n * primitives must not silently degrade.\n */\nexport function interpret(node: SduiNode, registry: ComponentRegistry): ReactElement\n{\n const Component = registry[node.type];\n if (!Component)\n {\n throw new Error(`Unknown SDUI primitive: ${String(node.type)}`);\n }\n\n const children: ReactNode[] | undefined = node.children?.map(\n (child, index) => interpretChild(child, registry, index),\n );\n\n return createElement(Component, { props: node.props }, children);\n}\n\nfunction interpretChild(node: SduiNode, registry: ComponentRegistry, index: number): ReactElement\n{\n const Component = registry[node.type];\n if (!Component)\n {\n throw new Error(`Unknown SDUI primitive: ${String(node.type)}`);\n }\n\n const children: ReactNode[] | undefined = node.children?.map(\n (child, childIndex) => interpretChild(child, registry, childIndex),\n );\n\n return createElement(Component, { props: node.props, key: index }, children);\n}\n","import { createContext, useContext, useEffect, useRef } from \"react\";\n\n/**\n * A single client-push event delivered from the host to a plugin surface. The host\n * relays the plugin backend's `IClientPushPublisher` events over its realtime\n * channel (SignalR); the transport envelope's extension identity is bound by the\n * host at mount time, so the plugin sees only the event body.\n *\n * SCOPE + ORDERING: which channel/user/group an event concerns is carried INSIDE\n * `payloadJson` by the emitting plugin — the transport envelope intentionally has no\n * group field. Consumers therefore demultiplex + order by their own payload fields\n * (e.g. a per-channel sequence in the payload), NOT by {@link eventSequence}, which\n * is per-group at the host and would produce false gaps when multiple groups\n * multiplex over one connection.\n */\nexport interface ClientPushEvent\n{\n /** Plugin-defined discriminator, e.g. `\"chatMessageReceived\"`. */\n eventType: string;\n /** Raw JSON payload authored by the plugin backend. */\n payloadJson: string;\n /**\n * Host per-group monotonic sequence. Advisory only — do NOT use for\n * cross-group gap detection (see the scope note above).\n */\n eventSequence: number;\n}\n\nexport interface ClientPushSubscribeOptions\n{\n /**\n * Opaque group names to enrol in (e.g. `\"chat:channel:{id}\"`). The host\n * authorises each subscription via the plugin's `authorize-subscription` tool\n * and enforces org/extension isolation — a plugin cannot subscribe outside its\n * own extension + organisation.\n */\n groups: string[];\n /** Called for each delivered (non-resync) event for the subscribed groups. */\n onEvent: (event: ClientPushEvent) => void;\n /**\n * Called when the host signals a gap/resync for the subscribed groups (a\n * dropped-event backpressure signal, or a reconnect). The consumer should\n * re-fetch authoritative state (e.g. a delta/cold-load) rather than trusting\n * incremental events.\n */\n onResync?: () => void;\n}\n\n/**\n * Host-provided channel for realtime server-push. The channel is already scoped to\n * the mounted surface's extension + organisation (bound by the host from the trusted\n * mount descriptor — a plugin CANNOT widen it), so {@link subscribe} takes only\n * opaque group names and returns an unsubscribe function.\n */\nexport interface ClientPushChannel\n{\n subscribe(options: ClientPushSubscribeOptions): () => void;\n}\n\n/**\n * `null` = no host channel (standalone/mock, or a host that predates client-push) →\n * {@link useClientPushSubscription} is inert. Provided by\n * {@link ExtensionRuntimeProvider}'s optional `clientPush` prop.\n */\nexport const ClientPushContext = createContext<ClientPushChannel | null>(null);\n\nexport interface UseClientPushSubscriptionOptions\n{\n /** Opaque groups to subscribe. Changing the SET re-subscribes; identity/order changes alone do not. */\n groups: string[];\n onEvent: (event: ClientPushEvent) => void;\n onResync?: () => void;\n /** Gate the subscription (e.g. until an id is known). Default `true`. */\n enabled?: boolean;\n}\n\n/**\n * Subscribe a plugin surface to host client-push events for `groups`.\n *\n * Inert (no-op) when no host channel is present (standalone/mock), when `enabled` is\n * false, or when `groups` is empty. Re-subscribes when the group set changes and\n * unsubscribes on unmount. Callback identities are held in refs, so passing new\n * inline `onEvent`/`onResync` closures every render does NOT churn the subscription.\n */\nexport function useClientPushSubscription(options: UseClientPushSubscriptionOptions): void\n{\n const { groups, onEvent, onResync, enabled = true } = options;\n const channel = useContext(ClientPushContext);\n\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n const onResyncRef = useRef(onResync);\n onResyncRef.current = onResync;\n\n // Normalise (dedupe + sort) so the effect re-runs only when the group SET actually\n // changes — reordering the same groups, or passing a fresh array literal of the\n // same set each render, must NOT churn the subscription.\n const normalizedGroups = [...new Set(groups)].sort();\n // JSON-encode (not space-join) so group names containing a delimiter can't alias\n // distinct sets to the same key (e.g. [\"a b\"] vs [\"a\",\"b\"]).\n const groupsKey = JSON.stringify(normalizedGroups);\n\n useEffect(() =>\n {\n if (!channel || !enabled || normalizedGroups.length === 0)\n {\n return;\n }\n const unsubscribe = channel.subscribe({\n groups: normalizedGroups,\n onEvent: (event) => onEventRef.current(event),\n onResync: () => onResyncRef.current?.(),\n });\n return unsubscribe;\n // normalizedGroups is captured via groupsKey; callbacks via refs — intentionally excluded.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [channel, enabled, groupsKey]);\n}\n","/**\n * Host identity/permission seam for platform-react plugin surfaces (WI 5108, F-AUTH-SEAM).\n *\n * Mirrors the clientPush seam: the host binds a HostIdentity to the mounted surface and\n * provides it via ExtensionRuntimeProvider's `identity` prop; the plugin reads it with\n * `useHostIdentity`. Inert (returns null) when no host provides the context — standalone /\n * mock, or a host that predates this feature.\n *\n * SECURITY: this is presentation/UX data. The plugin BACKEND re-authorises every MCP call\n * from the server session; `permission` is NOT an authorization boundary. The host forwards\n * ONLY the caller's grant for the surface's own extension (least-privilege), never the full set.\n */\nimport { createContext, useContext } from \"react\";\n\n/** Current-user identity for a mounted plugin surface, bound by the host. */\nexport interface HostIdentityUser\n{\n id: string;\n firstName: string;\n lastName: string;\n fullName: string;\n isExternal: boolean;\n}\n\n/** One resolved permission grant (bitMask over the plugin's PermissionMask bits). */\nexport interface HostPermission\n{\n groupCode: string;\n bitMask: number;\n}\n\n/** Host-provided identity context for the mounted surface. */\nexport interface HostIdentity\n{\n /** null while host auth is still loading (see isLoading) OR when unauthenticated. */\n user: HostIdentityUser | null;\n /** true while the host's /auth/user resolution is in flight — disambiguates loading from unauthenticated. */\n isLoading: boolean;\n /** The mounted surface's OWN grant only, or null when the user has no grant for this extension. */\n permission: HostPermission | null;\n /** The mounted surface's own extension groupCode, host-bound from the trusted manifest. */\n extensionGroupCode: string;\n /**\n * The active organisation id for the mounted surface, or null while host auth is loading /\n * unauthenticated. Host-bound from the SPA's active-organisation context. Surfaces that scope\n * realtime subscriptions or org-keyed queries read this (e.g. the chat client-push gate); it is\n * NOT a security token — the plugin backend derives org from the server session independently.\n */\n organisationId: string | null;\n}\n\n/**\n * `null` = no host channel (standalone/mock, or a host that predates the identity seam) →\n * {@link useHostIdentity} returns null and the plugin falls back to its deny-by-default path.\n * Provided by {@link ExtensionRuntimeProvider}'s optional `identity` prop.\n */\nexport const HostIdentityContext = createContext<HostIdentity | null>(null);\n\n/** Returns the host identity for the mounted surface, or null when no host context is present. */\nexport function useHostIdentity(): HostIdentity | null\n{\n return useContext(HostIdentityContext);\n}\n","import { createContext, useContext } from \"react\";\n\n/**\n * A host-supplied realtime subscription source for a plugin.\n *\n * The SDK keeps this intentionally dumb — it only calls `source.subscribe`.\n * All SignalR wiring, extensionId filtering, and connection lifecycle management\n * live host-side (Task 6 in coreconnect-web). This lets the SDK be tested with\n * a simple fake source.\n */\nexport interface PluginRealtimeSource\n{\n /**\n * Subscribe to notifications whose `typeCode` matches the given value.\n *\n * @param typeCode The application-level event type code to filter on\n * (e.g. `\"HelpdeskTicketCreated\"`). Filtering by\n * extensionId is the host's responsibility.\n * @param handler Called with the raw notification payload whenever a\n * matching notification arrives.\n * @returns An unsubscribe function. Calling it removes this handler.\n */\n subscribe(typeCode: string, handler: (payload: unknown) => void): () => void;\n}\n\n/**\n * React context carrying the plugin's active {@link PluginRealtimeSource}.\n *\n * `null` is the explicit \"not provided\" sentinel — hooks must treat null as a\n * clean no-op (dev/mock/no-connection) rather than an error.\n *\n * Provided by {@link ExtensionRuntimeProvider} when the host passes a\n * `realtime` prop; consumed by `usePluginRealtimeSource()`.\n */\nexport const PluginRealtimeContext = createContext<PluginRealtimeSource | null>(null);\n\n/**\n * Returns the {@link PluginRealtimeSource} from context, or `null` when none\n * is wired (dev/mock environments, unit tests that only care about MCP).\n *\n * Hooks built on top of this (e.g. `usePluginRealtime` in `plugin-ui`) should\n * skip their subscription entirely when this returns `null`.\n */\nexport function usePluginRealtimeSource(): PluginRealtimeSource | null\n{\n return useContext(PluginRealtimeContext);\n}\n","import { createContext, useContext, useMemo, type ReactNode } from \"react\";\nimport type { McpTransport } from \"./transport\";\nimport { ClientPushContext, type ClientPushChannel } from \"./clientPush\";\nimport { HostIdentityContext, type HostIdentity } from \"./hostIdentity\";\nimport { PluginRealtimeContext, type PluginRealtimeSource } from \"./PluginRealtimeContext\";\n\n/**\n * React context carrying the {@link McpTransport} the plugin should use to\n * reach the host. `null` is the explicit \"not provided\" sentinel so the hooks\n * can disambiguate from a transport that was provided but is incidentally\n * falsy in some other dimension.\n */\nconst ExtensionRuntimeContext = createContext<McpTransport | null>(null);\n\nexport interface ExtensionRuntimeProviderProps\n{\n transport: McpTransport;\n /**\n * Optional host realtime channel consumed by {@link useClientPushSubscription}.\n * Absent (or `null`) in standalone/mock hosts and hosts that predate client-push,\n * in which case the hook is inert. The host binds this channel to the mounted\n * surface's trusted extension + organisation identity.\n */\n clientPush?: ClientPushChannel | null;\n /**\n * Optional host identity/permission context consumed by {@link useHostIdentity}.\n * Absent (or `null`) in standalone/mock hosts and hosts that predate this seam,\n * in which case the hook is inert. The host binds this to the mounted surface's\n * trusted extension identity and forwards only that extension's own grant.\n */\n identity?: HostIdentity | null;\n /**\n * Optional realtime subscription source supplied by the host.\n *\n * When provided, descendant components can call `usePluginRealtimeSource()`\n * to obtain it and subscribe to push notifications. When omitted (dev/mock\n * environments or plugins that don't need realtime), the context defaults\n * to `null` and consumers no-op cleanly.\n */\n realtime?: PluginRealtimeSource;\n children?: ReactNode;\n}\n\n/**\n * Wrap a plugin's React tree so descendant {@link useMcpResource} and\n * {@link useMcpTool} calls resolve a default transport without having to\n * thread it through every component.\n *\n * Hooks still accept a per-call `transport` override, which takes precedence\n * over the context value — useful for tests and for plugins that want to\n * shard work across multiple hosts.\n */\nexport function ExtensionRuntimeProvider({ transport, clientPush = null, identity = null, realtime, children }: ExtensionRuntimeProviderProps): ReactNode\n{\n // Memoise so swapping `children` doesn't churn the context identity.\n const value = useMemo(() => transport, [transport]);\n const push = useMemo(() => clientPush, [clientPush]);\n const id = useMemo(() => identity, [identity]);\n const realtimeValue = useMemo(() => realtime ?? null, [realtime]);\n return (\n <ExtensionRuntimeContext.Provider value={value}>\n <ClientPushContext.Provider value={push}>\n <HostIdentityContext.Provider value={id}>\n <PluginRealtimeContext.Provider value={realtimeValue}>\n {children}\n </PluginRealtimeContext.Provider>\n </HostIdentityContext.Provider>\n </ClientPushContext.Provider>\n </ExtensionRuntimeContext.Provider>\n );\n}\n\n/**\n * Internal helper used by the hooks. Returns the explicit override when\n * supplied, otherwise falls back to the context. Throws a deterministic\n * error if neither is available so misconfiguration fails loudly at the\n * first render rather than producing silent no-ops.\n */\nexport function useExtensionRuntimeTransport(override?: McpTransport): McpTransport\n{\n const fromContext = useContext(ExtensionRuntimeContext);\n const resolved = override ?? fromContext;\n if (!resolved)\n {\n throw new Error(\n \"No McpTransport available. Wrap your plugin in <ExtensionRuntimeProvider transport={...}> \"\n + \"or pass `transport` directly to the hook.\",\n );\n }\n return resolved;\n}\n","import type { McpTransport, UploadDocumentMeta, UploadDocumentResult } from \"../plugin/transport\";\nimport type { SduiNode } from \"../host/declarative/types\";\n\n/**\n * Handler for a mocked tool invocation. The handler receives the request\n * payload supplied by the caller and may return synchronously or\n * asynchronously. The result is forwarded verbatim through\n * {@link InMemoryMcpTransport.invokeTool}.\n */\nexport type MockToolHandler = (args: unknown) => Promise<unknown> | unknown;\n\n/**\n * Handler for a mocked document upload. Receives the {@link UploadDocumentMeta}\n * and the transferred bytes; returns the {@link UploadDocumentResult} the FE\n * hook resolves. Optional — when omitted, the transport returns a synthetic\n * result echoing the metadata so a standalone plugin can exercise the flow.\n */\nexport type MockUploadHandler = (\n meta: UploadDocumentMeta,\n buffer: ArrayBuffer,\n signal?: AbortSignal,\n) => Promise<UploadDocumentResult> | UploadDocumentResult;\n\n/**\n * An in-memory {@link McpTransport} backed by a `{ resources, tools }` map.\n *\n * Used by {@link DeclarativeMockHost} so plugin authors can run their app\n * standalone for local development without a real host. The transport mirrors\n * the runtime contract exactly:\n *\n * - `getResource(uri)` resolves a `SduiNode` keyed by URI, or rejects with a\n * descriptive error if the URI is not registered.\n * - `invokeTool(name, args)` dispatches to a synchronous or async handler,\n * or rejects if the tool name is unknown.\n *\n * `getResource` / `invokeTool` intentionally do NOT honour the supplied\n * `AbortSignal` — mock handlers are synchronous from the caller's perspective\n * and there is no in-flight network call to abort. Hooks still work correctly\n * because they treat the `AbortSignal` as a one-way notification, not a\n * contract. `uploadDocument` DOES observe the signal (rejecting with an\n * `AbortError`): its contract mandates it, a mock upload handler may be\n * genuinely async, and dev-host flows need to simulate upload cancellation.\n */\nexport class InMemoryMcpTransport implements McpTransport\n{\n private readonly resources: Record<string, SduiNode>;\n private readonly tools: Record<string, MockToolHandler>;\n private readonly uploadHandler?: MockUploadHandler;\n\n public constructor(\n resources: Record<string, SduiNode>,\n tools: Record<string, MockToolHandler> = {},\n uploadHandler?: MockUploadHandler,\n )\n {\n this.resources = resources;\n this.tools = tools;\n this.uploadHandler = uploadHandler;\n }\n\n public async getResource<T>(uri: string): Promise<{ uri: string; data: T }>\n {\n const data = this.resources[uri];\n if (data === undefined)\n {\n throw new Error(`Mock resource not found: ${uri}`);\n }\n return { uri, data: data as unknown as T };\n }\n\n public async invokeTool<TReq, TRes>(name: string, args: TReq): Promise<TRes>\n {\n const tool = this.tools[name];\n if (!tool)\n {\n throw new Error(`Mock tool not found: ${name}`);\n }\n const result = await tool(args);\n return result as TRes;\n }\n\n public async uploadDocument(\n meta: UploadDocumentMeta,\n buffer: ArrayBuffer,\n signal?: AbortSignal,\n ): Promise<UploadDocumentResult>\n {\n if (signal?.aborted === true)\n {\n throw abortError();\n }\n const produce = (): Promise<UploadDocumentResult> | UploadDocumentResult =>\n {\n if (this.uploadHandler)\n {\n return this.uploadHandler(meta, buffer, signal);\n }\n // Synthetic default: echo the metadata with a generated id so a\n // standalone plugin can drive the upload flow without a real host.\n return {\n storedDocumentId: `mock-${meta.entityName}-${meta.fileName}`,\n fileName: meta.fileName,\n contentType: meta.contentType,\n sizeBytes: buffer.byteLength,\n };\n };\n if (signal === undefined)\n {\n return produce();\n }\n // Honour cancellation for an async upload handler: reject as soon as the\n // signal fires rather than waiting for the handler to settle.\n return new Promise<UploadDocumentResult>((resolve, reject) =>\n {\n const onAbort = (): void => reject(abortError());\n signal.addEventListener(\"abort\", onAbort, { once: true });\n Promise.resolve(produce()).then(\n (result) =>\n {\n signal.removeEventListener(\"abort\", onAbort);\n resolve(result);\n },\n (err: unknown) =>\n {\n signal.removeEventListener(\"abort\", onAbort);\n reject(err);\n },\n );\n });\n }\n}\n\n/** An `AbortError`-shaped `Error`, matching native fetch cancellation. */\nfunction abortError(): Error\n{\n const e = new Error(\"Aborted\");\n e.name = \"AbortError\";\n return e;\n}\n","import { useMemo, type ReactElement, type ReactNode } from \"react\";\nimport { interpret } from \"../host/declarative/interpreter\";\nimport type { ComponentRegistry } from \"../host/declarative/registry\";\nimport type { SduiNode } from \"../host/declarative/types\";\nimport { ExtensionRuntimeProvider } from \"../plugin/ExtensionRuntimeProvider\";\nimport { InMemoryMcpTransport, type MockToolHandler } from \"./InMemoryMcpTransport\";\n\n/**\n * Props for {@link DeclarativeMockHost}.\n *\n * Plugin authors `npm link` the runtime and render `<DeclarativeMockHost>` in\n * their local dev app to exercise the same declarative pipeline the real host\n * uses, but backed by in-memory fakes instead of the platform.\n */\nexport interface DeclarativeMockHostProps\n{\n /**\n * In-memory resource map. Keys are MCP resource URIs; values are SDUI trees\n * that {@link interpret} will render against the supplied registry.\n */\n resources: Record<string, SduiNode>;\n\n /**\n * In-memory tool map. Keys are MCP tool names; values are handlers invoked\n * when a child component calls `useMcpTool(name).invoke(args)`.\n *\n * Handlers may be sync or async — the transport awaits the result before\n * forwarding it to the caller.\n */\n tools?: Record<string, MockToolHandler>;\n\n /**\n * URI of the resource rendered as the host's default tree. If the URI is\n * not present in `resources` the host renders the supplied `children`\n * instead — useful for stubs that exercise only tool invocations.\n */\n defaultResourceUri: string;\n\n /**\n * The same primitive → component registry the real host uses. Passed\n * verbatim to {@link interpret}; the mock host owns no UI of its own.\n */\n registry: ComponentRegistry;\n\n /**\n * Optional fallback content rendered when `defaultResourceUri` does not\n * resolve to a registered resource. Children also have access to the wired\n * transport via {@link ExtensionRuntimeProvider}, so they can invoke\n * mocked tools and resources directly through the React hooks.\n */\n children?: ReactNode;\n}\n\n/**\n * In-memory host for declarative (Contract A) plugin local-dev.\n *\n * Renders a plugin's SDUI resource against the supplied registry and wires an\n * {@link InMemoryMcpTransport} into context so descendant components that use\n * `useMcpResource` / `useMcpTool` resolve against the same fakes.\n *\n * The host is intentionally minimal: it does not simulate permissions, theme\n * propagation, or capability tokens. Its purpose is to exercise the\n * declarative pipeline end-to-end against deterministic in-memory data so\n * plugin authors can iterate without standing up the real platform.\n */\nexport function DeclarativeMockHost(props: DeclarativeMockHostProps): ReactElement\n{\n const { resources, tools, defaultResourceUri, registry, children } = props;\n\n // Memoise the transport so React doesn't churn the context identity every\n // render — re-rendering this host with stable inputs must not abort\n // in-flight hook calls.\n const transport = useMemo(\n () => new InMemoryMcpTransport(resources, tools ?? {}),\n [resources, tools],\n );\n\n const tree = resources[defaultResourceUri];\n const renderedTree: ReactNode = tree ? interpret(tree, registry) : children;\n\n return (\n <ExtensionRuntimeProvider transport={transport}>\n {renderedTree}\n {tree ? children : null}\n </ExtensionRuntimeProvider>\n );\n}\n","/**\n * In-realm bridge transport for plugin local-dev and contract testing.\n *\n * Calling `pushTheme(...)`, `pushLocale(...)`, etc. invokes the registered\n * subscriber callbacks **synchronously** — no serialisation, no port. This\n * lets Vitest + React Testing Library drive bridge state changes with `act()`\n * without a real MessageChannel.\n *\n * In production, the bridge client is `createPortBridgeClient` backed by a\n * real MessagePort. `InMemoryBridgeTransport` is the dev/test equivalent:\n * both expose the same `PortBridgeClient`-compatible subscriber API on the\n * consumer side, but `InMemoryBridgeTransport` also exposes the push-side\n * and the `onChromeRequest` handler for test assertions.\n */\nimport type { PortBridgeClient, ThemePayload, LocalePayload, DensityPayload, A11yPayload, NavPayload, SessionTokenPayload } from \"../plugin/bridge-client\";\n\ntype ChromeRequestHandler = (\n action: string,\n payload: Record<string, unknown>,\n) => Promise<unknown> | unknown;\n\nexport class InMemoryBridgeTransport implements PortBridgeClient {\n private _themeCb: ((p: ThemePayload) => void) | undefined;\n private _localeCb: ((p: LocalePayload) => void) | undefined;\n private _densityCb: ((p: DensityPayload) => void) | undefined;\n private _a11yCb: ((p: A11yPayload) => void) | undefined;\n private _navCb: ((p: NavPayload) => void) | undefined;\n private _tokenCb: ((p: SessionTokenPayload) => void) | undefined;\n private _chromeHandler: ChromeRequestHandler | undefined;\n\n // ── PortBridgeClient subscriber interface ──────────────────────────────────\n\n onTheme(cb: (p: ThemePayload) => void): void { this._themeCb = cb; }\n onLocale(cb: (p: LocalePayload) => void): void { this._localeCb = cb; }\n onDensity(cb: (p: DensityPayload) => void): void { this._densityCb = cb; }\n onA11y(cb: (p: A11yPayload) => void): void { this._a11yCb = cb; }\n onNav(cb: (p: NavPayload) => void): void { this._navCb = cb; }\n onSessionToken(cb: (p: SessionTokenPayload) => void): void { this._tokenCb = cb; }\n\n requestChrome(action: string, payload: Record<string, unknown>): Promise<unknown> {\n return this.simulateChromeRequest(action, payload);\n }\n\n announceA11y(_message: string, _politeness: \"polite\" | \"assertive\"): void {\n // No-op in the mock — tests assert via `onChromeRequest` or inspect DOM.\n }\n\n // ── Test / dev control surface ─────────────────────────────────────────────\n\n /** Push a theme update to the registered subscriber (synchronous). */\n pushTheme(payload: ThemePayload): void { this._themeCb?.(payload); }\n /** Push a locale update to the registered subscriber. */\n pushLocale(payload: LocalePayload): void { this._localeCb?.(payload); }\n /** Push a density update. */\n pushDensity(payload: DensityPayload): void { this._densityCb?.(payload); }\n /** Push a11y preference changes. */\n pushA11y(payload: A11yPayload): void { this._a11yCb?.(payload); }\n /** Push a nav state update. */\n pushNav(payload: NavPayload): void { this._navCb?.(payload); }\n /** Push a frontend-session token. */\n pushSessionToken(payload: SessionTokenPayload): void { this._tokenCb?.(payload); }\n\n /**\n * Register a handler for plugin→host chrome requests (toast, confirm, etc).\n * Called by `requestChrome` and by `simulateChromeRequest`.\n */\n onChromeRequest(handler: ChromeRequestHandler): void {\n this._chromeHandler = handler;\n }\n\n /**\n * Programmatically send a chrome request as if a plugin component called\n * `PortBridgeClient.requestChrome(...)`. Useful for test assertions.\n */\n async simulateChromeRequest(action: string, payload: Record<string, unknown>): Promise<unknown> {\n if (this._chromeHandler === undefined) {\n return null;\n }\n return this._chromeHandler(action, payload);\n }\n}\n","import { createContext, useContext } from \"react\";\nimport type { PortBridgeClient } from \"./bridge-client\";\n\n/**\n * React context carrying the plugin's active {@link PortBridgeClient}.\n * Provided by the host mount (WorkerMockHost in dev, real bridge in production)\n * and consumed by `useBridgeTheme`, `useBridgeLocale`, and the `plugin-ui` hooks.\n */\nexport const BridgeClientContext = createContext<PortBridgeClient | null>(null);\n\n/**\n * Returns the bridge client from context, throwing a clear error when missing.\n * Used by the `useBridge*` hooks to fail loudly on misconfiguration.\n */\nexport function useBridgeClient(): PortBridgeClient {\n const client = useContext(BridgeClientContext);\n if (client === null) {\n throw new Error(\n \"No PortBridgeClient available. Wrap your plugin in <BridgeClientProvider> \"\n + \"or ensure the host mount wires a BridgeClientContext.Provider.\",\n );\n }\n return client;\n}\n","/**\n * Mock host for plugins that use bridge hooks (`useBridgeTheme`,\n * `useBridgeLocale`, `useBridgeA11y`, etc.) during local-dev or contract\n * testing.\n *\n * Wraps {@link DeclarativeMockHost} and wires a {@link BridgeClientContext}\n * provider so any descendant bridge hook resolves against the supplied\n * `bridgeTransport` instead of throwing \"no bridge client available\".\n *\n * For tests where bridge state must be driven externally (push a new theme,\n * assert that a component re-renders), pass an {@link InMemoryBridgeTransport}\n * instance and call `transport.pushTheme(...)` wrapped in `act()`.\n */\nimport { type ReactElement } from \"react\";\nimport { DeclarativeMockHost, type DeclarativeMockHostProps } from \"./DeclarativeMockHost\";\nimport { BridgeClientContext } from \"../plugin/BridgeClientContext\";\nimport type { PortBridgeClient } from \"../plugin/bridge-client\";\n\nexport interface WorkerMockHostProps extends DeclarativeMockHostProps {\n /**\n * The bridge transport to wire into context. Pass an\n * {@link InMemoryBridgeTransport} for tests; pass a\n * `createPortBridgeClient(port)` instance for postMessage integration tests.\n */\n bridgeTransport: PortBridgeClient;\n}\n\n/**\n * Mock host that combines the SDUI declarative pipeline with bridge context.\n *\n * Rendering contract (same as DeclarativeMockHost):\n * - `defaultResourceUri` present in `resources` → renders the SDUI tree.\n * - URI absent → renders `children` instead (tool-invocation stubs, etc).\n *\n * Bridge contract:\n * - All `useBridgeTheme`, `useBridgeLocale`, etc. hooks in the subtree resolve\n * against `bridgeTransport`.\n */\nexport function WorkerMockHost(props: WorkerMockHostProps): ReactElement {\n const { bridgeTransport, ...declarativeProps } = props;\n\n return (\n <BridgeClientContext.Provider value={bridgeTransport}>\n <DeclarativeMockHost {...declarativeProps} />\n </BridgeClientContext.Provider>\n );\n}\n"]}
@@ -1,8 +1,8 @@
1
1
  import { ReactNode, ReactElement } from 'react';
2
2
  import { C as ComponentRegistry } from '../registry-DpCx_LxF.cjs';
3
3
  import { SduiNode } from '@ethisyscore/protocol';
4
- import { M as McpTransport, P as PortBridgeClient, T as ThemePayload, L as LocalePayload, D as DensityPayload, A as A11yPayload, N as NavPayload, S as SessionTokenPayload } from '../bridge-client-CIThO7jZ.cjs';
5
- import '../bridge-envelopes-BRKGSiSC.cjs';
4
+ import { M as McpTransport, U as UploadDocumentMeta, f as UploadDocumentResult } from '../bridge-envelopes-DA6vxbyb.cjs';
5
+ import { P as PortBridgeClient, T as ThemePayload, L as LocalePayload, D as DensityPayload, A as A11yPayload, N as NavPayload, S as SessionTokenPayload } from '../bridge-client-DzRcKIJT.cjs';
6
6
 
7
7
  /**
8
8
  * Handler for a mocked tool invocation. The handler receives the request
@@ -11,6 +11,13 @@ import '../bridge-envelopes-BRKGSiSC.cjs';
11
11
  * {@link InMemoryMcpTransport.invokeTool}.
12
12
  */
13
13
  type MockToolHandler = (args: unknown) => Promise<unknown> | unknown;
14
+ /**
15
+ * Handler for a mocked document upload. Receives the {@link UploadDocumentMeta}
16
+ * and the transferred bytes; returns the {@link UploadDocumentResult} the FE
17
+ * hook resolves. Optional — when omitted, the transport returns a synthetic
18
+ * result echoing the metadata so a standalone plugin can exercise the flow.
19
+ */
20
+ type MockUploadHandler = (meta: UploadDocumentMeta, buffer: ArrayBuffer, signal?: AbortSignal) => Promise<UploadDocumentResult> | UploadDocumentResult;
14
21
  /**
15
22
  * An in-memory {@link McpTransport} backed by a `{ resources, tools }` map.
16
23
  *
@@ -23,20 +30,25 @@ type MockToolHandler = (args: unknown) => Promise<unknown> | unknown;
23
30
  * - `invokeTool(name, args)` dispatches to a synchronous or async handler,
24
31
  * or rejects if the tool name is unknown.
25
32
  *
26
- * The transport intentionally does NOT honour the supplied `AbortSignal` —
27
- * mock handlers are synchronous from the caller's perspective and there is
28
- * no in-flight network call to abort. Hooks still work correctly because
29
- * they treat the `AbortSignal` as a one-way notification, not a contract.
33
+ * `getResource` / `invokeTool` intentionally do NOT honour the supplied
34
+ * `AbortSignal` — mock handlers are synchronous from the caller's perspective
35
+ * and there is no in-flight network call to abort. Hooks still work correctly
36
+ * because they treat the `AbortSignal` as a one-way notification, not a
37
+ * contract. `uploadDocument` DOES observe the signal (rejecting with an
38
+ * `AbortError`): its contract mandates it, a mock upload handler may be
39
+ * genuinely async, and dev-host flows need to simulate upload cancellation.
30
40
  */
31
41
  declare class InMemoryMcpTransport implements McpTransport {
32
42
  private readonly resources;
33
43
  private readonly tools;
34
- constructor(resources: Record<string, SduiNode>, tools?: Record<string, MockToolHandler>);
44
+ private readonly uploadHandler?;
45
+ constructor(resources: Record<string, SduiNode>, tools?: Record<string, MockToolHandler>, uploadHandler?: MockUploadHandler);
35
46
  getResource<T>(uri: string): Promise<{
36
47
  uri: string;
37
48
  data: T;
38
49
  }>;
39
50
  invokeTool<TReq, TRes>(name: string, args: TReq): Promise<TRes>;
51
+ uploadDocument(meta: UploadDocumentMeta, buffer: ArrayBuffer, signal?: AbortSignal): Promise<UploadDocumentResult>;
40
52
  }
41
53
 
42
54
  /**
@@ -1,8 +1,8 @@
1
1
  import { ReactNode, ReactElement } from 'react';
2
2
  import { C as ComponentRegistry } from '../registry-DpCx_LxF.js';
3
3
  import { SduiNode } from '@ethisyscore/protocol';
4
- import { M as McpTransport, P as PortBridgeClient, T as ThemePayload, L as LocalePayload, D as DensityPayload, A as A11yPayload, N as NavPayload, S as SessionTokenPayload } from '../bridge-client-UK3qcGoi.js';
5
- import '../bridge-envelopes-BRKGSiSC.js';
4
+ import { M as McpTransport, U as UploadDocumentMeta, f as UploadDocumentResult } from '../bridge-envelopes-DA6vxbyb.js';
5
+ import { P as PortBridgeClient, T as ThemePayload, L as LocalePayload, D as DensityPayload, A as A11yPayload, N as NavPayload, S as SessionTokenPayload } from '../bridge-client-DD99netz.js';
6
6
 
7
7
  /**
8
8
  * Handler for a mocked tool invocation. The handler receives the request
@@ -11,6 +11,13 @@ import '../bridge-envelopes-BRKGSiSC.js';
11
11
  * {@link InMemoryMcpTransport.invokeTool}.
12
12
  */
13
13
  type MockToolHandler = (args: unknown) => Promise<unknown> | unknown;
14
+ /**
15
+ * Handler for a mocked document upload. Receives the {@link UploadDocumentMeta}
16
+ * and the transferred bytes; returns the {@link UploadDocumentResult} the FE
17
+ * hook resolves. Optional — when omitted, the transport returns a synthetic
18
+ * result echoing the metadata so a standalone plugin can exercise the flow.
19
+ */
20
+ type MockUploadHandler = (meta: UploadDocumentMeta, buffer: ArrayBuffer, signal?: AbortSignal) => Promise<UploadDocumentResult> | UploadDocumentResult;
14
21
  /**
15
22
  * An in-memory {@link McpTransport} backed by a `{ resources, tools }` map.
16
23
  *
@@ -23,20 +30,25 @@ type MockToolHandler = (args: unknown) => Promise<unknown> | unknown;
23
30
  * - `invokeTool(name, args)` dispatches to a synchronous or async handler,
24
31
  * or rejects if the tool name is unknown.
25
32
  *
26
- * The transport intentionally does NOT honour the supplied `AbortSignal` —
27
- * mock handlers are synchronous from the caller's perspective and there is
28
- * no in-flight network call to abort. Hooks still work correctly because
29
- * they treat the `AbortSignal` as a one-way notification, not a contract.
33
+ * `getResource` / `invokeTool` intentionally do NOT honour the supplied
34
+ * `AbortSignal` — mock handlers are synchronous from the caller's perspective
35
+ * and there is no in-flight network call to abort. Hooks still work correctly
36
+ * because they treat the `AbortSignal` as a one-way notification, not a
37
+ * contract. `uploadDocument` DOES observe the signal (rejecting with an
38
+ * `AbortError`): its contract mandates it, a mock upload handler may be
39
+ * genuinely async, and dev-host flows need to simulate upload cancellation.
30
40
  */
31
41
  declare class InMemoryMcpTransport implements McpTransport {
32
42
  private readonly resources;
33
43
  private readonly tools;
34
- constructor(resources: Record<string, SduiNode>, tools?: Record<string, MockToolHandler>);
44
+ private readonly uploadHandler?;
45
+ constructor(resources: Record<string, SduiNode>, tools?: Record<string, MockToolHandler>, uploadHandler?: MockUploadHandler);
35
46
  getResource<T>(uri: string): Promise<{
36
47
  uri: string;
37
48
  data: T;
38
49
  }>;
39
50
  invokeTool<TReq, TRes>(name: string, args: TReq): Promise<TRes>;
51
+ uploadDocument(meta: UploadDocumentMeta, buffer: ArrayBuffer, signal?: AbortSignal): Promise<UploadDocumentResult>;
40
52
  }
41
53
 
42
54
  /**
@@ -38,9 +38,11 @@ function ExtensionRuntimeProvider({ transport, clientPush = null, identity = nul
38
38
  var InMemoryMcpTransport = class {
39
39
  resources;
40
40
  tools;
41
- constructor(resources, tools = {}) {
41
+ uploadHandler;
42
+ constructor(resources, tools = {}, uploadHandler) {
42
43
  this.resources = resources;
43
44
  this.tools = tools;
45
+ this.uploadHandler = uploadHandler;
44
46
  }
45
47
  async getResource(uri) {
46
48
  const data = this.resources[uri];
@@ -57,7 +59,45 @@ var InMemoryMcpTransport = class {
57
59
  const result = await tool(args);
58
60
  return result;
59
61
  }
62
+ async uploadDocument(meta, buffer, signal) {
63
+ if (signal?.aborted === true) {
64
+ throw abortError();
65
+ }
66
+ const produce = () => {
67
+ if (this.uploadHandler) {
68
+ return this.uploadHandler(meta, buffer, signal);
69
+ }
70
+ return {
71
+ storedDocumentId: `mock-${meta.entityName}-${meta.fileName}`,
72
+ fileName: meta.fileName,
73
+ contentType: meta.contentType,
74
+ sizeBytes: buffer.byteLength
75
+ };
76
+ };
77
+ if (signal === void 0) {
78
+ return produce();
79
+ }
80
+ return new Promise((resolve, reject) => {
81
+ const onAbort = () => reject(abortError());
82
+ signal.addEventListener("abort", onAbort, { once: true });
83
+ Promise.resolve(produce()).then(
84
+ (result) => {
85
+ signal.removeEventListener("abort", onAbort);
86
+ resolve(result);
87
+ },
88
+ (err) => {
89
+ signal.removeEventListener("abort", onAbort);
90
+ reject(err);
91
+ }
92
+ );
93
+ });
94
+ }
60
95
  };
96
+ function abortError() {
97
+ const e = new Error("Aborted");
98
+ e.name = "AbortError";
99
+ return e;
100
+ }
61
101
  function DeclarativeMockHost(props) {
62
102
  const { resources, tools, defaultResourceUri, registry, children } = props;
63
103
  const transport = useMemo(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/host/declarative/interpreter.ts","../../src/plugin/clientPush.ts","../../src/plugin/hostIdentity.ts","../../src/plugin/PluginRealtimeContext.ts","../../src/plugin/ExtensionRuntimeProvider.tsx","../../src/mock-host/InMemoryMcpTransport.ts","../../src/mock-host/DeclarativeMockHost.tsx","../../src/mock-host/InMemoryBridgeTransport.ts","../../src/plugin/BridgeClientContext.ts","../../src/mock-host/WorkerMockHost.tsx"],"names":["createContext","useMemo","jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { createElement, type ReactElement, type ReactNode } from \"react\";\nimport type { ComponentRegistry } from \"./registry\";\nimport type { SduiNode } from \"./types\";\n\n/**\n * Walk a parsed SDUI tree and render it as a React element by looking up each\n * node's `type` in the supplied {@link ComponentRegistry}.\n *\n * The interpreter is purely structural:\n *\n * - It owns no UI styling, layout, or data fetching.\n * - It never reads `node.props` — props are forwarded opaquely to the host\n * component, which owns interpretation per primitive.\n * - It does not evaluate `node.bindings` — reactive rules are handled in a\n * separate task (E2.S2). For v1 the interpreter passes through the static\n * tree only.\n *\n * Each child is given a stable React `key` derived from its position so that\n * React's reconciler can identify list items across renders. The key is a\n * sibling-local index; the registry consumer is responsible for opting into a\n * stable identity if it has a domain-meaningful `props.key`.\n *\n * @throws Error when `node.type` is not present in the registry. This is the\n * fail-loud behaviour required by the closed v1 vocabulary — unknown\n * primitives must not silently degrade.\n */\nexport function interpret(node: SduiNode, registry: ComponentRegistry): ReactElement\n{\n const Component = registry[node.type];\n if (!Component)\n {\n throw new Error(`Unknown SDUI primitive: ${String(node.type)}`);\n }\n\n const children: ReactNode[] | undefined = node.children?.map(\n (child, index) => interpretChild(child, registry, index),\n );\n\n return createElement(Component, { props: node.props }, children);\n}\n\nfunction interpretChild(node: SduiNode, registry: ComponentRegistry, index: number): ReactElement\n{\n const Component = registry[node.type];\n if (!Component)\n {\n throw new Error(`Unknown SDUI primitive: ${String(node.type)}`);\n }\n\n const children: ReactNode[] | undefined = node.children?.map(\n (child, childIndex) => interpretChild(child, registry, childIndex),\n );\n\n return createElement(Component, { props: node.props, key: index }, children);\n}\n","import { createContext, useContext, useEffect, useRef } from \"react\";\n\n/**\n * A single client-push event delivered from the host to a plugin surface. The host\n * relays the plugin backend's `IClientPushPublisher` events over its realtime\n * channel (SignalR); the transport envelope's extension identity is bound by the\n * host at mount time, so the plugin sees only the event body.\n *\n * SCOPE + ORDERING: which channel/user/group an event concerns is carried INSIDE\n * `payloadJson` by the emitting plugin — the transport envelope intentionally has no\n * group field. Consumers therefore demultiplex + order by their own payload fields\n * (e.g. a per-channel sequence in the payload), NOT by {@link eventSequence}, which\n * is per-group at the host and would produce false gaps when multiple groups\n * multiplex over one connection.\n */\nexport interface ClientPushEvent\n{\n /** Plugin-defined discriminator, e.g. `\"chatMessageReceived\"`. */\n eventType: string;\n /** Raw JSON payload authored by the plugin backend. */\n payloadJson: string;\n /**\n * Host per-group monotonic sequence. Advisory only — do NOT use for\n * cross-group gap detection (see the scope note above).\n */\n eventSequence: number;\n}\n\nexport interface ClientPushSubscribeOptions\n{\n /**\n * Opaque group names to enrol in (e.g. `\"chat:channel:{id}\"`). The host\n * authorises each subscription via the plugin's `authorize-subscription` tool\n * and enforces org/extension isolation — a plugin cannot subscribe outside its\n * own extension + organisation.\n */\n groups: string[];\n /** Called for each delivered (non-resync) event for the subscribed groups. */\n onEvent: (event: ClientPushEvent) => void;\n /**\n * Called when the host signals a gap/resync for the subscribed groups (a\n * dropped-event backpressure signal, or a reconnect). The consumer should\n * re-fetch authoritative state (e.g. a delta/cold-load) rather than trusting\n * incremental events.\n */\n onResync?: () => void;\n}\n\n/**\n * Host-provided channel for realtime server-push. The channel is already scoped to\n * the mounted surface's extension + organisation (bound by the host from the trusted\n * mount descriptor — a plugin CANNOT widen it), so {@link subscribe} takes only\n * opaque group names and returns an unsubscribe function.\n */\nexport interface ClientPushChannel\n{\n subscribe(options: ClientPushSubscribeOptions): () => void;\n}\n\n/**\n * `null` = no host channel (standalone/mock, or a host that predates client-push) →\n * {@link useClientPushSubscription} is inert. Provided by\n * {@link ExtensionRuntimeProvider}'s optional `clientPush` prop.\n */\nexport const ClientPushContext = createContext<ClientPushChannel | null>(null);\n\nexport interface UseClientPushSubscriptionOptions\n{\n /** Opaque groups to subscribe. Changing the SET re-subscribes; identity/order changes alone do not. */\n groups: string[];\n onEvent: (event: ClientPushEvent) => void;\n onResync?: () => void;\n /** Gate the subscription (e.g. until an id is known). Default `true`. */\n enabled?: boolean;\n}\n\n/**\n * Subscribe a plugin surface to host client-push events for `groups`.\n *\n * Inert (no-op) when no host channel is present (standalone/mock), when `enabled` is\n * false, or when `groups` is empty. Re-subscribes when the group set changes and\n * unsubscribes on unmount. Callback identities are held in refs, so passing new\n * inline `onEvent`/`onResync` closures every render does NOT churn the subscription.\n */\nexport function useClientPushSubscription(options: UseClientPushSubscriptionOptions): void\n{\n const { groups, onEvent, onResync, enabled = true } = options;\n const channel = useContext(ClientPushContext);\n\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n const onResyncRef = useRef(onResync);\n onResyncRef.current = onResync;\n\n // Normalise (dedupe + sort) so the effect re-runs only when the group SET actually\n // changes — reordering the same groups, or passing a fresh array literal of the\n // same set each render, must NOT churn the subscription.\n const normalizedGroups = [...new Set(groups)].sort();\n // JSON-encode (not space-join) so group names containing a delimiter can't alias\n // distinct sets to the same key (e.g. [\"a b\"] vs [\"a\",\"b\"]).\n const groupsKey = JSON.stringify(normalizedGroups);\n\n useEffect(() =>\n {\n if (!channel || !enabled || normalizedGroups.length === 0)\n {\n return;\n }\n const unsubscribe = channel.subscribe({\n groups: normalizedGroups,\n onEvent: (event) => onEventRef.current(event),\n onResync: () => onResyncRef.current?.(),\n });\n return unsubscribe;\n // normalizedGroups is captured via groupsKey; callbacks via refs — intentionally excluded.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [channel, enabled, groupsKey]);\n}\n","/**\n * Host identity/permission seam for platform-react plugin surfaces (WI 5108, F-AUTH-SEAM).\n *\n * Mirrors the clientPush seam: the host binds a HostIdentity to the mounted surface and\n * provides it via ExtensionRuntimeProvider's `identity` prop; the plugin reads it with\n * `useHostIdentity`. Inert (returns null) when no host provides the context — standalone /\n * mock, or a host that predates this feature.\n *\n * SECURITY: this is presentation/UX data. The plugin BACKEND re-authorises every MCP call\n * from the server session; `permission` is NOT an authorization boundary. The host forwards\n * ONLY the caller's grant for the surface's own extension (least-privilege), never the full set.\n */\nimport { createContext, useContext } from \"react\";\n\n/** Current-user identity for a mounted plugin surface, bound by the host. */\nexport interface HostIdentityUser\n{\n id: string;\n firstName: string;\n lastName: string;\n fullName: string;\n isExternal: boolean;\n}\n\n/** One resolved permission grant (bitMask over the plugin's PermissionMask bits). */\nexport interface HostPermission\n{\n groupCode: string;\n bitMask: number;\n}\n\n/** Host-provided identity context for the mounted surface. */\nexport interface HostIdentity\n{\n /** null while host auth is still loading (see isLoading) OR when unauthenticated. */\n user: HostIdentityUser | null;\n /** true while the host's /auth/user resolution is in flight — disambiguates loading from unauthenticated. */\n isLoading: boolean;\n /** The mounted surface's OWN grant only, or null when the user has no grant for this extension. */\n permission: HostPermission | null;\n /** The mounted surface's own extension groupCode, host-bound from the trusted manifest. */\n extensionGroupCode: string;\n /**\n * The active organisation id for the mounted surface, or null while host auth is loading /\n * unauthenticated. Host-bound from the SPA's active-organisation context. Surfaces that scope\n * realtime subscriptions or org-keyed queries read this (e.g. the chat client-push gate); it is\n * NOT a security token — the plugin backend derives org from the server session independently.\n */\n organisationId: string | null;\n}\n\n/**\n * `null` = no host channel (standalone/mock, or a host that predates the identity seam) →\n * {@link useHostIdentity} returns null and the plugin falls back to its deny-by-default path.\n * Provided by {@link ExtensionRuntimeProvider}'s optional `identity` prop.\n */\nexport const HostIdentityContext = createContext<HostIdentity | null>(null);\n\n/** Returns the host identity for the mounted surface, or null when no host context is present. */\nexport function useHostIdentity(): HostIdentity | null\n{\n return useContext(HostIdentityContext);\n}\n","import { createContext, useContext } from \"react\";\n\n/**\n * A host-supplied realtime subscription source for a plugin.\n *\n * The SDK keeps this intentionally dumb — it only calls `source.subscribe`.\n * All SignalR wiring, extensionId filtering, and connection lifecycle management\n * live host-side (Task 6 in coreconnect-web). This lets the SDK be tested with\n * a simple fake source.\n */\nexport interface PluginRealtimeSource\n{\n /**\n * Subscribe to notifications whose `typeCode` matches the given value.\n *\n * @param typeCode The application-level event type code to filter on\n * (e.g. `\"HelpdeskTicketCreated\"`). Filtering by\n * extensionId is the host's responsibility.\n * @param handler Called with the raw notification payload whenever a\n * matching notification arrives.\n * @returns An unsubscribe function. Calling it removes this handler.\n */\n subscribe(typeCode: string, handler: (payload: unknown) => void): () => void;\n}\n\n/**\n * React context carrying the plugin's active {@link PluginRealtimeSource}.\n *\n * `null` is the explicit \"not provided\" sentinel — hooks must treat null as a\n * clean no-op (dev/mock/no-connection) rather than an error.\n *\n * Provided by {@link ExtensionRuntimeProvider} when the host passes a\n * `realtime` prop; consumed by `usePluginRealtimeSource()`.\n */\nexport const PluginRealtimeContext = createContext<PluginRealtimeSource | null>(null);\n\n/**\n * Returns the {@link PluginRealtimeSource} from context, or `null` when none\n * is wired (dev/mock environments, unit tests that only care about MCP).\n *\n * Hooks built on top of this (e.g. `usePluginRealtime` in `plugin-ui`) should\n * skip their subscription entirely when this returns `null`.\n */\nexport function usePluginRealtimeSource(): PluginRealtimeSource | null\n{\n return useContext(PluginRealtimeContext);\n}\n","import { createContext, useContext, useMemo, type ReactNode } from \"react\";\nimport type { McpTransport } from \"./transport\";\nimport { ClientPushContext, type ClientPushChannel } from \"./clientPush\";\nimport { HostIdentityContext, type HostIdentity } from \"./hostIdentity\";\nimport { PluginRealtimeContext, type PluginRealtimeSource } from \"./PluginRealtimeContext\";\n\n/**\n * React context carrying the {@link McpTransport} the plugin should use to\n * reach the host. `null` is the explicit \"not provided\" sentinel so the hooks\n * can disambiguate from a transport that was provided but is incidentally\n * falsy in some other dimension.\n */\nconst ExtensionRuntimeContext = createContext<McpTransport | null>(null);\n\nexport interface ExtensionRuntimeProviderProps\n{\n transport: McpTransport;\n /**\n * Optional host realtime channel consumed by {@link useClientPushSubscription}.\n * Absent (or `null`) in standalone/mock hosts and hosts that predate client-push,\n * in which case the hook is inert. The host binds this channel to the mounted\n * surface's trusted extension + organisation identity.\n */\n clientPush?: ClientPushChannel | null;\n /**\n * Optional host identity/permission context consumed by {@link useHostIdentity}.\n * Absent (or `null`) in standalone/mock hosts and hosts that predate this seam,\n * in which case the hook is inert. The host binds this to the mounted surface's\n * trusted extension identity and forwards only that extension's own grant.\n */\n identity?: HostIdentity | null;\n /**\n * Optional realtime subscription source supplied by the host.\n *\n * When provided, descendant components can call `usePluginRealtimeSource()`\n * to obtain it and subscribe to push notifications. When omitted (dev/mock\n * environments or plugins that don't need realtime), the context defaults\n * to `null` and consumers no-op cleanly.\n */\n realtime?: PluginRealtimeSource;\n children?: ReactNode;\n}\n\n/**\n * Wrap a plugin's React tree so descendant {@link useMcpResource} and\n * {@link useMcpTool} calls resolve a default transport without having to\n * thread it through every component.\n *\n * Hooks still accept a per-call `transport` override, which takes precedence\n * over the context value — useful for tests and for plugins that want to\n * shard work across multiple hosts.\n */\nexport function ExtensionRuntimeProvider({ transport, clientPush = null, identity = null, realtime, children }: ExtensionRuntimeProviderProps): ReactNode\n{\n // Memoise so swapping `children` doesn't churn the context identity.\n const value = useMemo(() => transport, [transport]);\n const push = useMemo(() => clientPush, [clientPush]);\n const id = useMemo(() => identity, [identity]);\n const realtimeValue = useMemo(() => realtime ?? null, [realtime]);\n return (\n <ExtensionRuntimeContext.Provider value={value}>\n <ClientPushContext.Provider value={push}>\n <HostIdentityContext.Provider value={id}>\n <PluginRealtimeContext.Provider value={realtimeValue}>\n {children}\n </PluginRealtimeContext.Provider>\n </HostIdentityContext.Provider>\n </ClientPushContext.Provider>\n </ExtensionRuntimeContext.Provider>\n );\n}\n\n/**\n * Internal helper used by the hooks. Returns the explicit override when\n * supplied, otherwise falls back to the context. Throws a deterministic\n * error if neither is available so misconfiguration fails loudly at the\n * first render rather than producing silent no-ops.\n */\nexport function useExtensionRuntimeTransport(override?: McpTransport): McpTransport\n{\n const fromContext = useContext(ExtensionRuntimeContext);\n const resolved = override ?? fromContext;\n if (!resolved)\n {\n throw new Error(\n \"No McpTransport available. Wrap your plugin in <ExtensionRuntimeProvider transport={...}> \"\n + \"or pass `transport` directly to the hook.\",\n );\n }\n return resolved;\n}\n","import type { McpTransport } from \"../plugin/transport\";\nimport type { SduiNode } from \"../host/declarative/types\";\n\n/**\n * Handler for a mocked tool invocation. The handler receives the request\n * payload supplied by the caller and may return synchronously or\n * asynchronously. The result is forwarded verbatim through\n * {@link InMemoryMcpTransport.invokeTool}.\n */\nexport type MockToolHandler = (args: unknown) => Promise<unknown> | unknown;\n\n/**\n * An in-memory {@link McpTransport} backed by a `{ resources, tools }` map.\n *\n * Used by {@link DeclarativeMockHost} so plugin authors can run their app\n * standalone for local development without a real host. The transport mirrors\n * the runtime contract exactly:\n *\n * - `getResource(uri)` resolves a `SduiNode` keyed by URI, or rejects with a\n * descriptive error if the URI is not registered.\n * - `invokeTool(name, args)` dispatches to a synchronous or async handler,\n * or rejects if the tool name is unknown.\n *\n * The transport intentionally does NOT honour the supplied `AbortSignal` —\n * mock handlers are synchronous from the caller's perspective and there is\n * no in-flight network call to abort. Hooks still work correctly because\n * they treat the `AbortSignal` as a one-way notification, not a contract.\n */\nexport class InMemoryMcpTransport implements McpTransport\n{\n private readonly resources: Record<string, SduiNode>;\n private readonly tools: Record<string, MockToolHandler>;\n\n public constructor(\n resources: Record<string, SduiNode>,\n tools: Record<string, MockToolHandler> = {},\n )\n {\n this.resources = resources;\n this.tools = tools;\n }\n\n public async getResource<T>(uri: string): Promise<{ uri: string; data: T }>\n {\n const data = this.resources[uri];\n if (data === undefined)\n {\n throw new Error(`Mock resource not found: ${uri}`);\n }\n return { uri, data: data as unknown as T };\n }\n\n public async invokeTool<TReq, TRes>(name: string, args: TReq): Promise<TRes>\n {\n const tool = this.tools[name];\n if (!tool)\n {\n throw new Error(`Mock tool not found: ${name}`);\n }\n const result = await tool(args);\n return result as TRes;\n }\n}\n","import { useMemo, type ReactElement, type ReactNode } from \"react\";\nimport { interpret } from \"../host/declarative/interpreter\";\nimport type { ComponentRegistry } from \"../host/declarative/registry\";\nimport type { SduiNode } from \"../host/declarative/types\";\nimport { ExtensionRuntimeProvider } from \"../plugin/ExtensionRuntimeProvider\";\nimport { InMemoryMcpTransport, type MockToolHandler } from \"./InMemoryMcpTransport\";\n\n/**\n * Props for {@link DeclarativeMockHost}.\n *\n * Plugin authors `npm link` the runtime and render `<DeclarativeMockHost>` in\n * their local dev app to exercise the same declarative pipeline the real host\n * uses, but backed by in-memory fakes instead of the platform.\n */\nexport interface DeclarativeMockHostProps\n{\n /**\n * In-memory resource map. Keys are MCP resource URIs; values are SDUI trees\n * that {@link interpret} will render against the supplied registry.\n */\n resources: Record<string, SduiNode>;\n\n /**\n * In-memory tool map. Keys are MCP tool names; values are handlers invoked\n * when a child component calls `useMcpTool(name).invoke(args)`.\n *\n * Handlers may be sync or async — the transport awaits the result before\n * forwarding it to the caller.\n */\n tools?: Record<string, MockToolHandler>;\n\n /**\n * URI of the resource rendered as the host's default tree. If the URI is\n * not present in `resources` the host renders the supplied `children`\n * instead — useful for stubs that exercise only tool invocations.\n */\n defaultResourceUri: string;\n\n /**\n * The same primitive → component registry the real host uses. Passed\n * verbatim to {@link interpret}; the mock host owns no UI of its own.\n */\n registry: ComponentRegistry;\n\n /**\n * Optional fallback content rendered when `defaultResourceUri` does not\n * resolve to a registered resource. Children also have access to the wired\n * transport via {@link ExtensionRuntimeProvider}, so they can invoke\n * mocked tools and resources directly through the React hooks.\n */\n children?: ReactNode;\n}\n\n/**\n * In-memory host for declarative (Contract A) plugin local-dev.\n *\n * Renders a plugin's SDUI resource against the supplied registry and wires an\n * {@link InMemoryMcpTransport} into context so descendant components that use\n * `useMcpResource` / `useMcpTool` resolve against the same fakes.\n *\n * The host is intentionally minimal: it does not simulate permissions, theme\n * propagation, or capability tokens. Its purpose is to exercise the\n * declarative pipeline end-to-end against deterministic in-memory data so\n * plugin authors can iterate without standing up the real platform.\n */\nexport function DeclarativeMockHost(props: DeclarativeMockHostProps): ReactElement\n{\n const { resources, tools, defaultResourceUri, registry, children } = props;\n\n // Memoise the transport so React doesn't churn the context identity every\n // render — re-rendering this host with stable inputs must not abort\n // in-flight hook calls.\n const transport = useMemo(\n () => new InMemoryMcpTransport(resources, tools ?? {}),\n [resources, tools],\n );\n\n const tree = resources[defaultResourceUri];\n const renderedTree: ReactNode = tree ? interpret(tree, registry) : children;\n\n return (\n <ExtensionRuntimeProvider transport={transport}>\n {renderedTree}\n {tree ? children : null}\n </ExtensionRuntimeProvider>\n );\n}\n","/**\n * In-realm bridge transport for plugin local-dev and contract testing.\n *\n * Calling `pushTheme(...)`, `pushLocale(...)`, etc. invokes the registered\n * subscriber callbacks **synchronously** — no serialisation, no port. This\n * lets Vitest + React Testing Library drive bridge state changes with `act()`\n * without a real MessageChannel.\n *\n * In production, the bridge client is `createPortBridgeClient` backed by a\n * real MessagePort. `InMemoryBridgeTransport` is the dev/test equivalent:\n * both expose the same `PortBridgeClient`-compatible subscriber API on the\n * consumer side, but `InMemoryBridgeTransport` also exposes the push-side\n * and the `onChromeRequest` handler for test assertions.\n */\nimport type { PortBridgeClient, ThemePayload, LocalePayload, DensityPayload, A11yPayload, NavPayload, SessionTokenPayload } from \"../plugin/bridge-client\";\n\ntype ChromeRequestHandler = (\n action: string,\n payload: Record<string, unknown>,\n) => Promise<unknown> | unknown;\n\nexport class InMemoryBridgeTransport implements PortBridgeClient {\n private _themeCb: ((p: ThemePayload) => void) | undefined;\n private _localeCb: ((p: LocalePayload) => void) | undefined;\n private _densityCb: ((p: DensityPayload) => void) | undefined;\n private _a11yCb: ((p: A11yPayload) => void) | undefined;\n private _navCb: ((p: NavPayload) => void) | undefined;\n private _tokenCb: ((p: SessionTokenPayload) => void) | undefined;\n private _chromeHandler: ChromeRequestHandler | undefined;\n\n // ── PortBridgeClient subscriber interface ──────────────────────────────────\n\n onTheme(cb: (p: ThemePayload) => void): void { this._themeCb = cb; }\n onLocale(cb: (p: LocalePayload) => void): void { this._localeCb = cb; }\n onDensity(cb: (p: DensityPayload) => void): void { this._densityCb = cb; }\n onA11y(cb: (p: A11yPayload) => void): void { this._a11yCb = cb; }\n onNav(cb: (p: NavPayload) => void): void { this._navCb = cb; }\n onSessionToken(cb: (p: SessionTokenPayload) => void): void { this._tokenCb = cb; }\n\n requestChrome(action: string, payload: Record<string, unknown>): Promise<unknown> {\n return this.simulateChromeRequest(action, payload);\n }\n\n announceA11y(_message: string, _politeness: \"polite\" | \"assertive\"): void {\n // No-op in the mock — tests assert via `onChromeRequest` or inspect DOM.\n }\n\n // ── Test / dev control surface ─────────────────────────────────────────────\n\n /** Push a theme update to the registered subscriber (synchronous). */\n pushTheme(payload: ThemePayload): void { this._themeCb?.(payload); }\n /** Push a locale update to the registered subscriber. */\n pushLocale(payload: LocalePayload): void { this._localeCb?.(payload); }\n /** Push a density update. */\n pushDensity(payload: DensityPayload): void { this._densityCb?.(payload); }\n /** Push a11y preference changes. */\n pushA11y(payload: A11yPayload): void { this._a11yCb?.(payload); }\n /** Push a nav state update. */\n pushNav(payload: NavPayload): void { this._navCb?.(payload); }\n /** Push a frontend-session token. */\n pushSessionToken(payload: SessionTokenPayload): void { this._tokenCb?.(payload); }\n\n /**\n * Register a handler for plugin→host chrome requests (toast, confirm, etc).\n * Called by `requestChrome` and by `simulateChromeRequest`.\n */\n onChromeRequest(handler: ChromeRequestHandler): void {\n this._chromeHandler = handler;\n }\n\n /**\n * Programmatically send a chrome request as if a plugin component called\n * `PortBridgeClient.requestChrome(...)`. Useful for test assertions.\n */\n async simulateChromeRequest(action: string, payload: Record<string, unknown>): Promise<unknown> {\n if (this._chromeHandler === undefined) {\n return null;\n }\n return this._chromeHandler(action, payload);\n }\n}\n","import { createContext, useContext } from \"react\";\nimport type { PortBridgeClient } from \"./bridge-client\";\n\n/**\n * React context carrying the plugin's active {@link PortBridgeClient}.\n * Provided by the host mount (WorkerMockHost in dev, real bridge in production)\n * and consumed by `useBridgeTheme`, `useBridgeLocale`, and the `plugin-ui` hooks.\n */\nexport const BridgeClientContext = createContext<PortBridgeClient | null>(null);\n\n/**\n * Returns the bridge client from context, throwing a clear error when missing.\n * Used by the `useBridge*` hooks to fail loudly on misconfiguration.\n */\nexport function useBridgeClient(): PortBridgeClient {\n const client = useContext(BridgeClientContext);\n if (client === null) {\n throw new Error(\n \"No PortBridgeClient available. Wrap your plugin in <BridgeClientProvider> \"\n + \"or ensure the host mount wires a BridgeClientContext.Provider.\",\n );\n }\n return client;\n}\n","/**\n * Mock host for plugins that use bridge hooks (`useBridgeTheme`,\n * `useBridgeLocale`, `useBridgeA11y`, etc.) during local-dev or contract\n * testing.\n *\n * Wraps {@link DeclarativeMockHost} and wires a {@link BridgeClientContext}\n * provider so any descendant bridge hook resolves against the supplied\n * `bridgeTransport` instead of throwing \"no bridge client available\".\n *\n * For tests where bridge state must be driven externally (push a new theme,\n * assert that a component re-renders), pass an {@link InMemoryBridgeTransport}\n * instance and call `transport.pushTheme(...)` wrapped in `act()`.\n */\nimport { type ReactElement } from \"react\";\nimport { DeclarativeMockHost, type DeclarativeMockHostProps } from \"./DeclarativeMockHost\";\nimport { BridgeClientContext } from \"../plugin/BridgeClientContext\";\nimport type { PortBridgeClient } from \"../plugin/bridge-client\";\n\nexport interface WorkerMockHostProps extends DeclarativeMockHostProps {\n /**\n * The bridge transport to wire into context. Pass an\n * {@link InMemoryBridgeTransport} for tests; pass a\n * `createPortBridgeClient(port)` instance for postMessage integration tests.\n */\n bridgeTransport: PortBridgeClient;\n}\n\n/**\n * Mock host that combines the SDUI declarative pipeline with bridge context.\n *\n * Rendering contract (same as DeclarativeMockHost):\n * - `defaultResourceUri` present in `resources` → renders the SDUI tree.\n * - URI absent → renders `children` instead (tool-invocation stubs, etc).\n *\n * Bridge contract:\n * - All `useBridgeTheme`, `useBridgeLocale`, etc. hooks in the subtree resolve\n * against `bridgeTransport`.\n */\nexport function WorkerMockHost(props: WorkerMockHostProps): ReactElement {\n const { bridgeTransport, ...declarativeProps } = props;\n\n return (\n <BridgeClientContext.Provider value={bridgeTransport}>\n <DeclarativeMockHost {...declarativeProps} />\n </BridgeClientContext.Provider>\n );\n}\n"]}
1
+ {"version":3,"sources":["../../src/host/declarative/interpreter.ts","../../src/plugin/clientPush.ts","../../src/plugin/hostIdentity.ts","../../src/plugin/PluginRealtimeContext.ts","../../src/plugin/ExtensionRuntimeProvider.tsx","../../src/mock-host/InMemoryMcpTransport.ts","../../src/mock-host/DeclarativeMockHost.tsx","../../src/mock-host/InMemoryBridgeTransport.ts","../../src/plugin/BridgeClientContext.ts","../../src/mock-host/WorkerMockHost.tsx"],"names":["createContext","useMemo","jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { createElement, type ReactElement, type ReactNode } from \"react\";\nimport type { ComponentRegistry } from \"./registry\";\nimport type { SduiNode } from \"./types\";\n\n/**\n * Walk a parsed SDUI tree and render it as a React element by looking up each\n * node's `type` in the supplied {@link ComponentRegistry}.\n *\n * The interpreter is purely structural:\n *\n * - It owns no UI styling, layout, or data fetching.\n * - It never reads `node.props` — props are forwarded opaquely to the host\n * component, which owns interpretation per primitive.\n * - It does not evaluate `node.bindings` — reactive rules are handled in a\n * separate task (E2.S2). For v1 the interpreter passes through the static\n * tree only.\n *\n * Each child is given a stable React `key` derived from its position so that\n * React's reconciler can identify list items across renders. The key is a\n * sibling-local index; the registry consumer is responsible for opting into a\n * stable identity if it has a domain-meaningful `props.key`.\n *\n * @throws Error when `node.type` is not present in the registry. This is the\n * fail-loud behaviour required by the closed v1 vocabulary — unknown\n * primitives must not silently degrade.\n */\nexport function interpret(node: SduiNode, registry: ComponentRegistry): ReactElement\n{\n const Component = registry[node.type];\n if (!Component)\n {\n throw new Error(`Unknown SDUI primitive: ${String(node.type)}`);\n }\n\n const children: ReactNode[] | undefined = node.children?.map(\n (child, index) => interpretChild(child, registry, index),\n );\n\n return createElement(Component, { props: node.props }, children);\n}\n\nfunction interpretChild(node: SduiNode, registry: ComponentRegistry, index: number): ReactElement\n{\n const Component = registry[node.type];\n if (!Component)\n {\n throw new Error(`Unknown SDUI primitive: ${String(node.type)}`);\n }\n\n const children: ReactNode[] | undefined = node.children?.map(\n (child, childIndex) => interpretChild(child, registry, childIndex),\n );\n\n return createElement(Component, { props: node.props, key: index }, children);\n}\n","import { createContext, useContext, useEffect, useRef } from \"react\";\n\n/**\n * A single client-push event delivered from the host to a plugin surface. The host\n * relays the plugin backend's `IClientPushPublisher` events over its realtime\n * channel (SignalR); the transport envelope's extension identity is bound by the\n * host at mount time, so the plugin sees only the event body.\n *\n * SCOPE + ORDERING: which channel/user/group an event concerns is carried INSIDE\n * `payloadJson` by the emitting plugin — the transport envelope intentionally has no\n * group field. Consumers therefore demultiplex + order by their own payload fields\n * (e.g. a per-channel sequence in the payload), NOT by {@link eventSequence}, which\n * is per-group at the host and would produce false gaps when multiple groups\n * multiplex over one connection.\n */\nexport interface ClientPushEvent\n{\n /** Plugin-defined discriminator, e.g. `\"chatMessageReceived\"`. */\n eventType: string;\n /** Raw JSON payload authored by the plugin backend. */\n payloadJson: string;\n /**\n * Host per-group monotonic sequence. Advisory only — do NOT use for\n * cross-group gap detection (see the scope note above).\n */\n eventSequence: number;\n}\n\nexport interface ClientPushSubscribeOptions\n{\n /**\n * Opaque group names to enrol in (e.g. `\"chat:channel:{id}\"`). The host\n * authorises each subscription via the plugin's `authorize-subscription` tool\n * and enforces org/extension isolation — a plugin cannot subscribe outside its\n * own extension + organisation.\n */\n groups: string[];\n /** Called for each delivered (non-resync) event for the subscribed groups. */\n onEvent: (event: ClientPushEvent) => void;\n /**\n * Called when the host signals a gap/resync for the subscribed groups (a\n * dropped-event backpressure signal, or a reconnect). The consumer should\n * re-fetch authoritative state (e.g. a delta/cold-load) rather than trusting\n * incremental events.\n */\n onResync?: () => void;\n}\n\n/**\n * Host-provided channel for realtime server-push. The channel is already scoped to\n * the mounted surface's extension + organisation (bound by the host from the trusted\n * mount descriptor — a plugin CANNOT widen it), so {@link subscribe} takes only\n * opaque group names and returns an unsubscribe function.\n */\nexport interface ClientPushChannel\n{\n subscribe(options: ClientPushSubscribeOptions): () => void;\n}\n\n/**\n * `null` = no host channel (standalone/mock, or a host that predates client-push) →\n * {@link useClientPushSubscription} is inert. Provided by\n * {@link ExtensionRuntimeProvider}'s optional `clientPush` prop.\n */\nexport const ClientPushContext = createContext<ClientPushChannel | null>(null);\n\nexport interface UseClientPushSubscriptionOptions\n{\n /** Opaque groups to subscribe. Changing the SET re-subscribes; identity/order changes alone do not. */\n groups: string[];\n onEvent: (event: ClientPushEvent) => void;\n onResync?: () => void;\n /** Gate the subscription (e.g. until an id is known). Default `true`. */\n enabled?: boolean;\n}\n\n/**\n * Subscribe a plugin surface to host client-push events for `groups`.\n *\n * Inert (no-op) when no host channel is present (standalone/mock), when `enabled` is\n * false, or when `groups` is empty. Re-subscribes when the group set changes and\n * unsubscribes on unmount. Callback identities are held in refs, so passing new\n * inline `onEvent`/`onResync` closures every render does NOT churn the subscription.\n */\nexport function useClientPushSubscription(options: UseClientPushSubscriptionOptions): void\n{\n const { groups, onEvent, onResync, enabled = true } = options;\n const channel = useContext(ClientPushContext);\n\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n const onResyncRef = useRef(onResync);\n onResyncRef.current = onResync;\n\n // Normalise (dedupe + sort) so the effect re-runs only when the group SET actually\n // changes — reordering the same groups, or passing a fresh array literal of the\n // same set each render, must NOT churn the subscription.\n const normalizedGroups = [...new Set(groups)].sort();\n // JSON-encode (not space-join) so group names containing a delimiter can't alias\n // distinct sets to the same key (e.g. [\"a b\"] vs [\"a\",\"b\"]).\n const groupsKey = JSON.stringify(normalizedGroups);\n\n useEffect(() =>\n {\n if (!channel || !enabled || normalizedGroups.length === 0)\n {\n return;\n }\n const unsubscribe = channel.subscribe({\n groups: normalizedGroups,\n onEvent: (event) => onEventRef.current(event),\n onResync: () => onResyncRef.current?.(),\n });\n return unsubscribe;\n // normalizedGroups is captured via groupsKey; callbacks via refs — intentionally excluded.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [channel, enabled, groupsKey]);\n}\n","/**\n * Host identity/permission seam for platform-react plugin surfaces (WI 5108, F-AUTH-SEAM).\n *\n * Mirrors the clientPush seam: the host binds a HostIdentity to the mounted surface and\n * provides it via ExtensionRuntimeProvider's `identity` prop; the plugin reads it with\n * `useHostIdentity`. Inert (returns null) when no host provides the context — standalone /\n * mock, or a host that predates this feature.\n *\n * SECURITY: this is presentation/UX data. The plugin BACKEND re-authorises every MCP call\n * from the server session; `permission` is NOT an authorization boundary. The host forwards\n * ONLY the caller's grant for the surface's own extension (least-privilege), never the full set.\n */\nimport { createContext, useContext } from \"react\";\n\n/** Current-user identity for a mounted plugin surface, bound by the host. */\nexport interface HostIdentityUser\n{\n id: string;\n firstName: string;\n lastName: string;\n fullName: string;\n isExternal: boolean;\n}\n\n/** One resolved permission grant (bitMask over the plugin's PermissionMask bits). */\nexport interface HostPermission\n{\n groupCode: string;\n bitMask: number;\n}\n\n/** Host-provided identity context for the mounted surface. */\nexport interface HostIdentity\n{\n /** null while host auth is still loading (see isLoading) OR when unauthenticated. */\n user: HostIdentityUser | null;\n /** true while the host's /auth/user resolution is in flight — disambiguates loading from unauthenticated. */\n isLoading: boolean;\n /** The mounted surface's OWN grant only, or null when the user has no grant for this extension. */\n permission: HostPermission | null;\n /** The mounted surface's own extension groupCode, host-bound from the trusted manifest. */\n extensionGroupCode: string;\n /**\n * The active organisation id for the mounted surface, or null while host auth is loading /\n * unauthenticated. Host-bound from the SPA's active-organisation context. Surfaces that scope\n * realtime subscriptions or org-keyed queries read this (e.g. the chat client-push gate); it is\n * NOT a security token — the plugin backend derives org from the server session independently.\n */\n organisationId: string | null;\n}\n\n/**\n * `null` = no host channel (standalone/mock, or a host that predates the identity seam) →\n * {@link useHostIdentity} returns null and the plugin falls back to its deny-by-default path.\n * Provided by {@link ExtensionRuntimeProvider}'s optional `identity` prop.\n */\nexport const HostIdentityContext = createContext<HostIdentity | null>(null);\n\n/** Returns the host identity for the mounted surface, or null when no host context is present. */\nexport function useHostIdentity(): HostIdentity | null\n{\n return useContext(HostIdentityContext);\n}\n","import { createContext, useContext } from \"react\";\n\n/**\n * A host-supplied realtime subscription source for a plugin.\n *\n * The SDK keeps this intentionally dumb — it only calls `source.subscribe`.\n * All SignalR wiring, extensionId filtering, and connection lifecycle management\n * live host-side (Task 6 in coreconnect-web). This lets the SDK be tested with\n * a simple fake source.\n */\nexport interface PluginRealtimeSource\n{\n /**\n * Subscribe to notifications whose `typeCode` matches the given value.\n *\n * @param typeCode The application-level event type code to filter on\n * (e.g. `\"HelpdeskTicketCreated\"`). Filtering by\n * extensionId is the host's responsibility.\n * @param handler Called with the raw notification payload whenever a\n * matching notification arrives.\n * @returns An unsubscribe function. Calling it removes this handler.\n */\n subscribe(typeCode: string, handler: (payload: unknown) => void): () => void;\n}\n\n/**\n * React context carrying the plugin's active {@link PluginRealtimeSource}.\n *\n * `null` is the explicit \"not provided\" sentinel — hooks must treat null as a\n * clean no-op (dev/mock/no-connection) rather than an error.\n *\n * Provided by {@link ExtensionRuntimeProvider} when the host passes a\n * `realtime` prop; consumed by `usePluginRealtimeSource()`.\n */\nexport const PluginRealtimeContext = createContext<PluginRealtimeSource | null>(null);\n\n/**\n * Returns the {@link PluginRealtimeSource} from context, or `null` when none\n * is wired (dev/mock environments, unit tests that only care about MCP).\n *\n * Hooks built on top of this (e.g. `usePluginRealtime` in `plugin-ui`) should\n * skip their subscription entirely when this returns `null`.\n */\nexport function usePluginRealtimeSource(): PluginRealtimeSource | null\n{\n return useContext(PluginRealtimeContext);\n}\n","import { createContext, useContext, useMemo, type ReactNode } from \"react\";\nimport type { McpTransport } from \"./transport\";\nimport { ClientPushContext, type ClientPushChannel } from \"./clientPush\";\nimport { HostIdentityContext, type HostIdentity } from \"./hostIdentity\";\nimport { PluginRealtimeContext, type PluginRealtimeSource } from \"./PluginRealtimeContext\";\n\n/**\n * React context carrying the {@link McpTransport} the plugin should use to\n * reach the host. `null` is the explicit \"not provided\" sentinel so the hooks\n * can disambiguate from a transport that was provided but is incidentally\n * falsy in some other dimension.\n */\nconst ExtensionRuntimeContext = createContext<McpTransport | null>(null);\n\nexport interface ExtensionRuntimeProviderProps\n{\n transport: McpTransport;\n /**\n * Optional host realtime channel consumed by {@link useClientPushSubscription}.\n * Absent (or `null`) in standalone/mock hosts and hosts that predate client-push,\n * in which case the hook is inert. The host binds this channel to the mounted\n * surface's trusted extension + organisation identity.\n */\n clientPush?: ClientPushChannel | null;\n /**\n * Optional host identity/permission context consumed by {@link useHostIdentity}.\n * Absent (or `null`) in standalone/mock hosts and hosts that predate this seam,\n * in which case the hook is inert. The host binds this to the mounted surface's\n * trusted extension identity and forwards only that extension's own grant.\n */\n identity?: HostIdentity | null;\n /**\n * Optional realtime subscription source supplied by the host.\n *\n * When provided, descendant components can call `usePluginRealtimeSource()`\n * to obtain it and subscribe to push notifications. When omitted (dev/mock\n * environments or plugins that don't need realtime), the context defaults\n * to `null` and consumers no-op cleanly.\n */\n realtime?: PluginRealtimeSource;\n children?: ReactNode;\n}\n\n/**\n * Wrap a plugin's React tree so descendant {@link useMcpResource} and\n * {@link useMcpTool} calls resolve a default transport without having to\n * thread it through every component.\n *\n * Hooks still accept a per-call `transport` override, which takes precedence\n * over the context value — useful for tests and for plugins that want to\n * shard work across multiple hosts.\n */\nexport function ExtensionRuntimeProvider({ transport, clientPush = null, identity = null, realtime, children }: ExtensionRuntimeProviderProps): ReactNode\n{\n // Memoise so swapping `children` doesn't churn the context identity.\n const value = useMemo(() => transport, [transport]);\n const push = useMemo(() => clientPush, [clientPush]);\n const id = useMemo(() => identity, [identity]);\n const realtimeValue = useMemo(() => realtime ?? null, [realtime]);\n return (\n <ExtensionRuntimeContext.Provider value={value}>\n <ClientPushContext.Provider value={push}>\n <HostIdentityContext.Provider value={id}>\n <PluginRealtimeContext.Provider value={realtimeValue}>\n {children}\n </PluginRealtimeContext.Provider>\n </HostIdentityContext.Provider>\n </ClientPushContext.Provider>\n </ExtensionRuntimeContext.Provider>\n );\n}\n\n/**\n * Internal helper used by the hooks. Returns the explicit override when\n * supplied, otherwise falls back to the context. Throws a deterministic\n * error if neither is available so misconfiguration fails loudly at the\n * first render rather than producing silent no-ops.\n */\nexport function useExtensionRuntimeTransport(override?: McpTransport): McpTransport\n{\n const fromContext = useContext(ExtensionRuntimeContext);\n const resolved = override ?? fromContext;\n if (!resolved)\n {\n throw new Error(\n \"No McpTransport available. Wrap your plugin in <ExtensionRuntimeProvider transport={...}> \"\n + \"or pass `transport` directly to the hook.\",\n );\n }\n return resolved;\n}\n","import type { McpTransport, UploadDocumentMeta, UploadDocumentResult } from \"../plugin/transport\";\nimport type { SduiNode } from \"../host/declarative/types\";\n\n/**\n * Handler for a mocked tool invocation. The handler receives the request\n * payload supplied by the caller and may return synchronously or\n * asynchronously. The result is forwarded verbatim through\n * {@link InMemoryMcpTransport.invokeTool}.\n */\nexport type MockToolHandler = (args: unknown) => Promise<unknown> | unknown;\n\n/**\n * Handler for a mocked document upload. Receives the {@link UploadDocumentMeta}\n * and the transferred bytes; returns the {@link UploadDocumentResult} the FE\n * hook resolves. Optional — when omitted, the transport returns a synthetic\n * result echoing the metadata so a standalone plugin can exercise the flow.\n */\nexport type MockUploadHandler = (\n meta: UploadDocumentMeta,\n buffer: ArrayBuffer,\n signal?: AbortSignal,\n) => Promise<UploadDocumentResult> | UploadDocumentResult;\n\n/**\n * An in-memory {@link McpTransport} backed by a `{ resources, tools }` map.\n *\n * Used by {@link DeclarativeMockHost} so plugin authors can run their app\n * standalone for local development without a real host. The transport mirrors\n * the runtime contract exactly:\n *\n * - `getResource(uri)` resolves a `SduiNode` keyed by URI, or rejects with a\n * descriptive error if the URI is not registered.\n * - `invokeTool(name, args)` dispatches to a synchronous or async handler,\n * or rejects if the tool name is unknown.\n *\n * `getResource` / `invokeTool` intentionally do NOT honour the supplied\n * `AbortSignal` — mock handlers are synchronous from the caller's perspective\n * and there is no in-flight network call to abort. Hooks still work correctly\n * because they treat the `AbortSignal` as a one-way notification, not a\n * contract. `uploadDocument` DOES observe the signal (rejecting with an\n * `AbortError`): its contract mandates it, a mock upload handler may be\n * genuinely async, and dev-host flows need to simulate upload cancellation.\n */\nexport class InMemoryMcpTransport implements McpTransport\n{\n private readonly resources: Record<string, SduiNode>;\n private readonly tools: Record<string, MockToolHandler>;\n private readonly uploadHandler?: MockUploadHandler;\n\n public constructor(\n resources: Record<string, SduiNode>,\n tools: Record<string, MockToolHandler> = {},\n uploadHandler?: MockUploadHandler,\n )\n {\n this.resources = resources;\n this.tools = tools;\n this.uploadHandler = uploadHandler;\n }\n\n public async getResource<T>(uri: string): Promise<{ uri: string; data: T }>\n {\n const data = this.resources[uri];\n if (data === undefined)\n {\n throw new Error(`Mock resource not found: ${uri}`);\n }\n return { uri, data: data as unknown as T };\n }\n\n public async invokeTool<TReq, TRes>(name: string, args: TReq): Promise<TRes>\n {\n const tool = this.tools[name];\n if (!tool)\n {\n throw new Error(`Mock tool not found: ${name}`);\n }\n const result = await tool(args);\n return result as TRes;\n }\n\n public async uploadDocument(\n meta: UploadDocumentMeta,\n buffer: ArrayBuffer,\n signal?: AbortSignal,\n ): Promise<UploadDocumentResult>\n {\n if (signal?.aborted === true)\n {\n throw abortError();\n }\n const produce = (): Promise<UploadDocumentResult> | UploadDocumentResult =>\n {\n if (this.uploadHandler)\n {\n return this.uploadHandler(meta, buffer, signal);\n }\n // Synthetic default: echo the metadata with a generated id so a\n // standalone plugin can drive the upload flow without a real host.\n return {\n storedDocumentId: `mock-${meta.entityName}-${meta.fileName}`,\n fileName: meta.fileName,\n contentType: meta.contentType,\n sizeBytes: buffer.byteLength,\n };\n };\n if (signal === undefined)\n {\n return produce();\n }\n // Honour cancellation for an async upload handler: reject as soon as the\n // signal fires rather than waiting for the handler to settle.\n return new Promise<UploadDocumentResult>((resolve, reject) =>\n {\n const onAbort = (): void => reject(abortError());\n signal.addEventListener(\"abort\", onAbort, { once: true });\n Promise.resolve(produce()).then(\n (result) =>\n {\n signal.removeEventListener(\"abort\", onAbort);\n resolve(result);\n },\n (err: unknown) =>\n {\n signal.removeEventListener(\"abort\", onAbort);\n reject(err);\n },\n );\n });\n }\n}\n\n/** An `AbortError`-shaped `Error`, matching native fetch cancellation. */\nfunction abortError(): Error\n{\n const e = new Error(\"Aborted\");\n e.name = \"AbortError\";\n return e;\n}\n","import { useMemo, type ReactElement, type ReactNode } from \"react\";\nimport { interpret } from \"../host/declarative/interpreter\";\nimport type { ComponentRegistry } from \"../host/declarative/registry\";\nimport type { SduiNode } from \"../host/declarative/types\";\nimport { ExtensionRuntimeProvider } from \"../plugin/ExtensionRuntimeProvider\";\nimport { InMemoryMcpTransport, type MockToolHandler } from \"./InMemoryMcpTransport\";\n\n/**\n * Props for {@link DeclarativeMockHost}.\n *\n * Plugin authors `npm link` the runtime and render `<DeclarativeMockHost>` in\n * their local dev app to exercise the same declarative pipeline the real host\n * uses, but backed by in-memory fakes instead of the platform.\n */\nexport interface DeclarativeMockHostProps\n{\n /**\n * In-memory resource map. Keys are MCP resource URIs; values are SDUI trees\n * that {@link interpret} will render against the supplied registry.\n */\n resources: Record<string, SduiNode>;\n\n /**\n * In-memory tool map. Keys are MCP tool names; values are handlers invoked\n * when a child component calls `useMcpTool(name).invoke(args)`.\n *\n * Handlers may be sync or async — the transport awaits the result before\n * forwarding it to the caller.\n */\n tools?: Record<string, MockToolHandler>;\n\n /**\n * URI of the resource rendered as the host's default tree. If the URI is\n * not present in `resources` the host renders the supplied `children`\n * instead — useful for stubs that exercise only tool invocations.\n */\n defaultResourceUri: string;\n\n /**\n * The same primitive → component registry the real host uses. Passed\n * verbatim to {@link interpret}; the mock host owns no UI of its own.\n */\n registry: ComponentRegistry;\n\n /**\n * Optional fallback content rendered when `defaultResourceUri` does not\n * resolve to a registered resource. Children also have access to the wired\n * transport via {@link ExtensionRuntimeProvider}, so they can invoke\n * mocked tools and resources directly through the React hooks.\n */\n children?: ReactNode;\n}\n\n/**\n * In-memory host for declarative (Contract A) plugin local-dev.\n *\n * Renders a plugin's SDUI resource against the supplied registry and wires an\n * {@link InMemoryMcpTransport} into context so descendant components that use\n * `useMcpResource` / `useMcpTool` resolve against the same fakes.\n *\n * The host is intentionally minimal: it does not simulate permissions, theme\n * propagation, or capability tokens. Its purpose is to exercise the\n * declarative pipeline end-to-end against deterministic in-memory data so\n * plugin authors can iterate without standing up the real platform.\n */\nexport function DeclarativeMockHost(props: DeclarativeMockHostProps): ReactElement\n{\n const { resources, tools, defaultResourceUri, registry, children } = props;\n\n // Memoise the transport so React doesn't churn the context identity every\n // render — re-rendering this host with stable inputs must not abort\n // in-flight hook calls.\n const transport = useMemo(\n () => new InMemoryMcpTransport(resources, tools ?? {}),\n [resources, tools],\n );\n\n const tree = resources[defaultResourceUri];\n const renderedTree: ReactNode = tree ? interpret(tree, registry) : children;\n\n return (\n <ExtensionRuntimeProvider transport={transport}>\n {renderedTree}\n {tree ? children : null}\n </ExtensionRuntimeProvider>\n );\n}\n","/**\n * In-realm bridge transport for plugin local-dev and contract testing.\n *\n * Calling `pushTheme(...)`, `pushLocale(...)`, etc. invokes the registered\n * subscriber callbacks **synchronously** — no serialisation, no port. This\n * lets Vitest + React Testing Library drive bridge state changes with `act()`\n * without a real MessageChannel.\n *\n * In production, the bridge client is `createPortBridgeClient` backed by a\n * real MessagePort. `InMemoryBridgeTransport` is the dev/test equivalent:\n * both expose the same `PortBridgeClient`-compatible subscriber API on the\n * consumer side, but `InMemoryBridgeTransport` also exposes the push-side\n * and the `onChromeRequest` handler for test assertions.\n */\nimport type { PortBridgeClient, ThemePayload, LocalePayload, DensityPayload, A11yPayload, NavPayload, SessionTokenPayload } from \"../plugin/bridge-client\";\n\ntype ChromeRequestHandler = (\n action: string,\n payload: Record<string, unknown>,\n) => Promise<unknown> | unknown;\n\nexport class InMemoryBridgeTransport implements PortBridgeClient {\n private _themeCb: ((p: ThemePayload) => void) | undefined;\n private _localeCb: ((p: LocalePayload) => void) | undefined;\n private _densityCb: ((p: DensityPayload) => void) | undefined;\n private _a11yCb: ((p: A11yPayload) => void) | undefined;\n private _navCb: ((p: NavPayload) => void) | undefined;\n private _tokenCb: ((p: SessionTokenPayload) => void) | undefined;\n private _chromeHandler: ChromeRequestHandler | undefined;\n\n // ── PortBridgeClient subscriber interface ──────────────────────────────────\n\n onTheme(cb: (p: ThemePayload) => void): void { this._themeCb = cb; }\n onLocale(cb: (p: LocalePayload) => void): void { this._localeCb = cb; }\n onDensity(cb: (p: DensityPayload) => void): void { this._densityCb = cb; }\n onA11y(cb: (p: A11yPayload) => void): void { this._a11yCb = cb; }\n onNav(cb: (p: NavPayload) => void): void { this._navCb = cb; }\n onSessionToken(cb: (p: SessionTokenPayload) => void): void { this._tokenCb = cb; }\n\n requestChrome(action: string, payload: Record<string, unknown>): Promise<unknown> {\n return this.simulateChromeRequest(action, payload);\n }\n\n announceA11y(_message: string, _politeness: \"polite\" | \"assertive\"): void {\n // No-op in the mock — tests assert via `onChromeRequest` or inspect DOM.\n }\n\n // ── Test / dev control surface ─────────────────────────────────────────────\n\n /** Push a theme update to the registered subscriber (synchronous). */\n pushTheme(payload: ThemePayload): void { this._themeCb?.(payload); }\n /** Push a locale update to the registered subscriber. */\n pushLocale(payload: LocalePayload): void { this._localeCb?.(payload); }\n /** Push a density update. */\n pushDensity(payload: DensityPayload): void { this._densityCb?.(payload); }\n /** Push a11y preference changes. */\n pushA11y(payload: A11yPayload): void { this._a11yCb?.(payload); }\n /** Push a nav state update. */\n pushNav(payload: NavPayload): void { this._navCb?.(payload); }\n /** Push a frontend-session token. */\n pushSessionToken(payload: SessionTokenPayload): void { this._tokenCb?.(payload); }\n\n /**\n * Register a handler for plugin→host chrome requests (toast, confirm, etc).\n * Called by `requestChrome` and by `simulateChromeRequest`.\n */\n onChromeRequest(handler: ChromeRequestHandler): void {\n this._chromeHandler = handler;\n }\n\n /**\n * Programmatically send a chrome request as if a plugin component called\n * `PortBridgeClient.requestChrome(...)`. Useful for test assertions.\n */\n async simulateChromeRequest(action: string, payload: Record<string, unknown>): Promise<unknown> {\n if (this._chromeHandler === undefined) {\n return null;\n }\n return this._chromeHandler(action, payload);\n }\n}\n","import { createContext, useContext } from \"react\";\nimport type { PortBridgeClient } from \"./bridge-client\";\n\n/**\n * React context carrying the plugin's active {@link PortBridgeClient}.\n * Provided by the host mount (WorkerMockHost in dev, real bridge in production)\n * and consumed by `useBridgeTheme`, `useBridgeLocale`, and the `plugin-ui` hooks.\n */\nexport const BridgeClientContext = createContext<PortBridgeClient | null>(null);\n\n/**\n * Returns the bridge client from context, throwing a clear error when missing.\n * Used by the `useBridge*` hooks to fail loudly on misconfiguration.\n */\nexport function useBridgeClient(): PortBridgeClient {\n const client = useContext(BridgeClientContext);\n if (client === null) {\n throw new Error(\n \"No PortBridgeClient available. Wrap your plugin in <BridgeClientProvider> \"\n + \"or ensure the host mount wires a BridgeClientContext.Provider.\",\n );\n }\n return client;\n}\n","/**\n * Mock host for plugins that use bridge hooks (`useBridgeTheme`,\n * `useBridgeLocale`, `useBridgeA11y`, etc.) during local-dev or contract\n * testing.\n *\n * Wraps {@link DeclarativeMockHost} and wires a {@link BridgeClientContext}\n * provider so any descendant bridge hook resolves against the supplied\n * `bridgeTransport` instead of throwing \"no bridge client available\".\n *\n * For tests where bridge state must be driven externally (push a new theme,\n * assert that a component re-renders), pass an {@link InMemoryBridgeTransport}\n * instance and call `transport.pushTheme(...)` wrapped in `act()`.\n */\nimport { type ReactElement } from \"react\";\nimport { DeclarativeMockHost, type DeclarativeMockHostProps } from \"./DeclarativeMockHost\";\nimport { BridgeClientContext } from \"../plugin/BridgeClientContext\";\nimport type { PortBridgeClient } from \"../plugin/bridge-client\";\n\nexport interface WorkerMockHostProps extends DeclarativeMockHostProps {\n /**\n * The bridge transport to wire into context. Pass an\n * {@link InMemoryBridgeTransport} for tests; pass a\n * `createPortBridgeClient(port)` instance for postMessage integration tests.\n */\n bridgeTransport: PortBridgeClient;\n}\n\n/**\n * Mock host that combines the SDUI declarative pipeline with bridge context.\n *\n * Rendering contract (same as DeclarativeMockHost):\n * - `defaultResourceUri` present in `resources` → renders the SDUI tree.\n * - URI absent → renders `children` instead (tool-invocation stubs, etc).\n *\n * Bridge contract:\n * - All `useBridgeTheme`, `useBridgeLocale`, etc. hooks in the subtree resolve\n * against `bridgeTransport`.\n */\nexport function WorkerMockHost(props: WorkerMockHostProps): ReactElement {\n const { bridgeTransport, ...declarativeProps } = props;\n\n return (\n <BridgeClientContext.Provider value={bridgeTransport}>\n <DeclarativeMockHost {...declarativeProps} />\n </BridgeClientContext.Provider>\n );\n}\n"]}
@@ -473,13 +473,15 @@ var WIRE_INVOKE_TOOL_REQUEST = "ethisys:mcp:invokeTool";
473
473
  var WIRE_INVOKE_TOOL_RESULT = "ethisys:mcp:invokeTool:result";
474
474
  var WIRE_GET_RESOURCE_REQUEST = "ethisys:mcp:getResource";
475
475
  var WIRE_GET_RESOURCE_RESULT = "ethisys:mcp:getResource:result";
476
+ var WIRE_UPLOAD_DOCUMENT_REQUEST = "ethisys:mcp:uploadDocument";
477
+ var WIRE_UPLOAD_DOCUMENT_RESULT = "ethisys:mcp:uploadDocument:result";
476
478
  var WIRE_ABORT = "ethisys:mcp:abort";
477
479
  function isMcpResultEnvelope(value) {
478
480
  if (value === null || typeof value !== "object") {
479
481
  return false;
480
482
  }
481
483
  const v = value;
482
- return (v.type === WIRE_INVOKE_TOOL_RESULT || v.type === WIRE_GET_RESOURCE_RESULT) && typeof v.id === "string";
484
+ return (v.type === WIRE_INVOKE_TOOL_RESULT || v.type === WIRE_GET_RESOURCE_RESULT || v.type === WIRE_UPLOAD_DOCUMENT_RESULT) && typeof v.id === "string";
483
485
  }
484
486
  function createPortMcpTransport(port, options = {}) {
485
487
  if (port === void 0 || port === null) {
@@ -515,7 +517,7 @@ function createPortMcpTransport(port, options = {}) {
515
517
  if (!options.portShimForTests && typeof realPort.start === "function") {
516
518
  realPort.start();
517
519
  }
518
- function send(requestType, body, signal) {
520
+ function send(requestType, body, signal, transfer) {
519
521
  const id = factory();
520
522
  return new Promise((resolve, reject) => {
521
523
  if (signal?.aborted === true) {
@@ -532,7 +534,7 @@ function createPortMcpTransport(port, options = {}) {
532
534
  reject(abortError());
533
535
  };
534
536
  signal?.addEventListener("abort", onAbort, { once: true });
535
- shim.postMessage({ type: requestType, id, ...body });
537
+ shim.postMessage({ type: requestType, id, ...body }, transfer);
536
538
  });
537
539
  }
538
540
  return {
@@ -543,6 +545,15 @@ function createPortMcpTransport(port, options = {}) {
543
545
  async invokeTool(name, args, signal) {
544
546
  const data = await send(WIRE_INVOKE_TOOL_REQUEST, { name, args }, signal);
545
547
  return data;
548
+ },
549
+ async uploadDocument(meta, buffer, signal) {
550
+ const data = await send(
551
+ WIRE_UPLOAD_DOCUMENT_REQUEST,
552
+ { meta, buffer },
553
+ signal,
554
+ [buffer]
555
+ );
556
+ return data;
546
557
  }
547
558
  };
548
559
  }
@@ -814,6 +825,7 @@ exports.useBridgeClient = useBridgeClient;
814
825
  exports.useBridgeLocale = useBridgeLocale;
815
826
  exports.useBridgeTheme = useBridgeTheme;
816
827
  exports.useClientPushSubscription = useClientPushSubscription;
828
+ exports.useExtensionRuntimeTransport = useExtensionRuntimeTransport;
817
829
  exports.useFrontendSessionToken = useFrontendSessionToken;
818
830
  exports.useHostIdentity = useHostIdentity;
819
831
  exports.useMcpQuery = useMcpQuery;