dsh-mobile 0.5.0 → 0.5.2

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
@@ -356,6 +356,8 @@ declare class JsonMobileAccessControlStore implements MobileAccessControlStore {
356
356
  private readonly file;
357
357
  private readonly initiallyEnabled;
358
358
  constructor(file: string, initiallyEnabled: boolean);
359
+ /** Avoid writing an absent already-disabled state without changing recovery of existing files. */
360
+ disableIfPresent(): Promise<void>;
359
361
  load(): Promise<MobileAccessControlState>;
360
362
  save(state: MobileAccessControlState): Promise<void>;
361
363
  }
@@ -919,6 +921,8 @@ interface FrpConfigurationStatus {
919
921
  readonly publicOrigin?: string;
920
922
  readonly vhostHttpPort: number;
921
923
  readonly storagePath: string;
924
+ /** Proxy name frpc registers on the shared frps; unique per DSH installation. */
925
+ readonly proxyName: string;
922
926
  readonly errorCode?: string;
923
927
  /** Present only when the saved configuration leaves the upstream `deploy` default. */
924
928
  readonly mode?: FrpMode;
@@ -976,14 +980,46 @@ declare function mergeSavedFrpTarget(partial: Readonly<Record<string, unknown>>,
976
980
  readonly serverAddress: string;
977
981
  readonly serverPort: number;
978
982
  };
983
+ /**
984
+ * Legacy proxy name, kept only as the default of {@link createFrpcToml}.
985
+ *
986
+ * A shared frps accepts one proxy per name across *all* its clients, so a fixed
987
+ * name let any other DSH installation — or a not yet reaped session of this one —
988
+ * own `dsh-mobile` first. frpc then logged `start error: proxy name [dsh-mobile]
989
+ * is already in use`, kept running without publishing the entry port, and the
990
+ * start-up self-check ended in `frp_start_timeout` ("the public endpoint did not
991
+ * become ready"). Product callers therefore pass an installation-derived name
992
+ * from {@link frpProxyName}; this constant only keeps the exported API and the
993
+ * byte-for-byte compatibility test working for legacy callers.
994
+ */
995
+ declare const FRP_DEFAULT_PROXY_NAME = "dsh-mobile";
996
+ /**
997
+ * Validate a proxy name for the generated frpc configuration.
998
+ *
999
+ * frps accepts one proxy per name on the whole server, so the name must be a
1000
+ * single safe token: no whitespace, no quotes, no path or bracket characters.
1001
+ */
1002
+ declare function validateFrpProxyName(value: unknown): string;
1003
+ /**
1004
+ * Proxy name for one DSH installation.
1005
+ *
1006
+ * Derived from the stable installation identity so two computers sharing one
1007
+ * frps never register the same proxy, while a single installation keeps the
1008
+ * same name across restarts (so its own stale registration is the only possible
1009
+ * conflict, and that one expires with the frps heartbeat).
1010
+ */
1011
+ declare function frpProxyName(instanceId: string): string;
979
1012
  /**
980
1013
  * Build the single-purpose frpc configuration for the current loopback gateway.
981
1014
  *
982
1015
  * The default (public-CA) entry is an HTTP vhost behind Caddy and is emitted byte
983
1016
  * for byte as before. The self-signed entry cannot use a vhost at all: frps only
984
1017
  * forwards raw TCP, and the gateway terminates TLS on that connection.
1018
+ *
1019
+ * `proxyName` defaults to the legacy fixed name only for legacy callers; the
1020
+ * provider and the attach preview always pass {@link frpProxyName}.
985
1021
  */
986
- declare function createFrpcToml(settings: FrpSettings, localPort: number): string;
1022
+ declare function createFrpcToml(settings: FrpSettings, localPort: number, proxyName?: string): string;
987
1023
  /** Build the matching restricted frps and Caddy templates for one VPS. */
988
1024
  declare function createFrpServerTemplate(settings: FrpSettings): string;
989
1025
  /** Owns private FRP settings and generation-specific frpc configuration. */
@@ -991,9 +1027,17 @@ declare class FrpConfigStore {
991
1027
  readonly stateRoot: string;
992
1028
  readonly settingsFile: string;
993
1029
  readonly runtimeConfigFile: string;
1030
+ /**
1031
+ * Proxy name every generated frpc.toml registers.
1032
+ *
1033
+ * Injected by the composing plugin ({@link frpProxyName}) so one frps can
1034
+ * serve several DSH installations; the legacy fixed name stays the default
1035
+ * for direct constructions in tests and for the exported API.
1036
+ */
1037
+ readonly proxyName: string;
994
1038
  private settingsValue;
995
1039
  private errorCode;
996
- constructor(stateDirectory: string);
1040
+ constructor(stateDirectory: string, proxyName?: string);
997
1041
  /** Load private settings while rejecting links, oversized files, and unknown fields. */
998
1042
  initialize(): Promise<void>;
999
1043
  /**
@@ -1030,6 +1074,14 @@ interface FrpAttachOptions {
1030
1074
  readonly certDir?: string;
1031
1075
  /** Absolute path of the plugin-written frpc.toml, used for the `frpc verify` self-check. */
1032
1076
  readonly configFile?: string;
1077
+ /**
1078
+ * Proxy name the previewed frpc.toml registers.
1079
+ *
1080
+ * Defaults to the legacy fixed name; the panel and the provider pass the
1081
+ * installation-derived {@link frpProxyName} so a preview never describes a
1082
+ * registration that would collide on a shared frps.
1083
+ */
1084
+ readonly proxyName?: string;
1033
1085
  /** Loopback port the gateway will listen on; only a placeholder inside the copied runbook. */
1034
1086
  readonly localPort?: number;
1035
1087
  /**
@@ -1819,5 +1871,5 @@ declare function originGatewayConfig(template: ResolvedGatewayConfig, settings:
1819
1871
  /** Mount the resident control route and its optional authenticated LAN gateway. */
1820
1872
  declare function apply(ctx: Context, config: PluginConfig): Promise<void>;
1821
1873
  //#endregion
1822
- export { AUTH_PREFIX, AccessController, type AccessControllerOptions, AccessError, type AuthoritySpec, type BlockedUpgradePathEntry, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CSRF_COOKIE, CSRF_HEADER, type CaddySiteOptions, type CertRenewalStatus, CloudflaredComponentManager, type CloudflaredComponentStatus, CloudflaredController, type CloudflaredControllerOptions, type CloudflaredState, type CloudflaredStatus, type CloudflaredTunnelMode, type CloudflaredTunnelSettings, type CloudflaredTunnelStatus, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, type DeviceProbeResult, type DeviceSnapshot, type DeviceStore, type DeviceSummary, type DisabledTlsConfig, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, type FrpAttachOptions, type FrpAttachPlan, type FrpAttachPlanLocal, type FrpAttachPlanStep, type FrpAttachStepId, type FrpAttachTemplate, type FrpAttachVpsParts, FrpComponentManager, type FrpComponentStatus, FrpConfigStore, type FrpConfigurationStatus, FrpController, type FrpControllerOptions, type FrpEntryTls, type FrpIngressCertificate, type FrpIngressPaths, type FrpIngressSelfCheck, 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, OriginConfigStore, type OriginConfigurationStatus, OriginController, type OriginControllerOptions, type OriginSettings, type OriginState, type OriginStatus, type PairingResult, type ParsedCidr, type PluginConfig, type ProvidedTlsConfig, REMOTE_PROVIDERS, type RemoteProvider, type RemoteProviderController, type RemoteProviderState, type RemoteProviderStatus, type RenewalResult, RequestTrustPolicy, type ResolvedGatewayConfig, SESSION_COOKIE, type ServerCertificateAuthority, type ServerCertificateFiles, type ServerCertificateTarget, type SessionAuthorization, type SessionEndReason, type StoredDevice, TASK_EVENT_DEBOUNCE_MS, type TaskCompletionEvent, type TaskEventContext, TaskEventHub, type TaskEventSession, type TaskEventSink, type TaskEventWatcherOptions, type TaskTurnEvent, type TlsConfig, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
1874
+ export { AUTH_PREFIX, AccessController, type AccessControllerOptions, AccessError, type AuthoritySpec, type BlockedUpgradePathEntry, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CSRF_COOKIE, CSRF_HEADER, type CaddySiteOptions, type CertRenewalStatus, CloudflaredComponentManager, type CloudflaredComponentStatus, CloudflaredController, type CloudflaredControllerOptions, type CloudflaredState, type CloudflaredStatus, type CloudflaredTunnelMode, type CloudflaredTunnelSettings, type CloudflaredTunnelStatus, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, type DeviceProbeResult, type DeviceSnapshot, type DeviceStore, type DeviceSummary, type DisabledTlsConfig, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, type FrpAttachOptions, type FrpAttachPlan, type FrpAttachPlanLocal, type FrpAttachPlanStep, type FrpAttachStepId, type FrpAttachTemplate, type FrpAttachVpsParts, FrpComponentManager, type FrpComponentStatus, FrpConfigStore, type FrpConfigurationStatus, FrpController, type FrpControllerOptions, type FrpEntryTls, type FrpIngressCertificate, type FrpIngressPaths, type FrpIngressSelfCheck, 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, OriginConfigStore, type OriginConfigurationStatus, OriginController, type OriginControllerOptions, type OriginSettings, type OriginState, type OriginStatus, type PairingResult, type ParsedCidr, type PluginConfig, type ProvidedTlsConfig, REMOTE_PROVIDERS, type RemoteProvider, type RemoteProviderController, type RemoteProviderState, type RemoteProviderStatus, type RenewalResult, RequestTrustPolicy, type ResolvedGatewayConfig, SESSION_COOKIE, type ServerCertificateAuthority, type ServerCertificateFiles, type ServerCertificateTarget, type SessionAuthorization, type SessionEndReason, type StoredDevice, TASK_EVENT_DEBOUNCE_MS, type TaskCompletionEvent, type TaskEventContext, TaskEventHub, type TaskEventSession, type TaskEventSink, type TaskEventWatcherOptions, type TaskTurnEvent, type TlsConfig, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
1823
1875
  //# sourceMappingURL=index.d.mts.map
package/lib/index.mjs CHANGED
@@ -1078,6 +1078,25 @@ var JsonMobileAccessControlStore = class {
1078
1078
  this.file = file;
1079
1079
  this.initiallyEnabled = initiallyEnabled;
1080
1080
  }
1081
+ /** Avoid writing an absent already-disabled state without changing recovery of existing files. */
1082
+ async disableIfPresent() {
1083
+ try {
1084
+ await lstat(this.file);
1085
+ } catch (error) {
1086
+ if (error.code === "ENOENT") {
1087
+ if (this.initiallyEnabled) await this.save({
1088
+ version: 1,
1089
+ enabled: false
1090
+ });
1091
+ return;
1092
+ }
1093
+ throw error;
1094
+ }
1095
+ await this.save({
1096
+ version: 1,
1097
+ enabled: false
1098
+ });
1099
+ }
1081
1100
  async load() {
1082
1101
  let stat;
1083
1102
  try {
@@ -6101,13 +6120,51 @@ function tomlString(value) {
6101
6120
  return JSON.stringify(value);
6102
6121
  }
6103
6122
  /**
6123
+ * Legacy proxy name, kept only as the default of {@link createFrpcToml}.
6124
+ *
6125
+ * A shared frps accepts one proxy per name across *all* its clients, so a fixed
6126
+ * name let any other DSH installation — or a not yet reaped session of this one —
6127
+ * own `dsh-mobile` first. frpc then logged `start error: proxy name [dsh-mobile]
6128
+ * is already in use`, kept running without publishing the entry port, and the
6129
+ * start-up self-check ended in `frp_start_timeout` ("the public endpoint did not
6130
+ * become ready"). Product callers therefore pass an installation-derived name
6131
+ * from {@link frpProxyName}; this constant only keeps the exported API and the
6132
+ * byte-for-byte compatibility test working for legacy callers.
6133
+ */
6134
+ const FRP_DEFAULT_PROXY_NAME = "dsh-mobile";
6135
+ /**
6136
+ * Validate a proxy name for the generated frpc configuration.
6137
+ *
6138
+ * frps accepts one proxy per name on the whole server, so the name must be a
6139
+ * single safe token: no whitespace, no quotes, no path or bracket characters.
6140
+ */
6141
+ function validateFrpProxyName(value) {
6142
+ if (typeof value !== "string" || value.length < 1 || value.length > 128 || !/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("frp_proxy_name_invalid");
6143
+ return value;
6144
+ }
6145
+ /**
6146
+ * Proxy name for one DSH installation.
6147
+ *
6148
+ * Derived from the stable installation identity so two computers sharing one
6149
+ * frps never register the same proxy, while a single installation keeps the
6150
+ * same name across restarts (so its own stale registration is the only possible
6151
+ * conflict, and that one expires with the frps heartbeat).
6152
+ */
6153
+ function frpProxyName(instanceId) {
6154
+ if (typeof instanceId !== "string" || !/^[a-f0-9]{64}$/u.test(instanceId)) throw new Error("frp_instance_id_invalid");
6155
+ return `${FRP_DEFAULT_PROXY_NAME}-${instanceId.slice(0, 12)}`;
6156
+ }
6157
+ /**
6104
6158
  * Build the single-purpose frpc configuration for the current loopback gateway.
6105
6159
  *
6106
6160
  * The default (public-CA) entry is an HTTP vhost behind Caddy and is emitted byte
6107
6161
  * for byte as before. The self-signed entry cannot use a vhost at all: frps only
6108
6162
  * forwards raw TCP, and the gateway terminates TLS on that connection.
6163
+ *
6164
+ * `proxyName` defaults to the legacy fixed name only for legacy callers; the
6165
+ * provider and the attach preview always pass {@link frpProxyName}.
6109
6166
  */
6110
- function createFrpcToml(settings, localPort) {
6167
+ function createFrpcToml(settings, localPort, proxyName = FRP_DEFAULT_PROXY_NAME) {
6111
6168
  if (!Number.isSafeInteger(localPort) || localPort < 1 || localPort > 65535) throw new Error("frp_local_port_invalid");
6112
6169
  const header = [
6113
6170
  `serverAddr = ${tomlString(settings.serverAddress)}`,
@@ -6117,7 +6174,7 @@ function createFrpcToml(settings, localPort) {
6117
6174
  "transport.tls.enable = true",
6118
6175
  "",
6119
6176
  "[[proxies]]",
6120
- "name = \"dsh-mobile\""
6177
+ `name = ${tomlString(validateFrpProxyName(proxyName))}`
6121
6178
  ];
6122
6179
  if (isFrpSelfSignedIngress(settings)) return [
6123
6180
  ...header,
@@ -6179,13 +6236,22 @@ var FrpConfigStore = class {
6179
6236
  stateRoot;
6180
6237
  settingsFile;
6181
6238
  runtimeConfigFile;
6239
+ /**
6240
+ * Proxy name every generated frpc.toml registers.
6241
+ *
6242
+ * Injected by the composing plugin ({@link frpProxyName}) so one frps can
6243
+ * serve several DSH installations; the legacy fixed name stays the default
6244
+ * for direct constructions in tests and for the exported API.
6245
+ */
6246
+ proxyName;
6182
6247
  settingsValue;
6183
6248
  errorCode;
6184
- constructor(stateDirectory) {
6249
+ constructor(stateDirectory, proxyName = FRP_DEFAULT_PROXY_NAME) {
6185
6250
  if (!isAbsolute(stateDirectory)) throw new Error("frp config state directory must be absolute");
6186
6251
  this.stateRoot = resolve(stateDirectory);
6187
6252
  this.settingsFile = join(this.stateRoot, "settings.json");
6188
6253
  this.runtimeConfigFile = join(this.stateRoot, "frpc.toml");
6254
+ this.proxyName = validateFrpProxyName(proxyName);
6189
6255
  }
6190
6256
  /** Load private settings while rejecting links, oversized files, and unknown fields. */
6191
6257
  async initialize() {
@@ -6229,6 +6295,7 @@ var FrpConfigStore = class {
6229
6295
  },
6230
6296
  vhostHttpPort: settings === void 0 ? FRP_VHOST_HTTP_PORT : resolveFrpVhostHttpPort(settings),
6231
6297
  storagePath: this.stateRoot,
6298
+ proxyName: this.proxyName,
6232
6299
  ...this.errorCode === void 0 ? {} : { errorCode: this.errorCode },
6233
6300
  ...mode === void 0 || mode === "deploy" ? {} : { mode },
6234
6301
  ...entryTls === void 0 || entryTls === "public-ip-cert" ? {} : { entryTls },
@@ -6252,7 +6319,7 @@ var FrpConfigStore = class {
6252
6319
  async writeRuntimeConfig(localPort) {
6253
6320
  const settings = this.settingsValue;
6254
6321
  if (settings === void 0) throw new Error("frp_config_missing");
6255
- await atomicPrivateWrite$2(this.runtimeConfigFile, createFrpcToml(settings, localPort));
6322
+ await atomicPrivateWrite$2(this.runtimeConfigFile, createFrpcToml(settings, localPort, this.proxyName));
6256
6323
  return this.runtimeConfigFile;
6257
6324
  }
6258
6325
  /** Remove only configuration files owned by the FRP provider. */
@@ -6311,7 +6378,7 @@ function redactFrpcTomlToken(frpcToml) {
6311
6378
  * the provider writes stays byte-for-byte identical to the upstream output.
6312
6379
  */
6313
6380
  function createFrpAttachFrpcToml(settings, options = {}) {
6314
- const frpcToml = createFrpcToml(settings, attachLocalPort(options));
6381
+ const frpcToml = createFrpcToml(settings, attachLocalPort(options), options.proxyName ?? "dsh-mobile");
6315
6382
  return options.revealToken === true ? frpcToml : redactFrpcTomlToken(frpcToml);
6316
6383
  }
6317
6384
  const DEFAULT_ATTACH_CONFIG_FILE = "<DSH Mobile 私有目录>/remote/frp/config/frpc.toml";
@@ -7503,6 +7570,30 @@ const DISCOVERY_RETRY_MS = 1e3;
7503
7570
  const MAX_DISCOVERY_BYTES = 16384;
7504
7571
  const VHOST_PROBE_TIMEOUT_MS = 1500;
7505
7572
  const INGRESS_CERT_CHECK_MS = 432e5;
7573
+ /**
7574
+ * frpc start-failure signatures, most specific first.
7575
+ *
7576
+ * frpc reports a refused proxy registration as a warning and keeps running, so
7577
+ * without this mapping the controller only ever saw a healthy child process and
7578
+ * an entry port nobody published: the start-up self-check burned its full 45s
7579
+ * deadline and reported `frp_start_timeout` ("the public endpoint did not become
7580
+ * ready"), which sent users looking at DNS, Caddy, and firewalls.
7581
+ *
7582
+ * The two real conflicts are a proxy name another client of the same frps holds
7583
+ * and an entry port another proxy already owns. Both are reported directly.
7584
+ */
7585
+ const CLIENT_FAILURE_CODES = Object.freeze([
7586
+ [/proxy name \[[^\]]*\] is already in use/iu, "frp_proxy_name_in_use"],
7587
+ [/(?:remote )?port (?:\[?[0-9]+\]? )?already (?:used|in use)/iu, "frp_remote_port_in_use"],
7588
+ [/start error:/iu, "frp_proxy_start_failed"]
7589
+ ]);
7590
+ /**
7591
+ * Stable error code for a chunk of frpc output, or undefined when it reports no
7592
+ * start failure. Exported for the conflict tests.
7593
+ */
7594
+ function frpcDiagnosisCode(output) {
7595
+ return CLIENT_FAILURE_CODES.find(([pattern]) => pattern.test(output))?.[1];
7596
+ }
7506
7597
  function publicStatus$3(status) {
7507
7598
  return Object.freeze({
7508
7599
  enabled: status.enabled,
@@ -7904,15 +7995,37 @@ var FrpController = class {
7904
7995
  return;
7905
7996
  }
7906
7997
  this.child = child;
7998
+ const pending = {
7999
+ stdout: "",
8000
+ stderr: ""
8001
+ };
8002
+ let diagnosed = false;
8003
+ const inspectLine = (line) => {
8004
+ if (diagnosed) return;
8005
+ const code = frpcDiagnosisCode(line);
8006
+ if (code === void 0) return;
8007
+ diagnosed = true;
8008
+ this.enqueue(() => this.failGeneration(generation, code));
8009
+ };
8010
+ const inspect = (stream, chunk) => {
8011
+ if (diagnosed) return;
8012
+ const lines = (pending[stream] + String(chunk)).split(/\r\n|\r|\n/u);
8013
+ pending[stream] = (lines.pop() ?? "").slice(-8192);
8014
+ for (const line of lines) inspectLine(line.slice(-8192));
8015
+ };
8016
+ child.stdout.on("data", (chunk) => inspect("stdout", chunk));
8017
+ child.stderr.on("data", (chunk) => inspect("stderr", chunk));
7907
8018
  child.stdout.resume();
7908
8019
  child.stderr.resume();
7909
8020
  child.once("error", () => {
7910
8021
  this.enqueue(() => this.failGeneration(generation, "frp_launch_failed"));
7911
8022
  });
7912
8023
  child.once("close", (code) => {
8024
+ inspectLine(pending.stdout);
8025
+ inspectLine(pending.stderr);
7913
8026
  if (generation !== this.generation || this.child !== child) return;
7914
8027
  this.child = void 0;
7915
- if (this.enabled) this.enqueue(() => this.failGeneration(generation, code === 0 ? "frp_stopped" : "frp_exited"));
8028
+ if (this.enabled && !diagnosed) this.enqueue(() => this.failGeneration(generation, code === 0 ? "frp_stopped" : "frp_exited"));
7916
8029
  });
7917
8030
  this.publish({
7918
8031
  enabled: true,
@@ -7928,6 +8041,7 @@ var FrpController = class {
7928
8041
  return;
7929
8042
  }
7930
8043
  if (generation !== this.generation || !this.enabled) return;
8044
+ if (diagnosed) return;
7931
8045
  const controller = new AbortController();
7932
8046
  this.startupAbort = controller;
7933
8047
  this.waitForDiscovery(generation, check, gateway.config.instanceId, settings, gateway, controller.signal);
@@ -9552,6 +9666,9 @@ const REMOTE_ERROR_GUIDANCE = Object.freeze({
9552
9666
  frp_vhost_probe_failed: "确认 VPS 地址可解析后重新连接。",
9553
9667
  frp_launch_failed: "重新安装 FRP 官方组件后重试。",
9554
9668
  frp_start_timeout: "确认 frps、Caddy 和域名解析正常后重新连接。",
9669
+ frp_proxy_name_in_use: "该 frps 上已有同名代理:在另一台 DSH 电脑上关闭自建 FRP,或在 frps 上清掉该代理后重新连接。",
9670
+ frp_remote_port_in_use: "公网入口端口被其他代理占用:改用其他入口端口后重新连接。",
9671
+ frp_proxy_start_failed: "查看 frps 日志与 allowPorts,确认它能接受该代理名和入口端口。",
9555
9672
  frp_discovery_mismatch: "公开域名连接到了另一台电脑,请核对 Caddy 与 frps 配置。",
9556
9673
  frp_discovery_invalid: "公开域名返回了非 DSH Mobile 响应。",
9557
9674
  frp_stopped: "点击“重新连接”。",
@@ -9977,7 +10094,7 @@ function buildMobileGuide(state) {
9977
10094
  const scriptLine = state.hasCustomJs ? "存在(当前生效的自定义脚本)" : "不存在(无自定义脚本)";
9978
10095
  const extensionLines = state.extensions.length === 0 ? "(无)" : state.extensions.map((entry) => `- ${entry.id}(${entry.name} v${entry.version})`).join("\n");
9979
10096
  const failureLine = state.failedExtensionCount > 0 ? `注意:${state.failedExtensionCount} 个扩展的电脑端 host 激活失败,如改动相关扩展请先检查其 host.mjs 与 extension.json。` : "";
9980
- return `${`## 手机端当前状态(改名前必读,避免覆盖已有定制)
10097
+ return `${`## 手机端当前状态(改动前必读,避免覆盖已有定制)
9981
10098
 
9982
10099
  - 定制目录:${state.directory}(所有改动只允许在这里进行)
9983
10100
  - mobile.css:${styleLine}
@@ -10003,6 +10120,8 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
10003
10120
  - $DSH_HOME/mobile-access/mobile.js:手机端脚本,用 window.dshMobile.register(({ root }) => { ... }) 把内容挂载到 root,返回清理函数
10004
10121
  - 保存后手机端几秒内自动应用,无需重启
10005
10122
 
10123
+ 动手前先读已有的定制文件。若要调整 DSH 自带的输入框、侧栏或设置等现有界面,先确认当前 DSH 版本与手机页面模式(专用移动页面或 ?frontend=stock),只读核对运行页面的 DOM、属性和样式;本机有同版本前端源码时可参考相应组件,没有源码时可查看已安装的客户端产物,不必下载源码。优先使用扩展 API、插槽和稳定的 data-* 标记,不凭猜测依赖构建类名。只读查看不改变前述写入范围。
10124
+
10006
10125
  2. 电脑端能力 —— 手机需要读电脑文件、执行命令或访问硬件时,创建扩展:
10007
10126
  - 目录:$DSH_HOME/mobile-access/extensions/<id>/,id 用小写字母数字和连字符(如 media-remote)
10008
10127
  - extension.json:{"schemaVersion":1,"id":"<id>","name":"显示名","version":"0.1.0","description":"说明"}
@@ -10019,6 +10138,7 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
10019
10138
 
10020
10139
  完成前请自检:
10021
10140
  - 改动涉及 mobile.js 或扩展的 mobile.js / host.mjs 时,先做语法检查再保存(如 node --check <file>),确保没有语法错误
10141
+ - 改动 DSH 现有界面后尽可能在实际手机页面核对效果;无法观察运行页面时,明确告诉用户尚未实测
10022
10142
  - 创建或修改扩展后,确认 extension.json 的 schemaVersion 为 1、id 与目录名一致、且 id 只含小写字母数字和连字符
10023
10143
  - 扩展的 host.mjs 若在完成前无法激活,先修正而不是留下损坏的扩展
10024
10144
  - 完成后检查自己实际写入了哪些文件,向用户简要说明改了什么、手机端会有什么变化`;
@@ -12647,6 +12767,36 @@ async function installMobileFileLogger(ctx, stateDirectory) {
12647
12767
  return file;
12648
12768
  }
12649
12769
  //#endregion
12770
+ //#region src/installation-id.ts
12771
+ const INSTALLATION_ID_PATTERN = /^[a-f0-9]{64}\n$/u;
12772
+ const INSTALLATION_ID_BYTES = 65;
12773
+ async function readInstallationId(file) {
12774
+ const entry = await lstat(file);
12775
+ if (!entry.isFile() || entry.isSymbolicLink() || entry.size !== INSTALLATION_ID_BYTES) throw new Error("installation_id_invalid");
12776
+ const contents = await readFile(file, "utf8");
12777
+ if (!INSTALLATION_ID_PATTERN.test(contents)) throw new Error("installation_id_invalid");
12778
+ await restrictPrivateFile(file);
12779
+ return contents.slice(0, -1);
12780
+ }
12781
+ /** Keep one random plugin identity across setup changes, restarts, and FRP resets. */
12782
+ async function loadOrCreateInstallationId(file) {
12783
+ if (!isAbsolute(file)) throw new Error("installation ID path must be absolute");
12784
+ const target = resolve(file);
12785
+ await mkdir(dirname(target), {
12786
+ recursive: true,
12787
+ mode: 448
12788
+ });
12789
+ try {
12790
+ await writeFile(target, `${randomBytes(32).toString("hex")}\n`, {
12791
+ flag: "wx",
12792
+ mode: 384
12793
+ });
12794
+ } catch (error) {
12795
+ if (error.code !== "EEXIST") throw error;
12796
+ }
12797
+ return readInstallationId(target);
12798
+ }
12799
+ //#endregion
12650
12800
  //#region src/lan-setup.ts
12651
12801
  const FIREWALL_TCP_RULE = "DSH Mobile HTTPS";
12652
12802
  const FIREWALL_UDP_RULE = "DSH Mobile Discovery";
@@ -13044,7 +13194,8 @@ async function apply(ctx, config) {
13044
13194
  await cloudflaredComponent.initialize();
13045
13195
  const frpComponent = new FrpComponentManager({ stateDirectory });
13046
13196
  await frpComponent.initialize();
13047
- const frpConfig = new FrpConfigStore(join(remoteDirectory, "frp", "config"));
13197
+ const frpIdentity = await loadOrCreateInstallationId(join(stateDirectory, "installation-id"));
13198
+ const frpConfig = new FrpConfigStore(join(remoteDirectory, "frp", "config"), frpProxyName(frpIdentity));
13048
13199
  await frpConfig.initialize();
13049
13200
  const originConfig = new OriginConfigStore(join(remoteDirectory, "origin", "config"));
13050
13201
  await originConfig.initialize();
@@ -13522,7 +13673,10 @@ async function apply(ctx, config) {
13522
13673
  }
13523
13674
  if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/frp/attach-plan`) {
13524
13675
  const settings = mergeSavedFrpSettings(await readJsonObject(request, 8192), frpConfig.settings());
13525
- const options = { configFile: frpConfig.runtimeConfigFile };
13676
+ const options = {
13677
+ configFile: frpConfig.runtimeConfigFile,
13678
+ proxyName: frpConfig.proxyName
13679
+ };
13526
13680
  logger.info("frp attach plan requested mode=%s entryTls=%s vhostHttpPort=%d", settings.mode ?? "deploy", settings.entryTls ?? "public-ip-cert", resolveFrpVhostHttpPort(settings));
13527
13681
  sendJson(response, 200, {
13528
13682
  ...remotePayload(),
@@ -13705,7 +13859,7 @@ async function apply(ctx, config) {
13705
13859
  const unregister = ctx.webServer.register(adminRoute);
13706
13860
  const disposeMobileCommand = ctx.commands.register({
13707
13861
  name: "mobile",
13708
- description: "按需求修改 DSH Mobile 的手机端界面或添加电脑端能力",
13862
+ description: "定制手机界面或添加电脑能力",
13709
13863
  input: { hint: "<要做什么>" },
13710
13864
  handler: async ({ agent, rawInput }) => {
13711
13865
  const task = rawInput.trim();
@@ -13751,10 +13905,7 @@ async function apply(ctx, config) {
13751
13905
  frp: frpStore,
13752
13906
  origin: originStore
13753
13907
  };
13754
- await Promise.all(Object.keys(stores).filter((provider) => provider !== remoteProviders.selected).map((provider) => stores[provider].save({
13755
- version: 1,
13756
- enabled: false
13757
- })));
13908
+ await Promise.all(Object.keys(stores).filter((provider) => provider !== remoteProviders.selected).map((provider) => stores[provider].disableIfPresent()));
13758
13909
  for (const provider of REMOTE_PROVIDERS) await remoteControllers[provider].initialize();
13759
13910
  const lanDiscovery = lanGateway?.discoveryStatus();
13760
13911
  if (lanDiscovery !== void 0 && !lanDiscovery.broadcast) logger.warn("broadcast discovery is unavailable (%s); mDNS is still published, so pair manually or free the UDP port", lanDiscovery.errorCode ?? "unknown_cause");
@@ -13795,6 +13946,6 @@ async function apply(ctx, config) {
13795
13946
  }, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
13796
13947
  }
13797
13948
  //#endregion
13798
- export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CSRF_COOKIE, CSRF_HEADER, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, 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, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
13949
+ export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CSRF_COOKIE, CSRF_HEADER, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, 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, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
13799
13950
 
13800
13951
  //# sourceMappingURL=index.mjs.map