@linkforty/core 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +154 -0
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/event-emitter.d.ts +46 -0
- package/dist/lib/event-emitter.d.ts.map +1 -0
- package/dist/lib/event-emitter.js +29 -0
- package/dist/lib/event-emitter.js.map +1 -0
- package/dist/lib/fingerprint.d.ts +64 -0
- package/dist/lib/fingerprint.d.ts.map +1 -0
- package/dist/lib/fingerprint.js +387 -0
- package/dist/lib/fingerprint.js.map +1 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +1 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/webhook.d.ts +18 -0
- package/dist/lib/webhook.d.ts.map +1 -0
- package/dist/lib/webhook.js +150 -0
- package/dist/lib/webhook.js.map +1 -0
- package/dist/routes/debug.d.ts +7 -0
- package/dist/routes/debug.d.ts.map +1 -0
- package/dist/routes/debug.js +321 -0
- package/dist/routes/debug.js.map +1 -0
- package/dist/routes/index.d.ts +5 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +11 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/links.d.ts.map +1 -1
- package/dist/routes/links.js +21 -3
- package/dist/routes/links.js.map +1 -1
- package/dist/routes/preview.d.ts +3 -0
- package/dist/routes/preview.d.ts.map +1 -0
- package/dist/routes/preview.js +225 -0
- package/dist/routes/preview.js.map +1 -0
- package/dist/routes/qr.d.ts +6 -0
- package/dist/routes/qr.d.ts.map +1 -0
- package/dist/routes/qr.js +136 -0
- package/dist/routes/qr.js.map +1 -0
- package/dist/routes/redirect.d.ts.map +1 -1
- package/dist/routes/redirect.js +167 -11
- package/dist/routes/redirect.js.map +1 -1
- package/dist/routes/sdk.d.ts +7 -0
- package/dist/routes/sdk.d.ts.map +1 -0
- package/dist/routes/sdk.js +265 -0
- package/dist/routes/sdk.js.map +1 -0
- package/dist/routes/webhooks.d.ts +3 -0
- package/dist/routes/webhooks.d.ts.map +1 -0
- package/dist/routes/webhooks.js +212 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/types/index.d.ts +81 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +11 -7
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.previewRoutes = previewRoutes;
|
|
4
|
+
const database_js_1 = require("../lib/database.js");
|
|
5
|
+
/**
|
|
6
|
+
* Detect if the request is from a social media scraper/bot
|
|
7
|
+
* These bots crawl links to generate previews when shared on social platforms
|
|
8
|
+
*/
|
|
9
|
+
function isSocialScraper(userAgent) {
|
|
10
|
+
const scraperPatterns = [
|
|
11
|
+
/facebookexternalhit/i, // Facebook
|
|
12
|
+
/Facebot/i, // Facebook
|
|
13
|
+
/Twitterbot/i, // Twitter
|
|
14
|
+
/LinkedInBot/i, // LinkedIn
|
|
15
|
+
/Slackbot/i, // Slack
|
|
16
|
+
/Discordbot/i, // Discord
|
|
17
|
+
/TelegramBot/i, // Telegram
|
|
18
|
+
/WhatsApp/i, // WhatsApp
|
|
19
|
+
/PinterestBot/i, // Pinterest
|
|
20
|
+
/SkypeUriPreview/i, // Skype
|
|
21
|
+
/Googlebot/i, // Google (for search previews)
|
|
22
|
+
/bingbot/i, // Bing
|
|
23
|
+
/ia_archiver/i, // Alexa
|
|
24
|
+
];
|
|
25
|
+
return scraperPatterns.some(pattern => pattern.test(userAgent));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate HTML preview page with Open Graph meta tags
|
|
29
|
+
*/
|
|
30
|
+
function generatePreviewHTML(link, shortUrl, autoRedirect = true) {
|
|
31
|
+
// Use OG-specific values if provided, otherwise fall back to regular title/description
|
|
32
|
+
const ogTitle = link.og_title || link.title || 'Shared Link';
|
|
33
|
+
const ogDescription = link.og_description || link.description || '';
|
|
34
|
+
const ogImage = link.og_image_url || '';
|
|
35
|
+
const ogType = link.og_type || 'website';
|
|
36
|
+
const ogUrl = shortUrl;
|
|
37
|
+
// Auto-redirect after 2 seconds for human visitors
|
|
38
|
+
const metaRefresh = autoRedirect
|
|
39
|
+
? `<meta http-equiv="refresh" content="2;url=${link.original_url}">`
|
|
40
|
+
: '';
|
|
41
|
+
return `<!DOCTYPE html>
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="UTF-8">
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
46
|
+
<title>${escapeHtml(ogTitle)}</title>
|
|
47
|
+
|
|
48
|
+
<!-- Open Graph / Facebook -->
|
|
49
|
+
<meta property="og:type" content="${escapeHtml(ogType)}">
|
|
50
|
+
<meta property="og:url" content="${escapeHtml(ogUrl)}">
|
|
51
|
+
<meta property="og:title" content="${escapeHtml(ogTitle)}">
|
|
52
|
+
<meta property="og:description" content="${escapeHtml(ogDescription)}">
|
|
53
|
+
${ogImage ? `<meta property="og:image" content="${escapeHtml(ogImage)}">` : ''}
|
|
54
|
+
${ogImage ? `<meta property="og:image:secure_url" content="${escapeHtml(ogImage)}">` : ''}
|
|
55
|
+
|
|
56
|
+
<!-- Twitter -->
|
|
57
|
+
<meta name="twitter:card" content="${ogImage ? 'summary_large_image' : 'summary'}">
|
|
58
|
+
<meta name="twitter:url" content="${escapeHtml(ogUrl)}">
|
|
59
|
+
<meta name="twitter:title" content="${escapeHtml(ogTitle)}">
|
|
60
|
+
<meta name="twitter:description" content="${escapeHtml(ogDescription)}">
|
|
61
|
+
${ogImage ? `<meta name="twitter:image" content="${escapeHtml(ogImage)}">` : ''}
|
|
62
|
+
|
|
63
|
+
<!-- LinkedIn -->
|
|
64
|
+
<meta property="og:site_name" content="LinkForty">
|
|
65
|
+
|
|
66
|
+
${metaRefresh}
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
body {
|
|
70
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
min-height: 100vh;
|
|
75
|
+
margin: 0;
|
|
76
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
77
|
+
color: white;
|
|
78
|
+
text-align: center;
|
|
79
|
+
padding: 20px;
|
|
80
|
+
}
|
|
81
|
+
.container {
|
|
82
|
+
max-width: 600px;
|
|
83
|
+
}
|
|
84
|
+
h1 {
|
|
85
|
+
font-size: 2.5rem;
|
|
86
|
+
margin-bottom: 1rem;
|
|
87
|
+
}
|
|
88
|
+
p {
|
|
89
|
+
font-size: 1.25rem;
|
|
90
|
+
margin-bottom: 2rem;
|
|
91
|
+
opacity: 0.9;
|
|
92
|
+
}
|
|
93
|
+
.link {
|
|
94
|
+
display: inline-block;
|
|
95
|
+
padding: 12px 24px;
|
|
96
|
+
background: white;
|
|
97
|
+
color: #667eea;
|
|
98
|
+
text-decoration: none;
|
|
99
|
+
border-radius: 8px;
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
transition: transform 0.2s;
|
|
102
|
+
}
|
|
103
|
+
.link:hover {
|
|
104
|
+
transform: translateY(-2px);
|
|
105
|
+
}
|
|
106
|
+
.loader {
|
|
107
|
+
margin: 2rem auto;
|
|
108
|
+
border: 4px solid rgba(255, 255, 255, 0.3);
|
|
109
|
+
border-top: 4px solid white;
|
|
110
|
+
border-radius: 50%;
|
|
111
|
+
width: 40px;
|
|
112
|
+
height: 40px;
|
|
113
|
+
animation: spin 1s linear infinite;
|
|
114
|
+
}
|
|
115
|
+
@keyframes spin {
|
|
116
|
+
0% { transform: rotate(0deg); }
|
|
117
|
+
100% { transform: rotate(360deg); }
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
120
|
+
</head>
|
|
121
|
+
<body>
|
|
122
|
+
<div class="container">
|
|
123
|
+
${ogImage ? `<img src="${escapeHtml(ogImage)}" alt="${escapeHtml(ogTitle)}" style="max-width: 100%; border-radius: 12px; margin-bottom: 2rem; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">` : ''}
|
|
124
|
+
<h1>${escapeHtml(ogTitle)}</h1>
|
|
125
|
+
${ogDescription ? `<p>${escapeHtml(ogDescription)}</p>` : ''}
|
|
126
|
+
${autoRedirect ? '<div class="loader"></div><p>Redirecting you...</p>' : ''}
|
|
127
|
+
<a href="${escapeHtml(link.original_url)}" class="link">
|
|
128
|
+
${autoRedirect ? 'Click here if not redirected' : 'Continue to destination'}
|
|
129
|
+
</a>
|
|
130
|
+
</div>
|
|
131
|
+
</body>
|
|
132
|
+
</html>`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Escape HTML to prevent XSS
|
|
136
|
+
*/
|
|
137
|
+
function escapeHtml(text) {
|
|
138
|
+
const map = {
|
|
139
|
+
'&': '&',
|
|
140
|
+
'<': '<',
|
|
141
|
+
'>': '>',
|
|
142
|
+
'"': '"',
|
|
143
|
+
"'": ''',
|
|
144
|
+
};
|
|
145
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
146
|
+
}
|
|
147
|
+
async function previewRoutes(fastify) {
|
|
148
|
+
/**
|
|
149
|
+
* GET /:shortCode/preview
|
|
150
|
+
* Always return HTML preview page with Open Graph tags
|
|
151
|
+
* Auto-redirects after 2 seconds for human visitors
|
|
152
|
+
*/
|
|
153
|
+
fastify.get('/:shortCode/preview', async (request, reply) => {
|
|
154
|
+
const { shortCode } = request.params;
|
|
155
|
+
const baseUrl = request.headers.host
|
|
156
|
+
? `${request.protocol}://${request.headers.host}`
|
|
157
|
+
: 'https://link.forty';
|
|
158
|
+
try {
|
|
159
|
+
// Lookup link by short code
|
|
160
|
+
const result = await database_js_1.db.query(`SELECT * FROM links
|
|
161
|
+
WHERE short_code = $1 AND is_active = true
|
|
162
|
+
AND (expires_at IS NULL OR expires_at > NOW())`, [shortCode]);
|
|
163
|
+
if (result.rows.length === 0) {
|
|
164
|
+
return reply.status(404).send('Link not found');
|
|
165
|
+
}
|
|
166
|
+
const link = result.rows[0];
|
|
167
|
+
const shortUrl = `${baseUrl}/${shortCode}`;
|
|
168
|
+
// Return HTML with OG tags and auto-redirect
|
|
169
|
+
const html = generatePreviewHTML(link, shortUrl, true);
|
|
170
|
+
return reply
|
|
171
|
+
.header('Content-Type', 'text/html; charset=utf-8')
|
|
172
|
+
.send(html);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
fastify.log.error(`Error generating preview: ${error}`);
|
|
176
|
+
return reply.status(500).send('Error generating preview');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
/**
|
|
180
|
+
* Middleware for /:shortCode route to detect social scrapers
|
|
181
|
+
* If scraper detected, return OG preview instead of redirecting
|
|
182
|
+
*
|
|
183
|
+
* Note: This should be registered BEFORE the main redirect route
|
|
184
|
+
*/
|
|
185
|
+
fastify.addHook('preHandler', async (request, reply) => {
|
|
186
|
+
// Only apply to short code routes (not API routes, not preview routes)
|
|
187
|
+
const path = request.url.split('?')[0]; // Remove query string
|
|
188
|
+
if (path.startsWith('/api/') ||
|
|
189
|
+
path.endsWith('/preview') ||
|
|
190
|
+
path === '/' ||
|
|
191
|
+
path.includes('.')) {
|
|
192
|
+
return; // Skip this hook
|
|
193
|
+
}
|
|
194
|
+
const userAgent = request.headers['user-agent'] || '';
|
|
195
|
+
const shortCode = path.split('/').pop() || '';
|
|
196
|
+
const baseUrl = request.headers.host
|
|
197
|
+
? `${request.protocol}://${request.headers.host}`
|
|
198
|
+
: 'https://link.forty';
|
|
199
|
+
// If it's a social scraper, return OG preview HTML
|
|
200
|
+
if (isSocialScraper(userAgent)) {
|
|
201
|
+
try {
|
|
202
|
+
const result = await database_js_1.db.query(`SELECT * FROM links
|
|
203
|
+
WHERE short_code = $1 AND is_active = true
|
|
204
|
+
AND (expires_at IS NULL OR expires_at > NOW())`, [shortCode]);
|
|
205
|
+
if (result.rows.length > 0) {
|
|
206
|
+
const link = result.rows[0];
|
|
207
|
+
const shortUrl = `${baseUrl}/${shortCode}`;
|
|
208
|
+
// Return HTML with OG tags, no auto-redirect for bots
|
|
209
|
+
const html = generatePreviewHTML(link, shortUrl, false);
|
|
210
|
+
reply
|
|
211
|
+
.header('Content-Type', 'text/html; charset=utf-8')
|
|
212
|
+
.send(html);
|
|
213
|
+
// Stop the request here, don't continue to redirect route
|
|
214
|
+
return reply;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
fastify.log.error(`Error in social scraper detection: ${error}`);
|
|
219
|
+
// Continue to normal redirect route on error
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Not a social scraper, continue to normal redirect logic
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../../src/routes/preview.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr.d.ts","sourceRoot":"","sources":["../../src/routes/qr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI1C;;GAEG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,iBAuItD"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.qrRoutes = qrRoutes;
|
|
7
|
+
const qrcode_1 = __importDefault(require("qrcode"));
|
|
8
|
+
const database_js_1 = require("../lib/database.js");
|
|
9
|
+
/**
|
|
10
|
+
* QR Code Routes - Generate QR codes for links
|
|
11
|
+
*/
|
|
12
|
+
async function qrRoutes(fastify) {
|
|
13
|
+
/**
|
|
14
|
+
* GET /api/links/:id/qr
|
|
15
|
+
* Generate QR code for a link
|
|
16
|
+
*
|
|
17
|
+
* Query parameters:
|
|
18
|
+
* - format: 'png' | 'svg' (default: 'png')
|
|
19
|
+
* - size: number 128-2048 (default: 512)
|
|
20
|
+
* - color: hex color for foreground (default: '#000000')
|
|
21
|
+
* - bgcolor: hex color for background (default: '#ffffff')
|
|
22
|
+
*
|
|
23
|
+
* Returns: QR code image (PNG or SVG)
|
|
24
|
+
*/
|
|
25
|
+
fastify.get('/api/links/:id/qr', async (request, reply) => {
|
|
26
|
+
const { id } = request.params;
|
|
27
|
+
const query = request.query;
|
|
28
|
+
const format = (query.format || 'png');
|
|
29
|
+
const size = Math.min(Math.max(parseInt(query.size || '512', 10), 128), 2048);
|
|
30
|
+
const color = query.color || '#000000';
|
|
31
|
+
const bgcolor = query.bgcolor || '#ffffff';
|
|
32
|
+
// Validate format
|
|
33
|
+
if (!['png', 'svg'].includes(format)) {
|
|
34
|
+
return reply.status(400).send({ error: 'Invalid format. Use "png" or "svg".' });
|
|
35
|
+
}
|
|
36
|
+
// Build cache key
|
|
37
|
+
const cacheKey = `qr:${id}:${format}:${size}:${color}:${bgcolor}`;
|
|
38
|
+
// Try to get from cache
|
|
39
|
+
if (fastify.redis) {
|
|
40
|
+
try {
|
|
41
|
+
const cached = await fastify.redis.get(cacheKey);
|
|
42
|
+
if (cached) {
|
|
43
|
+
fastify.log.info(`QR code cache hit: ${cacheKey}`);
|
|
44
|
+
if (format === 'png') {
|
|
45
|
+
// Cached PNG is base64
|
|
46
|
+
const buffer = Buffer.from(cached, 'base64');
|
|
47
|
+
return reply
|
|
48
|
+
.type('image/png')
|
|
49
|
+
.header('Cache-Control', 'public, max-age=86400') // 24 hours
|
|
50
|
+
.send(buffer);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Cached SVG is text
|
|
54
|
+
return reply
|
|
55
|
+
.type('image/svg+xml')
|
|
56
|
+
.header('Cache-Control', 'public, max-age=86400')
|
|
57
|
+
.send(cached);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
fastify.log.warn('Redis QR cache lookup failed');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Get link from database
|
|
66
|
+
const result = await database_js_1.db.query('SELECT short_code, original_url FROM links WHERE id = $1 AND is_active = true', [id]);
|
|
67
|
+
if (result.rows.length === 0) {
|
|
68
|
+
return reply.status(404).send({ error: 'Link not found' });
|
|
69
|
+
}
|
|
70
|
+
const link = result.rows[0];
|
|
71
|
+
// Build short URL (use original_url as fallback if short_code not available)
|
|
72
|
+
// In production, you'd want to use your actual domain
|
|
73
|
+
const shortUrl = link.short_code
|
|
74
|
+
? `${request.protocol}://${request.hostname}/${link.short_code}`
|
|
75
|
+
: link.original_url;
|
|
76
|
+
try {
|
|
77
|
+
// QR code options
|
|
78
|
+
const options = {
|
|
79
|
+
errorCorrectionLevel: 'M', // Medium error correction
|
|
80
|
+
margin: 1, // Quiet zone margin
|
|
81
|
+
width: size,
|
|
82
|
+
color: {
|
|
83
|
+
dark: color,
|
|
84
|
+
light: bgcolor,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
if (format === 'png') {
|
|
88
|
+
// Generate PNG as buffer
|
|
89
|
+
const buffer = await qrcode_1.default.toBuffer(shortUrl, options);
|
|
90
|
+
// Cache as base64
|
|
91
|
+
if (fastify.redis) {
|
|
92
|
+
try {
|
|
93
|
+
await fastify.redis.setex(cacheKey, 86400, buffer.toString('base64')); // 24 hour TTL
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
fastify.log.warn('Failed to cache QR code');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return reply
|
|
100
|
+
.type('image/png')
|
|
101
|
+
.header('Cache-Control', 'public, max-age=86400')
|
|
102
|
+
.header('Content-Disposition', `inline; filename="qr-${link.short_code || 'code'}.png"`)
|
|
103
|
+
.send(buffer);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Generate SVG as string
|
|
107
|
+
const svg = await qrcode_1.default.toString(shortUrl, {
|
|
108
|
+
...options,
|
|
109
|
+
type: 'svg',
|
|
110
|
+
});
|
|
111
|
+
// Cache SVG text
|
|
112
|
+
if (fastify.redis) {
|
|
113
|
+
try {
|
|
114
|
+
await fastify.redis.setex(cacheKey, 86400, svg);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
fastify.log.warn('Failed to cache QR code');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return reply
|
|
121
|
+
.type('image/svg+xml')
|
|
122
|
+
.header('Cache-Control', 'public, max-age=86400')
|
|
123
|
+
.header('Content-Disposition', `inline; filename="qr-${link.short_code || 'code'}.svg"`)
|
|
124
|
+
.send(svg);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
fastify.log.error(`QR code generation failed: ${error.message}`);
|
|
129
|
+
return reply.status(500).send({
|
|
130
|
+
error: 'Failed to generate QR code',
|
|
131
|
+
message: error.message
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=qr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/routes/qr.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAM1C,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,iBAsS5D"}
|
package/dist/routes/redirect.js
CHANGED
|
@@ -1,15 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.redirectRoutes = redirectRoutes;
|
|
4
37
|
const database_js_1 = require("../lib/database.js");
|
|
5
38
|
const utils_js_1 = require("../lib/utils.js");
|
|
39
|
+
const fingerprint_js_1 = require("../lib/fingerprint.js");
|
|
40
|
+
const event_emitter_js_1 = require("../lib/event-emitter.js");
|
|
6
41
|
async function redirectRoutes(fastify) {
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
const { shortCode } = request.params;
|
|
42
|
+
// Helper function to handle the actual redirect logic
|
|
43
|
+
async function handleRedirect(request, reply, shortCode, templateSlug) {
|
|
10
44
|
let linkData = null;
|
|
45
|
+
// Build cache key (include template if present)
|
|
46
|
+
const cacheKey = templateSlug ? `link:${templateSlug}:${shortCode}` : `link:${shortCode}`;
|
|
11
47
|
// Try to get link from cache if Redis is available
|
|
12
|
-
const cacheKey = `link:${shortCode}`;
|
|
13
48
|
if (fastify.redis) {
|
|
14
49
|
try {
|
|
15
50
|
linkData = await fastify.redis.get(cacheKey);
|
|
@@ -19,10 +54,30 @@ async function redirectRoutes(fastify) {
|
|
|
19
54
|
}
|
|
20
55
|
}
|
|
21
56
|
if (!linkData) {
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
57
|
+
// Build query based on whether template slug is provided
|
|
58
|
+
let query;
|
|
59
|
+
let params;
|
|
60
|
+
if (templateSlug) {
|
|
61
|
+
// Template-based URL: verify both template and link match
|
|
62
|
+
query = `
|
|
63
|
+
SELECT l.* FROM links l
|
|
64
|
+
LEFT JOIN link_templates t ON l.template_id = t.id
|
|
65
|
+
WHERE l.short_code = $1 AND t.slug = $2
|
|
66
|
+
AND l.is_active = true
|
|
67
|
+
AND (l.expires_at IS NULL OR l.expires_at > NOW())
|
|
68
|
+
`;
|
|
69
|
+
params = [shortCode, templateSlug];
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Legacy URL: just lookup by short code
|
|
73
|
+
query = `
|
|
74
|
+
SELECT * FROM links
|
|
75
|
+
WHERE short_code = $1 AND is_active = true
|
|
76
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
77
|
+
`;
|
|
78
|
+
params = [shortCode];
|
|
79
|
+
}
|
|
80
|
+
const result = await database_js_1.db.query(query, params);
|
|
26
81
|
if (result.rows.length === 0) {
|
|
27
82
|
return reply.status(404).send({ error: 'Link not found' });
|
|
28
83
|
}
|
|
@@ -81,18 +136,27 @@ async function redirectRoutes(fastify) {
|
|
|
81
136
|
const userAgent = request.headers['user-agent'] || '';
|
|
82
137
|
const ip = request.ip;
|
|
83
138
|
const referrer = request.headers.referer || null;
|
|
84
|
-
const
|
|
139
|
+
const acceptLanguage = request.headers['accept-language'] || '';
|
|
140
|
+
const deviceType = (0, utils_js_1.detectDevice)(userAgent);
|
|
141
|
+
const { platform, platformVersion } = (0, utils_js_1.parseUserAgent)(userAgent);
|
|
85
142
|
const { countryCode, countryName, region, city, latitude, longitude, timezone } = (0, utils_js_1.getLocationFromIP)(ip);
|
|
86
143
|
// Extract UTM parameters from query string
|
|
87
144
|
const query = request.query;
|
|
88
145
|
const utmSource = query?.utm_source;
|
|
89
146
|
const utmMedium = query?.utm_medium;
|
|
90
147
|
const utmCampaign = query?.utm_campaign;
|
|
91
|
-
|
|
148
|
+
// Extract fingerprint data from query params (sent by SDK/client)
|
|
149
|
+
const fpTimezone = query?.fp_tz || timezone || undefined;
|
|
150
|
+
const fpLanguage = query?.fp_lang || acceptLanguage.split(',')[0]?.split(';')[0] || undefined;
|
|
151
|
+
const fpScreenWidth = query?.fp_sw ? parseInt(query.fp_sw, 10) : undefined;
|
|
152
|
+
const fpScreenHeight = query?.fp_sh ? parseInt(query.fp_sh, 10) : undefined;
|
|
153
|
+
// Insert click event
|
|
154
|
+
const clickResult = await database_js_1.db.query(`INSERT INTO click_events (
|
|
92
155
|
link_id, ip_address, user_agent, device_type, platform,
|
|
93
156
|
country_code, country_name, region, city, latitude, longitude, timezone,
|
|
94
157
|
utm_source, utm_medium, utm_campaign, referrer
|
|
95
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
158
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
159
|
+
RETURNING id`, [
|
|
96
160
|
link.id,
|
|
97
161
|
ip,
|
|
98
162
|
userAgent,
|
|
@@ -110,6 +174,88 @@ async function redirectRoutes(fastify) {
|
|
|
110
174
|
utmCampaign,
|
|
111
175
|
referrer,
|
|
112
176
|
]);
|
|
177
|
+
const clickId = clickResult.rows[0].id;
|
|
178
|
+
// Store device fingerprint for deferred deep linking
|
|
179
|
+
const fingerprintData = {
|
|
180
|
+
ipAddress: ip,
|
|
181
|
+
userAgent,
|
|
182
|
+
timezone: fpTimezone,
|
|
183
|
+
language: fpLanguage,
|
|
184
|
+
screenWidth: fpScreenWidth,
|
|
185
|
+
screenHeight: fpScreenHeight,
|
|
186
|
+
platform: deviceType,
|
|
187
|
+
platformVersion,
|
|
188
|
+
};
|
|
189
|
+
await (0, fingerprint_js_1.storeFingerprintForClick)(clickId, fingerprintData);
|
|
190
|
+
// Determine redirect URL for event emission
|
|
191
|
+
let redirectUrl = link.original_url;
|
|
192
|
+
let redirectReason = 'original_url';
|
|
193
|
+
if (deviceType === 'ios' && link.ios_url) {
|
|
194
|
+
redirectUrl = link.ios_url;
|
|
195
|
+
redirectReason = 'ios_url';
|
|
196
|
+
}
|
|
197
|
+
else if (deviceType === 'android' && link.android_url) {
|
|
198
|
+
redirectUrl = link.android_url;
|
|
199
|
+
redirectReason = 'android_url';
|
|
200
|
+
}
|
|
201
|
+
else if (deviceType === 'web' && link.web_fallback_url) {
|
|
202
|
+
redirectUrl = link.web_fallback_url;
|
|
203
|
+
redirectReason = 'web_fallback_url';
|
|
204
|
+
}
|
|
205
|
+
const finalRedirectUrl = (0, utils_js_1.buildRedirectUrl)(redirectUrl, link.utm_parameters);
|
|
206
|
+
// Emit click event for real-time streaming to WebSocket clients
|
|
207
|
+
(0, event_emitter_js_1.emitClickEvent)({
|
|
208
|
+
eventId: clickId,
|
|
209
|
+
timestamp: new Date().toISOString(),
|
|
210
|
+
linkId: link.id,
|
|
211
|
+
shortCode: link.short_code,
|
|
212
|
+
userId: link.user_id,
|
|
213
|
+
ipAddress: ip,
|
|
214
|
+
userAgent,
|
|
215
|
+
country: countryCode || undefined,
|
|
216
|
+
city: city || undefined,
|
|
217
|
+
deviceType,
|
|
218
|
+
platform: platform || undefined,
|
|
219
|
+
redirectUrl: finalRedirectUrl,
|
|
220
|
+
redirectReason,
|
|
221
|
+
targetingMatched: true, // If we got here, targeting matched
|
|
222
|
+
utmParameters: link.utm_parameters || undefined,
|
|
223
|
+
referer: referrer || undefined,
|
|
224
|
+
language: fpLanguage,
|
|
225
|
+
});
|
|
226
|
+
// Trigger webhooks for click_event
|
|
227
|
+
try {
|
|
228
|
+
const webhooksResult = await database_js_1.db.query('SELECT * FROM webhooks WHERE user_id = $1 AND is_active = true', [link.user_id]);
|
|
229
|
+
if (webhooksResult.rows.length > 0) {
|
|
230
|
+
const { triggerWebhooks } = await Promise.resolve().then(() => __importStar(require('../lib/webhook.js')));
|
|
231
|
+
const clickEventData = {
|
|
232
|
+
id: clickId,
|
|
233
|
+
linkId: link.id,
|
|
234
|
+
clickedAt: new Date().toISOString(),
|
|
235
|
+
ipAddress: ip,
|
|
236
|
+
userAgent,
|
|
237
|
+
deviceType,
|
|
238
|
+
platform,
|
|
239
|
+
countryCode,
|
|
240
|
+
countryName,
|
|
241
|
+
region,
|
|
242
|
+
city,
|
|
243
|
+
latitude,
|
|
244
|
+
longitude,
|
|
245
|
+
timezone,
|
|
246
|
+
utmSource,
|
|
247
|
+
utmMedium,
|
|
248
|
+
utmCampaign,
|
|
249
|
+
referrer,
|
|
250
|
+
};
|
|
251
|
+
// Trigger webhooks without delivery logging (basic version)
|
|
252
|
+
// For delivery logging, use @linkforty/cloud premium features
|
|
253
|
+
await triggerWebhooks(webhooksResult.rows, 'click_event', clickId, clickEventData);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (webhookError) {
|
|
257
|
+
fastify.log.error(`Error triggering click webhooks: ${webhookError}`);
|
|
258
|
+
}
|
|
113
259
|
}
|
|
114
260
|
catch (error) {
|
|
115
261
|
fastify.log.error(`Error tracking click: ${error}`);
|
|
@@ -133,6 +279,16 @@ async function redirectRoutes(fastify) {
|
|
|
133
279
|
const finalUrl = (0, utils_js_1.buildRedirectUrl)(redirectUrl, link.utm_parameters);
|
|
134
280
|
// Redirect
|
|
135
281
|
return reply.redirect(302, finalUrl);
|
|
282
|
+
}
|
|
283
|
+
// Template-based shortlink route: /:templateSlug/:shortCode
|
|
284
|
+
fastify.get('/:templateSlug/:shortCode', async (request, reply) => {
|
|
285
|
+
const { templateSlug, shortCode } = request.params;
|
|
286
|
+
return handleRedirect(request, reply, shortCode, templateSlug);
|
|
287
|
+
});
|
|
288
|
+
// Legacy shortlink route (no template): /:shortCode
|
|
289
|
+
fastify.get('/:shortCode', async (request, reply) => {
|
|
290
|
+
const { shortCode } = request.params;
|
|
291
|
+
return handleRedirect(request, reply, shortCode);
|
|
136
292
|
});
|
|
137
293
|
}
|
|
138
294
|
//# sourceMappingURL=redirect.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"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"}
|