@cocreate/utils 1.44.2 → 1.44.3

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.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Comprehensive Cloud Provider Registry (ESM)
3
+ * Maps infrastructure probes (AWS, GCP, Azure, DigitalOcean, OCI, Hetzner, Vultr, Equinix)
4
+ * to exact geographical coordinates (lat/lng), country, and region IDs.
5
+ */
6
+ export const cloudProviders = [
7
+ // AWS Regions
8
+ { provider: 'aws', region: 'us-east-1', name: 'N. Virginia', country: 'US', lat: 38.9489, lng: -77.4499 },
9
+ { provider: 'aws', region: 'us-east-2', name: 'Ohio', country: 'US', lat: 40.1089, lng: -82.9545 },
10
+ { provider: 'aws', region: 'us-west-1', name: 'N. California', country: 'US', lat: 37.3541, lng: -121.9552 },
11
+ { provider: 'aws', region: 'us-west-2', name: 'Oregon', country: 'US', lat: 45.8399, lng: -119.7006 },
12
+ { provider: 'aws', region: 'ca-central-1', name: 'Montreal', country: 'CA', lat: 45.5017, lng: -73.5673 },
13
+ { provider: 'aws', region: 'eu-central-1', name: 'Frankfurt', country: 'DE', lat: 50.1109, lng: 8.6821 },
14
+ { provider: 'aws', region: 'eu-west-1', name: 'Ireland', country: 'IE', lat: 53.3498, lng: -6.2603 },
15
+ { provider: 'aws', region: 'eu-west-2', name: 'London', country: 'GB', lat: 51.5074, lng: -0.1278 },
16
+ { provider: 'aws', region: 'eu-west-3', name: 'Paris', country: 'FR', lat: 48.8566, lng: 2.3522 },
17
+ { provider: 'aws', region: 'eu-north-1', name: 'Stockholm', country: 'SE', lat: 59.3293, lng: 18.0686 },
18
+ { provider: 'aws', region: 'eu-south-1', name: 'Milan', country: 'IT', lat: 45.4642, lng: 9.1900 },
19
+ { provider: 'aws', region: 'ap-northeast-1', name: 'Tokyo', country: 'JP', lat: 35.6762, lng: 139.6503 },
20
+ { provider: 'aws', region: 'ap-northeast-2', name: 'Seoul', country: 'KR', lat: 37.5665, lng: 126.9780 },
21
+ { provider: 'aws', region: 'ap-northeast-3', name: 'Osaka', country: 'JP', lat: 34.6937, lng: 135.5023 },
22
+ { provider: 'aws', region: 'ap-southeast-1', name: 'Singapore', country: 'SG', lat: 1.3521, lng: 103.8198 },
23
+ { provider: 'aws', region: 'ap-southeast-2', name: 'Sydney', country: 'AU', lat: -33.8688, lng: 151.2093 },
24
+ { provider: 'aws', region: 'ap-south-1', name: 'Mumbai', country: 'IN', lat: 19.0760, lng: 72.8777 },
25
+ { provider: 'aws', region: 'sa-east-1', name: 'São Paulo', country: 'BR', lat: -23.5505, lng: -46.6333 },
26
+ { provider: 'aws', region: 'af-south-1', name: 'Cape Town', country: 'ZA', lat: -33.9249, lng: 18.4241 },
27
+ { provider: 'aws', region: 'me-south-1', name: 'Bahrain', country: 'BH', lat: 26.0667, lng: 50.5577 },
28
+
29
+ // GCP Regions
30
+ { provider: 'gcp', region: 'us-central1', name: 'Iowa', country: 'US', lat: 41.2619, lng: -95.8608 },
31
+ { provider: 'gcp', region: 'us-east1', name: 'South Carolina', country: 'US', lat: 33.1960, lng: -80.0131 },
32
+ { provider: 'gcp', region: 'us-east4', name: 'N. Virginia', country: 'US', lat: 38.9489, lng: -77.4499 },
33
+ { provider: 'gcp', region: 'us-west1', name: 'Oregon', country: 'US', lat: 45.6018, lng: -121.1842 },
34
+ { provider: 'gcp', region: 'us-west2', name: 'Los Angeles', country: 'US', lat: 34.0522, lng: -118.2437 },
35
+ { provider: 'gcp', region: 'northamerica-northeast1', name: 'Montreal', country: 'CA', lat: 45.5017, lng: -73.5673 },
36
+ { provider: 'gcp', region: 'southamerica-east1', name: 'São Paulo', country: 'BR', lat: -23.5505, lng: -46.6333 },
37
+ { provider: 'gcp', region: 'europe-west1', name: 'Belgium', country: 'BE', lat: 50.4541, lng: 3.8258 },
38
+ { provider: 'gcp', region: 'europe-west2', name: 'London', country: 'GB', lat: 51.5074, lng: -0.1278 },
39
+ { provider: 'gcp', region: 'europe-west3', name: 'Frankfurt', country: 'DE', lat: 50.1109, lng: 8.6821 },
40
+ { provider: 'gcp', region: 'europe-west4', name: 'Eemshaven', country: 'NL', lat: 53.4373, lng: 6.8335 },
41
+ { provider: 'gcp', region: 'asia-east1', name: 'Taiwan', country: 'TW', lat: 24.0518, lng: 120.5161 },
42
+ { provider: 'gcp', region: 'asia-east2', name: 'Hong Kong', country: 'HK', lat: 22.3193, lng: 114.1694 },
43
+ { provider: 'gcp', region: 'asia-northeast1', name: 'Tokyo', country: 'JP', lat: 35.6762, lng: 139.6503 },
44
+ { provider: 'gcp', region: 'asia-northeast2', name: 'Osaka', country: 'JP', lat: 34.6937, lng: 135.5023 },
45
+ { provider: 'gcp', region: 'asia-southeast1', name: 'Singapore', country: 'SG', lat: 1.3521, lng: 103.8198 },
46
+ { provider: 'gcp', region: 'asia-south1', name: 'Mumbai', country: 'IN', lat: 19.0760, lng: 72.8777 },
47
+ { provider: 'gcp', region: 'australia-southeast1', name: 'Sydney', country: 'AU', lat: -33.8688, lng: 151.2093 },
48
+
49
+ // Azure Regions
50
+ { provider: 'azure', region: 'eastus', name: 'Virginia', country: 'US', lat: 37.3719, lng: -79.8164 },
51
+ { provider: 'azure', region: 'eastus2', name: 'Virginia 2', country: 'US', lat: 36.6681, lng: -78.3881 },
52
+ { provider: 'azure', region: 'westus', name: 'California', country: 'US', lat: 37.7833, lng: -122.4167 },
53
+ { provider: 'azure', region: 'westus2', name: 'Washington', country: 'US', lat: 47.2330, lng: -119.8520 },
54
+ { provider: 'azure', region: 'centralus', name: 'Iowa', country: 'US', lat: 41.5908, lng: -93.6208 },
55
+ { provider: 'azure', region: 'canadacentral', name: 'Toronto', country: 'CA', lat: 43.6532, lng: -79.3832 },
56
+ { provider: 'azure', region: 'brazilsouth', name: 'São Paulo', country: 'BR', lat: -23.5505, lng: -46.6333 },
57
+ { provider: 'azure', region: 'northeurope', name: 'Ireland', country: 'IE', lat: 53.3498, lng: -6.2603 },
58
+ { provider: 'azure', region: 'westeurope', name: 'Netherlands', country: 'NL', lat: 52.3676, lng: 4.9041 },
59
+ { provider: 'azure', region: 'ukwest', name: 'Cardiff', country: 'GB', lat: 51.4815, lng: -3.1791 },
60
+ { provider: 'azure', region: 'ukeast', name: 'London', country: 'GB', lat: 51.5074, lng: -0.1278 },
61
+ { provider: 'azure', region: 'francecentral', name: 'Paris', country: 'FR', lat: 48.8566, lng: 2.3522 },
62
+ { provider: 'azure', region: 'germanywestcentral', name: 'Frankfurt', country: 'DE', lat: 50.1109, lng: 8.6821 },
63
+ { provider: 'azure', region: 'japaneast', name: 'Tokyo', country: 'JP', lat: 35.6762, lng: 139.6503 },
64
+ { provider: 'azure', region: 'japanwest', name: 'Osaka', country: 'JP', lat: 34.6937, lng: 135.5023 },
65
+ { provider: 'azure', region: 'southeastasia', name: 'Singapore', country: 'SG', lat: 1.3521, lng: 103.8198 },
66
+ { provider: 'azure', region: 'australiaeast', name: 'New South Wales', country: 'AU', lat: -33.8688, lng: 151.2093 },
67
+ { provider: 'azure', region: 'centralindia', name: 'Pune', country: 'IN', lat: 18.5204, lng: 73.8567 },
68
+
69
+ // DigitalOcean Regions
70
+ { provider: 'digitalocean', region: 'nyc1', name: 'New York 1', country: 'US', lat: 40.7128, lng: -74.0060 },
71
+ { provider: 'digitalocean', region: 'nyc3', name: 'New York 3', country: 'US', lat: 40.7128, lng: -74.0060 },
72
+ { provider: 'digitalocean', region: 'sfo3', name: 'San Francisco', country: 'US', lat: 37.7749, lng: -122.4194 },
73
+ { provider: 'digitalocean', region: 'ams3', name: 'Amsterdam', country: 'NL', lat: 52.3676, lng: 4.9041 },
74
+ { provider: 'digitalocean', region: 'sgp1', name: 'Singapore', country: 'SG', lat: 1.3521, lng: 103.8198 },
75
+ { provider: 'digitalocean', region: 'lon1', name: 'London', country: 'GB', lat: 51.5074, lng: -0.1278 },
76
+ { provider: 'digitalocean', region: 'fra1', name: 'Frankfurt', country: 'DE', lat: 50.1109, lng: 8.6821 },
77
+ { provider: 'digitalocean', region: 'tor1', name: 'Toronto', country: 'CA', lat: 43.6532, lng: -79.3832 },
78
+ { provider: 'digitalocean', region: 'blr1', name: 'Bangalore', country: 'IN', lat: 12.9716, lng: 77.5946 },
79
+ { provider: 'digitalocean', region: 'syd1', name: 'Sydney', country: 'AU', lat: -33.8688, lng: 151.2093 },
80
+
81
+ // OCI Regions
82
+ { provider: 'oci', region: 'us-ashburn-1', name: 'Ashburn', country: 'US', lat: 39.0438, lng: -77.4874 },
83
+ { provider: 'oci', region: 'us-phoenix-1', name: 'Phoenix', country: 'US', lat: 33.4484, lng: -112.0740 },
84
+ { provider: 'oci', region: 'us-sanjose-1', name: 'San Jose', country: 'US', lat: 37.3382, lng: -121.8863 },
85
+ { provider: 'oci', region: 'ca-toronto-1', name: 'Toronto', country: 'CA', lat: 43.6532, lng: -79.3832 },
86
+ { provider: 'oci', region: 'sa-saopaulo-1', name: 'São Paulo', country: 'BR', lat: -23.5505, lng: -46.6333 },
87
+ { provider: 'oci', region: 'eu-frankfurt-1', name: 'Frankfurt', country: 'DE', lat: 50.1109, lng: 8.6821 },
88
+ { provider: 'oci', region: 'eu-zurich-1', name: 'Zurich', country: 'CH', lat: 47.3769, lng: 8.5417 },
89
+ { provider: 'oci', region: 'eu-amsterdam-1', name: 'Amsterdam', country: 'NL', lat: 52.3676, lng: 4.9041 },
90
+ { provider: 'oci', region: 'uk-london-1', name: 'London', country: 'GB', lat: 51.5074, lng: -0.1278 },
91
+ { provider: 'oci', region: 'ap-tokyo-1', name: 'Tokyo', country: 'JP', lat: 35.6762, lng: 139.6503 },
92
+ { provider: 'oci', region: 'ap-osaka-1', name: 'Osaka', country: 'JP', lat: 34.6937, lng: 135.5023 },
93
+ { provider: 'oci', region: 'ap-seoul-1', name: 'Seoul', country: 'KR', lat: 37.5665, lng: 126.9780 },
94
+ { provider: 'oci', region: 'ap-sydney-1', name: 'Sydney', country: 'AU', lat: -33.8688, lng: 151.2093 },
95
+ { provider: 'oci', region: 'ap-mumbai-1', name: 'Mumbai', country: 'IN', lat: 19.0760, lng: 72.8777 },
96
+ { provider: 'oci', region: 'me-jeddah-1', name: 'Jeddah', country: 'SA', lat: 21.5433, lng: 39.1728 },
97
+
98
+ // Hetzner Regions
99
+ { provider: 'hetzner', region: 'fsn1', name: 'Falkenstein', country: 'DE', lat: 50.4779, lng: 12.3713 },
100
+ { provider: 'hetzner', region: 'nbg1', name: 'Nuremberg', country: 'DE', lat: 49.4521, lng: 11.0767 },
101
+ { provider: 'hetzner', region: 'hel1', name: 'Helsinki', country: 'FI', lat: 60.1699, lng: 24.9384 },
102
+ { provider: 'hetzner', region: 'ash', name: 'Ashburn', country: 'US', lat: 39.0438, lng: -77.4874 },
103
+ { provider: 'hetzner', region: 'hil', name: 'Hillsboro', country: 'US', lat: 45.5229, lng: -122.9898 },
104
+ { provider: 'hetzner', region: 'sin', name: 'Singapore', country: 'SG', lat: 1.3521, lng: 103.8198 },
105
+
106
+ // Vultr Regions
107
+ { provider: 'vultr', region: 'ewr', name: 'New Jersey', country: 'US', lat: 40.7128, lng: -74.0060 },
108
+ { provider: 'vultr', region: 'ord', name: 'Chicago', country: 'US', lat: 41.8781, lng: -87.6298 },
109
+ { provider: 'vultr', region: 'dfw', name: 'Dallas', country: 'US', lat: 32.7767, lng: -96.7970 },
110
+ { provider: 'vultr', region: 'sea', name: 'Seattle', country: 'US', lat: 47.6062, lng: -122.3321 },
111
+ { provider: 'vultr', region: 'lax', name: 'Los Angeles', country: 'US', lat: 34.0522, lng: -118.2437 },
112
+ { provider: 'vultr', region: 'atl', name: 'Atlanta', country: 'US', lat: 33.7490, lng: -84.3880 },
113
+ { provider: 'vultr', region: 'mia', name: 'Miami', country: 'US', lat: 25.7617, lng: -80.1918 },
114
+ { provider: 'vultr', region: 'sjc', name: 'Silicon Valley', country: 'US', lat: 37.3382, lng: -121.8863 },
115
+ { provider: 'vultr', region: 'yto', name: 'Toronto', country: 'CA', lat: 43.6532, lng: -79.3832 },
116
+ { provider: 'vultr', region: 'sao', name: 'São Paulo', country: 'BR', lat: -23.5505, lng: -46.6333 },
117
+ { provider: 'vultr', region: 'ams', name: 'Amsterdam', country: 'NL', lat: 52.3676, lng: 4.9041 },
118
+ { provider: 'vultr', region: 'fra', name: 'Frankfurt', country: 'DE', lat: 50.1109, lng: 8.6821 },
119
+ { provider: 'vultr', region: 'lhr', name: 'London', country: 'GB', lat: 51.5074, lng: -0.1278 },
120
+ { provider: 'vultr', region: 'par', name: 'Paris', country: 'FR', lat: 48.8566, lng: 2.3522 },
121
+ { provider: 'vultr', region: 'sgp', name: 'Singapore', country: 'SG', lat: 1.3521, lng: 103.8198 },
122
+ { provider: 'vultr', region: 'nrt', name: 'Tokyo', country: 'JP', lat: 35.6762, lng: 139.6503 },
123
+ { provider: 'vultr', region: 'icn', name: 'Seoul', country: 'KR', lat: 37.5665, lng: 126.9780 },
124
+ { provider: 'vultr', region: 'syd', name: 'Sydney', country: 'AU', lat: -33.8688, lng: 151.2093 },
125
+ { provider: 'vultr', region: 'mel', name: 'Melbourne', country: 'AU', lat: -37.8136, lng: 144.9631 },
126
+ { provider: 'vultr', region: 'blr', name: 'Bangalore', country: 'IN', lat: 12.9716, lng: 77.5946 },
127
+ { provider: 'vultr', region: 'jnb', name: 'Johannesburg', country: 'ZA', lat: -26.2041, lng: 28.0473 },
128
+
129
+ // Equinix Regions
130
+ { provider: 'equinix', region: 'da', name: 'Dallas', country: 'US', lat: 32.7767, lng: -96.7970 },
131
+ { provider: 'equinix', region: 'ny', name: 'New York', country: 'US', lat: 40.7128, lng: -74.0060 },
132
+ { provider: 'equinix', region: 'sv', name: 'Silicon Valley', country: 'US', lat: 37.3382, lng: -121.8863 },
133
+ { provider: 'equinix', region: 'ch', name: 'Chicago', country: 'US', lat: 41.8781, lng: -87.6298 },
134
+ { provider: 'equinix', region: 'la', name: 'Los Angeles', country: 'US', lat: 34.0522, lng: -118.2437 },
135
+ { provider: 'equinix', region: 'se', name: 'Seattle', country: 'US', lat: 47.6062, lng: -122.3321 },
136
+ { provider: 'equinix', region: 'at', name: 'Atlanta', country: 'US', lat: 33.7490, lng: -84.3880 },
137
+ { provider: 'equinix', region: 'tr', name: 'Toronto', country: 'CA', lat: 43.6532, lng: -79.3832 },
138
+ { provider: 'equinix', region: 'sp', name: 'São Paulo', country: 'BR', lat: -23.5505, lng: -46.6333 },
139
+ { provider: 'equinix', region: 'fr', name: 'Frankfurt', country: 'DE', lat: 50.1109, lng: 8.6821 },
140
+ { provider: 'equinix', region: 'am', name: 'Amsterdam', country: 'NL', lat: 52.3676, lng: 4.9041 },
141
+ { provider: 'equinix', region: 'ld', name: 'London', country: 'GB', lat: 51.5074, lng: -0.1278 },
142
+ { provider: 'equinix', region: 'pa', name: 'Paris', country: 'FR', lat: 48.8566, lng: 2.3522 },
143
+ { provider: 'equinix', region: 'ma', name: 'Madrid', country: 'ES', lat: 40.4168, lng: -3.7038 },
144
+ { provider: 'equinix', region: 'ty', name: 'Tokyo', country: 'JP', lat: 35.6762, lng: 139.6503 },
145
+ { provider: 'equinix', region: 'sg', name: 'Singapore', country: 'SG', lat: 1.3521, lng: 103.8198 },
146
+ { provider: 'equinix', region: 'hk', name: 'Hong Kong', country: 'HK', lat: 22.3193, lng: 114.1694 },
147
+ { provider: 'equinix', region: 'sy', name: 'Sydney', country: 'AU', lat: -33.8688, lng: 151.2093 }
148
+ ];
149
+
150
+ export default cloudProviders
@@ -1,35 +1,7 @@
1
1
  import { dotNotationToObject } from "./dotNotationToObject.js";
2
2
  import { objectToDotNotation } from "./objectToDotNotation.js";
3
3
  import { getValueFromObject } from "./getValueFromObject.js";
4
-
5
- function isEqualArray(arr1, arr2) {
6
- if (arr1.length !== arr2.length) {
7
- return false;
8
- }
9
- for (let i = 0; i < arr1.length; i++) {
10
- if (!isEqualObject(arr1[i], arr2[i])) {
11
- return false;
12
- }
13
- }
14
- return true;
15
- }
16
-
17
- function isEqualObject(obj1, obj2) {
18
- const keys1 = Object.keys(obj1);
19
- const keys2 = Object.keys(obj2);
20
-
21
- if (keys1.length !== keys2.length) {
22
- return false;
23
- }
24
-
25
- for (const key of keys1) {
26
- if (obj1[key] !== obj2[key]) {
27
- return false;
28
- }
29
- }
30
-
31
- return true;
32
- }
4
+ import { isEqualArray, isEqualObject } from "./dataQuery.js";
33
5
 
34
6
  function dotNotationToObjectUpdate(data, object = {}) {
35
7
  try {
package/src/dataQuery.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getValueFromObject } from "./getValueFromObject.js";
2
2
  import { isValidDate } from "./isValidDate.js";
3
3
 
4
- function isEqualArray(arr1, arr2) {
4
+ export function isEqualArray(arr1, arr2) {
5
5
  if (arr1.length !== arr2.length) {
6
6
  return false;
7
7
  }
@@ -13,7 +13,7 @@ function isEqualArray(arr1, arr2) {
13
13
  return true;
14
14
  }
15
15
 
16
- function isEqualObject(obj1, obj2) {
16
+ export function isEqualObject(obj1, obj2) {
17
17
  const keys1 = Object.keys(obj1);
18
18
  const keys2 = Object.keys(obj2);
19
19
 
@@ -0,0 +1,357 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2026 CoCreate LLC and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ********************************************************************************/
17
+
18
+ import getCountryLocation from './getCountryLocation.js';
19
+ import cloudProviders from './cloudProviders.js';
20
+
21
+ // Strict validation patterns for IPv4 and IPv6 addresses
22
+ const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
23
+ const IPV6_REGEX = /^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/;
24
+
25
+ // Operational defaults and fallback configurations
26
+ const DEFAULT_TIMEOUT_MS = 3000;
27
+ const LOCAL_FALLBACK_IP = "127.0.0.1";
28
+ const SESSION_STORAGE_KEY = "client_info";
29
+
30
+ // Single environment flags
31
+ const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
32
+ const isNode = typeof process !== "undefined" && process.versions && !!process.versions.node;
33
+
34
+ // Single ordered list of rich JSON geolocation providers
35
+ const PROVIDERS = [
36
+ {
37
+ name: "ipapi.co",
38
+ url: "https://ipapi.co/json/",
39
+ parser: (data) => ({
40
+ ip: data.ip,
41
+ city: data.city,
42
+ region: data.region,
43
+ country: data.country_name,
44
+ countryCode: data.country_code,
45
+ latitude: (data.latitude != null && data.latitude !== "") ? parseFloat(data.latitude) : null,
46
+ longitude: (data.longitude != null && data.longitude !== "") ? parseFloat(data.longitude) : null,
47
+ timezone: data.timezone,
48
+ org: data.org || data.asn,
49
+ })
50
+ },
51
+ {
52
+ name: "ip-api.com",
53
+ url: "https://ip-api.com/json/?fields=status,message,country,countryCode,region,regionName,city,lat,lon,timezone,isp,org,query",
54
+ parser: (data) => {
55
+ if (data.status !== "success") return null;
56
+ return {
57
+ ip: data.query,
58
+ city: data.city,
59
+ region: data.regionName,
60
+ country: data.country,
61
+ countryCode: data.countryCode,
62
+ latitude: (data.lat != null && data.lat !== "") ? parseFloat(data.lat) : null,
63
+ longitude: (data.lon != null && data.lon !== "") ? parseFloat(data.lon) : null,
64
+ timezone: data.timezone,
65
+ org: data.org || data.isp,
66
+ };
67
+ }
68
+ },
69
+ {
70
+ name: "ipwho.is",
71
+ url: "https://ipwho.is/",
72
+ parser: (data) => {
73
+ if (!data.success) return null;
74
+ return {
75
+ ip: data.ip,
76
+ city: data.city,
77
+ region: data.region,
78
+ country: data.country,
79
+ countryCode: data.country_code,
80
+ latitude: (data.latitude != null && data.latitude !== "") ? parseFloat(data.latitude) : null,
81
+ longitude: (data.longitude != null && data.longitude !== "") ? parseFloat(data.longitude) : null,
82
+ timezone: data.timezone?.id,
83
+ org: data.connection?.isp || data.connection?.org,
84
+ };
85
+ }
86
+ },
87
+ {
88
+ name: "my-ip.io",
89
+ url: "https://api.my-ip.io/v2/ip.json",
90
+ parser: (data) => {
91
+ if (!data || !data.success) return null;
92
+ return {
93
+ ip: data.ip,
94
+ city: data.city,
95
+ region: data.region,
96
+ country: data.country?.name,
97
+ countryCode: data.country?.code,
98
+ latitude: (data.location?.lat != null && data.location.lat !== "") ? parseFloat(data.location.lat) : null,
99
+ longitude: (data.location?.lon != null && data.location.lon !== "") ? parseFloat(data.location.lon) : null,
100
+ timezone: data.timeZone,
101
+ org: data.asn?.name,
102
+ };
103
+ }
104
+ }
105
+ ];
106
+
107
+ /**
108
+ * Internal helper to perform fetch requests with an AbortSignal timeout.
109
+ */
110
+ async function fetchWithTimeout(url, timeoutMs = DEFAULT_TIMEOUT_MS) {
111
+ const controller = new AbortController();
112
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
113
+
114
+ try {
115
+ const response = await fetch(url, {
116
+ signal: controller.signal,
117
+ headers: { "Accept": "application/json, text/plain, */*" }
118
+ });
119
+ clearTimeout(timeoutId);
120
+ return response;
121
+ } catch (err) {
122
+ clearTimeout(timeoutId);
123
+ throw err;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Internal helper to validate IPv4 and IPv6 strings.
129
+ */
130
+ function isValidIP(ip) {
131
+ if (typeof ip !== "string") return false;
132
+ const cleanIp = ip.trim();
133
+ return IPV4_REGEX.test(cleanIp) || IPV6_REGEX.test(cleanIp);
134
+ }
135
+
136
+ /**
137
+ * Inspects node OS network interfaces to find a valid non-internal local IP.
138
+ */
139
+ function getLocalNodeIP() {
140
+ if (!isNode) return null;
141
+ try {
142
+ const req = typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require;
143
+ const os = req("os");
144
+ const interfaces = os.networkInterfaces();
145
+ for (const interfaceName of Object.keys(interfaces)) {
146
+ for (const iface of interfaces[interfaceName]) {
147
+ if (!iface.internal && iface.family === "IPv4" && iface.address) {
148
+ return iface.address;
149
+ }
150
+ }
151
+ }
152
+ } catch (e) {
153
+ // Fallback gracefully if restricted
154
+ }
155
+ return null;
156
+ }
157
+
158
+ /**
159
+ * Resolves a raw client information object into concrete latitude, longitude, and ISO country code.
160
+ * Optimized path:
161
+ * 1. Direct valid coordinates -> Immediate return with continent added
162
+ * 2. Missing coordinates but region present -> Attempt cloud region match for lat/lon
163
+ * 3. Fallback -> Return getCountryLocation coordinates
164
+ *
165
+ * @param {Object|string} info - Client telemetry or location metadata
166
+ * @returns {{latitude: number, longitude: number, countryCode: string, continent: string}|null} Resolved location object
167
+ */
168
+ export function resolveInfo(info) {
169
+ if (!info || typeof info !== 'object') return null;
170
+
171
+ const { latitude, longitude, countryCode, region } = info;
172
+
173
+ // Fetch location data; use fallback empty object in case of undefined return
174
+ const countryLocation = getCountryLocation(countryCode) || {};
175
+
176
+ // 1. Direct path: Valid numeric coordinates present
177
+ if (latitude !== null && longitude !== null && latitude !== undefined && longitude !== undefined) {
178
+ return {
179
+ latitude,
180
+ longitude,
181
+ countryCode,
182
+ continent: countryLocation.continent || null
183
+ };
184
+ }
185
+
186
+ // 2. Region String Match Path: Try cloudProviders if coordinates are missing
187
+ if (region) {
188
+ const cleanRegion = String(region).trim().toLowerCase();
189
+ const match = cloudProviders.find(cloud =>
190
+ cloud.name && cloud.name.toLowerCase().includes(cleanRegion)
191
+ );
192
+
193
+ if (match) {
194
+ // If the cloud provider match has a specific country, update the continent reference
195
+ const matchCountryCode = match.country || countryCode;
196
+ const matchLocation = match.country ? (getCountryLocation(match.country) || {}) : countryLocation;
197
+
198
+ return {
199
+ latitude: match.lat ?? match.latitude ?? null,
200
+ longitude: match.lng ?? match.longitude ?? null,
201
+ countryCode: matchCountryCode,
202
+ continent: matchLocation.continent || countryLocation.continent || null
203
+ };
204
+ }
205
+ }
206
+
207
+ // 3. Ultimate Fallback: Use getCountryLocation coordinates
208
+ return {
209
+ latitude: countryLocation.latitude ?? countryLocation.lat ?? null,
210
+ longitude: countryLocation.longitude ?? countryLocation.lng ?? null,
211
+ countryCode: countryLocation.countryCode ?? countryLocation.country ?? countryCode,
212
+ continent: countryLocation.continent || null
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Formats data into a normalized client information payload.
218
+ */
219
+ function normalizeInfoPayload(rawData = {}, sourceProvider = "unknown", isPartial = false, isLocal = false) {
220
+ const payload = {
221
+ ip: rawData.ip || LOCAL_FALLBACK_IP,
222
+ city: rawData.city || null,
223
+ region: rawData.region || null,
224
+ country: rawData.country || null,
225
+ countryCode: rawData.countryCode || null,
226
+ latitude: typeof rawData.latitude === "number" && !isNaN(rawData.latitude) ? rawData.latitude : null,
227
+ longitude: typeof rawData.longitude === "number" && !isNaN(rawData.longitude) ? rawData.longitude : null,
228
+ timezone: rawData.timezone || null,
229
+ org: rawData.org || null,
230
+ source: sourceProvider,
231
+ isPartial,
232
+ isLocal,
233
+ timestamp: Date.now()
234
+ };
235
+
236
+ // Enhance and normalize the payload using the external fallback maps
237
+ const resolved = resolveInfo(payload);
238
+
239
+ if (resolved) {
240
+ payload.latitude = resolved.latitude ?? payload.latitude;
241
+ payload.longitude = resolved.longitude ?? payload.longitude;
242
+ payload.continent = resolved.continent || null;
243
+
244
+ if (resolved.countryCode) {
245
+ payload.countryCode = resolved.countryCode;
246
+ }
247
+ } else {
248
+ payload.continent = null;
249
+ }
250
+
251
+ return payload;
252
+ }
253
+
254
+ /**
255
+ * Internal resolver that attempts provider endpoints sequentially.
256
+ */
257
+ async function fetchClientInfo(options = {}) {
258
+ const timeout = options.timeout || DEFAULT_TIMEOUT_MS;
259
+
260
+ // Single iteration loop over ordered JSON providers
261
+ for (const provider of PROVIDERS) {
262
+ try {
263
+ const res = await fetchWithTimeout(provider.url, timeout);
264
+ if (res.ok) {
265
+ const json = await res.json();
266
+ const parsed = provider.parser(json);
267
+
268
+ if (parsed && isValidIP(parsed.ip)) {
269
+ const isPartial = Boolean(provider.isPartial || !parsed.city);
270
+ return normalizeInfoPayload(parsed, provider.name, isPartial, false);
271
+ }
272
+ }
273
+ } catch (err) {
274
+ // Fail silently and try the next provider
275
+ }
276
+ }
277
+
278
+ // Local Interface Fallback if all external APIs fail
279
+ const localNodeIp = getLocalNodeIP();
280
+ const finalIp = localNodeIp || LOCAL_FALLBACK_IP;
281
+
282
+ return normalizeInfoPayload({ ip: finalIp }, localNodeIp ? "node:os" : "fallback", true, true);
283
+ }
284
+
285
+ // Single set of module-level references for active promise and resolved payload
286
+ let cachedInfoPromise = null;
287
+ let cachedInfoPayload = null;
288
+
289
+ /**
290
+ * Single unified function to retrieve client IP, location, and network metadata.
291
+ * Returns the same in-memory object reference across calls.
292
+ *
293
+ * @param {Object} [options]
294
+ * @param {number} [options.timeout=3000] Hard timeout limit per request (ms)
295
+ * @param {boolean} [options.bypassCache=false] Force a fresh network request
296
+ * @returns {Promise<Object>} Standardized location object
297
+ */
298
+ export async function getClientInfo(options = {}) {
299
+ const { bypassCache = false } = options;
300
+
301
+ // 1. Return existing in-memory object reference if available
302
+ if (!bypassCache && cachedInfoPayload) {
303
+ return cachedInfoPayload;
304
+ }
305
+
306
+ // 2. Return active in-flight request promise if currently executing
307
+ if (!bypassCache && cachedInfoPromise) {
308
+ return cachedInfoPromise;
309
+ }
310
+
311
+ // 3. Rehydrate from browser sessionStorage across page refreshes
312
+ if (!bypassCache && isBrowser) {
313
+ try {
314
+ const stored = sessionStorage.getItem(SESSION_STORAGE_KEY);
315
+ if (stored) {
316
+ const parsed = JSON.parse(stored);
317
+ if (parsed && isValidIP(parsed.ip)) {
318
+ cachedInfoPayload = parsed;
319
+ return cachedInfoPayload;
320
+ }
321
+ }
322
+ } catch (e) {
323
+ // Fall back silently
324
+ }
325
+ }
326
+
327
+ // 4. Fetch fresh network data, assign to cachedInfoPayload, and persist to sessionStorage
328
+ cachedInfoPromise = fetchClientInfo(options).then((info) => {
329
+ cachedInfoPayload = info;
330
+ cachedInfoPromise = null;
331
+
332
+ if (isBrowser) {
333
+ try {
334
+ sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(info));
335
+ } catch (e) {
336
+ // Ignore storage errors
337
+ }
338
+ }
339
+ return cachedInfoPayload;
340
+ }).catch((err) => {
341
+ cachedInfoPromise = null;
342
+ throw err;
343
+ });
344
+
345
+ return cachedInfoPromise;
346
+ }
347
+
348
+ // Automatically listen for browser network reconnects to refresh location data
349
+ if (isBrowser) {
350
+ window.addEventListener("online", () => {
351
+ getClientInfo({ bypassCache: true }).catch(() => {
352
+ // Suppress background reconnect error
353
+ });
354
+ });
355
+ }
356
+
357
+ export default getClientInfo;
@@ -0,0 +1,46 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2026 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ ********************************************************************************/
18
+
19
+ let clockOffset = 0;
20
+ const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
21
+
22
+ // Runs EXACTLY ONCE across your entire app load
23
+ if (isBrowser) {
24
+ const t0 = Date.now();
25
+ fetch('/api/ping', { method: 'HEAD' })
26
+ .then((res) => {
27
+ const serverDateStr = res.headers.get('date');
28
+ if (serverDateStr) {
29
+ const t1 = Date.now();
30
+ const tServer = new Date(serverDateStr).getTime();
31
+ const latency = Math.floor((t1 - t0) / 2);
32
+ clockOffset = tServer - (t0 + latency);
33
+ }
34
+ })
35
+ .catch(() => {
36
+ clockOffset = 0;
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Returns the cached time offset in milliseconds.
42
+ * Always 0 on the server (NTP-managed) or fallback.
43
+ */
44
+ export function getClockOffset() {
45
+ return clockOffset;
46
+ }