tatvic-implementation-sdk 0.0.0-stage → 1.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.
package/dist/index.js ADDED
@@ -0,0 +1,871 @@
1
+ // src/defaults.ts
2
+ var DEFAULT_PROXY_ENDPOINT = "/_tatvic/relay";
3
+ var DEFAULT_HEADER_NAME = "x-tatvic-target";
4
+ var DEFAULT_TARGET_DOMAINS = [
5
+ "google-analytics.com",
6
+ "analytics.google.com",
7
+ "googletagmanager.com",
8
+ "stats.g.doubleclick.net",
9
+ "googleadservices.com",
10
+ "googleads.g.doubleclick.net",
11
+ "pagead2.googlesyndication.com",
12
+ "google.com",
13
+ "stape.io",
14
+ "stape.net",
15
+ "a.run.app",
16
+ "appspot.com",
17
+ "connect.facebook.net",
18
+ "facebook.com",
19
+ "graph.facebook.com",
20
+ "facebook.net",
21
+ "clarity.ms",
22
+ "c.bing.com",
23
+ "bat.bing.com",
24
+ "analytics.tiktok.com",
25
+ "tiktok.com",
26
+ "static.hotjar.com",
27
+ "script.hotjar.com",
28
+ "in.hotjar.com",
29
+ "static.ads-twitter.com",
30
+ "ads-twitter.com",
31
+ "snap.licdn.com",
32
+ "px.ads.linkedin.com",
33
+ "ct.pinterest.com",
34
+ "pintrk.com",
35
+ "sc-static.net",
36
+ "tr.snapchat.com",
37
+ "static.criteo.net",
38
+ "sslwidget.criteo.com",
39
+ "widgets.outbrain.com",
40
+ "tr.outbrain.com",
41
+ "cdn.taboola.com",
42
+ "trc.taboola.com",
43
+ "segment.io",
44
+ "api.segment.io",
45
+ "cdn.segment.com",
46
+ "mixpanel.com",
47
+ "api-js.mixpanel.com",
48
+ "amplitude.com",
49
+ "api.amplitude.com",
50
+ "api2.amplitude.com",
51
+ "heapanalytics.com",
52
+ "cdn.heapanalytics.com",
53
+ "posthog.com",
54
+ "app.posthog.com",
55
+ "eu.posthog.com"
56
+ ];
57
+ var DEFAULT_EXCLUDE_DOMAINS = [
58
+ "api.stripe.com",
59
+ "js.stripe.com",
60
+ "m.stripe.network",
61
+ "paypal.com",
62
+ "sandbox.paypal.com",
63
+ "razorpay.com",
64
+ "checkout.razorpay.com",
65
+ "square.com",
66
+ "squareup.com",
67
+ "auth0.com",
68
+ "clerk.dev",
69
+ "clerk.com",
70
+ "supabase.co",
71
+ "firebaseio.com",
72
+ "accounts.google.com",
73
+ "appleid.apple.com",
74
+ "amazonaws.com",
75
+ "googleapis.com",
76
+ "cloudflare.com"
77
+ ];
78
+
79
+ // src/url-matcher.ts
80
+ class UrlMatcher {
81
+ targetDomains;
82
+ excludeDomains;
83
+ proxyEndpoint;
84
+ targetHeaderName;
85
+ obfuscateTarget;
86
+ routingMode;
87
+ debug;
88
+ constructor(options = {}) {
89
+ this.targetDomains = options.targetDomains ?? DEFAULT_TARGET_DOMAINS;
90
+ this.excludeDomains = options.excludeDomains ?? DEFAULT_EXCLUDE_DOMAINS;
91
+ this.proxyEndpoint = options.proxyEndpoint ?? DEFAULT_PROXY_ENDPOINT;
92
+ this.targetHeaderName = (options.targetHeaderName ?? DEFAULT_HEADER_NAME).toLowerCase();
93
+ this.obfuscateTarget = options.obfuscateTarget ?? false;
94
+ this.routingMode = options.routingMode ?? "auto";
95
+ this.debug = options.debug ?? false;
96
+ }
97
+ shouldProxy(urlStr) {
98
+ const parsed = this.parseUrl(urlStr);
99
+ if (!parsed)
100
+ return false;
101
+ const hostname = parsed.hostname.toLowerCase();
102
+ if (typeof window !== "undefined" && window.location?.origin && parsed.origin === window.location.origin) {
103
+ if (parsed.pathname.startsWith(this.proxyEndpoint)) {
104
+ return false;
105
+ }
106
+ }
107
+ for (const rule of this.excludeDomains) {
108
+ if (this.matchesRule(hostname, rule)) {
109
+ return false;
110
+ }
111
+ }
112
+ for (const rule of this.targetDomains) {
113
+ if (this.matchesRule(hostname, rule)) {
114
+ return true;
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+ encodeTarget(targetUrl) {
120
+ if (this.obfuscateTarget) {
121
+ if (typeof btoa !== "undefined") {
122
+ return btoa(targetUrl);
123
+ }
124
+ return Buffer.from(targetUrl).toString("base64");
125
+ }
126
+ return targetUrl;
127
+ }
128
+ getProxiedRequestInfo(originalUrl, forcedMode) {
129
+ const mode = forcedMode ?? (this.routingMode === "auto" ? "header" : this.routingMode);
130
+ const encodedTarget = this.encodeTarget(originalUrl);
131
+ if (mode === "query") {
132
+ const separator = this.proxyEndpoint.includes("?") ? "&" : "?";
133
+ const proxyUrl = `${this.proxyEndpoint}${separator}_tgt=${encodeURIComponent(encodedTarget)}`;
134
+ return { proxyUrl };
135
+ }
136
+ return {
137
+ proxyUrl: this.proxyEndpoint,
138
+ headerName: this.targetHeaderName,
139
+ headerValue: encodedTarget
140
+ };
141
+ }
142
+ log(...args) {
143
+ if (this.debug) {
144
+ console.log("[Tatvic Proxy SDK]", ...args);
145
+ }
146
+ }
147
+ matchesRule(hostname, rule) {
148
+ if (rule instanceof RegExp) {
149
+ return rule.test(hostname);
150
+ }
151
+ const cleanRule = rule.toLowerCase().trim();
152
+ return hostname === cleanRule || hostname.endsWith(`.${cleanRule}`);
153
+ }
154
+ parseUrl(urlStr) {
155
+ try {
156
+ if (typeof window !== "undefined" && window.location?.origin) {
157
+ return new URL(urlStr, window.location.origin);
158
+ }
159
+ return new URL(urlStr);
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+ }
165
+
166
+ // src/interceptor.ts
167
+ class RequestInterceptor {
168
+ matcher;
169
+ options;
170
+ isInstalled = false;
171
+ originalFetch;
172
+ originalXhrOpen;
173
+ originalXhrSend;
174
+ originalSendBeacon;
175
+ originalScriptSrcDesc;
176
+ originalImageSrcDesc;
177
+ constructor(options = {}) {
178
+ this.options = options;
179
+ this.matcher = new UrlMatcher(options);
180
+ }
181
+ install() {
182
+ if (this.isInstalled || typeof window === "undefined") {
183
+ return;
184
+ }
185
+ if (this.options.enabled === false) {
186
+ this.matcher.log("SDK disabled via configuration.");
187
+ return;
188
+ }
189
+ this.patchFetch();
190
+ this.patchXhr();
191
+ this.patchSendBeacon();
192
+ this.patchScriptElements();
193
+ this.patchImageElements();
194
+ this.isInstalled = true;
195
+ this.matcher.log("First-party request interceptor active.");
196
+ }
197
+ uninstall() {
198
+ if (!this.isInstalled || typeof window === "undefined") {
199
+ return;
200
+ }
201
+ if (this.originalFetch) {
202
+ window.fetch = this.originalFetch;
203
+ this.originalFetch = undefined;
204
+ }
205
+ if (this.originalXhrOpen) {
206
+ XMLHttpRequest.prototype.open = this.originalXhrOpen;
207
+ this.originalXhrOpen = undefined;
208
+ }
209
+ if (this.originalXhrSend) {
210
+ XMLHttpRequest.prototype.send = this.originalXhrSend;
211
+ this.originalXhrSend = undefined;
212
+ }
213
+ if (this.originalSendBeacon && navigator.sendBeacon) {
214
+ navigator.sendBeacon = this.originalSendBeacon;
215
+ this.originalSendBeacon = undefined;
216
+ }
217
+ if (this.originalScriptSrcDesc && typeof HTMLScriptElement !== "undefined") {
218
+ Object.defineProperty(HTMLScriptElement.prototype, "src", this.originalScriptSrcDesc);
219
+ this.originalScriptSrcDesc = undefined;
220
+ }
221
+ if (this.originalImageSrcDesc && typeof HTMLImageElement !== "undefined") {
222
+ Object.defineProperty(HTMLImageElement.prototype, "src", this.originalImageSrcDesc);
223
+ this.originalImageSrcDesc = undefined;
224
+ }
225
+ this.isInstalled = false;
226
+ this.matcher.log("First-party request interceptor uninstalled.");
227
+ }
228
+ patchFetch() {
229
+ if (!window.fetch)
230
+ return;
231
+ this.originalFetch = window.fetch.bind(window);
232
+ const originalFetch = this.originalFetch;
233
+ const matcher = this.matcher;
234
+ const patchedFetch = async (input, init) => {
235
+ let targetUrl;
236
+ if (typeof input === "string") {
237
+ targetUrl = input;
238
+ } else if (input instanceof URL) {
239
+ targetUrl = input.toString();
240
+ } else if (typeof Request !== "undefined" && input instanceof Request) {
241
+ targetUrl = input.url;
242
+ } else {
243
+ targetUrl = String(input);
244
+ }
245
+ if (!matcher.shouldProxy(targetUrl)) {
246
+ return originalFetch(input, init);
247
+ }
248
+ matcher.log("Intercepted fetch:", targetUrl);
249
+ const isNoCors = init?.mode === "no-cors" || typeof Request !== "undefined" && input instanceof Request && input.mode === "no-cors";
250
+ const info = matcher.getProxiedRequestInfo(targetUrl, isNoCors ? "query" : undefined);
251
+ const previewToken = this.options.serverPreviewToken || this.detectPreviewToken();
252
+ if (typeof Request !== "undefined" && input instanceof Request) {
253
+ const headers = new Headers(input.headers);
254
+ if (init?.headers) {
255
+ new Headers(init.headers).forEach((val, key) => headers.set(key, val));
256
+ }
257
+ if (info.headerName && info.headerValue && !isNoCors) {
258
+ headers.set(info.headerName, info.headerValue);
259
+ }
260
+ if (previewToken && !headers.has("x-gtm-server-preview")) {
261
+ headers.set("x-gtm-server-preview", previewToken);
262
+ }
263
+ const proxiedRequest = new Request(info.proxyUrl, {
264
+ method: init?.method ?? input.method,
265
+ headers,
266
+ body: init?.body ?? input.body,
267
+ mode: isNoCors ? "cors" : init?.mode ?? input.mode ?? "cors",
268
+ credentials: init?.credentials ?? input.credentials,
269
+ cache: init?.cache ?? input.cache,
270
+ redirect: init?.redirect ?? input.redirect,
271
+ referrer: init?.referrer ?? input.referrer,
272
+ integrity: init?.integrity ?? input.integrity,
273
+ keepalive: init?.keepalive ?? input.keepalive,
274
+ signal: init?.signal ?? input.signal
275
+ });
276
+ return originalFetch(proxiedRequest);
277
+ }
278
+ const headers = new Headers(init?.headers);
279
+ if (info.headerName && info.headerValue && !isNoCors) {
280
+ headers.set(info.headerName, info.headerValue);
281
+ }
282
+ if (previewToken && !headers.has("x-gtm-server-preview")) {
283
+ headers.set("x-gtm-server-preview", previewToken);
284
+ }
285
+ const modifiedInit = {
286
+ ...init,
287
+ headers,
288
+ mode: isNoCors ? "cors" : init?.mode ?? "cors"
289
+ };
290
+ return originalFetch(info.proxyUrl, modifiedInit);
291
+ };
292
+ Object.assign(patchedFetch, originalFetch);
293
+ window.fetch = patchedFetch;
294
+ }
295
+ patchXhr() {
296
+ if (typeof XMLHttpRequest === "undefined")
297
+ return;
298
+ this.originalXhrOpen = XMLHttpRequest.prototype.open;
299
+ this.originalXhrSend = XMLHttpRequest.prototype.send;
300
+ const originalOpen = this.originalXhrOpen;
301
+ const originalSend = this.originalXhrSend;
302
+ const matcher = this.matcher;
303
+ const getPreviewToken = () => this.options.serverPreviewToken || this.detectPreviewToken();
304
+ XMLHttpRequest.prototype.open = function(method, url, async = true, username, password) {
305
+ const targetUrl = url.toString();
306
+ if (matcher.shouldProxy(targetUrl)) {
307
+ matcher.log("Intercepted XHR:", targetUrl);
308
+ const info = matcher.getProxiedRequestInfo(targetUrl);
309
+ const previewToken = getPreviewToken();
310
+ this.__tatvicMeta = {
311
+ isProxied: true,
312
+ originalUrl: targetUrl,
313
+ headerName: info.headerName,
314
+ headerValue: info.headerValue,
315
+ previewToken
316
+ };
317
+ return originalOpen.call(this, method, info.proxyUrl, async, username, password);
318
+ }
319
+ this.__tatvicMeta = { isProxied: false };
320
+ return originalOpen.call(this, method, url, async, username, password);
321
+ };
322
+ XMLHttpRequest.prototype.send = function(body) {
323
+ if (this.__tatvicMeta?.isProxied) {
324
+ const { headerName, headerValue, previewToken } = this.__tatvicMeta;
325
+ if (headerName && headerValue) {
326
+ try {
327
+ this.setRequestHeader(headerName, headerValue);
328
+ } catch (e) {
329
+ matcher.log("Failed to set proxy header on XHR:", e);
330
+ }
331
+ }
332
+ if (previewToken) {
333
+ try {
334
+ this.setRequestHeader("x-gtm-server-preview", previewToken);
335
+ } catch (e) {
336
+ matcher.log("Failed to set preview header on XHR:", e);
337
+ }
338
+ }
339
+ }
340
+ return originalSend.call(this, body);
341
+ };
342
+ }
343
+ patchSendBeacon() {
344
+ if (typeof navigator === "undefined" || !navigator.sendBeacon)
345
+ return;
346
+ this.originalSendBeacon = navigator.sendBeacon.bind(navigator);
347
+ const originalSendBeacon = this.originalSendBeacon;
348
+ const matcher = this.matcher;
349
+ const polyfillWithKeepalive = this.options.polyfillBeaconWithKeepalive !== false;
350
+ navigator.sendBeacon = (url, data) => {
351
+ const targetUrl = url.toString();
352
+ if (!matcher.shouldProxy(targetUrl)) {
353
+ return originalSendBeacon(url, data);
354
+ }
355
+ matcher.log("Intercepted sendBeacon:", targetUrl);
356
+ if (polyfillWithKeepalive && typeof window.fetch === "function") {
357
+ const info = matcher.getProxiedRequestInfo(targetUrl);
358
+ const previewToken = this.options.serverPreviewToken || this.detectPreviewToken();
359
+ const headers = new Headers;
360
+ if (info.headerName && info.headerValue) {
361
+ headers.set(info.headerName, info.headerValue);
362
+ }
363
+ if (previewToken) {
364
+ headers.set("x-gtm-server-preview", previewToken);
365
+ }
366
+ try {
367
+ window.fetch(info.proxyUrl, {
368
+ method: "POST",
369
+ body: data,
370
+ headers,
371
+ keepalive: true,
372
+ mode: "cors"
373
+ }).catch((err) => {
374
+ matcher.log("Async sendBeacon keepalive fetch error:", err);
375
+ });
376
+ return true;
377
+ } catch {}
378
+ }
379
+ const queryInfo = matcher.getProxiedRequestInfo(targetUrl, "query");
380
+ let fallbackUrl = queryInfo.proxyUrl;
381
+ const previewToken = this.options.serverPreviewToken || this.detectPreviewToken();
382
+ if (previewToken) {
383
+ fallbackUrl += (fallbackUrl.includes("?") ? "&" : "?") + "_gtm_server_preview=" + encodeURIComponent(previewToken);
384
+ }
385
+ return originalSendBeacon(fallbackUrl, data);
386
+ };
387
+ }
388
+ patchScriptElements() {
389
+ if (typeof HTMLScriptElement === "undefined")
390
+ return;
391
+ const desc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, "src");
392
+ if (!desc || !desc.set || !desc.get)
393
+ return;
394
+ this.originalScriptSrcDesc = desc;
395
+ const matcher = this.matcher;
396
+ const originalSet = desc.set;
397
+ const originalGet = desc.get;
398
+ Object.defineProperty(HTMLScriptElement.prototype, "src", {
399
+ set(val) {
400
+ if (typeof val === "string" && (val.includes("googletagmanager.com/gtm.js") || val.includes("/_tatvic/gtm.js") || val.includes("/_tatvic/loader.js"))) {
401
+ try {
402
+ const pageSearch = typeof window !== "undefined" && window.location ? window.location.search : "";
403
+ const pageParams = new URLSearchParams(pageSearch);
404
+ const targetUrl = new URL(val, typeof window !== "undefined" && window.location ? window.location.origin : "http://localhost");
405
+ const containerId = targetUrl.searchParams.get("cid") || targetUrl.searchParams.get("container") || targetUrl.searchParams.get("tag") || targetUrl.searchParams.get("id") || "GTM-T32XGF8F";
406
+ const morphedParams = new URLSearchParams;
407
+ morphedParams.set("cid", containerId);
408
+ targetUrl.searchParams.forEach((v, k) => {
409
+ if (k !== "id" && k !== "cid" && k !== "container" && k !== "tag") {
410
+ morphedParams.set(k, v);
411
+ }
412
+ });
413
+ for (const param of ["gtm_debug", "gtm_auth", "gtm_preview", "gtm_cookies_win"]) {
414
+ if (pageParams.has(param) && !morphedParams.has(param)) {
415
+ morphedParams.set(param, pageParams.get(param));
416
+ }
417
+ }
418
+ const morphedUrl = `/_tatvic/loader.js?${morphedParams.toString()}`;
419
+ matcher.log("Morphed GTM script element to stealth loader:", val, "->", morphedUrl);
420
+ return originalSet.call(this, morphedUrl);
421
+ } catch {}
422
+ }
423
+ if (matcher.shouldProxy(val)) {
424
+ const info = matcher.getProxiedRequestInfo(val, "query");
425
+ matcher.log("Intercepted dynamic script element:", val, "->", info.proxyUrl);
426
+ return originalSet.call(this, info.proxyUrl);
427
+ }
428
+ return originalSet.call(this, val);
429
+ },
430
+ get() {
431
+ return originalGet.call(this);
432
+ },
433
+ configurable: true,
434
+ enumerable: true
435
+ });
436
+ }
437
+ patchImageElements() {
438
+ if (typeof HTMLImageElement === "undefined")
439
+ return;
440
+ const desc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "src");
441
+ if (!desc || !desc.set || !desc.get)
442
+ return;
443
+ this.originalImageSrcDesc = desc;
444
+ const matcher = this.matcher;
445
+ const originalSet = desc.set;
446
+ const originalGet = desc.get;
447
+ Object.defineProperty(HTMLImageElement.prototype, "src", {
448
+ set(val) {
449
+ if (matcher.shouldProxy(val)) {
450
+ const info = matcher.getProxiedRequestInfo(val, "query");
451
+ matcher.log("Intercepted tracking pixel image:", val, "->", info.proxyUrl);
452
+ return originalSet.call(this, info.proxyUrl);
453
+ }
454
+ return originalSet.call(this, val);
455
+ },
456
+ get() {
457
+ return originalGet.call(this);
458
+ },
459
+ configurable: true,
460
+ enumerable: true
461
+ });
462
+ }
463
+ detectPreviewToken() {
464
+ if (typeof window === "undefined" || !window.location)
465
+ return;
466
+ try {
467
+ const params = new URLSearchParams(window.location.search);
468
+ return params.get("gtm_server_preview") || undefined;
469
+ } catch {
470
+ return;
471
+ }
472
+ }
473
+ }
474
+ // src/morph-gtm.ts
475
+ function morphGtmScript(originalCode, relayEndpoint = "/_tatvic/relay") {
476
+ let morphed = originalCode;
477
+ const endpointReplacements = [
478
+ [/https:\/\/adservice\.google\.com\//g, `${relayEndpoint}?_tgt=https%3A%2F%2Fadservice.google.com%2F`],
479
+ [/https:\/\/www\.googleadservices\.com\//g, `${relayEndpoint}?_tgt=https%3A%2F%2Fwww.googleadservices.com%2F`],
480
+ [/https:\/\/pagead2\.googlesyndication\.com\//g, `${relayEndpoint}?_tgt=https%3A%2F%2Fpagead2.googlesyndication.com%2F`],
481
+ [/https:\/\/ad\.doubleclick\.net\//g, `${relayEndpoint}?_tgt=https%3A%2F%2Fad.doubleclick.net%2F`],
482
+ [/https:\/\/www\.google-analytics\.com\//g, `${relayEndpoint}?_tgt=https%3A%2F%2Fwww.google-analytics.com%2F`],
483
+ [/https:\/\/analytics\.google\.com\//g, `${relayEndpoint}?_tgt=https%3A%2F%2Fanalytics.google.com%2F`],
484
+ [/https:\/\/region1\.google-analytics\.com\//g, `${relayEndpoint}?_tgt=https%3A%2F%2Fregion1.google-analytics.com%2F`]
485
+ ];
486
+ for (const [pattern, replacement] of endpointReplacements) {
487
+ morphed = morphed.replace(pattern, replacement);
488
+ }
489
+ return morphed;
490
+ }
491
+
492
+ // src/server-proxy.ts
493
+ function createProxyHandler(options = {}) {
494
+ const headerName = (options.targetHeaderName ?? DEFAULT_HEADER_NAME).toLowerCase();
495
+ const allowedDomains = options.allowedDomains ?? DEFAULT_TARGET_DOMAINS;
496
+ const timeoutMs = options.timeoutMs ?? 1e4;
497
+ const relayEndpoint = DEFAULT_PROXY_ENDPOINT;
498
+ const stripHeaders = new Set((options.stripHeaders ?? ["host", "authorization", "connection"]).map((h) => h.toLowerCase()));
499
+ return async function handleProxyRequest(request) {
500
+ try {
501
+ const reqUrl = new URL(request.url);
502
+ const pathname = reqUrl.pathname;
503
+ if (pathname.endsWith("/gtm.js") || pathname.endsWith("/loader.js") || pathname.endsWith("/core.js") || pathname.endsWith("/loader") || pathname.includes("/_tatvic/loader.js") || pathname.includes("/_tatvic/gtm.js") || pathname.includes("/_tatvic/core.js")) {
504
+ return handleMorphedGtmRequest(request, reqUrl);
505
+ }
506
+ let targetUrlStr = request.headers.get(headerName);
507
+ if (!targetUrlStr) {
508
+ targetUrlStr = reqUrl.searchParams.get("_tgt");
509
+ }
510
+ if (!targetUrlStr) {
511
+ return new Response(JSON.stringify({ error: "Missing target destination" }), {
512
+ status: 400,
513
+ headers: { "Content-Type": "application/json" }
514
+ });
515
+ }
516
+ if (options.obfuscateTarget) {
517
+ try {
518
+ if (typeof atob !== "undefined") {
519
+ targetUrlStr = atob(targetUrlStr);
520
+ } else {
521
+ targetUrlStr = Buffer.from(targetUrlStr, "base64").toString("utf-8");
522
+ }
523
+ } catch {
524
+ return new Response(JSON.stringify({ error: "Invalid obfuscated target" }), {
525
+ status: 400,
526
+ headers: { "Content-Type": "application/json" }
527
+ });
528
+ }
529
+ }
530
+ let targetUrl;
531
+ try {
532
+ targetUrl = new URL(targetUrlStr);
533
+ } catch {
534
+ return new Response(JSON.stringify({ error: "Malformed target URL" }), {
535
+ status: 400,
536
+ headers: { "Content-Type": "application/json" }
537
+ });
538
+ }
539
+ if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
540
+ return new Response(JSON.stringify({ error: "Unsupported protocol" }), {
541
+ status: 403,
542
+ headers: { "Content-Type": "application/json" }
543
+ });
544
+ }
545
+ const isAllowed = matchesAllowlist(targetUrl.hostname, allowedDomains);
546
+ if (!isAllowed) {
547
+ return new Response(JSON.stringify({ error: "Target host is not permitted" }), {
548
+ status: 403,
549
+ headers: { "Content-Type": "application/json" }
550
+ });
551
+ }
552
+ if (options.serverContainerUrl && (targetUrl.hostname.includes("google-analytics.com") || targetUrl.hostname.includes("analytics.google.com"))) {
553
+ try {
554
+ const sGtmOrigin = new URL(options.serverContainerUrl);
555
+ targetUrl.protocol = sGtmOrigin.protocol;
556
+ targetUrl.host = sGtmOrigin.host;
557
+ targetUrl.port = sGtmOrigin.port;
558
+ } catch {}
559
+ }
560
+ const upstreamHeaders = new Headers;
561
+ request.headers.forEach((val, key) => {
562
+ const lowerKey = key.toLowerCase();
563
+ if (!stripHeaders.has(lowerKey) && lowerKey !== headerName) {
564
+ upstreamHeaders.set(key, val);
565
+ }
566
+ });
567
+ const serverPreviewToken = request.headers.get("x-gtm-server-preview") || reqUrl.searchParams.get("_gtm_server_preview") || options.serverPreviewToken;
568
+ if (serverPreviewToken) {
569
+ upstreamHeaders.set("x-gtm-server-preview", serverPreviewToken);
570
+ if (targetUrl.pathname.includes("/collect") || targetUrl.pathname.includes("/g/collect")) {
571
+ if (!targetUrl.searchParams.has("_dbg")) {
572
+ targetUrl.searchParams.set("_dbg", "1");
573
+ }
574
+ if (!targetUrl.searchParams.has("ep.debug_mode")) {
575
+ targetUrl.searchParams.set("ep.debug_mode", "true");
576
+ }
577
+ }
578
+ }
579
+ const clientIp = request.headers.get("x-forwarded-for") ?? request.headers.get("cf-connecting-ip");
580
+ if (clientIp) {
581
+ upstreamHeaders.set("x-forwarded-for", clientIp);
582
+ }
583
+ const controller = new AbortController;
584
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
585
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
586
+ const body = hasBody ? await request.arrayBuffer() : undefined;
587
+ const upstreamResponse = await fetch(targetUrl.toString(), {
588
+ method: request.method,
589
+ headers: upstreamHeaders,
590
+ body,
591
+ redirect: "follow",
592
+ signal: controller.signal
593
+ });
594
+ clearTimeout(timer);
595
+ if (targetUrl.hostname.includes("googletagmanager.com") && targetUrl.pathname.includes("/gtm.js")) {
596
+ const rawCode = await upstreamResponse.text();
597
+ const morphedCode = morphGtmScript(rawCode, relayEndpoint);
598
+ const isDebug = targetUrl.searchParams.has("gtm_debug");
599
+ return new Response(morphedCode, {
600
+ status: upstreamResponse.status,
601
+ headers: {
602
+ "Content-Type": "application/javascript; charset=UTF-8",
603
+ "Access-Control-Allow-Origin": "*",
604
+ "Cache-Control": isDebug ? "no-cache, no-store, must-revalidate" : "public, max-age=900"
605
+ }
606
+ });
607
+ }
608
+ const responseHeaders = new Headers(upstreamResponse.headers);
609
+ responseHeaders.delete("transfer-encoding");
610
+ responseHeaders.delete("content-encoding");
611
+ responseHeaders.delete("content-length");
612
+ responseHeaders.set("Access-Control-Allow-Origin", "*");
613
+ responseHeaders.set("Access-Control-Allow-Headers", `*, ${headerName}`);
614
+ return new Response(upstreamResponse.body, {
615
+ status: upstreamResponse.status,
616
+ statusText: upstreamResponse.statusText,
617
+ headers: responseHeaders
618
+ });
619
+ } catch (err) {
620
+ const message = err instanceof Error ? err.message : "Proxy forwarding failed";
621
+ return new Response(JSON.stringify({ error: message }), {
622
+ status: 502,
623
+ headers: { "Content-Type": "application/json" }
624
+ });
625
+ }
626
+ };
627
+ async function handleMorphedGtmRequest(request, reqUrl) {
628
+ const containerId = reqUrl.searchParams.get("cid") || reqUrl.searchParams.get("container") || reqUrl.searchParams.get("tag") || reqUrl.searchParams.get("id");
629
+ if (!containerId || !/^[A-Z0-9-_]+$/i.test(containerId)) {
630
+ return new Response(JSON.stringify({ error: "Invalid or missing container id" }), {
631
+ status: 400,
632
+ headers: { "Content-Type": "application/json" }
633
+ });
634
+ }
635
+ const upstreamUrl = new URL("https://www.googletagmanager.com/gtm.js");
636
+ upstreamUrl.searchParams.set("id", containerId);
637
+ reqUrl.searchParams.forEach((val, key) => {
638
+ if (key !== "cid" && key !== "container" && key !== "tag" && key !== "id") {
639
+ upstreamUrl.searchParams.set(key, val);
640
+ }
641
+ });
642
+ try {
643
+ const upstreamRes = await fetch(upstreamUrl.toString(), {
644
+ headers: {
645
+ "User-Agent": request.headers.get("user-agent") || "Mozilla/5.0",
646
+ Accept: "application/javascript, */*"
647
+ }
648
+ });
649
+ if (!upstreamRes.ok) {
650
+ return new Response(JSON.stringify({ error: `Upstream GTM returned ${upstreamRes.status}` }), {
651
+ status: upstreamRes.status,
652
+ headers: { "Content-Type": "application/json" }
653
+ });
654
+ }
655
+ const rawCode = await upstreamRes.text();
656
+ const morphedCode = morphGtmScript(rawCode, relayEndpoint);
657
+ const isDebug = reqUrl.searchParams.has("gtm_debug");
658
+ return new Response(morphedCode, {
659
+ status: 200,
660
+ headers: {
661
+ "Content-Type": "application/javascript; charset=UTF-8",
662
+ "Access-Control-Allow-Origin": "*",
663
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
664
+ "Cache-Control": isDebug ? "no-cache, no-store, must-revalidate" : "public, max-age=900"
665
+ }
666
+ });
667
+ } catch (err) {
668
+ const msg = err instanceof Error ? err.message : "Failed to fetch upstream GTM";
669
+ return new Response(JSON.stringify({ error: msg }), {
670
+ status: 502,
671
+ headers: { "Content-Type": "application/json" }
672
+ });
673
+ }
674
+ }
675
+ }
676
+ function matchesAllowlist(hostname, allowlist) {
677
+ const cleanHost = hostname.toLowerCase();
678
+ for (const rule of allowlist) {
679
+ if (rule instanceof RegExp) {
680
+ if (rule.test(cleanHost))
681
+ return true;
682
+ } else {
683
+ const cleanRule = rule.toLowerCase().trim();
684
+ if (cleanHost === cleanRule || cleanHost.endsWith(`.${cleanRule}`)) {
685
+ return true;
686
+ }
687
+ }
688
+ }
689
+ return false;
690
+ }
691
+ // src/custom-element.ts
692
+ class TatvicWrapperElement extends (typeof HTMLElement !== "undefined" ? HTMLElement : Object) {
693
+ connectedCallback() {
694
+ if (typeof window === "undefined")
695
+ return;
696
+ if (this.style) {
697
+ this.style.display = "contents";
698
+ }
699
+ const endpoint = this.getAttribute ? this.getAttribute("endpoint") ?? undefined : undefined;
700
+ const debug = this.getAttribute ? this.getAttribute("debug") === "true" : false;
701
+ const serverPreview = this.getAttribute ? this.getAttribute("server-preview") || this.getAttribute("server-preview-token") || undefined : undefined;
702
+ const options = {};
703
+ if (endpoint)
704
+ options.proxyEndpoint = endpoint;
705
+ if (debug)
706
+ options.debug = true;
707
+ if (serverPreview)
708
+ options.serverPreviewToken = serverPreview;
709
+ initFirstPartyWrapper(options);
710
+ const morphScripts = () => {
711
+ try {
712
+ if (this.querySelectorAll && typeof document !== "undefined") {
713
+ const scripts = this.querySelectorAll("script[src]");
714
+ scripts.forEach((s) => {
715
+ const src = s.getAttribute("src");
716
+ if (!src)
717
+ return;
718
+ if (src.includes("googletagmanager.com/gtm.js") || src.includes("/_tatvic/gtm.js") || src.includes("/_tatvic/loader.js")) {
719
+ const pageSearch = typeof window !== "undefined" && window.location ? window.location.search : "";
720
+ const pageParams = new URLSearchParams(pageSearch);
721
+ const targetUrl = new URL(src, typeof window !== "undefined" && window.location ? window.location.origin : "http://localhost");
722
+ const containerId = targetUrl.searchParams.get("cid") || targetUrl.searchParams.get("container") || targetUrl.searchParams.get("tag") || targetUrl.searchParams.get("id") || "GTM-T32XGF8F";
723
+ const morphedParams = new URLSearchParams;
724
+ morphedParams.set("cid", containerId);
725
+ targetUrl.searchParams.forEach((v, k) => {
726
+ if (k !== "id" && k !== "cid" && k !== "container" && k !== "tag") {
727
+ morphedParams.set(k, v);
728
+ }
729
+ });
730
+ for (const param of ["gtm_debug", "gtm_auth", "gtm_preview", "gtm_cookies_win"]) {
731
+ if (pageParams.has(param) && !morphedParams.has(param)) {
732
+ morphedParams.set(param, pageParams.get(param));
733
+ }
734
+ }
735
+ const morphedUrl = `/_tatvic/loader.js?${morphedParams.toString()}`;
736
+ if (s.getAttribute("src") !== morphedUrl) {
737
+ s.setAttribute("src", morphedUrl);
738
+ const replacement = document.createElement("script");
739
+ replacement.async = s.hasAttribute("async");
740
+ replacement.src = morphedUrl;
741
+ s.parentNode?.replaceChild(replacement, s);
742
+ }
743
+ return;
744
+ }
745
+ if (!src.startsWith("/_tatvic/")) {
746
+ const separator = (options.proxyEndpoint || "/_tatvic/relay").includes("?") ? "&" : "?";
747
+ const proxied = `${options.proxyEndpoint || "/_tatvic/relay"}${separator}_tgt=${encodeURIComponent(src)}`;
748
+ s.setAttribute("src", proxied);
749
+ }
750
+ });
751
+ }
752
+ } catch {}
753
+ };
754
+ morphScripts();
755
+ if (typeof queueMicrotask !== "undefined") {
756
+ queueMicrotask(morphScripts);
757
+ } else if (typeof setTimeout !== "undefined") {
758
+ setTimeout(morphScripts, 0);
759
+ }
760
+ }
761
+ }
762
+ if (typeof window !== "undefined" && typeof customElements !== "undefined" && !customElements.get("tatvic-wrapper")) {
763
+ try {
764
+ customElements.define("tatvic-wrapper", TatvicWrapperElement);
765
+ } catch {}
766
+ }
767
+ // src/react.tsx
768
+ import React, { useEffect } from "react";
769
+ "use client";
770
+ function TatvicWrapper({
771
+ children,
772
+ className,
773
+ style,
774
+ ...options
775
+ }) {
776
+ useEffect(() => {
777
+ initFirstPartyWrapper(options);
778
+ }, [options.proxyEndpoint, options.debug]);
779
+ return React.createElement("tatvic-wrapper", {
780
+ class: className,
781
+ style: { display: "contents", ...style },
782
+ endpoint: options.proxyEndpoint,
783
+ debug: options.debug ? "true" : undefined
784
+ }, children);
785
+ }
786
+ // src/next.ts
787
+ function tatvicRelayRoute(options = {}) {
788
+ const handler = createProxyHandler(options);
789
+ return {
790
+ GET: handler,
791
+ POST: handler,
792
+ OPTIONS: handler
793
+ };
794
+ }
795
+ // src/express.ts
796
+ function tatvicExpressMiddleware(options = {}) {
797
+ const handler = createProxyHandler(options);
798
+ return async function(req, res, next) {
799
+ try {
800
+ const host = req.headers["host"] || "localhost";
801
+ const fullUrl = `http://${host}${req.originalUrl || req.url || "/_tatvic/relay"}`;
802
+ const headers = new Headers;
803
+ for (const [key, val] of Object.entries(req.headers)) {
804
+ if (Array.isArray(val)) {
805
+ val.forEach((v) => headers.append(key, v));
806
+ } else if (typeof val === "string") {
807
+ headers.set(key, val);
808
+ }
809
+ }
810
+ let body = undefined;
811
+ if (req.method !== "GET" && req.method !== "HEAD") {
812
+ if (typeof req.body === "string" || Buffer.isBuffer(req.body)) {
813
+ body = req.body;
814
+ } else if (req.body && typeof req.body === "object") {
815
+ body = JSON.stringify(req.body);
816
+ }
817
+ }
818
+ const webRequest = new Request(fullUrl, {
819
+ method: req.method,
820
+ headers,
821
+ body
822
+ });
823
+ const webResponse = await handler(webRequest);
824
+ res.status(webResponse.status);
825
+ webResponse.headers.forEach((value, key) => {
826
+ res.setHeader(key, value);
827
+ });
828
+ const arrayBuffer = await webResponse.arrayBuffer();
829
+ res.end(Buffer.from(arrayBuffer));
830
+ } catch (err) {
831
+ if (next) {
832
+ next(err);
833
+ } else {
834
+ res.status(500).send("Tatvic Relay Error");
835
+ }
836
+ }
837
+ };
838
+ }
839
+
840
+ // src/index.ts
841
+ var activeInterceptor = null;
842
+ function initFirstPartyWrapper(options = {}) {
843
+ if (activeInterceptor) {
844
+ activeInterceptor.uninstall();
845
+ }
846
+ activeInterceptor = new RequestInterceptor(options);
847
+ activeInterceptor.install();
848
+ return activeInterceptor;
849
+ }
850
+ function uninstallFirstPartyWrapper() {
851
+ if (activeInterceptor) {
852
+ activeInterceptor.uninstall();
853
+ activeInterceptor = null;
854
+ }
855
+ }
856
+ export {
857
+ DEFAULT_EXCLUDE_DOMAINS,
858
+ DEFAULT_HEADER_NAME,
859
+ DEFAULT_PROXY_ENDPOINT,
860
+ DEFAULT_TARGET_DOMAINS,
861
+ RequestInterceptor,
862
+ TatvicWrapper,
863
+ TatvicWrapperElement,
864
+ UrlMatcher,
865
+ createProxyHandler,
866
+ initFirstPartyWrapper,
867
+ morphGtmScript,
868
+ tatvicExpressMiddleware,
869
+ tatvicRelayRoute,
870
+ uninstallFirstPartyWrapper
871
+ };