@ethisyscore/extension-runtime 1.58.0 → 1.61.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.
@@ -32,6 +32,50 @@ function createOffscreenCanvasTransfer(options) {
32
32
  return { offscreen };
33
33
  }
34
34
 
35
+ // src/host/version-skew.ts
36
+ var EXTENSION_VERSION_HEADER = "x-extension-version";
37
+ var EXTENSION_ACTIVE_VERSION_HEADER = "x-extension-active-version";
38
+ var CAPABILITY_REJECTION_REASON_HEADER = "x-capability-token-rejection-reason";
39
+ var VERSION_MISMATCH_REASON = "extensionversionmismatch";
40
+ function readHeader(headers, name) {
41
+ if (!headers) {
42
+ return void 0;
43
+ }
44
+ const direct = headers[name];
45
+ if (direct !== void 0) {
46
+ return direct;
47
+ }
48
+ const lowered = name.toLowerCase();
49
+ for (const key of Object.keys(headers)) {
50
+ if (key.toLowerCase() === lowered) {
51
+ return headers[key];
52
+ }
53
+ }
54
+ return void 0;
55
+ }
56
+ function trimmedOrUndefined(value) {
57
+ const trimmed = value?.trim();
58
+ return trimmed !== void 0 && trimmed.length > 0 ? trimmed : void 0;
59
+ }
60
+ function detectVersionSkew(response, mountedExtensionVersion) {
61
+ const reason = trimmedOrUndefined(readHeader(response.headers, CAPABILITY_REJECTION_REASON_HEADER));
62
+ if (reason !== void 0 && reason.toLowerCase() === VERSION_MISMATCH_REASON) {
63
+ return {
64
+ activeVersion: trimmedOrUndefined(readHeader(response.headers, EXTENSION_ACTIVE_VERSION_HEADER)),
65
+ detectedFrom: "rejection"
66
+ };
67
+ }
68
+ if (!response.ok) {
69
+ return null;
70
+ }
71
+ const mounted = trimmedOrUndefined(mountedExtensionVersion);
72
+ const served = trimmedOrUndefined(readHeader(response.headers, EXTENSION_VERSION_HEADER));
73
+ if (mounted === void 0 || served === void 0 || served === mounted) {
74
+ return null;
75
+ }
76
+ return { activeVersion: served, detectedFrom: "response-stamp" };
77
+ }
78
+
35
79
  // src/host/worker/bridge-envelopes.ts
36
80
  var BRIDGE_PUSH_THEME = "ethisys:bridge:theme";
37
81
  var BRIDGE_PUSH_LOCALE = "ethisys:bridge:locale";
@@ -51,6 +95,9 @@ var WorkerRemoteDomTransport = class {
51
95
  workerPort;
52
96
  mcpClient;
53
97
  capabilityTokenProvider;
98
+ mountedExtensionVersion;
99
+ onVersionSkew;
100
+ versionSkewReported = false;
54
101
  coalesceMs;
55
102
  maxConcurrentMcpRequests;
56
103
  abortController;
@@ -73,6 +120,8 @@ var WorkerRemoteDomTransport = class {
73
120
  }
74
121
  this.mcpClient = options.mcpClient;
75
122
  this.capabilityTokenProvider = options.capabilityToken;
123
+ this.mountedExtensionVersion = options.mountedExtensionVersion;
124
+ this.onVersionSkew = options.onVersionSkew;
76
125
  this.coalesceMs = options.coalesceMs ?? 16;
77
126
  this.maxConcurrentMcpRequests = Math.max(
78
127
  1,
@@ -356,6 +405,34 @@ var WorkerRemoteDomTransport = class {
356
405
  requestSignal(id) {
357
406
  return this.inFlightAborts.get(id)?.signal ?? this.abortController.signal;
358
407
  }
408
+ /**
409
+ * Raises the skew callback the first time this surface is seen to be running against a version
410
+ * that is no longer installed.
411
+ *
412
+ * Latched deliberately. A page typically has several MCP calls in flight, and after an upgrade
413
+ * every one of them reports the same skew — firing per response would ask the host to remount
414
+ * the same surface repeatedly, and the second remount would tear down the first. One signal per
415
+ * transport is all a remount needs, and the transport is discarded along with the surface.
416
+ *
417
+ * The callback is host code, so it is assumed to be able to throw. Two consequences are guarded
418
+ * here: the throw must not escape into the MCP handler's catch (which would reply `ok: false`
419
+ * for a call that actually succeeded), and it must not consume the latch (which would leave the
420
+ * surface with no response AND no further chance of being told to remount).
421
+ */
422
+ reportVersionSkew(response) {
423
+ if (this.versionSkewReported || this.onVersionSkew === void 0) {
424
+ return;
425
+ }
426
+ const skew = detectVersionSkew(response, this.mountedExtensionVersion);
427
+ if (skew === null) {
428
+ return;
429
+ }
430
+ try {
431
+ this.onVersionSkew(skew);
432
+ this.versionSkewReported = true;
433
+ } catch {
434
+ }
435
+ }
359
436
  async handleInvokeTool(message) {
360
437
  let token;
361
438
  try {
@@ -375,6 +452,7 @@ var WorkerRemoteDomTransport = class {
375
452
  capabilityToken: token,
376
453
  signal: this.requestSignal(message.id)
377
454
  });
455
+ this.reportVersionSkew(result);
378
456
  this.safePostMessage({
379
457
  id: message.id,
380
458
  type: "ethisys:mcp:invokeTool:result",
@@ -404,6 +482,7 @@ var WorkerRemoteDomTransport = class {
404
482
  capabilityToken: token,
405
483
  signal: this.requestSignal(message.id)
406
484
  });
485
+ this.reportVersionSkew(result);
407
486
  this.safePostMessage({
408
487
  id: message.id,
409
488
  type: "ethisys:mcp:getResource:result",
@@ -434,6 +513,7 @@ var WorkerRemoteDomTransport = class {
434
513
  capabilityToken: token,
435
514
  signal: this.requestSignal(message.id)
436
515
  });
516
+ this.reportVersionSkew(result);
437
517
  this.safePostMessage({
438
518
  id: message.id,
439
519
  type: "ethisys:mcp:uploadDocument:result",