dsh-mobile 0.7.0 → 0.7.1

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/lib/index.d.mts CHANGED
@@ -2451,6 +2451,8 @@ export declare class IrohComponentManager {
2451
2451
  private findCompanionExecutable;
2452
2452
  private downloadArtifact;
2453
2453
  private publishBytes;
2454
+ /** Remove the installed sidecar component from the plugin private directory. */
2455
+ purge(): Promise<IrohComponentStatus>;
2454
2456
  private refresh;
2455
2457
  }
2456
2458
  //#endregion
package/lib/index.mjs CHANGED
@@ -14589,6 +14589,22 @@ var IrohComponentManager = class {
14589
14589
  if (failures.length > 0) throw new AggregateError(failures, "iroh_component_cleanup_failed");
14590
14590
  }
14591
14591
  }
14592
+ /** Remove the installed sidecar component from the plugin private directory. */
14593
+ async purge() {
14594
+ return this.enqueue(async () => {
14595
+ const failures = [];
14596
+ try {
14597
+ await removeIrohTree(this.stateDirectory, this.componentStorage);
14598
+ } catch (error) {
14599
+ if (error.code !== "ENOENT") failures.push(error);
14600
+ }
14601
+ this.cachedExecutable = void 0;
14602
+ this.origin = void 0;
14603
+ this.cached = this.baseStatus();
14604
+ if (failures.length > 0) throw new AggregateError(failures, "iroh_component_cleanup_failed");
14605
+ return this.cached;
14606
+ });
14607
+ }
14592
14608
  async refresh() {
14593
14609
  if (this.artifact === void 0) {
14594
14610
  this.origin = void 0;
@@ -19297,9 +19313,37 @@ function androidRelease(version, tag) {
19297
19313
  if (typeof version !== "string" || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u.test(version) || parseSemver(version) === void 0 || tag !== `v${version}` && tag !== `android-v${version}`) return void 0;
19298
19314
  return Object.freeze({
19299
19315
  version,
19316
+ tag,
19300
19317
  downloadUrl: `https://github.com/saya-ch/dsh-mobile/releases/download/${tag}/dsh-mobile-android-v${version}.apk`
19301
19318
  });
19302
19319
  }
19320
+ /** Read the published APK byte sizes from the GitHub release assets (best effort). */
19321
+ async function fetchAndroidAssetSizes(fetcher, version, tag, signal) {
19322
+ try {
19323
+ const response = await fetcher(`${GITHUB_RELEASE_API}${encodeURIComponent(tag)}`, {
19324
+ headers: {
19325
+ accept: "application/vnd.github+json",
19326
+ "user-agent": "dsh-mobile-release-check"
19327
+ },
19328
+ signal: AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)])
19329
+ });
19330
+ if (!response.ok) return {};
19331
+ const payload = await response.json();
19332
+ if (payload === null || typeof payload !== "object" || !("assets" in payload) || !Array.isArray(payload.assets)) return {};
19333
+ const assets = payload.assets;
19334
+ const sizeOf = (name) => {
19335
+ for (const asset of assets) if (asset !== null && typeof asset === "object" && "name" in asset && asset.name === name && "size" in asset && typeof asset.size === "number" && Number.isSafeInteger(asset.size) && asset.size > 0) return asset.size;
19336
+ };
19337
+ const compatBytes = sizeOf(`dsh-mobile-android-v${version}.apk`);
19338
+ const irohBytes = sizeOf(`dsh-mobile-android-v${version}-iroh.apk`);
19339
+ return {
19340
+ ...compatBytes === void 0 ? {} : { compatBytes },
19341
+ ...irohBytes === void 0 ? {} : { irohBytes }
19342
+ };
19343
+ } catch {
19344
+ return {};
19345
+ }
19346
+ }
19303
19347
  function androidReleaseDescriptor(payload) {
19304
19348
  if (typeof payload !== "object" || payload === null) return void 0;
19305
19349
  const fields = payload;
@@ -19518,6 +19562,9 @@ var PluginReleaseManager = class {
19518
19562
  now;
19519
19563
  cache;
19520
19564
  activeUpdate;
19565
+ assetLookupOwner = new AbortController();
19566
+ assetLookup;
19567
+ assetSizes;
19521
19568
  constructor(options) {
19522
19569
  this.profileDirectory = options.profileDirectory;
19523
19570
  this.installedVersion = options.installedVersion ?? DSH_MOBILE_VERSION;
@@ -19533,12 +19580,16 @@ var PluginReleaseManager = class {
19533
19580
  const npmVersion = fetchNpmVersion(this.fetcher);
19534
19581
  const [npmResult, androidResult, notesResult] = await Promise.allSettled([
19535
19582
  npmVersion,
19536
- fetchAndroidRelease(this.fetcher),
19583
+ fetchAndroidRelease(this.fetcher).then((android) => {
19584
+ if (android !== void 0) this.startAssetLookup(android);
19585
+ return android;
19586
+ }),
19537
19587
  npmVersion.then((version) => version === void 0 ? void 0 : fetchReleaseNotes(this.fetcher, version))
19538
19588
  ]);
19539
19589
  const latestVersion = npmResult.status === "fulfilled" ? npmResult.value : void 0;
19540
19590
  const android = androidResult.status === "fulfilled" ? androidResult.value : void 0;
19541
19591
  const releaseNotes = notesResult.status === "fulfilled" ? notesResult.value : void 0;
19592
+ const androidSizes = this.assetSizes?.downloadUrl === android?.downloadUrl ? this.assetSizes : void 0;
19542
19593
  const comparison = latestVersion === void 0 ? void 0 : comparePluginVersions(latestVersion, this.installedVersion);
19543
19594
  const status = Object.freeze({
19544
19595
  installedVersion: this.installedVersion,
@@ -19547,6 +19598,8 @@ var PluginReleaseManager = class {
19547
19598
  updateSupported,
19548
19599
  ...android === void 0 ? {} : { androidVersion: android.version },
19549
19600
  androidDownloadUrl: android?.downloadUrl ?? GITHUB_RELEASES_URL,
19601
+ ...androidSizes?.compatBytes === void 0 ? {} : { androidCompatBytes: androidSizes.compatBytes },
19602
+ ...androidSizes?.irohBytes === void 0 ? {} : { androidIrohBytes: androidSizes.irohBytes },
19550
19603
  ...releaseNotes === void 0 ? {} : { releaseNotes }
19551
19604
  });
19552
19605
  this.cache = {
@@ -19555,6 +19608,31 @@ var PluginReleaseManager = class {
19555
19608
  };
19556
19609
  return status;
19557
19610
  }
19611
+ /** Cancel and drain the optional APK-size lookup without delaying provider cleanup. */
19612
+ async dispose() {
19613
+ this.assetLookupOwner.abort();
19614
+ await this.assetLookup;
19615
+ }
19616
+ startAssetLookup(android) {
19617
+ if (this.assetLookupOwner.signal.aborted || this.assetLookup !== void 0 || this.assetSizes?.downloadUrl === android.downloadUrl) return;
19618
+ this.assetLookup = fetchAndroidAssetSizes(this.fetcher, android.version, android.tag, this.assetLookupOwner.signal).then((sizes) => {
19619
+ if (this.assetLookupOwner.signal.aborted) return;
19620
+ this.assetSizes = {
19621
+ downloadUrl: android.downloadUrl,
19622
+ ...sizes
19623
+ };
19624
+ if (this.cache?.status.androidDownloadUrl === android.downloadUrl) this.cache = {
19625
+ ...this.cache,
19626
+ status: Object.freeze({
19627
+ ...this.cache.status,
19628
+ ...sizes.compatBytes === void 0 ? {} : { androidCompatBytes: sizes.compatBytes },
19629
+ ...sizes.irohBytes === void 0 ? {} : { androidIrohBytes: sizes.irohBytes }
19630
+ })
19631
+ };
19632
+ }).finally(() => {
19633
+ this.assetLookup = void 0;
19634
+ });
19635
+ }
19558
19636
  /** Install the latest npm release into the active profile, then require a DSH restart. */
19559
19637
  async update() {
19560
19638
  if (this.activeUpdate !== void 0) return this.activeUpdate;
@@ -19908,6 +19986,7 @@ function installedDshVersion() {
19908
19986
  function mapAdminError(error) {
19909
19987
  if (error instanceof HttpError) return error;
19910
19988
  if (error instanceof MobileExtensionError) return new HttpError(error.status, error.code);
19989
+ if (error instanceof Error && error.message === "remote_provider_changed") return new HttpError(409, "remote_provider_changed");
19911
19990
  const code = error.code;
19912
19991
  if (error instanceof Error && error.message.includes("spawn UNKNOWN")) return new HttpError(409, "frp_component_launch_failed");
19913
19992
  if (code === "EADDRNOTAVAIL") return new HttpError(409, "network_address_changed");
@@ -20918,10 +20997,14 @@ async function apply(ctx, config) {
20918
20997
  }
20919
20998
  if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/cloudflared/tunnel`) {
20920
20999
  const body = await readJsonObject(request, 8192);
20921
- await remoteProviders.mutate(async () => {
21000
+ if (body.connect !== void 0 && typeof body.connect !== "boolean") throw new HttpError(400, "bad_request");
21001
+ const configure = async () => {
20922
21002
  await cloudflaredTunnel.configure(mergeSavedCloudflaredTunnelSettings(body, cloudflaredTunnel.settings()));
20923
- if (remoteControllers.cloudflared.status().enabled) await remoteControllers.cloudflared.reconnect();
20924
- });
21003
+ if (body.connect === true && !remoteControllers.cloudflared.status().enabled) await remoteControllers.cloudflared.setEnabled(true);
21004
+ else if (remoteControllers.cloudflared.status().enabled) await remoteControllers.cloudflared.reconnect();
21005
+ };
21006
+ if (body.connect === true) await remoteProviders.mutateSelected("cloudflared", configure);
21007
+ else await remoteProviders.mutate(configure);
20925
21008
  sendJson(response, 200, remotePayload(), false);
20926
21009
  return;
20927
21010
  }
@@ -20969,6 +21052,23 @@ async function apply(ctx, config) {
20969
21052
  sendJson(response, 200, remotePayload(), false);
20970
21053
  return;
20971
21054
  }
21055
+ if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/iroh/component/purge`) {
21056
+ if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
21057
+ logger.info("iroh component purge started");
21058
+ try {
21059
+ await remoteProviders.mutate(async () => {
21060
+ if (remoteControllers.iroh.status().enabled) throw new HttpError(409, "iroh_component_running");
21061
+ await remoteControllers.iroh.setEnabled(false);
21062
+ await irohComponent.purge();
21063
+ });
21064
+ logger.info("iroh component purge completed");
21065
+ } catch (error) {
21066
+ logger.error("iroh component purge failed: %s", error instanceof Error ? error.stack ?? error.message : String(error));
21067
+ throw error;
21068
+ }
21069
+ sendJson(response, 200, remotePayload(), false);
21070
+ return;
21071
+ }
20972
21072
  if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/iroh/pairing/begin`) {
20973
21073
  await readJsonObject(request, 4096);
20974
21074
  logger.info("iroh pairing window requested");
@@ -21134,9 +21234,15 @@ async function apply(ctx, config) {
21134
21234
  return;
21135
21235
  }
21136
21236
  if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/control`) {
21137
- const running = (await readJsonObject(request, 4096)).running;
21237
+ const body = await readJsonObject(request, 4096);
21238
+ const running = body.running;
21138
21239
  if (typeof running !== "boolean") throw new HttpError(400, "bad_request");
21139
- await remoteProviders.mutate(async (controller) => controller.setEnabled(running));
21240
+ if (body.provider !== void 0 && body.provider !== "tailscale" && body.provider !== "cpolar" && body.provider !== "cloudflared" && body.provider !== "frp" && body.provider !== "origin" && body.provider !== "iroh") throw new HttpError(400, "bad_request");
21241
+ const control = async (controller) => {
21242
+ await controller.setEnabled(running);
21243
+ };
21244
+ if (body.provider !== void 0) await remoteProviders.mutateSelected(body.provider, control);
21245
+ else await remoteProviders.mutate(control);
21140
21246
  sendJson(response, 200, remotePayload(), false);
21141
21247
  return;
21142
21248
  }
@@ -21256,6 +21362,7 @@ async function apply(ctx, config) {
21256
21362
  await settleCleanupSteps([
21257
21363
  stopAdmin,
21258
21364
  drainRemoteWork,
21365
+ () => releaseManager.dispose(),
21259
21366
  () => trustedNetworksWrite,
21260
21367
  disposeMobileCommand,
21261
21368
  disposeTaskEvents,
@@ -21276,6 +21383,7 @@ async function apply(ctx, config) {
21276
21383
  await settleCleanupSteps([
21277
21384
  stopAdmin,
21278
21385
  drainRemoteWork,
21386
+ () => releaseManager.dispose(),
21279
21387
  () => trustedNetworksWrite,
21280
21388
  disposeMobileCommand,
21281
21389
  disposeTaskEvents,