@linkforty/core 1.4.4 → 1.6.6
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/LICENSE +620 -21
- package/README.md +306 -128
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +25 -7
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/event-emitter.d.ts +1 -1
- package/dist/lib/event-emitter.d.ts.map +1 -1
- package/dist/lib/fingerprint.js +4 -4
- package/dist/lib/fingerprint.js.map +1 -1
- package/dist/routes/analytics.d.ts.map +1 -1
- package/dist/routes/analytics.js +22 -19
- package/dist/routes/analytics.js.map +1 -1
- package/dist/routes/debug.d.ts.map +1 -1
- package/dist/routes/debug.js +17 -19
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/links.d.ts.map +1 -1
- package/dist/routes/links.js +75 -38
- package/dist/routes/links.js.map +1 -1
- package/dist/routes/sdk.js +4 -4
- package/dist/routes/sdk.js.map +1 -1
- package/dist/routes/templates.d.ts +3 -0
- package/dist/routes/templates.d.ts.map +1 -0
- package/dist/routes/templates.js +261 -0
- package/dist/routes/templates.js.map +1 -0
- package/dist/routes/webhooks.d.ts.map +1 -1
- package/dist/routes/webhooks.js +40 -24
- package/dist/routes/webhooks.js.map +1 -1
- package/dist/types/index.d.ts +29 -26
- package/dist/types/index.d.ts.map +1 -1
- package/llms.txt +763 -0
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
# LinkForty Core
|
|
5
5
|
|
|
6
|
-
**Open-source
|
|
6
|
+
**Open-source alternative to Branch.io, AppsFlyer OneLink, and Firebase Dynamic Links**
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Self-hosted deep linking engine with device detection, analytics, deferred deep linking, and smart routing. No per-click pricing, no vendor lock-in, full data ownership — runs on your own PostgreSQL. Firebase Dynamic Links shut down in August 2025; LinkForty is a production-ready, open-source replacement you can deploy today.
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
11
|
[](https://www.npmjs.com/package/@linkforty/core)
|
|
@@ -13,18 +13,52 @@
|
|
|
13
13
|
[](https://codecov.io/gh/linkforty/core)
|
|
14
14
|
[](https://hub.docker.com/r/linkforty/core)
|
|
15
15
|
[](https://hub.docker.com/r/linkforty/core)
|
|
16
|
-
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
17
|
+
|
|
18
|
+
## Why LinkForty?
|
|
19
|
+
|
|
20
|
+
- **Self-hosted and open-source** — AGPL-3.0 licensed, deploy on your own infrastructure
|
|
21
|
+
- **No per-click pricing** — No usage-based fees, no monthly minimums, no enterprise sales calls
|
|
22
|
+
- **Full data ownership** — All click data, analytics, and attribution stored in your PostgreSQL database
|
|
23
|
+
- **Privacy-first** — No third-party data sharing, no tracking pixels, your users' data stays with you
|
|
24
|
+
- **Drop-in replacement** — REST API + mobile SDKs for React Native, Expo, iOS (Swift), and Android (Kotlin)
|
|
25
|
+
- **Firebase Dynamic Links replacement** — Google shut down Firebase Dynamic Links in August 2025. LinkForty provides the same capabilities with a self-hosted, open-source stack
|
|
26
|
+
|
|
27
|
+
### How LinkForty Compares
|
|
28
|
+
|
|
29
|
+
| Feature | LinkForty Core | Branch | AppsFlyer | Firebase Dynamic Links |
|
|
30
|
+
|---------|---------------|--------|-----------|----------------------|
|
|
31
|
+
| **Pricing** | Free (self-hosted) | Starts at $299/mo | Starts at $500/mo | Shut down (Aug 2025) |
|
|
32
|
+
| **Open Source** | Yes (AGPL-3.0) | No | No | No |
|
|
33
|
+
| **Self-Hosted** | Yes | No | No | No |
|
|
34
|
+
| **Data Ownership** | Complete | Vendor-controlled | Vendor-controlled | Was Google-controlled |
|
|
35
|
+
| **Deferred Deep Linking** | Yes | Yes | Yes | Was supported |
|
|
36
|
+
| **Device Detection & Routing** | Yes | Yes | Yes | Was supported |
|
|
37
|
+
| **Click Analytics** | Yes | Yes | Yes | Basic |
|
|
38
|
+
| **QR Code Generation** | Built-in | No | No | No |
|
|
39
|
+
| **Webhooks** | Yes | Enterprise only | Enterprise only | No |
|
|
40
|
+
| **iOS Universal Links** | Yes | Yes | Yes | Was supported |
|
|
41
|
+
| **Android App Links** | Yes | Yes | Yes | Was supported |
|
|
42
|
+
| **UTM Parameter Tracking** | Yes | Yes | Custom params | Was supported |
|
|
43
|
+
| **Custom Domains** | Yes | Enterprise only | Enterprise only | No |
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
**Smart Link Routing** - Create short links with device-specific URLs for iOS, Android, and web \
|
|
48
|
+
**Device Detection** - Automatic detection and routing based on user device \
|
|
49
|
+
**Click Analytics** - Track clicks with geolocation, device type, platform, and more \
|
|
50
|
+
**UTM Parameters** - Built-in support for UTM campaign tracking \
|
|
51
|
+
**Targeting Rules** - Filter by country, device, and language before redirecting \
|
|
52
|
+
**QR Code Generation** - Generate QR codes (PNG/SVG) for any link \
|
|
53
|
+
**Deferred Deep Linking** - Probabilistic fingerprint matching for install attribution \
|
|
54
|
+
**Webhooks** - Event-driven integrations with HMAC-signed payloads and retry logic \
|
|
55
|
+
**OG Preview Pages** - Social media scraper detection with Open Graph meta tags \
|
|
56
|
+
**iOS Universal Links & Android App Links** - Serve `.well-known` files automatically \
|
|
57
|
+
**Link Expiration** - Set expiration dates for time-sensitive links \
|
|
58
|
+
**Redis Caching** - Optional Redis support for high-performance link lookups \
|
|
59
|
+
**PostgreSQL Storage** - Reliable data persistence with full SQL capabilities \
|
|
60
|
+
**TypeScript** - Fully typed API for better developer experience \
|
|
61
|
+
**No Auth Included** - Bring your own authentication; `userId` is optional for multi-tenant scoping
|
|
28
62
|
|
|
29
63
|
## Installation
|
|
30
64
|
|
|
@@ -81,17 +115,21 @@ docker run -d \
|
|
|
81
115
|
```
|
|
82
116
|
|
|
83
117
|
**Features:**
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
118
|
+
- Pre-built multi-architecture images (AMD64 + ARM64)
|
|
119
|
+
- Automatic updates with version tags
|
|
120
|
+
- Non-root user for security
|
|
121
|
+
- Built-in health checks
|
|
122
|
+
- Supply chain attestations (SBOM + Provenance)
|
|
89
123
|
|
|
90
124
|
See [DOCKER.md](DOCKER.md) for complete deployment guide.
|
|
91
125
|
|
|
92
126
|
## API Reference
|
|
93
127
|
|
|
94
|
-
###
|
|
128
|
+
### Links
|
|
129
|
+
|
|
130
|
+
#### Create a Link
|
|
131
|
+
|
|
132
|
+
`userId` is optional. When provided, the link is scoped to that user (multi-tenant mode). When omitted, the link has no owner (single-tenant mode).
|
|
95
133
|
|
|
96
134
|
```bash
|
|
97
135
|
POST /api/links
|
|
@@ -101,57 +139,54 @@ Content-Type: application/json
|
|
|
101
139
|
"userId": "user-uuid",
|
|
102
140
|
"originalUrl": "https://example.com",
|
|
103
141
|
"title": "My Link",
|
|
104
|
-
"
|
|
105
|
-
"
|
|
142
|
+
"description": "Summer campaign link",
|
|
143
|
+
"iosAppStoreUrl": "https://apps.apple.com/app/id123456",
|
|
144
|
+
"androidAppStoreUrl": "https://play.google.com/store/apps/details?id=com.example",
|
|
106
145
|
"webFallbackUrl": "https://example.com/product/123",
|
|
146
|
+
"appScheme": "myapp",
|
|
147
|
+
"iosUniversalLink": "https://example.com/app/product/123",
|
|
148
|
+
"androidAppLink": "https://example.com/app/product/123",
|
|
149
|
+
"deepLinkPath": "/product/123",
|
|
150
|
+
"deepLinkParameters": { "ref": "campaign-1" },
|
|
107
151
|
"utmParameters": {
|
|
108
152
|
"source": "twitter",
|
|
109
153
|
"medium": "social",
|
|
110
154
|
"campaign": "summer-sale"
|
|
111
155
|
},
|
|
156
|
+
"ogTitle": "Check out this deal",
|
|
157
|
+
"ogDescription": "50% off summer sale",
|
|
158
|
+
"ogImageUrl": "https://example.com/og-image.png",
|
|
159
|
+
"targetingRules": {
|
|
160
|
+
"countries": ["US", "CA"],
|
|
161
|
+
"devices": ["ios", "android"],
|
|
162
|
+
"languages": ["en"]
|
|
163
|
+
},
|
|
164
|
+
"attributionWindowHours": 168,
|
|
112
165
|
"customCode": "summer-sale",
|
|
113
|
-
"expiresAt": "
|
|
166
|
+
"expiresAt": "2026-12-31T23:59:59Z"
|
|
114
167
|
}
|
|
115
168
|
```
|
|
116
169
|
|
|
117
|
-
|
|
170
|
+
All fields except `originalUrl` are optional.
|
|
118
171
|
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
"id": "link-uuid",
|
|
122
|
-
"userId": "user-uuid",
|
|
123
|
-
"short_code": "summer-sale",
|
|
124
|
-
"original_url": "https://example.com",
|
|
125
|
-
"title": "My Link",
|
|
126
|
-
"ios_url": "myapp://product/123",
|
|
127
|
-
"android_url": "myapp://product/123",
|
|
128
|
-
"web_fallback_url": "https://example.com/product/123",
|
|
129
|
-
"utmParameters": {
|
|
130
|
-
"source": "twitter",
|
|
131
|
-
"medium": "social",
|
|
132
|
-
"campaign": "summer-sale"
|
|
133
|
-
},
|
|
134
|
-
"is_active": true,
|
|
135
|
-
"expires_at": "2024-12-31T23:59:59Z",
|
|
136
|
-
"created_at": "2024-01-01T00:00:00Z",
|
|
137
|
-
"updated_at": "2024-01-01T00:00:00Z",
|
|
138
|
-
"clickCount": 0
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### Get All Links
|
|
172
|
+
#### Get All Links
|
|
143
173
|
|
|
144
174
|
```bash
|
|
175
|
+
# Single-tenant (all links)
|
|
176
|
+
GET /api/links
|
|
177
|
+
|
|
178
|
+
# Multi-tenant (scoped to user)
|
|
145
179
|
GET /api/links?userId=user-uuid
|
|
146
180
|
```
|
|
147
181
|
|
|
148
|
-
|
|
182
|
+
#### Get a Specific Link
|
|
149
183
|
|
|
150
184
|
```bash
|
|
185
|
+
GET /api/links/:id
|
|
151
186
|
GET /api/links/:id?userId=user-uuid
|
|
152
187
|
```
|
|
153
188
|
|
|
154
|
-
|
|
189
|
+
#### Update a Link
|
|
155
190
|
|
|
156
191
|
```bash
|
|
157
192
|
PUT /api/links/:id?userId=user-uuid
|
|
@@ -163,62 +198,99 @@ Content-Type: application/json
|
|
|
163
198
|
}
|
|
164
199
|
```
|
|
165
200
|
|
|
166
|
-
|
|
201
|
+
#### Duplicate a Link
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
POST /api/links/:id/duplicate?userId=user-uuid
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### Delete a Link
|
|
167
208
|
|
|
168
209
|
```bash
|
|
169
210
|
DELETE /api/links/:id?userId=user-uuid
|
|
170
211
|
```
|
|
171
212
|
|
|
172
|
-
###
|
|
213
|
+
### Analytics
|
|
214
|
+
|
|
215
|
+
#### Get Analytics Overview
|
|
173
216
|
|
|
174
217
|
```bash
|
|
218
|
+
# All links
|
|
219
|
+
GET /api/analytics/overview?days=30
|
|
220
|
+
|
|
221
|
+
# Scoped to user
|
|
175
222
|
GET /api/analytics/overview?userId=user-uuid&days=30
|
|
176
223
|
```
|
|
177
224
|
|
|
178
|
-
|
|
225
|
+
Returns: `totalClicks`, `uniqueClicks`, `clicksByDate`, `clicksByCountry`, `clicksByDevice`, `clicksByPlatform`, `topLinks`
|
|
179
226
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"clicksByDate": [
|
|
185
|
-
{ "date": "2024-01-01", "clicks": 45 }
|
|
186
|
-
],
|
|
187
|
-
"clicksByCountry": [
|
|
188
|
-
{ "countryCode": "US", "country": "United States", "clicks": 234 }
|
|
189
|
-
],
|
|
190
|
-
"clicksByDevice": [
|
|
191
|
-
{ "device": "mobile", "clicks": 789 }
|
|
192
|
-
],
|
|
193
|
-
"clicksByPlatform": [
|
|
194
|
-
{ "platform": "iOS", "clicks": 456 }
|
|
195
|
-
],
|
|
196
|
-
"topLinks": [
|
|
197
|
-
{
|
|
198
|
-
"id": "link-uuid",
|
|
199
|
-
"shortCode": "summer-sale",
|
|
200
|
-
"title": "My Link",
|
|
201
|
-
"originalUrl": "https://example.com",
|
|
202
|
-
"totalClicks": 123,
|
|
203
|
-
"uniqueClicks": 67
|
|
204
|
-
}
|
|
205
|
-
]
|
|
206
|
-
}
|
|
227
|
+
#### Get Link-Specific Analytics
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
GET /api/analytics/links/:linkId?days=30
|
|
207
231
|
```
|
|
208
232
|
|
|
209
|
-
###
|
|
233
|
+
### Redirect
|
|
210
234
|
|
|
211
235
|
```bash
|
|
212
|
-
GET
|
|
236
|
+
GET /:shortCode
|
|
237
|
+
GET /:templateSlug/:shortCode
|
|
213
238
|
```
|
|
214
239
|
|
|
215
|
-
|
|
240
|
+
Automatically redirects users to the appropriate URL based on device type (iOS/Android/web), evaluates targeting rules, and tracks the click asynchronously.
|
|
241
|
+
|
|
242
|
+
### QR Codes
|
|
216
243
|
|
|
217
244
|
```bash
|
|
218
|
-
GET /:
|
|
245
|
+
GET /api/links/:id/qr?format=png&size=300
|
|
246
|
+
GET /api/links/:id/qr?format=svg
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Webhooks
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
GET /api/webhooks?userId=user-uuid
|
|
253
|
+
POST /api/webhooks # Body: { name, url, events, userId? }
|
|
254
|
+
GET /api/webhooks/:id?userId=user-uuid
|
|
255
|
+
PUT /api/webhooks/:id?userId=user-uuid
|
|
256
|
+
DELETE /api/webhooks/:id?userId=user-uuid
|
|
257
|
+
POST /api/webhooks/:id/test?userId=user-uuid
|
|
219
258
|
```
|
|
220
259
|
|
|
221
|
-
|
|
260
|
+
Events: `click_event`, `install_event`, `conversion_event`. Payloads are HMAC SHA-256 signed.
|
|
261
|
+
|
|
262
|
+
### Mobile SDK Endpoints
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
POST /api/sdk/v1/install # Report app install, get deferred deep link
|
|
266
|
+
GET /api/sdk/v1/attribution/:fingerprint # Debug attribution lookups
|
|
267
|
+
POST /api/sdk/v1/event # Track in-app conversion events
|
|
268
|
+
GET /api/sdk/v1/resolve/:shortCode # Resolve link to deep link data (no redirect)
|
|
269
|
+
GET /api/sdk/v1/health # Health check
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Debug & Testing
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
POST /api/debug/simulate # Simulate a link click with custom parameters
|
|
276
|
+
WS /api/debug/live?userId=user-uuid # WebSocket live click event stream
|
|
277
|
+
GET /api/debug/user-agents # Common UA strings for testing
|
|
278
|
+
GET /api/debug/countries # Common countries list
|
|
279
|
+
GET /api/debug/languages # Common languages list
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Well-Known Routes
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
GET /.well-known/apple-app-site-association # iOS Universal Links
|
|
286
|
+
GET /.well-known/assetlinks.json # Android App Links
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### OG Preview
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
GET /:shortCode/preview # OG meta tag page for social scrapers
|
|
293
|
+
```
|
|
222
294
|
|
|
223
295
|
## Configuration
|
|
224
296
|
|
|
@@ -251,39 +323,50 @@ REDIS_URL=redis://localhost:6379
|
|
|
251
323
|
PORT=3000
|
|
252
324
|
NODE_ENV=production
|
|
253
325
|
CORS_ORIGIN=*
|
|
326
|
+
|
|
327
|
+
# Mobile SDK (optional — for iOS Universal Links and Android App Links)
|
|
328
|
+
IOS_TEAM_ID=ABC123XYZ
|
|
329
|
+
IOS_BUNDLE_ID=com.yourcompany.yourapp
|
|
330
|
+
ANDROID_PACKAGE_NAME=com.yourcompany.yourapp
|
|
331
|
+
ANDROID_SHA256_FINGERPRINTS=AA:BB:CC:DD:...
|
|
332
|
+
|
|
333
|
+
# Custom domain for QR code URLs (optional)
|
|
334
|
+
SHORTLINK_DOMAIN=yourdomain.com
|
|
254
335
|
```
|
|
255
336
|
|
|
256
337
|
## Database Schema
|
|
257
338
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
| Column | Type | Description |
|
|
261
|
-
|---------------|--------------|-----------------------|
|
|
262
|
-
| id | UUID | Primary key |
|
|
263
|
-
| email | VARCHAR(255) | Unique email |
|
|
264
|
-
| name | VARCHAR(255) | User name |
|
|
265
|
-
| password_hash | VARCHAR(255) | Hashed password |
|
|
266
|
-
| created_at | TIMESTAMP | Creation timestamp |
|
|
267
|
-
| updated_at | TIMESTAMP | Last update timestamp |
|
|
339
|
+
Core does not create a `users` table. Authentication and user management are the consumer's responsibility. The `user_id` column on `links` and `webhooks` is optional (nullable, no foreign key) — use it for multi-tenant scoping when your auth layer provides a user identity.
|
|
268
340
|
|
|
269
341
|
### Links Table
|
|
270
342
|
|
|
271
|
-
| Column
|
|
272
|
-
|
|
273
|
-
| id
|
|
274
|
-
| user_id
|
|
275
|
-
| short_code
|
|
276
|
-
| original_url
|
|
277
|
-
| title
|
|
278
|
-
|
|
|
279
|
-
|
|
|
280
|
-
|
|
|
281
|
-
|
|
|
282
|
-
|
|
|
283
|
-
|
|
|
284
|
-
|
|
|
285
|
-
|
|
|
286
|
-
|
|
|
343
|
+
| Column | Type | Description |
|
|
344
|
+
|-------------------------|--------------|------------------------------------------|
|
|
345
|
+
| id | UUID | Primary key |
|
|
346
|
+
| user_id | UUID | Optional owner/tenant identifier |
|
|
347
|
+
| short_code | VARCHAR(20) | Unique short code |
|
|
348
|
+
| original_url | TEXT | Original URL |
|
|
349
|
+
| title | VARCHAR(255) | Link title |
|
|
350
|
+
| description | TEXT | Link description |
|
|
351
|
+
| ios_app_store_url | TEXT | iOS App Store URL |
|
|
352
|
+
| android_app_store_url | TEXT | Android Play Store URL |
|
|
353
|
+
| web_fallback_url | TEXT | Web fallback URL |
|
|
354
|
+
| app_scheme | VARCHAR(255) | URI scheme (e.g., "myapp") |
|
|
355
|
+
| ios_universal_link | TEXT | iOS Universal Link URL |
|
|
356
|
+
| android_app_link | TEXT | Android App Link URL |
|
|
357
|
+
| deep_link_path | TEXT | In-app destination path |
|
|
358
|
+
| deep_link_parameters | JSONB | Custom app parameters |
|
|
359
|
+
| utm_parameters | JSONB | UTM tracking parameters |
|
|
360
|
+
| targeting_rules | JSONB | Country/device/language targeting |
|
|
361
|
+
| og_title | VARCHAR(255) | Open Graph title |
|
|
362
|
+
| og_description | TEXT | Open Graph description |
|
|
363
|
+
| og_image_url | TEXT | Open Graph image URL |
|
|
364
|
+
| og_type | VARCHAR(50) | Open Graph type (default: "website") |
|
|
365
|
+
| attribution_window_hours| INTEGER | Install attribution window (default: 168)|
|
|
366
|
+
| is_active | BOOLEAN | Active status |
|
|
367
|
+
| expires_at | TIMESTAMP | Expiration date |
|
|
368
|
+
| created_at | TIMESTAMP | Creation timestamp |
|
|
369
|
+
| updated_at | TIMESTAMP | Last update timestamp |
|
|
287
370
|
|
|
288
371
|
### Click Events Table
|
|
289
372
|
|
|
@@ -294,7 +377,7 @@ CORS_ORIGIN=*
|
|
|
294
377
|
| clicked_at | TIMESTAMP | Click timestamp |
|
|
295
378
|
| ip_address | INET | User IP address |
|
|
296
379
|
| user_agent | TEXT | User agent string |
|
|
297
|
-
| device_type | VARCHAR(20) | Device type (
|
|
380
|
+
| device_type | VARCHAR(20) | Device type (ios/android/web)|
|
|
298
381
|
| platform | VARCHAR(20) | Platform (iOS/Android/Web) |
|
|
299
382
|
| country_code | CHAR(2) | Country code |
|
|
300
383
|
| country_name | VARCHAR(100) | Country name |
|
|
@@ -308,6 +391,68 @@ CORS_ORIGIN=*
|
|
|
308
391
|
| utm_campaign | VARCHAR(255) | UTM campaign |
|
|
309
392
|
| referrer | TEXT | Referrer URL |
|
|
310
393
|
|
|
394
|
+
### Device Fingerprints Table
|
|
395
|
+
|
|
396
|
+
| Column | Type | Description |
|
|
397
|
+
|------------------|-------------|-------------------------------------|
|
|
398
|
+
| id | UUID | Primary key |
|
|
399
|
+
| click_id | UUID | Foreign key to click_events |
|
|
400
|
+
| fingerprint_hash | VARCHAR(64) | SHA-256 hash of fingerprint signals |
|
|
401
|
+
| ip_address | INET | IP address |
|
|
402
|
+
| user_agent | TEXT | User agent string |
|
|
403
|
+
| timezone | VARCHAR(100)| Timezone |
|
|
404
|
+
| language | VARCHAR(10) | Browser language |
|
|
405
|
+
| screen_width | INTEGER | Screen width |
|
|
406
|
+
| screen_height | INTEGER | Screen height |
|
|
407
|
+
| platform | VARCHAR(50) | Platform |
|
|
408
|
+
| platform_version | VARCHAR(50) | Platform version |
|
|
409
|
+
| created_at | TIMESTAMP | Creation timestamp |
|
|
410
|
+
|
|
411
|
+
### Install Events Table
|
|
412
|
+
|
|
413
|
+
| Column | Type | Description |
|
|
414
|
+
|-------------------------|-------------|------------------------------------|
|
|
415
|
+
| id | UUID | Primary key |
|
|
416
|
+
| link_id | UUID | Attributed link (nullable) |
|
|
417
|
+
| click_id | UUID | Attributed click (nullable) |
|
|
418
|
+
| fingerprint_hash | VARCHAR(64) | Device fingerprint hash |
|
|
419
|
+
| confidence_score | DECIMAL | Match confidence (0-100) |
|
|
420
|
+
| installed_at | TIMESTAMP | Install timestamp |
|
|
421
|
+
| first_open_at | TIMESTAMP | First app open |
|
|
422
|
+
| deep_link_retrieved | BOOLEAN | Whether deferred link was fetched |
|
|
423
|
+
| deep_link_data | JSONB | Deferred deep link data |
|
|
424
|
+
| attribution_window_hours| INTEGER | Attribution window used (default: 168) |
|
|
425
|
+
| device_id | VARCHAR(255)| Optional device identifier |
|
|
426
|
+
| created_at | TIMESTAMP | Creation timestamp |
|
|
427
|
+
|
|
428
|
+
### In-App Events Table
|
|
429
|
+
|
|
430
|
+
| Column | Type | Description |
|
|
431
|
+
|-----------------|--------------|------------------------------|
|
|
432
|
+
| id | UUID | Primary key |
|
|
433
|
+
| install_id | UUID | Foreign key to install_events|
|
|
434
|
+
| event_name | VARCHAR(255) | Event name |
|
|
435
|
+
| event_data | JSONB | Custom event properties |
|
|
436
|
+
| event_timestamp | TIMESTAMP | When the event occurred |
|
|
437
|
+
| created_at | TIMESTAMP | Creation timestamp |
|
|
438
|
+
|
|
439
|
+
### Webhooks Table
|
|
440
|
+
|
|
441
|
+
| Column | Type | Description |
|
|
442
|
+
|------------|--------------|----------------------------------|
|
|
443
|
+
| id | UUID | Primary key |
|
|
444
|
+
| user_id | UUID | Optional owner/tenant identifier |
|
|
445
|
+
| name | VARCHAR(255) | Webhook name |
|
|
446
|
+
| url | TEXT | Delivery URL |
|
|
447
|
+
| secret | VARCHAR(255) | HMAC signing secret |
|
|
448
|
+
| events | TEXT[] | Subscribed event types |
|
|
449
|
+
| is_active | BOOLEAN | Active status |
|
|
450
|
+
| retry_count| INTEGER | Max retries (default: 3) |
|
|
451
|
+
| timeout_ms | INTEGER | Request timeout (default: 10000) |
|
|
452
|
+
| headers | JSONB | Custom HTTP headers |
|
|
453
|
+
| created_at | TIMESTAMP | Creation timestamp |
|
|
454
|
+
| updated_at | TIMESTAMP | Last update timestamp |
|
|
455
|
+
|
|
311
456
|
## Utilities
|
|
312
457
|
|
|
313
458
|
### Generate Short Code
|
|
@@ -389,7 +534,7 @@ await fastify.listen({ port: 3000 });
|
|
|
389
534
|
|
|
390
535
|
LinkForty can be deployed in multiple ways depending on your needs:
|
|
391
536
|
|
|
392
|
-
###
|
|
537
|
+
### Production Deployment (Recommended)
|
|
393
538
|
|
|
394
539
|
Deploy to managed platforms with minimal DevOps overhead:
|
|
395
540
|
|
|
@@ -399,11 +544,11 @@ Deploy to managed platforms with minimal DevOps overhead:
|
|
|
399
544
|
- Auto-scaling and SSL included
|
|
400
545
|
- Starting at ~$10-15/month
|
|
401
546
|
|
|
402
|
-
[View Fly.io deployment guide
|
|
547
|
+
[View Fly.io deployment guide](infra/fly.io/DEPLOYMENT.md)
|
|
403
548
|
|
|
404
549
|
See [`infra/`](infra/) directory for all deployment options and platform-specific guides.
|
|
405
550
|
|
|
406
|
-
###
|
|
551
|
+
### Docker Deployment (Recommended for Self-Hosting)
|
|
407
552
|
|
|
408
553
|
**Production-ready Docker images available on Docker Hub:**
|
|
409
554
|
|
|
@@ -424,7 +569,7 @@ docker compose up -d
|
|
|
424
569
|
```yaml
|
|
425
570
|
services:
|
|
426
571
|
linkforty:
|
|
427
|
-
image: linkforty/core:v1.
|
|
572
|
+
image: linkforty/core:v1.5.0 # Pin to specific version
|
|
428
573
|
```
|
|
429
574
|
|
|
430
575
|
See [DOCKER.md](DOCKER.md) for complete deployment guide including:
|
|
@@ -456,8 +601,8 @@ See [`infra/CONTRIBUTING.md`](infra/CONTRIBUTING.md) to add support for addition
|
|
|
456
601
|
|
|
457
602
|
- **Redis caching**: 5-minute TTL on link lookups reduces database queries by 90%
|
|
458
603
|
- **Database indexes**: Optimized queries for fast link lookups and analytics
|
|
459
|
-
- **Async click tracking**: Non-blocking click event logging
|
|
460
|
-
- **Connection pooling**: Efficient database connection management
|
|
604
|
+
- **Async click tracking**: Non-blocking click event logging via `setImmediate()`
|
|
605
|
+
- **Connection pooling**: Efficient database connection management (min 2, max 10)
|
|
461
606
|
|
|
462
607
|
## Security
|
|
463
608
|
|
|
@@ -465,6 +610,8 @@ See [`infra/CONTRIBUTING.md`](infra/CONTRIBUTING.md) to add support for addition
|
|
|
465
610
|
- **Input validation**: Zod schema validation on all inputs
|
|
466
611
|
- **CORS configuration**: Configurable CORS for API access control
|
|
467
612
|
- **Link expiration**: Automatic handling of expired links
|
|
613
|
+
- **Webhook signing**: HMAC SHA-256 signed payloads
|
|
614
|
+
- **No auth included**: Core does not include authentication. The optional `userId` parameter provides data scoping but does not verify identity. Add your own auth middleware as needed.
|
|
468
615
|
|
|
469
616
|
## Mobile SDK Integration
|
|
470
617
|
|
|
@@ -556,12 +703,14 @@ LinkForty Core supports iOS Universal Links and Android App Links for seamless d
|
|
|
556
703
|
|
|
557
704
|
### Available Mobile SDKs
|
|
558
705
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
-
|
|
562
|
-
-
|
|
706
|
+
| Platform | Package | Install |
|
|
707
|
+
|----------|---------|---------|
|
|
708
|
+
| React Native | [`@linkforty/mobile-sdk-react-native`](https://github.com/LinkForty/mobile-sdk-react-native) | `npm install @linkforty/mobile-sdk-react-native` |
|
|
709
|
+
| Expo | [`@linkforty/mobile-sdk-expo`](https://github.com/LinkForty/mobile-sdk-expo) | `npx expo install @linkforty/mobile-sdk-expo` |
|
|
710
|
+
| iOS (Swift) | [LinkFortySDK](https://github.com/LinkForty/mobile-sdk-ios) | Swift Package Manager |
|
|
711
|
+
| Android (Kotlin) | [LinkFortySDK](https://github.com/LinkForty/mobile-sdk-android) | Gradle dependency |
|
|
563
712
|
|
|
564
|
-
See [SDK
|
|
713
|
+
See the [SDK documentation](https://docs.linkforty.com/sdks/react-native) for integration guides.
|
|
565
714
|
|
|
566
715
|
### Testing Domain Verification
|
|
567
716
|
|
|
@@ -577,18 +726,47 @@ adb shell am start -a android.intent.action.VIEW \
|
|
|
577
726
|
```
|
|
578
727
|
|
|
579
728
|
|
|
729
|
+
## Migrate from Another Platform
|
|
730
|
+
|
|
731
|
+
Switching from an existing deep linking provider? LinkForty supports zero-downtime migration via custom domain DNS cutover.
|
|
732
|
+
|
|
733
|
+
- [Migrate from Branch.io](https://docs.linkforty.com/migrations/branch)
|
|
734
|
+
- [Migrate from AppsFlyer OneLink](https://docs.linkforty.com/migrations/appsflyer)
|
|
735
|
+
- [Migrate from Firebase Dynamic Links](https://docs.linkforty.com/comparisons/firebase-dynamic-links-migration) (shut down August 2025)
|
|
736
|
+
- [Migrate from Adjust](https://docs.linkforty.com/migrations/adjust)
|
|
737
|
+
- [Migrate from Kochava](https://docs.linkforty.com/migrations/kochava)
|
|
738
|
+
- [Migration overview and checklist](https://docs.linkforty.com/migrations/overview)
|
|
739
|
+
|
|
740
|
+
## For AI Tools (llms.txt)
|
|
741
|
+
|
|
742
|
+
LinkForty provides machine-readable documentation for AI coding assistants (Claude, ChatGPT, Cursor, Copilot).
|
|
743
|
+
|
|
744
|
+
- **Quick reference**: [docs.linkforty.com/llms.txt](https://docs.linkforty.com/llms.txt)
|
|
745
|
+
- **Complete integration guide**: [docs.linkforty.com/llms-full.txt](https://docs.linkforty.com/llms-full.txt)
|
|
746
|
+
|
|
747
|
+
Download into your project for AI-assisted integration:
|
|
748
|
+
|
|
749
|
+
```bash
|
|
750
|
+
curl -o LINKFORTY.md https://docs.linkforty.com/llms-full.txt
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
The npm package also ships with an `llms.txt` file — AI tools that read from `node_modules` can discover it automatically.
|
|
754
|
+
|
|
580
755
|
## Contributing
|
|
581
756
|
|
|
582
757
|
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
583
758
|
|
|
584
759
|
## License
|
|
585
760
|
|
|
586
|
-
|
|
761
|
+
AGPL-3.0 - see [LICENSE](LICENSE) file for details.
|
|
587
762
|
|
|
588
763
|
## Related Projects
|
|
589
764
|
|
|
590
|
-
-
|
|
591
|
-
- **[
|
|
765
|
+
- **[@linkforty/mobile-sdk-react-native](https://github.com/LinkForty/mobile-sdk-react-native)** - React Native SDK
|
|
766
|
+
- **[@linkforty/mobile-sdk-expo](https://github.com/LinkForty/mobile-sdk-expo)** - Expo SDK
|
|
767
|
+
- **[mobile-sdk-ios](https://github.com/LinkForty/mobile-sdk-ios)** - iOS SDK (Swift)
|
|
768
|
+
- **[mobile-sdk-android](https://github.com/LinkForty/mobile-sdk-android)** - Android SDK (Kotlin)
|
|
769
|
+
- **[LinkForty Cloud](https://linkforty.com)** - Hosted SaaS version with authentication, teams, billing, and dashboard
|
|
592
770
|
|
|
593
771
|
## Support
|
|
594
772
|
|
|
@@ -600,8 +778,8 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
600
778
|
- [Fastify](https://www.fastify.io/) - Fast web framework
|
|
601
779
|
- [PostgreSQL](https://www.postgresql.org/) - Powerful database
|
|
602
780
|
- [Redis](https://redis.io/) - In-memory cache
|
|
781
|
+
- [Zod](https://zod.dev/) - TypeScript-first schema validation
|
|
603
782
|
- [nanoid](https://github.com/ai/nanoid) - Unique ID generation
|
|
604
783
|
- [geoip-lite](https://github.com/geoip-lite/node-geoip) - IP geolocation
|
|
605
784
|
- [ua-parser-js](https://github.com/faisalman/ua-parser-js) - User agent parsing
|
|
606
|
-
|
|
607
|
-
|
|
785
|
+
- [qrcode](https://github.com/soldair/node-qrcode) - QR code generation
|
package/dist/index.d.ts
CHANGED
|
@@ -17,5 +17,5 @@ export * from './lib/fingerprint.js';
|
|
|
17
17
|
export * from './lib/webhook.js';
|
|
18
18
|
export * from './lib/event-emitter.js';
|
|
19
19
|
export * from './types/index.js';
|
|
20
|
-
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes } from './routes/index.js';
|
|
20
|
+
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes } from './routes/index.js';
|
|
21
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAAsB,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAAsB,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAUxE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,aAAkB,kTA+B7D;AAGD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { linkRoutes } from './routes/links.js';
|
|
|
7
7
|
import { analyticsRoutes } from './routes/analytics.js';
|
|
8
8
|
import { sdkRoutes } from './routes/sdk.js';
|
|
9
9
|
import { webhookRoutes } from './routes/webhooks.js';
|
|
10
|
+
import { templateRoutes } from './routes/templates.js';
|
|
10
11
|
import { qrRoutes } from './routes/qr.js';
|
|
11
12
|
import { wellKnownRoutes } from './routes/well-known.js';
|
|
12
13
|
export async function createServer(options = {}) {
|
|
@@ -32,6 +33,7 @@ export async function createServer(options = {}) {
|
|
|
32
33
|
await fastify.register(analyticsRoutes);
|
|
33
34
|
await fastify.register(sdkRoutes);
|
|
34
35
|
await fastify.register(webhookRoutes);
|
|
36
|
+
await fastify.register(templateRoutes);
|
|
35
37
|
await fastify.register(qrRoutes);
|
|
36
38
|
return fastify;
|
|
37
39
|
}
|
|
@@ -42,5 +44,5 @@ export * from './lib/fingerprint.js';
|
|
|
42
44
|
export * from './lib/webhook.js';
|
|
43
45
|
export * from './lib/event-emitter.js';
|
|
44
46
|
export * from './types/index.js';
|
|
45
|
-
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes } from './routes/index.js';
|
|
47
|
+
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes } from './routes/index.js';
|
|
46
48
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAA4B,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAmB,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAazD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAyB,EAAE;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;KAC7D,CAAC,CAAC;IAEH,OAAO;IACP,MAAM,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,IAAI,GAAG;KACpC,CAAC,CAAC;IAEH,mBAAmB;IACnB,IAAI,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE;YAC5B,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG;SACvB,CAAC,CAAC;IACL,CAAC;IAED,WAAW;IACX,MAAM,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE3C,SAAS;IACT,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,MAAM,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;IACtC,MAAM,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,gCAAgC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAA4B,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAmB,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAazD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAyB,EAAE;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;KAC7D,CAAC,CAAC;IAEH,OAAO;IACP,MAAM,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,IAAI,GAAG;KACpC,CAAC,CAAC;IAEH,mBAAmB;IACnB,IAAI,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE;YAC5B,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG;SACvB,CAAC,CAAC;IACL,CAAC;IAED,WAAW;IACX,MAAM,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE3C,SAAS;IACT,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,MAAM,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;IACtC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,MAAM,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,gCAAgC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|