screeps-connectivity 0.15.0 → 0.15.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 4526471: Namespace the pre-login `/api/authmod` session cache by host **and path**, not hostname alone. Behind `screeps-client-proxy` every backend is wrapped under one host (`localhost/(https://server)`), so hostname-only keys collided and one server's auth capabilities could be shown for another. Also disambiguates private servers that share a hostname on different ports. Matches the path-based namespacing already used for the persistent cache in `ScreepsClient`.
8
+
3
9
  ## 0.15.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screeps-connectivity",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "repository": {
@@ -14,7 +14,15 @@ interface CachedEntry<T> {
14
14
 
15
15
  function sessionKey(suffix: string, url: string): string {
16
16
  try {
17
- return `screeps:${suffix}:${new URL(url).hostname}`
17
+ const parsed = new URL(url)
18
+ // Hostname alone isn't enough: multiple worlds can live under one host via a
19
+ // path (screeps.com vs screeps.com/season), and behind screeps-client-proxy
20
+ // every backend is wrapped under the same host (localhost/(https://server)),
21
+ // so the path must disambiguate them. Same path-based approach as the Cache
22
+ // namespace in ScreepsClient; host (incl. port) also separates private
23
+ // servers sharing a hostname on different ports.
24
+ const path = parsed.pathname.replace(/\/+$/, '')
25
+ return `screeps:${suffix}:${path ? `${parsed.host}${path}` : parsed.host}`
18
26
  } catch {
19
27
  return `screeps:${suffix}:${url}`
20
28
  }
@@ -60,7 +68,7 @@ export async function fetchServerVersion(url: string): Promise<ServerVersion> {
60
68
  /**
61
69
  * Fetch screepsmod-auth capabilities from `/api/authmod` without authentication.
62
70
  * Returns `null` if the server does not run screepsmod-auth.
63
- * The result is cached in `sessionStorage` for 5 minutes (per server hostname).
71
+ * The result is cached in `sessionStorage` for 5 minutes (per server host+path).
64
72
  */
65
73
  export async function fetchAuthModInfo(url: string): Promise<ApiAuthModInfoResponse | null> {
66
74
  const key = sessionKey('authmod', url)
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { fetchAuthModInfo } from '../../src/http/fetchServerVersion.js'
3
+
4
+ // Minimal in-memory sessionStorage stub (the node test env has none).
5
+ class MemorySessionStorage {
6
+ private readonly map = new Map<string, string>()
7
+ getItem(k: string): string | null { return this.map.get(k) ?? null }
8
+ setItem(k: string, v: string): void { this.map.set(k, v) }
9
+ removeItem(k: string): void { this.map.delete(k) }
10
+ clear(): void { this.map.clear() }
11
+ }
12
+
13
+ describe('fetchAuthModInfo — session cache namespacing', () => {
14
+ beforeEach(() => {
15
+ vi.stubGlobal('sessionStorage', new MemorySessionStorage())
16
+ })
17
+ afterEach(() => { vi.unstubAllGlobals() })
18
+
19
+ it('does not collide between two backends wrapped under the same proxy host', async () => {
20
+ // Both URLs share host `localhost:8080`; only the /(backend) path differs —
21
+ // exactly how screeps-client-proxy addresses distinct servers. The cache key
22
+ // must include the path, otherwise the second lookup returns the first's data.
23
+ const worldUrl = 'http://localhost:8080/(https://screeps.com)'
24
+ const privUrl = 'http://localhost:8080/(http://my-private-server:21025)'
25
+ // Exact URL → payload map (no substring host matching).
26
+ const payloads: Record<string, string> = {
27
+ [`${worldUrl}/api/authmod`]: 'official',
28
+ [`${privUrl}/api/authmod`]: 'private',
29
+ }
30
+ const fetchMock = vi.fn().mockImplementation((input: string) => {
31
+ return Promise.resolve(
32
+ new Response(JSON.stringify({ ok: 1, backend: payloads[input] }), {
33
+ headers: { 'content-type': 'application/json' },
34
+ }),
35
+ )
36
+ })
37
+ vi.stubGlobal('fetch', fetchMock)
38
+
39
+ const world = await fetchAuthModInfo(worldUrl)
40
+ const priv = await fetchAuthModInfo(privUrl)
41
+
42
+ expect((world as { backend?: string })?.backend).toBe('official')
43
+ expect((priv as { backend?: string })?.backend).toBe('private')
44
+ // Both were real network fetches — neither served the other's cached entry.
45
+ expect(fetchMock).toHaveBeenCalledTimes(2)
46
+
47
+ // A repeat of the first URL is served from cache (no third fetch).
48
+ const worldAgain = await fetchAuthModInfo(worldUrl)
49
+ expect((worldAgain as { backend?: string })?.backend).toBe('official')
50
+ expect(fetchMock).toHaveBeenCalledTimes(2)
51
+ })
52
+ })