dsh-mobile 0.6.5 → 0.6.7

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/client.js CHANGED
@@ -3204,6 +3204,127 @@ window.__ModuleLoader__.load({
3204
3204
  [data-dsh-mobile-composer-model-label] { flex:1 1 auto !important; max-width:none !important; min-width:0 !important; overflow:hidden !important; text-overflow:ellipsis !important; white-space:nowrap !important; }
3205
3205
  }
3206
3206
  `;
3207
+ //#endregion
3208
+ //#region src/browser-file-picker.ts
3209
+ /** Browser chooser lifetime shared by camera controls and the extension SDK. */
3210
+ const BROWSER_FILE_PICKER_DEADLINE_MS = 3e5;
3211
+ const browserEnvironment = {
3212
+ createSource: () => {
3213
+ const input = document.createElement("input");
3214
+ return {
3215
+ input,
3216
+ open: () => {
3217
+ document.body.append(input);
3218
+ input.click();
3219
+ },
3220
+ remove: () => {
3221
+ input.remove();
3222
+ }
3223
+ };
3224
+ },
3225
+ deadline: (callback, milliseconds) => {
3226
+ const timer = window.setTimeout(callback, milliseconds);
3227
+ return () => {
3228
+ window.clearTimeout(timer);
3229
+ };
3230
+ }
3231
+ };
3232
+ /** Select files without treating focus/visibility restoration as cancellation.
3233
+ * @param options - MIME selection, lifetime and optional camera capture mode.
3234
+ * @param environment - Browser DOM and timer owner; tests may supply an owned source.
3235
+ * @returns Selected files, or undefined for explicit cancellation or deadline; abort rejects.
3236
+ */
3237
+ function pickBrowserFiles(options, environment = browserEnvironment) {
3238
+ const abortReason = () => options.abortReason?.() ?? options.signal.reason ?? new DOMException("File selection aborted", "AbortError");
3239
+ if (options.signal.aborted) return Promise.reject(abortReason());
3240
+ return new Promise((resolve, reject) => {
3241
+ let source;
3242
+ let input;
3243
+ let settled = false;
3244
+ let cancelDeadline;
3245
+ const cleanup = () => {
3246
+ const errors = [];
3247
+ const attempt = (operation) => {
3248
+ try {
3249
+ operation();
3250
+ } catch (error) {
3251
+ errors.push(error);
3252
+ }
3253
+ };
3254
+ if (cancelDeadline !== void 0) attempt(cancelDeadline);
3255
+ cancelDeadline = void 0;
3256
+ attempt(() => {
3257
+ input?.removeEventListener("change", onChange);
3258
+ });
3259
+ attempt(() => {
3260
+ input?.removeEventListener("cancel", onCancel);
3261
+ });
3262
+ attempt(() => {
3263
+ options.signal.removeEventListener("abort", onAbort);
3264
+ });
3265
+ attempt(() => {
3266
+ source?.remove();
3267
+ });
3268
+ return errors;
3269
+ };
3270
+ const finish = (outcome) => {
3271
+ if (settled) return;
3272
+ settled = true;
3273
+ const cleanupErrors = cleanup();
3274
+ if (outcome.kind === "failed") reject(outcome.error);
3275
+ else if (cleanupErrors.length > 0) reject(cleanupErrors[0]);
3276
+ else if (outcome.kind === "cancelled") resolve(void 0);
3277
+ else resolve(outcome.files);
3278
+ };
3279
+ const onChange = () => {
3280
+ if (settled || input === void 0) return;
3281
+ try {
3282
+ const files = input.files === null ? [] : [...input.files];
3283
+ finish(files.length === 0 ? { kind: "cancelled" } : {
3284
+ kind: "files",
3285
+ files: Object.freeze(files)
3286
+ });
3287
+ } catch (error) {
3288
+ finish({
3289
+ kind: "failed",
3290
+ error
3291
+ });
3292
+ }
3293
+ };
3294
+ const onCancel = () => {
3295
+ finish({ kind: "cancelled" });
3296
+ };
3297
+ const onAbort = () => {
3298
+ finish({
3299
+ kind: "failed",
3300
+ error: abortReason()
3301
+ });
3302
+ };
3303
+ try {
3304
+ source = environment.createSource();
3305
+ input = source.input;
3306
+ input.type = "file";
3307
+ input.accept = options.accept;
3308
+ if (options.capture !== void 0) input.capture = options.capture;
3309
+ input.multiple = options.multiple ?? false;
3310
+ input.hidden = true;
3311
+ input.addEventListener("change", onChange);
3312
+ input.addEventListener("cancel", onCancel);
3313
+ options.signal.addEventListener("abort", onAbort, { once: true });
3314
+ cancelDeadline = environment.deadline(onCancel, BROWSER_FILE_PICKER_DEADLINE_MS);
3315
+ if (options.signal.aborted) {
3316
+ onAbort();
3317
+ return;
3318
+ }
3319
+ source.open();
3320
+ } catch (error) {
3321
+ finish({
3322
+ kind: "failed",
3323
+ error
3324
+ });
3325
+ }
3326
+ });
3327
+ }
3207
3328
  //#endregion
3208
3329
  //#region src/native-mobile.ts
3209
3330
  /**
@@ -3276,6 +3397,13 @@ window.__ModuleLoader__.load({
3276
3397
  html.dsh-native-mobile-active [data-dsh-mobile-sidebar] [class*="_sessionRow"] [class*="_pinIndicator"] { display:none !important; }
3277
3398
  html.dsh-native-mobile-active [data-dsh-mobile-sidebar] [class*="_rowActions"] button { box-sizing:border-box !important; width:32px !important; min-width:32px !important; height:32px !important; min-height:32px !important; }
3278
3399
  [data-dsh-mobile-frame] { grid-template-columns:0 minmax(0,1fr) 0 !important; width:100% !important; height:100dvh !important; overflow:hidden !important; }
3400
+ /* The stock frame owns layout, not a tap target. Restore only its content
3401
+ columns; the stock shell-overlay retains its own descendant hit policy. */
3402
+ html.dsh-native-mobile-active [data-dsh-mobile-frame] { pointer-events:none !important; }
3403
+ html.dsh-native-mobile-active [data-dsh-mobile-frame] > :is([data-dsh-mobile-sidebar],[data-dsh-mobile-center],[class*="_rightbarCol"]) { pointer-events:auto !important; }
3404
+ /* Floating slot surfaces paint above mobile drawers; their empty layer
3405
+ stays click-through and each stock occupant retains its own hit policy. */
3406
+ html.dsh-native-mobile-active [data-shell-overlay] { z-index:300 !important; pointer-events:none !important; }
3279
3407
  [data-dsh-mobile-center] { grid-column:2 !important; width:100vw !important; min-width:0 !important; }
3280
3408
  [data-dsh-mobile-center] > * { min-width:0 !important; }
3281
3409
  [data-dsh-mobile-header] { box-sizing:border-box !important; width:calc(100% - 16px) !important; margin:0 8px !important; min-width:0; padding-top:max(4px,env(safe-area-inset-top)) !important; padding-right:8px !important; padding-left:42px !important; }
@@ -3297,6 +3425,10 @@ window.__ModuleLoader__.load({
3297
3425
  [data-dsh-mobile-sidebar] { position:fixed !important; z-index:240 !important; inset:0 auto 0 0 !important; width:0 !important; overflow:visible !important; }
3298
3426
  [data-dsh-mobile-sidebar-root] { position:fixed !important; z-index:241 !important; inset:max(env(safe-area-inset-top),0px) auto 0 0 !important; height:auto !important; transition:width 180ms var(--dsh-mobile-motion-ease),box-shadow 180ms ease !important; }
3299
3427
  [data-dsh-mobile-sidebar][data-open="true"] [data-dsh-mobile-sidebar-root] { width:min(88vw,340px) !important; padding-top:0 !important; box-shadow:18px 0 46px rgb(15 23 42 / 18%); }
3428
+ /* Extra panel/footer seats may exceed a short drawer. Keep a bounded
3429
+ workspace scrollport and let the drawer scroll to the remaining seats. */
3430
+ [data-dsh-mobile-sidebar][data-open="true"]:not(:has([data-dsh-mobile-settings])) [data-dsh-mobile-sidebar-root] { overflow-y:auto !important; overscroll-behavior-y:contain; }
3431
+ [data-dsh-mobile-sidebar][data-open="true"]:not(:has([data-dsh-mobile-settings])) [data-dsh-mobile-sidebar-root] > [class*="_regionArea"] { flex:1 0 50% !important; }
3300
3432
  [data-dsh-mobile-sidebar][data-open="true"] [data-dsh-mobile-sidebar-root] [class*="_logoRow"] { height:52px !important; padding:4px 0 4px 4px !important; margin-bottom:4px !important; }
3301
3433
  [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] { width:0 !important; border:0 !important; background:transparent !important; box-shadow:none !important; overflow:visible !important; }
3302
3434
  [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] > :not(:has([data-dsh-mobile-toggle])) { display:none !important; }
@@ -3624,19 +3756,66 @@ window.__ModuleLoader__.load({
3624
3756
  }
3625
3757
  };
3626
3758
  }
3627
- function controlledFileDragEvent(type, files, initialDropEffect) {
3628
- const event = new Event(type, {
3629
- bubbles: true,
3630
- cancelable: true
3759
+ /** A read-only, file-only transfer for engines without usable native constructors. */
3760
+ function controlledFileTransfer(files, initialDropEffect) {
3761
+ const snapshot = [...files];
3762
+ const fileList = Object.assign(snapshot, { item: (index) => snapshot[index] ?? null });
3763
+ const items = Object.assign(snapshot.map((file) => ({
3764
+ kind: "file",
3765
+ type: file.type,
3766
+ getAsFile: () => file,
3767
+ getAsString: () => {},
3768
+ webkitGetAsEntry: () => null
3769
+ })), {
3770
+ add: () => null,
3771
+ clear: () => {},
3772
+ remove: () => {}
3631
3773
  });
3632
- const dataTransfer = {
3774
+ Object.freeze(fileList);
3775
+ Object.freeze(items);
3776
+ return {
3633
3777
  dropEffect: initialDropEffect,
3634
3778
  effectAllowed: "copy",
3635
- files: Object.freeze([...files]),
3636
- types: Object.freeze(["Files"])
3637
- };
3638
- Object.defineProperty(event, "dataTransfer", { value: dataTransfer });
3639
- return event;
3779
+ files: fileList,
3780
+ items,
3781
+ types: Object.freeze(["Files"]),
3782
+ clearData: () => {},
3783
+ getData: () => "",
3784
+ setData: () => {},
3785
+ setDragImage: () => {}
3786
+ };
3787
+ }
3788
+ function nativeFileTransfer(files, initialDropEffect) {
3789
+ const transfer = new DataTransfer();
3790
+ for (const file of files) if (transfer.items.add(file) === null) throw new Error("file_transfer_unavailable");
3791
+ if (transfer.files.length !== files.length) throw new Error("file_transfer_unavailable");
3792
+ transfer.effectAllowed = "copy";
3793
+ transfer.dropEffect = "copy";
3794
+ if (transfer.effectAllowed !== "copy" || transfer.dropEffect !== "copy") throw new Error("file_transfer_unavailable");
3795
+ transfer.dropEffect = initialDropEffect;
3796
+ if (transfer.dropEffect !== initialDropEffect) throw new Error("file_transfer_unavailable");
3797
+ return transfer;
3798
+ }
3799
+ function controlledFileDragEvent(type, files, initialDropEffect) {
3800
+ let transfer;
3801
+ if (typeof DataTransfer === "function") try {
3802
+ transfer = nativeFileTransfer(files, initialDropEffect);
3803
+ } catch (_error) {
3804
+ transfer = controlledFileTransfer(files, initialDropEffect);
3805
+ }
3806
+ else transfer = controlledFileTransfer(files, initialDropEffect);
3807
+ if (typeof DragEvent === "function") try {
3808
+ const event = new DragEvent(type, {
3809
+ bubbles: true,
3810
+ cancelable: true,
3811
+ dataTransfer: transfer
3812
+ });
3813
+ if (event.dataTransfer !== null) return event;
3814
+ } catch (_error) {}
3815
+ return Object.assign(new Event(type, {
3816
+ bubbles: true,
3817
+ cancelable: true
3818
+ }), { dataTransfer: transfer });
3640
3819
  }
3641
3820
  /** Ask the official document dragover listener whether the current composer accepts files. */
3642
3821
  function preflightComposerImageDrop(target, files) {
@@ -4086,7 +4265,6 @@ window.__ModuleLoader__.load({
4086
4265
  const preflightFile = new File([], "dsh-mobile-preflight.png", { type: "image/png" });
4087
4266
  const canAcceptComposerDrop = () => preflightComposerImageDrop(document, [preflightFile]);
4088
4267
  const mediaPickerAbortController = new AbortController();
4089
- const browserPickerCleanups = /* @__PURE__ */ new Set();
4090
4268
  let boundEnterEditor = null;
4091
4269
  let boundEnterCard = null;
4092
4270
  let disposeEnterBinding;
@@ -4133,63 +4311,18 @@ window.__ModuleLoader__.load({
4133
4311
  };
4134
4312
  const deliverImages = (files, context) => {
4135
4313
  if (files.length === 0 || !mediaRequestIsCurrent(context)) return;
4136
- dispatchComposerImageDrop(document, files);
4314
+ if (!dispatchComposerImageDrop(document, files)) showMediaToast(label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
4137
4315
  };
4138
4316
  const launchBrowserCamera = (context) => {
4139
- const input = document.createElement("input");
4140
- input.type = "file";
4141
- input.accept = "image/*";
4142
- input.capture = "environment";
4143
- input.hidden = true;
4144
- const signal = mediaPickerAbortController.signal;
4145
- let cleanupTimer = 0;
4146
- let watchdogTimer = 0;
4147
- let cleaned = false;
4148
- const scheduleCleanup = () => {
4149
- if (!cleaned && cleanupTimer === 0) cleanupTimer = window.setTimeout(cleanup, 1e3);
4150
- };
4151
- const onVisibilityChange = () => {
4152
- if (document.visibilityState === "visible") scheduleCleanup();
4153
- };
4154
- const onChange = () => {
4155
- if (cleaned) return;
4156
- const files = input.files === null ? [] : [...input.files];
4157
- cleanup();
4158
- deliverImages(files, context);
4159
- };
4160
- const cleanup = () => {
4161
- if (cleaned) return;
4162
- cleaned = true;
4163
- if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
4164
- if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
4165
- cleanupTimer = 0;
4166
- watchdogTimer = 0;
4167
- input.removeEventListener("change", onChange);
4168
- input.removeEventListener("cancel", cleanup);
4169
- window.removeEventListener("focus", scheduleCleanup);
4170
- document.removeEventListener("visibilitychange", onVisibilityChange);
4171
- signal.removeEventListener("abort", cleanup);
4172
- browserPickerCleanups.delete(cleanup);
4173
- input.remove();
4174
- };
4175
- input.addEventListener("change", onChange);
4176
- input.addEventListener("cancel", cleanup);
4177
- window.addEventListener("focus", scheduleCleanup);
4178
- document.addEventListener("visibilitychange", onVisibilityChange);
4179
- signal.addEventListener("abort", cleanup, { once: true });
4180
- watchdogTimer = window.setTimeout(cleanup, 3e5);
4181
- browserPickerCleanups.add(cleanup);
4182
- if (signal.aborted) {
4183
- cleanup();
4184
- return;
4185
- }
4186
- try {
4187
- document.body.append(input);
4188
- input.click();
4189
- } catch {
4190
- cleanup();
4191
- if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aprire la fotocamera", "Could not open the camera", "无法打开相机"));
4192
- }
4317
+ pickBrowserFiles({
4318
+ accept: "image/*",
4319
+ capture: "environment",
4320
+ signal: mediaPickerAbortController.signal
4321
+ }).then((files) => {
4322
+ if (files !== void 0) deliverImages(files, context);
4323
+ }).catch((_error) => {
4324
+ if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aggiungere la foto", "Could not attach the photo", "无法附加照片"));
4325
+ });
4193
4326
  };
4194
4327
  const dismissCommandMenu = () => {
4195
4328
  (boundComposer?.querySelector("button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]"))?.click();
@@ -4647,7 +4780,6 @@ window.__ModuleLoader__.load({
4647
4780
  mediaRequestGeneration += 1;
4648
4781
  mediaPickerAbortController.abort();
4649
4782
  restoreLanguageMarker();
4650
- for (const cleanup of [...browserPickerCleanups]) cleanup();
4651
4783
  disposeEnterBinding?.();
4652
4784
  disposeBrowserComposerSoftEnter();
4653
4785
  observer.disconnect();
@@ -6686,6 +6818,122 @@ window.__ModuleLoader__.load({
6686
6818
  });
6687
6819
  }
6688
6820
  //#endregion
6821
+ //#region src/mobile-custom-assets.ts
6822
+ const COPY = {
6823
+ en: {
6824
+ gateway_required: "Custom mobile CSS, scripts and extensions require a paired DSH Mobile access entry. This page is not using the Mobile gateway.",
6825
+ authentication_required: "Mobile customization was not refreshed because device authentication is unavailable. Reconnect through the current Mobile access entry.",
6826
+ manifest_unavailable: "The mobile extension manifest is unavailable. Check the plugin version and proxy routing; legacy custom files can still load when authorized.",
6827
+ manifest_invalid: "The mobile extension manifest is invalid. Existing customization is retained; check the plugin version and proxy routing.",
6828
+ resource_unavailable: "A mobile customization resource could not be loaded or activated. Check the custom files and current Mobile access entry.",
6829
+ refresh_failed: "Mobile customization could not be refreshed. Check the connection and current Mobile access entry.",
6830
+ recovered: "Mobile customization is available again."
6831
+ },
6832
+ it: {
6833
+ gateway_required: "CSS, script ed estensioni mobili personalizzati richiedono un accesso DSH Mobile abbinato. Questa pagina non usa il gateway Mobile.",
6834
+ authentication_required: "La personalizzazione mobile non è stata aggiornata perché l’autenticazione del dispositivo non è disponibile. Riconnettiti tramite l’accesso Mobile attuale.",
6835
+ manifest_unavailable: "Il manifest delle estensioni mobili non è disponibile. Controlla la versione del plugin e le route del proxy; i file personalizzati legacy possono essere caricati se autorizzati.",
6836
+ manifest_invalid: "Il manifest delle estensioni mobili non è valido. La personalizzazione esistente viene mantenuta; controlla la versione del plugin e le route del proxy.",
6837
+ resource_unavailable: "Una risorsa mobile personalizzata non è stata caricata o attivata. Controlla i file personalizzati e l’accesso Mobile attuale.",
6838
+ refresh_failed: "Impossibile aggiornare la personalizzazione mobile. Controlla la connessione e l’accesso Mobile attuale.",
6839
+ recovered: "La personalizzazione mobile è di nuovo disponibile."
6840
+ },
6841
+ zh: {
6842
+ gateway_required: "自定义移动样式、脚本与扩展需要通过已配对的 DSH Mobile 入口访问;当前页面不是移动网关页面。",
6843
+ authentication_required: "设备认证不可用,移动定制暂未刷新;请通过当前移动访问入口重新连接。",
6844
+ manifest_unavailable: "移动扩展清单不可用;请检查插件版本及反向代理路由。通过认证时,旧版自定义文件仍可加载。",
6845
+ manifest_invalid: "移动扩展清单无效,保留现有定制;请检查插件版本及反向代理路由。",
6846
+ resource_unavailable: "移动定制资源加载或激活失败;请检查定制文件及当前移动访问入口。",
6847
+ refresh_failed: "移动定制暂未刷新;请检查网络及当前移动访问入口。",
6848
+ recovered: "移动定制已恢复加载。"
6849
+ }
6850
+ };
6851
+ /** Only the authenticated Gateway owns customization APIs; the older dedicated marker remains supported. */
6852
+ function ownsMobileCustomization(gateway, frontend) {
6853
+ return gateway === true || frontend === "dedicated";
6854
+ }
6855
+ /** Emit each safe failure once until recovery, and ignore work after cancellation or disposal. */
6856
+ function createMobileAssetLoadReporter(locale, output = console) {
6857
+ const reported = /* @__PURE__ */ new Set();
6858
+ let disposed = false;
6859
+ return {
6860
+ fail(reason, signal) {
6861
+ if (disposed || signal?.aborted === true || reported.has(reason)) return;
6862
+ if (reason === "resource_unavailable" && reported.has("authentication_required")) return;
6863
+ reported.add(reason);
6864
+ output.warn(`[DSH Mobile] ${COPY[locale()][reason]}`);
6865
+ },
6866
+ recover(signal) {
6867
+ if (disposed || signal.aborted || reported.size === 0) return;
6868
+ reported.clear();
6869
+ output.info(`[DSH Mobile] ${COPY[locale()].recovered}`);
6870
+ },
6871
+ dispose() {
6872
+ disposed = true;
6873
+ reported.clear();
6874
+ }
6875
+ };
6876
+ }
6877
+ /** Read customization text within its file budget; the aggregate manifest has no existing byte cap. */
6878
+ async function readMobileAssetText(response, signal, maxBytes) {
6879
+ const assertActive = () => {
6880
+ if (signal.aborted) throw signal.reason ?? new DOMException("mobile customization cancelled", "AbortError");
6881
+ };
6882
+ if (response.body === null) {
6883
+ assertActive();
6884
+ return "";
6885
+ }
6886
+ const reader = response.body.getReader();
6887
+ let cancelled;
6888
+ const cancel = () => cancelled ??= reader.cancel().catch((error) => {});
6889
+ const aborted = () => {
6890
+ cancel();
6891
+ };
6892
+ signal.addEventListener("abort", aborted, { once: true });
6893
+ let completed = false;
6894
+ try {
6895
+ assertActive();
6896
+ const chunks = [];
6897
+ let bytes = 0;
6898
+ while (true) {
6899
+ const next = await reader.read();
6900
+ assertActive();
6901
+ if (next.done) {
6902
+ completed = true;
6903
+ break;
6904
+ }
6905
+ bytes += next.value.byteLength;
6906
+ if (maxBytes !== void 0 && bytes > maxBytes) throw new Error("mobile customization resource is too large");
6907
+ chunks.push(next.value);
6908
+ }
6909
+ const body = new Uint8Array(bytes);
6910
+ let offset = 0;
6911
+ for (const chunk of chunks) {
6912
+ body.set(chunk, offset);
6913
+ offset += chunk.byteLength;
6914
+ }
6915
+ return new TextDecoder().decode(body);
6916
+ } finally {
6917
+ signal.removeEventListener("abort", aborted);
6918
+ if (!completed) await cancel();
6919
+ reader.releaseLock();
6920
+ }
6921
+ }
6922
+ //#endregion
6923
+ //#region src/extension-limits.ts
6924
+ /** Maximum sizes accepted for legacy mobile customization resources. */
6925
+ const LEGACY_CUSTOM_ASSET_LIMIT = 262144;
6926
+ /** Maximum sizes enforced when reading local extension files. */
6927
+ const EXTENSION_LIMITS = Object.freeze({
6928
+ manifest: 65536,
6929
+ script: 1048576,
6930
+ css: 524288,
6931
+ asset: 8388608,
6932
+ assetFiles: 256,
6933
+ assetBytes: 33554432,
6934
+ assetDepth: 8
6935
+ });
6936
+ //#endregion
6689
6937
  //#region src/client.ts
6690
6938
  /** Map a host-reported provider onto the UI union, defaulting to the built-in one. */
6691
6939
  function parseMobileRemoteProvider(value) {
@@ -10966,6 +11214,7 @@ window.__ModuleLoader__.load({
10966
11214
  }
10967
11215
  /** A missing manifest is authoritative before optional legacy resources finish refreshing. */
10968
11216
  async function handleMissingExtensionManifest(clearManifestResources, refreshLegacyResources, signal) {
11217
+ if (signal.aborted) return false;
10969
11218
  clearManifestResources();
10970
11219
  const results = await refreshLegacyResources();
10971
11220
  return !signal.aborted && results.every(Boolean);
@@ -11058,10 +11307,11 @@ window.__ModuleLoader__.load({
11058
11307
  function startExtensionChangeStream(changed, runtime = {
11059
11308
  window,
11060
11309
  create: (url) => new EventSource(url, { withCredentials: true })
11061
- }, onTaskEvent, onDeviceRevoked) {
11310
+ }, onTaskEvent, onDeviceRevoked, initiallyEnabled = true) {
11062
11311
  let source;
11063
11312
  let timer;
11064
11313
  let disposed = false;
11314
+ let enabled = initiallyEnabled;
11065
11315
  let retryMs = 1e3;
11066
11316
  const clearTimer = () => {
11067
11317
  if (timer === void 0) return;
@@ -11069,19 +11319,24 @@ window.__ModuleLoader__.load({
11069
11319
  timer = void 0;
11070
11320
  };
11071
11321
  const connect = () => {
11072
- if (disposed || source !== void 0) return;
11322
+ if (disposed || !enabled || source !== void 0) return;
11073
11323
  clearTimer();
11074
11324
  const next = runtime.create("/mobile-access/extensions/events");
11075
11325
  source = next;
11326
+ const current = () => !disposed && enabled && source === next;
11076
11327
  next.onopen = () => {
11077
- retryMs = 1e3;
11328
+ if (current()) retryMs = 1e3;
11078
11329
  };
11079
- next.addEventListener("extensions-changed", changed);
11330
+ next.addEventListener("extensions-changed", () => {
11331
+ if (current()) changed();
11332
+ });
11080
11333
  if (onTaskEvent !== void 0) next.addEventListener("task-notify", (message) => {
11334
+ if (!current()) return;
11081
11335
  const data = message.data;
11082
11336
  onTaskEvent(data);
11083
11337
  });
11084
11338
  if (onDeviceRevoked !== void 0) next.addEventListener("device-revoked", (message) => {
11339
+ if (!current()) return;
11085
11340
  const data = message.data;
11086
11341
  if (isDeviceRevokedPayload(data)) onDeviceRevoked();
11087
11342
  });
@@ -11089,14 +11344,14 @@ window.__ModuleLoader__.load({
11089
11344
  if (source !== next) return;
11090
11345
  next.close();
11091
11346
  source = void 0;
11092
- if (disposed) return;
11347
+ if (disposed || !enabled) return;
11093
11348
  const delay = retryMs;
11094
11349
  retryMs = Math.min(3e4, retryMs * 2);
11095
11350
  timer = runtime.window.setTimeout(connect, delay);
11096
11351
  };
11097
11352
  };
11098
11353
  const reconnectNow = () => {
11099
- if (disposed) return;
11354
+ if (disposed || !enabled) return;
11100
11355
  source?.close();
11101
11356
  source = void 0;
11102
11357
  retryMs = 1e3;
@@ -11104,13 +11359,23 @@ window.__ModuleLoader__.load({
11104
11359
  };
11105
11360
  runtime.window.addEventListener("online", reconnectNow);
11106
11361
  connect();
11107
- return () => {
11362
+ const stop = () => {
11108
11363
  disposed = true;
11109
11364
  clearTimer();
11110
11365
  source?.close();
11111
11366
  source = void 0;
11112
11367
  runtime.window.removeEventListener("online", reconnectNow);
11113
11368
  };
11369
+ return Object.assign(stop, { setEnabled(value) {
11370
+ if (disposed || value === enabled) return;
11371
+ enabled = value;
11372
+ if (value) connect();
11373
+ else {
11374
+ clearTimer();
11375
+ source?.close();
11376
+ source = void 0;
11377
+ }
11378
+ } });
11114
11379
  }
11115
11380
  /** Keep at most one activation in flight per id and commit only its latest generation. */
11116
11381
  var PerIdActivationLifecycle = class {
@@ -11238,7 +11503,10 @@ window.__ModuleLoader__.load({
11238
11503
  });
11239
11504
  }
11240
11505
  };
11506
+ /** Install authenticated mobile customization refresh, retaining loaded legacy assets through temporary failures. */
11241
11507
  function installCustomAssets() {
11508
+ const reporter = createMobileAssetLoadReporter(selectedMobileControlLocale);
11509
+ const customizationOwner = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
11242
11510
  const legacyStyle = element("style");
11243
11511
  legacyStyle.dataset.plugin = "dsh-mobile-custom";
11244
11512
  document.head.append(legacyStyle);
@@ -11263,6 +11531,16 @@ window.__ModuleLoader__.load({
11263
11531
  const managedDefinitionIds = /* @__PURE__ */ new Set();
11264
11532
  let manifestEtag = "";
11265
11533
  let disposed = false;
11534
+ let events;
11535
+ const failed = (reason, signal) => {
11536
+ if (disposed || signal.aborted) return;
11537
+ reporter.fail(reason, signal);
11538
+ if (reason === "authentication_required") events?.setEnabled(false);
11539
+ };
11540
+ const unavailable = (response, signal, fallback) => {
11541
+ failed(response.status === 401 || response.status === 403 ? "authentication_required" : fallback, signal);
11542
+ response.body?.cancel().catch((error) => {});
11543
+ };
11266
11544
  let expectedDefinitionId;
11267
11545
  const SURFACE_HOST_STYLES = {
11268
11546
  "sidebar-action": "position:fixed;z-index:1100;top:calc(env(safe-area-inset-top) + 8px);left:8px;display:flex;flex-direction:column;gap:6px;pointer-events:none",
@@ -11300,6 +11578,16 @@ window.__ModuleLoader__.load({
11300
11578
  const stopNativeBack = installMobileNativeBack(window, document, () => {
11301
11579
  toast(mobileBackBlockedMessage(selectedMobileControlLocale()));
11302
11580
  });
11581
+ if (!customizationOwner) {
11582
+ reporter.fail("gateway_required");
11583
+ return () => {
11584
+ disposed = true;
11585
+ reporter.dispose();
11586
+ stopNativeBack();
11587
+ legacyStyle.remove();
11588
+ document.querySelector("[data-dsh-mobile-extension-layer]")?.remove();
11589
+ };
11590
+ }
11303
11591
  const materializeNativeFile = (value) => {
11304
11592
  if (typeof value !== "object" || value === null || !("base64" in value) || !("name" in value)) return value;
11305
11593
  const candidate = value;
@@ -11326,73 +11614,12 @@ window.__ModuleLoader__.load({
11326
11614
  await navigator.clipboard.writeText(typeof input === "object" && input !== null && "text" in input ? String(input.text) : "");
11327
11615
  return { ok: true };
11328
11616
  }
11329
- if (action === "files.pick" || action === "camera.capture") {
11330
- const inputElement = element("input");
11331
- inputElement.type = "file";
11332
- inputElement.accept = action === "camera.capture" ? "image/*" : "*/*";
11333
- if (action === "camera.capture") inputElement.capture = "environment";
11334
- inputElement.hidden = true;
11335
- return new Promise((resolve, reject) => {
11336
- let settled = false;
11337
- let cleaned = false;
11338
- let cleanupTimer = 0;
11339
- let watchdogTimer = 0;
11340
- const cleanup = () => {
11341
- if (cleaned) return;
11342
- cleaned = true;
11343
- if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
11344
- if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
11345
- window.removeEventListener("focus", scheduleCancel);
11346
- document.removeEventListener("visibilitychange", onVisibilityChange);
11347
- signal.removeEventListener("abort", onAbort);
11348
- inputElement.removeEventListener("change", onChange);
11349
- inputElement.removeEventListener("cancel", onCancel);
11350
- inputElement.remove();
11351
- };
11352
- const finish = (callback) => {
11353
- if (settled) return;
11354
- settled = true;
11355
- cleanup();
11356
- callback();
11357
- };
11358
- const onChange = () => {
11359
- const file = inputElement.files?.[0];
11360
- finish(() => {
11361
- resolve(file);
11362
- });
11363
- };
11364
- const onCancel = () => {
11365
- finish(() => {
11366
- resolve(void 0);
11367
- });
11368
- };
11369
- const onAbort = () => {
11370
- finish(() => {
11371
- reject(abortReason());
11372
- });
11373
- };
11374
- const scheduleCancel = () => {
11375
- if (!settled && cleanupTimer === 0) cleanupTimer = window.setTimeout(onCancel, 1e3);
11376
- };
11377
- const onVisibilityChange = () => {
11378
- if (document.visibilityState === "visible") scheduleCancel();
11379
- };
11380
- inputElement.addEventListener("change", onChange, { once: true });
11381
- inputElement.addEventListener("cancel", onCancel, { once: true });
11382
- window.addEventListener("focus", scheduleCancel);
11383
- document.addEventListener("visibilitychange", onVisibilityChange);
11384
- signal.addEventListener("abort", onAbort, { once: true });
11385
- watchdogTimer = window.setTimeout(onCancel, 3e5);
11386
- try {
11387
- document.body.append(inputElement);
11388
- inputElement.click();
11389
- } catch (error) {
11390
- finish(() => {
11391
- reject(error);
11392
- });
11393
- }
11394
- });
11395
- }
11617
+ if (action === "files.pick" || action === "camera.capture") return pickBrowserFiles({
11618
+ signal,
11619
+ accept: action === "camera.capture" ? "image/*" : "*/*",
11620
+ ...action === "camera.capture" ? { capture: "environment" } : {},
11621
+ abortReason
11622
+ }).then((files) => files?.[0]);
11396
11623
  throw new Error("native capability is unavailable");
11397
11624
  };
11398
11625
  const makeApi = (id, hostGeneration, controller, surfaces, surfaceIds) => {
@@ -11567,10 +11794,13 @@ window.__ModuleLoader__.load({
11567
11794
  signal
11568
11795
  });
11569
11796
  if (response.status === 304) return true;
11570
- if (!response.ok) return false;
11797
+ if (!response.ok) {
11798
+ unavailable(response, signal, "resource_unavailable");
11799
+ return false;
11800
+ }
11571
11801
  const nextEtag = response.headers.get("etag") ?? "";
11572
11802
  const nextModified = response.headers.get("last-modified") ?? "";
11573
- const next = await response.text();
11803
+ const next = await readMobileAssetText(response, signal, LEGACY_CUSTOM_ASSET_LIMIT);
11574
11804
  if (refreshAborted(signal)) return false;
11575
11805
  if (next === legacySource) {
11576
11806
  legacyJsEtag = nextEtag;
@@ -11626,6 +11856,7 @@ window.__ModuleLoader__.load({
11626
11856
  } catch {
11627
11857
  pendingRoot?.remove();
11628
11858
  legacyMount = previousMount;
11859
+ failed("resource_unavailable", signal);
11629
11860
  return false;
11630
11861
  }
11631
11862
  };
@@ -11663,16 +11894,40 @@ window.__ModuleLoader__.load({
11663
11894
  headers,
11664
11895
  signal
11665
11896
  });
11897
+ if (disposed || signal.aborted) {
11898
+ response.body?.cancel().catch((error) => {});
11899
+ return false;
11900
+ }
11666
11901
  if (response.status === 404) {
11902
+ unavailable(response, signal, "manifest_unavailable");
11667
11903
  legacyScriptRevision = "";
11668
11904
  legacyStyleRevision = "";
11669
- return handleMissingExtensionManifest(clearManifestExtensions, () => Promise.all([refreshLegacy(signal), refreshCssLegacy(legacyStyle, signal, legacyCssState)]), signal);
11905
+ if (!await handleMissingExtensionManifest(clearManifestExtensions, () => Promise.all([refreshLegacy(signal), refreshCssLegacy(legacyStyle, signal, legacyCssState, unavailable)]), signal)) failed("resource_unavailable", signal);
11906
+ return false;
11907
+ }
11908
+ if (response.status === 304) {
11909
+ if (!signal.aborted) events?.setEnabled(true);
11910
+ return true;
11911
+ }
11912
+ if (!response.ok) {
11913
+ unavailable(response, signal, "manifest_unavailable");
11914
+ return false;
11670
11915
  }
11671
- if (response.status === 304) return true;
11672
- if (!response.ok) return false;
11673
11916
  const nextManifestEtag = response.headers.get("etag") ?? "";
11674
- const payload = parseMobileExtensionManifest(await response.json());
11675
- if (refreshAborted(signal) || payload === void 0) return false;
11917
+ const manifestSource = await readMobileAssetText(response, signal, void 0);
11918
+ let payload;
11919
+ try {
11920
+ payload = parseMobileExtensionManifest(JSON.parse(manifestSource));
11921
+ } catch {
11922
+ failed("manifest_invalid", signal);
11923
+ return false;
11924
+ }
11925
+ if (refreshAborted(signal)) return false;
11926
+ if (payload === void 0) {
11927
+ failed("manifest_invalid", signal);
11928
+ return false;
11929
+ }
11930
+ events?.setEnabled(true);
11676
11931
  const entries = payload.extensions;
11677
11932
  const seen = new Set(entries.map((entry) => entry.id));
11678
11933
  const scriptRevision = payload.legacy.scriptRevision;
@@ -11683,8 +11938,11 @@ window.__ModuleLoader__.load({
11683
11938
  else refreshComplete = false;
11684
11939
  }
11685
11940
  if (styleRevision === "" || styleRevision !== legacyStyleRevision) {
11686
- if (await refreshCssLegacy(legacyStyle, signal, legacyCssState)) legacyStyleRevision = styleRevision;
11687
- else refreshComplete = false;
11941
+ if (await refreshCssLegacy(legacyStyle, signal, legacyCssState, unavailable)) legacyStyleRevision = styleRevision;
11942
+ else {
11943
+ failed("resource_unavailable", signal);
11944
+ refreshComplete = false;
11945
+ }
11688
11946
  }
11689
11947
  const commitStyle = (id, change, css, etag) => {
11690
11948
  if (change === "retain") return;
@@ -11726,9 +11984,12 @@ window.__ModuleLoader__.load({
11726
11984
  signal
11727
11985
  });
11728
11986
  if (cssResponse.status !== 304) {
11729
- if (!cssResponse.ok) throw new Error("mobile extension style failed to load");
11987
+ if (!cssResponse.ok) {
11988
+ unavailable(cssResponse, signal, "resource_unavailable");
11989
+ throw new Error("mobile extension style failed to load");
11990
+ }
11730
11991
  pendingStyleEtag = cssResponse.headers.get("etag") ?? void 0;
11731
- pendingCss = await cssResponse.text();
11992
+ pendingCss = await readMobileAssetText(cssResponse, signal, EXTENSION_LIMITS.css);
11732
11993
  styleChange = "replace";
11733
11994
  if (refreshAborted(signal)) return false;
11734
11995
  }
@@ -11755,9 +12016,12 @@ window.__ModuleLoader__.load({
11755
12016
  let nextDigest;
11756
12017
  let nextEtag;
11757
12018
  if (scriptResponse.status !== 304) {
11758
- if (!scriptResponse.ok) throw new Error("mobile extension script failed to load");
12019
+ if (!scriptResponse.ok) {
12020
+ unavailable(scriptResponse, signal, "resource_unavailable");
12021
+ throw new Error("mobile extension script failed to load");
12022
+ }
11759
12023
  nextEtag = scriptResponse.headers.get("etag") ?? void 0;
11760
- const source = await scriptResponse.text();
12024
+ const source = await readMobileAssetText(scriptResponse, signal, EXTENSION_LIMITS.script);
11761
12025
  if (refreshAborted(signal)) return false;
11762
12026
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(source));
11763
12027
  if (refreshAborted(signal)) return false;
@@ -11811,6 +12075,7 @@ window.__ModuleLoader__.load({
11811
12075
  failClosedExtensionGenerationReplacement(hadActiveGeneration, previousHostGeneration, entry.generation, () => {
11812
12076
  disposeManifestExtension(entry.id);
11813
12077
  });
12078
+ failed("resource_unavailable", signal);
11814
12079
  refreshComplete = false;
11815
12080
  }
11816
12081
  }
@@ -11821,13 +12086,17 @@ window.__ModuleLoader__.load({
11821
12086
  } else manifestEtag = "";
11822
12087
  return refreshComplete;
11823
12088
  } catch {
12089
+ failed("refresh_failed", signal);
11824
12090
  return false;
11825
12091
  }
11826
12092
  };
11827
12093
  started = true;
11828
12094
  for (const definition of definitions.values()) activateDefinition(definition);
11829
12095
  const stopRefresh = startLifecycleRefreshScheduler(async (signal) => {
11830
- await refreshExtensions(signal);
12096
+ if (await refreshExtensions(signal) && !disposed && !signal.aborted) {
12097
+ reporter.recover(signal);
12098
+ events?.setEnabled(true);
12099
+ }
11831
12100
  });
11832
12101
  const stopEvents = startExtensionChangeStream(() => {
11833
12102
  stopRefresh.refresh();
@@ -11845,9 +12114,11 @@ window.__ModuleLoader__.load({
11845
12114
  });
11846
12115
  }, () => {
11847
12116
  fireDeviceRevoked();
11848
- });
12117
+ }, false);
12118
+ events = stopEvents;
11849
12119
  return () => {
11850
12120
  disposed = true;
12121
+ reporter.dispose();
11851
12122
  stopNativeBack();
11852
12123
  stopEvents();
11853
12124
  stopRefresh();
@@ -11866,7 +12137,7 @@ window.__ModuleLoader__.load({
11866
12137
  else window.dshMobile = previous;
11867
12138
  };
11868
12139
  }
11869
- async function refreshCssLegacy(style, signal, state) {
12140
+ async function refreshCssLegacy(style, signal, state, unavailable) {
11870
12141
  try {
11871
12142
  const headers = {};
11872
12143
  if (state.etag !== "") headers["if-none-match"] = state.etag;
@@ -11890,13 +12161,14 @@ window.__ModuleLoader__.load({
11890
12161
  if (response.ok) {
11891
12162
  const nextEtag = response.headers.get("etag") ?? "";
11892
12163
  const nextModified = response.headers.get("last-modified") ?? "";
11893
- const css = await response.text();
12164
+ const css = await readMobileAssetText(response, signal, LEGACY_CUSTOM_ASSET_LIMIT);
11894
12165
  if (refreshAborted(signal)) return false;
11895
12166
  style.textContent = css;
11896
12167
  state.etag = nextEtag;
11897
12168
  state.modified = nextModified;
11898
12169
  return true;
11899
12170
  }
12171
+ unavailable(response, signal, "resource_unavailable");
11900
12172
  return false;
11901
12173
  } catch {
11902
12174
  return false;
@@ -11950,11 +12222,13 @@ window.__ModuleLoader__.load({
11950
12222
  }, "dsh-mobile: authenticated gateway client trust");
11951
12223
  ctx.effect(() => {
11952
12224
  const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
12225
+ const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
11953
12226
  const style = element("style");
11954
12227
  style.dataset.plugin = "dsh-mobile";
11955
12228
  style.textContent = desktopAdmin ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;
11956
12229
  style.textContent += CLIENT_MODULE_STYLES;
11957
12230
  document.head.append(style);
12231
+ const removeCustom = !desktopAdmin || gatewayCustomization ? installCustomAssets() : () => void 0;
11958
12232
  const removeVoiceSession = installVoiceSession(!desktopAdmin);
11959
12233
  const removeModuleSettings = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
11960
12234
  name: "settings.general.item",
@@ -11998,7 +12272,6 @@ window.__ModuleLoader__.load({
11998
12272
  removeFontSize();
11999
12273
  };
12000
12274
  });
12001
- const removeCustom = installCustomAssets();
12002
12275
  const removeSurface = installDshLanguageBoundSurface(() => installNativeMobileSurface({
12003
12276
  getLayout: () => ctx.get("layout"),
12004
12277
  getSidebarRight: () => ctx.get("sidebarRight")
@@ -12058,6 +12331,7 @@ window.__ModuleLoader__.load({
12058
12331
  disposeClientModuleDialogs();
12059
12332
  removeModuleSettings();
12060
12333
  removeControl();
12334
+ removeCustom();
12061
12335
  removeVoiceSession();
12062
12336
  style.remove();
12063
12337
  };
@@ -12091,6 +12365,7 @@ window.__ModuleLoader__.load({
12091
12365
  exports.frpAttachFormErrorCode = frpAttachFormErrorCode;
12092
12366
  exports.handleMissingExtensionManifest = handleMissingExtensionManifest;
12093
12367
  exports.inject = inject;
12368
+ exports.installCustomAssets = installCustomAssets;
12094
12369
  exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
12095
12370
  exports.installMobileNativeBack = installMobileNativeBack;
12096
12371
  exports.mobileBackBlockedMessage = mobileBackBlockedMessage;