@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.
- package/README.md +20 -588
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/dist/index.config.test.d.ts +0 -2
- package/dist/index.config.test.d.ts.map +0 -1
- package/dist/index.config.test.js +0 -87
- package/dist/index.config.test.js.map +0 -1
- package/dist/lib/bot-detection.test.d.ts +0 -2
- package/dist/lib/bot-detection.test.d.ts.map +0 -1
- package/dist/lib/bot-detection.test.js +0 -65
- package/dist/lib/bot-detection.test.js.map +0 -1
- package/dist/lib/client-ip.test.d.ts +0 -7
- package/dist/lib/client-ip.test.d.ts.map +0 -1
- package/dist/lib/client-ip.test.js +0 -114
- package/dist/lib/client-ip.test.js.map +0 -1
- package/dist/lib/config.test.d.ts +0 -2
- package/dist/lib/config.test.d.ts.map +0 -1
- package/dist/lib/config.test.js +0 -126
- package/dist/lib/config.test.js.map +0 -1
- package/dist/lib/database.test.d.ts +0 -2
- package/dist/lib/database.test.d.ts.map +0 -1
- package/dist/lib/database.test.js +0 -180
- package/dist/lib/database.test.js.map +0 -1
- package/dist/lib/deep-link-payload.test.d.ts +0 -2
- package/dist/lib/deep-link-payload.test.d.ts.map +0 -1
- package/dist/lib/deep-link-payload.test.js +0 -202
- package/dist/lib/deep-link-payload.test.js.map +0 -1
- package/dist/lib/event-emitter.test.d.ts +0 -2
- package/dist/lib/event-emitter.test.d.ts.map +0 -1
- package/dist/lib/event-emitter.test.js +0 -162
- package/dist/lib/event-emitter.test.js.map +0 -1
- package/dist/lib/fingerprint.test.d.ts +0 -2
- package/dist/lib/fingerprint.test.d.ts.map +0 -1
- package/dist/lib/fingerprint.test.js +0 -750
- package/dist/lib/fingerprint.test.js.map +0 -1
- package/dist/lib/launchpad.test.d.ts +0 -2
- package/dist/lib/launchpad.test.d.ts.map +0 -1
- package/dist/lib/launchpad.test.js +0 -368
- package/dist/lib/launchpad.test.js.map +0 -1
- package/dist/lib/link-lookup.test.d.ts +0 -2
- package/dist/lib/link-lookup.test.d.ts.map +0 -1
- package/dist/lib/link-lookup.test.js +0 -61
- package/dist/lib/link-lookup.test.js.map +0 -1
- package/dist/lib/link-resolution-cache.test.d.ts +0 -2
- package/dist/lib/link-resolution-cache.test.d.ts.map +0 -1
- package/dist/lib/link-resolution-cache.test.js +0 -45
- package/dist/lib/link-resolution-cache.test.js.map +0 -1
- package/dist/lib/link-safety.test.d.ts +0 -2
- package/dist/lib/link-safety.test.d.ts.map +0 -1
- package/dist/lib/link-safety.test.js +0 -211
- package/dist/lib/link-safety.test.js.map +0 -1
- package/dist/lib/not-found-page.test.d.ts +0 -2
- package/dist/lib/not-found-page.test.d.ts.map +0 -1
- package/dist/lib/not-found-page.test.js +0 -151
- package/dist/lib/not-found-page.test.js.map +0 -1
- package/dist/lib/portable/cli.test.d.ts +0 -2
- package/dist/lib/portable/cli.test.d.ts.map +0 -1
- package/dist/lib/portable/cli.test.js +0 -98
- package/dist/lib/portable/cli.test.js.map +0 -1
- package/dist/lib/portable/portable.integration.test.d.ts +0 -2
- package/dist/lib/portable/portable.integration.test.d.ts.map +0 -1
- package/dist/lib/portable/portable.integration.test.js +0 -139
- package/dist/lib/portable/portable.integration.test.js.map +0 -1
- package/dist/lib/portable/registry.schema.test.d.ts +0 -2
- package/dist/lib/portable/registry.schema.test.d.ts.map +0 -1
- package/dist/lib/portable/registry.schema.test.js +0 -59
- package/dist/lib/portable/registry.schema.test.js.map +0 -1
- package/dist/lib/scanner-detection.test.d.ts +0 -2
- package/dist/lib/scanner-detection.test.d.ts.map +0 -1
- package/dist/lib/scanner-detection.test.js +0 -124
- package/dist/lib/scanner-detection.test.js.map +0 -1
- package/dist/lib/schema-guard.test.d.ts +0 -2
- package/dist/lib/schema-guard.test.d.ts.map +0 -1
- package/dist/lib/schema-guard.test.js +0 -83
- package/dist/lib/schema-guard.test.js.map +0 -1
- package/dist/lib/utils.test.d.ts +0 -2
- package/dist/lib/utils.test.d.ts.map +0 -1
- package/dist/lib/utils.test.js +0 -261
- package/dist/lib/utils.test.js.map +0 -1
- package/dist/lib/webhook.test.d.ts +0 -2
- package/dist/lib/webhook.test.d.ts.map +0 -1
- package/dist/lib/webhook.test.js +0 -340
- package/dist/lib/webhook.test.js.map +0 -1
- package/dist/routes/health.test.d.ts +0 -2
- package/dist/routes/health.test.d.ts.map +0 -1
- package/dist/routes/health.test.js +0 -82
- package/dist/routes/health.test.js.map +0 -1
- package/dist/routes/preview.test.d.ts +0 -2
- package/dist/routes/preview.test.d.ts.map +0 -1
- package/dist/routes/preview.test.js +0 -111
- package/dist/routes/preview.test.js.map +0 -1
- package/dist/routes/qr.test.d.ts +0 -2
- package/dist/routes/qr.test.d.ts.map +0 -1
- package/dist/routes/qr.test.js +0 -67
- package/dist/routes/qr.test.js.map +0 -1
- package/dist/routes/redirect.click-event.test.d.ts +0 -2
- package/dist/routes/redirect.click-event.test.d.ts.map +0 -1
- package/dist/routes/redirect.click-event.test.js +0 -85
- package/dist/routes/redirect.click-event.test.js.map +0 -1
- package/dist/routes/redirect.host-scope.integration.test.d.ts +0 -2
- package/dist/routes/redirect.host-scope.integration.test.d.ts.map +0 -1
- package/dist/routes/redirect.host-scope.integration.test.js +0 -105
- package/dist/routes/redirect.host-scope.integration.test.js.map +0 -1
- package/dist/routes/redirect.interstitial.test.d.ts +0 -2
- package/dist/routes/redirect.interstitial.test.d.ts.map +0 -1
- package/dist/routes/redirect.interstitial.test.js +0 -128
- package/dist/routes/redirect.interstitial.test.js.map +0 -1
- package/dist/routes/redirect.launchpad.test.d.ts +0 -2
- package/dist/routes/redirect.launchpad.test.d.ts.map +0 -1
- package/dist/routes/redirect.launchpad.test.js +0 -515
- package/dist/routes/redirect.launchpad.test.js.map +0 -1
- package/dist/routes/redirect.no-web-destination.test.d.ts +0 -2
- package/dist/routes/redirect.no-web-destination.test.d.ts.map +0 -1
- package/dist/routes/redirect.no-web-destination.test.js +0 -176
- package/dist/routes/redirect.no-web-destination.test.js.map +0 -1
- package/dist/routes/redirect.not-found.test.d.ts +0 -2
- package/dist/routes/redirect.not-found.test.d.ts.map +0 -1
- package/dist/routes/redirect.not-found.test.js +0 -280
- package/dist/routes/redirect.not-found.test.js.map +0 -1
- package/dist/routes/redirect.safety.test.d.ts +0 -2
- package/dist/routes/redirect.safety.test.d.ts.map +0 -1
- package/dist/routes/redirect.safety.test.js +0 -284
- package/dist/routes/redirect.safety.test.js.map +0 -1
- package/dist/routes/redirect.store-url.test.d.ts +0 -2
- package/dist/routes/redirect.store-url.test.d.ts.map +0 -1
- package/dist/routes/redirect.store-url.test.js +0 -239
- package/dist/routes/redirect.store-url.test.js.map +0 -1
- package/dist/routes/redirect.test.d.ts +0 -2
- package/dist/routes/redirect.test.d.ts.map +0 -1
- package/dist/routes/redirect.test.js +0 -158
- package/dist/routes/redirect.test.js.map +0 -1
- package/dist/routes/redirect.web-link.test.d.ts +0 -2
- package/dist/routes/redirect.web-link.test.d.ts.map +0 -1
- package/dist/routes/redirect.web-link.test.js +0 -138
- package/dist/routes/redirect.web-link.test.js.map +0 -1
- package/dist/routes/sdk-cache-bypass.test.d.ts +0 -2
- package/dist/routes/sdk-cache-bypass.test.d.ts.map +0 -1
- package/dist/routes/sdk-cache-bypass.test.js +0 -175
- package/dist/routes/sdk-cache-bypass.test.js.map +0 -1
- package/dist/routes/sdk.device-values.integration.test.d.ts +0 -2
- package/dist/routes/sdk.device-values.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.device-values.integration.test.js +0 -132
- package/dist/routes/sdk.device-values.integration.test.js.map +0 -1
- package/dist/routes/sdk.event.test.d.ts +0 -2
- package/dist/routes/sdk.event.test.d.ts.map +0 -1
- package/dist/routes/sdk.event.test.js +0 -284
- package/dist/routes/sdk.event.test.js.map +0 -1
- package/dist/routes/sdk.install-certainty.integration.test.d.ts +0 -2
- package/dist/routes/sdk.install-certainty.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.install-certainty.integration.test.js +0 -104
- package/dist/routes/sdk.install-certainty.integration.test.js.map +0 -1
- package/dist/routes/sdk.install-live-links.integration.test.d.ts +0 -2
- package/dist/routes/sdk.install-live-links.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.install-live-links.integration.test.js +0 -92
- package/dist/routes/sdk.install-live-links.integration.test.js.map +0 -1
- package/dist/routes/sdk.install-referrer.integration.test.d.ts +0 -2
- package/dist/routes/sdk.install-referrer.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.install-referrer.integration.test.js +0 -187
- package/dist/routes/sdk.install-referrer.integration.test.js.map +0 -1
- package/dist/routes/sdk.install-window-once.integration.test.d.ts +0 -2
- package/dist/routes/sdk.install-window-once.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.install-window-once.integration.test.js +0 -147
- package/dist/routes/sdk.install-window-once.integration.test.js.map +0 -1
- package/dist/routes/sdk.install.test.d.ts +0 -2
- package/dist/routes/sdk.install.test.d.ts.map +0 -1
- package/dist/routes/sdk.install.test.js +0 -99
- package/dist/routes/sdk.install.test.js.map +0 -1
- package/dist/routes/sdk.resolve-click.test.d.ts +0 -2
- package/dist/routes/sdk.resolve-click.test.d.ts.map +0 -1
- package/dist/routes/sdk.resolve-click.test.js +0 -79
- package/dist/routes/sdk.resolve-click.test.js.map +0 -1
- package/dist/routes/sdk.resolve-scope.test.d.ts +0 -2
- package/dist/routes/sdk.resolve-scope.test.d.ts.map +0 -1
- package/dist/routes/sdk.resolve-scope.test.js +0 -105
- 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 +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(/&/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(/&/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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.test.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.test.ts"],"names":[],"mappings":""}
|