opentray 0.28.0 → 0.29.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/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { createRequire } from "node:module";
2
- import { PROTOCOL_VERSION, PROTOCOL_VERSION as PROTOCOL_VERSION$1, brokerArtifactIdentityEquals, createBrokerEndpointIdentity, createBrokerEndpointIdentity as createBrokerEndpointIdentity$1, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatUnixSocketPath as formatUnixSocketPath$1, formatWindowsPipeName, formatWindowsPipeName as formatWindowsPipeName$1, isBrokerArtifactIdentity, isSupportedProtocolVersion, parseServerFrame, sanitizeCallerLabel } from "@opentray/spec";
2
+ import { EXTENSION_EVENT_RECORD_MAX_BYTES, PROTOCOL_VERSION, PROTOCOL_VERSION as PROTOCOL_VERSION$1, brokerArtifactIdentityEquals, createBrokerEndpointIdentity, createBrokerEndpointIdentity as createBrokerEndpointIdentity$1, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatUnixSocketPath as formatUnixSocketPath$1, formatWindowsPipeName, formatWindowsPipeName as formatWindowsPipeName$1, isBrokerArtifactIdentity, isSupportedProtocolVersion, parseServerFrame, sanitizeCallerLabel } from "@opentray/spec";
3
3
  import { access, appendFile, link, mkdir, open, readFile, realpath, rm, stat, unlink, writeFile } from "node:fs/promises";
4
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
7
  import { createConnection } from "node:net";
6
8
  import { homedir } from "node:os";
7
- import { fileURLToPath } from "node:url";
8
9
  import { clearDarwinAppBundleOwner, convergeDarwinAppBundleIdentity, ensureDarwinAppBundle, resolveDefaultDarwinAppBundlePath, resolveOpenTrayPackageIdentity, updateDarwinAppLaunchDescriptor, writeDarwinAppBundleOwner } from "@opentray/packaging";
9
- import { createHash, randomUUID } from "node:crypto";
10
10
  import { constants } from "node:fs";
11
11
  import { spawn } from "node:child_process";
12
12
  import { generateDefaultAppIcon } from "@opentray/icon";
@@ -237,6 +237,20 @@ const normalizePlatform = (platform) => {
237
237
  };
238
238
  //#endregion
239
239
  //#region src/native-extension-artifact.ts
240
+ /**
241
+ * Frozen embedded staging matrix (add-ext-dialog section 6.4): a facade that
242
+ * embeds platform binaries stages exactly these four targets. The runtime
243
+ * resolver requires the staging manifest to be complete over this matrix
244
+ * (release staging enforces the same set before anything is packed); one
245
+ * missing cell is a manifest-invalid artifact, never a silently thinner
246
+ * catalog.
247
+ */
248
+ const NATIVE_EXTENSION_EMBEDDED_TARGET_MATRIX = [
249
+ "darwin-arm64",
250
+ "darwin-x64",
251
+ "win32-arm64",
252
+ "win32-x64"
253
+ ];
240
254
  var NativeExtensionArtifactResolutionError = class extends Error {
241
255
  target;
242
256
  code = "OPENTRAY_NATIVE_EXTENSION_ARTIFACT_RESOLUTION_FAILED";
@@ -250,6 +264,27 @@ var NativeExtensionArtifactResolutionError = class extends Error {
250
264
  if (options.facadePackageJsonUrl !== void 0) this.facadePackageJsonUrl = options.facadePackageJsonUrl;
251
265
  }
252
266
  };
267
+ /** Stable machine code per embedded rejection reason (same family style as the resolution error). */
268
+ const NATIVE_EXTENSION_EMBEDDED_ERROR_CODES = {
269
+ "target-unsupported": "OPENTRAY_NATIVE_EXTENSION_TARGET_UNSUPPORTED",
270
+ "path-outside-facade": "OPENTRAY_NATIVE_EXTENSION_PATH_OUTSIDE_FACADE",
271
+ "manifest-invalid": "OPENTRAY_NATIVE_EXTENSION_MANIFEST_INVALID",
272
+ "library-unreadable": "OPENTRAY_NATIVE_EXTENSION_LIBRARY_UNREADABLE"
273
+ };
274
+ var NativeExtensionEmbeddedArtifactError = class extends Error {
275
+ reason;
276
+ code;
277
+ target;
278
+ facadePackageJsonUrl;
279
+ constructor(reason, message, options) {
280
+ super(message, options);
281
+ this.name = "NativeExtensionEmbeddedArtifactError";
282
+ this.reason = reason;
283
+ this.code = NATIVE_EXTENSION_EMBEDDED_ERROR_CODES[reason];
284
+ this.target = options.target;
285
+ if (options.facadePackageJsonUrl !== void 0) this.facadePackageJsonUrl = options.facadePackageJsonUrl;
286
+ }
287
+ };
253
288
  /** Resolve one exact native library from the dependency closure that owns the facade. */
254
289
  const resolveNativeExtensionArtifact = async (artifact, platform = process.platform, arch = process.arch) => {
255
290
  const target = nativeExtensionTarget(platform, arch);
@@ -258,6 +293,7 @@ const resolveNativeExtensionArtifact = async (artifact, platform = process.platf
258
293
  expectedIdentity: artifact.expectedIdentity ?? await resolveExpectedIdentity(artifact.identitySource, target),
259
294
  target
260
295
  };
296
+ if (artifact.kind === "embedded") return resolveEmbeddedExtensionArtifact(artifact, target);
261
297
  const packageTarget = artifact.targets[target];
262
298
  if (packageTarget === void 0) throw new NativeExtensionArtifactResolutionError(`native extension does not support target ${target}`, target);
263
299
  const expectedIdentity = await resolveExpectedIdentity(artifact, target);
@@ -295,6 +331,102 @@ const resolveExpectedIdentity = async (source, target) => {
295
331
  }
296
332
  };
297
333
  };
334
+ /** Fixed location of the embedded staging manifest inside the facade package. */
335
+ const EMBEDDED_STAGING_MANIFEST_PATH = "platforms/manifest.json";
336
+ const sha256HexPattern = /^[a-f0-9]{64}$/u;
337
+ const isEmbeddedMatrixTarget = (target) => NATIVE_EXTENSION_EMBEDDED_TARGET_MATRIX.includes(target);
338
+ const lexicalContained = (root, candidate) => {
339
+ const rel = relative(root, candidate);
340
+ if (rel.length === 0) return false;
341
+ return !(rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel));
342
+ };
343
+ const resolveEmbeddedExtensionArtifact = async (artifact, target) => {
344
+ const facadePackageJsonUrl = artifact.packageJsonUrl;
345
+ const embeddedError = (reason, message, options = {}) => new NativeExtensionEmbeddedArtifactError(reason, message, {
346
+ target,
347
+ facadePackageJsonUrl,
348
+ ...options.cause === void 0 ? {} : { cause: options.cause }
349
+ });
350
+ if (!isEmbeddedMatrixTarget(target)) throw embeddedError("target-unsupported", `embedded native extension supports only the frozen staging matrix (${NATIVE_EXTENSION_EMBEDDED_TARGET_MATRIX.join(", ")}); ${target} is outside it`);
351
+ const embeddedTarget = artifact.targets[target];
352
+ if (embeddedTarget === void 0) throw embeddedError("target-unsupported", `embedded native extension does not declare target ${target}`);
353
+ const facadeRoot = dirname(fileURLToPath(new URL(facadePackageJsonUrl)));
354
+ if (isAbsolute(embeddedTarget.libraryPath)) throw embeddedError("path-outside-facade", `embedded library path must be relative to the facade package root: ${embeddedTarget.libraryPath}`);
355
+ const candidatePath = resolve(facadeRoot, embeddedTarget.libraryPath);
356
+ if (!lexicalContained(facadeRoot, candidatePath)) throw embeddedError("path-outside-facade", `embedded library path escapes the facade package root: ${embeddedTarget.libraryPath}`);
357
+ const [facadeManifest, contractManifest] = await Promise.all([readEmbeddedJson(facadePackageJsonUrl, embeddedError).then((parsed) => {
358
+ if (!isFacadePackageManifest(parsed)) throw embeddedError("manifest-invalid", `embedded facade package manifest is invalid at ${facadePackageJsonUrl}`);
359
+ return parsed;
360
+ }), readEmbeddedJson(artifact.contractManifestUrl, embeddedError).then((parsed) => {
361
+ if (!isExtensionContractManifest(parsed)) throw embeddedError("manifest-invalid", `embedded extension contract manifest is invalid at ${artifact.contractManifestUrl}`);
362
+ return parsed;
363
+ })]);
364
+ const manifestPath = join(facadeRoot, EMBEDDED_STAGING_MANIFEST_PATH);
365
+ const manifest = await readEmbeddedJson(pathToFileURL(manifestPath).href, embeddedError).then((parsed) => {
366
+ if (!isEmbeddedStagingManifest(parsed)) throw embeddedError("manifest-invalid", `embedded staging manifest is invalid at ${manifestPath}`);
367
+ return parsed;
368
+ });
369
+ if (manifest.facadeVersion !== facadeManifest.version) throw embeddedError("manifest-invalid", `embedded staging manifest facade version ${manifest.facadeVersion} does not match facade package version ${facadeManifest.version}`);
370
+ if (manifest.contractFingerprint !== contractManifest.contractFingerprint) throw embeddedError("manifest-invalid", `embedded staging manifest contract fingerprint does not match the contract manifest at ${artifact.contractManifestUrl}`);
371
+ for (const matrixTarget of NATIVE_EXTENSION_EMBEDDED_TARGET_MATRIX) if (manifest.targets[matrixTarget] === void 0) throw embeddedError("manifest-invalid", `embedded staging manifest is missing required matrix target ${matrixTarget}`);
372
+ for (const manifestTarget of Object.keys(manifest.targets)) {
373
+ if (!isEmbeddedMatrixTarget(manifestTarget)) throw embeddedError("manifest-invalid", `embedded staging manifest declares target ${manifestTarget} outside the frozen staging matrix`);
374
+ if (artifact.targets[manifestTarget] === void 0) throw embeddedError("manifest-invalid", `embedded staging manifest declares undeclared target ${manifestTarget}`);
375
+ }
376
+ const manifestEntry = manifest.targets[target];
377
+ if (manifestEntry === void 0) throw embeddedError("manifest-invalid", `embedded staging manifest has no entry for target ${target}`);
378
+ if (isAbsolute(manifestEntry.path)) throw embeddedError("path-outside-facade", `embedded staging manifest library path must be relative to the facade package root: ${manifestEntry.path}`);
379
+ const manifestLibraryPath = resolve(facadeRoot, manifestEntry.path);
380
+ if (!lexicalContained(facadeRoot, manifestLibraryPath)) throw embeddedError("path-outside-facade", `embedded staging manifest library path escapes the facade package root: ${manifestEntry.path}`);
381
+ if (manifestLibraryPath !== candidatePath) throw embeddedError("manifest-invalid", `embedded staging manifest path ${manifestEntry.path} does not match the declared library path ${embeddedTarget.libraryPath} for target ${target}`);
382
+ const realRoot = await realpath(facadeRoot).catch(() => facadeRoot);
383
+ let realCandidate;
384
+ try {
385
+ realCandidate = await realpath(candidatePath);
386
+ } catch (cause) {
387
+ throw embeddedError("library-unreadable", `embedded native extension library is not accessible at ${candidatePath}`, { cause });
388
+ }
389
+ if (!lexicalContained(realRoot, realCandidate)) throw embeddedError("path-outside-facade", `embedded library resolves outside the facade package root: ${embeddedTarget.libraryPath}`);
390
+ let libraryBytes;
391
+ try {
392
+ libraryBytes = await readFile(realCandidate);
393
+ } catch (cause) {
394
+ throw embeddedError("library-unreadable", `embedded native extension library is not readable at ${realCandidate}`, { cause });
395
+ }
396
+ const sha256 = createHash("sha256").update(libraryBytes).digest("hex");
397
+ if (sha256 !== manifestEntry.sha256) throw embeddedError("manifest-invalid", `embedded library bytes do not match the staging manifest SHA-256 for target ${target}`);
398
+ const [os, arch] = splitTarget(target);
399
+ const expectedIdentity = {
400
+ extensionName: contractManifest.extensionName,
401
+ artifactSetVersion: facadeManifest.version,
402
+ contractFingerprint: contractManifest.contractFingerprint,
403
+ target: {
404
+ os,
405
+ arch
406
+ },
407
+ sha256,
408
+ buildIdentity: manifestEntry.buildIdentity
409
+ };
410
+ return {
411
+ path: realCandidate,
412
+ expectedIdentity,
413
+ target
414
+ };
415
+ };
416
+ const readEmbeddedJson = async (url, embeddedError) => {
417
+ try {
418
+ return JSON.parse(await readFile(new URL(url), "utf8"));
419
+ } catch (cause) {
420
+ throw embeddedError("manifest-invalid", `unable to read embedded identity-chain manifest at ${url}`, { cause });
421
+ }
422
+ };
423
+ const isEmbeddedStagingManifest = (value) => {
424
+ if (!isRecordLike(value) || !isRecordLike(value.targets)) return false;
425
+ if (typeof value.facadeVersion !== "string" || typeof value.contractFingerprint !== "string") return false;
426
+ for (const entry of Object.values(value.targets)) if (!isRecordLike(entry) || typeof entry.path !== "string" || typeof entry.sha256 !== "string" || !sha256HexPattern.test(entry.sha256) || typeof entry.buildIdentity !== "string" || entry.buildIdentity.length === 0) return false;
427
+ return true;
428
+ };
429
+ const isRecordLike = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
298
430
  const nativeExtensionTarget = (platform, arch) => {
299
431
  if (arch !== "arm64" && arch !== "x64") throw new Error(`unsupported native extension architecture: ${arch}`);
300
432
  return `${platform}-${arch}`;
@@ -351,6 +483,23 @@ const resolveAccessibleLibrary = async (path, target, packageName) => {
351
483
  };
352
484
  //#endregion
353
485
  //#region src/client.ts
486
+ /**
487
+ * Typed rejection for synchronous broker error frames: carries the wire
488
+ * `code`, the structured `details` payload when the error family freezes one,
489
+ * and the transport cause when relevant. Consumers match on `code`; the human
490
+ * `message` is not a contract. The deferred-operation rejection counterpart
491
+ * is `ExtensionOperationError` (same `{ code, message, details }` family).
492
+ */
493
+ var BrokerServerError = class extends Error {
494
+ code;
495
+ details;
496
+ constructor(code, message, options = {}) {
497
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
498
+ this.name = "BrokerServerError";
499
+ this.code = code;
500
+ if (options.details !== void 0) this.details = options.details;
501
+ }
502
+ };
354
503
  const createInitFrame = (clientVersion) => ({
355
504
  type: "init",
356
505
  protocolVersion: PROTOCOL_VERSION$1,
@@ -472,9 +621,24 @@ function createTrayHandle(transport, appId, trayId, nextRequestId = createReques
472
621
  });
473
622
  if (response.type === "ack") {
474
623
  expectResponse(response, requestId, "ack");
475
- return [];
624
+ return {
625
+ kind: "immediate",
626
+ events: []
627
+ };
476
628
  }
477
- return expectResponse(response, requestId, "ext-command-result").events;
629
+ if (response.type === "ext-operation-terminal") {
630
+ const payload = response.payload;
631
+ if (payload.kind !== "result") throw new Error(`transport settled a deferred request with an error terminal for ${requestId}`);
632
+ return {
633
+ kind: "terminal",
634
+ operationId: response.operationId,
635
+ value: payload.value
636
+ };
637
+ }
638
+ return {
639
+ kind: "immediate",
640
+ events: expectResponse(response, requestId, "ext-command-result").events
641
+ };
478
642
  },
479
643
  extend(extension, options) {
480
644
  const context = createTrayExtensionContext(handle, appId, extension, options, nextMountId, sessionId);
@@ -665,7 +829,7 @@ const createRequestIdFactory = (prefix) => {
665
829
  };
666
830
  };
667
831
  const expectResponse = (frame, requestId, type) => {
668
- if (frame.type === "error") throw new Error(`${frame.code}: ${frame.message}`);
832
+ if (frame.type === "error") throw new BrokerServerError(frame.code, frame.message, { details: frame.details });
669
833
  if (frame.type !== type) throw new Error(`expected ${type} for ${requestId}, received ${frame.type}`);
670
834
  if (requestIdOf(frame) !== requestId) throw new Error(`expected response for ${requestId}, received ${requestIdOf(frame) ?? "none"}`);
671
835
  return frame;
@@ -681,6 +845,8 @@ const requestIdOf = (frame) => {
681
845
  case "ext-command-result":
682
846
  case "runtime-host-health": return frame.requestId;
683
847
  case "error": return frame.requestId;
848
+ case "ext-command-accepted":
849
+ case "ext-operation-terminal":
684
850
  case "ready":
685
851
  case "event":
686
852
  case "app-event":
@@ -1365,6 +1531,50 @@ const packageJsonUrl = new URL("../package.json", import.meta.url);
1365
1531
  * state instead of a failure (P3.6 quit-path finding, 2026-09-12).
1366
1532
  */
1367
1533
  const BROKER_CONNECTION_CLOSED_MESSAGE = "broker connection closed";
1534
+ /**
1535
+ * Generic transport-close rejection code for every pending deferred operation
1536
+ * (add-ext-dialog section 5.1 frozen): the core client carries no extension-specific
1537
+ * branch. Extension facades map this shared code onto their public surface
1538
+ * (e.g. ext-dialog's `dialog_transport_closed`) - that mapping is facade work,
1539
+ * never core work.
1540
+ */
1541
+ const EXTENSION_TRANSPORT_CLOSED_CODE = "extension_transport_closed";
1542
+ /**
1543
+ * Typed rejection every deferred operation settles with: the `error` payload
1544
+ * branch of an `ext-operation-terminal` frame, or the generic
1545
+ * `extension_transport_closed` rejection when the transport dies while an
1546
+ * operation is pending. Consumers match on `code`; the human `message` is not
1547
+ * a contract. `details` carries the wire error's structured payload when the
1548
+ * extension supplied one.
1549
+ */
1550
+ var ExtensionOperationError = class extends Error {
1551
+ code;
1552
+ details;
1553
+ constructor(code, message, options = {}) {
1554
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
1555
+ this.name = "ExtensionOperationError";
1556
+ this.code = code;
1557
+ if (options.details !== void 0) this.details = options.details;
1558
+ }
1559
+ };
1560
+ const extensionTransportClosedError = (cause) => new ExtensionOperationError(EXTENSION_TRANSPORT_CLOSED_CODE, "the broker transport closed while a deferred extension operation was pending", { cause });
1561
+ /**
1562
+ * Typed handshake rejection for a Ready frame whose protocol version does not
1563
+ * equal the version this client sent in its Init frame: a structurally valid
1564
+ * Ready frame alone must never accept the session (add-ext-dialog batch A
1565
+ * review P1-9). The message names both sides; consumers may match
1566
+ * `instanceof` or the two numeric fields.
1567
+ */
1568
+ var BrokerProtocolVersionError = class extends Error {
1569
+ clientProtocolVersion;
1570
+ brokerProtocolVersion;
1571
+ constructor(clientProtocolVersion, brokerProtocolVersion) {
1572
+ super(`broker protocol version mismatch: client=${clientProtocolVersion} broker=${brokerProtocolVersion}`);
1573
+ this.name = "BrokerProtocolVersionError";
1574
+ this.clientProtocolVersion = clientProtocolVersion;
1575
+ this.brokerProtocolVersion = brokerProtocolVersion;
1576
+ }
1577
+ };
1368
1578
  const connectLocalBroker = async (options = {}) => {
1369
1579
  const packageVersion = options.packageVersion ?? await readPackageVersion(packageJsonUrl);
1370
1580
  const clientVersion = options.clientVersion ?? packageVersion;
@@ -1456,6 +1666,8 @@ var LocalBrokerConnection = class {
1456
1666
  buffer = "";
1457
1667
  listeners = /* @__PURE__ */ new Set();
1458
1668
  pending = /* @__PURE__ */ new Map();
1669
+ /** operationId -> requestId of accepted-but-unsettled deferred operations. */
1670
+ deferredOperations = /* @__PURE__ */ new Map();
1459
1671
  deadListeners = /* @__PURE__ */ new Set();
1460
1672
  deadError;
1461
1673
  ready;
@@ -1496,6 +1708,7 @@ var LocalBrokerConnection = class {
1496
1708
  clientVersion
1497
1709
  });
1498
1710
  const frame = await ready;
1711
+ if (frame.protocolVersion !== protocolVersion) throw new BrokerProtocolVersionError(protocolVersion, frame.protocolVersion);
1499
1712
  if (!brokerArtifactIdentityEquals(frame.brokerArtifactIdentity, expectedBrokerArtifactIdentity)) throw new Error(`broker artifact identity mismatch: expected=${JSON.stringify(expectedBrokerArtifactIdentity)} actual=${JSON.stringify(frame.brokerArtifactIdentity)}`);
1500
1713
  this.sessionId = frame.sessionId;
1501
1714
  }
@@ -1549,7 +1762,7 @@ var LocalBrokerConnection = class {
1549
1762
  return;
1550
1763
  }
1551
1764
  if (frame.type === "error") {
1552
- const error = /* @__PURE__ */ new Error(`${frame.code}: ${frame.message}`);
1765
+ const error = new BrokerServerError(frame.code, frame.message, { details: frame.details });
1553
1766
  if (frame.requestId !== void 0) {
1554
1767
  this.pending.get(frame.requestId)?.reject(error);
1555
1768
  this.pending.delete(frame.requestId);
@@ -1563,6 +1776,14 @@ var LocalBrokerConnection = class {
1563
1776
  this.rejectAll(error);
1564
1777
  return;
1565
1778
  }
1779
+ if (frame.type === "ext-command-accepted") {
1780
+ this.acceptDeferredOperation(frame.requestId, frame.operationId);
1781
+ return;
1782
+ }
1783
+ if (frame.type === "ext-operation-terminal") {
1784
+ this.settleDeferredOperation(frame.operationId, frame.payload);
1785
+ return;
1786
+ }
1566
1787
  const requestId = responseRequestId(frame);
1567
1788
  if (requestId !== void 0) {
1568
1789
  this.pending.get(requestId)?.resolve(frame);
@@ -1574,21 +1795,72 @@ var LocalBrokerConnection = class {
1574
1795
  write(frame) {
1575
1796
  this.socket.write(`${JSON.stringify(frame)}\n`);
1576
1797
  }
1798
+ /**
1799
+ * Pending-until-final acceptance (add-ext-dialog section 5.1): acceptance never
1800
+ * settles the request. The broker's owner loop guarantees exactly one
1801
+ * terminal per operation; a second acceptance for the same request or an
1802
+ * already-bound operationId is defensively ignored, never a second
1803
+ * registration.
1804
+ */
1805
+ acceptDeferredOperation(requestId, operationId) {
1806
+ const entry = this.pending.get(requestId);
1807
+ if (entry === void 0 || entry.deferredOperation !== void 0) return;
1808
+ if (this.deferredOperations.has(operationId)) return;
1809
+ entry.deferredOperation = operationId;
1810
+ this.deferredOperations.set(operationId, requestId);
1811
+ }
1812
+ /**
1813
+ * The single terminal settlement: resolves the still-pending request with
1814
+ * the terminal frame (`payload.kind === "result"`), or rejects it with the
1815
+ * typed wire error (`"error"`). Terminals for unknown or already-settled
1816
+ * operationIds are silently dropped - the broker side already settles
1817
+ * through a one-shot CAS; the client mirrors that stateless drop instead of
1818
+ * guessing an owner.
1819
+ */
1820
+ settleDeferredOperation(operationId, payload) {
1821
+ const requestId = this.deferredOperations.get(operationId);
1822
+ if (requestId === void 0) return;
1823
+ const entry = this.pending.get(requestId);
1824
+ this.deferredOperations.delete(operationId);
1825
+ this.pending.delete(requestId);
1826
+ if (entry === void 0) return;
1827
+ if (payload.kind === "result") {
1828
+ const terminal = {
1829
+ type: "ext-operation-terminal",
1830
+ operationId,
1831
+ payload
1832
+ };
1833
+ entry.resolve(terminal);
1834
+ return;
1835
+ }
1836
+ const wireError = payload.error;
1837
+ entry.reject(new ExtensionOperationError(wireError.code, wireError.message, { details: wireError.details }));
1838
+ }
1577
1839
  rejectAll(error) {
1578
1840
  this.ready?.reject(error);
1579
1841
  this.ready = void 0;
1580
1842
  for (const pending of this.pending.values()) pending.reject(error);
1581
1843
  this.pending.clear();
1844
+ this.deferredOperations.clear();
1582
1845
  }
1583
1846
  /**
1584
1847
  * Terminal connection-death transition (D3): first socket error/close wins,
1585
1848
  * rejects everything pending, and notifies every terminal listener exactly
1586
1849
  * once. Later terminal events are no-ops.
1850
+ *
1851
+ * Pending-until-final (add-ext-dialog section 5.1): accepted deferred operations
1852
+ * reject with the generic typed `extension_transport_closed` (the core
1853
+ * client has no extension-name branch); every ordinary pending request
1854
+ * keeps the plain transport sentinel.
1587
1855
  */
1588
1856
  markDead(error) {
1589
1857
  if (this.deadError !== void 0) return;
1590
1858
  this.deadError = error;
1591
- this.rejectAll(error);
1859
+ this.ready?.reject(error);
1860
+ this.ready = void 0;
1861
+ for (const pending of this.pending.values()) pending.reject(pending.deferredOperation === void 0 ? error : extensionTransportClosedError(error));
1862
+ this.pending.clear();
1863
+ this.deferredOperations.clear();
1592
1864
  for (const listener of [...this.deadListeners]) try {
1593
1865
  listener(error);
1594
1866
  } catch {}
@@ -1612,6 +1884,8 @@ const responseRequestId = (frame) => {
1612
1884
  case "ack":
1613
1885
  case "ext-command-result":
1614
1886
  case "runtime-host-health": return frame.requestId;
1887
+ case "ext-command-accepted":
1888
+ case "ext-operation-terminal":
1615
1889
  case "ready":
1616
1890
  case "event":
1617
1891
  case "app-event":
@@ -1861,6 +2135,6 @@ const bindMenuClickHandlers = (tray, handlers) => {
1861
2135
  };
1862
2136
  const noop = () => {};
1863
2137
  //#endregion
1864
- export { AppIconVariantNotFoundError, BROKER_CONNECTION_CLOSED_MESSAGE, DEFAULT_APP_ICON_VARIANT, InvalidAppIconError, NativeExtensionArtifactResolutionError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact, validateAppIcon };
2138
+ export { AppIconVariantNotFoundError, BROKER_CONNECTION_CLOSED_MESSAGE, BrokerProtocolVersionError, BrokerServerError, DEFAULT_APP_ICON_VARIANT, EMBEDDED_STAGING_MANIFEST_PATH, EXTENSION_EVENT_RECORD_MAX_BYTES, EXTENSION_TRANSPORT_CLOSED_CODE, ExtensionOperationError, InvalidAppIconError, NATIVE_EXTENSION_EMBEDDED_ERROR_CODES, NATIVE_EXTENSION_EMBEDDED_TARGET_MATRIX, NativeExtensionArtifactResolutionError, NativeExtensionEmbeddedArtifactError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact, validateAppIcon };
1865
2139
 
1866
2140
  //# sourceMappingURL=index.mjs.map