@linkforty/core 1.31.0 → 1.33.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.
Files changed (50) hide show
  1. package/README.md +37 -1
  2. package/dist/index.d.ts +5 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/database.d.ts +2 -0
  7. package/dist/lib/database.d.ts.map +1 -1
  8. package/dist/lib/database.js +92 -2
  9. package/dist/lib/database.js.map +1 -1
  10. package/dist/lib/fingerprint.d.ts +28 -2
  11. package/dist/lib/fingerprint.d.ts.map +1 -1
  12. package/dist/lib/fingerprint.js +109 -43
  13. package/dist/lib/fingerprint.js.map +1 -1
  14. package/dist/lib/fingerprint.test.js +77 -0
  15. package/dist/lib/fingerprint.test.js.map +1 -1
  16. package/dist/lib/link-lookup.d.ts +50 -0
  17. package/dist/lib/link-lookup.d.ts.map +1 -0
  18. package/dist/lib/link-lookup.js +54 -0
  19. package/dist/lib/link-lookup.js.map +1 -0
  20. package/dist/lib/link-lookup.test.d.ts +2 -0
  21. package/dist/lib/link-lookup.test.d.ts.map +1 -0
  22. package/dist/lib/link-lookup.test.js +53 -0
  23. package/dist/lib/link-lookup.test.js.map +1 -0
  24. package/dist/lib/link-resolution-cache.d.ts +3 -4
  25. package/dist/lib/link-resolution-cache.d.ts.map +1 -1
  26. package/dist/lib/link-resolution-cache.js +8 -6
  27. package/dist/lib/link-resolution-cache.js.map +1 -1
  28. package/dist/lib/link-resolution-cache.test.js +5 -0
  29. package/dist/lib/link-resolution-cache.test.js.map +1 -1
  30. package/dist/lib/webhook.d.ts +22 -1
  31. package/dist/lib/webhook.d.ts.map +1 -1
  32. package/dist/lib/webhook.js +32 -2
  33. package/dist/lib/webhook.js.map +1 -1
  34. package/dist/lib/webhook.test.js +55 -4
  35. package/dist/lib/webhook.test.js.map +1 -1
  36. package/dist/routes/links.js +5 -5
  37. package/dist/routes/links.js.map +1 -1
  38. package/dist/routes/preview.d.ts.map +1 -1
  39. package/dist/routes/preview.js +31 -13
  40. package/dist/routes/preview.js.map +1 -1
  41. package/dist/routes/redirect.d.ts.map +1 -1
  42. package/dist/routes/redirect.js +16 -10
  43. package/dist/routes/redirect.js.map +1 -1
  44. package/dist/routes/sdk.d.ts +15 -2
  45. package/dist/routes/sdk.d.ts.map +1 -1
  46. package/dist/routes/sdk.js +30 -18
  47. package/dist/routes/sdk.js.map +1 -1
  48. package/dist/types/index.d.ts +3 -1
  49. package/dist/types/index.d.ts.map +1 -1
  50. package/package.json +1 -1
package/README.md CHANGED
@@ -296,6 +296,8 @@ POST /api/webhooks/:id/test?userId=user-uuid
296
296
 
297
297
  Events: `click_event`, `install_event`, `conversion_event`, `sdk_event`. Payloads are HMAC SHA-256 signed.
298
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
+
299
301
  ### Mobile SDK Endpoints
300
302
 
301
303
  ```bash
@@ -371,6 +373,7 @@ interface ServerOptions {
371
373
  logger?: boolean; // Enable Fastify logger (default: true)
372
374
  trustProxy?: boolean | number; // Trust X-Forwarded-For when behind a proxy (default: false)
373
375
  redirect?: RedirectRouteOptions; // See below
376
+ sdk?: SdkRouteOptions; // See below
374
377
  }
375
378
 
376
379
  interface RedirectRouteOptions {
@@ -398,8 +401,21 @@ interface RedirectRouteOptions {
398
401
  // A hook that throws is logged and treated as false.
399
402
  isWebLinkRequest?: (request) => Promise<boolean> | boolean;
400
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
+ }
401
415
  ```
402
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
+
403
419
  `renderLaunchpadPage()`, `defaultLaunchpadContent()` and `shouldServeLaunchpadOnDesktop()` are exported so a host can render the same page elsewhere (a settings preview, for instance).
404
420
 
405
421
  ### Running behind a reverse proxy
@@ -431,13 +447,32 @@ SHORTLINK_DOMAIN=yourdomain.com
431
447
 
432
448
  Core does not create a `users` table. Authentication and user management are the consumer's responsibility. The `user_id` column on `links` and `webhooks` is optional (nullable, no foreign key) — use it for multi-tenant scoping when your auth layer provides a user identity.
433
449
 
450
+ ### Short codes and URL resolution
451
+
452
+ Every short code is unique across the server. Links created through Core's API have no organization, so they all share one set of codes.
453
+
454
+ An application that builds on Core and assigns links to organizations (by setting `links.organization_id`) gets one set of codes per organization: two organizations can each have `promo`. Template slugs stay unique across the server, and URLs resolve like this:
455
+
456
+ - `/{templateSlug}/{shortCode}` resolves to exactly one link.
457
+ - `/{shortCode}` prefers the organization the request belongs to, then a link without a template, then the oldest link with that code.
458
+
459
+ To tell Core which organization a request belongs to, for example by custom domain, register a resolver:
460
+
461
+ ```typescript
462
+ import { setLinkScopeResolver } from '@linkforty/core';
463
+
464
+ setLinkScopeResolver(async (request) => organizationIdForHost(request.headers.host) ?? null);
465
+ ```
466
+
467
+ The redirect, SDK resolve and preview routes all use it. A resolver that throws is logged and treated as null.
468
+
434
469
  ### Links Table
435
470
 
436
471
  | Column | Type | Description |
437
472
  |-------------------------|--------------|------------------------------------------|
438
473
  | id | UUID | Primary key |
439
474
  | user_id | UUID | Optional owner/tenant identifier |
440
- | short_code | VARCHAR(20) | Unique short code |
475
+ | short_code | VARCHAR(20) | Short code, unique per organization_id |
441
476
  | original_url | TEXT | Original URL |
442
477
  | title | VARCHAR(255) | Link title |
443
478
  | description | TEXT | Link description |
@@ -536,6 +571,7 @@ Core does not create a `users` table. Authentication and user management are the
536
571
  |------------|--------------|----------------------------------|
537
572
  | id | UUID | Primary key |
538
573
  | user_id | UUID | Optional owner/tenant identifier |
574
+ | organization_id | UUID | Optional owning organization; receives events for all its links |
539
575
  | name | VARCHAR(255) | Webhook name |
540
576
  | url | TEXT | Delivery URL |
541
577
  | 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.
@@ -33,10 +36,12 @@ export * from './lib/client-ip.js';
33
36
  export * from './lib/database.js';
34
37
  export * from './lib/fingerprint.js';
35
38
  export * from './lib/webhook.js';
39
+ export * from './lib/link-lookup.js';
36
40
  export * from './lib/link-safety.js';
37
41
  export * from './lib/launchpad.js';
38
42
  export * from './lib/event-emitter.js';
39
43
  export * from './types/index.js';
40
44
  export type { RedirectRouteOptions } from './routes/redirect.js';
45
+ export type { SdkRouteOptions } from './routes/sdk.js';
41
46
  export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes, healthRoutes } from './routes/index.js';
42
47
  //# 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;AACxE,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAUjF;;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;CACjC;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,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"}
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,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);
@@ -55,6 +55,7 @@ export * from './lib/client-ip.js';
55
55
  export * from './lib/database.js';
56
56
  export * from './lib/fingerprint.js';
57
57
  export * from './lib/webhook.js';
58
+ export * from './lib/link-lookup.js';
58
59
  export * from './lib/link-safety.js';
59
60
  export * from './lib/launchpad.js';
60
61
  export * from './lib/event-emitter.js';
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,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;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAoBlD;;;;;;;;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,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,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AAEjC,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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -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;AAG5C,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,iBAuoBrE"}
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,iBA8qBrE"}
@@ -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
@@ -109,7 +111,7 @@ export async function initializeDatabase(options = {}) {
109
111
  CREATE TABLE IF NOT EXISTS links (
110
112
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
111
113
  user_id UUID,
112
- short_code VARCHAR(20) UNIQUE NOT NULL,
114
+ short_code VARCHAR(20) NOT NULL,
113
115
  original_url TEXT NOT NULL,
114
116
  title VARCHAR(255),
115
117
  description TEXT,
@@ -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
  //
@@ -240,6 +247,36 @@ export async function initializeDatabase(options = {}) {
240
247
  END IF;
241
248
  END $$;
242
249
  `);
250
+ // Short codes are unique per organization; links without one share a
251
+ // single namespace (lib/link-lookup.ts describes how URLs resolve).
252
+ // The per-organization index is created before any single-column unique
253
+ // constraint on short_code is dropped, so there is never a moment without
254
+ // one. idx_links_short_code_lookup serves lookups by code alone.
255
+ await client.query(`
256
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_links_org_short_code
257
+ ON links ((COALESCE(organization_id, '00000000-0000-0000-0000-000000000000'::uuid)), short_code)
258
+ `);
259
+ await client.query('CREATE INDEX IF NOT EXISTS idx_links_short_code_lookup ON links(short_code)');
260
+ await client.query(`
261
+ DO $$
262
+ DECLARE
263
+ c record;
264
+ BEGIN
265
+ FOR c IN
266
+ SELECT con.conname
267
+ FROM pg_constraint con
268
+ WHERE con.conrelid = 'links'::regclass
269
+ AND con.contype = 'u'
270
+ AND con.conkey = ARRAY[(
271
+ SELECT attnum FROM pg_attribute
272
+ WHERE attrelid = 'links'::regclass AND attname = 'short_code'
273
+ )]::smallint[]
274
+ LOOP
275
+ EXECUTE format('ALTER TABLE links DROP CONSTRAINT %I', c.conname);
276
+ END LOOP;
277
+ END $$;
278
+ `);
279
+ await client.query('DROP INDEX IF EXISTS idx_links_short_code');
243
280
  // Add template_id column to links table
244
281
  await client.query(`
245
282
  DO $$
@@ -584,7 +621,6 @@ export async function initializeDatabase(options = {}) {
584
621
  END $$;
585
622
  `);
586
623
  // Create indexes for performance
587
- await client.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_links_short_code ON links(short_code)');
588
624
  await client.query('CREATE INDEX IF NOT EXISTS idx_links_user_id ON links(user_id)');
589
625
  await client.query('CREATE INDEX IF NOT EXISTS idx_links_created_at ON links(created_at DESC)');
590
626
  await client.query('CREATE INDEX IF NOT EXISTS idx_clicks_link_id ON click_events(link_id)');
@@ -607,6 +643,7 @@ export async function initializeDatabase(options = {}) {
607
643
  // Indexes for webhooks
608
644
  await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_user_id ON webhooks(user_id)');
609
645
  await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(is_active) WHERE is_active = true');
646
+ await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_organization_id ON webhooks(organization_id)');
610
647
  // Indexes for in-app events
611
648
  await client.query('CREATE INDEX IF NOT EXISTS idx_in_app_events_install_id ON in_app_events(install_id)');
612
649
  await client.query('CREATE INDEX IF NOT EXISTS idx_in_app_events_name ON in_app_events(event_name)');
@@ -644,5 +681,58 @@ export async function initializeDatabase(options = {}) {
644
681
  }
645
682
  client.release();
646
683
  }
684
+ await ensureFingerprintIpIndex();
685
+ }
686
+ /**
687
+ * Index device_fingerprints by IP, which install matching selects candidate
688
+ * clicks on (matchInstallToClick).
689
+ *
690
+ * Built CONCURRENTLY because the table takes a write on every click and can be
691
+ * large, and a plain CREATE INDEX would hold those writes
692
+ * for the length of the build. That rules out the schema lock above: a
693
+ * concurrent build waits for every older snapshot to finish, including that of
694
+ * another instance blocked on the lock, which would wait on us in turn.
695
+ *
696
+ * A concurrent build that fails leaves an invalid index behind, which IF NOT
697
+ * EXISTS would then skip forever, so an invalid one is dropped and rebuilt. A
698
+ * build in progress also reads as invalid, hence the try-lock: only one instance
699
+ * ever works on the index, and the others move on rather than wait. Failure is
700
+ * logged, not thrown: matching is correct without the index, only slower.
701
+ */
702
+ async function ensureFingerprintIpIndex() {
703
+ let client;
704
+ let lockHeld = false;
705
+ try {
706
+ client = await db.connect();
707
+ const lock = await client.query('SELECT pg_try_advisory_lock($1) AS acquired', [FINGERPRINT_IP_INDEX_LOCK_KEY]);
708
+ lockHeld = lock.rows[0]?.acquired === true;
709
+ if (!lockHeld) {
710
+ return;
711
+ }
712
+ const existing = await client.query(`SELECT i.indisvalid
713
+ FROM pg_class c
714
+ INNER JOIN pg_index i ON i.indexrelid = c.oid
715
+ WHERE c.relname = 'idx_fingerprints_ip'`);
716
+ if (existing.rows[0]?.indisvalid) {
717
+ return;
718
+ }
719
+ if (existing.rows[0]) {
720
+ await client.query('DROP INDEX CONCURRENTLY IF EXISTS idx_fingerprints_ip');
721
+ }
722
+ await client.query('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_fingerprints_ip ON device_fingerprints(ip_address)');
723
+ }
724
+ catch (error) {
725
+ console.error('Could not build idx_fingerprints_ip; install matching will be slower:', error);
726
+ }
727
+ finally {
728
+ if (client) {
729
+ if (lockHeld) {
730
+ await client
731
+ .query('SELECT pg_advisory_unlock($1)', [FINGERPRINT_IP_INDEX_LOCK_KEY])
732
+ .catch(() => undefined);
733
+ }
734
+ client.release();
735
+ }
736
+ }
647
737
  }
648
738
  //# sourceMappingURL=database.js.map
@@ -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;AA+HD;;;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,GAChE,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAkFlC;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,GACtD,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,eAAe,GAAG,IAAI,CAAC;CACtC,CAAC,CAiKD"}
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"}
@@ -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
- function normalizeIP(ip) {
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
- // For IPv4, use first 3 octets (e.g., 192.168.1.x)
126
- if (ip.includes('.')) {
127
- const parts = ip.split('.');
128
- return parts.slice(0, 3).join('.');
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
- // For IPv6, use first 4 groups (e.g., 2001:0db8:85a3:0000:xxxx)
131
- if (ip.includes(':')) {
132
- const parts = ip.split(':');
133
- return parts.slice(0, 4).join(':');
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 ip;
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 ip1 = normalizeIP(fingerprint1.ipAddress);
240
- const ip2 = normalizeIP(fingerprint2.ipAddress);
241
- if (ip1 === ip2) {
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 click_events ce
337
- INNER JOIN device_fingerprints df ON df.click_id = ce.id
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 ce.clicked_at >= $1
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`, [cutoffTime]);
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
- // Get the link's user_id for webhook lookup
510
- const linkUserResult = await db.query('SELECT user_id FROM links WHERE id = $1', [match.linkId]);
511
- if (linkUserResult.rows.length > 0) {
512
- const userId = linkUserResult.rows[0].user_id;
513
- const webhooksResult = await db.query('SELECT * FROM webhooks WHERE user_id = $1 AND is_active = true', [userId]);
514
- if (webhooksResult.rows.length > 0) {
515
- const { triggerWebhooks } = await import('./webhook.js');
516
- const installEventData = {
517
- id: installId,
518
- linkId: match.linkId,
519
- fingerprintHash,
520
- confidenceScore: match.confidenceScore,
521
- installedAt: new Date().toISOString(),
522
- deepLinkData,
523
- ipAddress: fingerprintData.ipAddress,
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) {