@ethisyscore/extension-runtime 1.135.0 → 1.137.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 (34) hide show
  1. package/README.md +2 -2
  2. package/dist/MockPluginStorage-BM3bq99B.d.cts +35 -0
  3. package/dist/MockPluginStorage-BndTX2RR.d.ts +35 -0
  4. package/dist/{bridge-client-RJOl8WlU.d.ts → bridge-client-CkxYcZcm.d.ts} +1 -1
  5. package/dist/{bridge-client-D9EYFq1l.d.cts → bridge-client-DkogtCvn.d.cts} +1 -1
  6. package/dist/{bridge-envelopes-M5a42rAy.d.cts → bridge-envelopes-BcKu-nQm.d.cts} +72 -1
  7. package/dist/{bridge-envelopes-M5a42rAy.d.ts → bridge-envelopes-BcKu-nQm.d.ts} +72 -1
  8. package/dist/host/index.cjs +57 -2
  9. package/dist/host/index.cjs.map +1 -1
  10. package/dist/host/index.d.cts +3 -3
  11. package/dist/host/index.d.ts +3 -3
  12. package/dist/host/index.js +57 -2
  13. package/dist/host/index.js.map +1 -1
  14. package/dist/mock-host/cli.cjs +273 -7
  15. package/dist/mock-host/cli.cjs.map +1 -1
  16. package/dist/mock-host/cli.d.cts +10 -3
  17. package/dist/mock-host/cli.d.ts +10 -3
  18. package/dist/mock-host/cli.js +272 -8
  19. package/dist/mock-host/cli.js.map +1 -1
  20. package/dist/mock-host/index.cjs +236 -1
  21. package/dist/mock-host/index.cjs.map +1 -1
  22. package/dist/mock-host/index.d.cts +14 -4
  23. package/dist/mock-host/index.d.ts +14 -4
  24. package/dist/mock-host/index.js +236 -2
  25. package/dist/mock-host/index.js.map +1 -1
  26. package/dist/plugin/index.cjs +378 -100
  27. package/dist/plugin/index.cjs.map +1 -1
  28. package/dist/plugin/index.d.cts +171 -6
  29. package/dist/plugin/index.d.ts +171 -6
  30. package/dist/plugin/index.js +366 -101
  31. package/dist/plugin/index.js.map +1 -1
  32. package/dist/{transport-UaoDRdm3.d.cts → transport-D1lE-5nt.d.cts} +30 -3
  33. package/dist/{transport-CFzJQiFG.d.ts → transport-INVtDo2s.d.ts} +30 -3
  34. package/package.json +1 -1
@@ -1,8 +1,10 @@
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, U as UploadDocumentMeta, f as UploadDocumentResult } from '../bridge-envelopes-M5a42rAy.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-RJOl8WlU.js';
4
+ import { M as McpTransport, U as UploadDocumentMeta, f as UploadDocumentResult, g as UploadToStorageMeta, h as UploadToStorageResult } from '../bridge-envelopes-BcKu-nQm.js';
5
+ import { M as MockPluginStorage } from '../MockPluginStorage-BndTX2RR.js';
6
+ export { a as MockStoredFile } from '../MockPluginStorage-BndTX2RR.js';
7
+ 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-CkxYcZcm.js';
6
8
 
7
9
  /**
8
10
  * Handler for a mocked tool invocation. The handler receives the request
@@ -37,12 +39,20 @@ type MockUploadHandler = (meta: UploadDocumentMeta, buffer: ArrayBuffer, signal?
37
39
  * contract. `uploadDocument` DOES observe the signal (rejecting with an
38
40
  * `AbortError`): its contract mandates it, a mock upload handler may be
39
41
  * genuinely async, and dev-host flows need to simulate upload cancellation.
42
+ *
43
+ * `uploadToStorage` writes into {@link pluginStorage}, an in-memory {@link MockPluginStorage}, and
44
+ * returns a server-style `{prefix}/{guid}{ext}` path. It validates the prefix and size with the
45
+ * kernel's rules, rejecting with a typed `PluginStorageUploadError` (status 400 / 413), and
46
+ * honours the signal.
40
47
  */
41
48
  declare class InMemoryMcpTransport implements McpTransport {
42
49
  private readonly resources;
43
50
  private readonly tools;
44
51
  private readonly uploadHandler?;
45
- constructor(resources: Record<string, SduiNode>, tools?: Record<string, MockToolHandler>, uploadHandler?: MockUploadHandler);
52
+ /** Where `uploadToStorage` writes. Read it back in a test, or from devtools under `dev:mock`. */
53
+ readonly pluginStorage: MockPluginStorage;
54
+ constructor(resources: Record<string, SduiNode>, tools?: Record<string, MockToolHandler>, uploadHandler?: MockUploadHandler, pluginStorage?: MockPluginStorage);
55
+ uploadToStorage(meta: UploadToStorageMeta, buffer: ArrayBuffer, signal?: AbortSignal): Promise<UploadToStorageResult>;
46
56
  getResource<T>(uri: string): Promise<{
47
57
  uri: string;
48
58
  data: T;
@@ -196,4 +206,4 @@ interface WorkerMockHostProps extends DeclarativeMockHostProps {
196
206
  */
197
207
  declare function WorkerMockHost(props: WorkerMockHostProps): ReactElement;
198
208
 
199
- export { DeclarativeMockHost, type DeclarativeMockHostProps, InMemoryBridgeTransport, InMemoryMcpTransport, type MockToolHandler, WorkerMockHost, type WorkerMockHostProps };
209
+ export { DeclarativeMockHost, type DeclarativeMockHostProps, InMemoryBridgeTransport, InMemoryMcpTransport, MockPluginStorage, type MockToolHandler, WorkerMockHost, type WorkerMockHostProps };
@@ -34,15 +34,249 @@ function ExtensionRuntimeProvider({ transport, clientPush = null, identity = nul
34
34
  return /* @__PURE__ */ jsx(ExtensionRuntimeContext.Provider, { value, children: /* @__PURE__ */ jsx(ClientPushContext.Provider, { value: push, children: /* @__PURE__ */ jsx(HostIdentityContext.Provider, { value: id, children: /* @__PURE__ */ jsx(PluginRealtimeContext.Provider, { value: realtimeValue, children }) }) }) });
35
35
  }
36
36
 
37
+ // src/bridge/mcp-error.ts
38
+ var RETRYABLE = ["rate_limited", "timeout", "unavailable"];
39
+ function isRetryableMcpErrorCode(code) {
40
+ return RETRYABLE.includes(code);
41
+ }
42
+ var McpToolError = class extends Error {
43
+ /** Machine-readable classification. `internal` when the host sent none. */
44
+ code;
45
+ /**
46
+ * Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype
47
+ * check because the error can be constructed in one bundle and inspected in another, where
48
+ * `instanceof` compares two different class objects and answers false.
49
+ */
50
+ isMcpToolError = true;
51
+ constructor(message, code = "internal") {
52
+ super(message);
53
+ this.name = "McpHostError";
54
+ this.code = code;
55
+ }
56
+ /** True when this failure is worth retrying unchanged. */
57
+ get retryable() {
58
+ return isRetryableMcpErrorCode(this.code);
59
+ }
60
+ };
61
+ function mcpErrorCodeFromHttpStatus(status) {
62
+ switch (status) {
63
+ case 401:
64
+ return "unauthorized";
65
+ case 403:
66
+ return "forbidden";
67
+ case 404:
68
+ return "not_found";
69
+ case 400:
70
+ case 422:
71
+ return "invalid_request";
72
+ case 409:
73
+ case 412:
74
+ return "conflict";
75
+ case 429:
76
+ return "rate_limited";
77
+ case 408:
78
+ case 504:
79
+ return "timeout";
80
+ case 502:
81
+ case 503:
82
+ return "unavailable";
83
+ default:
84
+ return "internal";
85
+ }
86
+ }
87
+
88
+ // src/plugin/pluginStorageUpload.ts
89
+ var PLUGIN_STORAGE_UPLOAD_MAX_BYTES = 30 * 1024 * 1024;
90
+ var PLUGIN_STORAGE_PREFIX_MAX_LENGTH = 84;
91
+ var PLUGIN_STORAGE_PATH_MAX_LENGTH = 128;
92
+ var PluginStorageUploadError = class extends McpToolError {
93
+ /** What went wrong, in terms a caller branches on. */
94
+ reason;
95
+ /** HTTP status from the server, when the host surfaced one. */
96
+ status;
97
+ /** Structural marker for {@link isPluginStorageUploadError}. */
98
+ isPluginStorageUploadError = true;
99
+ constructor(message, reason, options = {}) {
100
+ super(message, options.code ?? codeForReason(reason, options.status));
101
+ this.name = "PluginStorageUploadError";
102
+ this.reason = reason;
103
+ this.status = options.status;
104
+ }
105
+ };
106
+ function validatePluginStoragePrefix(raw) {
107
+ if (typeof raw !== "string" || raw.trim().length === 0) {
108
+ return { ok: false, error: "A storage path prefix is required." };
109
+ }
110
+ if (raw.length > PLUGIN_STORAGE_PREFIX_MAX_LENGTH) {
111
+ return {
112
+ ok: false,
113
+ error: `The storage path prefix must be at most ${PLUGIN_STORAGE_PREFIX_MAX_LENGTH} characters, so the stored path fits ${PLUGIN_STORAGE_PATH_MAX_LENGTH}.`
114
+ };
115
+ }
116
+ if (!/^[A-Za-z0-9_\-/]+$/.test(raw)) {
117
+ return {
118
+ ok: false,
119
+ error: "The storage path prefix may contain only letters, digits, '-', '_' and '/' separators."
120
+ };
121
+ }
122
+ if (raw.split("/").some((segment) => segment.length === 0)) {
123
+ return {
124
+ ok: false,
125
+ error: "The storage path prefix must not start or end with '/' or contain an empty segment."
126
+ };
127
+ }
128
+ return { ok: true, prefix: raw };
129
+ }
130
+ var PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES = Object.freeze({
131
+ pdf: ["application/pdf"],
132
+ png: ["image/png"],
133
+ jpg: ["image/jpeg", "image/pjpeg", "image/jpg"],
134
+ jpeg: ["image/jpeg", "image/pjpeg", "image/jpg"],
135
+ gif: ["image/gif"],
136
+ webp: ["image/webp"],
137
+ doc: ["application/msword"],
138
+ docx: ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
139
+ xls: ["application/vnd.ms-excel"],
140
+ xlsx: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
141
+ ppt: ["application/vnd.ms-powerpoint"],
142
+ pptx: ["application/vnd.openxmlformats-officedocument.presentationml.presentation"],
143
+ odt: ["application/vnd.oasis.opendocument.text"],
144
+ ods: ["application/vnd.oasis.opendocument.spreadsheet"],
145
+ odp: ["application/vnd.oasis.opendocument.presentation"],
146
+ txt: ["text/plain"],
147
+ csv: ["text/csv", "application/vnd.ms-excel", "application/csv", "text/plain"],
148
+ zip: ["application/zip", "application/x-zip-compressed", "application/x-zip"],
149
+ mp3: ["audio/mpeg", "audio/mp3"],
150
+ m4a: ["audio/mp4", "audio/x-m4a"],
151
+ mp4: ["video/mp4"]
152
+ });
153
+ var OCTET_STREAM = "application/octet-stream";
154
+ function pluginStorageSafeExtension(fileName) {
155
+ if (typeof fileName !== "string" || fileName.trim().length === 0) {
156
+ return "";
157
+ }
158
+ const leaf = fileName.split(/[\\/]/).pop() ?? "";
159
+ const dot = leaf.lastIndexOf(".");
160
+ if (dot < 0) {
161
+ return "";
162
+ }
163
+ const body = leaf.slice(dot + 1);
164
+ return /^[A-Za-z0-9]{1,10}$/.test(body) ? `.${body.toLowerCase()}` : "";
165
+ }
166
+ function resolvePluginStorageContentType(fileName, declaredContentType) {
167
+ const extension = pluginStorageSafeExtension(fileName).slice(1);
168
+ const allowed = extension.length > 0 && Object.prototype.hasOwnProperty.call(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES, extension) ? PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES[extension] : void 0;
169
+ if (allowed === void 0) {
170
+ return {
171
+ ok: false,
172
+ error: "This file type is not allowed. Allowed: " + Object.keys(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES).sort().join(", ") + "."
173
+ };
174
+ }
175
+ const declared = mediaTypeOf(declaredContentType);
176
+ if (declared === void 0 || declared === OCTET_STREAM || allowed.includes(declared)) {
177
+ return { ok: true, contentType: allowed[0] };
178
+ }
179
+ return {
180
+ ok: false,
181
+ error: `The declared content type '${declared}' does not match the file extension '.${extension}'.`
182
+ };
183
+ }
184
+ function mediaTypeOf(raw) {
185
+ if (typeof raw !== "string") {
186
+ return void 0;
187
+ }
188
+ const mediaType = raw.split(";")[0].trim().toLowerCase();
189
+ return /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType) ? mediaType : void 0;
190
+ }
191
+ function codeForReason(reason, status) {
192
+ if (status !== void 0) {
193
+ return mcpErrorCodeFromHttpStatus(status);
194
+ }
195
+ switch (reason) {
196
+ case "invalid_prefix":
197
+ case "too_large":
198
+ case "unsupported_type":
199
+ return "invalid_request";
200
+ case "timeout":
201
+ return "timeout";
202
+ case "unauthorized":
203
+ return "unauthorized";
204
+ case "forbidden":
205
+ return "forbidden";
206
+ case "busy":
207
+ return "unavailable";
208
+ case "aborted":
209
+ return "timeout";
210
+ default:
211
+ return "internal";
212
+ }
213
+ }
214
+
215
+ // src/mock-host/MockPluginStorage.ts
216
+ var MockPluginStorage = class {
217
+ files = /* @__PURE__ */ new Map();
218
+ /** Store `buffer` under a new path below `meta.pathPrefix`. */
219
+ store(meta, buffer) {
220
+ const prefix = validatePluginStoragePrefix(meta.pathPrefix);
221
+ if (!prefix.ok) {
222
+ throw new PluginStorageUploadError(prefix.error, "invalid_prefix", { status: 400 });
223
+ }
224
+ if (buffer.byteLength > PLUGIN_STORAGE_UPLOAD_MAX_BYTES) {
225
+ throw new PluginStorageUploadError(
226
+ `The upload exceeds the ${PLUGIN_STORAGE_UPLOAD_MAX_BYTES}-byte limit.`,
227
+ "too_large",
228
+ { status: 413 }
229
+ );
230
+ }
231
+ const fileName = meta.fileName && meta.fileName.length > 0 ? meta.fileName : "upload.bin";
232
+ const type = resolvePluginStorageContentType(fileName, meta.contentType);
233
+ if (!type.ok) {
234
+ throw new PluginStorageUploadError(type.error, "unsupported_type", { status: 415 });
235
+ }
236
+ const path = `${prefix.prefix}/${randomGuidN()}${pluginStorageSafeExtension(fileName)}`;
237
+ const bytes = new Uint8Array(buffer.slice(0));
238
+ this.files.set(path, { path, fileName, contentType: type.contentType, bytes });
239
+ return { path, fileName, contentType: type.contentType, sizeBytes: bytes.byteLength };
240
+ }
241
+ /** The stored file at `path`, or `undefined`. */
242
+ get(path) {
243
+ return this.files.get(path);
244
+ }
245
+ /** Every stored file, in upload order. */
246
+ list() {
247
+ return [...this.files.values()];
248
+ }
249
+ /** Forget every stored file. */
250
+ clear() {
251
+ this.files.clear();
252
+ }
253
+ };
254
+ function randomGuidN() {
255
+ const bytes = new Uint8Array(16);
256
+ globalThis.crypto.getRandomValues(bytes);
257
+ bytes[6] = bytes[6] & 15 | 64;
258
+ bytes[8] = bytes[8] & 63 | 128;
259
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
260
+ }
261
+
37
262
  // src/mock-host/InMemoryMcpTransport.ts
38
263
  var InMemoryMcpTransport = class {
39
264
  resources;
40
265
  tools;
41
266
  uploadHandler;
42
- constructor(resources, tools = {}, uploadHandler) {
267
+ /** Where `uploadToStorage` writes. Read it back in a test, or from devtools under `dev:mock`. */
268
+ pluginStorage;
269
+ constructor(resources, tools = {}, uploadHandler, pluginStorage = new MockPluginStorage()) {
43
270
  this.resources = resources;
44
271
  this.tools = tools;
45
272
  this.uploadHandler = uploadHandler;
273
+ this.pluginStorage = pluginStorage;
274
+ }
275
+ async uploadToStorage(meta, buffer, signal) {
276
+ if (signal?.aborted === true) {
277
+ throw abortError();
278
+ }
279
+ return this.pluginStorage.store(meta, buffer);
46
280
  }
47
281
  async getResource(uri) {
48
282
  const data = this.resources[uri];
@@ -194,6 +428,6 @@ function WorkerMockHost(props) {
194
428
  return /* @__PURE__ */ jsx(BridgeClientContext.Provider, { value: bridgeTransport, children: /* @__PURE__ */ jsx(DeclarativeMockHost, { ...declarativeProps }) });
195
429
  }
196
430
 
197
- export { DeclarativeMockHost, InMemoryBridgeTransport, InMemoryMcpTransport, WorkerMockHost };
431
+ export { DeclarativeMockHost, InMemoryBridgeTransport, InMemoryMcpTransport, MockPluginStorage, WorkerMockHost };
198
432
  //# sourceMappingURL=index.js.map
199
433
  //# sourceMappingURL=index.js.map
@@ -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\n/**\n * Non-throwing variant of {@link useExtensionRuntimeTransport}: returns the\n * resolved transport, or `null` when neither an override nor a provider is\n * present. For optional, fire-and-forget consumers (e.g. an auto-injected\n * org-format sync) that must degrade to a no-op rather than crash a surface that\n * renders without a host transport — a standalone/mock-mode run or an isolated\n * unit test.\n */\nexport function useOptionalExtensionRuntimeTransport(override?: McpTransport): McpTransport | null\n{\n const fromContext = useContext(ExtensionRuntimeContext);\n return override ?? fromContext ?? null;\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
+ {"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/bridge/mcp-error.ts","../../src/plugin/pluginStorageUpload.ts","../../src/mock-host/MockPluginStorage.ts","../../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\n/**\n * Non-throwing variant of {@link useExtensionRuntimeTransport}: returns the\n * resolved transport, or `null` when neither an override nor a provider is\n * present. For optional, fire-and-forget consumers (e.g. an auto-injected\n * org-format sync) that must degrade to a no-op rather than crash a surface that\n * renders without a host transport — a standalone/mock-mode run or an isolated\n * unit test.\n */\nexport function useOptionalExtensionRuntimeTransport(override?: McpTransport): McpTransport | null\n{\n const fromContext = useContext(ExtensionRuntimeContext);\n return override ?? fromContext ?? null;\n}\n","/**\n * A machine-readable classification for an MCP call that failed, carried across the host/plugin\n * bridge alongside the human-readable message.\n *\n * ## Why this exists\n *\n * The bridge used to reduce every failure to `err.message`, a bare string. A plugin therefore could\n * not tell \"you lack permission for this tool\" from \"the host is unreachable\" without matching on\n * host error prose, and the practical consequence was that plugins swallowed *all* failures into a\n * successful empty result to keep a permission denial from looking like a crash. That turns a\n * transient outage into \"you have no connectors\" - a false statement about the user's data, and one\n * that also suppresses the query layer's retry.\n *\n * A closed vocabulary lets the plugin branch on the one case it wants to tolerate and rethrow the\n * rest.\n *\n * ## Deliberately small, and deliberately not HTTP\n *\n * These are the distinctions a *caller* acts on differently, not a mirror of any status enum. Codes\n * that would prompt the same handling are folded together, because a vocabulary nobody can apply is\n * just a wider surface to get wrong. `unavailable` and `timeout` stay separate only because retry\n * policy differs between them.\n */\nexport type McpErrorCode =\n /** The caller is not authenticated, or its token expired. Re-authentication may fix it. */\n | \"unauthorized\"\n /** Authenticated, but not permitted this tool or resource. Retrying will not help. */\n | \"forbidden\"\n /** The tool, resource, or addressed entity does not exist. */\n | \"not_found\"\n /** The arguments were rejected. A caller bug, or stale client-side validation. */\n | \"invalid_request\"\n /** A concurrency or state conflict - a row version, or a duplicate. */\n | \"conflict\"\n /** Throttled. Retry later, with backoff. */\n | \"rate_limited\"\n /** The call did not complete in time. Safe to retry only if the operation is idempotent. */\n | \"timeout\"\n /** The host or an upstream dependency is down. Retryable. */\n | \"unavailable\"\n /** Anything else, including an unclassifiable failure. The default - never a claim. */\n | \"internal\";\n\n/**\n * Every valid {@link McpErrorCode}. Used to validate a code arriving over the wire: an unknown\n * string is downgraded rather than trusted, so a newer host cannot make an older plugin branch on a\n * code it has never heard of.\n */\nexport const MCP_ERROR_CODES: readonly McpErrorCode[] = [\n \"unauthorized\",\n \"forbidden\",\n \"not_found\",\n \"invalid_request\",\n \"conflict\",\n \"rate_limited\",\n \"timeout\",\n \"unavailable\",\n \"internal\",\n];\n\n/** True when `value` is a code this build understands. */\nexport function isMcpErrorCode(value: unknown): value is McpErrorCode\n{\n return typeof value === \"string\"\n && (MCP_ERROR_CODES as readonly string[]).includes(value);\n}\n\n/**\n * Codes a caller can retry without changing anything about the request. Exposed so retry policy is\n * decided once here rather than re-derived, subtly differently, at each call site.\n *\n * `unauthorized` is absent on purpose: a retry only helps once something else has refreshed the\n * token, which is a different action from retrying.\n */\nconst RETRYABLE: readonly McpErrorCode[] = [\"rate_limited\", \"timeout\", \"unavailable\"];\n\n/** True when the failure is worth retrying as-is. */\nexport function isRetryableMcpErrorCode(code: McpErrorCode): boolean\n{\n return RETRYABLE.includes(code);\n}\n\n/**\n * An MCP call rejected by the host, carrying its {@link McpErrorCode}.\n *\n * `name` stays `\"McpHostError\"` - the string the bridge has always set - so code that matches on the\n * name keeps working. Prefer {@link isMcpToolError}, which survives a name change and works across\n * realm boundaries where `instanceof` does not.\n */\nexport class McpToolError extends Error\n{\n /** Machine-readable classification. `internal` when the host sent none. */\n public readonly code: McpErrorCode;\n\n /**\n * Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype\n * check because the error can be constructed in one bundle and inspected in another, where\n * `instanceof` compares two different class objects and answers false.\n */\n public readonly isMcpToolError = true as const;\n\n public constructor(message: string, code: McpErrorCode = \"internal\")\n {\n super(message);\n this.name = \"McpHostError\";\n this.code = code;\n }\n\n /** True when this failure is worth retrying unchanged. */\n public get retryable(): boolean\n {\n return isRetryableMcpErrorCode(this.code);\n }\n}\n\n/** True when `value` is an {@link McpToolError}, including one from another bundle. */\nexport function isMcpToolError(value: unknown): value is McpToolError\n{\n return value instanceof McpToolError\n || (value !== null\n && typeof value === \"object\"\n && (value as { isMcpToolError?: unknown }).isMcpToolError === true\n && isMcpErrorCode((value as { code?: unknown }).code));\n}\n\n/**\n * Maps an HTTP status onto a code. Split out because host MCP clients are commonly HTTP clients, so\n * a status is the classification most of them already have.\n *\n * Unrecognised statuses (including every 2xx and 3xx, which should not be reaching an error path)\n * yield `internal` rather than a guess.\n */\nexport function mcpErrorCodeFromHttpStatus(status: number): McpErrorCode\n{\n switch (status)\n {\n case 401:\n return \"unauthorized\";\n case 403:\n return \"forbidden\";\n case 404:\n return \"not_found\";\n case 400:\n case 422:\n return \"invalid_request\";\n case 409:\n case 412:\n return \"conflict\";\n case 429:\n return \"rate_limited\";\n case 408:\n case 504:\n return \"timeout\";\n case 502:\n case 503:\n return \"unavailable\";\n default:\n // Everything unmapped, 500 included. `internal` is the honest answer for a status this\n // vocabulary has no distinct handling for - inventing a closer-looking code would tell\n // the caller something the status does not actually say.\n return \"internal\";\n }\n}\n\n/**\n * Derives a code from a host MCP client's RETURNED failure, as opposed to a thrown one.\n *\n * Both paths exist and both have to be covered. A client that throws is handled by\n * {@link classifyHostError}; a client that reports failure as `{ ok: false, error, status }` - the\n * common shape for anything wrapping HTTP - comes through here. Covering only the throw path leaves\n * the majority of real failures arriving as `internal`, which is the same blindness the code was\n * added to remove.\n *\n * Returns `undefined` for a successful response, so a caller can spread it without putting a\n * meaningless code on the happy path.\n */\nexport function classifyHostResponse(response: {\n readonly ok: boolean;\n readonly status?: number;\n readonly mcpErrorCode?: unknown;\n}): McpErrorCode | undefined\n{\n if (response.ok)\n {\n return undefined;\n }\n\n // Same precedence as the thrown path: an explicit code from the client beats the transport's\n // status, and neither is ever inferred from the message.\n if (isMcpErrorCode(response.mcpErrorCode))\n {\n return response.mcpErrorCode;\n }\n\n return typeof response.status === \"number\"\n ? mcpErrorCodeFromHttpStatus(response.status)\n : \"internal\";\n}\n\n/**\n * Derives a code from an arbitrary thrown value, for the host side of the bridge.\n *\n * Precedence, most explicit first:\n *\n * 1. An `mcpErrorCode` property holding a known code. The intended contract: a host MCP client that\n * knows why a call failed says so directly.\n * 2. A numeric `status` / `statusCode`, mapped by {@link mcpErrorCodeFromHttpStatus}. Covers the\n * HTTP clients that already carry one without asking every host to adopt the field above.\n * 3. `name === \"AbortError\"` / `\"TimeoutError\"`, which is how the platform's own aborts surface.\n * 4. `internal`.\n *\n * **Never infers from the message.** Matching prose would make the classification depend on wording\n * nobody treats as a contract, and it would silently reclassify itself the day someone improves an\n * error string. An unclassifiable failure is `internal`, which is honest.\n */\nexport function classifyHostError(err: unknown): McpErrorCode\n{\n if (err === null || typeof err !== \"object\")\n {\n return \"internal\";\n }\n\n const candidate = err as {\n mcpErrorCode?: unknown;\n status?: unknown;\n statusCode?: unknown;\n name?: unknown;\n };\n\n if (isMcpErrorCode(candidate.mcpErrorCode))\n {\n return candidate.mcpErrorCode;\n }\n\n const status = typeof candidate.status === \"number\"\n ? candidate.status\n : typeof candidate.statusCode === \"number\" ? candidate.statusCode : undefined;\n\n if (status !== undefined)\n {\n return mcpErrorCodeFromHttpStatus(status);\n }\n\n if (candidate.name === \"AbortError\" || candidate.name === \"TimeoutError\")\n {\n return \"timeout\";\n }\n\n return \"internal\";\n}\n","/**\n * Browser file → the calling app's OWN plugin storage.\n *\n * The plugin-facing half of the `uploadToStorage` request kind: client-side prefix validation that\n * mirrors the kernel's `PluginStorageUploadPath.TryNormalizePrefix`, a typed error that keeps the\n * server's status and message, and {@link uploadFileToPluginStorage}, the one function every\n * plugin surface (and `@ethisyscore/plugin-ui`'s `useUploadToStorage`) calls.\n *\n * Browser uploads go through `uploadToStorage` into plugin storage; never base64 a file through a\n * tool call.\n *\n * The client checks are there to fail fast, before a 30 MB file is read into memory. The server\n * re-runs every one of them and is authoritative.\n */\nimport { McpToolError, isMcpToolError, mcpErrorCodeFromHttpStatus, type McpErrorCode } from \"../bridge/mcp-error\";\nimport type { McpTransport, UploadToStorageMeta, UploadToStorageResult } from \"./transport\";\n\n/** The kernel's hard cap on one upload, in bytes (`UploadExtensionDocumentStream.MaxUploadBytes`). */\nexport const PLUGIN_STORAGE_UPLOAD_MAX_BYTES = 30 * 1024 * 1024;\n\n/**\n * Maximum length of a normalised prefix: 128 (the path column HR and Legal record into) less the\n * `/`, the 32-character GUID leaf and an 11-character extension. `governance/{guid}/{guid}` is\n * exactly 84.\n */\nexport const PLUGIN_STORAGE_PREFIX_MAX_LENGTH = 84;\n\n/** Maximum length of a path the server returns. */\nexport const PLUGIN_STORAGE_PATH_MAX_LENGTH = 128;\n\n/**\n * Why a plugin-storage upload failed. Each value is something a caller can act on differently;\n * `status` on the error carries the exact HTTP status when the host supplied one.\n *\n * - `invalid_prefix`: the prefix broke the rules (client-side check, or a server 400).\n * - `too_large`: over the 30 MB cap (client-side check, or a server 413).\n * - `quota_exceeded`: the app's storage quota would be exceeded (507).\n * - `unauthorized`: no capability-token caller or no session (401).\n * - `forbidden`: token and session disagree, or the app owns no storage namespace (403).\n * - `unsupported_type`: the file's extension is not on the allow-list, or the declared type does\n * not match it (client-side check, or a server 415).\n * - `timeout`: the upload was too slow or missed the server's deadline (408). Retryable.\n * - `busy`: the app's storage lock is held, or the server's upload slots are full (503). Retryable.\n * - `storage_failed`: the store refused the write (500).\n * - `unsupported`: the active transport has no `uploadToStorage`, i.e. the host predates it.\n * - `aborted`: the caller's signal fired.\n * - `unknown`: anything else.\n */\nexport type PluginStorageUploadErrorReason =\n | \"invalid_prefix\"\n | \"too_large\"\n | \"quota_exceeded\"\n | \"unauthorized\"\n | \"forbidden\"\n | \"unsupported_type\"\n | \"timeout\"\n | \"busy\"\n | \"storage_failed\"\n | \"unsupported\"\n | \"aborted\"\n | \"unknown\";\n\n/**\n * A failed plugin-storage upload. `message` is the server's own `{ error }` text when the server\n * answered, otherwise the client-side reason.\n *\n * Extends {@link McpToolError}, so `isMcpToolError` and `code` keep working for code that handles\n * every MCP failure the same way. Prefer {@link isPluginStorageUploadError} over `instanceof`: the\n * error can be built in one bundle and inspected in another.\n */\nexport class PluginStorageUploadError extends McpToolError\n{\n /** What went wrong, in terms a caller branches on. */\n public readonly reason: PluginStorageUploadErrorReason;\n\n /** HTTP status from the server, when the host surfaced one. */\n public readonly status?: number;\n\n /** Structural marker for {@link isPluginStorageUploadError}. */\n public readonly isPluginStorageUploadError = true as const;\n\n public constructor(\n message: string,\n reason: PluginStorageUploadErrorReason,\n options: { status?: number; code?: McpErrorCode } = {},\n )\n {\n super(message, options.code ?? codeForReason(reason, options.status));\n this.name = \"PluginStorageUploadError\";\n this.reason = reason;\n this.status = options.status;\n }\n}\n\n/** True when `value` is a {@link PluginStorageUploadError}, including one from another bundle. */\nexport function isPluginStorageUploadError(value: unknown): value is PluginStorageUploadError\n{\n return value instanceof PluginStorageUploadError\n || (isMcpToolError(value)\n && (value as { isPluginStorageUploadError?: unknown }).isPluginStorageUploadError === true);\n}\n\n/** Maps a status from `extensions/storage/upload-stream` onto a {@link PluginStorageUploadErrorReason}. */\nexport function pluginStorageUploadReasonFromStatus(status: number): PluginStorageUploadErrorReason\n{\n switch (status)\n {\n case 400:\n return \"invalid_prefix\";\n case 401:\n return \"unauthorized\";\n case 403:\n return \"forbidden\";\n case 413:\n return \"too_large\";\n case 408:\n return \"timeout\";\n case 415:\n return \"unsupported_type\";\n case 503:\n return \"busy\";\n case 507:\n return \"quota_exceeded\";\n case 500:\n return \"storage_failed\";\n default:\n return \"unknown\";\n }\n}\n\n/**\n * Normalises any failure from a transport's `uploadToStorage` into a {@link PluginStorageUploadError},\n * keeping the server's message.\n *\n * Precedence: an existing `PluginStorageUploadError` passes through; then a numeric `status` /\n * `statusCode` (what a host HTTP client carries); then an `AbortError`; then the MCP error `code`\n * from the bridge; then `unknown`. Never inferred from the message text.\n */\nexport function toPluginStorageUploadError(err: unknown): PluginStorageUploadError\n{\n if (isPluginStorageUploadError(err))\n {\n return err;\n }\n\n const message = err instanceof Error\n ? err.message\n : typeof err === \"string\" ? err : \"Plugin storage upload failed\";\n\n const candidate = (err !== null && typeof err === \"object\" ? err : {}) as {\n status?: unknown;\n statusCode?: unknown;\n name?: unknown;\n };\n const status = typeof candidate.status === \"number\"\n ? candidate.status\n : typeof candidate.statusCode === \"number\" ? candidate.statusCode : undefined;\n\n if (status !== undefined)\n {\n return new PluginStorageUploadError(message, pluginStorageUploadReasonFromStatus(status), { status });\n }\n\n if (candidate.name === \"AbortError\")\n {\n return new PluginStorageUploadError(message, \"aborted\", { code: \"timeout\" });\n }\n\n if (isMcpToolError(err))\n {\n return new PluginStorageUploadError(message, reasonForCode(err.code), { code: err.code });\n }\n\n return new PluginStorageUploadError(message, \"unknown\");\n}\n\n/** Outcome of {@link validatePluginStoragePrefix}. */\nexport type PluginStoragePrefixValidation =\n | { readonly ok: true; readonly prefix: string }\n | { readonly ok: false; readonly error: string };\n\n/**\n * Validates a storage prefix with the kernel's rules (`PluginStorageUploadPath.TryNormalizePrefix`\n * in CoreConnect-Api, Application.Extensions/Storage). A malformed prefix is REFUSED, never\n * repaired, and an accepted one is returned exactly as given, so a caller that checks the\n * returned path with `path.startsWith(prefix + \"/\")` always matches. In the server's order:\n *\n * - required (not empty or whitespace);\n * - at most {@link PLUGIN_STORAGE_PREFIX_MAX_LENGTH} characters, measured on the raw value;\n * - only `[A-Za-z0-9_-]` and `/`, so `%` (any encoding), `.`/`..`, `\\`, whitespace and every\n * other character are refused;\n * - every `/`-separated segment non-empty, which also refuses a leading or trailing `/` and `//`.\n *\n * The server's last check (its storage normaliser must be the identity on the prefix) cannot fail\n * for a value that passed the rules above, so it has no client counterpart.\n */\nexport function validatePluginStoragePrefix(raw: string | null | undefined): PluginStoragePrefixValidation\n{\n if (typeof raw !== \"string\" || raw.trim().length === 0)\n {\n return { ok: false, error: \"A storage path prefix is required.\" };\n }\n\n if (raw.length > PLUGIN_STORAGE_PREFIX_MAX_LENGTH)\n {\n return {\n ok: false,\n error: `The storage path prefix must be at most ${PLUGIN_STORAGE_PREFIX_MAX_LENGTH} characters, so the stored path fits ${PLUGIN_STORAGE_PATH_MAX_LENGTH}.`,\n };\n }\n\n if (!/^[A-Za-z0-9_\\-/]+$/.test(raw))\n {\n return {\n ok: false,\n error: \"The storage path prefix may contain only letters, digits, '-', '_' and '/' separators.\",\n };\n }\n\n if (raw.split(\"/\").some((segment) => segment.length === 0))\n {\n return {\n ok: false,\n error: \"The storage path prefix must not start or end with '/' or contain an empty segment.\",\n };\n }\n\n return { ok: true, prefix: raw };\n}\n\n/**\n * {@link validatePluginStoragePrefix}, throwing a `PluginStorageUploadError` with reason\n * `invalid_prefix` instead of returning a result. Returns the prefix unchanged.\n */\nexport function assertValidPluginStoragePrefix(raw: string): string\n{\n const result = validatePluginStoragePrefix(raw);\n if (!result.ok)\n {\n throw new PluginStorageUploadError(result.error, \"invalid_prefix\");\n }\n return result.prefix;\n}\n\n/**\n * The file types a browser may upload into plugin storage, keyed by lower-case extension: the\n * canonical type the server stores first, then the aliases browsers commonly declare for it.\n *\n * SOURCE OF TRUTH: `PluginStorageUploadContentTypes` in CoreConnect-Api\n * (src/application/CoreConnect.Application.Extensions/Storage/PluginStorageUploadContentTypes.cs).\n * This copy only lets the client fail fast with `unsupported_type`; the server decides. Keep it in\n * step with the server. Passive formats only: never HTML, SVG, XML or JavaScript.\n */\nexport const PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES: Readonly<Record<string, readonly [canonical: string, ...aliases: string[]]>> = Object.freeze({\n pdf: [\"application/pdf\"],\n\n png: [\"image/png\"],\n jpg: [\"image/jpeg\", \"image/pjpeg\", \"image/jpg\"],\n jpeg: [\"image/jpeg\", \"image/pjpeg\", \"image/jpg\"],\n gif: [\"image/gif\"],\n webp: [\"image/webp\"],\n\n doc: [\"application/msword\"],\n docx: [\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"],\n xls: [\"application/vnd.ms-excel\"],\n xlsx: [\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"],\n ppt: [\"application/vnd.ms-powerpoint\"],\n pptx: [\"application/vnd.openxmlformats-officedocument.presentationml.presentation\"],\n odt: [\"application/vnd.oasis.opendocument.text\"],\n ods: [\"application/vnd.oasis.opendocument.spreadsheet\"],\n odp: [\"application/vnd.oasis.opendocument.presentation\"],\n\n txt: [\"text/plain\"],\n csv: [\"text/csv\", \"application/vnd.ms-excel\", \"application/csv\", \"text/plain\"],\n\n zip: [\"application/zip\", \"application/x-zip-compressed\", \"application/x-zip\"],\n\n mp3: [\"audio/mpeg\", \"audio/mp3\"],\n m4a: [\"audio/mp4\", \"audio/x-m4a\"],\n mp4: [\"video/mp4\"],\n});\n\nconst OCTET_STREAM = \"application/octet-stream\";\n\n/**\n * The kernel's `PluginStorageUploadPath.SafeExtension`: the lower-cased extension with its dot,\n * kept only when it is 1 to 10 ASCII letters or digits; otherwise empty.\n */\nexport function pluginStorageSafeExtension(fileName: string | null | undefined): string\n{\n if (typeof fileName !== \"string\" || fileName.trim().length === 0)\n {\n return \"\";\n }\n const leaf = fileName.split(/[\\\\/]/).pop() ?? \"\";\n const dot = leaf.lastIndexOf(\".\");\n if (dot < 0)\n {\n return \"\";\n }\n const body = leaf.slice(dot + 1);\n return /^[A-Za-z0-9]{1,10}$/.test(body) ? `.${body.toLowerCase()}` : \"\";\n}\n\n/** Outcome of {@link resolvePluginStorageContentType}. */\nexport type PluginStorageContentTypeResolution =\n | { readonly ok: true; readonly contentType: string }\n | { readonly ok: false; readonly error: string };\n\n/**\n * Mirrors `PluginStorageUploadContentTypes.TryResolve`: the extension decides. A file name with no\n * allow-listed extension is refused. A declared type is accepted when it is that extension's\n * canonical type or a listed alias (case-insensitive, parameters ignored); none, or\n * `application/octet-stream`, means \"use the extension's type\". Resolves to the CANONICAL type the\n * server will store.\n */\nexport function resolvePluginStorageContentType(\n fileName: string | null | undefined,\n declaredContentType?: string | null,\n): PluginStorageContentTypeResolution\n{\n const extension = pluginStorageSafeExtension(fileName).slice(1);\n const allowed = extension.length > 0 && Object.prototype.hasOwnProperty.call(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES, extension)\n ? PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES[extension]\n : undefined;\n if (allowed === undefined)\n {\n return {\n ok: false,\n error: \"This file type is not allowed. Allowed: \"\n + Object.keys(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES).sort().join(\", \") + \".\",\n };\n }\n\n const declared = mediaTypeOf(declaredContentType);\n if (declared === undefined || declared === OCTET_STREAM || allowed.includes(declared))\n {\n return { ok: true, contentType: allowed[0] };\n }\n return {\n ok: false,\n error: `The declared content type '${declared}' does not match the file extension '.${extension}'.`,\n };\n}\n\n/**\n * The lower-cased `type/subtype` of a declared content type without parameters, or `undefined`\n * when there is none or it does not parse (the server then treats it as undeclared too).\n */\nfunction mediaTypeOf(raw: string | null | undefined): string | undefined\n{\n if (typeof raw !== \"string\")\n {\n return undefined;\n }\n const mediaType = raw.split(\";\")[0]!.trim().toLowerCase();\n return /^[a-z0-9!#$&^_.+-]+\\/[a-z0-9!#$&^_.+-]+$/.test(mediaType) ? mediaType : undefined;\n}\n\n/** Options for {@link uploadFileToPluginStorage}. */\nexport interface UploadToStorageOptions\n{\n /** Folder inside the app's own storage namespace, e.g. `cvs` or `governance/{docId}/{versionId}`. */\n pathPrefix: string;\n /** File name. Defaults to the `File`'s name, else `upload.bin`. Required in practice for an `ArrayBuffer`. */\n fileName?: string;\n /** MIME type. Defaults to the `Blob`'s type, else `application/octet-stream` (the server then infers from the extension). */\n contentType?: string;\n /** Client-side size ceiling, never above the server's. Defaults to {@link PLUGIN_STORAGE_UPLOAD_MAX_BYTES}. */\n maxSizeBytes?: number;\n /** Cancels the upload. */\n signal?: AbortSignal;\n}\n\n/**\n * Upload a browser file into the calling app's own plugin storage and resolve the server-chosen\n * `path` that the app's backend reads with `IPluginStorage`.\n *\n * Validates the prefix, the size and the file type (against the server's allow-list) before\n * reading the file, then hands the bytes to the\n * transport's `uploadToStorage`. Every failure (client-side or server) rejects with a\n * {@link PluginStorageUploadError} whose `reason` is safe to branch on and whose `message` is the\n * server's when the server answered.\n *\n * An `ArrayBuffer` argument is transferred to the host and detached; pass a copy to keep it.\n */\nexport async function uploadFileToPluginStorage(\n transport: McpTransport,\n file: File | Blob | ArrayBuffer,\n options: UploadToStorageOptions,\n): Promise<UploadToStorageResult>\n{\n if (typeof transport.uploadToStorage !== \"function\")\n {\n throw new PluginStorageUploadError(\n \"The active McpTransport does not support plugin-storage uploads (no `uploadToStorage`). \"\n + \"The host runtime must implement the uploadToStorage request kind.\",\n \"unsupported\",\n );\n }\n // Bound now: property narrowing does not survive the await below, and binding keeps `this`\n // for class-based transports such as InMemoryMcpTransport.\n const uploadToStorage = transport.uploadToStorage.bind(transport);\n\n const pathPrefix = assertValidPluginStoragePrefix(options.pathPrefix);\n\n const maxSizeBytes = Math.min(options.maxSizeBytes ?? PLUGIN_STORAGE_UPLOAD_MAX_BYTES, PLUGIN_STORAGE_UPLOAD_MAX_BYTES);\n // The tag check covers an ArrayBuffer from another realm (a worker, an iframe), where\n // `instanceof` answers false.\n const isBuffer = file instanceof ArrayBuffer || Object.prototype.toString.call(file) === \"[object ArrayBuffer]\";\n const size = isBuffer ? (file as ArrayBuffer).byteLength : (file as Blob).size;\n if (size > maxSizeBytes)\n {\n throw new PluginStorageUploadError(\n `File is ${size} bytes, which exceeds the ${maxSizeBytes}-byte upload limit.`,\n \"too_large\",\n );\n }\n\n // Duck-typed so a named Blob or a File-like object works without the `File` global.\n const ownName = isBuffer ? undefined : (file as { name?: unknown }).name;\n const fileName = nonEmpty(options.fileName)\n ?? (typeof ownName === \"string\" ? nonEmpty(ownName) : undefined)\n ?? \"upload.bin\";\n const declaredType = nonEmpty(options.contentType)\n ?? (isBuffer ? undefined : nonEmpty((file as Blob).type));\n\n // The server's allow-list, checked before the file is read. A name with no allow-listed\n // extension (including the `upload.bin` fallback) is refused, as the server would.\n const resolvedType = resolvePluginStorageContentType(fileName, declaredType);\n if (!resolvedType.ok)\n {\n throw new PluginStorageUploadError(resolvedType.error, \"unsupported_type\");\n }\n\n if (options.signal?.aborted === true)\n {\n throw new PluginStorageUploadError(\"Aborted\", \"aborted\", { code: \"timeout\" });\n }\n\n try\n {\n const buffer = isBuffer ? file as ArrayBuffer : await readBlob(file as Blob);\n\n const meta: UploadToStorageMeta = {\n pathPrefix,\n fileName,\n // The canonical type for the extension: always one the server accepts, and the one\n // it stores.\n contentType: resolvedType.contentType,\n sizeBytes: buffer.byteLength,\n };\n return await uploadToStorage(meta, buffer, options.signal);\n }\n catch (err)\n {\n throw toPluginStorageUploadError(err);\n }\n}\n\n/**\n * `Blob.arrayBuffer()`, falling back to `FileReader` for runtimes whose `Blob` predates it (older\n * WebViews; jsdom).\n */\nfunction readBlob(blob: Blob): Promise<ArrayBuffer>\n{\n if (typeof blob.arrayBuffer === \"function\")\n {\n return blob.arrayBuffer();\n }\n return new Promise<ArrayBuffer>((resolve, reject) =>\n {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as ArrayBuffer);\n reader.onerror = () => reject(reader.error ?? new Error(\"Could not read the file.\"));\n reader.readAsArrayBuffer(blob);\n });\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined\n{\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nfunction codeForReason(reason: PluginStorageUploadErrorReason, status: number | undefined): McpErrorCode\n{\n if (status !== undefined)\n {\n return mcpErrorCodeFromHttpStatus(status);\n }\n switch (reason)\n {\n case \"invalid_prefix\":\n case \"too_large\":\n case \"unsupported_type\":\n return \"invalid_request\";\n case \"timeout\":\n return \"timeout\";\n case \"unauthorized\":\n return \"unauthorized\";\n case \"forbidden\":\n return \"forbidden\";\n case \"busy\":\n return \"unavailable\";\n case \"aborted\":\n return \"timeout\";\n default:\n return \"internal\";\n }\n}\n\nfunction reasonForCode(code: McpErrorCode): PluginStorageUploadErrorReason\n{\n switch (code)\n {\n case \"unauthorized\":\n return \"unauthorized\";\n case \"forbidden\":\n return \"forbidden\";\n case \"unavailable\":\n return \"busy\";\n default:\n return \"unknown\";\n }\n}\n","import type { UploadToStorageMeta, UploadToStorageResult } from \"../plugin/transport\";\nimport {\n PLUGIN_STORAGE_UPLOAD_MAX_BYTES,\n PluginStorageUploadError,\n pluginStorageSafeExtension,\n resolvePluginStorageContentType,\n validatePluginStoragePrefix,\n} from \"../plugin/pluginStorageUpload\";\n\n/** A file the mock host stored for an `uploadToStorage` call. */\nexport interface MockStoredFile\n{\n readonly path: string;\n readonly fileName: string;\n readonly contentType: string;\n readonly bytes: Uint8Array;\n}\n\n/**\n * In-memory stand-in for an app's plugin storage, answering the `uploadToStorage` request kind for\n * `dev:mock` and tests.\n *\n * It behaves like the kernel route where a plugin can observe the difference:\n * - the prefix is validated with the same rules (an invalid one rejects with status 400);\n * - the file type is checked against the same allow-list (415), and the CANONICAL type is stored;\n * - a body over 30 MB rejects with 413;\n * - the path is server-chosen, `{prefix}/{32 hex}{ext}` with the kernel's extension rule, so two\n * uploads never share a path.\n *\n * It does not model quota (507), the storage lock or upload slots (503), or the deadline (408).\n */\nexport class MockPluginStorage\n{\n private readonly files = new Map<string, MockStoredFile>();\n\n /** Store `buffer` under a new path below `meta.pathPrefix`. */\n public store(meta: UploadToStorageMeta, buffer: ArrayBuffer): UploadToStorageResult\n {\n const prefix = validatePluginStoragePrefix(meta.pathPrefix);\n if (!prefix.ok)\n {\n throw new PluginStorageUploadError(prefix.error, \"invalid_prefix\", { status: 400 });\n }\n if (buffer.byteLength > PLUGIN_STORAGE_UPLOAD_MAX_BYTES)\n {\n throw new PluginStorageUploadError(\n `The upload exceeds the ${PLUGIN_STORAGE_UPLOAD_MAX_BYTES}-byte limit.`,\n \"too_large\",\n { status: 413 },\n );\n }\n\n const fileName = meta.fileName && meta.fileName.length > 0 ? meta.fileName : \"upload.bin\";\n const type = resolvePluginStorageContentType(fileName, meta.contentType);\n if (!type.ok)\n {\n throw new PluginStorageUploadError(type.error, \"unsupported_type\", { status: 415 });\n }\n\n const path = `${prefix.prefix}/${randomGuidN()}${pluginStorageSafeExtension(fileName)}`;\n // Copied: the caller's buffer may be detached or reused after the call.\n const bytes = new Uint8Array(buffer.slice(0));\n this.files.set(path, { path, fileName, contentType: type.contentType, bytes });\n return { path, fileName, contentType: type.contentType, sizeBytes: bytes.byteLength };\n }\n\n /** The stored file at `path`, or `undefined`. */\n public get(path: string): MockStoredFile | undefined\n {\n return this.files.get(path);\n }\n\n /** Every stored file, in upload order. */\n public list(): readonly MockStoredFile[]\n {\n return [...this.files.values()];\n }\n\n /** Forget every stored file. */\n public clear(): void\n {\n this.files.clear();\n }\n}\n\n/** 32 lower-case hex characters, the shape of a .NET `Guid.ToString(\"N\")`. */\nfunction randomGuidN(): string\n{\n const bytes = new Uint8Array(16);\n globalThis.crypto.getRandomValues(bytes);\n // Version 4 / RFC 4122 variant bits, so the value is a well-formed GUID, not just 32 hex.\n bytes[6] = (bytes[6]! & 0x0f) | 0x40;\n bytes[8] = (bytes[8]! & 0x3f) | 0x80;\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n","import type {\n McpTransport,\n UploadDocumentMeta,\n UploadDocumentResult,\n UploadToStorageMeta,\n UploadToStorageResult,\n} from \"../plugin/transport\";\nimport { MockPluginStorage } from \"./MockPluginStorage\";\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 *\n * `uploadToStorage` writes into {@link pluginStorage}, an in-memory {@link MockPluginStorage}, and\n * returns a server-style `{prefix}/{guid}{ext}` path. It validates the prefix and size with the\n * kernel's rules, rejecting with a typed `PluginStorageUploadError` (status 400 / 413), and\n * honours the signal.\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 /** Where `uploadToStorage` writes. Read it back in a test, or from devtools under `dev:mock`. */\n public readonly pluginStorage: MockPluginStorage;\n\n public constructor(\n resources: Record<string, SduiNode>,\n tools: Record<string, MockToolHandler> = {},\n uploadHandler?: MockUploadHandler,\n pluginStorage: MockPluginStorage = new MockPluginStorage(),\n )\n {\n this.resources = resources;\n this.tools = tools;\n this.uploadHandler = uploadHandler;\n this.pluginStorage = pluginStorage;\n }\n\n public async uploadToStorage(\n meta: UploadToStorageMeta,\n buffer: ArrayBuffer,\n signal?: AbortSignal,\n ): Promise<UploadToStorageResult>\n {\n if (signal?.aborted === true)\n {\n throw abortError();\n }\n return this.pluginStorage.store(meta, buffer);\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"]}