@linkforty/core 1.44.0 → 1.44.2

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 (174) hide show
  1. package/README.md +6 -6
  2. package/package.json +2 -1
  3. package/dist/index.config.test.d.ts +0 -2
  4. package/dist/index.config.test.d.ts.map +0 -1
  5. package/dist/index.config.test.js +0 -87
  6. package/dist/index.config.test.js.map +0 -1
  7. package/dist/lib/bot-detection.test.d.ts +0 -2
  8. package/dist/lib/bot-detection.test.d.ts.map +0 -1
  9. package/dist/lib/bot-detection.test.js +0 -65
  10. package/dist/lib/bot-detection.test.js.map +0 -1
  11. package/dist/lib/client-ip.test.d.ts +0 -7
  12. package/dist/lib/client-ip.test.d.ts.map +0 -1
  13. package/dist/lib/client-ip.test.js +0 -114
  14. package/dist/lib/client-ip.test.js.map +0 -1
  15. package/dist/lib/config.test.d.ts +0 -2
  16. package/dist/lib/config.test.d.ts.map +0 -1
  17. package/dist/lib/config.test.js +0 -126
  18. package/dist/lib/config.test.js.map +0 -1
  19. package/dist/lib/database.test.d.ts +0 -2
  20. package/dist/lib/database.test.d.ts.map +0 -1
  21. package/dist/lib/database.test.js +0 -180
  22. package/dist/lib/database.test.js.map +0 -1
  23. package/dist/lib/deep-link-payload.test.d.ts +0 -2
  24. package/dist/lib/deep-link-payload.test.d.ts.map +0 -1
  25. package/dist/lib/deep-link-payload.test.js +0 -202
  26. package/dist/lib/deep-link-payload.test.js.map +0 -1
  27. package/dist/lib/event-emitter.test.d.ts +0 -2
  28. package/dist/lib/event-emitter.test.d.ts.map +0 -1
  29. package/dist/lib/event-emitter.test.js +0 -162
  30. package/dist/lib/event-emitter.test.js.map +0 -1
  31. package/dist/lib/fingerprint.test.d.ts +0 -2
  32. package/dist/lib/fingerprint.test.d.ts.map +0 -1
  33. package/dist/lib/fingerprint.test.js +0 -750
  34. package/dist/lib/fingerprint.test.js.map +0 -1
  35. package/dist/lib/launchpad.test.d.ts +0 -2
  36. package/dist/lib/launchpad.test.d.ts.map +0 -1
  37. package/dist/lib/launchpad.test.js +0 -368
  38. package/dist/lib/launchpad.test.js.map +0 -1
  39. package/dist/lib/link-lookup.test.d.ts +0 -2
  40. package/dist/lib/link-lookup.test.d.ts.map +0 -1
  41. package/dist/lib/link-lookup.test.js +0 -61
  42. package/dist/lib/link-lookup.test.js.map +0 -1
  43. package/dist/lib/link-resolution-cache.test.d.ts +0 -2
  44. package/dist/lib/link-resolution-cache.test.d.ts.map +0 -1
  45. package/dist/lib/link-resolution-cache.test.js +0 -45
  46. package/dist/lib/link-resolution-cache.test.js.map +0 -1
  47. package/dist/lib/link-safety.test.d.ts +0 -2
  48. package/dist/lib/link-safety.test.d.ts.map +0 -1
  49. package/dist/lib/link-safety.test.js +0 -211
  50. package/dist/lib/link-safety.test.js.map +0 -1
  51. package/dist/lib/not-found-page.test.d.ts +0 -2
  52. package/dist/lib/not-found-page.test.d.ts.map +0 -1
  53. package/dist/lib/not-found-page.test.js +0 -151
  54. package/dist/lib/not-found-page.test.js.map +0 -1
  55. package/dist/lib/portable/cli.test.d.ts +0 -2
  56. package/dist/lib/portable/cli.test.d.ts.map +0 -1
  57. package/dist/lib/portable/cli.test.js +0 -98
  58. package/dist/lib/portable/cli.test.js.map +0 -1
  59. package/dist/lib/portable/portable.integration.test.d.ts +0 -2
  60. package/dist/lib/portable/portable.integration.test.d.ts.map +0 -1
  61. package/dist/lib/portable/portable.integration.test.js +0 -139
  62. package/dist/lib/portable/portable.integration.test.js.map +0 -1
  63. package/dist/lib/portable/registry.schema.test.d.ts +0 -2
  64. package/dist/lib/portable/registry.schema.test.d.ts.map +0 -1
  65. package/dist/lib/portable/registry.schema.test.js +0 -59
  66. package/dist/lib/portable/registry.schema.test.js.map +0 -1
  67. package/dist/lib/scanner-detection.test.d.ts +0 -2
  68. package/dist/lib/scanner-detection.test.d.ts.map +0 -1
  69. package/dist/lib/scanner-detection.test.js +0 -124
  70. package/dist/lib/scanner-detection.test.js.map +0 -1
  71. package/dist/lib/schema-guard.test.d.ts +0 -2
  72. package/dist/lib/schema-guard.test.d.ts.map +0 -1
  73. package/dist/lib/schema-guard.test.js +0 -83
  74. package/dist/lib/schema-guard.test.js.map +0 -1
  75. package/dist/lib/utils.test.d.ts +0 -2
  76. package/dist/lib/utils.test.d.ts.map +0 -1
  77. package/dist/lib/utils.test.js +0 -261
  78. package/dist/lib/utils.test.js.map +0 -1
  79. package/dist/lib/webhook.test.d.ts +0 -2
  80. package/dist/lib/webhook.test.d.ts.map +0 -1
  81. package/dist/lib/webhook.test.js +0 -340
  82. package/dist/lib/webhook.test.js.map +0 -1
  83. package/dist/routes/health.test.d.ts +0 -2
  84. package/dist/routes/health.test.d.ts.map +0 -1
  85. package/dist/routes/health.test.js +0 -82
  86. package/dist/routes/health.test.js.map +0 -1
  87. package/dist/routes/preview.test.d.ts +0 -2
  88. package/dist/routes/preview.test.d.ts.map +0 -1
  89. package/dist/routes/preview.test.js +0 -111
  90. package/dist/routes/preview.test.js.map +0 -1
  91. package/dist/routes/qr.test.d.ts +0 -2
  92. package/dist/routes/qr.test.d.ts.map +0 -1
  93. package/dist/routes/qr.test.js +0 -67
  94. package/dist/routes/qr.test.js.map +0 -1
  95. package/dist/routes/redirect.click-event.test.d.ts +0 -2
  96. package/dist/routes/redirect.click-event.test.d.ts.map +0 -1
  97. package/dist/routes/redirect.click-event.test.js +0 -85
  98. package/dist/routes/redirect.click-event.test.js.map +0 -1
  99. package/dist/routes/redirect.host-scope.integration.test.d.ts +0 -2
  100. package/dist/routes/redirect.host-scope.integration.test.d.ts.map +0 -1
  101. package/dist/routes/redirect.host-scope.integration.test.js +0 -105
  102. package/dist/routes/redirect.host-scope.integration.test.js.map +0 -1
  103. package/dist/routes/redirect.interstitial.test.d.ts +0 -2
  104. package/dist/routes/redirect.interstitial.test.d.ts.map +0 -1
  105. package/dist/routes/redirect.interstitial.test.js +0 -128
  106. package/dist/routes/redirect.interstitial.test.js.map +0 -1
  107. package/dist/routes/redirect.launchpad.test.d.ts +0 -2
  108. package/dist/routes/redirect.launchpad.test.d.ts.map +0 -1
  109. package/dist/routes/redirect.launchpad.test.js +0 -515
  110. package/dist/routes/redirect.launchpad.test.js.map +0 -1
  111. package/dist/routes/redirect.no-web-destination.test.d.ts +0 -2
  112. package/dist/routes/redirect.no-web-destination.test.d.ts.map +0 -1
  113. package/dist/routes/redirect.no-web-destination.test.js +0 -176
  114. package/dist/routes/redirect.no-web-destination.test.js.map +0 -1
  115. package/dist/routes/redirect.not-found.test.d.ts +0 -2
  116. package/dist/routes/redirect.not-found.test.d.ts.map +0 -1
  117. package/dist/routes/redirect.not-found.test.js +0 -280
  118. package/dist/routes/redirect.not-found.test.js.map +0 -1
  119. package/dist/routes/redirect.safety.test.d.ts +0 -2
  120. package/dist/routes/redirect.safety.test.d.ts.map +0 -1
  121. package/dist/routes/redirect.safety.test.js +0 -284
  122. package/dist/routes/redirect.safety.test.js.map +0 -1
  123. package/dist/routes/redirect.store-url.test.d.ts +0 -2
  124. package/dist/routes/redirect.store-url.test.d.ts.map +0 -1
  125. package/dist/routes/redirect.store-url.test.js +0 -239
  126. package/dist/routes/redirect.store-url.test.js.map +0 -1
  127. package/dist/routes/redirect.test.d.ts +0 -2
  128. package/dist/routes/redirect.test.d.ts.map +0 -1
  129. package/dist/routes/redirect.test.js +0 -158
  130. package/dist/routes/redirect.test.js.map +0 -1
  131. package/dist/routes/redirect.web-link.test.d.ts +0 -2
  132. package/dist/routes/redirect.web-link.test.d.ts.map +0 -1
  133. package/dist/routes/redirect.web-link.test.js +0 -138
  134. package/dist/routes/redirect.web-link.test.js.map +0 -1
  135. package/dist/routes/sdk-cache-bypass.test.d.ts +0 -2
  136. package/dist/routes/sdk-cache-bypass.test.d.ts.map +0 -1
  137. package/dist/routes/sdk-cache-bypass.test.js +0 -175
  138. package/dist/routes/sdk-cache-bypass.test.js.map +0 -1
  139. package/dist/routes/sdk.device-values.integration.test.d.ts +0 -2
  140. package/dist/routes/sdk.device-values.integration.test.d.ts.map +0 -1
  141. package/dist/routes/sdk.device-values.integration.test.js +0 -132
  142. package/dist/routes/sdk.device-values.integration.test.js.map +0 -1
  143. package/dist/routes/sdk.event.test.d.ts +0 -2
  144. package/dist/routes/sdk.event.test.d.ts.map +0 -1
  145. package/dist/routes/sdk.event.test.js +0 -284
  146. package/dist/routes/sdk.event.test.js.map +0 -1
  147. package/dist/routes/sdk.install-certainty.integration.test.d.ts +0 -2
  148. package/dist/routes/sdk.install-certainty.integration.test.d.ts.map +0 -1
  149. package/dist/routes/sdk.install-certainty.integration.test.js +0 -104
  150. package/dist/routes/sdk.install-certainty.integration.test.js.map +0 -1
  151. package/dist/routes/sdk.install-live-links.integration.test.d.ts +0 -2
  152. package/dist/routes/sdk.install-live-links.integration.test.d.ts.map +0 -1
  153. package/dist/routes/sdk.install-live-links.integration.test.js +0 -92
  154. package/dist/routes/sdk.install-live-links.integration.test.js.map +0 -1
  155. package/dist/routes/sdk.install-referrer.integration.test.d.ts +0 -2
  156. package/dist/routes/sdk.install-referrer.integration.test.d.ts.map +0 -1
  157. package/dist/routes/sdk.install-referrer.integration.test.js +0 -187
  158. package/dist/routes/sdk.install-referrer.integration.test.js.map +0 -1
  159. package/dist/routes/sdk.install-window-once.integration.test.d.ts +0 -2
  160. package/dist/routes/sdk.install-window-once.integration.test.d.ts.map +0 -1
  161. package/dist/routes/sdk.install-window-once.integration.test.js +0 -147
  162. package/dist/routes/sdk.install-window-once.integration.test.js.map +0 -1
  163. package/dist/routes/sdk.install.test.d.ts +0 -2
  164. package/dist/routes/sdk.install.test.d.ts.map +0 -1
  165. package/dist/routes/sdk.install.test.js +0 -99
  166. package/dist/routes/sdk.install.test.js.map +0 -1
  167. package/dist/routes/sdk.resolve-click.test.d.ts +0 -2
  168. package/dist/routes/sdk.resolve-click.test.d.ts.map +0 -1
  169. package/dist/routes/sdk.resolve-click.test.js +0 -79
  170. package/dist/routes/sdk.resolve-click.test.js.map +0 -1
  171. package/dist/routes/sdk.resolve-scope.test.d.ts +0 -2
  172. package/dist/routes/sdk.resolve-scope.test.d.ts.map +0 -1
  173. package/dist/routes/sdk.resolve-scope.test.js +0 -105
  174. package/dist/routes/sdk.resolve-scope.test.js.map +0 -1
@@ -1,139 +0,0 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import zlib from 'zlib';
5
- import pg from 'pg';
6
- import { afterAll, beforeAll, describe, expect, it } from 'vitest';
7
- import { db, initializeDatabase } from '../database.js';
8
- import { BUNDLE_TABLES, exportedColumns } from './registry.js';
9
- import { exportBundleToFile, importBundleFile } from './files.js';
10
- import { BundleError } from './import.js';
11
- /**
12
- * A bundle restores exactly what it exported: every carried column, value for
13
- * value, with excluded columns left behind. Import refuses conflicts and
14
- * tampered files, and can target a different organization.
15
- *
16
- * Needs a real database; skipped without DATABASE_URL, like database.test.ts.
17
- */
18
- const ADMIN_URL = process.env.DATABASE_URL;
19
- const ORG_A = '0a0a0a0a-0000-4000-8000-00000000000a';
20
- const ORG_B = '0b0b0b0b-0000-4000-8000-00000000000b';
21
- const ORG_X = '0c0c0c0c-0000-4000-8000-00000000000c';
22
- const TEMPLATE = '1a1a1a1a-0000-4000-8000-00000000001a';
23
- const LINK_A = '2a2a2a2a-0000-4000-8000-00000000002a';
24
- const LINK_B = '2b2b2b2b-0000-4000-8000-00000000002b';
25
- const CLICK = '3a3a3a3a-0000-4000-8000-00000000003a';
26
- const INSTALL = '4a4a4a4a-0000-4000-8000-00000000004a';
27
- const WEBHOOK = '5a5a5a5a-0000-4000-8000-00000000005a';
28
- describe.skipIf(!ADMIN_URL)('LinkForty bundles (integration)', () => {
29
- const stamp = Date.now();
30
- const names = { source: `lf_core_bundle_src_${stamp}`, target: `lf_core_bundle_dst_${stamp}`, other: `lf_core_bundle_org_${stamp}` };
31
- const pools = {};
32
- let admin;
33
- let dir;
34
- const urlFor = (name) => {
35
- const url = new URL(ADMIN_URL);
36
- url.pathname = `/${name}`;
37
- return url.toString();
38
- };
39
- /** Every carried column of every row, as Postgres renders it. */
40
- async function snapshot(pool) {
41
- const out = {};
42
- for (const [table, rule] of Object.entries(BUNDLE_TABLES)) {
43
- const cols = exportedColumns(table).map(([c]) => c).join(', ');
44
- const result = await pool.query(`SELECT row_to_json(r) AS row FROM (SELECT ${cols} FROM ${table} ORDER BY ${rule.key}) r`);
45
- out[table] = result.rows.map((r) => r.row);
46
- }
47
- return out;
48
- }
49
- beforeAll(async () => {
50
- admin = new pg.Client({ connectionString: ADMIN_URL });
51
- await admin.connect();
52
- for (const key of ['other', 'target', 'source']) {
53
- await admin.query(`CREATE DATABASE ${names[key]}`);
54
- await initializeDatabase({ url: urlFor(names[key]), pool: { min: 1, max: 3 } });
55
- pools[key] = db;
56
- }
57
- dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lf-bundle-'));
58
- const q = (sql, params = []) => pools.source.query(sql, params);
59
- await q(`INSERT INTO organizations (id, name, settings, suspended_at) VALUES
60
- ($1, 'A', '{"appConfig":{"webFallbackUrl":"https://a.example"},"launchpad":{"mobile":"page"},"brandKit":{"accent":"#000"}}', NOW()),
61
- ($2, 'B', '{}', NULL)`, [ORG_A, ORG_B]);
62
- await q(`INSERT INTO link_templates (id, name, slug, settings, is_default) VALUES ($1, 'T', 'tpl-a', '{"defaultWebFallbackUrl":"https://t.example"}', true)`, [TEMPLATE]);
63
- await q(`INSERT INTO links (id, organization_id, template_id, short_code, original_url, web_fallback_url, deep_link_parameters,
64
- utm_parameters, expires_at, warn_at, disabled_reason, created_at)
65
- VALUES ($1, $2, $3, 'promo', 'https://a.example/p', 'https://a.example/p', '{"token":"x"}', '{"source":"ig"}',
66
- '2030-01-02 03:04:05.678', '2026-10-01 12:00:00+00', 'flagged', '2026-01-01 00:00:00.123456')`, [LINK_A, ORG_A, TEMPLATE]);
67
- await q(`INSERT INTO links (id, organization_id, short_code, original_url) VALUES ($1, $2, 'promo', 'https://b.example')`, [LINK_B, ORG_B]);
68
- await q(`INSERT INTO click_events (id, link_id, clicked_at, ip_address, latitude, longitude, link_params, is_bot)
69
- VALUES ($1, $2, '2026-10-15 10:00:00.5', '203.0.113.7', 37.7749, -122.4194, '{"fbclid":"abc"}', false)`, [CLICK, LINK_A]);
70
- await q(`INSERT INTO device_fingerprints (click_id, fingerprint_hash, ip_address, user_agent, screen_width) VALUES ($1, 'h', '2001:db8::1', 'UA', 1170)`, [CLICK]);
71
- await q(`INSERT INTO install_events (id, link_id, click_id, fingerprint_hash, confidence_score, matched_factors, deep_link_data, ip_address)
72
- VALUES ($1, $2, $3, 'h', 90.5, ARRAY['ip','platform'], '{"shortCode":"promo"}', '203.0.113.7')`, [INSTALL, LINK_A, CLICK]);
73
- await q(`INSERT INTO in_app_events (install_id, event_name, event_data, event_timestamp) VALUES ($1, 'purchase', '{"revenue":29.99}', NOW())`, [INSTALL]);
74
- await q(`INSERT INTO webhooks (id, organization_id, name, url, secret, events, headers) VALUES ($1, $2, 'W', 'https://hooks.example', 'old-secret', '{click_event,install_event}', '{"X-Key":"1"}')`, [WEBHOOK, ORG_A]);
75
- }, 120000);
76
- afterAll(async () => {
77
- for (const pool of Object.values(pools))
78
- await pool?.end().catch(() => undefined);
79
- for (const name of Object.values(names))
80
- await admin.query(`DROP DATABASE IF EXISTS ${name} WITH (FORCE)`);
81
- await admin.end();
82
- fs.rmSync(dir, { recursive: true, force: true });
83
- }, 60000);
84
- it('restores every carried column exactly, and nothing it excludes', async () => {
85
- const file = path.join(dir, 'all.lfbundle');
86
- const exported = await exportBundleToFile(pools.source, file);
87
- expect(exported.counts).toMatchObject({ organization: 2, link: 2, click: 1, fingerprint: 1, install: 1, event: 1, webhook: 1 });
88
- const report = await importBundleFile(pools.target, file);
89
- expect(report.conflicts).toEqual([]);
90
- expect(report.imported).toBe(true);
91
- const [source, target] = [await snapshot(pools.source), await snapshot(pools.target)];
92
- // Settings carry only the listed keys.
93
- const orgA = source.organizations.find((o) => o.id === ORG_A);
94
- delete orgA.settings.brandKit;
95
- expect(target).toEqual(source);
96
- const link = await pools.target.query('SELECT warn_at, disabled_reason FROM links WHERE id = $1', [LINK_A]);
97
- expect(link.rows[0]).toEqual({ warn_at: null, disabled_reason: null });
98
- const org = await pools.target.query('SELECT suspended_at FROM organizations WHERE id = $1', [ORG_A]);
99
- expect(org.rows[0].suspended_at).toBeNull();
100
- const webhook = await pools.target.query('SELECT secret FROM webhooks WHERE id = $1', [WEBHOOK]);
101
- expect(webhook.rows[0].secret).not.toBe('old-secret');
102
- expect(report.webhooks).toEqual([{ id: WEBHOOK, url: 'https://hooks.example', secret: webhook.rows[0].secret }]);
103
- }, 60000);
104
- it('reports conflicts and writes nothing when the data is already there', async () => {
105
- const file = path.join(dir, 'all.lfbundle');
106
- const before = await snapshot(pools.target);
107
- const report = await importBundleFile(pools.target, file);
108
- expect(report.imported).toBe(false);
109
- expect(report.conflicts).toEqual(expect.arrayContaining([
110
- { record: 'link', kind: 'id_exists', value: LINK_A },
111
- { record: 'link', kind: 'short_code_taken', value: 'promo' },
112
- { record: 'template', kind: 'template_slug_taken', value: 'tpl-a' },
113
- ]));
114
- expect(await snapshot(pools.target)).toEqual(before);
115
- }, 60000);
116
- it('refuses a bundle that was changed after export', async () => {
117
- const file = path.join(dir, 'all.lfbundle');
118
- const text = zlib.gunzipSync(fs.readFileSync(file)).toString().replace('https://a.example/p', 'https://evil.example/p');
119
- const tampered = path.join(dir, 'tampered.lfbundle');
120
- fs.writeFileSync(tampered, zlib.gzipSync(text));
121
- await expect(importBundleFile(pools.other, tampered)).rejects.toThrow(BundleError);
122
- });
123
- it("exports one organization's data and imports it into another organization", async () => {
124
- const file = path.join(dir, 'org-a.lfbundle');
125
- const exported = await exportBundleToFile(pools.source, file, { organizationId: ORG_A });
126
- expect(exported.counts).toMatchObject({ organization: 1, link: 1, click: 1, install: 1, event: 1, webhook: 1, template: 1 });
127
- await pools.other.query(`INSERT INTO organizations (id, name) VALUES ($1, 'X')`, [ORG_X]);
128
- const dry = await importBundleFile(pools.other, file, { organizationId: ORG_X, dryRun: true });
129
- expect(dry).toMatchObject({ imported: false, conflicts: [] });
130
- expect((await pools.other.query('SELECT COUNT(*)::int AS n FROM links')).rows[0].n).toBe(0);
131
- const report = await importBundleFile(pools.other, file, { organizationId: ORG_X });
132
- expect(report.imported).toBe(true);
133
- const links = await pools.other.query('SELECT id, organization_id FROM links');
134
- expect(links.rows).toEqual([{ id: LINK_A, organization_id: ORG_X }]);
135
- const orgs = await pools.other.query('SELECT id FROM organizations');
136
- expect(orgs.rows).toEqual([{ id: ORG_X }]);
137
- }, 60000);
138
- });
139
- //# sourceMappingURL=portable.integration.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"portable.integration.test.js","sourceRoot":"","sources":["../../../src/lib/portable/portable.integration.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=registry.schema.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"registry.schema.test.d.ts","sourceRoot":"","sources":["../../../src/lib/portable/registry.schema.test.ts"],"names":[],"mappings":""}
@@ -1,59 +0,0 @@
1
- import pg from 'pg';
2
- import { afterAll, beforeAll, describe, expect, it } from 'vitest';
3
- import { db, initializeDatabase } from '../database.js';
4
- import { BUNDLE_TABLES } from './registry.js';
5
- /**
6
- * Every column Core creates is classified in registry.ts.
7
- *
8
- * Fails when a migration adds a column (or table) the registry doesn't
9
- * classify, or the registry names a column that no longer exists. The fix is
10
- * to decide, in the same change, whether a bundle carries it: add it to
11
- * BUNDLE_TABLES as portable, remap or excluded with a reason.
12
- *
13
- * Needs a real database; skipped without DATABASE_URL, like database.test.ts.
14
- */
15
- const ADMIN_URL = process.env.DATABASE_URL;
16
- describe.skipIf(!ADMIN_URL)('bundle registry covers the schema', () => {
17
- const scratchName = `lf_core_registry_${Date.now()}`;
18
- let admin;
19
- let columns;
20
- beforeAll(async () => {
21
- admin = new pg.Client({ connectionString: ADMIN_URL });
22
- await admin.connect();
23
- await admin.query(`CREATE DATABASE ${scratchName}`);
24
- const url = new URL(ADMIN_URL);
25
- url.pathname = `/${scratchName}`;
26
- await initializeDatabase({ url: url.toString(), pool: { min: 1, max: 2 } });
27
- const result = await db.query(`SELECT table_name, column_name FROM information_schema.columns
28
- WHERE table_schema = 'public'
29
- ORDER BY table_name, ordinal_position`);
30
- columns = new Map();
31
- for (const { table_name, column_name } of result.rows) {
32
- columns.set(table_name, [...(columns.get(table_name) ?? []), column_name]);
33
- }
34
- }, 60000);
35
- afterAll(async () => {
36
- await db?.end().catch(() => undefined);
37
- await admin.query(`DROP DATABASE IF EXISTS ${scratchName} WITH (FORCE)`);
38
- await admin.end();
39
- }, 30000);
40
- it('classifies every table', () => {
41
- const unclassified = [...columns.keys()].filter((table) => !(table in BUNDLE_TABLES));
42
- expect(unclassified, 'add these tables to BUNDLE_TABLES in registry.ts').toEqual([]);
43
- });
44
- it('classifies every column of every table, and only columns that exist', () => {
45
- for (const [table, rule] of Object.entries(BUNDLE_TABLES)) {
46
- const actual = columns.get(table) ?? [];
47
- const classified = Object.keys(rule.columns);
48
- expect(actual.filter((c) => !classified.includes(c)), `classify these ${table} columns in registry.ts`).toEqual([]);
49
- expect(classified.filter((c) => !actual.includes(c)), `these ${table} columns in registry.ts no longer exist`).toEqual([]);
50
- }
51
- });
52
- it('gives every exported column a unique field name per table', () => {
53
- for (const [table, rule] of Object.entries(BUNDLE_TABLES)) {
54
- const fields = Object.values(rule.columns).flatMap((c) => (c.class === 'excluded' ? [] : [c.field]));
55
- expect(new Set(fields).size, table).toBe(fields.length);
56
- }
57
- });
58
- });
59
- //# sourceMappingURL=registry.schema.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"registry.schema.test.js","sourceRoot":"","sources":["../../../src/lib/portable/registry.schema.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=scanner-detection.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"scanner-detection.test.d.ts","sourceRoot":"","sources":["../../src/lib/scanner-detection.test.ts"],"names":[],"mappings":""}
@@ -1,124 +0,0 @@
1
- import { describe, it, expect, afterEach, vi } from 'vitest';
2
- import { mkdtempSync, rmSync, writeFileSync } from 'fs';
3
- import { tmpdir } from 'os';
4
- import { join } from 'path';
5
- import { configureScannerDetection, isEmailScanner, isScannerDetectionEnabled, loadScannerDetection, } from './scanner-detection.js';
6
- import { classifyBot } from './bot-detection.js';
7
- // Documentation ranges (RFC 5737 / RFC 3849) standing in for real networks.
8
- const CLOUD_IP = '192.0.2.10';
9
- const CLOUD_IPV6 = '2001:db8::10';
10
- const RESIDENTIAL_IP = '198.51.100.20';
11
- const UNKNOWN_IP = '203.0.113.30';
12
- const ASNS = {
13
- [CLOUD_IP]: 8075,
14
- [CLOUD_IPV6]: 15169,
15
- [RESIDENTIAL_IP]: 7922,
16
- };
17
- const lookup = (ip) => ASNS[ip] ?? null;
18
- const WINDOWS_CHROME = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36';
19
- const MAC_CHROME = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36';
20
- const LINUX_FIREFOX = 'Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0';
21
- const IPHONE_SAFARI = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7 Mobile/15E148 Safari/604.1';
22
- const ANDROID_CHROME = 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36';
23
- const APP_CFNETWORK = 'ExampleApp/1 CFNetwork/1410.0.3 Darwin/22.6.0';
24
- afterEach(() => {
25
- configureScannerDetection({ lookup: null });
26
- });
27
- describe('isEmailScanner', () => {
28
- it('is off until configured', () => {
29
- expect(isScannerDetectionEnabled()).toBe(false);
30
- expect(isEmailScanner(WINDOWS_CHROME, CLOUD_IP)).toBe(false);
31
- });
32
- it('flags desktop browsers on a scanner network', () => {
33
- configureScannerDetection({ lookup });
34
- expect(isEmailScanner(WINDOWS_CHROME, CLOUD_IP)).toBe(true);
35
- expect(isEmailScanner(MAC_CHROME, CLOUD_IP)).toBe(true);
36
- expect(isEmailScanner(LINUX_FIREFOX, CLOUD_IP)).toBe(true);
37
- expect(isEmailScanner(WINDOWS_CHROME, CLOUD_IPV6)).toBe(true);
38
- });
39
- it('does not flag mobile browsers or app HTTP clients on a scanner network', () => {
40
- configureScannerDetection({ lookup });
41
- expect(isEmailScanner(IPHONE_SAFARI, CLOUD_IP)).toBe(false);
42
- expect(isEmailScanner(ANDROID_CHROME, CLOUD_IP)).toBe(false);
43
- expect(isEmailScanner(APP_CFNETWORK, CLOUD_IP)).toBe(false);
44
- expect(isEmailScanner('okhttp/4.12.0', CLOUD_IP)).toBe(false);
45
- });
46
- it('does not flag desktop browsers on other networks or unknown IPs', () => {
47
- configureScannerDetection({ lookup });
48
- expect(isEmailScanner(WINDOWS_CHROME, RESIDENTIAL_IP)).toBe(false);
49
- expect(isEmailScanner(WINDOWS_CHROME, UNKNOWN_IP)).toBe(false);
50
- expect(isEmailScanner(WINDOWS_CHROME, '')).toBe(false);
51
- expect(isEmailScanner(WINDOWS_CHROME, undefined)).toBe(false);
52
- expect(isEmailScanner(undefined, CLOUD_IP)).toBe(false);
53
- });
54
- it('uses a configured ASN list in place of the defaults', () => {
55
- configureScannerDetection({ lookup, asns: [7922] });
56
- expect(isEmailScanner(WINDOWS_CHROME, RESIDENTIAL_IP)).toBe(true);
57
- expect(isEmailScanner(WINDOWS_CHROME, CLOUD_IP)).toBe(false);
58
- });
59
- it('is off with an empty ASN list', () => {
60
- configureScannerDetection({ lookup, asns: [] });
61
- expect(isScannerDetectionEnabled()).toBe(false);
62
- expect(isEmailScanner(WINDOWS_CHROME, CLOUD_IP)).toBe(false);
63
- });
64
- });
65
- describe('classifyBot with scanner detection', () => {
66
- it('classifies a scanner as email_scanner', () => {
67
- configureScannerDetection({ lookup });
68
- expect(classifyBot(WINDOWS_CHROME, 'GET', undefined, false, CLOUD_IP)).toEqual({
69
- isBot: true,
70
- reason: 'email_scanner',
71
- });
72
- expect(classifyBot(WINDOWS_CHROME, 'GET', undefined, false, RESIDENTIAL_IP)).toEqual({ isBot: false, reason: null });
73
- });
74
- it('keeps the earlier signals ahead of it', () => {
75
- configureScannerDetection({ lookup });
76
- expect(classifyBot(WINDOWS_CHROME, 'GET', true, false, CLOUD_IP).reason).toBe('edge');
77
- expect(classifyBot(WINDOWS_CHROME, 'HEAD', undefined, false, CLOUD_IP).reason).toBe('method');
78
- expect(classifyBot('Googlebot/2.1 (+http://www.google.com/bot.html)', 'GET', undefined, false, CLOUD_IP).reason).toBe('ua');
79
- });
80
- it('does not apply to SDK requests', () => {
81
- configureScannerDetection({ lookup });
82
- expect(classifyBot(WINDOWS_CHROME, 'GET', undefined, true, CLOUD_IP)).toEqual({ isBot: false, reason: null });
83
- });
84
- it('classifies as before when no IP is passed', () => {
85
- configureScannerDetection({ lookup });
86
- expect(classifyBot(WINDOWS_CHROME, 'GET')).toEqual({ isBot: false, reason: null });
87
- });
88
- });
89
- describe('loadScannerDetection', () => {
90
- let dir;
91
- afterEach(() => {
92
- if (dir)
93
- rmSync(dir, { recursive: true, force: true });
94
- dir = undefined;
95
- });
96
- it('stays off without a path, silently', () => {
97
- const log = vi.fn();
98
- expect(loadScannerDetection(undefined, { log })).toBe(false);
99
- expect(isScannerDetectionEnabled()).toBe(false);
100
- expect(log).not.toHaveBeenCalled();
101
- });
102
- it('stays off and logs when the file is missing', () => {
103
- const log = vi.fn();
104
- expect(loadScannerDetection('/nonexistent/GeoLite2-ASN.mmdb', { log })).toBe(false);
105
- expect(isScannerDetectionEnabled()).toBe(false);
106
- expect(log).toHaveBeenCalledOnce();
107
- expect(log.mock.calls[0][0]).toContain('/nonexistent/GeoLite2-ASN.mmdb');
108
- });
109
- it('stays off and logs when the file is not a MaxMind database', () => {
110
- dir = mkdtempSync(join(tmpdir(), 'linkforty-asn-'));
111
- const path = join(dir, 'not-a-db.mmdb');
112
- writeFileSync(path, 'not a database');
113
- const log = vi.fn();
114
- expect(loadScannerDetection(path, { log })).toBe(false);
115
- expect(isScannerDetectionEnabled()).toBe(false);
116
- expect(log).toHaveBeenCalledOnce();
117
- });
118
- it('turns a previously configured rule off when the new file fails', () => {
119
- configureScannerDetection({ lookup });
120
- loadScannerDetection('/nonexistent/GeoLite2-ASN.mmdb', { log: () => { } });
121
- expect(isScannerDetectionEnabled()).toBe(false);
122
- });
123
- });
124
- //# sourceMappingURL=scanner-detection.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"scanner-detection.test.js","sourceRoot":"","sources":["../../src/lib/scanner-detection.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=schema-guard.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"schema-guard.test.d.ts","sourceRoot":"","sources":["../../src/lib/schema-guard.test.ts"],"names":[],"mappings":""}
@@ -1,83 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { LOCK_NOT_AVAILABLE, alreadyApplied, guardSchemaClient, isLockTimeout } from './schema-guard.js';
3
- function fakeClient(catalog) {
4
- const query = vi.fn(async (text, params) => {
5
- if (text.includes('information_schema.columns')) {
6
- const hit = (catalog.columns ?? []).find(([t, c]) => t === params[0] && c === params[1]);
7
- return { rows: hit ? [{ is_nullable: hit[2] }] : [], rowCount: hit ? 1 : 0 };
8
- }
9
- if (text.includes('pg_class c JOIN pg_namespace')) {
10
- return { rows: (catalog.indexes ?? []).includes(params[0]) ? [{}] : [], rowCount: 0 };
11
- }
12
- if (text.includes('pg_constraint')) {
13
- const hit = (catalog.constraints ?? []).some(([n, t]) => n === params[0] && t === params[1]);
14
- return { rows: hit ? [{}] : [], rowCount: 0 };
15
- }
16
- return { rows: [], rowCount: 0, command: 'SENT' };
17
- });
18
- return { query, release: vi.fn() };
19
- }
20
- describe('alreadyApplied', () => {
21
- it('answers an existing column, index, absent constraint and settled NOT NULL from the catalog', async () => {
22
- const c = fakeClient({
23
- columns: [['links', 'append_click_id', 'YES'], ['links', 'warn_at', 'YES'], ['users', 'password_hash', 'YES']],
24
- indexes: ['idx_clicks_link_id'],
25
- constraints: [['present_key', 'events']],
26
- });
27
- expect(await alreadyApplied(c, 'ALTER TABLE links ADD COLUMN IF NOT EXISTS append_click_id BOOLEAN DEFAULT false')).toMatch(/exists/);
28
- expect(await alreadyApplied(c, ' ALTER TABLE "links" ADD COLUMN IF NOT EXISTS "append_click_id" BOOLEAN;\n')).toMatch(/exists/);
29
- expect(await alreadyApplied(c, 'ALTER TABLE public.links ADD COLUMN IF NOT EXISTS warn_at TIMESTAMPTZ')).toMatch(/exists/);
30
- expect(await alreadyApplied(c, 'ALTER TABLE links\n ADD COLUMN IF NOT EXISTS append_click_id BOOLEAN,\n ADD COLUMN IF NOT EXISTS warn_at TIMESTAMPTZ')).toBe('columns links.append_click_id, warn_at exist');
31
- expect(await alreadyApplied(c, 'CREATE INDEX IF NOT EXISTS idx_clicks_link_id ON click_events(link_id)')).toMatch(/exists/);
32
- expect(await alreadyApplied(c, 'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx_clicks_link_id ON click_events(link_id)')).toMatch(/exists/);
33
- expect(await alreadyApplied(c, 'ALTER TABLE events DROP CONSTRAINT IF EXISTS gone_key')).toMatch(/absent/);
34
- expect(await alreadyApplied(c, 'ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL')).toMatch(/already nullable/);
35
- });
36
- it('sends statements whose effect is missing, and anything it does not understand', async () => {
37
- const c = fakeClient({ columns: [['users', 'password_hash', 'NO'], ['links', 'warn_at', 'YES']], constraints: [['present_key', 'events']] });
38
- expect(await alreadyApplied(c, 'ALTER TABLE links ADD COLUMN IF NOT EXISTS brand_new TEXT')).toBeNull();
39
- expect(await alreadyApplied(c, 'ALTER TABLE links ADD COLUMN IF NOT EXISTS warn_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS brand_new INTEGER NOT NULL DEFAULT 0')).toBeNull();
40
- expect(await alreadyApplied(c, 'CREATE INDEX IF NOT EXISTS idx_missing ON links(id)')).toBeNull();
41
- expect(await alreadyApplied(c, 'ALTER TABLE events DROP CONSTRAINT IF EXISTS present_key')).toBeNull();
42
- expect(await alreadyApplied(c, 'ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL')).toBeNull();
43
- expect(await alreadyApplied(c, 'ALTER TABLE users ALTER COLUMN password_hash SET NOT NULL')).toMatch(/already NOT NULL/);
44
- // A comma inside a type or default does not split the action.
45
- expect(await alreadyApplied(c, 'ALTER TABLE links ADD COLUMN IF NOT EXISTS warn_at NUMERIC(18,4) DEFAULT 0')).toMatch(/exist/);
46
- // Shapes that are not idempotent are never guessed at.
47
- expect(await alreadyApplied(c, 'ALTER TABLE links ADD COLUMN brand_new TEXT')).toBeNull();
48
- expect(await alreadyApplied(c, 'ALTER TABLE links ADD COLUMN IF NOT EXISTS warn_at TIMESTAMPTZ, ALTER COLUMN warn_at SET DEFAULT NULL')).toBeNull();
49
- expect(await alreadyApplied(c, 'ALTER TABLE links ALTER COLUMN x SET DEFAULT 1')).toBeNull();
50
- expect(await alreadyApplied(c, 'CREATE TABLE IF NOT EXISTS t (id int)')).toBeNull();
51
- expect(await alreadyApplied(c, "DO $$ BEGIN ALTER TABLE links ADD COLUMN IF NOT EXISTS warn_at TEXT; END $$")).toBeNull();
52
- expect(await alreadyApplied(c, 'ALTER TABLE links ADD COLUMN IF NOT EXISTS a TEXT; ALTER TABLE links ADD COLUMN IF NOT EXISTS b TEXT')).toBeNull();
53
- });
54
- });
55
- describe('guardSchemaClient', () => {
56
- it('skips what exists, passes the rest through, records why, and releases the real client', async () => {
57
- const raw = fakeClient({ columns: [['links', 'warn_at', 'YES']] });
58
- const client = guardSchemaClient(raw);
59
- expect((await client.query('ALTER TABLE links ADD COLUMN IF NOT EXISTS warn_at TIMESTAMPTZ')).command).toBe('SKIP');
60
- expect((await client.query('ALTER TABLE links ADD COLUMN IF NOT EXISTS brand_new TEXT')).command).toBe('SENT');
61
- await client.query('SELECT pg_advisory_lock($1)', [1]);
62
- expect(client.skipped).toEqual(['column links.warn_at exists']);
63
- expect(raw.query.mock.calls.map((c) => String(c[0]).slice(0, 30))).toEqual([
64
- 'SELECT is_nullable FROM inform',
65
- 'SELECT is_nullable FROM inform',
66
- 'ALTER TABLE links ADD COLUMN I',
67
- 'SELECT pg_advisory_lock($1)',
68
- ]);
69
- client.release();
70
- expect(raw.release).toHaveBeenCalledTimes(1);
71
- });
72
- it('never inspects parameterised statements', async () => {
73
- const raw = fakeClient({ columns: [['links', 'x', 'YES']] });
74
- await guardSchemaClient(raw).query('ALTER TABLE links ADD COLUMN IF NOT EXISTS x TEXT', [1]);
75
- expect(raw.query).toHaveBeenCalledTimes(1);
76
- });
77
- it('recognises a lock timeout by SQLSTATE', () => {
78
- expect(isLockTimeout(Object.assign(new Error('x'), { code: LOCK_NOT_AVAILABLE }))).toBe(true);
79
- expect(isLockTimeout(new Error('x'))).toBe(false);
80
- expect(isLockTimeout(null)).toBe(false);
81
- });
82
- });
83
- //# sourceMappingURL=schema-guard.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"schema-guard.test.js","sourceRoot":"","sources":["../../src/lib/schema-guard.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=utils.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../src/lib/utils.test.ts"],"names":[],"mappings":""}