@linkforty/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
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.db = void 0;
7
+ exports.initializeDatabase = initializeDatabase;
8
+ const pg_1 = __importDefault(require("pg"));
9
+ const { Pool } = pg_1.default;
10
+ // Helper function to wait for a specified time
11
+ function sleep(ms) {
12
+ return new Promise(resolve => setTimeout(resolve, ms));
13
+ }
14
+ // Retry database connection with exponential backoff
15
+ async function connectWithRetry(maxRetries = 10, baseDelay = 1000) {
16
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
17
+ try {
18
+ const client = await exports.db.connect();
19
+ console.log('Database connection established successfully');
20
+ return client;
21
+ }
22
+ catch (error) {
23
+ if (error.code === 'ECONNREFUSED' && attempt < maxRetries) {
24
+ const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
25
+ console.log(`Database connection attempt ${attempt} failed. Retrying in ${delay}ms...`);
26
+ await sleep(delay);
27
+ }
28
+ else {
29
+ console.error('Failed to connect to database after all retries:', error);
30
+ throw error;
31
+ }
32
+ }
33
+ }
34
+ throw new Error('Max retries exceeded');
35
+ }
36
+ // Initialize database schema
37
+ async function initializeDatabase(options = {}) {
38
+ // Initialize pool
39
+ exports.db = new Pool({
40
+ connectionString: options.url || process.env.DATABASE_URL || 'postgresql://postgres:password@localhost:5432/linkforty',
41
+ ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
42
+ min: options.pool?.min || 2,
43
+ max: options.pool?.max || 10,
44
+ });
45
+ const client = await connectWithRetry();
46
+ try {
47
+ // Users table
48
+ await client.query(`
49
+ CREATE TABLE IF NOT EXISTS users (
50
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
51
+ email VARCHAR(255) UNIQUE NOT NULL,
52
+ name VARCHAR(255) NOT NULL,
53
+ password_hash VARCHAR(255) NOT NULL,
54
+ created_at TIMESTAMP DEFAULT NOW(),
55
+ updated_at TIMESTAMP DEFAULT NOW()
56
+ )
57
+ `);
58
+ // Links table
59
+ await client.query(`
60
+ CREATE TABLE IF NOT EXISTS links (
61
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
62
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
63
+ short_code VARCHAR(20) UNIQUE NOT NULL,
64
+ original_url TEXT NOT NULL,
65
+ title VARCHAR(255),
66
+ ios_url TEXT,
67
+ android_url TEXT,
68
+ web_fallback_url TEXT,
69
+ utm_parameters JSONB DEFAULT '{}',
70
+ targeting_rules JSONB DEFAULT '{}',
71
+ is_active BOOLEAN DEFAULT true,
72
+ expires_at TIMESTAMP,
73
+ created_at TIMESTAMP DEFAULT NOW(),
74
+ updated_at TIMESTAMP DEFAULT NOW()
75
+ )
76
+ `);
77
+ // Click events table
78
+ await client.query(`
79
+ CREATE TABLE IF NOT EXISTS click_events (
80
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
81
+ link_id UUID NOT NULL REFERENCES links(id) ON DELETE CASCADE,
82
+ clicked_at TIMESTAMP DEFAULT NOW(),
83
+ ip_address INET,
84
+ user_agent TEXT,
85
+ device_type VARCHAR(20),
86
+ platform VARCHAR(20),
87
+ country_code CHAR(2),
88
+ country_name VARCHAR(100),
89
+ region VARCHAR(100),
90
+ city VARCHAR(100),
91
+ latitude DECIMAL(10, 8),
92
+ longitude DECIMAL(11, 8),
93
+ timezone VARCHAR(100),
94
+ utm_source VARCHAR(255),
95
+ utm_medium VARCHAR(255),
96
+ utm_campaign VARCHAR(255),
97
+ referrer TEXT
98
+ )
99
+ `);
100
+ // Create indexes for performance
101
+ await client.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_links_short_code ON links(short_code)');
102
+ await client.query('CREATE INDEX IF NOT EXISTS idx_links_user_id ON links(user_id)');
103
+ await client.query('CREATE INDEX IF NOT EXISTS idx_links_created_at ON links(created_at DESC)');
104
+ await client.query('CREATE INDEX IF NOT EXISTS idx_clicks_link_id ON click_events(link_id)');
105
+ await client.query('CREATE INDEX IF NOT EXISTS idx_clicks_timestamp ON click_events(clicked_at DESC)');
106
+ await client.query('CREATE INDEX IF NOT EXISTS idx_clicks_link_date ON click_events(link_id, clicked_at DESC)');
107
+ await client.query('CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)');
108
+ console.log('Database schema initialized successfully');
109
+ }
110
+ catch (error) {
111
+ console.error('Error initializing database:', error);
112
+ throw error;
113
+ }
114
+ finally {
115
+ client.release();
116
+ }
117
+ }
118
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export declare function generateShortCode(length?: number): string;
2
+ export declare function parseUserAgent(userAgent: string): {
3
+ deviceType: string;
4
+ platform: string;
5
+ browser: string;
6
+ };
7
+ export declare function getLocationFromIP(ip: string): {
8
+ countryCode: null;
9
+ countryName: null;
10
+ region: null;
11
+ city: null;
12
+ latitude: null;
13
+ longitude: null;
14
+ timezone: null;
15
+ } | {
16
+ countryCode: string;
17
+ countryName: string;
18
+ region: string;
19
+ city: string;
20
+ latitude: number | null;
21
+ longitude: number | null;
22
+ timezone: string;
23
+ };
24
+ export declare function buildRedirectUrl(originalUrl: string, utmParameters?: Record<string, string>): string;
25
+ export declare function detectDevice(userAgent: string): 'ios' | 'android' | 'web';
26
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAIA,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,MAAU,GAAG,MAAM,CAE5D;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM;;;;EAS/C;AAoDD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;EAwB3C;AAED,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACrC,MAAM,CAYR;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,CAYzE"}
@@ -0,0 +1,119 @@
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.generateShortCode = generateShortCode;
7
+ exports.parseUserAgent = parseUserAgent;
8
+ exports.getLocationFromIP = getLocationFromIP;
9
+ exports.buildRedirectUrl = buildRedirectUrl;
10
+ exports.detectDevice = detectDevice;
11
+ const nanoid_1 = require("nanoid");
12
+ const geoip_lite_1 = __importDefault(require("geoip-lite"));
13
+ const ua_parser_js_1 = __importDefault(require("ua-parser-js"));
14
+ function generateShortCode(length = 8) {
15
+ return (0, nanoid_1.nanoid)(length);
16
+ }
17
+ function parseUserAgent(userAgent) {
18
+ const parser = new ua_parser_js_1.default(userAgent);
19
+ const result = parser.getResult();
20
+ return {
21
+ deviceType: result.device.type || 'desktop',
22
+ platform: result.os.name || 'unknown',
23
+ browser: result.browser.name || 'unknown',
24
+ };
25
+ }
26
+ // Country code to name mapping (common countries)
27
+ const COUNTRY_NAMES = {
28
+ US: 'United States',
29
+ GB: 'United Kingdom',
30
+ CA: 'Canada',
31
+ AU: 'Australia',
32
+ DE: 'Germany',
33
+ FR: 'France',
34
+ IT: 'Italy',
35
+ ES: 'Spain',
36
+ NL: 'Netherlands',
37
+ SE: 'Sweden',
38
+ NO: 'Norway',
39
+ DK: 'Denmark',
40
+ FI: 'Finland',
41
+ PL: 'Poland',
42
+ BR: 'Brazil',
43
+ MX: 'Mexico',
44
+ AR: 'Argentina',
45
+ IN: 'India',
46
+ CN: 'China',
47
+ JP: 'Japan',
48
+ KR: 'South Korea',
49
+ SG: 'Singapore',
50
+ MY: 'Malaysia',
51
+ TH: 'Thailand',
52
+ ID: 'Indonesia',
53
+ PH: 'Philippines',
54
+ VN: 'Vietnam',
55
+ ZA: 'South Africa',
56
+ EG: 'Egypt',
57
+ NG: 'Nigeria',
58
+ KE: 'Kenya',
59
+ RU: 'Russia',
60
+ TR: 'Turkey',
61
+ AE: 'United Arab Emirates',
62
+ SA: 'Saudi Arabia',
63
+ IL: 'Israel',
64
+ NZ: 'New Zealand',
65
+ IE: 'Ireland',
66
+ CH: 'Switzerland',
67
+ AT: 'Austria',
68
+ BE: 'Belgium',
69
+ PT: 'Portugal',
70
+ GR: 'Greece',
71
+ CZ: 'Czech Republic',
72
+ HU: 'Hungary',
73
+ RO: 'Romania',
74
+ };
75
+ function getLocationFromIP(ip) {
76
+ const geo = geoip_lite_1.default.lookup(ip);
77
+ if (!geo) {
78
+ return {
79
+ countryCode: null,
80
+ countryName: null,
81
+ region: null,
82
+ city: null,
83
+ latitude: null,
84
+ longitude: null,
85
+ timezone: null,
86
+ };
87
+ }
88
+ return {
89
+ countryCode: geo.country,
90
+ countryName: COUNTRY_NAMES[geo.country] || geo.country,
91
+ region: geo.region,
92
+ city: geo.city,
93
+ latitude: geo.ll?.[0] || null,
94
+ longitude: geo.ll?.[1] || null,
95
+ timezone: geo.timezone,
96
+ };
97
+ }
98
+ function buildRedirectUrl(originalUrl, utmParameters) {
99
+ const url = new URL(originalUrl);
100
+ if (utmParameters) {
101
+ Object.entries(utmParameters).forEach(([key, value]) => {
102
+ if (value) {
103
+ url.searchParams.set(`utm_${key}`, value);
104
+ }
105
+ });
106
+ }
107
+ return url.toString();
108
+ }
109
+ function detectDevice(userAgent) {
110
+ const ua = userAgent.toLowerCase();
111
+ if (ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod')) {
112
+ return 'ios';
113
+ }
114
+ if (ua.includes('android')) {
115
+ return 'android';
116
+ }
117
+ return 'web';
118
+ }
119
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ export declare function analyticsRoutes(fastify: FastifyInstance): Promise<void>;
3
+ //# sourceMappingURL=analytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/routes/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAG1D,wBAAsB,eAAe,CAAC,OAAO,EAAE,eAAe,iBAgO7D"}
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyticsRoutes = analyticsRoutes;
4
+ const database_js_1 = require("../lib/database.js");
5
+ async function analyticsRoutes(fastify) {
6
+ // Get overall analytics for a user
7
+ fastify.get('/api/analytics/overview', async (request) => {
8
+ const { userId, days = 30 } = request.query;
9
+ if (!userId) {
10
+ throw new Error('userId query parameter is required');
11
+ }
12
+ // Get total and unique clicks
13
+ const clicksResult = await database_js_1.db.query(`SELECT
14
+ COUNT(*) as total_clicks,
15
+ COUNT(DISTINCT ip_address) as unique_clicks
16
+ FROM click_events ce
17
+ JOIN links l ON ce.link_id = l.id
18
+ WHERE l.user_id = $1 AND ce.clicked_at >= NOW() - INTERVAL '${days} days'`, [userId]);
19
+ // Get clicks by date
20
+ const dateResult = await database_js_1.db.query(`SELECT
21
+ DATE(ce.clicked_at) as date,
22
+ COUNT(*) as clicks
23
+ FROM click_events ce
24
+ JOIN links l ON ce.link_id = l.id
25
+ WHERE l.user_id = $1 AND ce.clicked_at >= NOW() - INTERVAL '${days} days'
26
+ GROUP BY DATE(ce.clicked_at)
27
+ ORDER BY date`, [userId]);
28
+ // Get clicks by country
29
+ const countryResult = await database_js_1.db.query(`SELECT
30
+ COALESCE(ce.country_code, 'Unknown') as country_code,
31
+ COALESCE(ce.country_name, ce.country_code, 'Unknown') as country,
32
+ COUNT(*) as clicks
33
+ FROM click_events ce
34
+ JOIN links l ON ce.link_id = l.id
35
+ WHERE l.user_id = $1 AND ce.clicked_at >= NOW() - INTERVAL '${days} days'
36
+ GROUP BY ce.country_code, ce.country_name
37
+ ORDER BY clicks DESC`, [userId]);
38
+ // Get clicks by device
39
+ const deviceResult = await database_js_1.db.query(`SELECT
40
+ COALESCE(ce.device_type, 'Unknown') as device,
41
+ COUNT(*) as clicks
42
+ FROM click_events ce
43
+ JOIN links l ON ce.link_id = l.id
44
+ WHERE l.user_id = $1 AND ce.clicked_at >= NOW() - INTERVAL '${days} days'
45
+ GROUP BY ce.device_type
46
+ ORDER BY clicks DESC`, [userId]);
47
+ // Get clicks by platform
48
+ const platformResult = await database_js_1.db.query(`SELECT
49
+ COALESCE(ce.platform, 'Unknown') as platform,
50
+ COUNT(*) as clicks
51
+ FROM click_events ce
52
+ JOIN links l ON ce.link_id = l.id
53
+ WHERE l.user_id = $1 AND ce.clicked_at >= NOW() - INTERVAL '${days} days'
54
+ GROUP BY ce.platform
55
+ ORDER BY clicks DESC`, [userId]);
56
+ // Get top performing links
57
+ const topLinksResult = await database_js_1.db.query(`SELECT
58
+ l.id,
59
+ l.short_code,
60
+ l.title,
61
+ l.original_url,
62
+ COUNT(ce.id) as total_clicks,
63
+ COUNT(DISTINCT ce.ip_address) as unique_clicks
64
+ FROM links l
65
+ LEFT JOIN click_events ce ON l.id = ce.link_id
66
+ AND ce.clicked_at >= NOW() - INTERVAL '${days} days'
67
+ WHERE l.user_id = $1
68
+ GROUP BY l.id
69
+ ORDER BY total_clicks DESC
70
+ LIMIT 10`, [userId]);
71
+ return {
72
+ totalClicks: parseInt(clicksResult.rows[0]?.total_clicks || '0'),
73
+ uniqueClicks: parseInt(clicksResult.rows[0]?.unique_clicks || '0'),
74
+ clicksByDate: dateResult.rows.map(row => ({
75
+ date: row.date,
76
+ clicks: parseInt(row.clicks),
77
+ })),
78
+ clicksByCountry: countryResult.rows.map(row => ({
79
+ countryCode: row.country_code,
80
+ country: row.country,
81
+ clicks: parseInt(row.clicks),
82
+ })),
83
+ clicksByDevice: deviceResult.rows.map(row => ({
84
+ device: row.device,
85
+ clicks: parseInt(row.clicks),
86
+ })),
87
+ clicksByPlatform: platformResult.rows.map(row => ({
88
+ platform: row.platform,
89
+ clicks: parseInt(row.clicks),
90
+ })),
91
+ topLinks: topLinksResult.rows.map(row => ({
92
+ id: row.id,
93
+ shortCode: row.short_code,
94
+ title: row.title,
95
+ originalUrl: row.original_url,
96
+ totalClicks: parseInt(row.total_clicks),
97
+ uniqueClicks: parseInt(row.unique_clicks),
98
+ })),
99
+ };
100
+ });
101
+ // Get link-specific analytics
102
+ fastify.get('/api/analytics/links/:linkId', async (request) => {
103
+ const { linkId } = request.params;
104
+ const { userId, days = 30 } = request.query;
105
+ if (!userId) {
106
+ throw new Error('userId query parameter is required');
107
+ }
108
+ // Verify link ownership
109
+ const linkResult = await database_js_1.db.query('SELECT id FROM links WHERE id = $1 AND user_id = $2', [linkId, userId]);
110
+ if (linkResult.rows.length === 0) {
111
+ throw new Error('Link not found');
112
+ }
113
+ // Get analytics for specific link
114
+ const clicksResult = await database_js_1.db.query(`SELECT
115
+ COUNT(*) as total_clicks,
116
+ COUNT(DISTINCT ip_address) as unique_clicks
117
+ FROM click_events
118
+ WHERE link_id = $1 AND clicked_at >= NOW() - INTERVAL '${days} days'`, [linkId]);
119
+ const dateResult = await database_js_1.db.query(`SELECT
120
+ DATE(clicked_at) as date,
121
+ COUNT(*) as clicks
122
+ FROM click_events
123
+ WHERE link_id = $1 AND clicked_at >= NOW() - INTERVAL '${days} days'
124
+ GROUP BY DATE(clicked_at)
125
+ ORDER BY date`, [linkId]);
126
+ const countryResult = await database_js_1.db.query(`SELECT
127
+ COALESCE(country_code, 'Unknown') as country_code,
128
+ COALESCE(country_name, country_code, 'Unknown') as country,
129
+ COUNT(*) as clicks
130
+ FROM click_events
131
+ WHERE link_id = $1 AND clicked_at >= NOW() - INTERVAL '${days} days'
132
+ GROUP BY country_code, country_name
133
+ ORDER BY clicks DESC`, [linkId]);
134
+ const deviceResult = await database_js_1.db.query(`SELECT
135
+ COALESCE(device_type, 'Unknown') as device,
136
+ COUNT(*) as clicks
137
+ FROM click_events
138
+ WHERE link_id = $1 AND clicked_at >= NOW() - INTERVAL '${days} days'
139
+ GROUP BY device_type
140
+ ORDER BY clicks DESC`, [linkId]);
141
+ const platformResult = await database_js_1.db.query(`SELECT
142
+ COALESCE(platform, 'Unknown') as platform,
143
+ COUNT(*) as clicks
144
+ FROM click_events
145
+ WHERE link_id = $1 AND clicked_at >= NOW() - INTERVAL '${days} days'
146
+ GROUP BY platform
147
+ ORDER BY clicks DESC`, [linkId]);
148
+ return {
149
+ totalClicks: parseInt(clicksResult.rows[0]?.total_clicks || '0'),
150
+ uniqueClicks: parseInt(clicksResult.rows[0]?.unique_clicks || '0'),
151
+ clicksByDate: dateResult.rows.map(row => ({
152
+ date: row.date,
153
+ clicks: parseInt(row.clicks),
154
+ })),
155
+ clicksByCountry: countryResult.rows.map(row => ({
156
+ countryCode: row.country_code,
157
+ country: row.country,
158
+ clicks: parseInt(row.clicks),
159
+ })),
160
+ clicksByDevice: deviceResult.rows.map(row => ({
161
+ device: row.device,
162
+ clicks: parseInt(row.clicks),
163
+ })),
164
+ clicksByPlatform: platformResult.rows.map(row => ({
165
+ platform: row.platform,
166
+ clicks: parseInt(row.clicks),
167
+ })),
168
+ };
169
+ });
170
+ }
171
+ //# sourceMappingURL=analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/routes/analytics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { redirectRoutes } from './redirect.js';
2
+ export { linkRoutes } from './links.js';
3
+ export { analyticsRoutes } from './analytics.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyticsRoutes = exports.linkRoutes = exports.redirectRoutes = void 0;
4
+ var redirect_js_1 = require("./redirect.js");
5
+ Object.defineProperty(exports, "redirectRoutes", { enumerable: true, get: function () { return redirect_js_1.redirectRoutes; } });
6
+ var links_js_1 = require("./links.js");
7
+ Object.defineProperty(exports, "linkRoutes", { enumerable: true, get: function () { return links_js_1.linkRoutes; } });
8
+ var analytics_js_1 = require("./analytics.js");
9
+ Object.defineProperty(exports, "analyticsRoutes", { enumerable: true, get: function () { return analytics_js_1.analyticsRoutes; } });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":";;;AAAA,6CAA+C;AAAtC,6GAAA,cAAc,OAAA;AACvB,uCAAwC;AAA/B,sGAAA,UAAU,OAAA;AACnB,+CAAiD;AAAxC,+GAAA,eAAe,OAAA"}
@@ -0,0 +1,3 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ export declare function linkRoutes(fastify: FastifyInstance): Promise<void>;
3
+ //# sourceMappingURL=links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/routes/links.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAgC1D,wBAAsB,UAAU,CAAC,OAAO,EAAE,eAAe,iBA2MxD"}