@linkforty/core 1.30.1 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/database.d.ts +2 -0
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +61 -0
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/fingerprint.d.ts +28 -2
- package/dist/lib/fingerprint.d.ts.map +1 -1
- package/dist/lib/fingerprint.js +109 -43
- package/dist/lib/fingerprint.js.map +1 -1
- package/dist/lib/fingerprint.test.js +77 -0
- package/dist/lib/fingerprint.test.js.map +1 -1
- package/dist/lib/launchpad.d.ts +22 -1
- package/dist/lib/launchpad.d.ts.map +1 -1
- package/dist/lib/launchpad.js +27 -5
- package/dist/lib/launchpad.js.map +1 -1
- package/dist/lib/launchpad.test.js +98 -0
- package/dist/lib/launchpad.test.js.map +1 -1
- package/dist/lib/store-badges.d.ts +30 -0
- package/dist/lib/store-badges.d.ts.map +1 -0
- package/dist/lib/store-badges.js +201 -0
- package/dist/lib/store-badges.js.map +1 -0
- package/dist/lib/webhook.d.ts +22 -1
- package/dist/lib/webhook.d.ts.map +1 -1
- package/dist/lib/webhook.js +32 -2
- package/dist/lib/webhook.js.map +1 -1
- package/dist/lib/webhook.test.js +55 -4
- package/dist/lib/webhook.test.js.map +1 -1
- package/dist/routes/redirect.d.ts +5 -3
- package/dist/routes/redirect.d.ts.map +1 -1
- package/dist/routes/redirect.js +6 -7
- package/dist/routes/redirect.js.map +1 -1
- package/dist/routes/redirect.launchpad.test.js +31 -0
- package/dist/routes/redirect.launchpad.test.js.map +1 -1
- package/dist/routes/sdk.d.ts +15 -2
- package/dist/routes/sdk.d.ts.map +1 -1
- package/dist/routes/sdk.js +19 -14
- package/dist/routes/sdk.js.map +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -241,9 +241,9 @@ Automatically redirects users to the appropriate URL based on device type (iOS/A
|
|
|
241
241
|
|
|
242
242
|
**Mobile interstitial:** When a link has `appScheme` configured and a store fallback URL (iOS App Store or Google Play), mobile requests receive a smart interstitial page instead of a raw 302 redirect. The interstitial tries to open the app via URI scheme and falls back to the app store after 1.5 seconds. This handles the case where a 302 to a custom URI scheme fails silently when the app is not installed. URL fragments are preserved through the redirect, enabling patterns like E2E encryption where the decryption key lives in the fragment.
|
|
243
243
|
|
|
244
|
-
**Launchpad page (desktop):** A link to app-only content has nowhere to send a desktop visitor. When the chain resolves no web destination (link → template → workspace `appConfig`), a desktop request gets a hosted landing page instead of an error: the link's title, description and image (`ogTitle` / `ogDescription` / `ogImageUrl`, falling back to `title` / `description`), the app's icon and name, App Store / Google Play
|
|
244
|
+
**Launchpad page (desktop):** A link to app-only content has nowhere to send a desktop visitor. When the chain resolves no web destination (link → template → workspace `appConfig`), a desktop request gets a hosted landing page instead of an error: the link's title, description and image (`ogTitle` / `ogDescription` / `ogImageUrl`, falling back to `title` / `description`), the app's icon and name, the official App Store / Google Play badges (embedded in the page, so it makes no third-party request), and a QR code so the visitor can finish on their phone. The page is `noindex`, sent with `Cache-Control: no-store` and a content-security policy that allows no script without a per-response nonce, and it never navigates on its own — no timers, no automatic scheme attempts.
|
|
245
245
|
|
|
246
|
-
When the link does have a web destination, the page offers it as a "Continue on the web" link, so a visitor is never trapped. By default it appears **only** where the alternative is nothing; a link that resolves to a destination still gets its 302. Two knobs change that:
|
|
246
|
+
When the link does have a web destination, the page offers it as a "Continue on the web" link, and the title links there too, so a visitor is never trapped. By default it appears **only** where the alternative is nothing; a link that resolves to a destination still gets its 302. Two knobs change that:
|
|
247
247
|
|
|
248
248
|
- `organizations.settings.launchpad` (per workspace):
|
|
249
249
|
|
|
@@ -254,12 +254,15 @@ When the link does have a web destination, the page offers it as a "Continue on
|
|
|
254
254
|
"mobile": "store", // "store" (default) | "page"
|
|
255
255
|
"appName": "Ride Alert",
|
|
256
256
|
"appIconUrl": "https://cdn.example/icon.png",
|
|
257
|
+
"appWebsiteUrl": "https://ridealert.example", // optional; the header's icon and name link here
|
|
257
258
|
"accentColor": "#0f766e", // buttons, the web link, focus rings
|
|
258
259
|
"backgroundColor": "#101418" // optional; fixes the page's palette (see below)
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
```
|
|
262
263
|
|
|
264
|
+
`appWebsiteUrl` is deliberately explicit rather than derived from a link's web fallback, which may be someone else's page (an event listing, a video). Unset, the header is not a link.
|
|
265
|
+
|
|
263
266
|
Without `backgroundColor` the page follows the visitor's light/dark preference. With one, the background is fixed and the text, surfaces and borders are derived from it — a light or a dark brand color both stay legible, and the visitor's theme no longer changes the page.
|
|
264
267
|
|
|
265
268
|
- `links.launchpad_mode` (per link, `launchpadMode` on the links API): `inherit` (default), `on` or `off`. The link's value wins over the workspace's.
|
|
@@ -293,6 +296,8 @@ POST /api/webhooks/:id/test?userId=user-uuid
|
|
|
293
296
|
|
|
294
297
|
Events: `click_event`, `install_event`, `conversion_event`, `sdk_event`. Payloads are HMAC SHA-256 signed.
|
|
295
298
|
|
|
299
|
+
Which webhooks hear about a link: one with an `organization_id` receives events for every link in that organization; one without receives events for links created by its `user_id`; one with neither receives events for links that have neither. To keep a delivery log, pass a function to `setWebhookDeliveryLogger()`. It is called with the webhook id, the attempt's result and the payload sent.
|
|
300
|
+
|
|
296
301
|
### Mobile SDK Endpoints
|
|
297
302
|
|
|
298
303
|
```bash
|
|
@@ -368,6 +373,7 @@ interface ServerOptions {
|
|
|
368
373
|
logger?: boolean; // Enable Fastify logger (default: true)
|
|
369
374
|
trustProxy?: boolean | number; // Trust X-Forwarded-For when behind a proxy (default: false)
|
|
370
375
|
redirect?: RedirectRouteOptions; // See below
|
|
376
|
+
sdk?: SdkRouteOptions; // See below
|
|
371
377
|
}
|
|
372
378
|
|
|
373
379
|
interface RedirectRouteOptions {
|
|
@@ -378,10 +384,14 @@ interface RedirectRouteOptions {
|
|
|
378
384
|
// A hook that throws is logged and treated as null — the page never fails
|
|
379
385
|
// because of it. `heroHtml` is inserted unescaped: render it yourself,
|
|
380
386
|
// never from end-user input.
|
|
381
|
-
|
|
387
|
+
// `context.webUrl` is the link's web destination, decorated exactly as
|
|
388
|
+
// "Continue on the web" uses it (or null), so a hero that draws its own
|
|
389
|
+
// title can link it the way the default page does.
|
|
390
|
+
resolveContent?: (link, settings, context: { webUrl: string | null }) => Promise<LaunchpadContent | null>;
|
|
382
391
|
// Endpoint that receives `{ linkId, event }` beacons from the page —
|
|
383
|
-
// "view" on load
|
|
384
|
-
// cookies or identifiers. Unset means the
|
|
392
|
+
// "view" on load; "cta_ios" / "cta_android" / "cta_open" / "cta_web" /
|
|
393
|
+
// "cta_app_website" on tap. No cookies or identifiers. Unset means the
|
|
394
|
+
// page sends nothing. If you validate event names, accept all of these.
|
|
385
395
|
beaconUrl?: string;
|
|
386
396
|
};
|
|
387
397
|
// Return true when the request is on a host reserved for web links. Every
|
|
@@ -391,8 +401,21 @@ interface RedirectRouteOptions {
|
|
|
391
401
|
// A hook that throws is logged and treated as false.
|
|
392
402
|
isWebLinkRequest?: (request) => Promise<boolean> | boolean;
|
|
393
403
|
}
|
|
404
|
+
|
|
405
|
+
interface SdkRouteOptions {
|
|
406
|
+
// Narrow the clicks an install can be attributed to. A server shared by
|
|
407
|
+
// several apps resolves the installing app (from the SDK's `appToken`, say)
|
|
408
|
+
// and returns its organization, so an install only ever matches — and
|
|
409
|
+
// receives the deep link of — that organization's links. Return null to
|
|
410
|
+
// match against every click, as when the option is absent. A hook that
|
|
411
|
+
// throws is logged and treated as null.
|
|
412
|
+
resolveInstallScope?: (request, body: { appToken?: string }) =>
|
|
413
|
+
Promise<{ organizationId?: string } | null> | { organizationId?: string } | null;
|
|
414
|
+
}
|
|
394
415
|
```
|
|
395
416
|
|
|
417
|
+
Install matching only considers clicks from the installing device's own network (its IPv4 /24 or IPv6 /64), since no other click can reach the confidence threshold, so the attribution window holds however much traffic the server takes. It relies on the `idx_fingerprints_ip` index, which `initializeDatabase()` builds concurrently on first start so click writes are never blocked.
|
|
418
|
+
|
|
396
419
|
`renderLaunchpadPage()`, `defaultLaunchpadContent()` and `shouldServeLaunchpadOnDesktop()` are exported so a host can render the same page elsewhere (a settings preview, for instance).
|
|
397
420
|
|
|
398
421
|
### Running behind a reverse proxy
|
|
@@ -529,6 +552,7 @@ Core does not create a `users` table. Authentication and user management are the
|
|
|
529
552
|
|------------|--------------|----------------------------------|
|
|
530
553
|
| id | UUID | Primary key |
|
|
531
554
|
| user_id | UUID | Optional owner/tenant identifier |
|
|
555
|
+
| organization_id | UUID | Optional owning organization; receives events for all its links |
|
|
532
556
|
| name | VARCHAR(255) | Webhook name |
|
|
533
557
|
| url | TEXT | Delivery URL |
|
|
534
558
|
| secret | VARCHAR(255) | HMAC signing secret |
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FastifyInstance } from 'fastify';
|
|
2
2
|
import { DatabaseOptions } from './lib/database.js';
|
|
3
3
|
import { type RedirectRouteOptions } from './routes/redirect.js';
|
|
4
|
+
import { type SdkRouteOptions } from './routes/sdk.js';
|
|
4
5
|
/**
|
|
5
6
|
* Configuration options for creating a LinkForty server instance.
|
|
6
7
|
*/
|
|
@@ -17,6 +18,8 @@ export interface ServerOptions {
|
|
|
17
18
|
trustProxy?: boolean | number;
|
|
18
19
|
/** Options for the redirect routes: abuse-report link, launchpad page hooks. */
|
|
19
20
|
redirect?: RedirectRouteOptions;
|
|
21
|
+
/** Options for the SDK routes: install match scoping. */
|
|
22
|
+
sdk?: SdkRouteOptions;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Create and configure a LinkForty Fastify server instance.
|
|
@@ -38,5 +41,6 @@ export * from './lib/launchpad.js';
|
|
|
38
41
|
export * from './lib/event-emitter.js';
|
|
39
42
|
export * from './types/index.js';
|
|
40
43
|
export type { RedirectRouteOptions } from './routes/redirect.js';
|
|
44
|
+
export type { SdkRouteOptions } from './routes/sdk.js';
|
|
41
45
|
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes, healthRoutes } from './routes/index.js';
|
|
42
46
|
//# 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;AACxE,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,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;AACxE,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGjF,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAOlE;;GAEG;AACH,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;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,yDAAyD;IACzD,GAAG,CAAC,EAAE,eAAe,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,aAAkB,kTAiC7D;AAGD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ export async function createServer(options = {}) {
|
|
|
43
43
|
await fastify.register(redirectRoutes, options.redirect ?? {});
|
|
44
44
|
await fastify.register(linkRoutes);
|
|
45
45
|
await fastify.register(analyticsRoutes);
|
|
46
|
-
await fastify.register(sdkRoutes);
|
|
46
|
+
await fastify.register(sdkRoutes, options.sdk ?? {});
|
|
47
47
|
await fastify.register(webhookRoutes);
|
|
48
48
|
await fastify.register(templateRoutes);
|
|
49
49
|
await fastify.register(qrRoutes);
|
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,EAA6B,MAAM,sBAAsB,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,
|
|
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,EAA6B,MAAM,sBAAsB,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAwB,MAAM,iBAAiB,CAAC;AAClE,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;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAsBlD;;;;;;;;GAQG;AACH,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,YAAY,CAAC,CAAC;IACrC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IAC/D,MAAM,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IACrD,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,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/lib/database.d.ts
CHANGED
|
@@ -14,5 +14,7 @@ export declare let db: pg.Pool;
|
|
|
14
14
|
* database can avoid colliding with this one. Stable across releases.
|
|
15
15
|
*/
|
|
16
16
|
export declare const SCHEMA_INIT_LOCK_KEY = 4977261;
|
|
17
|
+
/** Advisory lock key held while building idx_fingerprints_ip. Stable across releases. */
|
|
18
|
+
export declare const FINGERPRINT_IP_INDEX_LOCK_KEY = 4977262;
|
|
17
19
|
export declare function initializeDatabase(options?: DatabaseOptions): Promise<void>;
|
|
18
20
|
//# sourceMappingURL=database.d.ts.map
|
|
@@ -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;AA4BvB;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,UAAU,CAAC;
|
|
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;AA4BvB;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAE5C,yFAAyF;AACzF,eAAO,MAAM,6BAA6B,UAAU,CAAC;AAGrD,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,iBAgpBrE"}
|
package/dist/lib/database.js
CHANGED
|
@@ -34,6 +34,8 @@ async function connectWithRetry(maxRetries = 10, baseDelay = 1000) {
|
|
|
34
34
|
* database can avoid colliding with this one. Stable across releases.
|
|
35
35
|
*/
|
|
36
36
|
export const SCHEMA_INIT_LOCK_KEY = 4977261;
|
|
37
|
+
/** Advisory lock key held while building idx_fingerprints_ip. Stable across releases. */
|
|
38
|
+
export const FINGERPRINT_IP_INDEX_LOCK_KEY = 4977262;
|
|
37
39
|
// Initialize database schema
|
|
38
40
|
export async function initializeDatabase(options = {}) {
|
|
39
41
|
// Initialize pool
|
|
@@ -222,6 +224,11 @@ export async function initializeDatabase(options = {}) {
|
|
|
222
224
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
223
225
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
224
226
|
)
|
|
227
|
+
`);
|
|
228
|
+
// A webhook can belong to an organization, in which case it hears about every
|
|
229
|
+
// link the organization owns (findWebhooksForLink). Null scopes it to its user.
|
|
230
|
+
await client.query(`
|
|
231
|
+
ALTER TABLE webhooks ADD COLUMN IF NOT EXISTS organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE
|
|
225
232
|
`);
|
|
226
233
|
// Add organization_id column to links table.
|
|
227
234
|
//
|
|
@@ -607,6 +614,7 @@ export async function initializeDatabase(options = {}) {
|
|
|
607
614
|
// Indexes for webhooks
|
|
608
615
|
await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_user_id ON webhooks(user_id)');
|
|
609
616
|
await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(is_active) WHERE is_active = true');
|
|
617
|
+
await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_organization_id ON webhooks(organization_id)');
|
|
610
618
|
// Indexes for in-app events
|
|
611
619
|
await client.query('CREATE INDEX IF NOT EXISTS idx_in_app_events_install_id ON in_app_events(install_id)');
|
|
612
620
|
await client.query('CREATE INDEX IF NOT EXISTS idx_in_app_events_name ON in_app_events(event_name)');
|
|
@@ -644,5 +652,58 @@ export async function initializeDatabase(options = {}) {
|
|
|
644
652
|
}
|
|
645
653
|
client.release();
|
|
646
654
|
}
|
|
655
|
+
await ensureFingerprintIpIndex();
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Index device_fingerprints by IP, which install matching selects candidate
|
|
659
|
+
* clicks on (matchInstallToClick).
|
|
660
|
+
*
|
|
661
|
+
* Built CONCURRENTLY because the table takes a write on every click and can be
|
|
662
|
+
* large, and a plain CREATE INDEX would hold those writes
|
|
663
|
+
* for the length of the build. That rules out the schema lock above: a
|
|
664
|
+
* concurrent build waits for every older snapshot to finish, including that of
|
|
665
|
+
* another instance blocked on the lock, which would wait on us in turn.
|
|
666
|
+
*
|
|
667
|
+
* A concurrent build that fails leaves an invalid index behind, which IF NOT
|
|
668
|
+
* EXISTS would then skip forever, so an invalid one is dropped and rebuilt. A
|
|
669
|
+
* build in progress also reads as invalid, hence the try-lock: only one instance
|
|
670
|
+
* ever works on the index, and the others move on rather than wait. Failure is
|
|
671
|
+
* logged, not thrown: matching is correct without the index, only slower.
|
|
672
|
+
*/
|
|
673
|
+
async function ensureFingerprintIpIndex() {
|
|
674
|
+
let client;
|
|
675
|
+
let lockHeld = false;
|
|
676
|
+
try {
|
|
677
|
+
client = await db.connect();
|
|
678
|
+
const lock = await client.query('SELECT pg_try_advisory_lock($1) AS acquired', [FINGERPRINT_IP_INDEX_LOCK_KEY]);
|
|
679
|
+
lockHeld = lock.rows[0]?.acquired === true;
|
|
680
|
+
if (!lockHeld) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const existing = await client.query(`SELECT i.indisvalid
|
|
684
|
+
FROM pg_class c
|
|
685
|
+
INNER JOIN pg_index i ON i.indexrelid = c.oid
|
|
686
|
+
WHERE c.relname = 'idx_fingerprints_ip'`);
|
|
687
|
+
if (existing.rows[0]?.indisvalid) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (existing.rows[0]) {
|
|
691
|
+
await client.query('DROP INDEX CONCURRENTLY IF EXISTS idx_fingerprints_ip');
|
|
692
|
+
}
|
|
693
|
+
await client.query('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_fingerprints_ip ON device_fingerprints(ip_address)');
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
console.error('Could not build idx_fingerprints_ip; install matching will be slower:', error);
|
|
697
|
+
}
|
|
698
|
+
finally {
|
|
699
|
+
if (client) {
|
|
700
|
+
if (lockHeld) {
|
|
701
|
+
await client
|
|
702
|
+
.query('SELECT pg_advisory_unlock($1)', [FINGERPRINT_IP_INDEX_LOCK_KEY])
|
|
703
|
+
.catch(() => undefined);
|
|
704
|
+
}
|
|
705
|
+
client.release();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
647
708
|
}
|
|
648
709
|
//# sourceMappingURL=database.js.map
|
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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"}
|
|
@@ -36,6 +36,25 @@ export declare const DEFAULT_ATTRIBUTION_WINDOW_HOURS = 168;
|
|
|
36
36
|
* Minimum confidence threshold for attribution (70%)
|
|
37
37
|
*/
|
|
38
38
|
export declare const CONFIDENCE_THRESHOLD = 70;
|
|
39
|
+
/**
|
|
40
|
+
* The most a candidate can score without the IP signal. Below the threshold, so
|
|
41
|
+
* an IP match is required for any attribution — which is what lets
|
|
42
|
+
* matchInstallToClick fetch only the clicks from the install's own network.
|
|
43
|
+
* (Across SDK and browser contexts, platform + OS version stand in for the user
|
|
44
|
+
* agent and carry the same weight.)
|
|
45
|
+
*/
|
|
46
|
+
export declare const MAX_SCORE_WITHOUT_IP: number;
|
|
47
|
+
/**
|
|
48
|
+
* Narrows the clicks an install can be matched against.
|
|
49
|
+
*/
|
|
50
|
+
export interface InstallMatchScope {
|
|
51
|
+
/**
|
|
52
|
+
* Only clicks on links owned by this organization. A deployment serving
|
|
53
|
+
* several apps passes the installing app's organization, so an install can
|
|
54
|
+
* never be attributed to — or receive the deep link of — another app's link.
|
|
55
|
+
*/
|
|
56
|
+
organizationId?: string;
|
|
57
|
+
}
|
|
39
58
|
/**
|
|
40
59
|
* Generate a fingerprint hash from device data
|
|
41
60
|
* Uses SHA-256 hash of concatenated device attributes
|
|
@@ -48,6 +67,13 @@ export declare function generateFingerprintHash(data: FingerprintData): string;
|
|
|
48
67
|
* must not contribute to a fingerprint match. Public IPs return true.
|
|
49
68
|
*/
|
|
50
69
|
export declare function isAttributableIp(ip: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The network an IP belongs to for matching purposes: its /24 for IPv4, its /64
|
|
72
|
+
* for IPv6 (the prefix a single household or device is normally assigned).
|
|
73
|
+
* Returned as CIDR text, so the same value both compares two addresses here and
|
|
74
|
+
* selects candidate clicks in SQL. Null when the address cannot be parsed.
|
|
75
|
+
*/
|
|
76
|
+
export declare function attributionNetwork(ip: string): string | null;
|
|
51
77
|
/**
|
|
52
78
|
* Calculate confidence score by comparing two fingerprints
|
|
53
79
|
* Returns a score from 0-100 based on matched components
|
|
@@ -62,7 +88,7 @@ export declare function calculateConfidenceScore(fingerprint1: FingerprintData,
|
|
|
62
88
|
*
|
|
63
89
|
* Note: Uses link-specific attribution windows - each link can have its own window
|
|
64
90
|
*/
|
|
65
|
-
export declare function matchInstallToClick(installFingerprint: FingerprintData, attributionWindowHours?: number): Promise<FingerprintMatch | null>;
|
|
91
|
+
export declare function matchInstallToClick(installFingerprint: FingerprintData, attributionWindowHours?: number, scope?: InstallMatchScope): Promise<FingerprintMatch | null>;
|
|
66
92
|
/**
|
|
67
93
|
* Store device fingerprint for a click event
|
|
68
94
|
*/
|
|
@@ -73,7 +99,7 @@ export declare function storeFingerprintForClick(clickId: string, fingerprintDat
|
|
|
73
99
|
export declare function recordInstallEvent(fingerprintData: FingerprintData, deviceId?: string, attributionWindowHours?: number, sdk?: {
|
|
74
100
|
name?: string | null;
|
|
75
101
|
version?: string | null;
|
|
76
|
-
}): Promise<{
|
|
102
|
+
}, scope?: InstallMatchScope): Promise<{
|
|
77
103
|
installId: string;
|
|
78
104
|
match: FingerprintMatch | null;
|
|
79
105
|
deepLinkData: DeepLinkPayload | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/lib/fingerprint.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC5C;AAwBD;;GAEG;AACH,eAAO,MAAM,gCAAgC,MAAM,CAAC;AAEpD;;GAEG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAcrE;AAwCD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAyBpD;
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/lib/fingerprint.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC5C;AAwBD;;GAEG;AACH,eAAO,MAAM,gCAAgC,MAAM,CAAC;AAEpD;;GAEG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,QAIM,CAAC;AAExC;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAcrE;AAwCD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAyBpD;AAiCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB5D;AA6GD;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,eAAe,EAC7B,YAAY,EAAE,eAAe,GAC5B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAkG7C;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,kBAAkB,EAAE,eAAe,EACnC,sBAAsB,GAAE,MAAyC,EACjE,KAAK,GAAE,iBAAsB,GAC5B,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA0GlC;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,eAAe,GAC/B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,eAAe,EAAE,eAAe,EAChC,QAAQ,CAAC,EAAE,MAAM,EACjB,sBAAsB,GAAE,MAAyC,EACjE,GAAG,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACvD,KAAK,GAAE,iBAAsB,GAC5B,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,eAAe,GAAG,IAAI,CAAC;CACtC,CAAC,CAiJD"}
|
package/dist/lib/fingerprint.js
CHANGED
|
@@ -29,6 +29,17 @@ export const DEFAULT_ATTRIBUTION_WINDOW_HOURS = 168;
|
|
|
29
29
|
* Minimum confidence threshold for attribution (70%)
|
|
30
30
|
*/
|
|
31
31
|
export const CONFIDENCE_THRESHOLD = 70;
|
|
32
|
+
/**
|
|
33
|
+
* The most a candidate can score without the IP signal. Below the threshold, so
|
|
34
|
+
* an IP match is required for any attribution — which is what lets
|
|
35
|
+
* matchInstallToClick fetch only the clicks from the install's own network.
|
|
36
|
+
* (Across SDK and browser contexts, platform + OS version stand in for the user
|
|
37
|
+
* agent and carry the same weight.)
|
|
38
|
+
*/
|
|
39
|
+
export const MAX_SCORE_WITHOUT_IP = FINGERPRINT_WEIGHTS.USER_AGENT +
|
|
40
|
+
FINGERPRINT_WEIGHTS.TIMEZONE +
|
|
41
|
+
FINGERPRINT_WEIGHTS.LANGUAGE +
|
|
42
|
+
FINGERPRINT_WEIGHTS.SCREEN_RESOLUTION;
|
|
32
43
|
/**
|
|
33
44
|
* Generate a fingerprint hash from device data
|
|
34
45
|
* Uses SHA-256 hash of concatenated device attributes
|
|
@@ -119,20 +130,60 @@ export function isAttributableIp(ip) {
|
|
|
119
130
|
// Not a recognizable IPv4 or IPv6 address.
|
|
120
131
|
return false;
|
|
121
132
|
}
|
|
122
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Expand an IPv6 address to its eight hextets, lowercased and without leading
|
|
135
|
+
* zeros, so two spellings of the same address compare equal. An embedded IPv4
|
|
136
|
+
* tail (`64:ff9b::1.2.3.4`) becomes its two hextets. Returns null for anything
|
|
137
|
+
* that is not an IPv6 address.
|
|
138
|
+
*/
|
|
139
|
+
function expandIPv6(ip) {
|
|
140
|
+
let addr = ip.toLowerCase().split('%')[0]; // drop a zone index
|
|
141
|
+
const lastColon = addr.lastIndexOf(':');
|
|
142
|
+
const tail = addr.slice(lastColon + 1);
|
|
143
|
+
if (tail.includes('.')) {
|
|
144
|
+
const n = ipv4ToInt(tail);
|
|
145
|
+
if (n === null)
|
|
146
|
+
return null;
|
|
147
|
+
addr = `${addr.slice(0, lastColon + 1)}${(n >>> 16).toString(16)}:${(n & 0xffff).toString(16)}`;
|
|
148
|
+
}
|
|
149
|
+
const halves = addr.split('::');
|
|
150
|
+
if (halves.length > 2)
|
|
151
|
+
return null;
|
|
152
|
+
const head = halves[0] ? halves[0].split(':') : [];
|
|
153
|
+
const rest = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
|
|
154
|
+
const missing = 8 - head.length - rest.length;
|
|
155
|
+
if (halves.length === 2 ? missing < 1 : missing !== 0)
|
|
156
|
+
return null;
|
|
157
|
+
const groups = [...head, ...Array(missing).fill('0'), ...rest];
|
|
158
|
+
if (!groups.every((g) => /^[0-9a-f]{1,4}$/.test(g)))
|
|
159
|
+
return null;
|
|
160
|
+
return groups.map((g) => parseInt(g, 16).toString(16));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The network an IP belongs to for matching purposes: its /24 for IPv4, its /64
|
|
164
|
+
* for IPv6 (the prefix a single household or device is normally assigned).
|
|
165
|
+
* Returned as CIDR text, so the same value both compares two addresses here and
|
|
166
|
+
* selects candidate clicks in SQL. Null when the address cannot be parsed.
|
|
167
|
+
*/
|
|
168
|
+
export function attributionNetwork(ip) {
|
|
123
169
|
if (!ip)
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
170
|
+
return null;
|
|
171
|
+
let addr = ip.trim();
|
|
172
|
+
if (addr.toLowerCase().startsWith('::ffff:') && addr.includes('.'))
|
|
173
|
+
addr = addr.slice(7);
|
|
174
|
+
if (addr.includes('.') && !addr.includes(':')) {
|
|
175
|
+
const n = ipv4ToInt(addr);
|
|
176
|
+
if (n === null)
|
|
177
|
+
return null;
|
|
178
|
+
return `${n >>> 24}.${(n >>> 16) & 0xff}.${(n >>> 8) & 0xff}.0/24`;
|
|
129
179
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
180
|
+
if (addr.includes(':')) {
|
|
181
|
+
const groups = expandIPv6(addr);
|
|
182
|
+
if (!groups)
|
|
183
|
+
return null;
|
|
184
|
+
return `${groups.slice(0, 4).join(':')}::/64`;
|
|
134
185
|
}
|
|
135
|
-
return
|
|
186
|
+
return null;
|
|
136
187
|
}
|
|
137
188
|
/**
|
|
138
189
|
* Normalize user agent for comparison
|
|
@@ -236,9 +287,9 @@ export function calculateConfidenceScore(fingerprint1, fingerprint2) {
|
|
|
236
287
|
fingerprint2.ipAddress &&
|
|
237
288
|
isAttributableIp(fingerprint1.ipAddress) &&
|
|
238
289
|
isAttributableIp(fingerprint2.ipAddress)) {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
290
|
+
const network1 = attributionNetwork(fingerprint1.ipAddress);
|
|
291
|
+
const network2 = attributionNetwork(fingerprint2.ipAddress);
|
|
292
|
+
if (network1 !== null && network1 === network2) {
|
|
242
293
|
score += FINGERPRINT_WEIGHTS.IP_ADDRESS;
|
|
243
294
|
matchedFactors.push('ip');
|
|
244
295
|
}
|
|
@@ -314,11 +365,31 @@ export function calculateConfidenceScore(fingerprint1, fingerprint2) {
|
|
|
314
365
|
*
|
|
315
366
|
* Note: Uses link-specific attribution windows - each link can have its own window
|
|
316
367
|
*/
|
|
317
|
-
export async function matchInstallToClick(installFingerprint, attributionWindowHours = DEFAULT_ATTRIBUTION_WINDOW_HOURS) {
|
|
368
|
+
export async function matchInstallToClick(installFingerprint, attributionWindowHours = DEFAULT_ATTRIBUTION_WINDOW_HOURS, scope = {}) {
|
|
369
|
+
// No candidate reaches the threshold without matching the install's network
|
|
370
|
+
// (MAX_SCORE_WITHOUT_IP), so an install from a shared or unparseable address
|
|
371
|
+
// cannot be attributed, and the only clicks worth scoring are the ones from
|
|
372
|
+
// its own /24 or /64. Selecting those in SQL keeps every click within the
|
|
373
|
+
// attribution window in reach, however much traffic the server takes.
|
|
374
|
+
if (!isAttributableIp(installFingerprint.ipAddress)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const network = attributionNetwork(installFingerprint.ipAddress);
|
|
378
|
+
if (!network) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
318
381
|
// Query recent click events within maximum possible attribution window (90 days)
|
|
319
382
|
// We'll validate against each link's specific window during matching
|
|
320
383
|
const maxWindowHours = 2160; // 90 days
|
|
321
384
|
const cutoffTime = new Date(Date.now() - maxWindowHours * 60 * 60 * 1000);
|
|
385
|
+
const params = [cutoffTime, network];
|
|
386
|
+
let scopeFilter = '';
|
|
387
|
+
if (scope.organizationId) {
|
|
388
|
+
params.push(scope.organizationId);
|
|
389
|
+
scopeFilter = `AND l.organization_id = $${params.length}`;
|
|
390
|
+
}
|
|
391
|
+
// The LIMIT only bounds work for a very busy network (a campus, an office);
|
|
392
|
+
// it applies after the network filter, so ordinary traffic never reaches it.
|
|
322
393
|
const clicksResult = await db.query(`SELECT
|
|
323
394
|
ce.id as click_id,
|
|
324
395
|
ce.link_id,
|
|
@@ -333,12 +404,14 @@ export async function matchInstallToClick(installFingerprint, attributionWindowH
|
|
|
333
404
|
df.screen_height,
|
|
334
405
|
df.platform,
|
|
335
406
|
df.platform_version
|
|
336
|
-
FROM
|
|
337
|
-
INNER JOIN
|
|
407
|
+
FROM device_fingerprints df
|
|
408
|
+
INNER JOIN click_events ce ON ce.id = df.click_id
|
|
338
409
|
INNER JOIN links l ON ce.link_id = l.id
|
|
339
|
-
WHERE
|
|
410
|
+
WHERE df.ip_address <<= $2::cidr
|
|
411
|
+
AND ce.clicked_at >= $1
|
|
412
|
+
${scopeFilter}
|
|
340
413
|
ORDER BY ce.clicked_at DESC
|
|
341
|
-
LIMIT 1000`,
|
|
414
|
+
LIMIT 1000`, params);
|
|
342
415
|
if (clicksResult.rows.length === 0) {
|
|
343
416
|
return null;
|
|
344
417
|
}
|
|
@@ -413,10 +486,10 @@ export async function storeFingerprintForClick(clickId, fingerprintData) {
|
|
|
413
486
|
/**
|
|
414
487
|
* Record an install event and attempt to match it to a click
|
|
415
488
|
*/
|
|
416
|
-
export async function recordInstallEvent(fingerprintData, deviceId, attributionWindowHours = DEFAULT_ATTRIBUTION_WINDOW_HOURS, sdk) {
|
|
489
|
+
export async function recordInstallEvent(fingerprintData, deviceId, attributionWindowHours = DEFAULT_ATTRIBUTION_WINDOW_HOURS, sdk, scope = {}) {
|
|
417
490
|
const fingerprintHash = generateFingerprintHash(fingerprintData);
|
|
418
491
|
// Attempt to match install to a click
|
|
419
|
-
const match = await matchInstallToClick(fingerprintData, attributionWindowHours);
|
|
492
|
+
const match = await matchInstallToClick(fingerprintData, attributionWindowHours, scope);
|
|
420
493
|
// Attribution metadata for measurement: install attribution is
|
|
421
494
|
// fingerprint-based, so the method is 'fingerprint' on a match and 'none'
|
|
422
495
|
// (organic) otherwise; matched_factors records which signals matched.
|
|
@@ -506,28 +579,21 @@ export async function recordInstallEvent(fingerprintData, deviceId, attributionW
|
|
|
506
579
|
WHERE id = $2`, [JSON.stringify(deepLinkData), installId]);
|
|
507
580
|
// Trigger webhooks for install_event (only if attributed)
|
|
508
581
|
try {
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
if (
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
userAgent: fingerprintData.userAgent,
|
|
525
|
-
platform: fingerprintData.platform,
|
|
526
|
-
};
|
|
527
|
-
// Trigger webhooks without delivery logging (basic version)
|
|
528
|
-
// For delivery logging, use @linkforty/cloud premium features
|
|
529
|
-
await triggerWebhooks(webhooksResult.rows, 'install_event', installId, installEventData);
|
|
530
|
-
}
|
|
582
|
+
const { findWebhooksForLink, triggerWebhooks } = await import('./webhook.js');
|
|
583
|
+
const webhooks = await findWebhooksForLink(match.linkId);
|
|
584
|
+
if (webhooks.length > 0) {
|
|
585
|
+
const installEventData = {
|
|
586
|
+
id: installId,
|
|
587
|
+
linkId: match.linkId,
|
|
588
|
+
fingerprintHash,
|
|
589
|
+
confidenceScore: match.confidenceScore,
|
|
590
|
+
installedAt: new Date().toISOString(),
|
|
591
|
+
deepLinkData,
|
|
592
|
+
ipAddress: fingerprintData.ipAddress,
|
|
593
|
+
userAgent: fingerprintData.userAgent,
|
|
594
|
+
platform: fingerprintData.platform,
|
|
595
|
+
};
|
|
596
|
+
await triggerWebhooks(webhooks, 'install_event', installId, installEventData);
|
|
531
597
|
}
|
|
532
598
|
}
|
|
533
599
|
catch (webhookError) {
|