dsh-mobile 0.3.12 → 0.3.13

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
@@ -364,6 +364,51 @@ declare class MobileAccessGatewayController {
364
364
  private enqueue;
365
365
  }
366
366
  //#endregion
367
+ //#region src/websocket-paths.d.ts
368
+ /** Cap the admin-approved extra WebSocket upgrade paths (exact pathnames). */
369
+ declare const MAX_EXTRA_WEBSOCKET_PATHS = 16;
370
+ declare const MAX_WEBSOCKET_PATH_LENGTH = 256;
371
+ /**
372
+ * Validate one exact pathname for proxying. Query strings are matched at
373
+ * upgrade time, so only the pathname is stored. Rejects anything the
374
+ * gateway cannot match exactly.
375
+ */
376
+ declare function validateWebSocketPath(value: unknown): string;
377
+ /** Validate a whole replacement list all-or-nothing; duplicates collapse. */
378
+ declare function normalizeWebSocketPaths(value: unknown): string[];
379
+ /** Cap remembered rejected upgrade paths offered for one-click approval. */
380
+ declare const MAX_BLOCKED_UPGRADE_PATHS = 32;
381
+ interface BlockedUpgradePathEntry {
382
+ readonly path: string;
383
+ readonly attempts: number;
384
+ readonly firstSeen: number;
385
+ readonly lastSeen: number;
386
+ }
387
+ /**
388
+ * In-memory log of rejected third-party upgrade paths, shared by every
389
+ * gateway instance so the approval UI sees attempts on any listener.
390
+ * Bounded and newest-first; a restart clears it (attempts reappear on
391
+ * the next blocked handshake).
392
+ */
393
+ declare class BlockedUpgradePathLog {
394
+ private readonly entries;
395
+ record(pathname: string): void;
396
+ report(): BlockedUpgradePathEntry[];
397
+ }
398
+ /** File-backed store shared by every gateway instance (LAN and remote). */
399
+ declare class WebSocketPathStore {
400
+ private readonly file;
401
+ private paths;
402
+ private loaded;
403
+ constructor(file: string);
404
+ /** Snapshot for the upgrade check. */
405
+ has(pathname: string): boolean;
406
+ list(): string[];
407
+ load(): Promise<string[]>;
408
+ /** Replace the whole list after validating; persists atomically. */
409
+ replace(paths: readonly string[]): Promise<string[]>;
410
+ }
411
+ //#endregion
367
412
  //#region src/extensions.d.ts
368
413
  /** Maximum sizes enforced at the local-extension filesystem boundary. */
369
414
  declare const EXTENSION_LIMITS: Readonly<{
@@ -536,6 +581,7 @@ declare class MobileAccessGateway {
536
581
  private readonly extensions?;
537
582
  private readonly upstreamAuthenticatedUrl?;
538
583
  private readonly extraWebSocketPaths?;
584
+ private readonly blockedUpgradeLog?;
539
585
  readonly access: AccessController;
540
586
  private readonly listenerTlsEnabled;
541
587
  private readonly tlsEnabled;
@@ -568,7 +614,7 @@ declare class MobileAccessGateway {
568
614
  private readonly renewLimiter;
569
615
  constructor(config: ResolvedGatewayConfig, store: DeviceStore, extensions?: MobileAccessService | undefined, upstreamAuthenticatedUrl?: string | undefined, extraWebSocketPaths?: {
570
616
  has(pathname: string): boolean;
571
- } | undefined);
617
+ } | undefined, blockedUpgradeLog?: BlockedUpgradePathLog | undefined);
572
618
  /** Initialize durable state, validate TLS, and bind the externally reachable listener. */
573
619
  start(): Promise<void>;
574
620
  private startDiscovery;
@@ -608,6 +654,8 @@ declare class MobileAccessGateway {
608
654
  private pollLegacyCustomChanges;
609
655
  private openExtensionEventStream;
610
656
  private readUpgradeResponse;
657
+ /** Snapshot of rejected upgrade paths for the approval UI (newest first). */
658
+ blockedUpgradePathReport(): BlockedUpgradePathEntry[];
611
659
  private handleUpgrade;
612
660
  /** Loopback-only DSH WebServer route for opening pairing and managing devices. */
613
661
  localAdminRoute(prefix?: string): WebRoute;
@@ -691,32 +739,6 @@ declare class FrpComponentManager {
691
739
  private enqueue;
692
740
  }
693
741
  //#endregion
694
- //#region src/websocket-paths.d.ts
695
- /** Cap the admin-approved extra WebSocket upgrade paths (exact pathnames). */
696
- declare const MAX_EXTRA_WEBSOCKET_PATHS = 16;
697
- declare const MAX_WEBSOCKET_PATH_LENGTH = 256;
698
- /**
699
- * Validate one exact pathname for proxying. Query strings are matched at
700
- * upgrade time, so only the pathname is stored. Rejects anything the
701
- * gateway cannot match exactly.
702
- */
703
- declare function validateWebSocketPath(value: unknown): string;
704
- /** Validate a whole replacement list all-or-nothing; duplicates collapse. */
705
- declare function normalizeWebSocketPaths(value: unknown): string[];
706
- /** File-backed store shared by every gateway instance (LAN and remote). */
707
- declare class WebSocketPathStore {
708
- private readonly file;
709
- private paths;
710
- private loaded;
711
- constructor(file: string);
712
- /** Snapshot for the upgrade check. */
713
- has(pathname: string): boolean;
714
- list(): string[];
715
- load(): Promise<string[]>;
716
- /** Replace the whole list after validating; persists atomically. */
717
- replace(paths: readonly string[]): Promise<string[]>;
718
- }
719
- //#endregion
720
742
  //#region src/frp-template.d.ts
721
743
  /** Loopback-only HTTP vhost port used between Caddy and frps. */
722
744
  declare const FRP_VHOST_HTTP_PORT = 7080;
@@ -907,5 +929,5 @@ declare const inject: string[];
907
929
  /** Mount the resident control route and its optional authenticated LAN gateway. */
908
930
  declare function apply(ctx: Context, config: PluginConfig): Promise<void>;
909
931
  //#endregion
910
- export { AUTH_PREFIX, AccessController, type AccessControllerOptions, AccessError, type AuthoritySpec, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, type DeviceSnapshot, type DeviceStore, type DeviceSummary, type DisabledTlsConfig, EXTENSION_LIMITS, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FrpComponentManager, type FrpComponentStatus, FrpConfigStore, type FrpConfigurationStatus, FrpController, type FrpControllerOptions, type FrpSettings, type FrpState, type FrpStatus, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, type LocalExtensionManifest, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, type MobileAccessControlState, type MobileAccessControlStore, MobileAccessGateway, MobileAccessGatewayController, type MobileAccessService as MobileAccessRegistry, MobileAccessService, type MobileAccessRuntime, type MobileActionContext, type MobileExtensionClientEntry, type MobileExtensionDefinition, MobileExtensionError, type MobileExtensionManifest, type MobileExtensionStatus, type MobileHostAction, type MobileHostRoute, type MobileRouteRequest, type MobileRouteResponse, type PairingResult, type ParsedCidr, type PluginConfig, type ProvidedTlsConfig, type RemoteProvider, type RemoteProviderController, type RemoteProviderState, type RemoteProviderStatus, type RenewalResult, RequestTrustPolicy, type ResolvedGatewayConfig, SESSION_COOKIE, type SessionAuthorization, type StoredDevice, type TlsConfig, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, inject, isGloballyRoutableIpv4, isLoopbackAddress, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseRemoteProviderState, resolveAuthority, rewriteMobileIndex, validateFrpPublicOrigin, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateWebSocketPath };
932
+ export { AUTH_PREFIX, AccessController, type AccessControllerOptions, AccessError, type AuthoritySpec, type BlockedUpgradePathEntry, BlockedUpgradePathLog, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, type DeviceSnapshot, type DeviceStore, type DeviceSummary, type DisabledTlsConfig, EXTENSION_LIMITS, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FrpComponentManager, type FrpComponentStatus, FrpConfigStore, type FrpConfigurationStatus, FrpController, type FrpControllerOptions, type FrpSettings, type FrpState, type FrpStatus, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, type LocalExtensionManifest, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, type MobileAccessControlState, type MobileAccessControlStore, MobileAccessGateway, MobileAccessGatewayController, type MobileAccessService as MobileAccessRegistry, MobileAccessService, type MobileAccessRuntime, type MobileActionContext, type MobileExtensionClientEntry, type MobileExtensionDefinition, MobileExtensionError, type MobileExtensionManifest, type MobileExtensionStatus, type MobileHostAction, type MobileHostRoute, type MobileRouteRequest, type MobileRouteResponse, type PairingResult, type ParsedCidr, type PluginConfig, type ProvidedTlsConfig, type RemoteProvider, type RemoteProviderController, type RemoteProviderState, type RemoteProviderStatus, type RenewalResult, RequestTrustPolicy, type ResolvedGatewayConfig, SESSION_COOKIE, type SessionAuthorization, type StoredDevice, type TlsConfig, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, inject, isGloballyRoutableIpv4, isLoopbackAddress, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseRemoteProviderState, resolveAuthority, rewriteMobileIndex, validateFrpPublicOrigin, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateWebSocketPath };
911
933
  //# sourceMappingURL=index.d.mts.map
package/lib/index.mjs CHANGED
@@ -2694,6 +2694,7 @@ var MobileAccessGateway = class {
2694
2694
  extensions;
2695
2695
  upstreamAuthenticatedUrl;
2696
2696
  extraWebSocketPaths;
2697
+ blockedUpgradeLog;
2697
2698
  access;
2698
2699
  listenerTlsEnabled;
2699
2700
  tlsEnabled;
@@ -2724,11 +2725,12 @@ var MobileAccessGateway = class {
2724
2725
  removeSessionListener;
2725
2726
  removeExtensionContentListener;
2726
2727
  renewLimiter;
2727
- constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths) {
2728
+ constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog) {
2728
2729
  this.config = config;
2729
2730
  this.extensions = extensions;
2730
2731
  this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
2731
2732
  this.extraWebSocketPaths = extraWebSocketPaths;
2733
+ this.blockedUpgradeLog = blockedUpgradeLog;
2732
2734
  this.listenerTlsEnabled = config.tls.mode === "provided";
2733
2735
  this.tlsEnabled = config.publicTls;
2734
2736
  this.access = new AccessController(store, {
@@ -3912,12 +3914,19 @@ var MobileAccessGateway = class {
3912
3914
  upstream.once("close", closed);
3913
3915
  });
3914
3916
  }
3917
+ /** Snapshot of rejected upgrade paths for the approval UI (newest first). */
3918
+ blockedUpgradePathReport() {
3919
+ return this.blockedUpgradeLog?.report() ?? [];
3920
+ }
3915
3921
  async handleUpgrade(request, client, head) {
3916
3922
  const target = parseRequestTarget(request.url);
3917
3923
  const policy = this.requirePolicy();
3918
3924
  assertExternalTrust(request, policy, false);
3919
3925
  if (!policy.acceptsOrigin(request.headers.origin)) throw new HttpError(403, "forbidden");
3920
- if (!(WS_PATHS.has(target.decodedPathname) || (this.extraWebSocketPaths?.has(target.decodedPathname) ?? false))) throw new HttpError(404, "not_found");
3926
+ if (!(WS_PATHS.has(target.decodedPathname) || (this.extraWebSocketPaths?.has(target.decodedPathname) ?? false))) {
3927
+ this.blockedUpgradeLog?.record(target.decodedPathname);
3928
+ throw new HttpError(404, "not_found");
3929
+ }
3921
3930
  if (request.method !== "GET" || headerValue(request.headers, "upgrade")?.toLowerCase() !== "websocket" || !hasToken(headerValue(request.headers, "connection"), "upgrade")) throw new HttpError(400, "bad_request");
3922
3931
  const key = headerValue(request.headers, "sec-websocket-key");
3923
3932
  if (key === void 0 || headerValue(request.headers, "sec-websocket-version") !== "13") throw new HttpError(400, "bad_request");
@@ -4632,6 +4641,42 @@ function normalizeWebSocketPaths(value) {
4632
4641
  }
4633
4642
  return [...seen];
4634
4643
  }
4644
+ /** Cap remembered rejected upgrade paths offered for one-click approval. */
4645
+ const MAX_BLOCKED_UPGRADE_PATHS = 32;
4646
+ /**
4647
+ * In-memory log of rejected third-party upgrade paths, shared by every
4648
+ * gateway instance so the approval UI sees attempts on any listener.
4649
+ * Bounded and newest-first; a restart clears it (attempts reappear on
4650
+ * the next blocked handshake).
4651
+ */
4652
+ var BlockedUpgradePathLog = class {
4653
+ entries = /* @__PURE__ */ new Map();
4654
+ record(pathname) {
4655
+ const now = Date.now();
4656
+ const stats = this.entries.get(pathname);
4657
+ if (stats === void 0) {
4658
+ if (this.entries.size >= 32) {
4659
+ let oldest;
4660
+ for (const [path, entry] of this.entries) if (oldest === void 0 || entry.lastSeen < (this.entries.get(oldest)?.lastSeen ?? 0)) oldest = path;
4661
+ if (oldest !== void 0) this.entries.delete(oldest);
4662
+ }
4663
+ this.entries.set(pathname, {
4664
+ attempts: 1,
4665
+ firstSeen: now,
4666
+ lastSeen: now
4667
+ });
4668
+ return;
4669
+ }
4670
+ stats.attempts += 1;
4671
+ stats.lastSeen = now;
4672
+ }
4673
+ report() {
4674
+ return [...this.entries].map(([path, stats]) => ({
4675
+ path,
4676
+ ...stats
4677
+ })).sort((a, b) => b.lastSeen - a.lastSeen);
4678
+ }
4679
+ };
4635
4680
  /** File-backed store shared by every gateway instance (LAN and remote). */
4636
4681
  var WebSocketPathStore = class {
4637
4682
  file;
@@ -8735,10 +8780,11 @@ async function apply(ctx, config) {
8735
8780
  });
8736
8781
  const webSocketPaths = new WebSocketPathStore(join(stateDirectory, "websocket-paths.json"));
8737
8782
  await webSocketPaths.load();
8783
+ const blockedUpgradePaths = new BlockedUpgradePathLog();
8738
8784
  let lanGateway;
8739
8785
  const startGateway = async (candidateConfig) => {
8740
8786
  const resolved = parseGatewayConfig(candidateConfig);
8741
- const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths);
8787
+ const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths);
8742
8788
  await candidate.start();
8743
8789
  lanGateway = candidate;
8744
8790
  return { close: async () => {
@@ -8778,7 +8824,7 @@ async function apply(ctx, config) {
8778
8824
  }
8779
8825
  const createRemoteGateway = async (publicOrigin, listenPort = 0) => {
8780
8826
  const resolved = remoteGatewayConfig(template, publicOrigin, remoteDeviceFile, instanceId, listenPort);
8781
- const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths);
8827
+ const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths);
8782
8828
  await candidate.start();
8783
8829
  return candidate;
8784
8830
  };
@@ -8944,6 +8990,10 @@ async function apply(ctx, config) {
8944
8990
  sendJson(response, 200, { paths: webSocketPaths.list() }, false);
8945
8991
  return;
8946
8992
  }
8993
+ if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/remote/websocket-paths/blocked`) {
8994
+ sendJson(response, 200, { blocked: blockedUpgradePaths.report() }, false);
8995
+ return;
8996
+ }
8947
8997
  if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/websocket-paths`) {
8948
8998
  const body = await readJsonObject(request, 4096);
8949
8999
  if (!Array.isArray(body.paths)) throw new HttpError(400, "bad_request");
@@ -9177,6 +9227,6 @@ async function apply(ctx, config) {
9177
9227
  }, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
9178
9228
  }
9179
9229
  //#endregion
9180
- export { AUTH_PREFIX, AccessController, AccessError, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, RequestTrustPolicy, SESSION_COOKIE, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, inject, isGloballyRoutableIpv4, isLoopbackAddress, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseRemoteProviderState, resolveAuthority, rewriteMobileIndex, validateFrpPublicOrigin, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateWebSocketPath };
9230
+ export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, RequestTrustPolicy, SESSION_COOKIE, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, inject, isGloballyRoutableIpv4, isLoopbackAddress, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseRemoteProviderState, resolveAuthority, rewriteMobileIndex, validateFrpPublicOrigin, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateWebSocketPath };
9181
9231
 
9182
9232
  //# sourceMappingURL=index.mjs.map