@linkforty/core 1.5.1 → 1.13.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.
Files changed (54) hide show
  1. package/README.md +75 -11
  2. package/dist/index.d.ts +4 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +5 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/client-ip.d.ts +10 -0
  7. package/dist/lib/client-ip.d.ts.map +1 -0
  8. package/dist/lib/client-ip.js +18 -0
  9. package/dist/lib/client-ip.js.map +1 -0
  10. package/dist/lib/client-ip.test.d.ts +7 -0
  11. package/dist/lib/client-ip.test.d.ts.map +1 -0
  12. package/dist/lib/client-ip.test.js +76 -0
  13. package/dist/lib/client-ip.test.js.map +1 -0
  14. package/dist/lib/database.d.ts.map +1 -1
  15. package/dist/lib/database.js +30 -0
  16. package/dist/lib/database.js.map +1 -1
  17. package/dist/lib/event-emitter.test.d.ts +2 -0
  18. package/dist/lib/event-emitter.test.d.ts.map +1 -0
  19. package/dist/lib/event-emitter.test.js +162 -0
  20. package/dist/lib/event-emitter.test.js.map +1 -0
  21. package/dist/lib/fingerprint.js +4 -4
  22. package/dist/lib/fingerprint.js.map +1 -1
  23. package/dist/lib/fingerprint.test.d.ts +2 -0
  24. package/dist/lib/fingerprint.test.d.ts.map +1 -0
  25. package/dist/lib/fingerprint.test.js +227 -0
  26. package/dist/lib/fingerprint.test.js.map +1 -0
  27. package/dist/lib/webhook.js +1 -1
  28. package/dist/lib/webhook.test.d.ts +2 -0
  29. package/dist/lib/webhook.test.d.ts.map +1 -0
  30. package/dist/lib/webhook.test.js +289 -0
  31. package/dist/lib/webhook.test.js.map +1 -0
  32. package/dist/routes/debug.js +6 -6
  33. package/dist/routes/debug.js.map +1 -1
  34. package/dist/routes/index.d.ts +1 -0
  35. package/dist/routes/index.d.ts.map +1 -1
  36. package/dist/routes/index.js +1 -0
  37. package/dist/routes/index.js.map +1 -1
  38. package/dist/routes/links.d.ts.map +1 -1
  39. package/dist/routes/links.js +19 -10
  40. package/dist/routes/links.js.map +1 -1
  41. package/dist/routes/redirect.d.ts.map +1 -1
  42. package/dist/routes/redirect.js +3 -2
  43. package/dist/routes/redirect.js.map +1 -1
  44. package/dist/routes/sdk.d.ts.map +1 -1
  45. package/dist/routes/sdk.js +20 -11
  46. package/dist/routes/sdk.js.map +1 -1
  47. package/dist/routes/templates.d.ts +3 -0
  48. package/dist/routes/templates.d.ts.map +1 -0
  49. package/dist/routes/templates.js +261 -0
  50. package/dist/routes/templates.js.map +1 -0
  51. package/dist/types/index.d.ts +32 -1
  52. package/dist/types/index.d.ts.map +1 -1
  53. package/llms.txt +763 -0
  54. package/package.json +19 -6
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
 
4
4
  # LinkForty Core
5
5
 
6
- **Open-source deeplink framework with device detection, analytics, and smart routing**
6
+ **Open-source alternative to Branch.io, AppsFlyer OneLink, and Firebase Dynamic Links**
7
7
 
8
- LinkForty Core is a TypeScript deeplink framework built on Fastify + PostgreSQL. It provides the engine for creating, managing, and tracking smart links with device-specific routing, targeting rules, QR codes, deferred deep linking, and webhook integrations. Auth, UI, and user management are not included bring your own, or use separate packages like `@linkforty/ui`.
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
  [![npm version](https://img.shields.io/npm/v/@linkforty/core.svg)](https://www.npmjs.com/package/@linkforty/core)
@@ -15,6 +15,33 @@
15
15
  [![Docker Image Size](https://img.shields.io/docker/image-size/linkforty/core/latest)](https://hub.docker.com/r/linkforty/core)
16
16
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
17
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
+
18
45
  ## Features
19
46
 
20
47
  **Smart Link Routing** - Create short links with device-specific URLs for iOS, Android, and web \
@@ -230,7 +257,7 @@ DELETE /api/webhooks/:id?userId=user-uuid
230
257
  POST /api/webhooks/:id/test?userId=user-uuid
231
258
  ```
232
259
 
233
- Events: `click_event`, `install_event`, `conversion_event`. Payloads are HMAC SHA-256 signed.
260
+ Events: `click_event`, `install_event`, `conversion_event`, `sdk_event`. Payloads are HMAC SHA-256 signed.
234
261
 
235
262
  ### Mobile SDK Endpoints
236
263
 
@@ -285,9 +312,14 @@ interface ServerOptions {
285
312
  origin: string | string[]; // CORS allowed origins (default: '*')
286
313
  };
287
314
  logger?: boolean; // Enable Fastify logger (default: true)
315
+ trustProxy?: boolean | number; // Trust X-Forwarded-For when behind a proxy (default: false)
288
316
  }
289
317
  ```
290
318
 
319
+ ### Running behind a reverse proxy
320
+
321
+ When Core runs behind a reverse proxy, CDN, or load balancer, set `trustProxy` so the server uses the real client IP from `X-Forwarded-For` for redirect targeting, geo, attribution, and fingerprinting. Pass it when creating the server (e.g. `trustProxy: true` or a number of proxy hops) or set the `TRUST_PROXY` environment variable (e.g. `TRUST_PROXY=1`). Client-provided `ipAddress` in the SDK install request body is **not** used as the trusted IP; it is optional debug metadata only and must not be relied on for attribution.
322
+
291
323
  ### Environment Variables
292
324
 
293
325
  ```bash
@@ -296,6 +328,8 @@ REDIS_URL=redis://localhost:6379
296
328
  PORT=3000
297
329
  NODE_ENV=production
298
330
  CORS_ORIGIN=*
331
+ # When behind a reverse proxy: TRUST_PROXY=1 (or number of hops) so client IP is read from X-Forwarded-For
332
+ # TRUST_PROXY=1
299
333
 
300
334
  # Mobile SDK (optional — for iOS Universal Links and Android App Links)
301
335
  IOS_TEAM_ID=ABC123XYZ
@@ -676,12 +710,14 @@ LinkForty Core supports iOS Universal Links and Android App Links for seamless d
676
710
 
677
711
  ### Available Mobile SDKs
678
712
 
679
- - **React Native**: `npm install @linkforty/mobile-sdk-react-native`
680
- - **iOS Native**: Coming soon
681
- - **Android Native**: Coming soon
682
- - **Flutter**: Coming soon
713
+ | Platform | Package | Install |
714
+ |----------|---------|---------|
715
+ | React Native | [`@linkforty/mobile-sdk-react-native`](https://github.com/LinkForty/mobile-sdk-react-native) | `npm install @linkforty/mobile-sdk-react-native` |
716
+ | Expo | [`@linkforty/mobile-sdk-expo`](https://github.com/LinkForty/mobile-sdk-expo) | `npx expo install @linkforty/mobile-sdk-expo` |
717
+ | iOS (Swift) | [LinkFortySDK](https://github.com/LinkForty/mobile-sdk-ios) | Swift Package Manager |
718
+ | Android (Kotlin) | [LinkFortySDK](https://github.com/LinkForty/mobile-sdk-android) | Gradle dependency |
683
719
 
684
- See [SDK Integration Guide](https://docs.linkforty.com/sdks/react-native/overview) for detailed documentation.
720
+ See the [SDK documentation](https://docs.linkforty.com/sdks/react-native) for integration guides.
685
721
 
686
722
  ### Testing Domain Verification
687
723
 
@@ -697,6 +733,32 @@ adb shell am start -a android.intent.action.VIEW \
697
733
  ```
698
734
 
699
735
 
736
+ ## Migrate from Another Platform
737
+
738
+ Switching from an existing deep linking provider? LinkForty supports zero-downtime migration via custom domain DNS cutover.
739
+
740
+ - [Migrate from Branch.io](https://docs.linkforty.com/migrations/branch)
741
+ - [Migrate from AppsFlyer OneLink](https://docs.linkforty.com/migrations/appsflyer)
742
+ - [Migrate from Firebase Dynamic Links](https://docs.linkforty.com/comparisons/firebase-dynamic-links-migration) (shut down August 2025)
743
+ - [Migrate from Adjust](https://docs.linkforty.com/migrations/adjust)
744
+ - [Migrate from Kochava](https://docs.linkforty.com/migrations/kochava)
745
+ - [Migration overview and checklist](https://docs.linkforty.com/migrations/overview)
746
+
747
+ ## For AI Tools (llms.txt)
748
+
749
+ LinkForty provides machine-readable documentation for AI coding assistants (Claude, ChatGPT, Cursor, Copilot).
750
+
751
+ - **Quick reference**: [docs.linkforty.com/llms.txt](https://docs.linkforty.com/llms.txt)
752
+ - **Complete integration guide**: [docs.linkforty.com/llms-full.txt](https://docs.linkforty.com/llms-full.txt)
753
+
754
+ Download into your project for AI-assisted integration:
755
+
756
+ ```bash
757
+ curl -o LINKFORTY.md https://docs.linkforty.com/llms-full.txt
758
+ ```
759
+
760
+ The npm package also ships with an `llms.txt` file — AI tools that read from `node_modules` can discover it automatically.
761
+
700
762
  ## Contributing
701
763
 
702
764
  Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
@@ -707,9 +769,11 @@ AGPL-3.0 - see [LICENSE](LICENSE) file for details.
707
769
 
708
770
  ## Related Projects
709
771
 
710
- - **@linkforty/ui** - React UI components for link management
711
- - **@linkforty/mobile-sdk-react-native** - React Native SDK for deferred deep linking
712
- - **[LinkForty Cloud](https://linkforty.com)** - Hosted SaaS version with authentication, teams, billing, and more
772
+ - **[@linkforty/mobile-sdk-react-native](https://github.com/LinkForty/mobile-sdk-react-native)** - React Native SDK
773
+ - **[@linkforty/mobile-sdk-expo](https://github.com/LinkForty/mobile-sdk-expo)** - Expo SDK
774
+ - **[mobile-sdk-ios](https://github.com/LinkForty/mobile-sdk-ios)** - iOS SDK (Swift)
775
+ - **[mobile-sdk-android](https://github.com/LinkForty/mobile-sdk-android)** - Android SDK (Kotlin)
776
+ - **[LinkForty Cloud](https://linkforty.com)** - Hosted SaaS version with authentication, teams, billing, and dashboard
713
777
 
714
778
  ## Support
715
779
 
package/dist/index.d.ts CHANGED
@@ -9,13 +9,16 @@ export interface ServerOptions {
9
9
  origin: string | string[];
10
10
  };
11
11
  logger?: boolean;
12
+ /** When true or a number (proxy hop count), Fastify trusts X-Forwarded-For so request.ip is the real client IP. Set when behind a reverse proxy. */
13
+ trustProxy?: boolean | number;
12
14
  }
13
15
  export declare function createServer(options?: ServerOptions): Promise<FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
14
16
  export * from './lib/utils.js';
17
+ export * from './lib/client-ip.js';
15
18
  export * from './lib/database.js';
16
19
  export * from './lib/fingerprint.js';
17
20
  export * from './lib/webhook.js';
18
21
  export * from './lib/event-emitter.js';
19
22
  export * from './types/index.js';
20
- export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes } from './routes/index.js';
23
+ export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes } from './routes/index.js';
21
24
  //# sourceMappingURL=index.d.ts.map
@@ -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;AASxE,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,kTA8B7D;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,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,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;IACjB,oJAAoJ;IACpJ,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC/B;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,aAAkB,kTAgC7D;AAGD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,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,11 +7,13 @@ 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 = {}) {
13
14
  const fastify = Fastify({
14
15
  logger: options.logger !== undefined ? options.logger : true,
16
+ trustProxy: options.trustProxy,
15
17
  });
16
18
  // CORS
17
19
  await fastify.register(cors, {
@@ -32,15 +34,17 @@ export async function createServer(options = {}) {
32
34
  await fastify.register(analyticsRoutes);
33
35
  await fastify.register(sdkRoutes);
34
36
  await fastify.register(webhookRoutes);
37
+ await fastify.register(templateRoutes);
35
38
  await fastify.register(qrRoutes);
36
39
  return fastify;
37
40
  }
38
41
  // Re-export utilities and types
39
42
  export * from './lib/utils.js';
43
+ export * from './lib/client-ip.js';
40
44
  export * from './lib/database.js';
41
45
  export * from './lib/fingerprint.js';
42
46
  export * from './lib/webhook.js';
43
47
  export * from './lib/event-emitter.js';
44
48
  export * from './types/index.js';
45
- export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes } from './routes/index.js';
49
+ export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes } from './routes/index.js';
46
50
  //# 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;AAezD,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;QAC5D,UAAU,EAAE,OAAO,CAAC,UAAU;KAC/B,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,oBAAoB,CAAC;AACnC,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"}
@@ -0,0 +1,10 @@
1
+ import type { FastifyRequest } from 'fastify';
2
+ /**
3
+ * Returns the trusted client IP for the request.
4
+ * Use this everywhere client IP is needed (targeting, attribution, fingerprinting).
5
+ * When the server is behind a reverse proxy, set Fastify's trustProxy option
6
+ * so that request.ip is populated from X-Forwarded-For; this helper then
7
+ * returns that trusted value.
8
+ */
9
+ export declare function getClientIp(request: FastifyRequest): string;
10
+ //# sourceMappingURL=client-ip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip.d.ts","sourceRoot":"","sources":["../../src/lib/client-ip.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAQ3D"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Returns the trusted client IP for the request.
3
+ * Use this everywhere client IP is needed (targeting, attribution, fingerprinting).
4
+ * When the server is behind a reverse proxy, set Fastify's trustProxy option
5
+ * so that request.ip is populated from X-Forwarded-For; this helper then
6
+ * returns that trusted value.
7
+ */
8
+ export function getClientIp(request) {
9
+ const ip = request.ip ?? request.socket?.remoteAddress;
10
+ if (ip && typeof ip === 'string') {
11
+ // IPv6-mapped IPv4: ::ffff:192.168.1.1 -> 192.168.1.1
12
+ if (ip.startsWith('::ffff:'))
13
+ return ip.slice(7);
14
+ return ip;
15
+ }
16
+ return 'unknown';
17
+ }
18
+ //# sourceMappingURL=client-ip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip.js","sourceRoot":"","sources":["../../src/lib/client-ip.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,OAAuB;IACjD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAK,OAAe,CAAC,MAAM,EAAE,aAAa,CAAC;IAChE,IAAI,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;QACjC,sDAAsD;QACtD,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACjD,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,7 @@
1
+ declare global {
2
+ var __capturedInstallFingerprint: {
3
+ ipAddress: string;
4
+ } | null;
5
+ }
6
+ export {};
7
+ //# sourceMappingURL=client-ip.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip.test.d.ts","sourceRoot":"","sources":["../../src/lib/client-ip.test.ts"],"names":[],"mappings":"AAIA,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,4BAA4B,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAChE"}
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { getClientIp } from './client-ip';
3
+ describe('getClientIp', () => {
4
+ it('returns request.ip when set', () => {
5
+ const request = { ip: '192.168.1.1' };
6
+ expect(getClientIp(request)).toBe('192.168.1.1');
7
+ });
8
+ it('returns socket.remoteAddress when request.ip is undefined', () => {
9
+ const request = {
10
+ ip: undefined,
11
+ socket: { remoteAddress: '10.0.0.2' },
12
+ };
13
+ expect(getClientIp(request)).toBe('10.0.0.2');
14
+ });
15
+ it('unwraps IPv6-mapped IPv4', () => {
16
+ const request = { ip: '::ffff:192.168.1.1' };
17
+ expect(getClientIp(request)).toBe('192.168.1.1');
18
+ });
19
+ it('returns "unknown" when neither ip nor socket.remoteAddress is available', () => {
20
+ const request = { ip: undefined, socket: {} };
21
+ expect(getClientIp(request)).toBe('unknown');
22
+ });
23
+ it('returns "unknown" when request has no socket', () => {
24
+ const request = { ip: undefined };
25
+ expect(getClientIp(request)).toBe('unknown');
26
+ });
27
+ });
28
+ describe('getClientIp with Fastify trustProxy (proxied request)', () => {
29
+ it('uses X-Forwarded-For when trustProxy is true', async () => {
30
+ const Fastify = (await import('fastify')).default;
31
+ const { getClientIp: getIp } = await import('./client-ip.js');
32
+ const app = Fastify({ trustProxy: true });
33
+ app.get('/ip', async (request, reply) => {
34
+ return reply.send({ ip: getIp(request) });
35
+ });
36
+ const res = await app.inject({
37
+ method: 'GET',
38
+ url: '/ip',
39
+ headers: { 'x-forwarded-for': '203.0.113.50' },
40
+ });
41
+ expect(res.statusCode).toBe(200);
42
+ const body = res.json();
43
+ expect(body.ip).toBe('203.0.113.50');
44
+ });
45
+ });
46
+ vi.mock('./fingerprint.js', async (importOriginal) => {
47
+ const mod = (await importOriginal());
48
+ return {
49
+ ...mod,
50
+ recordInstallEvent: vi.fn().mockImplementation(async (data) => {
51
+ globalThis.__capturedInstallFingerprint = data;
52
+ return { installId: 'test-id', match: null, deepLinkData: null };
53
+ }),
54
+ };
55
+ });
56
+ describe('SDK install does not trust client-provided ipAddress', () => {
57
+ it('uses connection/proxy IP for attribution, not body ipAddress', async () => {
58
+ globalThis.__capturedInstallFingerprint = null;
59
+ const Fastify = (await import('fastify')).default;
60
+ const { sdkRoutes } = await import('../routes/sdk.js');
61
+ const app = Fastify({ trustProxy: true });
62
+ await app.register(sdkRoutes);
63
+ const res = await app.inject({
64
+ method: 'POST',
65
+ url: '/api/sdk/v1/install',
66
+ payload: { ipAddress: '1.2.3.4', userAgent: 'Mozilla/5.0 Test' },
67
+ headers: { 'x-forwarded-for': '203.0.113.50', 'content-type': 'application/json' },
68
+ });
69
+ expect(res.statusCode).toBe(200);
70
+ expect(globalThis.__capturedInstallFingerprint).not.toBeNull();
71
+ expect(globalThis.__capturedInstallFingerprint.ipAddress).toBe('203.0.113.50');
72
+ const body = res.json();
73
+ expect(body.clientReportedIp).toBe('1.2.3.4');
74
+ });
75
+ });
76
+ //# sourceMappingURL=client-ip.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip.test.js","sourceRoot":"","sources":["../../src/lib/client-ip.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;AA6BvB,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,iBA2VrE"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;AA6BvB,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,iBA4XrE"}
@@ -38,6 +38,20 @@ export async function initializeDatabase(options = {}) {
38
38
  });
39
39
  const client = await connectWithRetry();
40
40
  try {
41
+ // Link templates table (must be created before links, which references it)
42
+ await client.query(`
43
+ CREATE TABLE IF NOT EXISTS link_templates (
44
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
45
+ user_id UUID,
46
+ name VARCHAR(255) NOT NULL,
47
+ slug VARCHAR(100) UNIQUE NOT NULL,
48
+ description TEXT,
49
+ settings JSONB DEFAULT '{}',
50
+ is_default BOOLEAN DEFAULT false,
51
+ created_at TIMESTAMP DEFAULT NOW(),
52
+ updated_at TIMESTAMP DEFAULT NOW()
53
+ )
54
+ `);
41
55
  // Links table
42
56
  await client.query(`
43
57
  CREATE TABLE IF NOT EXISTS links (
@@ -150,6 +164,18 @@ export async function initializeDatabase(options = {}) {
150
164
  created_at TIMESTAMP DEFAULT NOW(),
151
165
  updated_at TIMESTAMP DEFAULT NOW()
152
166
  )
167
+ `);
168
+ // Add template_id column to links table
169
+ await client.query(`
170
+ DO $$
171
+ BEGIN
172
+ IF NOT EXISTS (
173
+ SELECT 1 FROM information_schema.columns
174
+ WHERE table_name='links' AND column_name='template_id'
175
+ ) THEN
176
+ ALTER TABLE links ADD COLUMN template_id UUID REFERENCES link_templates(id) ON DELETE SET NULL;
177
+ END IF;
178
+ END $$;
153
179
  `);
154
180
  // Add description column to existing links table if it doesn't exist
155
181
  await client.query(`
@@ -324,6 +350,10 @@ export async function initializeDatabase(options = {}) {
324
350
  await client.query('CREATE INDEX IF NOT EXISTS idx_installs_link_id ON install_events(link_id)');
325
351
  await client.query('CREATE INDEX IF NOT EXISTS idx_installs_timestamp ON install_events(installed_at DESC)');
326
352
  await client.query('CREATE INDEX IF NOT EXISTS idx_installs_link_date ON install_events(link_id, installed_at DESC)');
353
+ // Indexes for link templates
354
+ await client.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_link_templates_slug ON link_templates(slug)');
355
+ await client.query('CREATE INDEX IF NOT EXISTS idx_link_templates_user_id ON link_templates(user_id)');
356
+ await client.query('CREATE INDEX IF NOT EXISTS idx_links_template_id ON links(template_id)');
327
357
  // Indexes for webhooks
328
358
  await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_user_id ON webhooks(user_id)');
329
359
  await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(is_active) WHERE is_active = true');
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=event-emitter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-emitter.test.d.ts","sourceRoot":"","sources":["../../src/lib/event-emitter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { clickEventEmitter, emitClickEvent, subscribeToClickEvents, } from './event-emitter';
3
+ const mockClickEvent = {
4
+ eventId: 'evt-123',
5
+ timestamp: '2026-03-10T00:00:00.000Z',
6
+ linkId: 'link-abc',
7
+ shortCode: 'abc123',
8
+ userId: 'user-1',
9
+ organizationId: 'org-1',
10
+ ipAddress: '1.2.3.4',
11
+ userAgent: 'Mozilla/5.0',
12
+ country: 'US',
13
+ city: 'New York',
14
+ deviceType: 'web',
15
+ platform: 'Windows',
16
+ browser: 'Chrome',
17
+ redirectUrl: 'https://example.com',
18
+ redirectReason: 'web_fallback',
19
+ targetingMatched: true,
20
+ utmParameters: {
21
+ source: 'newsletter',
22
+ medium: 'email',
23
+ campaign: 'spring',
24
+ },
25
+ referer: 'https://google.com',
26
+ language: 'en-US',
27
+ };
28
+ describe('clickEventEmitter', () => {
29
+ it('should be an EventEmitter instance', () => {
30
+ expect(clickEventEmitter).toBeDefined();
31
+ expect(typeof clickEventEmitter.on).toBe('function');
32
+ expect(typeof clickEventEmitter.emit).toBe('function');
33
+ expect(typeof clickEventEmitter.off).toBe('function');
34
+ });
35
+ });
36
+ describe('emitClickEvent', () => {
37
+ beforeEach(() => {
38
+ clickEventEmitter.removeAllListeners('click');
39
+ });
40
+ it('should emit a click event with the provided data', () => {
41
+ const handler = vi.fn();
42
+ clickEventEmitter.on('click', handler);
43
+ emitClickEvent(mockClickEvent);
44
+ expect(handler).toHaveBeenCalledOnce();
45
+ expect(handler).toHaveBeenCalledWith(mockClickEvent);
46
+ });
47
+ it('should emit to all registered listeners', () => {
48
+ const handler1 = vi.fn();
49
+ const handler2 = vi.fn();
50
+ clickEventEmitter.on('click', handler1);
51
+ clickEventEmitter.on('click', handler2);
52
+ emitClickEvent(mockClickEvent);
53
+ expect(handler1).toHaveBeenCalledOnce();
54
+ expect(handler2).toHaveBeenCalledOnce();
55
+ });
56
+ it('should emit multiple times when called multiple times', () => {
57
+ const handler = vi.fn();
58
+ clickEventEmitter.on('click', handler);
59
+ emitClickEvent(mockClickEvent);
60
+ emitClickEvent({ ...mockClickEvent, eventId: 'evt-456' });
61
+ expect(handler).toHaveBeenCalledTimes(2);
62
+ });
63
+ it('should pass the exact event data to the handler', () => {
64
+ const handler = vi.fn();
65
+ clickEventEmitter.on('click', handler);
66
+ const eventData = {
67
+ ...mockClickEvent,
68
+ deviceType: 'ios',
69
+ country: 'CA',
70
+ targetingMatched: false,
71
+ };
72
+ emitClickEvent(eventData);
73
+ expect(handler).toHaveBeenCalledWith(eventData);
74
+ });
75
+ it('should work with minimal required fields', () => {
76
+ const handler = vi.fn();
77
+ clickEventEmitter.on('click', handler);
78
+ const minimalEvent = {
79
+ eventId: 'evt-min',
80
+ timestamp: '2026-03-10T00:00:00.000Z',
81
+ linkId: 'link-min',
82
+ shortCode: 'min',
83
+ ipAddress: '0.0.0.0',
84
+ userAgent: '',
85
+ deviceType: 'web',
86
+ redirectUrl: 'https://example.com',
87
+ redirectReason: 'default',
88
+ targetingMatched: true,
89
+ };
90
+ emitClickEvent(minimalEvent);
91
+ expect(handler).toHaveBeenCalledWith(minimalEvent);
92
+ });
93
+ });
94
+ describe('subscribeToClickEvents', () => {
95
+ beforeEach(() => {
96
+ clickEventEmitter.removeAllListeners('click');
97
+ });
98
+ it('should call the callback when a click event is emitted', () => {
99
+ const callback = vi.fn();
100
+ subscribeToClickEvents(callback);
101
+ emitClickEvent(mockClickEvent);
102
+ expect(callback).toHaveBeenCalledOnce();
103
+ expect(callback).toHaveBeenCalledWith(mockClickEvent);
104
+ });
105
+ it('should return an unsubscribe function', () => {
106
+ const callback = vi.fn();
107
+ const unsubscribe = subscribeToClickEvents(callback);
108
+ expect(typeof unsubscribe).toBe('function');
109
+ });
110
+ it('should stop receiving events after unsubscribing', () => {
111
+ const callback = vi.fn();
112
+ const unsubscribe = subscribeToClickEvents(callback);
113
+ emitClickEvent(mockClickEvent);
114
+ expect(callback).toHaveBeenCalledOnce();
115
+ unsubscribe();
116
+ emitClickEvent(mockClickEvent);
117
+ expect(callback).toHaveBeenCalledOnce(); // still only once
118
+ });
119
+ it('should allow multiple subscribers independently', () => {
120
+ const callback1 = vi.fn();
121
+ const callback2 = vi.fn();
122
+ const unsubscribe1 = subscribeToClickEvents(callback1);
123
+ subscribeToClickEvents(callback2);
124
+ emitClickEvent(mockClickEvent);
125
+ expect(callback1).toHaveBeenCalledOnce();
126
+ expect(callback2).toHaveBeenCalledOnce();
127
+ unsubscribe1();
128
+ emitClickEvent(mockClickEvent);
129
+ expect(callback1).toHaveBeenCalledOnce(); // no new calls
130
+ expect(callback2).toHaveBeenCalledTimes(2);
131
+ });
132
+ it('should not affect other subscribers when one unsubscribes', () => {
133
+ const callback1 = vi.fn();
134
+ const callback2 = vi.fn();
135
+ const unsubscribe1 = subscribeToClickEvents(callback1);
136
+ subscribeToClickEvents(callback2);
137
+ unsubscribe1();
138
+ emitClickEvent(mockClickEvent);
139
+ expect(callback1).not.toHaveBeenCalled();
140
+ expect(callback2).toHaveBeenCalledOnce();
141
+ });
142
+ it('should handle unsubscribe called multiple times without error', () => {
143
+ const callback = vi.fn();
144
+ const unsubscribe = subscribeToClickEvents(callback);
145
+ expect(() => {
146
+ unsubscribe();
147
+ unsubscribe();
148
+ }).not.toThrow();
149
+ });
150
+ it('should pass event data correctly to callback', () => {
151
+ const received = [];
152
+ subscribeToClickEvents((data) => received.push(data));
153
+ const event1 = { ...mockClickEvent, eventId: 'e1', deviceType: 'ios' };
154
+ const event2 = { ...mockClickEvent, eventId: 'e2', deviceType: 'android' };
155
+ emitClickEvent(event1);
156
+ emitClickEvent(event2);
157
+ expect(received).toHaveLength(2);
158
+ expect(received[0]).toEqual(event1);
159
+ expect(received[1]).toEqual(event2);
160
+ });
161
+ });
162
+ //# sourceMappingURL=event-emitter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-emitter.test.js","sourceRoot":"","sources":["../../src/lib/event-emitter.test.ts"],"names":[],"mappings":"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"}