dsh-mobile 0.5.1 → 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
@@ -921,6 +921,8 @@ interface FrpConfigurationStatus {
921
921
  readonly publicOrigin?: string;
922
922
  readonly vhostHttpPort: number;
923
923
  readonly storagePath: string;
924
+ /** Proxy name frpc registers on the shared frps; unique per DSH installation. */
925
+ readonly proxyName: string;
924
926
  readonly errorCode?: string;
925
927
  /** Present only when the saved configuration leaves the upstream `deploy` default. */
926
928
  readonly mode?: FrpMode;
@@ -978,14 +980,46 @@ declare function mergeSavedFrpTarget(partial: Readonly<Record<string, unknown>>,
978
980
  readonly serverAddress: string;
979
981
  readonly serverPort: number;
980
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;
981
1012
  /**
982
1013
  * Build the single-purpose frpc configuration for the current loopback gateway.
983
1014
  *
984
1015
  * The default (public-CA) entry is an HTTP vhost behind Caddy and is emitted byte
985
1016
  * for byte as before. The self-signed entry cannot use a vhost at all: frps only
986
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}.
987
1021
  */
988
- declare function createFrpcToml(settings: FrpSettings, localPort: number): string;
1022
+ declare function createFrpcToml(settings: FrpSettings, localPort: number, proxyName?: string): string;
989
1023
  /** Build the matching restricted frps and Caddy templates for one VPS. */
990
1024
  declare function createFrpServerTemplate(settings: FrpSettings): string;
991
1025
  /** Owns private FRP settings and generation-specific frpc configuration. */
@@ -993,9 +1027,17 @@ declare class FrpConfigStore {
993
1027
  readonly stateRoot: string;
994
1028
  readonly settingsFile: string;
995
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;
996
1038
  private settingsValue;
997
1039
  private errorCode;
998
- constructor(stateDirectory: string);
1040
+ constructor(stateDirectory: string, proxyName?: string);
999
1041
  /** Load private settings while rejecting links, oversized files, and unknown fields. */
1000
1042
  initialize(): Promise<void>;
1001
1043
  /**
@@ -1032,6 +1074,14 @@ interface FrpAttachOptions {
1032
1074
  readonly certDir?: string;
1033
1075
  /** Absolute path of the plugin-written frpc.toml, used for the `frpc verify` self-check. */
1034
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;
1035
1085
  /** Loopback port the gateway will listen on; only a placeholder inside the copied runbook. */
1036
1086
  readonly localPort?: number;
1037
1087
  /**
@@ -1821,5 +1871,5 @@ declare function originGatewayConfig(template: ResolvedGatewayConfig, settings:
1821
1871
  /** Mount the resident control route and its optional authenticated LAN gateway. */
1822
1872
  declare function apply(ctx: Context, config: PluginConfig): Promise<void>;
1823
1873
  //#endregion
1824
- 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 };
1825
1875
  //# sourceMappingURL=index.d.mts.map
package/lib/index.mjs CHANGED
@@ -6120,13 +6120,51 @@ function tomlString(value) {
6120
6120
  return JSON.stringify(value);
6121
6121
  }
6122
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
+ /**
6123
6158
  * Build the single-purpose frpc configuration for the current loopback gateway.
6124
6159
  *
6125
6160
  * The default (public-CA) entry is an HTTP vhost behind Caddy and is emitted byte
6126
6161
  * for byte as before. The self-signed entry cannot use a vhost at all: frps only
6127
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}.
6128
6166
  */
6129
- function createFrpcToml(settings, localPort) {
6167
+ function createFrpcToml(settings, localPort, proxyName = FRP_DEFAULT_PROXY_NAME) {
6130
6168
  if (!Number.isSafeInteger(localPort) || localPort < 1 || localPort > 65535) throw new Error("frp_local_port_invalid");
6131
6169
  const header = [
6132
6170
  `serverAddr = ${tomlString(settings.serverAddress)}`,
@@ -6136,7 +6174,7 @@ function createFrpcToml(settings, localPort) {
6136
6174
  "transport.tls.enable = true",
6137
6175
  "",
6138
6176
  "[[proxies]]",
6139
- "name = \"dsh-mobile\""
6177
+ `name = ${tomlString(validateFrpProxyName(proxyName))}`
6140
6178
  ];
6141
6179
  if (isFrpSelfSignedIngress(settings)) return [
6142
6180
  ...header,
@@ -6198,13 +6236,22 @@ var FrpConfigStore = class {
6198
6236
  stateRoot;
6199
6237
  settingsFile;
6200
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;
6201
6247
  settingsValue;
6202
6248
  errorCode;
6203
- constructor(stateDirectory) {
6249
+ constructor(stateDirectory, proxyName = FRP_DEFAULT_PROXY_NAME) {
6204
6250
  if (!isAbsolute(stateDirectory)) throw new Error("frp config state directory must be absolute");
6205
6251
  this.stateRoot = resolve(stateDirectory);
6206
6252
  this.settingsFile = join(this.stateRoot, "settings.json");
6207
6253
  this.runtimeConfigFile = join(this.stateRoot, "frpc.toml");
6254
+ this.proxyName = validateFrpProxyName(proxyName);
6208
6255
  }
6209
6256
  /** Load private settings while rejecting links, oversized files, and unknown fields. */
6210
6257
  async initialize() {
@@ -6248,6 +6295,7 @@ var FrpConfigStore = class {
6248
6295
  },
6249
6296
  vhostHttpPort: settings === void 0 ? FRP_VHOST_HTTP_PORT : resolveFrpVhostHttpPort(settings),
6250
6297
  storagePath: this.stateRoot,
6298
+ proxyName: this.proxyName,
6251
6299
  ...this.errorCode === void 0 ? {} : { errorCode: this.errorCode },
6252
6300
  ...mode === void 0 || mode === "deploy" ? {} : { mode },
6253
6301
  ...entryTls === void 0 || entryTls === "public-ip-cert" ? {} : { entryTls },
@@ -6271,7 +6319,7 @@ var FrpConfigStore = class {
6271
6319
  async writeRuntimeConfig(localPort) {
6272
6320
  const settings = this.settingsValue;
6273
6321
  if (settings === void 0) throw new Error("frp_config_missing");
6274
- await atomicPrivateWrite$2(this.runtimeConfigFile, createFrpcToml(settings, localPort));
6322
+ await atomicPrivateWrite$2(this.runtimeConfigFile, createFrpcToml(settings, localPort, this.proxyName));
6275
6323
  return this.runtimeConfigFile;
6276
6324
  }
6277
6325
  /** Remove only configuration files owned by the FRP provider. */
@@ -6330,7 +6378,7 @@ function redactFrpcTomlToken(frpcToml) {
6330
6378
  * the provider writes stays byte-for-byte identical to the upstream output.
6331
6379
  */
6332
6380
  function createFrpAttachFrpcToml(settings, options = {}) {
6333
- const frpcToml = createFrpcToml(settings, attachLocalPort(options));
6381
+ const frpcToml = createFrpcToml(settings, attachLocalPort(options), options.proxyName ?? "dsh-mobile");
6334
6382
  return options.revealToken === true ? frpcToml : redactFrpcTomlToken(frpcToml);
6335
6383
  }
6336
6384
  const DEFAULT_ATTACH_CONFIG_FILE = "<DSH Mobile 私有目录>/remote/frp/config/frpc.toml";
@@ -7522,6 +7570,30 @@ const DISCOVERY_RETRY_MS = 1e3;
7522
7570
  const MAX_DISCOVERY_BYTES = 16384;
7523
7571
  const VHOST_PROBE_TIMEOUT_MS = 1500;
7524
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
+ }
7525
7597
  function publicStatus$3(status) {
7526
7598
  return Object.freeze({
7527
7599
  enabled: status.enabled,
@@ -7923,15 +7995,37 @@ var FrpController = class {
7923
7995
  return;
7924
7996
  }
7925
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));
7926
8018
  child.stdout.resume();
7927
8019
  child.stderr.resume();
7928
8020
  child.once("error", () => {
7929
8021
  this.enqueue(() => this.failGeneration(generation, "frp_launch_failed"));
7930
8022
  });
7931
8023
  child.once("close", (code) => {
8024
+ inspectLine(pending.stdout);
8025
+ inspectLine(pending.stderr);
7932
8026
  if (generation !== this.generation || this.child !== child) return;
7933
8027
  this.child = void 0;
7934
- 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"));
7935
8029
  });
7936
8030
  this.publish({
7937
8031
  enabled: true,
@@ -7947,6 +8041,7 @@ var FrpController = class {
7947
8041
  return;
7948
8042
  }
7949
8043
  if (generation !== this.generation || !this.enabled) return;
8044
+ if (diagnosed) return;
7950
8045
  const controller = new AbortController();
7951
8046
  this.startupAbort = controller;
7952
8047
  this.waitForDiscovery(generation, check, gateway.config.instanceId, settings, gateway, controller.signal);
@@ -9571,6 +9666,9 @@ const REMOTE_ERROR_GUIDANCE = Object.freeze({
9571
9666
  frp_vhost_probe_failed: "确认 VPS 地址可解析后重新连接。",
9572
9667
  frp_launch_failed: "重新安装 FRP 官方组件后重试。",
9573
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,确认它能接受该代理名和入口端口。",
9574
9672
  frp_discovery_mismatch: "公开域名连接到了另一台电脑,请核对 Caddy 与 frps 配置。",
9575
9673
  frp_discovery_invalid: "公开域名返回了非 DSH Mobile 响应。",
9576
9674
  frp_stopped: "点击“重新连接”。",
@@ -12669,6 +12767,36 @@ async function installMobileFileLogger(ctx, stateDirectory) {
12669
12767
  return file;
12670
12768
  }
12671
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
12672
12800
  //#region src/lan-setup.ts
12673
12801
  const FIREWALL_TCP_RULE = "DSH Mobile HTTPS";
12674
12802
  const FIREWALL_UDP_RULE = "DSH Mobile Discovery";
@@ -13066,7 +13194,8 @@ async function apply(ctx, config) {
13066
13194
  await cloudflaredComponent.initialize();
13067
13195
  const frpComponent = new FrpComponentManager({ stateDirectory });
13068
13196
  await frpComponent.initialize();
13069
- 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));
13070
13199
  await frpConfig.initialize();
13071
13200
  const originConfig = new OriginConfigStore(join(remoteDirectory, "origin", "config"));
13072
13201
  await originConfig.initialize();
@@ -13544,7 +13673,10 @@ async function apply(ctx, config) {
13544
13673
  }
13545
13674
  if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/frp/attach-plan`) {
13546
13675
  const settings = mergeSavedFrpSettings(await readJsonObject(request, 8192), frpConfig.settings());
13547
- const options = { configFile: frpConfig.runtimeConfigFile };
13676
+ const options = {
13677
+ configFile: frpConfig.runtimeConfigFile,
13678
+ proxyName: frpConfig.proxyName
13679
+ };
13548
13680
  logger.info("frp attach plan requested mode=%s entryTls=%s vhostHttpPort=%d", settings.mode ?? "deploy", settings.entryTls ?? "public-ip-cert", resolveFrpVhostHttpPort(settings));
13549
13681
  sendJson(response, 200, {
13550
13682
  ...remotePayload(),
@@ -13814,6 +13946,6 @@ async function apply(ctx, config) {
13814
13946
  }, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
13815
13947
  }
13816
13948
  //#endregion
13817
- 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 };
13818
13950
 
13819
13951
  //# sourceMappingURL=index.mjs.map