@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.
- package/README.md +75 -11
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/client-ip.d.ts +10 -0
- package/dist/lib/client-ip.d.ts.map +1 -0
- package/dist/lib/client-ip.js +18 -0
- package/dist/lib/client-ip.js.map +1 -0
- package/dist/lib/client-ip.test.d.ts +7 -0
- package/dist/lib/client-ip.test.d.ts.map +1 -0
- package/dist/lib/client-ip.test.js +76 -0
- package/dist/lib/client-ip.test.js.map +1 -0
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +30 -0
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/event-emitter.test.d.ts +2 -0
- package/dist/lib/event-emitter.test.d.ts.map +1 -0
- package/dist/lib/event-emitter.test.js +162 -0
- package/dist/lib/event-emitter.test.js.map +1 -0
- package/dist/lib/fingerprint.js +4 -4
- package/dist/lib/fingerprint.js.map +1 -1
- package/dist/lib/fingerprint.test.d.ts +2 -0
- package/dist/lib/fingerprint.test.d.ts.map +1 -0
- package/dist/lib/fingerprint.test.js +227 -0
- package/dist/lib/fingerprint.test.js.map +1 -0
- package/dist/lib/webhook.js +1 -1
- package/dist/lib/webhook.test.d.ts +2 -0
- package/dist/lib/webhook.test.d.ts.map +1 -0
- package/dist/lib/webhook.test.js +289 -0
- package/dist/lib/webhook.test.js.map +1 -0
- package/dist/routes/debug.js +6 -6
- 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 +19 -10
- package/dist/routes/links.js.map +1 -1
- package/dist/routes/redirect.d.ts.map +1 -1
- package/dist/routes/redirect.js +3 -2
- package/dist/routes/redirect.js.map +1 -1
- package/dist/routes/sdk.d.ts.map +1 -1
- package/dist/routes/sdk.js +20 -11
- 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/types/index.d.ts +32 -1
- package/dist/types/index.d.ts.map +1 -1
- package/llms.txt +763 -0
- package/package.json +19 -6
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)
|
|
@@ -15,6 +15,33 @@
|
|
|
15
15
|
[](https://hub.docker.com/r/linkforty/core)
|
|
16
16
|
[](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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
-
|
|
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
|
|
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
|
-
-
|
|
711
|
-
-
|
|
712
|
-
- **[
|
|
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
|
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;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;
|
|
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 @@
|
|
|
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,
|
|
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"}
|
package/dist/lib/database.js
CHANGED
|
@@ -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');
|
package/dist/lib/database.js.map
CHANGED
|
@@ -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 @@
|
|
|
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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"}
|