opentray 0.28.0 → 0.30.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/README.md +28 -0
- package/dist/index.d.mts +120 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +284 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
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
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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
|