@feralfile/cli 2.10.1 → 3.0.0

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.
@@ -11,8 +11,9 @@ const device_normalize_1 = require("./device-normalize");
11
11
  * so a stale row that happens to share the same IP cannot shadow the correct
12
12
  * entry. Requires the stored entry to have been written with an id field.
13
13
  * 2. Exact URL match — normal case after ID (same host format, no migration).
14
- * 3. mDNS hostname component match — both URLs are parsed and only the hostname
15
- * part is compared (same .local name, different underlying IP or port).
14
+ * 3. Hostname component match — both URLs are parsed and the hostname compared.
15
+ * For a `.local` name, port and protocol are ignored (same device, stored a
16
+ * different way); for any other host the port must match too.
16
17
  * 4a. Discovered IP → stored IP: the mDNS discovery reports a resolved IP that
17
18
  * matches the IP in a stored entry. Bridges .local ↔ stored-IP migration for
18
19
  * pre-id configs with no stored id.
@@ -41,18 +42,28 @@ function findExistingDeviceEntry(existingDevices, newHost, discoveredName, disco
41
42
  if (byHost) {
42
43
  return byHost;
43
44
  }
44
- // 3. mDNS hostname component match (ignores port and protocol differences)
45
- let newHostname = '';
45
+ // 3. Hostname component match. A `.local` name is the device itself, so port
46
+ // and protocol differences are ignored. Any other host (an IP, a tunnel or
47
+ // port-forwarding front) can serve several devices on different ports, so
48
+ // the port must match as well: matching on the address alone made
49
+ // `device add --host <same-ip>:<other-port> --name Kitchen` overwrite the
50
+ // existing device while reporting "Added device: Kitchen".
51
+ let newUrl = null;
46
52
  try {
47
- newHostname = new URL(newHost).hostname;
53
+ newUrl = new URL(newHost);
48
54
  }
49
55
  catch {
50
56
  // not a valid URL — skip hostname matching
51
57
  }
52
- if (newHostname) {
58
+ if (newUrl?.hostname) {
59
+ const candidate = newUrl;
53
60
  const byHostname = existingDevices.find((d) => {
54
61
  try {
55
- return new URL(d.host || '').hostname === newHostname;
62
+ const stored = new URL(d.host || '');
63
+ if (stored.hostname !== candidate.hostname) {
64
+ return false;
65
+ }
66
+ return (candidate.hostname.toLowerCase().endsWith('.local') || stored.port === candidate.port);
56
67
  }
57
68
  catch {
58
69
  return false;
@@ -76,8 +87,12 @@ function findExistingDeviceEntry(existingDevices, newHost, discoveredName, disco
76
87
  return false; // different physical devices — do not conflate
77
88
  }
78
89
  try {
79
- const storedIp = stripBrackets(new URL(d.host || '').hostname);
80
- return discoveredAddresses.includes(storedIp);
90
+ const stored = new URL(d.host || '');
91
+ const storedIp = stripBrackets(stored.hostname);
92
+ // Same port rule as steps 3 and 4b: an address match on another port is
93
+ // another device. (A matching stable id already returned in step 1.)
94
+ const samePort = stored.port === (newUrl?.port ?? stored.port);
95
+ return discoveredAddresses.includes(storedIp) && samePort;
81
96
  }
82
97
  catch {
83
98
  return false;
@@ -91,9 +106,19 @@ function findExistingDeviceEntry(existingDevices, newHost, discoveredName, disco
91
106
  // and a stored entry has that IP in its stored addresses list (populated from
92
107
  // prior mDNS discoveries). This bridges --host <ip/ipv6> → existing .local entry.
93
108
  // Strip IPv6 brackets before comparing (Node URL.hostname returns '[fe80::1]').
94
- const rawHostname = stripBrackets(newHostname);
109
+ // The port must match the stored entry's port, for the same reason as step 3:
110
+ // another port at a remembered address is another device, not this one.
111
+ const rawHostname = stripBrackets(newUrl?.hostname ?? '');
95
112
  if (rawHostname && (/^[0-9.]+$/.test(rawHostname) || rawHostname.includes(':'))) {
96
- const byStoredAddress = existingDevices.find((d) => d.addresses?.includes(rawHostname));
113
+ const samePort = (stored) => {
114
+ try {
115
+ return new URL(stored || '').port === newUrl?.port;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ };
121
+ const byStoredAddress = existingDevices.find((d) => d.addresses?.includes(rawHostname) && samePort(d.host));
97
122
  if (byStoredAddress) {
98
123
  return byStoredAddress;
99
124
  }
@@ -13,12 +13,12 @@ exports.parseFeralFileArtworkId = parseFeralFileArtworkId;
13
13
  const http_1 = require("./http");
14
14
  const user_agent_1 = require("./user-agent");
15
15
  /**
16
- * resolveFeralFileArtwork resolves a public Feral File artwork id into indexer
16
+ * resolveFeralFileArtwork resolves a public Feral File artwork id into token
17
17
  * token coordinates using the self-describing artwork API response.
18
18
  *
19
19
  * @param artworkId - Public artwork id from Feral File.
20
20
  * @param fetchImpl - Fetch implementation, injected by tests.
21
- * @returns Token coordinates suitable for ff-indexer-v2 lookup.
21
+ * @returns Token coordinates for the configured token source.
22
22
  */
23
23
  async function resolveFeralFileArtwork(artworkId, fetchImpl = http_1.defaultDeadlineFetch) {
24
24
  const normalizedArtworkId = artworkId.trim();