@ethisyscore/extension-runtime 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge-client-CIThO7jZ.d.cts +102 -0
- package/dist/bridge-client-UK3qcGoi.d.ts +102 -0
- package/dist/bridge-envelopes-BRKGSiSC.d.cts +63 -0
- package/dist/bridge-envelopes-BRKGSiSC.d.ts +63 -0
- package/dist/host/index.cjs +299 -0
- package/dist/host/index.cjs.map +1 -1
- package/dist/host/index.d.cts +94 -2
- package/dist/host/index.d.ts +94 -2
- package/dist/host/index.js +298 -1
- package/dist/host/index.js.map +1 -1
- package/dist/mock-host/cli.cjs +49 -0
- package/dist/mock-host/cli.cjs.map +1 -1
- package/dist/mock-host/cli.d.cts +2 -1
- package/dist/mock-host/cli.d.ts +2 -1
- package/dist/mock-host/cli.js +49 -0
- package/dist/mock-host/cli.js.map +1 -1
- package/dist/mock-host/index.cjs +84 -0
- package/dist/mock-host/index.cjs.map +1 -1
- package/dist/mock-host/index.d.cts +94 -2
- package/dist/mock-host/index.d.ts +94 -2
- package/dist/mock-host/index.js +83 -1
- package/dist/mock-host/index.js.map +1 -1
- package/dist/plugin/index.cjs +204 -0
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.d.cts +50 -2
- package/dist/plugin/index.d.ts +50 -2
- package/dist/plugin/index.js +199 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/{transport-73otePiw.d.ts → transport-BN9Mzn_m.d.cts} +24 -0
- package/dist/{transport-73otePiw.d.cts → transport-Jfd9KXAh.d.ts} +24 -0
- package/package.json +3 -2
- package/dist/transport-DVn2GVZh.d.cts +0 -32
- package/dist/transport-DVn2GVZh.d.ts +0 -32
package/dist/host/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { KNOWN_OPERATORS } from '@ethisyscore/protocol';
|
|
2
2
|
import { createElement } from 'react';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
export { RemoteReceiver } from '@remote-dom/core/receivers';
|
|
4
5
|
export { RemoteRootRenderer, createRemoteComponentRenderer } from '@remote-dom/react/host';
|
|
5
6
|
|
|
@@ -284,6 +285,16 @@ function createInputEventCoalescer(sink, options = {}) {
|
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
// src/host/worker/bridge-envelopes.ts
|
|
289
|
+
var BRIDGE_PUSH_THEME = "ethisys:bridge:theme";
|
|
290
|
+
var BRIDGE_PUSH_LOCALE = "ethisys:bridge:locale";
|
|
291
|
+
var BRIDGE_PUSH_DENSITY = "ethisys:bridge:density";
|
|
292
|
+
var BRIDGE_PUSH_A11Y = "ethisys:bridge:a11y";
|
|
293
|
+
var BRIDGE_NAV_PUSH = "ethisys:bridge:nav";
|
|
294
|
+
var BRIDGE_CHROME_REQUEST = "ethisys:bridge:chrome:request";
|
|
295
|
+
var BRIDGE_LIFECYCLE_EVENT = "ethisys:bridge:lifecycle";
|
|
296
|
+
var BRIDGE_A11Y_ANNOUNCE = "ethisys:bridge:a11y:announce";
|
|
297
|
+
|
|
287
298
|
// src/host/worker/transport.ts
|
|
288
299
|
var WORKER_TRANSPORT_PROTOCOL = "ethisys.worker.remotedom.v1";
|
|
289
300
|
var DEFAULT_MAX_CONCURRENT_MCP_REQUESTS = 8;
|
|
@@ -298,6 +309,7 @@ var WorkerRemoteDomTransport = class {
|
|
|
298
309
|
abortController;
|
|
299
310
|
inFlightMcpRequests = 0;
|
|
300
311
|
remoteDomConsumer;
|
|
312
|
+
bridgeMessageConsumer;
|
|
301
313
|
disposed = false;
|
|
302
314
|
connected = false;
|
|
303
315
|
constructor(options) {
|
|
@@ -434,6 +446,39 @@ var WorkerRemoteDomTransport = class {
|
|
|
434
446
|
}
|
|
435
447
|
};
|
|
436
448
|
}
|
|
449
|
+
// ─── Bridge push methods (WI 4858 sub-plan 2) ─────────────────────────────
|
|
450
|
+
/**
|
|
451
|
+
* Register a consumer for inbound plugin→host bridge messages
|
|
452
|
+
* (chrome requests, a11y announce, lifecycle events). Only the most recently
|
|
453
|
+
* registered consumer is kept — the host mount wires exactly one.
|
|
454
|
+
*/
|
|
455
|
+
onBridgeMessage(consumer) {
|
|
456
|
+
this.bridgeMessageConsumer = consumer;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Push the current host theme to the plugin worker. Safe to call on every
|
|
460
|
+
* host theme-context change — the transport coalesces nothing here; rate
|
|
461
|
+
* limiting at the call site is the host's responsibility.
|
|
462
|
+
*/
|
|
463
|
+
pushTheme(payload) {
|
|
464
|
+
this.safePostMessage({ type: BRIDGE_PUSH_THEME, ...payload });
|
|
465
|
+
}
|
|
466
|
+
/** Push the current host locale and text direction to the plugin worker. */
|
|
467
|
+
pushLocale(payload) {
|
|
468
|
+
this.safePostMessage({ type: BRIDGE_PUSH_LOCALE, ...payload });
|
|
469
|
+
}
|
|
470
|
+
/** Push the current UI density preference to the plugin worker. */
|
|
471
|
+
pushDensity(payload) {
|
|
472
|
+
this.safePostMessage({ type: BRIDGE_PUSH_DENSITY, ...payload });
|
|
473
|
+
}
|
|
474
|
+
/** Push updated accessibility preferences to the plugin worker. */
|
|
475
|
+
pushA11y(payload) {
|
|
476
|
+
this.safePostMessage({ type: BRIDGE_PUSH_A11Y, ...payload });
|
|
477
|
+
}
|
|
478
|
+
/** Push the current SPA navigation state to the plugin worker. */
|
|
479
|
+
pushNav(payload) {
|
|
480
|
+
this.safePostMessage({ type: BRIDGE_NAV_PUSH, ...payload });
|
|
481
|
+
}
|
|
437
482
|
/**
|
|
438
483
|
* Tear down the worker and port. Idempotent.
|
|
439
484
|
*
|
|
@@ -490,6 +535,11 @@ var WorkerRemoteDomTransport = class {
|
|
|
490
535
|
case "ethisys:remotedom":
|
|
491
536
|
this.remoteDomConsumer?.(message.payload);
|
|
492
537
|
return;
|
|
538
|
+
case BRIDGE_CHROME_REQUEST:
|
|
539
|
+
case BRIDGE_A11Y_ANNOUNCE:
|
|
540
|
+
case BRIDGE_LIFECYCLE_EVENT:
|
|
541
|
+
this.bridgeMessageConsumer?.(message);
|
|
542
|
+
return;
|
|
493
543
|
default:
|
|
494
544
|
return;
|
|
495
545
|
}
|
|
@@ -579,7 +629,254 @@ var WorkerRemoteDomTransport = class {
|
|
|
579
629
|
this.safePostMessage({ id, type, ok: false, error: message });
|
|
580
630
|
}
|
|
581
631
|
};
|
|
632
|
+
var IFRAME_BRIDGE_PROTOCOL = "ethisys.iframe.bridge.v1";
|
|
633
|
+
var InboundEnvelope = z.discriminatedUnion("type", [
|
|
634
|
+
z.object({ type: z.literal("ethisys:mcp:invokeTool"), id: z.string().min(1), name: z.string().min(1), args: z.unknown(), nonce: z.string() }),
|
|
635
|
+
z.object({ type: z.literal("ethisys:mcp:getResource"), id: z.string().min(1), uri: z.string().min(1), nonce: z.string() }),
|
|
636
|
+
z.object({ type: z.literal("ethisys:event"), name: z.string().min(1), payload: z.unknown(), nonce: z.string() })
|
|
637
|
+
]);
|
|
638
|
+
function base64url(bytes) {
|
|
639
|
+
let binary = "";
|
|
640
|
+
for (const b of bytes) {
|
|
641
|
+
binary += String.fromCharCode(b);
|
|
642
|
+
}
|
|
643
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
644
|
+
}
|
|
645
|
+
function mintNonce() {
|
|
646
|
+
const bytes = new Uint8Array(32);
|
|
647
|
+
crypto.getRandomValues(bytes);
|
|
648
|
+
return base64url(bytes);
|
|
649
|
+
}
|
|
650
|
+
var IframeBridgeTransport = class {
|
|
651
|
+
frameWindowRef;
|
|
652
|
+
targetOrigin;
|
|
653
|
+
capabilityTokenProvider;
|
|
654
|
+
mcpClient;
|
|
655
|
+
maxConcurrentMcpRequests;
|
|
656
|
+
maxMessagesPerSecond;
|
|
657
|
+
onSecurityEvent;
|
|
658
|
+
abortController = new AbortController();
|
|
659
|
+
hostPort;
|
|
660
|
+
framePort;
|
|
661
|
+
nonce;
|
|
662
|
+
eventConsumer;
|
|
663
|
+
inFlightMcpRequests = 0;
|
|
664
|
+
windowTimestamps = [];
|
|
665
|
+
connected = false;
|
|
666
|
+
disposed = false;
|
|
667
|
+
constructor(options) {
|
|
668
|
+
this.frameWindowRef = { current: options.frameWindow };
|
|
669
|
+
this.targetOrigin = options.targetOrigin;
|
|
670
|
+
this.capabilityTokenProvider = options.capabilityToken;
|
|
671
|
+
this.mcpClient = options.mcpClient;
|
|
672
|
+
this.maxConcurrentMcpRequests = Math.max(1, options.maxConcurrentMcpRequests ?? DEFAULT_MAX_CONCURRENT_MCP_REQUESTS);
|
|
673
|
+
this.maxMessagesPerSecond = Math.max(1, options.maxMessagesPerSecond ?? 64);
|
|
674
|
+
this.onSecurityEvent = options.onSecurityEvent;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Post the handshake to the frame. Idempotent — only the FIRST call mints the
|
|
678
|
+
* nonce, creates the channel, wires the host port, and transfers the frame
|
|
679
|
+
* port. The handshake uses the exact `targetOrigin` (never `"*"`).
|
|
680
|
+
*/
|
|
681
|
+
connect() {
|
|
682
|
+
if (this.connected || this.disposed) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
this.connected = true;
|
|
686
|
+
this.nonce = mintNonce();
|
|
687
|
+
const channel = new MessageChannel();
|
|
688
|
+
this.hostPort = channel.port1;
|
|
689
|
+
this.framePort = channel.port2;
|
|
690
|
+
this.hostPort.onmessage = (ev) => this.handlePortMessage(ev.data);
|
|
691
|
+
this.hostPort.start();
|
|
692
|
+
const handshake = {
|
|
693
|
+
type: "ethisys:iframe:handshake",
|
|
694
|
+
protocol: IFRAME_BRIDGE_PROTOCOL,
|
|
695
|
+
nonce: this.nonce
|
|
696
|
+
};
|
|
697
|
+
const frameWindow = this.frameWindowRef.current;
|
|
698
|
+
frameWindow?.postMessage(handshake, this.targetOrigin, [this.framePort]);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* The per-mount handshake nonce (set after {@link connect}, `undefined` before
|
|
702
|
+
* connect / after dispose). The host mount reads this to validate inbound
|
|
703
|
+
* cross-origin `window`-level messages (which carry no MessagePort identity).
|
|
704
|
+
*/
|
|
705
|
+
get connectedNonce() {
|
|
706
|
+
return this.nonce;
|
|
707
|
+
}
|
|
708
|
+
/** Register a consumer for inbound plugin→host `ethisys:event` envelopes. */
|
|
709
|
+
onEvent(consumer) {
|
|
710
|
+
this.eventConsumer = consumer;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Push a host→plugin envelope (theme/nav/chrome). Always uses the exact
|
|
714
|
+
* `targetOrigin` (never `"*"`) and stamps the handshake nonce.
|
|
715
|
+
*/
|
|
716
|
+
postOutbound(envelope) {
|
|
717
|
+
if (this.disposed || this.nonce === void 0) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const frameWindow = this.frameWindowRef.current;
|
|
721
|
+
frameWindow?.postMessage({ ...envelope, nonce: this.nonce }, this.targetOrigin);
|
|
722
|
+
}
|
|
723
|
+
/** Tear down. Idempotent. Does NOT remove the iframe (the host mount owns it). */
|
|
724
|
+
dispose() {
|
|
725
|
+
if (this.disposed) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
this.disposed = true;
|
|
729
|
+
try {
|
|
730
|
+
this.abortController.abort();
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
this.hostPort?.close();
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
this.nonce = void 0;
|
|
738
|
+
this.frameWindowRef.current = null;
|
|
739
|
+
}
|
|
740
|
+
// ─── Inbound port message handling ────────────────────────────────────────
|
|
741
|
+
handlePortMessage(data) {
|
|
742
|
+
if (this.disposed) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const parsed = InboundEnvelope.safeParse(data);
|
|
746
|
+
if (!parsed.success) {
|
|
747
|
+
this.onSecurityEvent?.({ reason: "schema-invalid" });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const message = parsed.data;
|
|
751
|
+
if (message.nonce !== this.nonce) {
|
|
752
|
+
this.onSecurityEvent?.({ reason: "nonce-mismatch" });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (this.isRateLimited()) {
|
|
756
|
+
this.onSecurityEvent?.({ reason: "rate-limit-exceeded", type: message.type });
|
|
757
|
+
if (message.type === "ethisys:mcp:invokeTool" || message.type === "ethisys:mcp:getResource") {
|
|
758
|
+
this.safePostMessage({
|
|
759
|
+
id: message.id,
|
|
760
|
+
type: `${message.type}:result`,
|
|
761
|
+
ok: false,
|
|
762
|
+
error: "iframe-bridge rate limit exceeded"
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
switch (message.type) {
|
|
768
|
+
case "ethisys:mcp:invokeTool":
|
|
769
|
+
this.dispatchMcp(message, "ethisys:mcp:invokeTool:result", (m) => this.handleInvokeTool(m));
|
|
770
|
+
return;
|
|
771
|
+
case "ethisys:mcp:getResource":
|
|
772
|
+
this.dispatchMcp(message, "ethisys:mcp:getResource:result", (m) => this.handleGetResource(m));
|
|
773
|
+
return;
|
|
774
|
+
case "ethisys:event":
|
|
775
|
+
this.eventConsumer?.(message.name, message.payload);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
isRateLimited() {
|
|
780
|
+
const now = Date.now();
|
|
781
|
+
const cutoff = now - 1e3;
|
|
782
|
+
this.windowTimestamps = this.windowTimestamps.filter((t) => t > cutoff);
|
|
783
|
+
if (this.windowTimestamps.length >= this.maxMessagesPerSecond) {
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
this.windowTimestamps.push(now);
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
// ─── MCP brokering (structurally identical to the worker transport) ───────
|
|
790
|
+
dispatchMcp(message, resultType, handler) {
|
|
791
|
+
if (this.inFlightMcpRequests >= this.maxConcurrentMcpRequests) {
|
|
792
|
+
this.safePostMessage({
|
|
793
|
+
id: message.id,
|
|
794
|
+
type: resultType,
|
|
795
|
+
ok: false,
|
|
796
|
+
error: `MCP back-pressure: in-flight cap of ${this.maxConcurrentMcpRequests} reached.`
|
|
797
|
+
});
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
this.inFlightMcpRequests++;
|
|
801
|
+
handler(message).finally(() => {
|
|
802
|
+
this.inFlightMcpRequests = Math.max(0, this.inFlightMcpRequests - 1);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
async handleInvokeTool(message) {
|
|
806
|
+
let token;
|
|
807
|
+
try {
|
|
808
|
+
token = await this.capabilityTokenProvider();
|
|
809
|
+
} catch (err) {
|
|
810
|
+
this.replyError(message.id, "ethisys:mcp:invokeTool:result", err);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (this.disposed) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
const result = await this.mcpClient.fetch({
|
|
818
|
+
kind: "invokeTool",
|
|
819
|
+
name: message.name,
|
|
820
|
+
args: message.args,
|
|
821
|
+
capabilityToken: token,
|
|
822
|
+
signal: this.abortController.signal
|
|
823
|
+
});
|
|
824
|
+
this.safePostMessage({
|
|
825
|
+
id: message.id,
|
|
826
|
+
type: "ethisys:mcp:invokeTool:result",
|
|
827
|
+
ok: result.ok,
|
|
828
|
+
data: result.data,
|
|
829
|
+
error: result.error
|
|
830
|
+
});
|
|
831
|
+
} catch (err) {
|
|
832
|
+
this.replyError(message.id, "ethisys:mcp:invokeTool:result", err);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async handleGetResource(message) {
|
|
836
|
+
let token;
|
|
837
|
+
try {
|
|
838
|
+
token = await this.capabilityTokenProvider();
|
|
839
|
+
} catch (err) {
|
|
840
|
+
this.replyError(message.id, "ethisys:mcp:getResource:result", err);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (this.disposed) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
const result = await this.mcpClient.fetch({
|
|
848
|
+
kind: "getResource",
|
|
849
|
+
uri: message.uri,
|
|
850
|
+
capabilityToken: token,
|
|
851
|
+
signal: this.abortController.signal
|
|
852
|
+
});
|
|
853
|
+
this.safePostMessage({
|
|
854
|
+
id: message.id,
|
|
855
|
+
type: "ethisys:mcp:getResource:result",
|
|
856
|
+
ok: result.ok,
|
|
857
|
+
data: result.data,
|
|
858
|
+
error: result.error
|
|
859
|
+
});
|
|
860
|
+
} catch (err) {
|
|
861
|
+
this.replyError(message.id, "ethisys:mcp:getResource:result", err);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
replyError(id, type, err) {
|
|
865
|
+
const message = err instanceof Error ? err.message : "MCP request failed";
|
|
866
|
+
this.safePostMessage({ id, type, ok: false, error: message });
|
|
867
|
+
}
|
|
868
|
+
/** Best-effort reply over the host port; tolerates post-dispose races. */
|
|
869
|
+
safePostMessage(message) {
|
|
870
|
+
if (this.disposed || this.hostPort === void 0) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
this.hostPort.postMessage(message);
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
};
|
|
582
879
|
|
|
583
|
-
export { CONTRACT_B_PRIMITIVES, DEFAULT_MAX_CONCURRENT_MCP_REQUESTS, DEFAULT_OFFSCREEN_COALESCE_MS, SemanticComponentRegistry, WORKER_TRANSPORT_PROTOCOL, WorkerRemoteDomTransport, createInputEventCoalescer, createOffscreenCanvasTransfer, evaluate, interpret };
|
|
880
|
+
export { CONTRACT_B_PRIMITIVES, DEFAULT_MAX_CONCURRENT_MCP_REQUESTS, DEFAULT_OFFSCREEN_COALESCE_MS, IFRAME_BRIDGE_PROTOCOL, IframeBridgeTransport, SemanticComponentRegistry, WORKER_TRANSPORT_PROTOCOL, WorkerRemoteDomTransport, createInputEventCoalescer, createOffscreenCanvasTransfer, evaluate, interpret };
|
|
584
881
|
//# sourceMappingURL=index.js.map
|
|
585
882
|
//# sourceMappingURL=index.js.map
|