@sanvika/geolocation 0.1.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.
- package/package.json +30 -0
- package/src/client/index.js +181 -0
- package/src/client/localStorageTTL.js +33 -0
- package/src/index.js +6 -0
- package/src/server/index.js +91 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sanvika/geolocation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sanvika ecosystem geolocation SDK — server-side IP lookup (ip-api.com) + client-side browser geolocation with cache/IP fallback",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js",
|
|
10
|
+
"./server": "./src/server/index.js",
|
|
11
|
+
"./client": "./src/client/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@sanvika/logger": ">=0.1"
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["sanvika", "geolocation", "ip", "location", "india"],
|
|
21
|
+
"author": "Sanvika Production",
|
|
22
|
+
"license": "UNLICENSED",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/sanvikaproduction/sanvika-geolocation.git"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// packages/sanvika-geolocation-sdk/src/client/index.js
|
|
2
|
+
// Client-side (browser) geolocation with HTML5 GPS + cache/IP fallback
|
|
3
|
+
// Usage: import { getLocationPromise, getFullLocationDetails } from "@sanvika/geolocation/client"
|
|
4
|
+
|
|
5
|
+
import { createLogger } from "@sanvika/logger";
|
|
6
|
+
import { setWithTTL, getWithTTL, TTL } from "./localStorageTTL.js";
|
|
7
|
+
|
|
8
|
+
const logger = createLogger({ namespace: "SanvikaGeolocation" });
|
|
9
|
+
const LOCATION_CACHE_KEY = "sanvika_location_cache";
|
|
10
|
+
|
|
11
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
async function tryFallbacks(options) {
|
|
14
|
+
if (options.useCachedFallback) {
|
|
15
|
+
const cached = getWithTTL(LOCATION_CACHE_KEY);
|
|
16
|
+
if (cached?.coords) {
|
|
17
|
+
logger.info("Using cached location fallback");
|
|
18
|
+
return {
|
|
19
|
+
latitude: cached.coords.latitude,
|
|
20
|
+
longitude: cached.coords.longitude,
|
|
21
|
+
accuracy: cached.coords.accuracy || 10000,
|
|
22
|
+
source: "cached",
|
|
23
|
+
timestamp: cached.timestamp,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.useIpFallback) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch("/api/location/getIpLocation");
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
if (data.success && data.location) {
|
|
33
|
+
logger.info("Using IP fallback for location");
|
|
34
|
+
return {
|
|
35
|
+
latitude: data.location.latitude,
|
|
36
|
+
longitude: data.location.longitude,
|
|
37
|
+
accuracy: 20000,
|
|
38
|
+
source: "ip",
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
} catch { /* silent — will throw below */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new Error("All location methods failed");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Main exports ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export function getLocationPromise(options = {}) {
|
|
51
|
+
const finalOptions = {
|
|
52
|
+
enableHighAccuracy: true,
|
|
53
|
+
timeout: 10000,
|
|
54
|
+
maximumAge: 0,
|
|
55
|
+
useIpFallback: true,
|
|
56
|
+
useCachedFallback: true,
|
|
57
|
+
...options,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
if (!navigator.geolocation) {
|
|
62
|
+
logger.warn("Geolocation API not supported — using fallback");
|
|
63
|
+
tryFallbacks(finalOptions).then(resolve).catch(reject);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
navigator.geolocation.getCurrentPosition(
|
|
68
|
+
(position) => {
|
|
69
|
+
if (finalOptions.useCachedFallback) {
|
|
70
|
+
setWithTTL(
|
|
71
|
+
LOCATION_CACHE_KEY,
|
|
72
|
+
{
|
|
73
|
+
coords: {
|
|
74
|
+
latitude: position.coords.latitude,
|
|
75
|
+
longitude: position.coords.longitude,
|
|
76
|
+
accuracy: position.coords.accuracy,
|
|
77
|
+
},
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
source: "browser_geolocation",
|
|
80
|
+
},
|
|
81
|
+
TTL.ONE_HOUR
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
resolve({
|
|
85
|
+
latitude: position.coords.latitude,
|
|
86
|
+
longitude: position.coords.longitude,
|
|
87
|
+
accuracy: position.coords.accuracy,
|
|
88
|
+
source: "html5_geo",
|
|
89
|
+
timestamp: position.timestamp || Date.now(),
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
(error) => {
|
|
93
|
+
const code = error.code;
|
|
94
|
+
if (code === 1) logger.warn("GPS permission denied — using fallback");
|
|
95
|
+
else if (code === 2) logger.warn("GPS unavailable — using fallback");
|
|
96
|
+
else if (code === 3) logger.warn("GPS timeout — using fallback");
|
|
97
|
+
else logger.error("GPS error:", error);
|
|
98
|
+
tryFallbacks(finalOptions).then(resolve).catch(reject);
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
enableHighAccuracy: finalOptions.enableHighAccuracy,
|
|
102
|
+
timeout: finalOptions.timeout,
|
|
103
|
+
maximumAge: finalOptions.maximumAge,
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getFullLocationDetails(options = {}) {
|
|
110
|
+
const locationData = await getLocationPromise(options);
|
|
111
|
+
if (!locationData.latitude || !locationData.longitude) return locationData;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch("/api/location/getLocationDetails", {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
latitude: locationData.latitude,
|
|
119
|
+
longitude: locationData.longitude,
|
|
120
|
+
source: locationData.source,
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
const data = await res.json();
|
|
124
|
+
if (data.success) return { ...data.location, source: locationData.source, timestamp: locationData.timestamp };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.error("Error fetching location details", error);
|
|
127
|
+
}
|
|
128
|
+
return locationData;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function cacheLocationData(locationData) {
|
|
132
|
+
try {
|
|
133
|
+
setWithTTL(
|
|
134
|
+
LOCATION_CACHE_KEY,
|
|
135
|
+
{
|
|
136
|
+
coords: {
|
|
137
|
+
latitude: locationData.latitude ?? locationData.coords?.latitude,
|
|
138
|
+
longitude: locationData.longitude ?? locationData.coords?.longitude,
|
|
139
|
+
accuracy: locationData.accuracy ?? locationData.coords?.accuracy,
|
|
140
|
+
},
|
|
141
|
+
address: {
|
|
142
|
+
city: locationData.city,
|
|
143
|
+
state: locationData.state,
|
|
144
|
+
country: locationData.country,
|
|
145
|
+
postalCode: locationData.postalCode,
|
|
146
|
+
fullAddress: locationData.fullAddress,
|
|
147
|
+
},
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
source: locationData.source || "manual",
|
|
150
|
+
},
|
|
151
|
+
TTL.ONE_HOUR
|
|
152
|
+
);
|
|
153
|
+
return true;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getCachedLocation() {
|
|
160
|
+
return getWithTTL(LOCATION_CACHE_KEY);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function getCityFromCoordinates(latitude, longitude) {
|
|
164
|
+
const cached = getCachedLocation();
|
|
165
|
+
if (cached?.city) return cached.city;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch("/api/location/getLocationDetails", {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "Content-Type": "application/json" },
|
|
171
|
+
body: JSON.stringify({ latitude, longitude, source: "city_fetch" }),
|
|
172
|
+
});
|
|
173
|
+
const data = await res.json();
|
|
174
|
+
if (data.success && data.location?.city) return data.location.city;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logger.error("Error getting city from coordinates:", error);
|
|
177
|
+
}
|
|
178
|
+
return "Unknown";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export default getLocationPromise;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// packages/sanvika-geolocation-sdk/src/client/localStorageTTL.js
|
|
2
|
+
// Minimal localStorage TTL helper for client-side geolocation cache
|
|
3
|
+
|
|
4
|
+
export const TTL = {
|
|
5
|
+
ONE_HOUR: 60 * 60 * 1000,
|
|
6
|
+
SIX_HOURS: 6 * 60 * 60 * 1000,
|
|
7
|
+
ONE_DAY: 24 * 60 * 60 * 1000,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function setWithTTL(key, value, ttlMs) {
|
|
11
|
+
try {
|
|
12
|
+
localStorage.setItem(key, JSON.stringify({ value, expiresAt: Date.now() + ttlMs }));
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getWithTTL(key) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = localStorage.getItem(key);
|
|
22
|
+
if (!raw) return null;
|
|
23
|
+
const item = JSON.parse(raw);
|
|
24
|
+
if (Date.now() > item.expiresAt) {
|
|
25
|
+
localStorage.removeItem(key);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return item.value;
|
|
29
|
+
} catch {
|
|
30
|
+
try { localStorage.removeItem(key); } catch { /* ignore */ }
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// packages/sanvika-geolocation-sdk/src/index.js
|
|
2
|
+
// Public entry point for @sanvika/geolocation
|
|
3
|
+
// Server exports: import from "@sanvika/geolocation/server"
|
|
4
|
+
// Client exports: import from "@sanvika/geolocation/client"
|
|
5
|
+
|
|
6
|
+
export { getLocationFromIp, clearLocationCache, isPrivateIp } from "./server/index.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// packages/sanvika-geolocation-sdk/src/server/index.js
|
|
2
|
+
// Server-side IP geolocation via ip-api.com (free, no API key, 45 req/min)
|
|
3
|
+
|
|
4
|
+
import { createLogger } from "@sanvika/logger";
|
|
5
|
+
|
|
6
|
+
const logger = createLogger({ namespace: "SanvikaGeolocation" });
|
|
7
|
+
|
|
8
|
+
const locationCache = new Map();
|
|
9
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
10
|
+
|
|
11
|
+
export function isPrivateIp(ip) {
|
|
12
|
+
const privateRanges = [
|
|
13
|
+
/^127\./,
|
|
14
|
+
/^10\./,
|
|
15
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
16
|
+
/^192\.168\./,
|
|
17
|
+
/^::1$/,
|
|
18
|
+
/^fe80:/i,
|
|
19
|
+
/^fc00:/i,
|
|
20
|
+
/^fd00:/i,
|
|
21
|
+
];
|
|
22
|
+
return privateRanges.some((r) => r.test(ip));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getDefaultLocation() {
|
|
26
|
+
return {
|
|
27
|
+
city: "Unknown",
|
|
28
|
+
state: "Unknown",
|
|
29
|
+
country: "India",
|
|
30
|
+
countryCode: "IN",
|
|
31
|
+
zipCode: null,
|
|
32
|
+
latitude: null,
|
|
33
|
+
longitude: null,
|
|
34
|
+
timezone: "Asia/Kolkata",
|
|
35
|
+
source: "default",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getLocationFromIp(ip) {
|
|
40
|
+
if (!ip || ip === "unknown" || isPrivateIp(ip)) {
|
|
41
|
+
return getDefaultLocation();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cached = locationCache.get(ip);
|
|
45
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
46
|
+
return cached.data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(
|
|
51
|
+
`http://ip-api.com/json/${ip}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone`,
|
|
52
|
+
{
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers: { Accept: "application/json" },
|
|
55
|
+
signal: AbortSignal.timeout(3000),
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!response.ok) throw new Error(`ip-api returned ${response.status}`);
|
|
60
|
+
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
|
|
63
|
+
if (data.status === "fail") {
|
|
64
|
+
logger.warn("IP lookup failed", { ip, message: data.message });
|
|
65
|
+
return getDefaultLocation();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const location = {
|
|
69
|
+
city: data.city || "Unknown",
|
|
70
|
+
state: data.regionName || data.region || "Unknown",
|
|
71
|
+
country: data.country || "India",
|
|
72
|
+
countryCode: data.countryCode || "IN",
|
|
73
|
+
zipCode: data.zip || null,
|
|
74
|
+
latitude: data.lat || null,
|
|
75
|
+
longitude: data.lon || null,
|
|
76
|
+
timezone: data.timezone || "Asia/Kolkata",
|
|
77
|
+
source: "ip_api",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
locationCache.set(ip, { data: location, timestamp: Date.now() });
|
|
81
|
+
logger.info("IP location resolved", { city: location.city });
|
|
82
|
+
return location;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
logger.warn("IP geolocation failed", { error: error.message });
|
|
85
|
+
return getDefaultLocation();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function clearLocationCache() {
|
|
90
|
+
locationCache.clear();
|
|
91
|
+
}
|