@linkforty/core 1.44.1 → 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.
Files changed (175) hide show
  1. package/README.md +20 -588
  2. package/llms.txt +1 -1
  3. package/package.json +2 -1
  4. package/dist/index.config.test.d.ts +0 -2
  5. package/dist/index.config.test.d.ts.map +0 -1
  6. package/dist/index.config.test.js +0 -87
  7. package/dist/index.config.test.js.map +0 -1
  8. package/dist/lib/bot-detection.test.d.ts +0 -2
  9. package/dist/lib/bot-detection.test.d.ts.map +0 -1
  10. package/dist/lib/bot-detection.test.js +0 -65
  11. package/dist/lib/bot-detection.test.js.map +0 -1
  12. package/dist/lib/client-ip.test.d.ts +0 -7
  13. package/dist/lib/client-ip.test.d.ts.map +0 -1
  14. package/dist/lib/client-ip.test.js +0 -114
  15. package/dist/lib/client-ip.test.js.map +0 -1
  16. package/dist/lib/config.test.d.ts +0 -2
  17. package/dist/lib/config.test.d.ts.map +0 -1
  18. package/dist/lib/config.test.js +0 -126
  19. package/dist/lib/config.test.js.map +0 -1
  20. package/dist/lib/database.test.d.ts +0 -2
  21. package/dist/lib/database.test.d.ts.map +0 -1
  22. package/dist/lib/database.test.js +0 -180
  23. package/dist/lib/database.test.js.map +0 -1
  24. package/dist/lib/deep-link-payload.test.d.ts +0 -2
  25. package/dist/lib/deep-link-payload.test.d.ts.map +0 -1
  26. package/dist/lib/deep-link-payload.test.js +0 -202
  27. package/dist/lib/deep-link-payload.test.js.map +0 -1
  28. package/dist/lib/event-emitter.test.d.ts +0 -2
  29. package/dist/lib/event-emitter.test.d.ts.map +0 -1
  30. package/dist/lib/event-emitter.test.js +0 -162
  31. package/dist/lib/event-emitter.test.js.map +0 -1
  32. package/dist/lib/fingerprint.test.d.ts +0 -2
  33. package/dist/lib/fingerprint.test.d.ts.map +0 -1
  34. package/dist/lib/fingerprint.test.js +0 -750
  35. package/dist/lib/fingerprint.test.js.map +0 -1
  36. package/dist/lib/launchpad.test.d.ts +0 -2
  37. package/dist/lib/launchpad.test.d.ts.map +0 -1
  38. package/dist/lib/launchpad.test.js +0 -368
  39. package/dist/lib/launchpad.test.js.map +0 -1
  40. package/dist/lib/link-lookup.test.d.ts +0 -2
  41. package/dist/lib/link-lookup.test.d.ts.map +0 -1
  42. package/dist/lib/link-lookup.test.js +0 -61
  43. package/dist/lib/link-lookup.test.js.map +0 -1
  44. package/dist/lib/link-resolution-cache.test.d.ts +0 -2
  45. package/dist/lib/link-resolution-cache.test.d.ts.map +0 -1
  46. package/dist/lib/link-resolution-cache.test.js +0 -45
  47. package/dist/lib/link-resolution-cache.test.js.map +0 -1
  48. package/dist/lib/link-safety.test.d.ts +0 -2
  49. package/dist/lib/link-safety.test.d.ts.map +0 -1
  50. package/dist/lib/link-safety.test.js +0 -211
  51. package/dist/lib/link-safety.test.js.map +0 -1
  52. package/dist/lib/not-found-page.test.d.ts +0 -2
  53. package/dist/lib/not-found-page.test.d.ts.map +0 -1
  54. package/dist/lib/not-found-page.test.js +0 -151
  55. package/dist/lib/not-found-page.test.js.map +0 -1
  56. package/dist/lib/portable/cli.test.d.ts +0 -2
  57. package/dist/lib/portable/cli.test.d.ts.map +0 -1
  58. package/dist/lib/portable/cli.test.js +0 -98
  59. package/dist/lib/portable/cli.test.js.map +0 -1
  60. package/dist/lib/portable/portable.integration.test.d.ts +0 -2
  61. package/dist/lib/portable/portable.integration.test.d.ts.map +0 -1
  62. package/dist/lib/portable/portable.integration.test.js +0 -139
  63. package/dist/lib/portable/portable.integration.test.js.map +0 -1
  64. package/dist/lib/portable/registry.schema.test.d.ts +0 -2
  65. package/dist/lib/portable/registry.schema.test.d.ts.map +0 -1
  66. package/dist/lib/portable/registry.schema.test.js +0 -59
  67. package/dist/lib/portable/registry.schema.test.js.map +0 -1
  68. package/dist/lib/scanner-detection.test.d.ts +0 -2
  69. package/dist/lib/scanner-detection.test.d.ts.map +0 -1
  70. package/dist/lib/scanner-detection.test.js +0 -124
  71. package/dist/lib/scanner-detection.test.js.map +0 -1
  72. package/dist/lib/schema-guard.test.d.ts +0 -2
  73. package/dist/lib/schema-guard.test.d.ts.map +0 -1
  74. package/dist/lib/schema-guard.test.js +0 -83
  75. package/dist/lib/schema-guard.test.js.map +0 -1
  76. package/dist/lib/utils.test.d.ts +0 -2
  77. package/dist/lib/utils.test.d.ts.map +0 -1
  78. package/dist/lib/utils.test.js +0 -261
  79. package/dist/lib/utils.test.js.map +0 -1
  80. package/dist/lib/webhook.test.d.ts +0 -2
  81. package/dist/lib/webhook.test.d.ts.map +0 -1
  82. package/dist/lib/webhook.test.js +0 -340
  83. package/dist/lib/webhook.test.js.map +0 -1
  84. package/dist/routes/health.test.d.ts +0 -2
  85. package/dist/routes/health.test.d.ts.map +0 -1
  86. package/dist/routes/health.test.js +0 -82
  87. package/dist/routes/health.test.js.map +0 -1
  88. package/dist/routes/preview.test.d.ts +0 -2
  89. package/dist/routes/preview.test.d.ts.map +0 -1
  90. package/dist/routes/preview.test.js +0 -111
  91. package/dist/routes/preview.test.js.map +0 -1
  92. package/dist/routes/qr.test.d.ts +0 -2
  93. package/dist/routes/qr.test.d.ts.map +0 -1
  94. package/dist/routes/qr.test.js +0 -67
  95. package/dist/routes/qr.test.js.map +0 -1
  96. package/dist/routes/redirect.click-event.test.d.ts +0 -2
  97. package/dist/routes/redirect.click-event.test.d.ts.map +0 -1
  98. package/dist/routes/redirect.click-event.test.js +0 -85
  99. package/dist/routes/redirect.click-event.test.js.map +0 -1
  100. package/dist/routes/redirect.host-scope.integration.test.d.ts +0 -2
  101. package/dist/routes/redirect.host-scope.integration.test.d.ts.map +0 -1
  102. package/dist/routes/redirect.host-scope.integration.test.js +0 -105
  103. package/dist/routes/redirect.host-scope.integration.test.js.map +0 -1
  104. package/dist/routes/redirect.interstitial.test.d.ts +0 -2
  105. package/dist/routes/redirect.interstitial.test.d.ts.map +0 -1
  106. package/dist/routes/redirect.interstitial.test.js +0 -128
  107. package/dist/routes/redirect.interstitial.test.js.map +0 -1
  108. package/dist/routes/redirect.launchpad.test.d.ts +0 -2
  109. package/dist/routes/redirect.launchpad.test.d.ts.map +0 -1
  110. package/dist/routes/redirect.launchpad.test.js +0 -515
  111. package/dist/routes/redirect.launchpad.test.js.map +0 -1
  112. package/dist/routes/redirect.no-web-destination.test.d.ts +0 -2
  113. package/dist/routes/redirect.no-web-destination.test.d.ts.map +0 -1
  114. package/dist/routes/redirect.no-web-destination.test.js +0 -176
  115. package/dist/routes/redirect.no-web-destination.test.js.map +0 -1
  116. package/dist/routes/redirect.not-found.test.d.ts +0 -2
  117. package/dist/routes/redirect.not-found.test.d.ts.map +0 -1
  118. package/dist/routes/redirect.not-found.test.js +0 -280
  119. package/dist/routes/redirect.not-found.test.js.map +0 -1
  120. package/dist/routes/redirect.safety.test.d.ts +0 -2
  121. package/dist/routes/redirect.safety.test.d.ts.map +0 -1
  122. package/dist/routes/redirect.safety.test.js +0 -284
  123. package/dist/routes/redirect.safety.test.js.map +0 -1
  124. package/dist/routes/redirect.store-url.test.d.ts +0 -2
  125. package/dist/routes/redirect.store-url.test.d.ts.map +0 -1
  126. package/dist/routes/redirect.store-url.test.js +0 -239
  127. package/dist/routes/redirect.store-url.test.js.map +0 -1
  128. package/dist/routes/redirect.test.d.ts +0 -2
  129. package/dist/routes/redirect.test.d.ts.map +0 -1
  130. package/dist/routes/redirect.test.js +0 -158
  131. package/dist/routes/redirect.test.js.map +0 -1
  132. package/dist/routes/redirect.web-link.test.d.ts +0 -2
  133. package/dist/routes/redirect.web-link.test.d.ts.map +0 -1
  134. package/dist/routes/redirect.web-link.test.js +0 -138
  135. package/dist/routes/redirect.web-link.test.js.map +0 -1
  136. package/dist/routes/sdk-cache-bypass.test.d.ts +0 -2
  137. package/dist/routes/sdk-cache-bypass.test.d.ts.map +0 -1
  138. package/dist/routes/sdk-cache-bypass.test.js +0 -175
  139. package/dist/routes/sdk-cache-bypass.test.js.map +0 -1
  140. package/dist/routes/sdk.device-values.integration.test.d.ts +0 -2
  141. package/dist/routes/sdk.device-values.integration.test.d.ts.map +0 -1
  142. package/dist/routes/sdk.device-values.integration.test.js +0 -132
  143. package/dist/routes/sdk.device-values.integration.test.js.map +0 -1
  144. package/dist/routes/sdk.event.test.d.ts +0 -2
  145. package/dist/routes/sdk.event.test.d.ts.map +0 -1
  146. package/dist/routes/sdk.event.test.js +0 -284
  147. package/dist/routes/sdk.event.test.js.map +0 -1
  148. package/dist/routes/sdk.install-certainty.integration.test.d.ts +0 -2
  149. package/dist/routes/sdk.install-certainty.integration.test.d.ts.map +0 -1
  150. package/dist/routes/sdk.install-certainty.integration.test.js +0 -104
  151. package/dist/routes/sdk.install-certainty.integration.test.js.map +0 -1
  152. package/dist/routes/sdk.install-live-links.integration.test.d.ts +0 -2
  153. package/dist/routes/sdk.install-live-links.integration.test.d.ts.map +0 -1
  154. package/dist/routes/sdk.install-live-links.integration.test.js +0 -92
  155. package/dist/routes/sdk.install-live-links.integration.test.js.map +0 -1
  156. package/dist/routes/sdk.install-referrer.integration.test.d.ts +0 -2
  157. package/dist/routes/sdk.install-referrer.integration.test.d.ts.map +0 -1
  158. package/dist/routes/sdk.install-referrer.integration.test.js +0 -187
  159. package/dist/routes/sdk.install-referrer.integration.test.js.map +0 -1
  160. package/dist/routes/sdk.install-window-once.integration.test.d.ts +0 -2
  161. package/dist/routes/sdk.install-window-once.integration.test.d.ts.map +0 -1
  162. package/dist/routes/sdk.install-window-once.integration.test.js +0 -147
  163. package/dist/routes/sdk.install-window-once.integration.test.js.map +0 -1
  164. package/dist/routes/sdk.install.test.d.ts +0 -2
  165. package/dist/routes/sdk.install.test.d.ts.map +0 -1
  166. package/dist/routes/sdk.install.test.js +0 -99
  167. package/dist/routes/sdk.install.test.js.map +0 -1
  168. package/dist/routes/sdk.resolve-click.test.d.ts +0 -2
  169. package/dist/routes/sdk.resolve-click.test.d.ts.map +0 -1
  170. package/dist/routes/sdk.resolve-click.test.js +0 -79
  171. package/dist/routes/sdk.resolve-click.test.js.map +0 -1
  172. package/dist/routes/sdk.resolve-scope.test.d.ts +0 -2
  173. package/dist/routes/sdk.resolve-scope.test.d.ts.map +0 -1
  174. package/dist/routes/sdk.resolve-scope.test.js +0 -105
  175. package/dist/routes/sdk.resolve-scope.test.js.map +0 -1
@@ -1,261 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { generateShortCode, parseUserAgent, buildRedirectUrl, detectDevice, getLocationFromIP, resolveClickUtms, extractLinkParams, } from './utils';
3
- describe('generateShortCode', () => {
4
- it('should generate a short code of default length 8', () => {
5
- const code = generateShortCode();
6
- expect(code).toHaveLength(8);
7
- expect(typeof code).toBe('string');
8
- });
9
- it('should generate a short code of custom length', () => {
10
- const code = generateShortCode(12);
11
- expect(code).toHaveLength(12);
12
- });
13
- it('should generate unique codes', () => {
14
- const code1 = generateShortCode();
15
- const code2 = generateShortCode();
16
- expect(code1).not.toBe(code2);
17
- });
18
- });
19
- describe('parseUserAgent', () => {
20
- it('should parse Chrome on Windows user agent', () => {
21
- const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
22
- const result = parseUserAgent(ua);
23
- expect(result.deviceType).toBe('desktop');
24
- expect(result.platform).toBe('Windows');
25
- expect(result.browser).toBe('Chrome');
26
- });
27
- it('should parse iPhone user agent', () => {
28
- const ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1';
29
- const result = parseUserAgent(ua);
30
- expect(result.deviceType).toBe('mobile');
31
- expect(result.platform).toBe('iOS');
32
- expect(result.browser).toBe('Mobile Safari');
33
- });
34
- it('should parse Android user agent', () => {
35
- const ua = 'Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36';
36
- const result = parseUserAgent(ua);
37
- expect(result.deviceType).toBe('mobile');
38
- expect(result.platform).toBe('Android');
39
- expect(result.browser).toBe('Chrome');
40
- });
41
- it('should handle empty user agent', () => {
42
- const result = parseUserAgent('');
43
- expect(result.deviceType).toBe('desktop');
44
- expect(result.platform).toBe('unknown');
45
- expect(result.browser).toBe('unknown');
46
- });
47
- });
48
- describe('detectDevice', () => {
49
- it('should detect iOS devices', () => {
50
- expect(detectDevice('Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X)')).toBe('ios');
51
- expect(detectDevice('Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X)')).toBe('ios');
52
- expect(detectDevice('Mozilla/5.0 (iPod touch; CPU iPhone OS 14_6 like Mac OS X)')).toBe('ios');
53
- });
54
- it('should detect Android devices', () => {
55
- expect(detectDevice('Mozilla/5.0 (Linux; Android 11; Pixel 5)')).toBe('android');
56
- expect(detectDevice('Mozilla/5.0 (Linux; Android 10; SM-G973F)')).toBe('android');
57
- });
58
- it('should default to web for desktop browsers', () => {
59
- expect(detectDevice('Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0')).toBe('web');
60
- expect(detectDevice('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)')).toBe('web');
61
- });
62
- it('should be case insensitive', () => {
63
- expect(detectDevice('MOZILLA/5.0 (IPHONE; CPU IPHONE OS 14_6)')).toBe('ios');
64
- expect(detectDevice('MOZILLA/5.0 (LINUX; ANDROID 11)')).toBe('android');
65
- });
66
- });
67
- describe('buildRedirectUrl', () => {
68
- it('should return original URL when no UTM parameters provided', () => {
69
- const url = 'https://example.com/page';
70
- const result = buildRedirectUrl(url);
71
- expect(result).toBe(url);
72
- });
73
- it('should append UTM parameters to URL', () => {
74
- const url = 'https://example.com/page';
75
- const utmParams = {
76
- source: 'newsletter',
77
- medium: 'email',
78
- campaign: 'summer-sale',
79
- };
80
- const result = buildRedirectUrl(url, utmParams);
81
- expect(result).toContain('utm_source=newsletter');
82
- expect(result).toContain('utm_medium=email');
83
- expect(result).toContain('utm_campaign=summer-sale');
84
- });
85
- it('should preserve existing query parameters', () => {
86
- const url = 'https://example.com/page?foo=bar';
87
- const utmParams = { source: 'google' };
88
- const result = buildRedirectUrl(url, utmParams);
89
- expect(result).toContain('foo=bar');
90
- expect(result).toContain('utm_source=google');
91
- });
92
- it('should skip UTM parameters with empty values', () => {
93
- const url = 'https://example.com/page';
94
- const utmParams = {
95
- source: 'twitter',
96
- medium: '',
97
- campaign: 'promo',
98
- };
99
- const result = buildRedirectUrl(url, utmParams);
100
- expect(result).toContain('utm_source=twitter');
101
- expect(result).not.toContain('utm_medium');
102
- expect(result).toContain('utm_campaign=promo');
103
- });
104
- it('should URL encode UTM parameter values', () => {
105
- const url = 'https://example.com/page';
106
- const utmParams = {
107
- source: 'email newsletter',
108
- campaign: 'summer sale 2024',
109
- };
110
- const result = buildRedirectUrl(url, utmParams);
111
- expect(result).toContain('utm_source=email+newsletter');
112
- expect(result).toContain('utm_campaign=summer+sale+2024');
113
- });
114
- });
115
- describe('getLocationFromIP', () => {
116
- it('should return null values for invalid IP', () => {
117
- const result = getLocationFromIP('invalid-ip');
118
- expect(result.countryCode).toBeNull();
119
- expect(result.countryName).toBeNull();
120
- expect(result.city).toBeNull();
121
- expect(result.latitude).toBeNull();
122
- expect(result.longitude).toBeNull();
123
- });
124
- it('should return null values for private IP addresses', () => {
125
- const result = getLocationFromIP('192.168.1.1');
126
- expect(result.countryCode).toBeNull();
127
- expect(result.countryName).toBeNull();
128
- });
129
- it('should parse public IP addresses', () => {
130
- // Google DNS IP (known to be US-based)
131
- const result = getLocationFromIP('8.8.8.8');
132
- expect(result.countryCode).toBe('US');
133
- expect(result.countryName).toBe('United States');
134
- expect(result.latitude).toBeDefined();
135
- expect(result.longitude).toBeDefined();
136
- });
137
- it('should handle countries not in the mapping', () => {
138
- // This test might need adjustment based on actual geoip-lite behavior
139
- // but demonstrates the fallback logic
140
- const result = getLocationFromIP('8.8.8.8');
141
- if (result.countryCode && !result.countryName) {
142
- // Fallback to country code
143
- expect(result.countryName).toBe(result.countryCode);
144
- }
145
- else {
146
- // Has a mapped name
147
- expect(typeof result.countryName).toBe('string');
148
- }
149
- });
150
- });
151
- describe('resolveClickUtms', () => {
152
- const CONFIG = { source: 'youtube', medium: 'social', campaign: 'time_video' };
153
- it('uses inbound query-string values when the link has no configured UTMs', () => {
154
- expect(resolveClickUtms({ utm_source: 'fb', utm_medium: 'cpc', utm_campaign: 'launch' }, undefined))
155
- .toEqual({ utmSource: 'fb', utmMedium: 'cpc', utmCampaign: 'launch' });
156
- });
157
- it('falls back to the link configuration when nothing is inbound', () => {
158
- expect(resolveClickUtms({}, CONFIG))
159
- .toEqual({ utmSource: 'youtube', utmMedium: 'social', utmCampaign: 'time_video' });
160
- });
161
- it('lets inbound win over configuration on conflict', () => {
162
- // An ad platform appending a per-ad campaign id must not be overwritten by
163
- // the link's static label.
164
- expect(resolveClickUtms({ utm_source: 'an', utm_campaign: '120252316702960562' }, { source: 'test', campaign: 'test' }))
165
- .toMatchObject({ utmSource: 'an', utmCampaign: '120252316702960562' });
166
- });
167
- it('mixes the two sources per key rather than all-or-nothing', () => {
168
- expect(resolveClickUtms({ utm_source: 'fb' }, CONFIG))
169
- .toEqual({ utmSource: 'fb', utmMedium: 'social', utmCampaign: 'time_video' });
170
- });
171
- it('treats an empty inbound value as absent and falls through to configuration', () => {
172
- expect(resolveClickUtms({ utm_source: '' }, CONFIG).utmSource).toBe('youtube');
173
- });
174
- it('treats a whitespace-only inbound value as absent', () => {
175
- expect(resolveClickUtms({ utm_source: ' ' }, CONFIG).utmSource).toBe('youtube');
176
- });
177
- it('never returns an empty string when configuration holds a blank', () => {
178
- // Real production shape: a link saved with campaign set to ''.
179
- const result = resolveClickUtms({}, { source: 'clevertap', medium: 'email', campaign: '' });
180
- expect(result.utmCampaign).toBeUndefined();
181
- expect(result.utmSource).toBe('clevertap');
182
- });
183
- it('trims values so " ig" and "ig" do not split into two library entries', () => {
184
- expect(resolveClickUtms({ utm_source: ' ig ' }, undefined).utmSource).toBe('ig');
185
- expect(resolveClickUtms({}, { source: ' youtube ' }).utmSource).toBe('youtube');
186
- });
187
- it.each([
188
- ['null', null],
189
- ['an array', []],
190
- ['a JSON string', '{"source":"x"}'],
191
- ['undefined', undefined],
192
- ])('treats %s utm_parameters as absent', (_label, configured) => {
193
- expect(resolveClickUtms({ utm_source: 'fb' }, configured))
194
- .toEqual({ utmSource: 'fb', utmMedium: undefined, utmCampaign: undefined });
195
- });
196
- it('ignores a repeated query parameter arriving as an array', () => {
197
- const query = { utm_source: ['a', 'b'] };
198
- expect(resolveClickUtms(query, CONFIG).utmSource).toBe('youtube');
199
- });
200
- it('returns all-undefined when neither source supplies anything', () => {
201
- expect(resolveClickUtms({}, {}))
202
- .toEqual({ utmSource: undefined, utmMedium: undefined, utmCampaign: undefined });
203
- expect(resolveClickUtms(undefined, undefined))
204
- .toEqual({ utmSource: undefined, utmMedium: undefined, utmCampaign: undefined });
205
- });
206
- it('ignores content and term, which have no click_events columns', () => {
207
- const result = resolveClickUtms({ utm_content: 'cta', utm_term: 'kw' }, {
208
- source: 'frontend', content: 'aasa_footer_cta', term: 'kw',
209
- });
210
- expect(Object.keys(result).sort()).toEqual(['utmCampaign', 'utmMedium', 'utmSource']);
211
- expect(result.utmSource).toBe('frontend');
212
- });
213
- });
214
- describe('extractLinkParams', () => {
215
- it('keeps ordinary parameters', () => {
216
- expect(extractLinkParams({ slug: 'titanic', id: '42' })).toEqual({
217
- slug: 'titanic',
218
- id: '42',
219
- });
220
- });
221
- it('drops utm_ parameters, which have their own columns', () => {
222
- expect(extractLinkParams({ utm_source: 'ig', utm_campaign: 'spring', slug: 'titanic' })).toEqual({ slug: 'titanic' });
223
- });
224
- it('drops fp_ parameters, which feed the fingerprint', () => {
225
- expect(extractLinkParams({ fp_tz: 'UTC', fp_sw: '390', slug: 'titanic' })).toEqual({
226
- slug: 'titanic',
227
- });
228
- });
229
- it('drops lf_click, the correlation id we set ourselves', () => {
230
- expect(extractLinkParams({ lf_click: 'abc-123', slug: 'titanic' })).toEqual({
231
- slug: 'titanic',
232
- });
233
- });
234
- it('matches reserved prefixes case-insensitively', () => {
235
- expect(extractLinkParams({ UTM_Source: 'ig', FP_TZ: 'UTC', LF_Click: 'x' })).toEqual({});
236
- });
237
- it('truncates a value longer than 256 characters', () => {
238
- const result = extractLinkParams({ slug: 'a'.repeat(300) });
239
- expect(result.slug).toHaveLength(256);
240
- });
241
- it('keeps a value of exactly 256 characters intact', () => {
242
- const result = extractLinkParams({ slug: 'a'.repeat(256) });
243
- expect(result.slug).toHaveLength(256);
244
- });
245
- it('stops after 16 parameters', () => {
246
- const query = {};
247
- for (let i = 0; i < 30; i++)
248
- query[`k${i}`] = String(i);
249
- expect(Object.keys(extractLinkParams(query))).toHaveLength(16);
250
- });
251
- it('skips empty and non-string values', () => {
252
- expect(extractLinkParams({ empty: '', missing: undefined, repeated: ['a', 'b'], slug: 'titanic' })).toEqual({ slug: 'titanic' });
253
- });
254
- it('returns {} for an empty query, which is the ordinary case', () => {
255
- expect(extractLinkParams({})).toEqual({});
256
- });
257
- it('returns {} rather than throwing when there is no query at all', () => {
258
- expect(extractLinkParams(undefined)).toEqual({});
259
- });
260
- });
261
- //# sourceMappingURL=utils.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.test.js","sourceRoot":"","sources":["../../src/lib/utils.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=webhook.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"webhook.test.d.ts","sourceRoot":"","sources":["../../src/lib/webhook.test.ts"],"names":[],"mappings":""}
@@ -1,340 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- vi.mock('./database.js', () => ({
3
- db: {
4
- query: vi.fn(),
5
- },
6
- }));
7
- import { generateWebhookSignature, generateWebhookSecret, deliverWebhook, triggerWebhooks, findWebhooksForLink, setWebhookDeliveryLogger, } from './webhook.js';
8
- import { db } from './database.js';
9
- // Helper to build a Webhook fixture
10
- function makeWebhook(overrides = {}) {
11
- return {
12
- id: 'wh-1',
13
- user_id: 'user-1',
14
- name: 'Test Webhook',
15
- url: 'https://example.com/hook',
16
- secret: 'test-secret',
17
- events: ['click_event'],
18
- is_active: true,
19
- retry_count: 1,
20
- timeout_ms: 5000,
21
- headers: {},
22
- created_at: '2024-01-01T00:00:00.000Z',
23
- updated_at: '2024-01-01T00:00:00.000Z',
24
- ...overrides,
25
- };
26
- }
27
- // Helper to build a WebhookPayload fixture
28
- function makePayload(overrides = {}) {
29
- return {
30
- event: 'click_event',
31
- event_id: 'evt-1',
32
- timestamp: '2024-01-01T00:00:00.000Z',
33
- data: { id: 'data-1' },
34
- ...overrides,
35
- };
36
- }
37
- // Helper to make a minimal Response-like mock
38
- function mockResponse(ok, status, statusText, body = '') {
39
- return {
40
- ok,
41
- status,
42
- statusText,
43
- text: () => Promise.resolve(body),
44
- };
45
- }
46
- describe('generateWebhookSignature', () => {
47
- it('returns a hex string', () => {
48
- const sig = generateWebhookSignature('payload', 'secret');
49
- expect(typeof sig).toBe('string');
50
- expect(sig).toMatch(/^[0-9a-f]{64}$/);
51
- });
52
- it('is deterministic for the same inputs', () => {
53
- const sig1 = generateWebhookSignature('hello', 'key');
54
- const sig2 = generateWebhookSignature('hello', 'key');
55
- expect(sig1).toBe(sig2);
56
- });
57
- it('produces different signatures for different payloads', () => {
58
- const sig1 = generateWebhookSignature('payload-a', 'secret');
59
- const sig2 = generateWebhookSignature('payload-b', 'secret');
60
- expect(sig1).not.toBe(sig2);
61
- });
62
- it('produces different signatures for different secrets', () => {
63
- const sig1 = generateWebhookSignature('payload', 'secret-a');
64
- const sig2 = generateWebhookSignature('payload', 'secret-b');
65
- expect(sig1).not.toBe(sig2);
66
- });
67
- });
68
- describe('generateWebhookSecret', () => {
69
- it('returns a 64-character hex string (32 random bytes)', () => {
70
- const secret = generateWebhookSecret();
71
- expect(typeof secret).toBe('string');
72
- expect(secret).toMatch(/^[0-9a-f]{64}$/);
73
- });
74
- it('returns unique values on each call', () => {
75
- const s1 = generateWebhookSecret();
76
- const s2 = generateWebhookSecret();
77
- expect(s1).not.toBe(s2);
78
- });
79
- });
80
- describe('deliverWebhook', () => {
81
- beforeEach(() => {
82
- vi.useFakeTimers();
83
- });
84
- afterEach(() => {
85
- vi.restoreAllMocks();
86
- vi.unstubAllGlobals();
87
- vi.useRealTimers();
88
- });
89
- it('returns a success result on HTTP 200', async () => {
90
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK', 'received')));
91
- const webhook = makeWebhook();
92
- const payload = makePayload();
93
- const result = await deliverWebhook(webhook, payload);
94
- expect(result.success).toBe(true);
95
- expect(result.webhookId).toBe(webhook.id);
96
- expect(result.eventType).toBe(payload.event);
97
- expect(result.eventId).toBe(payload.event_id);
98
- expect(result.responseStatus).toBe(200);
99
- expect(result.responseBody).toBe('received');
100
- expect(result.attemptNumber).toBe(1);
101
- expect(result.deliveredAt).toBeDefined();
102
- expect(result.errorMessage).toBeUndefined();
103
- });
104
- it('returns a failure result on HTTP 500', async () => {
105
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(false, 500, 'Internal Server Error', 'error body')));
106
- // Use logDelivery to capture per-attempt details (the final return is a
107
- // generic "Failed after N attempts" object once retries are exhausted).
108
- const logDelivery = vi.fn().mockResolvedValue(undefined);
109
- const webhook = makeWebhook({ retry_count: 1 });
110
- const payload = makePayload();
111
- const result = await deliverWebhook(webhook, payload, logDelivery);
112
- // Per-attempt result captured via logDelivery
113
- const attemptResult = logDelivery.mock.calls[0][0];
114
- expect(attemptResult.responseStatus).toBe(500);
115
- expect(attemptResult.errorMessage).toBe('HTTP 500: Internal Server Error');
116
- expect(attemptResult.responseBody).toBe('error body');
117
- // Final returned result after retries exhausted
118
- expect(result.success).toBe(false);
119
- expect(result.deliveredAt).toBeUndefined();
120
- });
121
- it('sets correct request headers', async () => {
122
- const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
123
- vi.stubGlobal('fetch', mockFetch);
124
- const webhook = makeWebhook({ headers: { 'X-Custom': 'value' } });
125
- const payload = makePayload();
126
- await deliverWebhook(webhook, payload);
127
- const [url, init] = mockFetch.mock.calls[0];
128
- expect(url).toBe(webhook.url);
129
- expect(init.method).toBe('POST');
130
- const headers = init.headers;
131
- expect(headers['Content-Type']).toBe('application/json');
132
- expect(headers['X-LinkForty-Event']).toBe(payload.event);
133
- expect(headers['X-LinkForty-Event-ID']).toBe(payload.event_id);
134
- expect(headers['X-LinkForty-Signature']).toMatch(/^sha256=[0-9a-f]{64}$/);
135
- expect(headers['User-Agent']).toBe('LinkForty-Webhook/1.0');
136
- expect(headers['X-Custom']).toBe('value');
137
- });
138
- it('sends the correct JSON body', async () => {
139
- const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
140
- vi.stubGlobal('fetch', mockFetch);
141
- const webhook = makeWebhook();
142
- const payload = makePayload();
143
- await deliverWebhook(webhook, payload);
144
- const [, init] = mockFetch.mock.calls[0];
145
- expect(JSON.parse(init.body)).toEqual(payload);
146
- });
147
- it('truncates response body to 1000 characters', async () => {
148
- const longBody = 'x'.repeat(2000);
149
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK', longBody)));
150
- const result = await deliverWebhook(makeWebhook(), makePayload());
151
- expect(result.responseBody).toHaveLength(1000);
152
- });
153
- it('retries on failure and succeeds on second attempt', async () => {
154
- const mockFetch = vi.fn()
155
- .mockResolvedValueOnce(mockResponse(false, 503, 'Service Unavailable'))
156
- .mockResolvedValueOnce(mockResponse(true, 200, 'OK'));
157
- vi.stubGlobal('fetch', mockFetch);
158
- const webhook = makeWebhook({ retry_count: 2 });
159
- const payload = makePayload();
160
- const promise = deliverWebhook(webhook, payload);
161
- await vi.runAllTimersAsync();
162
- const result = await promise;
163
- expect(mockFetch).toHaveBeenCalledTimes(2);
164
- expect(result.success).toBe(true);
165
- expect(result.attemptNumber).toBe(2);
166
- });
167
- it('exhausts all retries and returns final failure', async () => {
168
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(false, 500, 'Error')));
169
- const webhook = makeWebhook({ retry_count: 3 });
170
- const payload = makePayload();
171
- const promise = deliverWebhook(webhook, payload);
172
- await vi.runAllTimersAsync();
173
- const result = await promise;
174
- expect(result.success).toBe(false);
175
- expect(result.errorMessage).toBe('Failed after 3 attempts');
176
- expect(result.attemptNumber).toBe(3);
177
- });
178
- it('calls logDelivery for each attempt', async () => {
179
- const mockFetch = vi.fn()
180
- .mockResolvedValueOnce(mockResponse(false, 500, 'Error'))
181
- .mockResolvedValueOnce(mockResponse(true, 200, 'OK'));
182
- vi.stubGlobal('fetch', mockFetch);
183
- const logDelivery = vi.fn().mockResolvedValue(undefined);
184
- const webhook = makeWebhook({ retry_count: 2 });
185
- const payload = makePayload();
186
- const promise = deliverWebhook(webhook, payload, logDelivery);
187
- await vi.runAllTimersAsync();
188
- await promise;
189
- expect(logDelivery).toHaveBeenCalledTimes(2);
190
- expect(logDelivery.mock.calls[0][0].success).toBe(false);
191
- expect(logDelivery.mock.calls[1][0].success).toBe(true);
192
- });
193
- it('continues even if logDelivery throws', async () => {
194
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK')));
195
- const logDelivery = vi.fn().mockRejectedValue(new Error('log failed'));
196
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
197
- const result = await deliverWebhook(makeWebhook(), makePayload(), logDelivery);
198
- expect(result.success).toBe(true);
199
- consoleSpy.mockRestore();
200
- });
201
- it('captures timeout error in logDelivery when fetch exceeds timeout_ms', async () => {
202
- const abortError = new Error('The operation was aborted');
203
- abortError.name = 'AbortError';
204
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(abortError));
205
- const logDelivery = vi.fn().mockResolvedValue(undefined);
206
- const webhook = makeWebhook({ timeout_ms: 1000, retry_count: 1 });
207
- const result = await deliverWebhook(webhook, makePayload(), logDelivery);
208
- // Per-attempt error is exposed via logDelivery
209
- const attemptResult = logDelivery.mock.calls[0][0];
210
- expect(attemptResult.success).toBe(false);
211
- expect(attemptResult.errorMessage).toBe('Timeout after 1000ms');
212
- // Final result reflects exhausted retries
213
- expect(result.success).toBe(false);
214
- });
215
- it('captures network error message in logDelivery', async () => {
216
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
217
- const logDelivery = vi.fn().mockResolvedValue(undefined);
218
- const webhook = makeWebhook({ retry_count: 1 });
219
- const result = await deliverWebhook(webhook, makePayload(), logDelivery);
220
- const attemptResult = logDelivery.mock.calls[0][0];
221
- expect(attemptResult.success).toBe(false);
222
- expect(attemptResult.errorMessage).toBe('ECONNREFUSED');
223
- expect(result.success).toBe(false);
224
- });
225
- });
226
- describe('triggerWebhooks', () => {
227
- beforeEach(() => {
228
- vi.useFakeTimers();
229
- });
230
- afterEach(() => {
231
- vi.restoreAllMocks();
232
- vi.unstubAllGlobals();
233
- vi.useRealTimers();
234
- });
235
- it('does nothing when no webhooks provided', async () => {
236
- const mockFetch = vi.fn();
237
- vi.stubGlobal('fetch', mockFetch);
238
- await triggerWebhooks([], 'click_event', 'evt-1', {});
239
- expect(mockFetch).not.toHaveBeenCalled();
240
- });
241
- it('skips inactive webhooks', async () => {
242
- const mockFetch = vi.fn();
243
- vi.stubGlobal('fetch', mockFetch);
244
- const inactive = makeWebhook({ is_active: false });
245
- await triggerWebhooks([inactive], 'click_event', 'evt-1', {});
246
- expect(mockFetch).not.toHaveBeenCalled();
247
- });
248
- it('skips webhooks not subscribed to the event', async () => {
249
- const mockFetch = vi.fn();
250
- vi.stubGlobal('fetch', mockFetch);
251
- const webhook = makeWebhook({ events: ['install_event'] });
252
- await triggerWebhooks([webhook], 'click_event', 'evt-1', {});
253
- expect(mockFetch).not.toHaveBeenCalled();
254
- });
255
- it('delivers to matching active webhooks', async () => {
256
- const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
257
- vi.stubGlobal('fetch', mockFetch);
258
- const webhook = makeWebhook({ events: ['click_event', 'install_event'] });
259
- triggerWebhooks([webhook], 'click_event', 'evt-1', { foo: 'bar' });
260
- await vi.runAllTimersAsync();
261
- expect(mockFetch).toHaveBeenCalledOnce();
262
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
263
- expect(body.event).toBe('click_event');
264
- expect(body.event_id).toBe('evt-1');
265
- expect(body.data).toEqual({ foo: 'bar' });
266
- });
267
- it('delivers to multiple matching webhooks', async () => {
268
- const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
269
- vi.stubGlobal('fetch', mockFetch);
270
- const wh1 = makeWebhook({ id: 'wh-1', url: 'https://a.com/hook' });
271
- const wh2 = makeWebhook({ id: 'wh-2', url: 'https://b.com/hook' });
272
- triggerWebhooks([wh1, wh2], 'click_event', 'evt-2', {});
273
- await vi.runAllTimersAsync();
274
- expect(mockFetch).toHaveBeenCalledTimes(2);
275
- });
276
- it('calls logDelivery with webhookId, result and the payload sent', async () => {
277
- const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
278
- vi.stubGlobal('fetch', mockFetch);
279
- const logDelivery = vi.fn().mockResolvedValue(undefined);
280
- const webhook = makeWebhook({ id: 'wh-log' });
281
- triggerWebhooks([webhook], 'click_event', 'evt-3', { foo: 'bar' }, logDelivery);
282
- await vi.runAllTimersAsync();
283
- expect(logDelivery).toHaveBeenCalledWith('wh-log', expect.objectContaining({ success: true }), expect.objectContaining({ event: 'click_event', event_id: 'evt-3', data: { foo: 'bar' } }));
284
- });
285
- it('falls back to the logger set with setWebhookDeliveryLogger', async () => {
286
- const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
287
- vi.stubGlobal('fetch', mockFetch);
288
- const logger = vi.fn().mockResolvedValue(undefined);
289
- setWebhookDeliveryLogger(logger);
290
- try {
291
- triggerWebhooks([makeWebhook({ id: 'wh-default' })], 'click_event', 'evt-4', {});
292
- await vi.runAllTimersAsync();
293
- }
294
- finally {
295
- setWebhookDeliveryLogger(undefined);
296
- }
297
- expect(logger).toHaveBeenCalledWith('wh-default', expect.objectContaining({ success: true }), expect.any(Object));
298
- });
299
- it('does not log when no logger is set', async () => {
300
- const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
301
- vi.stubGlobal('fetch', mockFetch);
302
- const logger = vi.fn().mockResolvedValue(undefined);
303
- setWebhookDeliveryLogger(logger);
304
- setWebhookDeliveryLogger(undefined);
305
- triggerWebhooks([makeWebhook()], 'click_event', 'evt-5', {});
306
- await vi.runAllTimersAsync();
307
- expect(logger).not.toHaveBeenCalled();
308
- });
309
- it('includes the timestamp in the payload', async () => {
310
- const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
311
- vi.stubGlobal('fetch', mockFetch);
312
- triggerWebhooks([makeWebhook()], 'click_event', 'evt-4', {});
313
- await vi.runAllTimersAsync();
314
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
315
- expect(body.timestamp).toBeDefined();
316
- expect(new Date(body.timestamp).toISOString()).toBe(body.timestamp);
317
- });
318
- });
319
- describe('findWebhooksForLink', () => {
320
- const mockDbQuery = db.query;
321
- beforeEach(() => {
322
- mockDbQuery.mockReset();
323
- });
324
- it('looks webhooks up through the link, by organization or else by user', async () => {
325
- const rows = [makeWebhook({ id: 'wh-org', organization_id: 'org-1' })];
326
- mockDbQuery.mockResolvedValueOnce({ rows });
327
- const result = await findWebhooksForLink('link-1');
328
- expect(result).toEqual(rows);
329
- const [sql, params] = mockDbQuery.mock.calls[0];
330
- expect(params).toEqual(['link-1']);
331
- // An organization's webhook hears every link in the organization...
332
- expect(sql).toMatch(/w\.organization_id IS NOT NULL AND w\.organization_id = l\.organization_id/);
333
- // ...one without an organization only its own user's links...
334
- expect(sql).toMatch(/w\.organization_id IS NULL AND w\.user_id = l\.user_id/);
335
- // ...and an unowned one (no accounts) only unowned links.
336
- expect(sql).toMatch(/w\.user_id IS NULL\s+AND l\.user_id IS NULL AND l\.organization_id IS NULL/);
337
- expect(sql).toMatch(/w\.is_active = true/);
338
- });
339
- });
340
- //# sourceMappingURL=webhook.test.js.map