@ethisyscore/extension-runtime 1.59.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.
@@ -54,6 +54,50 @@ function createOffscreenCanvasTransfer(options) {
54
54
  return { offscreen };
55
55
  }
56
56
 
57
+ // src/host/version-skew.ts
58
+ var EXTENSION_VERSION_HEADER = "x-extension-version";
59
+ var EXTENSION_ACTIVE_VERSION_HEADER = "x-extension-active-version";
60
+ var CAPABILITY_REJECTION_REASON_HEADER = "x-capability-token-rejection-reason";
61
+ var VERSION_MISMATCH_REASON = "extensionversionmismatch";
62
+ function readHeader(headers, name) {
63
+ if (!headers) {
64
+ return void 0;
65
+ }
66
+ const direct = headers[name];
67
+ if (direct !== void 0) {
68
+ return direct;
69
+ }
70
+ const lowered = name.toLowerCase();
71
+ for (const key of Object.keys(headers)) {
72
+ if (key.toLowerCase() === lowered) {
73
+ return headers[key];
74
+ }
75
+ }
76
+ return void 0;
77
+ }
78
+ function trimmedOrUndefined(value) {
79
+ const trimmed = value?.trim();
80
+ return trimmed !== void 0 && trimmed.length > 0 ? trimmed : void 0;
81
+ }
82
+ function detectVersionSkew(response, mountedExtensionVersion) {
83
+ const reason = trimmedOrUndefined(readHeader(response.headers, CAPABILITY_REJECTION_REASON_HEADER));
84
+ if (reason !== void 0 && reason.toLowerCase() === VERSION_MISMATCH_REASON) {
85
+ return {
86
+ activeVersion: trimmedOrUndefined(readHeader(response.headers, EXTENSION_ACTIVE_VERSION_HEADER)),
87
+ detectedFrom: "rejection"
88
+ };
89
+ }
90
+ if (!response.ok) {
91
+ return null;
92
+ }
93
+ const mounted = trimmedOrUndefined(mountedExtensionVersion);
94
+ const served = trimmedOrUndefined(readHeader(response.headers, EXTENSION_VERSION_HEADER));
95
+ if (mounted === void 0 || served === void 0 || served === mounted) {
96
+ return null;
97
+ }
98
+ return { activeVersion: served, detectedFrom: "response-stamp" };
99
+ }
100
+
57
101
  // src/host/worker/bridge-envelopes.ts
58
102
  var BRIDGE_PUSH_THEME = "ethisys:bridge:theme";
59
103
  var BRIDGE_PUSH_LOCALE = "ethisys:bridge:locale";
@@ -73,6 +117,9 @@ var WorkerRemoteDomTransport = class {
73
117
  workerPort;
74
118
  mcpClient;
75
119
  capabilityTokenProvider;
120
+ mountedExtensionVersion;
121
+ onVersionSkew;
122
+ versionSkewReported = false;
76
123
  coalesceMs;
77
124
  maxConcurrentMcpRequests;
78
125
  abortController;
@@ -95,6 +142,8 @@ var WorkerRemoteDomTransport = class {
95
142
  }
96
143
  this.mcpClient = options.mcpClient;
97
144
  this.capabilityTokenProvider = options.capabilityToken;
145
+ this.mountedExtensionVersion = options.mountedExtensionVersion;
146
+ this.onVersionSkew = options.onVersionSkew;
98
147
  this.coalesceMs = options.coalesceMs ?? 16;
99
148
  this.maxConcurrentMcpRequests = Math.max(
100
149
  1,
@@ -378,6 +427,34 @@ var WorkerRemoteDomTransport = class {
378
427
  requestSignal(id) {
379
428
  return this.inFlightAborts.get(id)?.signal ?? this.abortController.signal;
380
429
  }
430
+ /**
431
+ * Raises the skew callback the first time this surface is seen to be running against a version
432
+ * that is no longer installed.
433
+ *
434
+ * Latched deliberately. A page typically has several MCP calls in flight, and after an upgrade
435
+ * every one of them reports the same skew — firing per response would ask the host to remount
436
+ * the same surface repeatedly, and the second remount would tear down the first. One signal per
437
+ * transport is all a remount needs, and the transport is discarded along with the surface.
438
+ *
439
+ * The callback is host code, so it is assumed to be able to throw. Two consequences are guarded
440
+ * here: the throw must not escape into the MCP handler's catch (which would reply `ok: false`
441
+ * for a call that actually succeeded), and it must not consume the latch (which would leave the
442
+ * surface with no response AND no further chance of being told to remount).
443
+ */
444
+ reportVersionSkew(response) {
445
+ if (this.versionSkewReported || this.onVersionSkew === void 0) {
446
+ return;
447
+ }
448
+ const skew = detectVersionSkew(response, this.mountedExtensionVersion);
449
+ if (skew === null) {
450
+ return;
451
+ }
452
+ try {
453
+ this.onVersionSkew(skew);
454
+ this.versionSkewReported = true;
455
+ } catch {
456
+ }
457
+ }
381
458
  async handleInvokeTool(message) {
382
459
  let token;
383
460
  try {
@@ -397,6 +474,7 @@ var WorkerRemoteDomTransport = class {
397
474
  capabilityToken: token,
398
475
  signal: this.requestSignal(message.id)
399
476
  });
477
+ this.reportVersionSkew(result);
400
478
  this.safePostMessage({
401
479
  id: message.id,
402
480
  type: "ethisys:mcp:invokeTool:result",
@@ -426,6 +504,7 @@ var WorkerRemoteDomTransport = class {
426
504
  capabilityToken: token,
427
505
  signal: this.requestSignal(message.id)
428
506
  });
507
+ this.reportVersionSkew(result);
429
508
  this.safePostMessage({
430
509
  id: message.id,
431
510
  type: "ethisys:mcp:getResource:result",
@@ -456,6 +535,7 @@ var WorkerRemoteDomTransport = class {
456
535
  capabilityToken: token,
457
536
  signal: this.requestSignal(message.id)
458
537
  });
538
+ this.reportVersionSkew(result);
459
539
  this.safePostMessage({
460
540
  id: message.id,
461
541
  type: "ethisys:mcp:uploadDocument:result",