dsh-mobile 0.6.15 → 0.6.16

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
@@ -394,6 +394,8 @@ interface ResolvedGatewayConfig {
394
394
  readonly excludedClientModules: readonly string[];
395
395
  readonly websocketCompression: ResolvedWebSocketCompressionConfig;
396
396
  readonly instanceId: string;
397
+ /** Plugin-wide installation identity, disclosed only after native authentication. */
398
+ readonly installationId?: string;
397
399
  /** Identity returned only for authenticated legacy native credentials without a stored binding. */
398
400
  readonly legacyInstanceId?: string;
399
401
  readonly pairingCaFile?: string;
@@ -1093,6 +1095,8 @@ export declare class MobileAccessGateway {
1093
1095
  private handleRenew;
1094
1096
  /** CA-backed entries retain their pin; public entries project the authenticated credential's identity. */
1095
1097
  private nativeResponseInstanceId;
1098
+ /** Older Apps require exact wire keys; expose this hint only to explicit opt-in clients after authentication. */
1099
+ private nativeResponseInstallationIdentity;
1096
1100
  private handleNativePair;
1097
1101
  private handleNativeRenew;
1098
1102
  private handleNativeProbe;
package/lib/index.mjs CHANGED
@@ -4911,6 +4911,26 @@ function renderLoginScript(locale) {
4911
4911
  `;
4912
4912
  }
4913
4913
  //#endregion
4914
+ //#region src/browser-ui-locale.ts
4915
+ const UI_LOCALE_COOKIE = "dsh_ma_ui_locale";
4916
+ /** Read presentation data only; malformed or ambiguous cookies keep browser negotiation. */
4917
+ function readBrowserUiLocale(cookieHeader) {
4918
+ if (cookieHeader === void 0 || cookieHeader.length > 8192) return void 0;
4919
+ let locale;
4920
+ for (const part of cookieHeader.split(";")) {
4921
+ const separator = part.indexOf("=");
4922
+ if ((separator === -1 ? part : part.slice(0, separator)).trim() !== UI_LOCALE_COOKIE) continue;
4923
+ const value = separator === -1 ? "" : part.slice(separator + 1).trim();
4924
+ if (locale !== void 0 || value !== "en" && value !== "zh" && value !== "it") return void 0;
4925
+ locale = value;
4926
+ }
4927
+ return locale;
4928
+ }
4929
+ /** Prefer the last DSH display language over the browser's default language. */
4930
+ function resolveBrowserUiLocale(cookieHeader, acceptLanguage) {
4931
+ return readBrowserUiLocale(cookieHeader) ?? resolveAuthPageLocale(acceptLanguage);
4932
+ }
4933
+ //#endregion
4914
4934
  //#region src/gateway.ts
4915
4935
  const MAX_CONTROL_BODY_BYTES = 16384;
4916
4936
  const MAX_HEADER_BYTES = 16384;
@@ -6257,12 +6277,17 @@ var MobileAccessGateway = class {
6257
6277
  nativeResponseInstanceId(result) {
6258
6278
  return this.config.pairingCaFile === void 0 ? result.nativeInstanceId ?? this.config.instanceId : this.config.instanceId;
6259
6279
  }
6280
+ /** Older Apps require exact wire keys; expose this hint only to explicit opt-in clients after authentication. */
6281
+ nativeResponseInstallationIdentity(request) {
6282
+ return this.config.installationId === void 0 || request.headers["x-dsh-mobile-capabilities"] !== "installation-id" ? {} : { installationId: this.config.installationId };
6283
+ }
6260
6284
  async handleNativePair(request, response) {
6261
6285
  const body = await readJsonObject(request, MAX_CONTROL_BODY_BYTES);
6262
6286
  if (typeof body.token !== "string" || body.label !== void 0 && typeof body.label !== "string") throw new HttpError(400, "bad_request");
6263
6287
  const result = await this.access.pair(request.socket.remoteAddress ?? "unknown", body.token, body.label);
6264
6288
  sendJson(response, 201, {
6265
6289
  instanceId: this.nativeResponseInstanceId(result),
6290
+ ...this.nativeResponseInstallationIdentity(request),
6266
6291
  deviceId: result.deviceId,
6267
6292
  deviceToken: result.deviceToken,
6268
6293
  deviceExpiresAt: result.deviceExpiresAt,
@@ -6283,6 +6308,7 @@ var MobileAccessGateway = class {
6283
6308
  const result = await this.access.renew(selected.token);
6284
6309
  sendJson(response, 200, {
6285
6310
  instanceId: this.nativeResponseInstanceId(result),
6311
+ ...this.nativeResponseInstallationIdentity(request),
6286
6312
  deviceId: result.deviceId,
6287
6313
  sessionToken: result.sessionToken,
6288
6314
  csrfToken: result.csrfToken,
@@ -6302,6 +6328,7 @@ var MobileAccessGateway = class {
6302
6328
  const result = await this.access.probe(selected.token);
6303
6329
  sendJson(response, 200, {
6304
6330
  instanceId: this.nativeResponseInstanceId(result),
6331
+ ...this.nativeResponseInstallationIdentity(request),
6305
6332
  deviceId: result.deviceId,
6306
6333
  deviceExpiresAt: result.deviceExpiresAt
6307
6334
  }, this.tlsEnabled);
@@ -6390,9 +6417,9 @@ var MobileAccessGateway = class {
6390
6417
  if (target.search === "" && request.method === "GET" && (target.decodedPathname === `/mobile-access/pair` || target.decodedPathname === `/mobile-access/pair.js`)) {
6391
6418
  if (!this.access.pairingStatus().open) throw new HttpError(404, "not_found");
6392
6419
  const languageHeader = request.headers["accept-language"];
6393
- const locale = resolveAuthPageLocale(typeof languageHeader === "string" ? languageHeader : void 0);
6420
+ const locale = resolveBrowserUiLocale(request.headers.cookie, typeof languageHeader === "string" ? languageHeader : void 0);
6394
6421
  setSecurityHeaders(response, this.tlsEnabled);
6395
- response.setHeader("Vary", "Accept-Language");
6422
+ response.setHeader("Vary", "Accept-Language, Cookie");
6396
6423
  const body = target.decodedPathname.endsWith(".js") ? renderPairScript(locale) : renderPairPage(locale);
6397
6424
  response.writeHead(200, {
6398
6425
  "Content-Type": target.decodedPathname.endsWith(".js") ? "text/javascript; charset=utf-8" : "text/html; charset=utf-8",
@@ -6404,9 +6431,9 @@ var MobileAccessGateway = class {
6404
6431
  if (request.method === "GET" && (target.decodedPathname === `/mobile-access/login` || target.decodedPathname === `/mobile-access/login.js`)) {
6405
6432
  if (target.decodedPathname.endsWith(".js") && target.search !== "") throw new HttpError(400, "bad_request");
6406
6433
  const languageHeader = request.headers["accept-language"];
6407
- const locale = resolveAuthPageLocale(typeof languageHeader === "string" ? languageHeader : void 0);
6434
+ const locale = resolveBrowserUiLocale(request.headers.cookie, typeof languageHeader === "string" ? languageHeader : void 0);
6408
6435
  setSecurityHeaders(response, this.tlsEnabled);
6409
- response.setHeader("Vary", "Accept-Language");
6436
+ response.setHeader("Vary", "Accept-Language, Cookie");
6410
6437
  const body = target.decodedPathname.endsWith(".js") ? renderLoginScript(locale) : renderLoginPage(locale);
6411
6438
  response.writeHead(200, {
6412
6439
  "Content-Type": target.decodedPathname.endsWith(".js") ? "text/javascript; charset=utf-8" : "text/html; charset=utf-8",
@@ -17670,16 +17697,20 @@ async function loadOrCreateInstallationId(file) {
17670
17697
  //#endregion
17671
17698
  //#region src/gateway-identity.ts
17672
17699
  /**
17673
- * Use the persisted installation identity for an unpinned, automatically identified entry.
17700
+ * Attach the authenticated installation identity while retaining existing entry identity rules.
17674
17701
  * @param config - Validated gateway configuration containing the legacy identity.
17675
17702
  * @param installationId - Confirmed random identity read from this installation's state.
17676
17703
  * @param explicitIdentity - Whether the operator supplied the gateway identity.
17677
17704
  * @returns Configuration retaining certificate/operator identities or the new and legacy public identities.
17678
17705
  */
17679
17706
  function withInstallationIdentity(config, installationId, explicitIdentity) {
17680
- if (config.pairingCaFile !== void 0 || explicitIdentity) return config;
17707
+ if (config.pairingCaFile !== void 0 || explicitIdentity) return Object.freeze({
17708
+ ...config,
17709
+ installationId
17710
+ });
17681
17711
  return Object.freeze({
17682
17712
  ...config,
17713
+ installationId,
17683
17714
  instanceId: installationId,
17684
17715
  legacyInstanceId: config.instanceId
17685
17716
  });
@@ -18065,10 +18096,10 @@ async function apply(ctx, config) {
18065
18096
  const dshVersion = installedDshVersion();
18066
18097
  const loaded = await loadSetup(config);
18067
18098
  const mobileAccess = createMobileAccessService(ctx);
18068
- const template = loopbackTemplate(loaded, ctx.webServer.port);
18069
- const upstreamLoginUrl = upstreamAuthenticatedUrl(ctx, template.upstreamOrigin);
18070
- const instanceId = await stableInstanceId(loaded, template);
18071
- const stateDirectory = dirname(template.stateFile);
18099
+ const baseTemplate = loopbackTemplate(loaded, ctx.webServer.port);
18100
+ const upstreamLoginUrl = upstreamAuthenticatedUrl(ctx, baseTemplate.upstreamOrigin);
18101
+ const instanceId = await stableInstanceId(loaded, baseTemplate);
18102
+ const stateDirectory = dirname(baseTemplate.stateFile);
18072
18103
  const logFile = await installMobileFileLogger(ctx, stateDirectory);
18073
18104
  const logger = ctx.logger("dsh-mobile");
18074
18105
  logger.info("logging initialized file=%s", logFile);
@@ -18091,6 +18122,10 @@ async function apply(ctx, config) {
18091
18122
  const frpComponent = new FrpComponentManager({ stateDirectory });
18092
18123
  await frpComponent.initialize();
18093
18124
  const frpIdentity = await loadOrCreateInstallationId(join(stateDirectory, "installation-id"));
18125
+ const template = Object.freeze({
18126
+ ...baseTemplate,
18127
+ installationId: frpIdentity
18128
+ });
18094
18129
  const remoteInstanceId = loaded.kind === "managed" ? frpIdentity : loaded.config.instanceId ?? frpIdentity;
18095
18130
  const frpConfig = new FrpConfigStore(join(remoteDirectory, "frp", "config"), frpProxyName(frpIdentity), stateDirectory);
18096
18131
  await frpConfig.initialize();