@callforge/tracking-client 0.0.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/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # @callforge/tracking-client
2
+
3
+ Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @callforge/tracking-client
9
+ # or
10
+ pnpm add @callforge/tracking-client
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Add preload snippet to `<head>` (optional but recommended)
16
+
17
+ For optimal performance on static sites, add this snippet to your HTML `<head>`:
18
+
19
+ ```typescript
20
+ import { getPreloadSnippet } from '@callforge/tracking-client';
21
+
22
+ const snippet = getPreloadSnippet({ categoryId: 'your-category-id' });
23
+ // Add snippet to your HTML <head>
24
+ ```
25
+
26
+ Generated HTML:
27
+ ```html
28
+ <link rel="preconnect" href="https://tracking.callforge.io">
29
+ <script>/* preload script */</script>
30
+ ```
31
+
32
+ ### 2. Initialize and use the client
33
+
34
+ ```typescript
35
+ import { CallForge } from '@callforge/tracking-client';
36
+
37
+ const client = CallForge.init({
38
+ categoryId: 'your-category-id',
39
+ // endpoint: 'https://tracking-dev.callforge.io', // Optional: override for dev
40
+ });
41
+
42
+ // Promise style
43
+ const session = await client.getSession();
44
+ console.log(session.phoneNumber); // "+17705550000" or null
45
+ console.log(session.location); // { city: "Woodstock", state: "Georgia", stateCode: "GA" } or null
46
+
47
+ // Subscription style
48
+ client.onReady((session) => {
49
+ document.getElementById('phone').textContent = session.phoneNumber;
50
+ document.getElementById('city').textContent = session.location?.city;
51
+ });
52
+ ```
53
+
54
+ ## API Reference
55
+
56
+ ### `CallForge.init(config)`
57
+
58
+ Initialize the tracking client.
59
+
60
+ ```typescript
61
+ interface CallForgeConfig {
62
+ categoryId: string; // Required - which number pool to use
63
+ endpoint?: string; // Optional - defaults to 'https://tracking.callforge.io'
64
+ }
65
+ ```
66
+
67
+ ### `client.getSession()`
68
+
69
+ Get tracking session data. Returns cached data if valid, otherwise fetches from API.
70
+
71
+ ```typescript
72
+ interface TrackingSession {
73
+ sessionId: string | null;
74
+ phoneNumber: string | null;
75
+ location: {
76
+ city: string;
77
+ state: string; // Full name: "Georgia"
78
+ stateCode: string; // Abbreviation: "GA"
79
+ } | null;
80
+ }
81
+ ```
82
+
83
+ **Behavior:**
84
+ - Returns `null` values immediately if no `loc_physical_ms` in URL (no API call)
85
+ - Returns cached data if valid (same location, not expired)
86
+ - Fetches fresh data if cache expired or location changed
87
+ - Throws on network errors or API errors
88
+
89
+ ### `client.onReady(callback)`
90
+
91
+ Subscribe to session ready event. Callback is called once session data is available.
92
+
93
+ ```typescript
94
+ client.onReady((session) => {
95
+ // session is the same TrackingSession object
96
+ });
97
+ ```
98
+
99
+ ### `getPreloadSnippet(config)`
100
+
101
+ Generate HTML snippet for preloading tracking data.
102
+
103
+ ```typescript
104
+ import { getPreloadSnippet } from '@callforge/tracking-client';
105
+
106
+ const html = getPreloadSnippet({
107
+ categoryId: 'your-category-id',
108
+ endpoint: 'https://tracking.callforge.io', // Optional
109
+ });
110
+ ```
111
+
112
+ ## Caching Behavior
113
+
114
+ - **Cache key:** `loc_physical_ms` parameter from URL
115
+ - **TTL:** 30 minutes (controlled by server)
116
+ - **Storage:** localStorage (falls back to memory if unavailable)
117
+ - **Invalidation:** Cache clears when location changes
118
+
119
+ ## Error Handling
120
+
121
+ ```typescript
122
+ try {
123
+ const session = await client.getSession();
124
+ if (session.phoneNumber) {
125
+ // Use phone number
126
+ } else {
127
+ // No phone number available for this location
128
+ }
129
+ } catch (err) {
130
+ // Network error or API error
131
+ console.error('Failed to get tracking session:', err);
132
+ }
133
+ ```
134
+
135
+ ## TypeScript
136
+
137
+ Full type definitions are included:
138
+
139
+ ```typescript
140
+ import type {
141
+ CallForgeConfig,
142
+ TrackingSession,
143
+ TrackingLocation,
144
+ ReadyCallback,
145
+ } from '@callforge/tracking-client';
146
+ ```
147
+
148
+ ## Environment URLs
149
+
150
+ | Environment | Endpoint |
151
+ |-------------|----------|
152
+ | Production | `https://tracking.callforge.io` (default) |
153
+ | Staging | `https://tracking-staging.callforge.io` |
154
+ | Dev | `https://tracking-dev.callforge.io` |
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Configuration options for the CallForge tracking client.
3
+ */
4
+ interface CallForgeConfig {
5
+ /** Required - which number pool to use */
6
+ categoryId: string;
7
+ /** Optional - API endpoint. Defaults to 'https://tracking.callforge.io' */
8
+ endpoint?: string;
9
+ }
10
+ /**
11
+ * Location data returned by the tracking API.
12
+ */
13
+ interface TrackingLocation {
14
+ /** City name */
15
+ city: string;
16
+ /** Full state name (e.g., "Georgia") */
17
+ state: string;
18
+ /** State abbreviation (e.g., "GA") */
19
+ stateCode: string;
20
+ }
21
+ /**
22
+ * Session data returned by getSession().
23
+ */
24
+ interface TrackingSession {
25
+ /** Session ID from the tracking API */
26
+ sessionId: string | null;
27
+ /** Assigned phone number, or null if no number available */
28
+ phoneNumber: string | null;
29
+ /** Location data, or null if location could not be resolved */
30
+ location: TrackingLocation | null;
31
+ }
32
+ /**
33
+ * Internal API response format (includes expiresAt).
34
+ */
35
+ interface ApiResponse {
36
+ sessionId: string;
37
+ phoneNumber: string | null;
38
+ location: {
39
+ city: string;
40
+ state: string;
41
+ stateCode: string;
42
+ } | null;
43
+ expiresAt: number;
44
+ }
45
+ /**
46
+ * Callback function for onReady subscription.
47
+ */
48
+ type ReadyCallback = (session: TrackingSession) => void;
49
+ /**
50
+ * Global window extension for preload promise.
51
+ */
52
+ declare global {
53
+ interface Window {
54
+ __cfTracking?: Promise<ApiResponse>;
55
+ }
56
+ }
57
+
58
+ declare class CallForge {
59
+ private readonly config;
60
+ private readonly cache;
61
+ private sessionPromise;
62
+ private constructor();
63
+ /**
64
+ * Initialize the CallForge tracking client.
65
+ */
66
+ static init(config: CallForgeConfig): CallForge;
67
+ /**
68
+ * Get tracking session data.
69
+ * Returns cached data if valid, otherwise fetches from API.
70
+ */
71
+ getSession(): Promise<TrackingSession>;
72
+ /**
73
+ * Subscribe to session ready event.
74
+ * Callback is called once session data is available.
75
+ */
76
+ onReady(callback: ReadyCallback): void;
77
+ private fetchSession;
78
+ private getLocationId;
79
+ private fetchFromApi;
80
+ private saveToCache;
81
+ private formatSession;
82
+ private formatApiResponse;
83
+ }
84
+
85
+ /**
86
+ * Generate HTML snippet for preloading tracking data.
87
+ * Add this to the <head> of your HTML for optimal performance.
88
+ *
89
+ * The generated snippet:
90
+ * - Checks for loc_physical_ms URL parameter
91
+ * - Checks localStorage cache for valid session
92
+ * - If cache valid, resolves Promise.resolve(cached) immediately
93
+ * - If cache expired but same location, includes sessionId for refresh
94
+ * - Otherwise fetches fresh session data
95
+ * - Sets window.__cfTracking with the session promise
96
+ *
97
+ * @throws {Error} If categoryId contains invalid characters
98
+ * @throws {Error} If endpoint is not a valid HTTPS URL
99
+ */
100
+ declare function getPreloadSnippet(config: CallForgeConfig): string;
101
+
102
+ export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingSession, getPreloadSnippet };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Configuration options for the CallForge tracking client.
3
+ */
4
+ interface CallForgeConfig {
5
+ /** Required - which number pool to use */
6
+ categoryId: string;
7
+ /** Optional - API endpoint. Defaults to 'https://tracking.callforge.io' */
8
+ endpoint?: string;
9
+ }
10
+ /**
11
+ * Location data returned by the tracking API.
12
+ */
13
+ interface TrackingLocation {
14
+ /** City name */
15
+ city: string;
16
+ /** Full state name (e.g., "Georgia") */
17
+ state: string;
18
+ /** State abbreviation (e.g., "GA") */
19
+ stateCode: string;
20
+ }
21
+ /**
22
+ * Session data returned by getSession().
23
+ */
24
+ interface TrackingSession {
25
+ /** Session ID from the tracking API */
26
+ sessionId: string | null;
27
+ /** Assigned phone number, or null if no number available */
28
+ phoneNumber: string | null;
29
+ /** Location data, or null if location could not be resolved */
30
+ location: TrackingLocation | null;
31
+ }
32
+ /**
33
+ * Internal API response format (includes expiresAt).
34
+ */
35
+ interface ApiResponse {
36
+ sessionId: string;
37
+ phoneNumber: string | null;
38
+ location: {
39
+ city: string;
40
+ state: string;
41
+ stateCode: string;
42
+ } | null;
43
+ expiresAt: number;
44
+ }
45
+ /**
46
+ * Callback function for onReady subscription.
47
+ */
48
+ type ReadyCallback = (session: TrackingSession) => void;
49
+ /**
50
+ * Global window extension for preload promise.
51
+ */
52
+ declare global {
53
+ interface Window {
54
+ __cfTracking?: Promise<ApiResponse>;
55
+ }
56
+ }
57
+
58
+ declare class CallForge {
59
+ private readonly config;
60
+ private readonly cache;
61
+ private sessionPromise;
62
+ private constructor();
63
+ /**
64
+ * Initialize the CallForge tracking client.
65
+ */
66
+ static init(config: CallForgeConfig): CallForge;
67
+ /**
68
+ * Get tracking session data.
69
+ * Returns cached data if valid, otherwise fetches from API.
70
+ */
71
+ getSession(): Promise<TrackingSession>;
72
+ /**
73
+ * Subscribe to session ready event.
74
+ * Callback is called once session data is available.
75
+ */
76
+ onReady(callback: ReadyCallback): void;
77
+ private fetchSession;
78
+ private getLocationId;
79
+ private fetchFromApi;
80
+ private saveToCache;
81
+ private formatSession;
82
+ private formatApiResponse;
83
+ }
84
+
85
+ /**
86
+ * Generate HTML snippet for preloading tracking data.
87
+ * Add this to the <head> of your HTML for optimal performance.
88
+ *
89
+ * The generated snippet:
90
+ * - Checks for loc_physical_ms URL parameter
91
+ * - Checks localStorage cache for valid session
92
+ * - If cache valid, resolves Promise.resolve(cached) immediately
93
+ * - If cache expired but same location, includes sessionId for refresh
94
+ * - Otherwise fetches fresh session data
95
+ * - Sets window.__cfTracking with the session promise
96
+ *
97
+ * @throws {Error} If categoryId contains invalid characters
98
+ * @throws {Error} If endpoint is not a valid HTTPS URL
99
+ */
100
+ declare function getPreloadSnippet(config: CallForgeConfig): string;
101
+
102
+ export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingSession, getPreloadSnippet };
package/dist/index.js ADDED
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CallForge: () => CallForge,
24
+ getPreloadSnippet: () => getPreloadSnippet
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/cache.ts
29
+ var EXPIRY_BUFFER_MS = 3e4;
30
+ var TrackingCache = class {
31
+ constructor(categoryId) {
32
+ this.memoryCache = null;
33
+ this.useMemory = false;
34
+ this.key = `cf_tracking_${categoryId}`;
35
+ this.useMemory = !this.isLocalStorageAvailable();
36
+ }
37
+ isLocalStorageAvailable() {
38
+ try {
39
+ localStorage.setItem("__cf_test__", "1");
40
+ localStorage.removeItem("__cf_test__");
41
+ return true;
42
+ } catch (e) {
43
+ return false;
44
+ }
45
+ }
46
+ /**
47
+ * Get cached session if valid (same locationId and not expired).
48
+ * Returns null if cache miss, location changed, or expired.
49
+ */
50
+ get(locationId) {
51
+ const cached = this.read();
52
+ if (!cached) return null;
53
+ if (cached.locId !== locationId) return null;
54
+ if (cached.expiresAt - EXPIRY_BUFFER_MS <= Date.now()) return null;
55
+ return cached;
56
+ }
57
+ /**
58
+ * Get sessionId for refresh if location matches (regardless of expiry).
59
+ * Used when cache is expired but location is same - send sessionId to server.
60
+ */
61
+ getSessionId(locationId) {
62
+ const cached = this.read();
63
+ if (!cached) return null;
64
+ if (cached.locId !== locationId) return null;
65
+ return cached.sessionId;
66
+ }
67
+ /**
68
+ * Store session data in cache.
69
+ */
70
+ set(session) {
71
+ if (this.useMemory) {
72
+ this.memoryCache = session;
73
+ } else {
74
+ try {
75
+ localStorage.setItem(this.key, JSON.stringify(session));
76
+ } catch (e) {
77
+ this.memoryCache = session;
78
+ }
79
+ }
80
+ }
81
+ /**
82
+ * Clear cached data.
83
+ */
84
+ clear() {
85
+ this.memoryCache = null;
86
+ if (!this.useMemory) {
87
+ try {
88
+ localStorage.removeItem(this.key);
89
+ } catch (e) {
90
+ }
91
+ }
92
+ }
93
+ read() {
94
+ if (this.useMemory) {
95
+ return this.memoryCache;
96
+ }
97
+ try {
98
+ const raw = localStorage.getItem(this.key);
99
+ if (!raw) return null;
100
+ return JSON.parse(raw);
101
+ } catch (e) {
102
+ return this.memoryCache;
103
+ }
104
+ }
105
+ };
106
+
107
+ // src/client.ts
108
+ var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
109
+ var FETCH_TIMEOUT_MS = 1e4;
110
+ var CallForge = class _CallForge {
111
+ constructor(config) {
112
+ this.sessionPromise = null;
113
+ this.config = {
114
+ categoryId: config.categoryId,
115
+ endpoint: config.endpoint || DEFAULT_ENDPOINT
116
+ };
117
+ this.cache = new TrackingCache(config.categoryId);
118
+ }
119
+ /**
120
+ * Initialize the CallForge tracking client.
121
+ */
122
+ static init(config) {
123
+ return new _CallForge(config);
124
+ }
125
+ /**
126
+ * Get tracking session data.
127
+ * Returns cached data if valid, otherwise fetches from API.
128
+ */
129
+ async getSession() {
130
+ if (this.sessionPromise) {
131
+ return this.sessionPromise;
132
+ }
133
+ this.sessionPromise = this.fetchSession();
134
+ return this.sessionPromise;
135
+ }
136
+ /**
137
+ * Subscribe to session ready event.
138
+ * Callback is called once session data is available.
139
+ */
140
+ onReady(callback) {
141
+ this.getSession().then(callback).catch(() => {
142
+ });
143
+ }
144
+ async fetchSession() {
145
+ const locationId = this.getLocationId();
146
+ if (!locationId) {
147
+ return { sessionId: null, phoneNumber: null, location: null };
148
+ }
149
+ const cached = this.cache.get(locationId);
150
+ if (cached) {
151
+ return this.formatSession(cached);
152
+ }
153
+ if (typeof window !== "undefined" && window.__cfTracking) {
154
+ try {
155
+ const data2 = await window.__cfTracking;
156
+ this.saveToCache(locationId, data2);
157
+ return this.formatApiResponse(data2);
158
+ } catch (e) {
159
+ }
160
+ }
161
+ const sessionId = this.cache.getSessionId(locationId);
162
+ const data = await this.fetchFromApi(locationId, sessionId);
163
+ this.saveToCache(locationId, data);
164
+ return this.formatApiResponse(data);
165
+ }
166
+ getLocationId() {
167
+ if (typeof window === "undefined") return null;
168
+ const params = new URLSearchParams(window.location.search);
169
+ return params.get("loc_physical_ms");
170
+ }
171
+ async fetchFromApi(locationId, sessionId) {
172
+ const { categoryId, endpoint } = this.config;
173
+ let url = `${endpoint}/v1/tracking/session?categoryId=${categoryId}&loc_physical_ms=${locationId}`;
174
+ if (sessionId) {
175
+ url += `&sessionId=${sessionId}`;
176
+ }
177
+ const controller = new AbortController();
178
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
179
+ try {
180
+ const response = await fetch(url, {
181
+ credentials: "omit",
182
+ signal: controller.signal
183
+ });
184
+ if (!response.ok) {
185
+ throw new Error(`API error: ${response.status} ${response.statusText}`);
186
+ }
187
+ return await response.json();
188
+ } finally {
189
+ clearTimeout(timeoutId);
190
+ }
191
+ }
192
+ saveToCache(locationId, data) {
193
+ const cached = {
194
+ locId: locationId,
195
+ sessionId: data.sessionId,
196
+ phoneNumber: data.phoneNumber,
197
+ location: data.location,
198
+ expiresAt: data.expiresAt
199
+ };
200
+ this.cache.set(cached);
201
+ }
202
+ formatSession(cached) {
203
+ return {
204
+ sessionId: cached.sessionId,
205
+ phoneNumber: cached.phoneNumber,
206
+ location: cached.location
207
+ };
208
+ }
209
+ formatApiResponse(data) {
210
+ return {
211
+ sessionId: data.sessionId,
212
+ phoneNumber: data.phoneNumber,
213
+ location: data.location
214
+ };
215
+ }
216
+ };
217
+
218
+ // src/preload.ts
219
+ var DEFAULT_ENDPOINT2 = "https://tracking.callforge.io";
220
+ var CATEGORY_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
221
+ var HTTPS_URL_PATTERN = /^https:\/\/[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}$/;
222
+ function getPreloadSnippet(config) {
223
+ const endpoint = config.endpoint || DEFAULT_ENDPOINT2;
224
+ const { categoryId } = config;
225
+ if (!CATEGORY_ID_PATTERN.test(categoryId)) {
226
+ throw new Error(
227
+ `Invalid categoryId: "${categoryId}". Must match pattern ${CATEGORY_ID_PATTERN}`
228
+ );
229
+ }
230
+ if (!HTTPS_URL_PATTERN.test(endpoint)) {
231
+ throw new Error(
232
+ `Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
233
+ );
234
+ }
235
+ const cacheKey = `cf_tracking_${categoryId}`;
236
+ const script = `(function(){
237
+ var loc=new URLSearchParams(location.search).get('loc_physical_ms');
238
+ if(!loc)return;
239
+ var key='${cacheKey}';
240
+ try{
241
+ var c=JSON.parse(localStorage.getItem(key));
242
+ if(c&&c.locId===loc&&c.expiresAt>Date.now()+30000){window.__cfTracking=Promise.resolve(c);return}
243
+ var sid=(c&&c.locId===loc)?c.sessionId:null;
244
+ }catch(e){}
245
+ var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}&loc_physical_ms='+loc;
246
+ if(sid)url+='&sessionId='+sid;
247
+ window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.json()});
248
+ })();`.replace(/\n/g, "");
249
+ return `<link rel="preconnect" href="${endpoint}">
250
+ <script>${script}</script>`;
251
+ }
252
+ // Annotate the CommonJS export names for ESM import in node:
253
+ 0 && (module.exports = {
254
+ CallForge,
255
+ getPreloadSnippet
256
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,228 @@
1
+ // src/cache.ts
2
+ var EXPIRY_BUFFER_MS = 3e4;
3
+ var TrackingCache = class {
4
+ constructor(categoryId) {
5
+ this.memoryCache = null;
6
+ this.useMemory = false;
7
+ this.key = `cf_tracking_${categoryId}`;
8
+ this.useMemory = !this.isLocalStorageAvailable();
9
+ }
10
+ isLocalStorageAvailable() {
11
+ try {
12
+ localStorage.setItem("__cf_test__", "1");
13
+ localStorage.removeItem("__cf_test__");
14
+ return true;
15
+ } catch (e) {
16
+ return false;
17
+ }
18
+ }
19
+ /**
20
+ * Get cached session if valid (same locationId and not expired).
21
+ * Returns null if cache miss, location changed, or expired.
22
+ */
23
+ get(locationId) {
24
+ const cached = this.read();
25
+ if (!cached) return null;
26
+ if (cached.locId !== locationId) return null;
27
+ if (cached.expiresAt - EXPIRY_BUFFER_MS <= Date.now()) return null;
28
+ return cached;
29
+ }
30
+ /**
31
+ * Get sessionId for refresh if location matches (regardless of expiry).
32
+ * Used when cache is expired but location is same - send sessionId to server.
33
+ */
34
+ getSessionId(locationId) {
35
+ const cached = this.read();
36
+ if (!cached) return null;
37
+ if (cached.locId !== locationId) return null;
38
+ return cached.sessionId;
39
+ }
40
+ /**
41
+ * Store session data in cache.
42
+ */
43
+ set(session) {
44
+ if (this.useMemory) {
45
+ this.memoryCache = session;
46
+ } else {
47
+ try {
48
+ localStorage.setItem(this.key, JSON.stringify(session));
49
+ } catch (e) {
50
+ this.memoryCache = session;
51
+ }
52
+ }
53
+ }
54
+ /**
55
+ * Clear cached data.
56
+ */
57
+ clear() {
58
+ this.memoryCache = null;
59
+ if (!this.useMemory) {
60
+ try {
61
+ localStorage.removeItem(this.key);
62
+ } catch (e) {
63
+ }
64
+ }
65
+ }
66
+ read() {
67
+ if (this.useMemory) {
68
+ return this.memoryCache;
69
+ }
70
+ try {
71
+ const raw = localStorage.getItem(this.key);
72
+ if (!raw) return null;
73
+ return JSON.parse(raw);
74
+ } catch (e) {
75
+ return this.memoryCache;
76
+ }
77
+ }
78
+ };
79
+
80
+ // src/client.ts
81
+ var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
82
+ var FETCH_TIMEOUT_MS = 1e4;
83
+ var CallForge = class _CallForge {
84
+ constructor(config) {
85
+ this.sessionPromise = null;
86
+ this.config = {
87
+ categoryId: config.categoryId,
88
+ endpoint: config.endpoint || DEFAULT_ENDPOINT
89
+ };
90
+ this.cache = new TrackingCache(config.categoryId);
91
+ }
92
+ /**
93
+ * Initialize the CallForge tracking client.
94
+ */
95
+ static init(config) {
96
+ return new _CallForge(config);
97
+ }
98
+ /**
99
+ * Get tracking session data.
100
+ * Returns cached data if valid, otherwise fetches from API.
101
+ */
102
+ async getSession() {
103
+ if (this.sessionPromise) {
104
+ return this.sessionPromise;
105
+ }
106
+ this.sessionPromise = this.fetchSession();
107
+ return this.sessionPromise;
108
+ }
109
+ /**
110
+ * Subscribe to session ready event.
111
+ * Callback is called once session data is available.
112
+ */
113
+ onReady(callback) {
114
+ this.getSession().then(callback).catch(() => {
115
+ });
116
+ }
117
+ async fetchSession() {
118
+ const locationId = this.getLocationId();
119
+ if (!locationId) {
120
+ return { sessionId: null, phoneNumber: null, location: null };
121
+ }
122
+ const cached = this.cache.get(locationId);
123
+ if (cached) {
124
+ return this.formatSession(cached);
125
+ }
126
+ if (typeof window !== "undefined" && window.__cfTracking) {
127
+ try {
128
+ const data2 = await window.__cfTracking;
129
+ this.saveToCache(locationId, data2);
130
+ return this.formatApiResponse(data2);
131
+ } catch (e) {
132
+ }
133
+ }
134
+ const sessionId = this.cache.getSessionId(locationId);
135
+ const data = await this.fetchFromApi(locationId, sessionId);
136
+ this.saveToCache(locationId, data);
137
+ return this.formatApiResponse(data);
138
+ }
139
+ getLocationId() {
140
+ if (typeof window === "undefined") return null;
141
+ const params = new URLSearchParams(window.location.search);
142
+ return params.get("loc_physical_ms");
143
+ }
144
+ async fetchFromApi(locationId, sessionId) {
145
+ const { categoryId, endpoint } = this.config;
146
+ let url = `${endpoint}/v1/tracking/session?categoryId=${categoryId}&loc_physical_ms=${locationId}`;
147
+ if (sessionId) {
148
+ url += `&sessionId=${sessionId}`;
149
+ }
150
+ const controller = new AbortController();
151
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
152
+ try {
153
+ const response = await fetch(url, {
154
+ credentials: "omit",
155
+ signal: controller.signal
156
+ });
157
+ if (!response.ok) {
158
+ throw new Error(`API error: ${response.status} ${response.statusText}`);
159
+ }
160
+ return await response.json();
161
+ } finally {
162
+ clearTimeout(timeoutId);
163
+ }
164
+ }
165
+ saveToCache(locationId, data) {
166
+ const cached = {
167
+ locId: locationId,
168
+ sessionId: data.sessionId,
169
+ phoneNumber: data.phoneNumber,
170
+ location: data.location,
171
+ expiresAt: data.expiresAt
172
+ };
173
+ this.cache.set(cached);
174
+ }
175
+ formatSession(cached) {
176
+ return {
177
+ sessionId: cached.sessionId,
178
+ phoneNumber: cached.phoneNumber,
179
+ location: cached.location
180
+ };
181
+ }
182
+ formatApiResponse(data) {
183
+ return {
184
+ sessionId: data.sessionId,
185
+ phoneNumber: data.phoneNumber,
186
+ location: data.location
187
+ };
188
+ }
189
+ };
190
+
191
+ // src/preload.ts
192
+ var DEFAULT_ENDPOINT2 = "https://tracking.callforge.io";
193
+ var CATEGORY_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
194
+ var HTTPS_URL_PATTERN = /^https:\/\/[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}$/;
195
+ function getPreloadSnippet(config) {
196
+ const endpoint = config.endpoint || DEFAULT_ENDPOINT2;
197
+ const { categoryId } = config;
198
+ if (!CATEGORY_ID_PATTERN.test(categoryId)) {
199
+ throw new Error(
200
+ `Invalid categoryId: "${categoryId}". Must match pattern ${CATEGORY_ID_PATTERN}`
201
+ );
202
+ }
203
+ if (!HTTPS_URL_PATTERN.test(endpoint)) {
204
+ throw new Error(
205
+ `Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
206
+ );
207
+ }
208
+ const cacheKey = `cf_tracking_${categoryId}`;
209
+ const script = `(function(){
210
+ var loc=new URLSearchParams(location.search).get('loc_physical_ms');
211
+ if(!loc)return;
212
+ var key='${cacheKey}';
213
+ try{
214
+ var c=JSON.parse(localStorage.getItem(key));
215
+ if(c&&c.locId===loc&&c.expiresAt>Date.now()+30000){window.__cfTracking=Promise.resolve(c);return}
216
+ var sid=(c&&c.locId===loc)?c.sessionId:null;
217
+ }catch(e){}
218
+ var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}&loc_physical_ms='+loc;
219
+ if(sid)url+='&sessionId='+sid;
220
+ window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.json()});
221
+ })();`.replace(/\n/g, "");
222
+ return `<link rel="preconnect" href="${endpoint}">
223
+ <script>${script}</script>`;
224
+ }
225
+ export {
226
+ CallForge,
227
+ getPreloadSnippet
228
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@callforge/tracking-client",
3
+ "version": "0.0.1",
4
+ "main": "dist/index.js",
5
+ "module": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup src/index.ts --format esm,cjs --dts",
19
+ "clean": "rm -rf dist",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest"
22
+ },
23
+ "devDependencies": {
24
+ "@callforge/tsconfig": "workspace:*",
25
+ "jsdom": "^27.4.0",
26
+ "tsup": "^8.0.0",
27
+ "typescript": "^5.3.0",
28
+ "vitest": "^1.6.0"
29
+ }
30
+ }