@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,284 +0,0 @@
1
- /**
2
- * Route-level tests for the link safety gate.
3
- *
4
- * These drive the real `redirectRoutes` plugin through fastify.inject(), with only
5
- * the data layer mocked. The unit tests in lib/link-safety.test.ts already cover
6
- * the decision table; what matters here is the behaviour a visitor actually gets:
7
- * status codes, headers, whether a 302 happens, and whether a click is recorded.
8
- */
9
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
- import Fastify from 'fastify';
11
- // vi.mock is hoisted above these imports, so redirect.js still receives the mock.
12
- import { redirectRoutes } from './redirect.js';
13
- const query = vi.fn();
14
- vi.mock('../lib/database.js', () => ({
15
- db: {
16
- query: (...args) => query(...args),
17
- },
18
- }));
19
- /** A resolved link row as the redirect query would return it. */
20
- function linkRow(overrides = {}) {
21
- return {
22
- id: '00000000-0000-0000-0000-0000000000aa',
23
- short_code: 'abc123',
24
- original_url: 'https://example.com/landing',
25
- web_fallback_url: null,
26
- deep_link_path: null,
27
- is_active: true,
28
- warn_at: null,
29
- disabled_at: null,
30
- owner_suspended_at: null,
31
- expires_at: null,
32
- targeting_rules: null,
33
- template_settings: null,
34
- org_settings: null,
35
- utm_parameters: null,
36
- append_click_id: false,
37
- ...overrides,
38
- };
39
- }
40
- /**
41
- * @param row the link the lookup resolves to, or null for "not found"
42
- * @param suspensionColumn whether organizations.suspended_at exists in this database
43
- */
44
- function mockDb(row, suspensionColumn = true) {
45
- query.mockReset();
46
- query.mockImplementation(async (sql) => {
47
- if (/information_schema\.columns/i.test(sql)) {
48
- return { rows: suspensionColumn ? [{ '?column?': 1 }] : [], rowCount: suspensionColumn ? 1 : 0 };
49
- }
50
- if (/^\s*SELECT\s+l\.\*/i.test(sql) || /FROM links l/i.test(sql)) {
51
- return { rows: row ? [row] : [], rowCount: row ? 1 : 0 };
52
- }
53
- // click inserts, anything else
54
- return { rows: [], rowCount: 0 };
55
- });
56
- }
57
- /** Did any click get written? Flushes the setImmediate the redirect uses. */
58
- async function clickWasRecorded() {
59
- await new Promise((r) => setImmediate(r));
60
- return query.mock.calls.some(([sql]) => /INSERT INTO click_events/i.test(String(sql)));
61
- }
62
- let app;
63
- beforeEach(async () => {
64
- // No probe reset needed: the probe is scoped to each registration, so a fresh
65
- // Fastify instance per test gets a fresh probe. That the reset export is gone is
66
- // the point — it existed only to work around module-global state.
67
- app = Fastify();
68
- await app.register(redirectRoutes, { abuseReportUrl: 'https://example.org/abuse' });
69
- await app.ready();
70
- });
71
- afterEach(async () => {
72
- // Click recording is fire-and-forget (setImmediate). Let any pending insert from
73
- // this test finish BEFORE the next test resets the mock, otherwise a stray click
74
- // from a previous redirect lands in the next test's call log and looks like a bug.
75
- await new Promise((r) => setImmediate(r));
76
- await app.close();
77
- });
78
- describe('redirect safety gate', () => {
79
- it('redirects a healthy link', async () => {
80
- mockDb(linkRow());
81
- const res = await app.inject({ method: 'GET', url: '/abc123' });
82
- expect(res.statusCode).toBe(302);
83
- expect(res.headers.location).toBe('https://example.com/landing');
84
- });
85
- /**
86
- * An inactive link with no `disabled_at` is expired or was switched off by whoever
87
- * made it. Nobody decided anything about abuse, so nothing is explained.
88
- *
89
- * In practice the WHERE clause excludes these before they reach the gate; this
90
- * covers the row arriving from cache, where it can still be seen.
91
- */
92
- it('404s an inactive link that was not disabled by a decision', async () => {
93
- mockDb(linkRow({ is_active: false }));
94
- const res = await app.inject({ method: 'GET', url: '/abc123' });
95
- expect(res.statusCode).toBe(404);
96
- expect(res.body).not.toContain('This link is no longer available');
97
- });
98
- /**
99
- * The narrowed form of the old "leaks nothing" test.
100
- *
101
- * That test asserted every blocked link was byte-identical to an unknown code.
102
- * Deliberately no longer true — an abuse decision now earns an explanation, see
103
- * link-safety.ts. What must still hold is that a link which is merely *off* gives
104
- * nothing away, because its owner did nothing wrong and its visitor was not
105
- * targeted. This is the assertion that fails if the notice is ever widened to
106
- * plain `is_active`.
107
- */
108
- it('gives a merely inactive link the SAME response as an unknown code', async () => {
109
- mockDb(linkRow({ is_active: false }));
110
- const inactive = await app.inject({ method: 'GET', url: '/abc123' });
111
- mockDb(null);
112
- const unknown = await app.inject({ method: 'GET', url: '/nosuchcode' });
113
- expect(inactive.statusCode).toBe(unknown.statusCode);
114
- expect(inactive.body).toBe(unknown.body);
115
- });
116
- describe('a link blocked by an abuse decision', () => {
117
- const SUSPENDED = { owner_suspended_at: '2026-08-10T00:00:00Z' };
118
- const DISABLED = { is_active: false, disabled_at: '2026-08-10T00:00:00Z' };
119
- it.each([
120
- ['the owner is restricted', SUSPENDED],
121
- ['the link itself was disabled', DISABLED],
122
- ])('serves the notice with 410 Gone when %s', async (_label, row) => {
123
- mockDb(linkRow(row));
124
- const res = await app.inject({ method: 'GET', url: '/abc123' });
125
- expect(res.statusCode).toBe(410);
126
- expect(res.headers['content-type']).toMatch(/text\/html/);
127
- expect(res.body).toContain('This link is no longer available');
128
- });
129
- it('never redirects — there is nowhere safe to send anyone', async () => {
130
- mockDb(linkRow(SUSPENDED));
131
- const res = await app.inject({ method: 'GET', url: '/abc123' });
132
- expect(res.statusCode).not.toBe(302);
133
- expect(res.headers.location).toBeUndefined();
134
- });
135
- /** The warning page offers a way through because it is only a suspicion. This is not. */
136
- it('offers no way to continue', async () => {
137
- mockDb(linkRow(SUSPENDED));
138
- const res = await app.inject({ method: 'GET', url: '/abc123' });
139
- expect(res.body).not.toContain('Continue anyway');
140
- expect(res.body).not.toMatch(/<a[^>]+href="https?:/i);
141
- });
142
- /**
143
- * The assertion that matters most. Naming the destination puts the hostile URL
144
- * back in front of the one person already proven to click it, and the workspace
145
- * and owner are somebody else's details.
146
- */
147
- it('names no destination, workspace, or account holder', async () => {
148
- mockDb(linkRow({
149
- ...SUSPENDED,
150
- original_url: 'https://phish.example/steal',
151
- web_fallback_url: 'https://phish.example/fallback',
152
- deep_link_path: '/secret-path',
153
- }));
154
- const res = await app.inject({ method: 'GET', url: '/abc123' });
155
- expect(res.body).not.toContain('phish.example');
156
- expect(res.body).not.toContain('secret-path');
157
- });
158
- /**
159
- * The page must not tell a visitor why the link went away. It is a claim about
160
- * a user's content on a domain we serve, and it would be false the first time a
161
- * link is disabled in error.
162
- */
163
- it('does not say why the link was removed', async () => {
164
- mockDb(linkRow(SUSPENDED));
165
- const res = await app.inject({ method: 'GET', url: '/abc123' });
166
- for (const word of ['phishing', 'fraud', 'scam', 'abuse', 'reported', 'suspended', 'restricted']) {
167
- expect(res.body.toLowerCase(), `must not mention "${word}"`).not.toContain(word);
168
- }
169
- });
170
- it('asks not to be indexed or cached', async () => {
171
- mockDb(linkRow(SUSPENDED));
172
- const res = await app.inject({ method: 'GET', url: '/abc123' });
173
- expect(res.headers['x-robots-tag']).toMatch(/noindex/);
174
- expect(res.headers['cache-control']).toMatch(/no-store/);
175
- });
176
- it('records NO click — nobody reached the destination', async () => {
177
- mockDb(linkRow(SUSPENDED));
178
- await app.inject({ method: 'GET', url: '/abc123' });
179
- expect(await clickWasRecorded()).toBe(false);
180
- });
181
- /** An unknown code must stay a plain 404; only a real, withdrawn code is 410. */
182
- it('still 404s an unknown short code', async () => {
183
- mockDb(null);
184
- const res = await app.inject({ method: 'GET', url: '/nosuchcode' });
185
- expect(res.statusCode).toBe(404);
186
- expect(res.body).not.toContain('This link is no longer available');
187
- });
188
- });
189
- describe('a link flagged to warn', () => {
190
- it('serves an interstitial instead of redirecting', async () => {
191
- mockDb(linkRow({ warn_at: '2026-08-10T00:00:00Z' }));
192
- const res = await app.inject({ method: 'GET', url: '/abc123' });
193
- expect(res.statusCode).toBe(200);
194
- expect(res.statusCode).not.toBe(302);
195
- expect(res.headers['content-type']).toMatch(/text\/html/);
196
- expect(res.body).toContain('Check this link before continuing');
197
- });
198
- it('shows the destination the short link was hiding', async () => {
199
- mockDb(linkRow({ warn_at: '2026-08-10T00:00:00Z' }));
200
- const res = await app.inject({ method: 'GET', url: '/abc123' });
201
- expect(res.body).toContain('https://example.com/landing');
202
- });
203
- it('asks not to be indexed or cached', async () => {
204
- mockDb(linkRow({ warn_at: '2026-08-10T00:00:00Z' }));
205
- const res = await app.inject({ method: 'GET', url: '/abc123' });
206
- expect(res.headers['x-robots-tag']).toMatch(/noindex/);
207
- expect(res.headers['cache-control']).toMatch(/no-store/);
208
- });
209
- it('links to the configured reporting page', async () => {
210
- mockDb(linkRow({ warn_at: '2026-08-10T00:00:00Z' }));
211
- const res = await app.inject({ method: 'GET', url: '/abc123' });
212
- expect(res.body).toContain('https://example.org/abuse');
213
- });
214
- it('records NO click — a warning view is not a click on the link', async () => {
215
- mockDb(linkRow({ warn_at: '2026-08-10T00:00:00Z' }));
216
- await app.inject({ method: 'GET', url: '/abc123' });
217
- expect(await clickWasRecorded()).toBe(false);
218
- });
219
- it('falls back to the web fallback url when there is no original url', async () => {
220
- mockDb(linkRow({ warn_at: '2026-08-10T00:00:00Z', original_url: '', web_fallback_url: 'https://example.net/x' }));
221
- const res = await app.inject({ method: 'GET', url: '/abc123' });
222
- expect(res.body).toContain('https://example.net/x');
223
- });
224
- /**
225
- * Precedence is unchanged — a block still outranks a warn. What changed is the
226
- * response it produces. The property under test is that the visitor never gets
227
- * the interstitial's way through, since a restricted owner's link must be
228
- * unreachable even when it was only flagged to warn.
229
- */
230
- it('is outranked by owner restriction, and offers no way through', async () => {
231
- mockDb(linkRow({ warn_at: '2026-08-10T00:00:00Z', owner_suspended_at: '2026-08-10T00:00:00Z' }));
232
- const res = await app.inject({ method: 'GET', url: '/abc123' });
233
- expect(res.statusCode).toBe(410);
234
- expect(res.body).toContain('This link is no longer available');
235
- expect(res.body).not.toContain('Check this link before continuing');
236
- expect(res.body).not.toContain('Continue anyway');
237
- });
238
- /** Same precedence, for a link disabled directly rather than via its owner. */
239
- it('is outranked by an explicit disable', async () => {
240
- mockDb(linkRow({ warn_at: '2026-08-10T00:00:00Z', is_active: false, disabled_at: '2026-08-10T00:00:00Z' }));
241
- const res = await app.inject({ method: 'GET', url: '/abc123' });
242
- expect(res.statusCode).toBe(410);
243
- expect(res.body).not.toContain('Continue anyway');
244
- });
245
- });
246
- describe('a database without organizations.suspended_at', () => {
247
- it('still resolves links normally rather than erroring', async () => {
248
- mockDb(linkRow(), /* suspensionColumn */ false);
249
- const res = await app.inject({ method: 'GET', url: '/abc123' });
250
- expect(res.statusCode).toBe(302);
251
- });
252
- it('never asks for the column it just proved absent', async () => {
253
- mockDb(linkRow(), false);
254
- await app.inject({ method: 'GET', url: '/abc123' });
255
- const lookups = query.mock.calls
256
- .map(([sql]) => String(sql))
257
- .filter((sql) => /FROM links l/i.test(sql));
258
- expect(lookups.length).toBeGreaterThan(0);
259
- for (const sql of lookups)
260
- expect(sql).not.toMatch(/owner_suspended_at/);
261
- });
262
- it('probes once even under CONCURRENT cold requests', async () => {
263
- // The sequential test below passes trivially because inject() awaits. This one
264
- // fires them together, which is what the memoised promise actually buys.
265
- mockDb(linkRow(), false);
266
- await Promise.all([
267
- app.inject({ method: 'GET', url: '/abc123' }),
268
- app.inject({ method: 'GET', url: '/abc123' }),
269
- app.inject({ method: 'GET', url: '/abc123' }),
270
- app.inject({ method: 'GET', url: '/abc123' }),
271
- ]);
272
- const probes = query.mock.calls.filter(([sql]) => /information_schema\.columns/i.test(String(sql)));
273
- expect(probes).toHaveLength(1);
274
- });
275
- it('probes only once across sequential requests', async () => {
276
- mockDb(linkRow(), false);
277
- await app.inject({ method: 'GET', url: '/abc123' });
278
- await app.inject({ method: 'GET', url: '/abc123' });
279
- const probes = query.mock.calls.filter(([sql]) => /information_schema\.columns/i.test(String(sql)));
280
- expect(probes).toHaveLength(1);
281
- });
282
- });
283
- });
284
- //# sourceMappingURL=redirect.safety.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"redirect.safety.test.js","sourceRoot":"","sources":["../../src/routes/redirect.safety.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=redirect.store-url.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"redirect.store-url.test.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.store-url.test.ts"],"names":[],"mappings":""}
@@ -1,239 +0,0 @@
1
- /**
2
- * Store URLs are not decorated like websites.
3
- *
4
- * The destination decorator adds UTM tags, deep-link parameters and the
5
- * opt-in click id — right for a website, wrong for a store listing. Google
6
- * Play identifies the app by `?id=`, so a deep-link parameter named `id`
7
- * replaced it and Play answered 404 to every Android visitor sent to install.
8
- * The one thing a Play listing gains, for an Android visitor, is the click id
9
- * in its `referrer`, which Play hands to the installed app.
10
- *
11
- * The app-open guard cases matter as much as the fix: an in-app browser must
12
- * still be sent to the web fallback (where a Universal Link / App Link gets a
13
- * second chance to open an installed app), decorated as before.
14
- *
15
- * Follows redirect.launchpad.test.ts: the real `redirectRoutes` plugin through
16
- * `fastify.inject()`, only the data layer mocked.
17
- */
18
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
19
- import Fastify from 'fastify';
20
- import { redirectRoutes, isAppStoreUrl, withPlayInstallReferrer } from './redirect.js';
21
- const query = vi.fn();
22
- vi.mock('../lib/database.js', () => ({
23
- db: {
24
- query: (...args) => query(...args),
25
- },
26
- }));
27
- const DESKTOP_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36';
28
- const IPHONE_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
29
- const ANDROID_CHROME_UA = 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Mobile Safari/537.36';
30
- const ANDROID_INSTAGRAM_UA = 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 Instagram 320.0.0.0';
31
- const PLAY = 'https://play.google.com/store/apps/details?id=com.example.app';
32
- const APP_STORE = 'https://apps.apple.com/app/id1234567890';
33
- const WEB = 'https://example.com/event';
34
- /** A link carrying everything the decorator would add, including an `id` parameter. */
35
- function linkRow(overrides = {}) {
36
- return {
37
- id: '00000000-0000-0000-0000-0000000000aa',
38
- short_code: 'abc123',
39
- title: null,
40
- description: null,
41
- og_title: null,
42
- og_description: null,
43
- og_image_url: null,
44
- original_url: null,
45
- web_fallback_url: null,
46
- ios_app_store_url: APP_STORE,
47
- android_app_store_url: PLAY,
48
- ios_universal_link: null,
49
- android_app_link: null,
50
- app_scheme: null,
51
- deep_link_path: null,
52
- launchpad_mode: 'inherit',
53
- is_active: true,
54
- warn_at: null,
55
- owner_suspended_at: null,
56
- expires_at: null,
57
- targeting_rules: null,
58
- template_settings: null,
59
- org_settings: null,
60
- utm_parameters: { source: 'newsletter', medium: 'email' },
61
- deep_link_parameters: { id: 'evt_42', route: 'EVENT_VIEW' },
62
- append_click_id: true,
63
- ...overrides,
64
- };
65
- }
66
- function mockDb(row) {
67
- query.mockReset();
68
- query.mockImplementation(async (sql) => {
69
- if (/information_schema\.columns/i.test(sql))
70
- return { rows: [{ '?column?': 1 }], rowCount: 1 };
71
- if (/^\s*SELECT\s+l\.\*/i.test(sql) || /FROM links l/i.test(sql))
72
- return { rows: [row], rowCount: 1 };
73
- return { rows: [], rowCount: 0 };
74
- });
75
- }
76
- const get = (app, ua) => app.inject({ method: 'GET', url: '/abc123', headers: { 'user-agent': ua, host: 'go.example' } });
77
- let app;
78
- beforeEach(async () => {
79
- app = Fastify();
80
- await app.register(redirectRoutes);
81
- await app.ready();
82
- });
83
- afterEach(async () => {
84
- await new Promise((r) => setImmediate(r));
85
- await app.close();
86
- });
87
- /** The click id the redirect wrote to click_events. */
88
- function insertedClickId() {
89
- const insert = query.mock.calls.find(([sql]) => /INSERT INTO click_events/i.test(String(sql)));
90
- return insert?.[1]?.[0];
91
- }
92
- /** `location` as a Play URL: its app id, and its referrer's keys. */
93
- function playParts(location) {
94
- const url = new URL(String(location));
95
- return {
96
- base: `${url.origin}${url.pathname}`,
97
- keys: [...url.searchParams.keys()].sort(),
98
- id: url.searchParams.get('id'),
99
- referrer: new URLSearchParams(url.searchParams.get('referrer') ?? ''),
100
- };
101
- }
102
- describe('store URLs are sent exactly as configured', () => {
103
- it('Android Chrome → the Play URL with only the click id added, so a deep-link `id` cannot replace the app id', async () => {
104
- mockDb(linkRow({ web_fallback_url: WEB }));
105
- const res = await get(app, ANDROID_CHROME_UA);
106
- expect(res.statusCode).toBe(302);
107
- const play = playParts(res.headers.location);
108
- expect(play.base).toBe('https://play.google.com/store/apps/details');
109
- expect(play.keys).toEqual(['id', 'referrer']);
110
- expect(play.id).toBe('com.example.app');
111
- expect([...play.referrer.keys()]).toEqual(['lf_click']);
112
- });
113
- it('iOS Safari → the App Store URL untouched', async () => {
114
- mockDb(linkRow({ web_fallback_url: WEB }));
115
- const res = await get(app, IPHONE_UA);
116
- expect(res.statusCode).toBe(302);
117
- expect(res.headers.location).toBe(APP_STORE);
118
- });
119
- it('a store URL configured as the web fallback is not decorated either', async () => {
120
- mockDb(linkRow({ ios_app_store_url: null, android_app_store_url: null, web_fallback_url: PLAY }));
121
- const res = await get(app, IPHONE_UA);
122
- expect(res.headers.location).toBe(PLAY);
123
- });
124
- });
125
- describe('the Play install referrer carries the click id', () => {
126
- it('is the id of the click the redirect records', async () => {
127
- mockDb(linkRow());
128
- const res = await get(app, ANDROID_CHROME_UA);
129
- await new Promise((r) => setImmediate(r));
130
- const clickId = playParts(res.headers.location).referrer.get('lf_click');
131
- expect(clickId).toMatch(/^[0-9a-f-]{36}$/);
132
- expect(insertedClickId()).toBe(clickId);
133
- });
134
- it('is added to a Play URL configured as the web fallback', async () => {
135
- mockDb(linkRow({ android_app_store_url: null, web_fallback_url: PLAY }));
136
- const res = await get(app, ANDROID_CHROME_UA);
137
- const play = playParts(res.headers.location);
138
- expect(play.id).toBe('com.example.app');
139
- expect(play.referrer.get('lf_click')).toBeTruthy();
140
- });
141
- it('keeps the keys of a referrer the Play URL already carries', async () => {
142
- mockDb(linkRow({ android_app_store_url: `${PLAY}&referrer=utm_source%3Dposter` }));
143
- const res = await get(app, ANDROID_CHROME_UA);
144
- const { referrer } = playParts(res.headers.location);
145
- expect(referrer.get('utm_source')).toBe('poster');
146
- expect(referrer.get('lf_click')).toBeTruthy();
147
- });
148
- it('is the scheme interstitial\'s store fallback on Android', async () => {
149
- mockDb(linkRow({ app_scheme: 'example', deep_link_path: '/e/42' }));
150
- const res = await get(app, ANDROID_CHROME_UA);
151
- expect(res.statusCode).toBe(200);
152
- const fallback = /id="store-btn" href="([^"]+)"/.exec(res.body)?.[1].replace(/&amp;/g, '&');
153
- expect(playParts(fallback).referrer.get('lf_click')).toBeTruthy();
154
- });
155
- it('is the launchpad page\'s Google Play button on Android', async () => {
156
- mockDb(linkRow({ org_settings: { launchpad: { mobile: 'page' } } }));
157
- const res = await get(app, ANDROID_CHROME_UA);
158
- expect(res.statusCode).toBe(200);
159
- const button = /data-lp-cta="cta_android" href="([^"]+)"/.exec(res.body)?.[1].replace(/&amp;/g, '&');
160
- expect(playParts(button).referrer.get('lf_click')).toBeTruthy();
161
- });
162
- it('is not added for an iPhone or a desktop visitor', async () => {
163
- mockDb(linkRow({ ios_app_store_url: null, android_app_store_url: null, web_fallback_url: PLAY }));
164
- expect((await get(app, IPHONE_UA)).headers.location).toBe(PLAY);
165
- expect((await get(app, DESKTOP_UA)).headers.location).toBe(PLAY);
166
- });
167
- it('app-open guard: an Android in-app browser still goes to the web fallback, with no referrer', async () => {
168
- mockDb(linkRow({ web_fallback_url: WEB }));
169
- const res = await get(app, ANDROID_INSTAGRAM_UA);
170
- const url = new URL(String(res.headers.location));
171
- expect(url.hostname).toBe('example.com');
172
- expect(url.searchParams.has('referrer')).toBe(false);
173
- });
174
- });
175
- describe('withPlayInstallReferrer', () => {
176
- const CLICK = '11111111-2222-4333-8444-555555555555';
177
- it('adds the click id to a Play listing, https or market://', () => {
178
- expect(withPlayInstallReferrer(PLAY, CLICK)).toBe(`${PLAY}&referrer=lf_click%3D${CLICK}`);
179
- expect(withPlayInstallReferrer('market://details?id=com.example.app', CLICK)).toBe(`market://details?id=com.example.app&referrer=lf_click%3D${CLICK}`);
180
- });
181
- it('replaces a click id the referrer already names', () => {
182
- const url = withPlayInstallReferrer(`${PLAY}&referrer=lf_click%3Dold%26utm_source%3Dx`, CLICK);
183
- const referrer = new URLSearchParams(new URL(url).searchParams.get('referrer') ?? '');
184
- expect(referrer.getAll('lf_click')).toEqual([CLICK]);
185
- expect(referrer.get('utm_source')).toBe('x');
186
- });
187
- it('leaves every other URL alone', () => {
188
- for (const url of [
189
- APP_STORE,
190
- WEB,
191
- 'https://play.google.com/store/apps/dev?id=123',
192
- 'https://play.google.com.evil.example/store/apps/details?id=x',
193
- 'not a url',
194
- ]) {
195
- expect(withPlayInstallReferrer(url, CLICK)).toBe(url);
196
- }
197
- });
198
- });
199
- describe('websites are decorated exactly as before', () => {
200
- it('desktop → web fallback with UTM, deep-link parameters and the click id', async () => {
201
- mockDb(linkRow({ web_fallback_url: WEB }));
202
- const res = await get(app, DESKTOP_UA);
203
- expect(res.statusCode).toBe(302);
204
- const url = new URL(String(res.headers.location));
205
- expect(url.origin + url.pathname).toBe(WEB);
206
- expect(url.searchParams.get('id')).toBe('evt_42');
207
- expect(url.searchParams.get('route')).toBe('EVENT_VIEW');
208
- expect(url.searchParams.get('utm_source')).toBe('newsletter');
209
- expect(url.searchParams.get('lf_click')).toBeTruthy();
210
- });
211
- it('app-open guard: an Android in-app browser still goes to the web fallback, not the store', async () => {
212
- mockDb(linkRow({ web_fallback_url: WEB }));
213
- const res = await get(app, ANDROID_INSTAGRAM_UA);
214
- expect(res.statusCode).toBe(302);
215
- const url = new URL(String(res.headers.location));
216
- expect(url.hostname).toBe('example.com');
217
- expect(url.searchParams.get('id')).toBe('evt_42');
218
- });
219
- it('app-open guard: an App Link still wins over every fallback', async () => {
220
- mockDb(linkRow({ web_fallback_url: WEB, android_app_link: 'https://app.example.com/e/42' }));
221
- const res = await get(app, ANDROID_CHROME_UA);
222
- expect(new URL(String(res.headers.location)).hostname).toBe('app.example.com');
223
- });
224
- });
225
- describe('isAppStoreUrl', () => {
226
- it('recognises Google Play and the App Store, whatever the case', () => {
227
- expect(isAppStoreUrl(PLAY)).toBe(true);
228
- expect(isAppStoreUrl(APP_STORE)).toBe(true);
229
- expect(isAppStoreUrl('https://itunes.apple.com/us/app/id1')).toBe(true);
230
- expect(isAppStoreUrl('https://PLAY.google.com/store/apps/details?id=x')).toBe(true);
231
- });
232
- it('does not treat websites, look-alikes or garbage as stores', () => {
233
- expect(isAppStoreUrl(WEB)).toBe(false);
234
- expect(isAppStoreUrl('https://play.google.com.evil.example/x')).toBe(false);
235
- expect(isAppStoreUrl('https://example.com/?u=https://play.google.com')).toBe(false);
236
- expect(isAppStoreUrl('not a url')).toBe(false);
237
- });
238
- });
239
- //# sourceMappingURL=redirect.store-url.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"redirect.store-url.test.js","sourceRoot":"","sources":["../../src/routes/redirect.store-url.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=redirect.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"redirect.test.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.test.ts"],"names":[],"mappings":""}