@blue-soda/dsh-remote 0.5.2 → 0.5.3
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 +2 -2
- package/dist/index.js +114 -20
- package/dist/index.js.map +3 -3
- package/dsh-plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Continue DeepSeek Harness sessions and experimental Codex workspaces from anothe
|
|
|
23
23
|
Add the current package version to the `web` profile, then restart Harness:
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
|
-
dsh plugin --profile web add -w @blue-soda/dsh-remote@0.5.
|
|
26
|
+
dsh plugin --profile web add -w @blue-soda/dsh-remote@0.5.3
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### dsh-TUI Host
|
|
@@ -31,7 +31,7 @@ dsh plugin --profile web add -w @blue-soda/dsh-remote@0.5.2
|
|
|
31
31
|
Remote can also run as a Host in a terminal-only [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) profile:
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
dsh plugin --profile dsh-tui add -w @blue-soda/dsh-remote@0.5.
|
|
34
|
+
dsh plugin --profile dsh-tui add -w @blue-soda/dsh-remote@0.5.3
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
After starting dsh-TUI, manage Remote with `/remote`, `/remote login`, `/remote status`, and `/remote logout`.
|
package/dist/index.js
CHANGED
|
@@ -13162,7 +13162,7 @@ function fromBase64Url2(value) {
|
|
|
13162
13162
|
import { platform } from "node:os";
|
|
13163
13163
|
|
|
13164
13164
|
// src/server-credentials.ts
|
|
13165
|
-
import { chmod, mkdir, readFile, rm as rm2, stat as stat2, writeFile } from "node:fs/promises";
|
|
13165
|
+
import { chmod, mkdir, readFile, rename as rename2, rm as rm2, stat as stat2, writeFile } from "node:fs/promises";
|
|
13166
13166
|
import { dirname, join as join2 } from "node:path";
|
|
13167
13167
|
|
|
13168
13168
|
// src/atomic-file.ts
|
|
@@ -13237,6 +13237,11 @@ function uuidV7(now = Date.now()) {
|
|
|
13237
13237
|
}
|
|
13238
13238
|
|
|
13239
13239
|
// src/server-credentials.ts
|
|
13240
|
+
var LOCK_ABANDONED_MS = 10 * 6e4;
|
|
13241
|
+
var lockOwnerSchema = external_exports.object({
|
|
13242
|
+
pid: external_exports.number().int().positive(),
|
|
13243
|
+
at: external_exports.number().int().nonnegative()
|
|
13244
|
+
});
|
|
13240
13245
|
var credentialSchema = external_exports.object({
|
|
13241
13246
|
schemaVersion: external_exports.literal(1),
|
|
13242
13247
|
serverUrl: external_exports.string().url(),
|
|
@@ -13254,8 +13259,13 @@ var ServerCredentialStore = class {
|
|
|
13254
13259
|
this.path = join2(directory, "server-credentials.json");
|
|
13255
13260
|
}
|
|
13256
13261
|
/** Serialize the complete read/refresh/write transaction across processes.
|
|
13257
|
-
*
|
|
13258
|
-
*
|
|
13262
|
+
*
|
|
13263
|
+
* A lock left by a process that no longer exists can never be released by that process, and being killed
|
|
13264
|
+
* mid-refresh is routine on a phone (the app is backgrounded or swept away) - the old rule then left the user
|
|
13265
|
+
* with `SERVER_CREDENTIALS_BUSY` forever and told them to delete a file they cannot reach. So the lock now
|
|
13266
|
+
* records its owner and a contender reclaims it when that owner is **gone**. Liveness is what is checked, not
|
|
13267
|
+
* age: an owner that is merely suspended still protects its one-use refresh token, which is exactly what the
|
|
13268
|
+
* old unconditional rule was guarding against.
|
|
13259
13269
|
*/
|
|
13260
13270
|
async withRefreshLock(operation) {
|
|
13261
13271
|
const lock = `${this.path}.refresh-lock`;
|
|
@@ -13263,10 +13273,11 @@ var ServerCredentialStore = class {
|
|
|
13263
13273
|
const deadline = Date.now() + 15e3;
|
|
13264
13274
|
for (; ; ) {
|
|
13265
13275
|
try {
|
|
13266
|
-
await
|
|
13276
|
+
await claimRefreshLock(lock);
|
|
13267
13277
|
break;
|
|
13268
13278
|
} catch (error) {
|
|
13269
13279
|
if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") throw error;
|
|
13280
|
+
if (await reclaimAbandonedRefreshLock(lock)) continue;
|
|
13270
13281
|
if (Date.now() >= deadline) throw new ServerCredentialsBusyError();
|
|
13271
13282
|
await new Promise((resolve4) => setTimeout(resolve4, 50));
|
|
13272
13283
|
}
|
|
@@ -13274,7 +13285,7 @@ var ServerCredentialStore = class {
|
|
|
13274
13285
|
try {
|
|
13275
13286
|
return await operation();
|
|
13276
13287
|
} finally {
|
|
13277
|
-
await rm2(lock, { recursive: true });
|
|
13288
|
+
await rm2(lock, { recursive: true, force: true });
|
|
13278
13289
|
}
|
|
13279
13290
|
}
|
|
13280
13291
|
async load(serverUrl, deviceId) {
|
|
@@ -13307,6 +13318,49 @@ var ServerCredentialsBusyError = class extends Error {
|
|
|
13307
13318
|
super("Credential refresh is locked. Stop other instances; after a crash, stop all instances before removing server-credentials.json.refresh-lock and authorizing again.");
|
|
13308
13319
|
}
|
|
13309
13320
|
};
|
|
13321
|
+
async function claimRefreshLock(lock) {
|
|
13322
|
+
await mkdir(lock, { mode: 448 });
|
|
13323
|
+
await writeFile(join2(lock, "owner.json"), `${JSON.stringify({ pid: process.pid, at: Date.now() })}
|
|
13324
|
+
`, {
|
|
13325
|
+
encoding: "utf8",
|
|
13326
|
+
mode: 384
|
|
13327
|
+
});
|
|
13328
|
+
}
|
|
13329
|
+
async function reclaimAbandonedRefreshLock(lock) {
|
|
13330
|
+
const owner = await readRefreshLockOwner(lock);
|
|
13331
|
+
const abandoned = owner === void 0 ? await olderThan(lock, LOCK_ABANDONED_MS) : !processAlive(owner.pid);
|
|
13332
|
+
if (!abandoned) return false;
|
|
13333
|
+
const tomb = `${lock}.${process.pid}.${uuidV7()}.abandoned`;
|
|
13334
|
+
try {
|
|
13335
|
+
await rename2(lock, tomb);
|
|
13336
|
+
} catch {
|
|
13337
|
+
return false;
|
|
13338
|
+
}
|
|
13339
|
+
await rm2(tomb, { recursive: true, force: true });
|
|
13340
|
+
return true;
|
|
13341
|
+
}
|
|
13342
|
+
async function readRefreshLockOwner(lock) {
|
|
13343
|
+
try {
|
|
13344
|
+
return lockOwnerSchema.parse(JSON.parse(await readFile(join2(lock, "owner.json"), "utf8")));
|
|
13345
|
+
} catch {
|
|
13346
|
+
return void 0;
|
|
13347
|
+
}
|
|
13348
|
+
}
|
|
13349
|
+
function processAlive(pid) {
|
|
13350
|
+
try {
|
|
13351
|
+
process.kill(pid, 0);
|
|
13352
|
+
return true;
|
|
13353
|
+
} catch (error) {
|
|
13354
|
+
return !(error instanceof Error && "code" in error && error.code === "ESRCH");
|
|
13355
|
+
}
|
|
13356
|
+
}
|
|
13357
|
+
async function olderThan(path, ageMs) {
|
|
13358
|
+
try {
|
|
13359
|
+
return Date.now() - (await stat2(path)).mtimeMs > ageMs;
|
|
13360
|
+
} catch {
|
|
13361
|
+
return false;
|
|
13362
|
+
}
|
|
13363
|
+
}
|
|
13310
13364
|
async function atomicWrite(path, contents) {
|
|
13311
13365
|
const directory = dirname(path);
|
|
13312
13366
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
@@ -13462,7 +13516,7 @@ function normalizeServerUrl(value) {
|
|
|
13462
13516
|
}
|
|
13463
13517
|
|
|
13464
13518
|
// src/version.ts
|
|
13465
|
-
var PLUGIN_VERSION = "0.5.
|
|
13519
|
+
var PLUGIN_VERSION = "0.5.3";
|
|
13466
13520
|
|
|
13467
13521
|
// src/server-api.ts
|
|
13468
13522
|
var ENABLED_QR_PROVIDERS = ["github"];
|
|
@@ -20291,8 +20345,12 @@ var TypertGatewaySwitch = class {
|
|
|
20291
20345
|
installed = false;
|
|
20292
20346
|
/** Defaults to reachable so a caller that never wires this keeps the old behaviour. */
|
|
20293
20347
|
remoteAvailability = () => true;
|
|
20294
|
-
|
|
20348
|
+
/** Supplies the peer carrier calls speak for; see {@link ownerPeer}. */
|
|
20349
|
+
peerFor;
|
|
20350
|
+
peer;
|
|
20351
|
+
constructor(gateway, options = {}) {
|
|
20295
20352
|
this.runtime = gateway;
|
|
20353
|
+
this.peerFor = options.peer;
|
|
20296
20354
|
this.originalInvoke = gateway.invoke;
|
|
20297
20355
|
this.localInvoke = this.originalInvoke.bind(gateway);
|
|
20298
20356
|
this.originalStream = gateway.stream;
|
|
@@ -20300,18 +20358,42 @@ var TypertGatewaySwitch = class {
|
|
|
20300
20358
|
this.originalDispatch = this.runtime.dispatchRpc;
|
|
20301
20359
|
this.localDispatch = this.runtime.dispatchRpc?.bind(gateway);
|
|
20302
20360
|
this.originalOpen = this.runtime.openWireStream;
|
|
20303
|
-
this.localOpen = createLocalOpen(gateway, this.runtime);
|
|
20361
|
+
this.localOpen = createLocalOpen(gateway, this.runtime, () => this.ownerPeer());
|
|
20362
|
+
}
|
|
20363
|
+
/**
|
|
20364
|
+
* The peer a carrier call speaks for, created once and reused.
|
|
20365
|
+
*
|
|
20366
|
+
* dsh 0.2.1-rc.2 reads `peer.signal` on every carrier call and rejects one that arrives without a peer; a remote
|
|
20367
|
+
* client runs with the Host owner's authority (decided 2026-10-19), which is the same principal dsh gives its own
|
|
20368
|
+
* browser peer, so the plugin serves remote calls as the owner. Releases before rc.2 ignore the argument, and a
|
|
20369
|
+
* profile without a `connection` service simply has no peer to offer.
|
|
20370
|
+
*/
|
|
20371
|
+
ownerPeer() {
|
|
20372
|
+
if (this.peer !== void 0) return this.peer;
|
|
20373
|
+
this.peer = this.peerFor?.();
|
|
20374
|
+
return this.peer;
|
|
20375
|
+
}
|
|
20376
|
+
/** Trailing argument the carrier wants when the caller did not supply one. */
|
|
20377
|
+
peerArguments() {
|
|
20378
|
+
const peer = this.ownerPeer();
|
|
20379
|
+
return peer === void 0 ? [] : [peer];
|
|
20304
20380
|
}
|
|
20305
20381
|
/** Original local dispatcher, used by the Host bridge without switch recursion. */
|
|
20306
20382
|
local() {
|
|
20307
|
-
const
|
|
20383
|
+
const bare = this.localDispatch;
|
|
20384
|
+
const dispatch = bare === void 0 ? (async (endpoint, payload, signal) => {
|
|
20308
20385
|
try {
|
|
20309
20386
|
const request = requestFromCarrier(endpoint, payload, signal);
|
|
20310
20387
|
return { ok: true, value: await this.localInvoke(request) };
|
|
20311
20388
|
} catch (error) {
|
|
20312
20389
|
return { ok: false, error: this.failure(error) };
|
|
20313
20390
|
}
|
|
20314
|
-
})
|
|
20391
|
+
}) : (endpoint, payload, signal, ...rest) => Promise.resolve(bare(
|
|
20392
|
+
endpoint,
|
|
20393
|
+
payload,
|
|
20394
|
+
signal,
|
|
20395
|
+
...rest.length === 0 ? this.peerArguments() : rest
|
|
20396
|
+
));
|
|
20315
20397
|
const open = this.localOpen ?? (async (endpoint, payload, signal) => {
|
|
20316
20398
|
if (this.localStream === void 0) throw new Error("The local Harness Gateway does not support Remote streams.");
|
|
20317
20399
|
return this.localStream(requestFromCarrier(endpoint, payload, signal));
|
|
@@ -20346,17 +20428,17 @@ var TypertGatewaySwitch = class {
|
|
|
20346
20428
|
);
|
|
20347
20429
|
}
|
|
20348
20430
|
if (previousDispatch !== void 0) {
|
|
20349
|
-
this.runtime.dispatchRpc = (endpoint, payload, signal) => forwards(endpoint) ? Promise.resolve(previousDispatch(endpoint, payload, signal)).catch((error) => {
|
|
20431
|
+
this.runtime.dispatchRpc = (endpoint, payload, signal, ...rest) => forwards(endpoint) ? Promise.resolve(previousDispatch(endpoint, payload, signal, ...rest)).catch((error) => {
|
|
20350
20432
|
console.warn("[dsh-remote] local call failed with no remote target selected", {
|
|
20351
20433
|
endpoint,
|
|
20352
20434
|
code: typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0,
|
|
20353
20435
|
message: error instanceof Error ? error.message : String(error)
|
|
20354
20436
|
});
|
|
20355
20437
|
throw error;
|
|
20356
|
-
}) : !this.routesToRemote(endpoint) ? this.localDispatch(endpoint, payload, signal) : this.withLocalFallback(
|
|
20438
|
+
}) : !this.routesToRemote(endpoint) ? this.localDispatch(endpoint, payload, signal, ...rest) : this.withLocalFallback(
|
|
20357
20439
|
endpoint,
|
|
20358
20440
|
() => Promise.resolve(this.remoteTarget.dispatch(endpoint, payload, signal)),
|
|
20359
|
-
() => this.localDispatch(endpoint, payload, signal)
|
|
20441
|
+
() => this.localDispatch(endpoint, payload, signal, ...rest)
|
|
20360
20442
|
).then((result) => normalizeByteResult3(endpoint, result));
|
|
20361
20443
|
}
|
|
20362
20444
|
if (previousOpen !== void 0) {
|
|
@@ -20495,17 +20577,17 @@ function linkControl(signal) {
|
|
|
20495
20577
|
else signal.addEventListener("abort", () => control.abort(signal.reason), { once: true });
|
|
20496
20578
|
return control;
|
|
20497
20579
|
}
|
|
20498
|
-
function createLocalOpen(gateway, runtime) {
|
|
20580
|
+
function createLocalOpen(gateway, runtime, peer = () => void 0) {
|
|
20499
20581
|
const open = runtime.openWireStream;
|
|
20500
20582
|
if (open !== void 0) {
|
|
20501
|
-
return usesRc1Arity(open) ? (endpoint, payload, signal) => open.call(runtime, endpoint, payload, endedUplink(),
|
|
20583
|
+
return usesRc1Arity(open) ? (endpoint, payload, signal) => open.call(runtime, endpoint, payload, endedUplink(), peer(), signal, linkControl(signal)) : (endpoint, payload, signal) => open.call(runtime, endpoint, payload, signal);
|
|
20502
20584
|
}
|
|
20503
20585
|
const wire = gateway.wireStream;
|
|
20504
20586
|
if (wire === void 0) return void 0;
|
|
20505
20587
|
const wireOpen = wire.open;
|
|
20506
20588
|
if (usesRc1Arity(wireOpen)) {
|
|
20507
20589
|
const rc1 = wireOpen;
|
|
20508
|
-
return (endpoint, payload, signal) => rc1.call(wire, endpoint, payload, endedUplink(),
|
|
20590
|
+
return (endpoint, payload, signal) => rc1.call(wire, endpoint, payload, endedUplink(), peer(), signal);
|
|
20509
20591
|
}
|
|
20510
20592
|
const legacy = wireOpen;
|
|
20511
20593
|
return (endpoint, payload, signal) => legacy.call(wire, endpoint, payload, signal);
|
|
@@ -21673,7 +21755,7 @@ var QUICK_RECONNECT_RETRY_DELAY_MS = 5e3;
|
|
|
21673
21755
|
var QUICK_RECONNECT_FORCE_RELAY = true;
|
|
21674
21756
|
var BACKGROUND_RETRY_DELAYS_MS = [5e3, 1e4, 2e4, 3e4, 3e4, 3e4, 3e4];
|
|
21675
21757
|
var ClientModeRuntime = class {
|
|
21676
|
-
constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory, targetStore) {
|
|
21758
|
+
constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory, targetStore, peer) {
|
|
21677
21759
|
this.config = config;
|
|
21678
21760
|
this.identities = identities;
|
|
21679
21761
|
this.server = server;
|
|
@@ -21681,8 +21763,9 @@ var ClientModeRuntime = class {
|
|
|
21681
21763
|
this.host = host;
|
|
21682
21764
|
this.rtcFactoryProvider = rtcFactoryProvider;
|
|
21683
21765
|
this.targetStore = targetStore;
|
|
21766
|
+
this.peer = peer;
|
|
21684
21767
|
this.proxySwitch = apiProxy === void 0 ? void 0 : new ApiProxySwitch(apiProxy);
|
|
21685
|
-
this.gatewaySwitch = new TypertGatewaySwitch(typertGateway);
|
|
21768
|
+
this.gatewaySwitch = new TypertGatewaySwitch(typertGateway, { peer: this.peer });
|
|
21686
21769
|
this.gatewaySwitch.setRemoteAvailability(() => this.connected !== void 0);
|
|
21687
21770
|
}
|
|
21688
21771
|
preview;
|
|
@@ -30269,8 +30352,18 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
|
|
|
30269
30352
|
const deviceDirectory = config.serverUrl === void 0 ? defaultIdentityDirectory : await ensureDeviceDirectory(defaultIdentityDirectory, config.serverUrl);
|
|
30270
30353
|
const hostIdentities = new IdentityStore({ directory: deviceDirectory });
|
|
30271
30354
|
const apiProxy = ctx.get("apiProxy");
|
|
30355
|
+
const connectionService = ctx.get("connection", false);
|
|
30356
|
+
let ownerPeerResolved = false;
|
|
30357
|
+
let ownerPeer;
|
|
30358
|
+
const peerFor = () => {
|
|
30359
|
+
if (!ownerPeerResolved) {
|
|
30360
|
+
ownerPeerResolved = true;
|
|
30361
|
+
ownerPeer = connectionService?.createPeer?.({ principal: { kind: "owner" } });
|
|
30362
|
+
}
|
|
30363
|
+
return ownerPeer;
|
|
30364
|
+
};
|
|
30272
30365
|
const nativeTypertGateway = ctx.get("typertGateway");
|
|
30273
|
-
const localTypertGateway = new TypertGatewaySwitch(nativeTypertGateway).local();
|
|
30366
|
+
const localTypertGateway = new TypertGatewaySwitch(nativeTypertGateway, { peer: peerFor }).local();
|
|
30274
30367
|
const subprocess = ctx.get("subprocess", false);
|
|
30275
30368
|
const deepseekAccount = ctx.get("deepseekAccount", false);
|
|
30276
30369
|
const deepseekSession = deepseekAccount === void 0 ? void 0 : {
|
|
@@ -30319,7 +30412,8 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
|
|
|
30319
30412
|
hostControl,
|
|
30320
30413
|
// The RTC factory keeps its default; the target store lives beside the plugin state.
|
|
30321
30414
|
void 0,
|
|
30322
|
-
new ClientTargetStore(defaultIdentityDirectory)
|
|
30415
|
+
new ClientTargetStore(defaultIdentityDirectory),
|
|
30416
|
+
peerFor
|
|
30323
30417
|
);
|
|
30324
30418
|
}
|
|
30325
30419
|
const controlRuntime = connection === void 0 ? void 0 : new PluginControlRuntime(config, defaultIdentityDirectory, settingsBinding, clientRuntime, hostControl, deepseekSession);
|