File size: 7,492 Bytes
feb9f49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d3bef6
 
 
 
 
 
 
 
 
feb9f49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/* A native content mount lives below Capacitor's same-origin file mapping,
   while the installed updater, audio and commander portraits stay at their
   original game root. Never infer that root by climbing a filesystem URL. */
export const NATIVE_EXPLORATION_MOUNT_KEY = 'massfront.exploration.mount.v1';
export const NATIVE_EXPLORATION_READY_KEY = 'massfront.exploration.mount.ready.v1';
const MOUNT_KEYS = ['baseUrl', 'generation', 'issuedAt', 'kind', 'moduleUrl', 'schema', 'token'];
const MOUNT_SUFFIX = '/modules/space_exploration/index.html';
const nativeLoaderResolvers = new WeakSet();

function canonicalUrl(value) {
  if (typeof value !== 'string') return null;
  const url = new URL(value);
  if (!['https:', 'http:'].includes(url.protocol) || url.href !== value
    || url.username || url.password || url.search || url.hash) return null;
  return url;
}

export function readNativeExplorationMount(options = {}) {
  try {
    const storage = options.sessionStorage || globalThis.sessionStorage;
    const location = options.location || globalThis.location;
    const now = options.now === undefined ? Date.now() : options.now;
    const current = new URL(location.href);
    const record = JSON.parse(storage.getItem(NATIVE_EXPLORATION_MOUNT_KEY) || 'null');
    if (!record || Array.isArray(record)
      || Object.keys(record).sort().join('|') !== MOUNT_KEYS.join('|')
      || record.schema !== 1 || record.kind !== 'MassfrontNativeExplorationMountV1'
      || typeof record.generation !== 'string' || typeof record.token !== 'string'
      || !/^[a-f0-9]{64}$/.test(record.generation) || !/^[a-f0-9]{32}$/.test(record.token)
      || !Number.isSafeInteger(record.issuedAt) || record.issuedAt < 0
      || !Number.isSafeInteger(now) || now < record.issuedAt) return null;
    const base = canonicalUrl(record.baseUrl), module = canonicalUrl(record.moduleUrl);
    if (!base || !module || base.origin !== current.origin || module.origin !== current.origin
      || !base.pathname.endsWith('/index.html') || base.pathname.startsWith('/_capacitor_file_')
      || !module.pathname.startsWith('/_capacitor_file_/') || !module.pathname.endsWith(MOUNT_SUFFIX)
      || current.pathname !== module.pathname
      || /%(?:2e|2f|5c)/i.test(module.pathname + base.pathname)) return null;
    return Object.freeze({ ...record });
  } catch (_) { return null; }
}

export function resolveBaseRuntimeUrl(relativeUrl, referenceUrl, options = {}) {
  const fallback = new URL(relativeUrl, referenceUrl);
  const mount = readNativeExplorationMount(options);
  if (!mount) return fallback;
  const module = new URL(mount.moduleUrl), reference = new URL(referenceUrl);
  const moduleRoot = module.pathname.slice(0, -'index.html'.length);
  const contentRoot = module.pathname.slice(0, -'modules/space_exploration/index.html'.length);
  if (reference.origin !== module.origin || !reference.pathname.startsWith(moduleRoot)
    || fallback.origin !== module.origin || !fallback.pathname.startsWith(contentRoot)
    || fallback.pathname.startsWith(moduleRoot)) return fallback;
  const assetPath = fallback.pathname.slice(contentRoot.length);
  // Only base assets are relocated; module imports, GLBs and their external
  // textures retain the immutable generation path and dependency closure.
  if (!assetPath.startsWith('assets/') || /%(?:2e|2f|5c)/i.test(assetPath)) return fallback;
  return new URL(assetPath + fallback.search + fallback.hash, new URL('./', mount.baseUrl));
}

export function resolveBaseRuntimeNavigation(url, options = {}) {
  const mount = readNativeExplorationMount(options);
  if (typeof url !== 'string' || !/^(?:\.\.\/)+index\.html(?:[?#].*)?$/.test(url)) return url;
  if (!mount) {
    const current = new URL((options.location || globalThis.location).href);
    if (current.pathname.startsWith('/_capacitor_file_/')) {
      /* Capacitor's packaged document is always https://localhost/index.html.
         A damaged session mount used to make the emergency return throw even
         though that unprivileged origin-root document was still available.
         Keep the fallback deliberately narrower than the mounted resolver: it
         may only leave a local Capacitor file route for localhost itself. */
      if (['https:', 'http:'].includes(current.protocol) && current.hostname === 'localhost'
        && !current.username && !current.password) {
        return new URL(`/${url.slice(url.indexOf('index.html'))}`, current.origin).href;
      }
      throw new Error('The installed-game return path is unavailable. Restart MASSFRONT to recover safely.');
    }
    return url;
  }
  // This is a routing record, not authorization. Expiring it overnight would
  // strand valid missions in DATA; profile tickets and mission nonces retain
  // their independent security lifetimes and validation.
  // Existing host callers pass ../../../index.html. That happens to clamp to
  // the origin root in ordinary installs, but would escape the mounted tree.
  return new URL(url.slice(url.indexOf('index.html')), new URL('./', mount.baseUrl)).href;
}

export function nativeExplorationLoaderUrl(value, options = {}) {
  const mount = readNativeExplorationMount(options);
  if (!mount || typeof value !== 'string' || !/^https?:\/\//i.test(value)) return value;
  try {
    const url = new URL(value), module = new URL(mount.moduleUrl), base = new URL('./', mount.baseUrl);
    const moduleRoot = module.pathname.slice(0, -'index.html'.length);
    if (url.origin !== module.origin || url.username || url.password
      || (!url.pathname.startsWith(moduleRoot) && !url.pathname.startsWith(base.pathname + 'assets/'))) return value;
    return url.pathname + url.search + url.hash;
  } catch (_) { return value; }
}

export function installNativeExplorationLoaderUrls(options = {}) {
  if (!readNativeExplorationMount(options)) return false;
  const manager = options.manager || globalThis.THREE?.DefaultLoadingManager;
  if (!manager || typeof manager.resolveURL !== 'function') return false;
  if (nativeLoaderResolvers.has(manager.resolveURL)) return true;
  const previous = manager.resolveURL;
  const resolve = function (url) {
    return nativeExplorationLoaderUrl(previous.call(this, url), options);
  };
  // Capacitor 8.5's XHR interceptor proxies absolute localhost URLs whereas
  // fetch has a local-origin exemption. Three r128 FileLoader uses XHR. Keep
  // its local URLs root-relative without changing global XHR or remote traffic.
  // Do not use setURLModifier here: previous resolveURL closes over that
  // modifier and calling it from the modifier would recurse.
  nativeLoaderResolvers.add(resolve);
  manager.resolveURL = resolve;
  return true;
}

export function acknowledgeMountedRuntimeReady(context, options = {}) {
  if (!context) return false;
  const current = readNativeExplorationMount(options);
  // A resolved promise from the previous document/generation cannot confirm
  // a newer pending activation, even when it reused the same module filename.
  if (!current || MOUNT_KEYS.some(key => current[key] !== context[key])) return false;
  try {
    const readyAt = options.now === undefined ? Date.now() : options.now;
    const storage = options.localStorage || globalThis.localStorage;
    storage.setItem(NATIVE_EXPLORATION_READY_KEY, JSON.stringify({
      schema: 1, generation: current.generation, token: current.token,
      moduleUrl: current.moduleUrl, readyAt
    }));
    return true;
  } catch (_) { return false; }
}