@sanvika/geolocation 0.3.0 → 0.4.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 +1 -1
- package/src/client/index.js +116 -32
- package/src/server/serviceClient.js +21 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanvika/geolocation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Sanvika ecosystem geolocation SDK — fully centralized via geolocation.sanvikaproduction.com. IP lookup, reverse/forward geocoding, Places autocomplete + details, Maps JS loader. Zero Google API keys in consumer projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/client/index.js
CHANGED
|
@@ -5,10 +5,114 @@
|
|
|
5
5
|
// Env vars (read from process.env in Next.js / similar):
|
|
6
6
|
// NEXT_PUBLIC_GEOLOCATION_URL=https://geolocation.sanvikaproduction.com
|
|
7
7
|
// NEXT_PUBLIC_GEOLOCATION_CLIENT_ID=fapk
|
|
8
|
+
//
|
|
9
|
+
// Consumer projects NO LONGER need proxy routes for geolocation.
|
|
10
|
+
// All calls go directly to geolocation.sanvikaproduction.com via public clientId auth.
|
|
11
|
+
// Only getUserLocation / updateUserLocation still need FAPK routes (they use SA SSO auth).
|
|
8
12
|
|
|
9
13
|
import { createLogger } from "@sanvika/logger";
|
|
10
14
|
import { setWithTTL, getWithTTL, TTL } from "./localStorageTTL.js";
|
|
11
15
|
|
|
16
|
+
// ─── Direct service helpers (no consumer proxy routes needed) ────────────────
|
|
17
|
+
|
|
18
|
+
function _geoUrl() {
|
|
19
|
+
return (typeof process !== "undefined" && process.env?.NEXT_PUBLIC_GEOLOCATION_URL)
|
|
20
|
+
|| "https://geolocation.sanvikaproduction.com";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _clientId() {
|
|
24
|
+
return (typeof process !== "undefined" && process.env?.NEXT_PUBLIC_GEOLOCATION_CLIENT_ID) || "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function _servicePost(path, body) {
|
|
28
|
+
const clientId = _clientId();
|
|
29
|
+
const qs = clientId ? `?clientId=${encodeURIComponent(clientId)}` : "";
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(`${_geoUrl()}${path}${qs}`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
body: JSON.stringify(body),
|
|
35
|
+
});
|
|
36
|
+
return res.ok ? res.json() : { success: false };
|
|
37
|
+
} catch {
|
|
38
|
+
return { success: false };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Reverse geocode lat/lng → address parts via geolocation service directly.
|
|
44
|
+
* Replaces fetch("/api/location/getLocationDetails") in consumer projects.
|
|
45
|
+
*/
|
|
46
|
+
export async function reverseGeocode(latitude, longitude, opts = {}) {
|
|
47
|
+
const r = await _servicePost("/api/v1/geocode/reverse", { lat: latitude, lng: longitude });
|
|
48
|
+
if (!r.success || !r.found) return null;
|
|
49
|
+
return {
|
|
50
|
+
country: r.country || "",
|
|
51
|
+
state: r.normalizedState || r.state || "",
|
|
52
|
+
city: r.city || "",
|
|
53
|
+
nearbyLandmark: r.sublocality || "",
|
|
54
|
+
postalCode: r.postalCode || "",
|
|
55
|
+
fullAddress: r.fullAddress || "",
|
|
56
|
+
latitude: Number(latitude),
|
|
57
|
+
longitude: Number(longitude),
|
|
58
|
+
geoLocation: { type: "Point", coordinates: [Number(longitude), Number(latitude)] },
|
|
59
|
+
placeId: r.placeId || "",
|
|
60
|
+
source: opts.source || "reverse_geocode",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Forward geocode address → lat/lng via geolocation service directly.
|
|
66
|
+
* Replaces fetch("/api/location/getCoordinates") in consumer projects.
|
|
67
|
+
*/
|
|
68
|
+
export async function forwardGeocode(address, { country = "IN" } = {}) {
|
|
69
|
+
return _servicePost("/api/v1/geocode/forward", { address, country });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* IP-based location lookup via geolocation service directly from browser.
|
|
74
|
+
* Replaces fetch("/api/location/getIpLocation") in consumer projects.
|
|
75
|
+
* Browser's real IP is detected server-side by the geolocation service.
|
|
76
|
+
*/
|
|
77
|
+
export async function getIpLocation() {
|
|
78
|
+
try {
|
|
79
|
+
const clientId = _clientId();
|
|
80
|
+
const qs = clientId ? `?clientId=${encodeURIComponent(clientId)}` : "";
|
|
81
|
+
const res = await fetch(`${_geoUrl()}/api/v1/locate${qs}`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({}),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) return null;
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
if (!data.success || !data.location) return null;
|
|
89
|
+
const l = data.location;
|
|
90
|
+
return { latitude: l.lat ?? null, longitude: l.lng ?? null, city: l.city || "Unknown", state: l.region || "", country: l.country || "India", source: "ip" };
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Places autocomplete via geolocation service directly.
|
|
98
|
+
* Replaces fetch("/api/location/places/autocomplete") in consumer projects.
|
|
99
|
+
*/
|
|
100
|
+
export async function placesAutocomplete(input, { country = "IN", types = "geocode", language = "en", sessionToken } = {}) {
|
|
101
|
+
if (!input || input.trim().length < 2) return { success: true, predictions: [] };
|
|
102
|
+
return _servicePost("/api/v1/places/autocomplete", { input: input.trim(), country, types, language, sessionToken });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Place details by placeId via geolocation service directly.
|
|
107
|
+
* Replaces fetch("/api/location/places/details") in consumer projects.
|
|
108
|
+
*/
|
|
109
|
+
export async function placesDetails(placeId, { language = "en", sessionToken } = {}) {
|
|
110
|
+
if (!placeId) return { success: false };
|
|
111
|
+
return _servicePost("/api/v1/places/details", { placeId, language, sessionToken });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
12
116
|
export {
|
|
13
117
|
loadGoogleMapsScript,
|
|
14
118
|
getMapsConfig,
|
|
@@ -35,17 +139,11 @@ async function tryFallbacks(options) {
|
|
|
35
139
|
|
|
36
140
|
if (options.useIpFallback) {
|
|
37
141
|
try {
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
142
|
+
// Call geolocation service directly — no FAPK proxy needed
|
|
143
|
+
const ipLoc = await getIpLocation();
|
|
144
|
+
if (ipLoc?.latitude && ipLoc?.longitude) {
|
|
41
145
|
logger.info("Using IP fallback for location");
|
|
42
|
-
return {
|
|
43
|
-
latitude: data.location.latitude,
|
|
44
|
-
longitude: data.location.longitude,
|
|
45
|
-
accuracy: 20000,
|
|
46
|
-
source: "ip",
|
|
47
|
-
timestamp: Date.now(),
|
|
48
|
-
};
|
|
146
|
+
return { latitude: ipLoc.latitude, longitude: ipLoc.longitude, accuracy: 20000, source: "ip", timestamp: Date.now() };
|
|
49
147
|
}
|
|
50
148
|
} catch { /* silent */ }
|
|
51
149
|
}
|
|
@@ -116,19 +214,10 @@ export async function getFullLocationDetails(options = {}) {
|
|
|
116
214
|
const locationData = await getLocationPromise(options);
|
|
117
215
|
if (!locationData.latitude || !locationData.longitude) return locationData;
|
|
118
216
|
|
|
119
|
-
const detailsUrl = options.detailsUrl || "/api/location/getLocationDetails";
|
|
120
217
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
body: JSON.stringify({
|
|
125
|
-
latitude: locationData.latitude,
|
|
126
|
-
longitude: locationData.longitude,
|
|
127
|
-
source: locationData.source,
|
|
128
|
-
}),
|
|
129
|
-
});
|
|
130
|
-
const data = await res.json();
|
|
131
|
-
if (data.success) return { ...data.location, source: locationData.source, timestamp: locationData.timestamp };
|
|
218
|
+
// Call geolocation service directly — no FAPK proxy needed
|
|
219
|
+
const details = await reverseGeocode(locationData.latitude, locationData.longitude, { source: locationData.source });
|
|
220
|
+
if (details) return { ...details, source: locationData.source, timestamp: locationData.timestamp };
|
|
132
221
|
} catch (error) {
|
|
133
222
|
logger.error("Error fetching location details", error);
|
|
134
223
|
}
|
|
@@ -167,18 +256,13 @@ export function getCachedLocation() {
|
|
|
167
256
|
return getWithTTL(LOCATION_CACHE_KEY);
|
|
168
257
|
}
|
|
169
258
|
|
|
170
|
-
export async function getCityFromCoordinates(latitude, longitude
|
|
259
|
+
export async function getCityFromCoordinates(latitude, longitude) {
|
|
171
260
|
const cached = getCachedLocation();
|
|
172
|
-
if (cached?.city) return cached.city;
|
|
173
|
-
const detailsUrl = options.detailsUrl || "/api/location/getLocationDetails";
|
|
261
|
+
if (cached?.address?.city) return cached.address.city;
|
|
174
262
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
body: JSON.stringify({ latitude, longitude, source: "city_fetch" }),
|
|
179
|
-
});
|
|
180
|
-
const data = await res.json();
|
|
181
|
-
if (data.success && data.location?.city) return data.location.city;
|
|
263
|
+
// Call geolocation service directly — no FAPK proxy needed
|
|
264
|
+
const details = await reverseGeocode(latitude, longitude);
|
|
265
|
+
if (details?.city) return details.city;
|
|
182
266
|
} catch (error) {
|
|
183
267
|
logger.error("Error getting city from coordinates:", error);
|
|
184
268
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// packages/sanvika-geolocation-sdk/src/server/serviceClient.js
|
|
2
2
|
// HTTP client for geolocation.sanvikaproduction.com — used by all server-side helpers.
|
|
3
|
-
//
|
|
3
|
+
// Universal S2S Auth Pattern (ecosystem rule §21):
|
|
4
|
+
// PROJECT_NAME — clientId
|
|
5
|
+
// SANVIKA_SERVICE_KEY — Tier 1 trust
|
|
6
|
+
// GEOLOCATION_URL (or GEO_URL) — service endpoint
|
|
7
|
+
// GEOLOCATION_CLIENT_SECRET — (optional, Tier 2 fallback for 3rd-party)
|
|
4
8
|
|
|
5
9
|
const DEFAULT_TIMEOUT_MS = 6000;
|
|
6
10
|
|
|
@@ -14,15 +18,21 @@ function readEnv(key) {
|
|
|
14
18
|
|
|
15
19
|
export class GeolocationServiceClient {
|
|
16
20
|
#url;
|
|
21
|
+
#clientId;
|
|
22
|
+
#serviceKey;
|
|
17
23
|
#secret;
|
|
18
24
|
|
|
19
|
-
constructor({ url, secret } = {}) {
|
|
20
|
-
const finalUrl = url || readEnv("GEOLOCATION_URL");
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
constructor({ url, clientId, serviceKey, secret } = {}) {
|
|
26
|
+
const finalUrl = url || readEnv("GEOLOCATION_URL") || readEnv("GEO_URL");
|
|
27
|
+
const finalClientId = clientId || readEnv("PROJECT_NAME") || readEnv("GEOLOCATION_CLIENT_ID");
|
|
28
|
+
const finalServiceKey = serviceKey || readEnv("SANVIKA_SERVICE_KEY") || "";
|
|
29
|
+
const finalSecret = secret || readEnv("GEOLOCATION_CLIENT_SECRET") || ""; // Tier 2 fallback (optional)
|
|
30
|
+
if (!finalUrl || !finalClientId) {
|
|
31
|
+
throw new Error("@sanvika/geolocation/server: GEOLOCATION_URL and PROJECT_NAME are required");
|
|
24
32
|
}
|
|
25
33
|
this.#url = finalUrl.replace(/\/$/, "");
|
|
34
|
+
this.#clientId = finalClientId;
|
|
35
|
+
this.#serviceKey = finalServiceKey;
|
|
26
36
|
this.#secret = finalSecret;
|
|
27
37
|
}
|
|
28
38
|
|
|
@@ -35,7 +45,11 @@ export class GeolocationServiceClient {
|
|
|
35
45
|
const qs = query ? "?" + new URLSearchParams(query).toString() : "";
|
|
36
46
|
const init = {
|
|
37
47
|
method,
|
|
38
|
-
headers: {
|
|
48
|
+
headers: {
|
|
49
|
+
"x-client-id": this.#clientId,
|
|
50
|
+
"x-service-key": this.#serviceKey,
|
|
51
|
+
"x-client-secret": this.#secret,
|
|
52
|
+
},
|
|
39
53
|
signal: controller.signal,
|
|
40
54
|
};
|
|
41
55
|
if (body !== undefined) {
|