@linkforty/core 1.41.2 → 1.42.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 (47) hide show
  1. package/README.md +81 -1
  2. package/dist/index.d.ts +8 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/config.d.ts +33 -0
  7. package/dist/lib/config.d.ts.map +1 -0
  8. package/dist/lib/config.js +111 -0
  9. package/dist/lib/config.js.map +1 -0
  10. package/dist/lib/config.test.d.ts +2 -0
  11. package/dist/lib/config.test.d.ts.map +1 -0
  12. package/dist/lib/config.test.js +86 -0
  13. package/dist/lib/config.test.js.map +1 -0
  14. package/dist/lib/database.d.ts +15 -0
  15. package/dist/lib/database.d.ts.map +1 -1
  16. package/dist/lib/database.js +36 -2
  17. package/dist/lib/database.js.map +1 -1
  18. package/dist/lib/database.test.js +73 -0
  19. package/dist/lib/database.test.js.map +1 -1
  20. package/dist/lib/not-found-page.d.ts +106 -0
  21. package/dist/lib/not-found-page.d.ts.map +1 -0
  22. package/dist/lib/not-found-page.js +212 -0
  23. package/dist/lib/not-found-page.js.map +1 -0
  24. package/dist/lib/not-found-page.test.d.ts +2 -0
  25. package/dist/lib/not-found-page.test.d.ts.map +1 -0
  26. package/dist/lib/not-found-page.test.js +151 -0
  27. package/dist/lib/not-found-page.test.js.map +1 -0
  28. package/dist/lib/schema-guard.d.ts +46 -0
  29. package/dist/lib/schema-guard.d.ts.map +1 -0
  30. package/dist/lib/schema-guard.js +135 -0
  31. package/dist/lib/schema-guard.js.map +1 -0
  32. package/dist/lib/schema-guard.test.d.ts +2 -0
  33. package/dist/lib/schema-guard.test.d.ts.map +1 -0
  34. package/dist/lib/schema-guard.test.js +83 -0
  35. package/dist/lib/schema-guard.test.js.map +1 -0
  36. package/dist/routes/redirect.d.ts +21 -0
  37. package/dist/routes/redirect.d.ts.map +1 -1
  38. package/dist/routes/redirect.js +72 -7
  39. package/dist/routes/redirect.js.map +1 -1
  40. package/dist/routes/redirect.no-web-destination.test.js +2 -2
  41. package/dist/routes/redirect.no-web-destination.test.js.map +1 -1
  42. package/dist/routes/redirect.not-found.test.d.ts +2 -0
  43. package/dist/routes/redirect.not-found.test.d.ts.map +1 -0
  44. package/dist/routes/redirect.not-found.test.js +280 -0
  45. package/dist/routes/redirect.not-found.test.js.map +1 -0
  46. package/linkforty.config.example.json +11 -0
  47. package/package.json +3 -2
@@ -77,6 +77,79 @@ describe.skipIf(!ADMIN_URL)('initializeDatabase schema lock', () => {
77
77
  }
78
78
  }, 30000);
79
79
  });
80
+ describe.skipIf(!ADMIN_URL)('initialisation against a table someone is reading', () => {
81
+ // Postgres takes a table's lock before it evaluates IF NOT EXISTS, so a
82
+ // second start used to park an ACCESS EXCLUSIVE request on `links` behind
83
+ // any long reader — and every redirect behind that. With the catalog guard
84
+ // nothing is sent for a column that exists, and with a lock timeout a
85
+ // statement that does need the table gives up instead of queueing it.
86
+ const scratchName = `lf_core_schema_guard_${Date.now()}`;
87
+ let admin;
88
+ let scratchUrl;
89
+ const pools = [];
90
+ beforeAll(async () => {
91
+ admin = new pg.Client({ connectionString: ADMIN_URL });
92
+ await admin.connect();
93
+ await admin.query(`CREATE DATABASE ${scratchName}`);
94
+ const url = new URL(ADMIN_URL);
95
+ url.pathname = `/${scratchName}`;
96
+ scratchUrl = url.toString();
97
+ // First start: every table, column and index exists afterwards.
98
+ await initializeDatabase({ url: scratchUrl, pool: { min: 1, max: 2 } });
99
+ pools.push(db);
100
+ }, 60000);
101
+ afterAll(async () => {
102
+ for (const p of pools)
103
+ await p.end().catch(() => undefined);
104
+ await db?.end().catch(() => undefined);
105
+ await admin.query(`DROP DATABASE IF EXISTS ${scratchName} WITH (FORCE)`);
106
+ await admin.end();
107
+ }, 30000);
108
+ it('a second start completes while a transaction holds ACCESS SHARE on links, and the setting does not leak', async () => {
109
+ const reader = new pg.Client({ connectionString: scratchUrl });
110
+ await reader.connect();
111
+ try {
112
+ await reader.query('BEGIN');
113
+ await reader.query('SELECT 1 FROM links LIMIT 1');
114
+ const started = Date.now();
115
+ await initializeDatabase({ url: scratchUrl, pool: { min: 1, max: 2 }, lockTimeoutMs: 2000 });
116
+ pools.push(db);
117
+ // Nothing waited: well under a single lock timeout.
118
+ expect(Date.now() - started).toBeLessThan(10000);
119
+ // The reader was never blocked either.
120
+ const t = Date.now();
121
+ await reader.query('SELECT 1 FROM links LIMIT 1');
122
+ expect(Date.now() - t).toBeLessThan(1000);
123
+ // The timeout was session-level on the initialisation client and was
124
+ // reset before that client went back to the pool.
125
+ const { rows } = await db.query('SHOW lock_timeout');
126
+ expect(rows[0].lock_timeout).toBe('0');
127
+ }
128
+ finally {
129
+ await reader.query('ROLLBACK').catch(() => undefined);
130
+ await reader.end();
131
+ }
132
+ }, 60000);
133
+ it('a statement that does need the table fails with 55P03 after the timeout instead of queueing it', async () => {
134
+ const reader = new pg.Client({ connectionString: scratchUrl });
135
+ const other = new pg.Client({ connectionString: scratchUrl });
136
+ await reader.connect();
137
+ await other.connect();
138
+ try {
139
+ await reader.query('BEGIN');
140
+ await reader.query('SELECT 1 FROM links LIMIT 1');
141
+ await other.query('SET lock_timeout = 1000');
142
+ const t = Date.now();
143
+ await expect(other.query('ALTER TABLE links ADD COLUMN probe_column_not_guarded INTEGER')).rejects.toMatchObject({ code: '55P03' });
144
+ expect(Date.now() - t).toBeLessThan(5000);
145
+ }
146
+ finally {
147
+ await reader.query('ROLLBACK').catch(() => undefined);
148
+ await reader.end();
149
+ await other.end();
150
+ }
151
+ }, 30000);
152
+ });
80
153
  describe.skipIf(!ADMIN_URL)('idle connection errors', () => {
81
154
  const scratchName = `lf_core_idle_error_${Date.now()}`;
82
155
  let admin;
@@ -1 +1 @@
1
- {"version":3,"file":"database.test.js","sourceRoot":"","sources":["../../src/lib/database.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,EAAE,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAE7E;;;;;;;;;;;;;GAaG;AAEH,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAE3C,IAAI,CAAC,SAAS,EAAE,CAAC;IACf,sCAAsC;IACtC,OAAO,CAAC,IAAI,CACV,2DAA2D;QACzD,4EAA4E,CAC/E,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,gCAAgC,EAAE,GAAG,EAAE;IACjE,4EAA4E;IAC5E,0EAA0E;IAC1E,MAAM,WAAW,GAAG,uBAAuB,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IACxD,IAAI,KAAgB,CAAC;IACrB,IAAI,UAAkB,CAAC;IAEvB,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,KAAK,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE,CAAC,CAAC;QACvD,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC;QACtB,MAAM,KAAK,CAAC,KAAK,CAAC,mBAAmB,WAAW,EAAE,CAAC,CAAC;QAEpD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAmB,CAAC,CAAC;QACzC,GAAG,CAAC,QAAQ,GAAG,IAAI,WAAW,EAAE,CAAC;QACjC,UAAU,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC9B,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,QAAQ,CAAC,KAAK,IAAI,EAAE;QAClB,qEAAqE;QACrE,oDAAoD;QACpD,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvC,MAAM,KAAK,CAAC,KAAK,CAAC,2BAA2B,WAAW,eAAe,CAAC,CAAC;QACzE,MAAM,KAAK,CAAC,GAAG,EAAE,CAAC;IACpB,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,uDAAuD;QACvD,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,UAAU,EAAE,CAAC,CAAC;QAC/D,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;QACvB,MAAM,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC;QAE1E,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;YACvF,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC,CAAC,CAAC;QAEH,wEAAwE;QACxE,gCAAgC;QAChC,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QACzD,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE5B,MAAM,MAAM,CAAC,KAAK,CAAC,+BAA+B,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC;QAC5E,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC;QAEnB,MAAM,IAAI,CAAC;QACX,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,wEAAwE;QACxE,uEAAuE;QACvE,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,UAAU,EAAE,CAAC,CAAC;QAC9D,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,6CAA6C,EAAE;gBAChF,oBAAoB;aACrB,CAAC,CAAC;YACH,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpC,MAAM,KAAK,CAAC,KAAK,CAAC,+BAA+B,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC;QAC7E,CAAC;gBAAS,CAAC;YACT,MAAM,KAAK,CAAC,GAAG,EAAE,CAAC;QACpB,CAAC;IACH,CAAC,EAAE,KAAM,CAAC,CAAC;AACb,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACzD,MAAM,WAAW,GAAG,sBAAsB,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IACvD,IAAI,KAAgB,CAAC;IAErB,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,KAAK,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE,CAAC,CAAC;QACvD,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC;QACtB,MAAM,KAAK,CAAC,KAAK,CAAC,mBAAmB,WAAW,EAAE,CAAC,CAAC;IACtD,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,QAAQ,CAAC,KAAK,IAAI,EAAE;QAClB,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvC,MAAM,KAAK,CAAC,KAAK,CAAC,2BAA2B,WAAW,eAAe,CAAC,CAAC;QACzE,MAAM,KAAK,CAAC,GAAG,EAAE,CAAC;IACpB,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,EAAE,CAAC,8FAA8F,EAAE,KAAK,IAAI,EAAE;QAC5G,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAmB,CAAC,CAAC;QACzC,GAAG,CAAC,QAAQ,GAAG,IAAI,WAAW,EAAE,CAAC;QACjC,MAAM,kBAAkB,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5E,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC3B,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAExC,yEAAyE;QACzE,+BAA+B;QAC/B,MAAM,KAAK,CAAC,KAAK,CACf,uGAAuG,EACvG,CAAC,WAAW,CAAC,CACd,CAAC;QACF,wEAAwE;QACxE,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,KAAM,EAAE,CAAC,CAAC;QAE3E,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChE,CAAC,EAAE,KAAM,CAAC,CAAC;AACb,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"database.test.js","sourceRoot":"","sources":["../../src/lib/database.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ /** Why the link couldn't be served. For the host's hook; the page never shows it. */
2
+ export type NotFoundReason =
3
+ /** No such code, an expired or switched-off link, or another organization's link on this host. */
4
+ 'unknown'
5
+ /** Switched off by a decision that isn't about abuse. */
6
+ | 'disabled'
7
+ /** The visitor is outside the link's country, device or language rules. */
8
+ | 'not_targeted'
9
+ /** The link has no destination for this visitor's device. */
10
+ | 'no_destination';
11
+ /** Everything optional: an empty object renders the default page. */
12
+ export interface NotFoundContent {
13
+ title?: string;
14
+ message?: string;
15
+ /** http(s) only; anything else is dropped. */
16
+ logoUrl?: string;
17
+ /** `#rgb`, `#rrggbb` or with alpha; anything else is dropped. */
18
+ accentColor?: string;
19
+ backgroundColor?: string;
20
+ /** The button is shown only when both label and a valid http(s) URL are present. */
21
+ buttonLabel?: string;
22
+ buttonUrl?: string;
23
+ }
24
+ /**
25
+ * Not-found settings, as an organization stores them in `settings.notFound` and
26
+ * as the instance sets them in linkforty.config.json. With `redirectUrl`, browsers
27
+ * are sent there (302) and the rest is ignored; without it, the rest is the page.
28
+ */
29
+ export interface NotFoundSettings extends NotFoundContent {
30
+ /** http(s) only. */
31
+ redirectUrl?: string;
32
+ }
33
+ export type NotFoundResolution = {
34
+ kind: 'page';
35
+ content: NotFoundContent;
36
+ } | {
37
+ kind: 'redirect';
38
+ url: string;
39
+ };
40
+ export interface NotFoundContext {
41
+ reason: NotFoundReason;
42
+ /**
43
+ * The resolved link row, when there is one. Null for `unknown`, including a
44
+ * link that belongs to another organization: that row is not this host's to
45
+ * hand out.
46
+ */
47
+ link: Record<string, any> | null;
48
+ /** The organization the request's host belongs to (lib/link-lookup.ts), if any. */
49
+ scope: string | null;
50
+ }
51
+ export declare const NOT_FOUND_DEFAULTS: {
52
+ readonly title: "Link not found";
53
+ readonly message: "This link doesn't exist or is no longer available.";
54
+ };
55
+ /** JSON bodies kept for clients that don't accept HTML. */
56
+ export declare const NOT_FOUND_JSON: {
57
+ readonly default: {
58
+ readonly error: "Link not found";
59
+ };
60
+ readonly no_destination: {
61
+ readonly error: "No destination URL configured for this link";
62
+ };
63
+ };
64
+ /** True when the request will render HTML — a browser navigation. */
65
+ export declare function wantsHtml(accept: string | string[] | undefined): boolean;
66
+ /**
67
+ * The page's content-security policy. It runs no script at all, so none is
68
+ * allowed; images may come from any https origin because the logo is the host's.
69
+ */
70
+ export declare function notFoundContentSecurityPolicy(): string;
71
+ /** A redirect target from the host's hook, or null when it isn't http(s). */
72
+ export declare function safeNotFoundRedirect(url: unknown): string | null;
73
+ /**
74
+ * Settings from stored JSON (an organization's `settings`): keeps the known
75
+ * string fields and returns null when none is set. Lenient on purpose, since a
76
+ * bad value in the database must not break a redirect; values are checked
77
+ * again where they are used. The config file is validated strictly instead
78
+ * (lib/config.ts).
79
+ */
80
+ export declare function readNotFoundSettings(raw: unknown): NotFoundSettings | null;
81
+ /** An organization's not-found settings, from its `settings` column. */
82
+ export declare function readOrganizationNotFoundSettings(orgSettings: unknown): NotFoundSettings | null;
83
+ /** How long an organization's settings are reused before being read again. */
84
+ export declare const NOT_FOUND_SETTINGS_TTL_MS = 60000;
85
+ /**
86
+ * Read an organization's not-found settings, reusing each answer for a minute.
87
+ *
88
+ * Only an unknown code on an organization's host needs this: every other case
89
+ * already has the link row, which carries its organization's settings. Unknown
90
+ * codes are what scanners hit, so the answer is held rather than read per
91
+ * request. A change to the settings shows within NOT_FOUND_SETTINGS_TTL_MS.
92
+ *
93
+ * A factory rather than module state, so each registration (and each test) has
94
+ * its own. A failed read resolves to null and is not held.
95
+ */
96
+ export declare function createOrganizationNotFoundSettingsLookup(deps: {
97
+ query: (sql: string, params: unknown[]) => Promise<{
98
+ rows: Array<Record<string, unknown>>;
99
+ }>;
100
+ onError?: (error: unknown) => void;
101
+ now?: () => number;
102
+ }): (organizationId: string) => Promise<NotFoundSettings | null>;
103
+ /** What settings ask for: a redirect when they name a valid URL, otherwise the page. */
104
+ export declare function resolveNotFoundSettings(settings: NotFoundSettings): NotFoundResolution;
105
+ export declare function renderNotFoundPage(content?: NotFoundContent): string;
106
+ //# sourceMappingURL=not-found-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"not-found-page.d.ts","sourceRoot":"","sources":["../../src/lib/not-found-page.ts"],"names":[],"mappings":"AAkCA,qFAAqF;AACrF,MAAM,MAAM,cAAc;AACxB,kGAAkG;AAChG,SAAS;AACX,yDAAyD;GACvD,UAAU;AACZ,2EAA2E;GACzE,cAAc;AAChB,6DAA6D;GAC3D,gBAAgB,CAAC;AAErB,qEAAqE;AACrE,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,oBAAoB;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACjC,mFAAmF;IACnF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,kBAAkB;;;CAGrB,CAAC;AAEX,2DAA2D;AAC3D,eAAO,MAAM,cAAc;;;;;;;CAGjB,CAAC;AAOX,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAGxE;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,IAAI,MAAM,CAQtD;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEhE;AAuBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAQ1E;AAED,wEAAwE;AACxE,wBAAgB,gCAAgC,CAAC,WAAW,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAG9F;AAED,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAGhD;;;;;;;;;;GAUG;AACH,wBAAgB,wCAAwC,CAAC,IAAI,EAAE;IAC7D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;KAAE,CAAC,CAAC;IAC7F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,GAAG,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAiB/D;AAED,wFAAwF;AACxF,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,CAKtF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,GAAG,MAAM,CA4DxE"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The page a browser gets when a short link can't be served.
3
+ *
4
+ * Several different things end here: a code that never existed, one that expired
5
+ * or was switched off, a link on another organization's host, a visitor outside
6
+ * the link's targeting rules, and a link with nowhere to send this device. They
7
+ * all render the SAME page. An expired or switched-off link must not be told
8
+ * apart from a code that never existed (see the safety gate in
9
+ * routes/redirect.ts), and a page that varied its copy by reason would do exactly
10
+ * that. The reason decides whose settings apply, never what the page says.
11
+ *
12
+ * What the page says, or where the visitor is sent instead, comes from the first
13
+ * of these that answers:
14
+ *
15
+ * 1. the `notFound` hook, for a deployment that needs logic;
16
+ * 2. the organization's `settings.notFound`, read by Core itself;
17
+ * 3. the instance's defaults: the `notFound` section of linkforty.config.json
18
+ * (lib/config.ts), passed to the routes as `notFoundDefaults`;
19
+ * 4. the built-in page.
20
+ *
21
+ * The organization is the one the request's host belongs to. Failing that, for a
22
+ * live link that just has nothing for this visitor (a targeting miss, no
23
+ * destination), it is the link's own organization. An unknown, expired or
24
+ * switched-off code on a shared host has no organization to speak for it and gets
25
+ * the instance's page.
26
+ *
27
+ * A link withdrawn for abuse is not a not-found: it keeps its own 410 notice
28
+ * (generateBlockedLinkHTML in link-safety.ts) and never reaches this module.
29
+ *
30
+ * Clients that don't ask for HTML — API callers, scripts, health checks — keep
31
+ * the JSON body they have always had.
32
+ */
33
+ import { escapeHtml, safeHref } from './link-safety.js';
34
+ export const NOT_FOUND_DEFAULTS = {
35
+ title: 'Link not found',
36
+ message: "This link doesn't exist or is no longer available.",
37
+ };
38
+ /** JSON bodies kept for clients that don't accept HTML. */
39
+ export const NOT_FOUND_JSON = {
40
+ default: { error: 'Link not found' },
41
+ no_destination: { error: 'No destination URL configured for this link' },
42
+ };
43
+ const TITLE_MAX = 120;
44
+ const MESSAGE_MAX = 500;
45
+ const BUTTON_LABEL_MAX = 60;
46
+ const HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
47
+ /** True when the request will render HTML — a browser navigation. */
48
+ export function wantsHtml(accept) {
49
+ const value = Array.isArray(accept) ? accept.join(',') : accept ?? '';
50
+ return value.toLowerCase().includes('text/html');
51
+ }
52
+ /**
53
+ * The page's content-security policy. It runs no script at all, so none is
54
+ * allowed; images may come from any https origin because the logo is the host's.
55
+ */
56
+ export function notFoundContentSecurityPolicy() {
57
+ return [
58
+ "default-src 'none'",
59
+ "img-src https: data:",
60
+ "style-src 'unsafe-inline'",
61
+ "base-uri 'none'",
62
+ "form-action 'none'",
63
+ ].join('; ');
64
+ }
65
+ /** A redirect target from the host's hook, or null when it isn't http(s). */
66
+ export function safeNotFoundRedirect(url) {
67
+ return typeof url === 'string' ? safeHref(url) : null;
68
+ }
69
+ function text(value, max) {
70
+ if (typeof value !== 'string')
71
+ return null;
72
+ const trimmed = value.trim();
73
+ return trimmed ? trimmed.slice(0, max) : null;
74
+ }
75
+ function color(value) {
76
+ return typeof value === 'string' && HEX_COLOR.test(value.trim()) ? value.trim() : null;
77
+ }
78
+ const SETTING_KEYS = [
79
+ 'redirectUrl',
80
+ 'title',
81
+ 'message',
82
+ 'logoUrl',
83
+ 'accentColor',
84
+ 'backgroundColor',
85
+ 'buttonLabel',
86
+ 'buttonUrl',
87
+ ];
88
+ /**
89
+ * Settings from stored JSON (an organization's `settings`): keeps the known
90
+ * string fields and returns null when none is set. Lenient on purpose, since a
91
+ * bad value in the database must not break a redirect; values are checked
92
+ * again where they are used. The config file is validated strictly instead
93
+ * (lib/config.ts).
94
+ */
95
+ export function readNotFoundSettings(raw) {
96
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
97
+ return null;
98
+ const settings = {};
99
+ for (const key of SETTING_KEYS) {
100
+ const value = raw[key];
101
+ if (typeof value === 'string' && value.trim())
102
+ settings[key] = value.trim();
103
+ }
104
+ return Object.keys(settings).length > 0 ? settings : null;
105
+ }
106
+ /** An organization's not-found settings, from its `settings` column. */
107
+ export function readOrganizationNotFoundSettings(orgSettings) {
108
+ if (!orgSettings || typeof orgSettings !== 'object')
109
+ return null;
110
+ return readNotFoundSettings(orgSettings.notFound);
111
+ }
112
+ /** How long an organization's settings are reused before being read again. */
113
+ export const NOT_FOUND_SETTINGS_TTL_MS = 60000;
114
+ const NOT_FOUND_SETTINGS_MAX_ENTRIES = 1000;
115
+ /**
116
+ * Read an organization's not-found settings, reusing each answer for a minute.
117
+ *
118
+ * Only an unknown code on an organization's host needs this: every other case
119
+ * already has the link row, which carries its organization's settings. Unknown
120
+ * codes are what scanners hit, so the answer is held rather than read per
121
+ * request. A change to the settings shows within NOT_FOUND_SETTINGS_TTL_MS.
122
+ *
123
+ * A factory rather than module state, so each registration (and each test) has
124
+ * its own. A failed read resolves to null and is not held.
125
+ */
126
+ export function createOrganizationNotFoundSettingsLookup(deps) {
127
+ const held = new Map();
128
+ const now = deps.now ?? Date.now;
129
+ return async (organizationId) => {
130
+ const hit = held.get(organizationId);
131
+ if (hit && hit.until > now())
132
+ return hit.settings;
133
+ try {
134
+ const result = await deps.query('SELECT settings FROM organizations WHERE id = $1', [organizationId]);
135
+ const settings = readOrganizationNotFoundSettings(result.rows[0]?.settings);
136
+ if (held.size >= NOT_FOUND_SETTINGS_MAX_ENTRIES)
137
+ held.clear();
138
+ held.set(organizationId, { settings, until: now() + NOT_FOUND_SETTINGS_TTL_MS });
139
+ return settings;
140
+ }
141
+ catch (error) {
142
+ deps.onError?.(error);
143
+ return null;
144
+ }
145
+ };
146
+ }
147
+ /** What settings ask for: a redirect when they name a valid URL, otherwise the page. */
148
+ export function resolveNotFoundSettings(settings) {
149
+ const target = settings.redirectUrl ? safeNotFoundRedirect(settings.redirectUrl) : null;
150
+ if (target)
151
+ return { kind: 'redirect', url: target };
152
+ const { redirectUrl: _ignored, ...content } = settings;
153
+ return { kind: 'page', content };
154
+ }
155
+ export function renderNotFoundPage(content = {}) {
156
+ const title = escapeHtml(text(content.title, TITLE_MAX) ?? NOT_FOUND_DEFAULTS.title);
157
+ const message = escapeHtml(text(content.message, MESSAGE_MAX) ?? NOT_FOUND_DEFAULTS.message);
158
+ const logoUrl = typeof content.logoUrl === 'string' ? safeHref(content.logoUrl.trim()) : null;
159
+ const buttonUrl = typeof content.buttonUrl === 'string' ? safeHref(content.buttonUrl.trim()) : null;
160
+ const buttonLabel = text(content.buttonLabel, BUTTON_LABEL_MAX);
161
+ const accent = color(content.accentColor);
162
+ const background = color(content.backgroundColor);
163
+ // Written after the dark-mode block so the accent wins in both schemes. The
164
+ // background applies in light mode only: a brand background is almost always
165
+ // light, and behind the dark-mode card it would show as a bright frame.
166
+ const brandStyles = [
167
+ accent ? `.button { background:${accent}; color:#fff; }` : '',
168
+ background ? `@media (prefers-color-scheme: light) { body { background:${background}; } }` : '',
169
+ ].join('\n ');
170
+ const logo = logoUrl
171
+ ? `<img class="logo" src="${escapeHtml(logoUrl)}" alt="" referrerpolicy="no-referrer">\n `
172
+ : '';
173
+ const button = buttonUrl && buttonLabel
174
+ ? `\n <a class="button" href="${escapeHtml(buttonUrl)}" rel="noopener noreferrer">${escapeHtml(buttonLabel)}</a>`
175
+ : '';
176
+ return `<!doctype html>
177
+ <html lang="en">
178
+ <head>
179
+ <meta charset="utf-8">
180
+ <meta name="viewport" content="width=device-width,initial-scale=1">
181
+ <meta name="robots" content="noindex,nofollow">
182
+ <title>${title}</title>
183
+ <style>
184
+ :root { color-scheme: light dark; }
185
+ body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
186
+ background:#f6f7f8; color:#16191d; padding:24px; box-sizing:border-box;
187
+ font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }
188
+ .card { max-width:30rem; width:100%; background:#fff; border:1px solid #e3e6ea;
189
+ border-radius:10px; padding:28px; text-align:center; box-sizing:border-box; }
190
+ .logo { display:block; max-height:48px; max-width:60%; margin:0 auto 18px; }
191
+ h1 { margin:0 0 10px; font-size:1.25rem; line-height:1.3; }
192
+ p { margin:0; color:#5b636d; }
193
+ .button { display:inline-block; margin-top:20px; padding:10px 18px; border-radius:8px;
194
+ background:#16191d; color:#fff; text-decoration:none; font-weight:600; }
195
+ @media (prefers-color-scheme: dark) {
196
+ body { background:#14171a; color:#e8eaed; }
197
+ .card { background:#1d2126; border-color:#2c3238; }
198
+ p { color:#98a1ab; }
199
+ .button { background:#e8eaed; color:#14171a; }
200
+ }
201
+ ${brandStyles}
202
+ </style>
203
+ </head>
204
+ <body>
205
+ <main class="card">
206
+ ${logo}<h1>${title}</h1>
207
+ <p>${message}</p>${button}
208
+ </main>
209
+ </body>
210
+ </html>`;
211
+ }
212
+ //# sourceMappingURL=not-found-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"not-found-page.js","sourceRoot":"","sources":["../../src/lib/not-found-page.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=not-found-page.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"not-found-page.test.d.ts","sourceRoot":"","sources":["../../src/lib/not-found-page.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { NOT_FOUND_DEFAULTS, NOT_FOUND_SETTINGS_TTL_MS, createOrganizationNotFoundSettingsLookup, readNotFoundSettings, readOrganizationNotFoundSettings, resolveNotFoundSettings, notFoundContentSecurityPolicy, renderNotFoundPage, safeNotFoundRedirect, wantsHtml, } from './not-found-page.js';
3
+ describe('wantsHtml', () => {
4
+ it('is true for a browser navigation', () => {
5
+ expect(wantsHtml('text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')).toBe(true);
6
+ });
7
+ it('is false for JSON clients, wildcards and a missing header', () => {
8
+ expect(wantsHtml('application/json')).toBe(false);
9
+ expect(wantsHtml('*/*')).toBe(false);
10
+ expect(wantsHtml(undefined)).toBe(false);
11
+ });
12
+ it('reads a repeated header', () => {
13
+ expect(wantsHtml(['application/json', 'text/html'])).toBe(true);
14
+ });
15
+ });
16
+ describe('renderNotFoundPage', () => {
17
+ it('renders the default copy with no content', () => {
18
+ const html = renderNotFoundPage();
19
+ expect(html).toContain(`<title>${NOT_FOUND_DEFAULTS.title}</title>`);
20
+ expect(html).toContain('doesn&#39;t exist or is no longer available.');
21
+ expect(html).not.toContain('<a ');
22
+ expect(html).not.toContain('<img');
23
+ });
24
+ it('runs no script', () => {
25
+ expect(renderNotFoundPage({ title: 'x' })).not.toContain('<script');
26
+ expect(notFoundContentSecurityPolicy()).not.toContain('script-src');
27
+ expect(notFoundContentSecurityPolicy()).toContain("default-src 'none'");
28
+ });
29
+ it('renders host content', () => {
30
+ const html = renderNotFoundPage({
31
+ title: 'Nothing here',
32
+ message: 'Try our homepage.',
33
+ logoUrl: 'https://cdn.example.com/logo.png',
34
+ accentColor: '#0d9488',
35
+ backgroundColor: '#fafafa',
36
+ buttonLabel: 'Go home',
37
+ buttonUrl: 'https://example.com/',
38
+ });
39
+ expect(html).toContain('<h1>Nothing here</h1>');
40
+ expect(html).toContain('<p>Try our homepage.</p>');
41
+ expect(html).toContain('src="https://cdn.example.com/logo.png"');
42
+ expect(html).toContain('href="https://example.com/"');
43
+ expect(html).toContain('>Go home</a>');
44
+ expect(html).toContain('background:#0d9488');
45
+ expect(html).toContain('background:#fafafa');
46
+ });
47
+ it('applies the brand background in light mode only', () => {
48
+ const html = renderNotFoundPage({ backgroundColor: '#ecfdf5' });
49
+ expect(html).toContain('@media (prefers-color-scheme: light) { body { background:#ecfdf5; } }');
50
+ expect(html.match(/background:#ecfdf5/g)).toHaveLength(1);
51
+ });
52
+ it('escapes host copy', () => {
53
+ const html = renderNotFoundPage({ title: '<script>alert(1)</script>', message: '"><img src=x>' });
54
+ expect(html).not.toContain('<script>alert(1)');
55
+ expect(html).toContain('&lt;script&gt;alert(1)&lt;/script&gt;');
56
+ expect(html).not.toContain('"><img src=x>');
57
+ });
58
+ it('drops non-http URLs', () => {
59
+ const html = renderNotFoundPage({
60
+ logoUrl: 'javascript:alert(1)',
61
+ buttonLabel: 'Click',
62
+ buttonUrl: 'javascript:alert(1)',
63
+ });
64
+ expect(html).not.toContain('javascript:');
65
+ expect(html).not.toContain('<a ');
66
+ expect(html).not.toContain('<img');
67
+ });
68
+ it('needs both a label and a URL for the button', () => {
69
+ expect(renderNotFoundPage({ buttonUrl: 'https://example.com/' })).not.toContain('<a ');
70
+ expect(renderNotFoundPage({ buttonLabel: 'Go' })).not.toContain('<a ');
71
+ });
72
+ it('drops colors that are not hex', () => {
73
+ const html = renderNotFoundPage({ accentColor: 'red;}body{display:none', backgroundColor: 'url(x)' });
74
+ expect(html).not.toContain('display:none');
75
+ expect(html).not.toContain('url(x)');
76
+ });
77
+ it('treats blank copy as absent', () => {
78
+ expect(renderNotFoundPage({ title: ' ' })).toContain(`<h1>${NOT_FOUND_DEFAULTS.title}</h1>`);
79
+ });
80
+ it('caps long copy', () => {
81
+ const html = renderNotFoundPage({ title: 'x'.repeat(1000) });
82
+ expect(html).not.toContain('x'.repeat(121));
83
+ });
84
+ });
85
+ describe('safeNotFoundRedirect', () => {
86
+ it('accepts http(s)', () => {
87
+ expect(safeNotFoundRedirect('https://example.com/a?b=1')).toBe('https://example.com/a?b=1');
88
+ });
89
+ it('rejects everything else', () => {
90
+ expect(safeNotFoundRedirect('javascript:alert(1)')).toBeNull();
91
+ expect(safeNotFoundRedirect('/relative')).toBeNull();
92
+ expect(safeNotFoundRedirect(undefined)).toBeNull();
93
+ });
94
+ });
95
+ describe('readNotFoundSettings', () => {
96
+ it('keeps known, non-empty string fields', () => {
97
+ expect(readNotFoundSettings({ title: ' Hi ', message: '', logoUrl: 42, extra: 'x', redirectUrl: 'https://a.example' }))
98
+ .toEqual({ title: 'Hi', redirectUrl: 'https://a.example' });
99
+ });
100
+ it('is null when nothing is set', () => {
101
+ expect(readNotFoundSettings({})).toBeNull();
102
+ expect(readNotFoundSettings(null)).toBeNull();
103
+ expect(readNotFoundSettings(['title'])).toBeNull();
104
+ });
105
+ it('reads an organization settings column', () => {
106
+ expect(readOrganizationNotFoundSettings({ notFound: { title: 'Org' }, launchpad: {} })).toEqual({ title: 'Org' });
107
+ expect(readOrganizationNotFoundSettings({ launchpad: {} })).toBeNull();
108
+ });
109
+ });
110
+ describe('resolveNotFoundSettings', () => {
111
+ it('redirects when the settings name a valid URL', () => {
112
+ expect(resolveNotFoundSettings({ redirectUrl: 'https://example.com/', title: 'x' }))
113
+ .toEqual({ kind: 'redirect', url: 'https://example.com/' });
114
+ });
115
+ it('renders the page when the redirect is not http(s)', () => {
116
+ expect(resolveNotFoundSettings({ redirectUrl: 'javascript:alert(1)', title: 'x' }))
117
+ .toEqual({ kind: 'page', content: { title: 'x' } });
118
+ });
119
+ });
120
+ describe('createOrganizationNotFoundSettingsLookup', () => {
121
+ it('holds an answer for the TTL, then reads again', async () => {
122
+ let clock = 0;
123
+ const query = vi.fn(async () => ({ rows: [{ settings: { notFound: { title: 'Org' } } }] }));
124
+ const lookup = createOrganizationNotFoundSettingsLookup({ query, now: () => clock });
125
+ expect(await lookup('org-a')).toEqual({ title: 'Org' });
126
+ await lookup('org-a');
127
+ expect(query).toHaveBeenCalledTimes(1);
128
+ clock += NOT_FOUND_SETTINGS_TTL_MS + 1;
129
+ await lookup('org-a');
130
+ expect(query).toHaveBeenCalledTimes(2);
131
+ });
132
+ it('holds a "no settings" answer too', async () => {
133
+ const query = vi.fn(async () => ({ rows: [] }));
134
+ const lookup = createOrganizationNotFoundSettingsLookup({ query });
135
+ expect(await lookup('org-a')).toBeNull();
136
+ await lookup('org-a');
137
+ expect(query).toHaveBeenCalledTimes(1);
138
+ });
139
+ it('does not hold a failed read', async () => {
140
+ const onError = vi.fn();
141
+ const query = vi
142
+ .fn()
143
+ .mockRejectedValueOnce(new Error('db down'))
144
+ .mockResolvedValueOnce({ rows: [{ settings: { notFound: { title: 'Org' } } }] });
145
+ const lookup = createOrganizationNotFoundSettingsLookup({ query, onError });
146
+ expect(await lookup('org-a')).toBeNull();
147
+ expect(onError).toHaveBeenCalledOnce();
148
+ expect(await lookup('org-a')).toEqual({ title: 'Org' });
149
+ });
150
+ });
151
+ //# sourceMappingURL=not-found-page.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"not-found-page.test.js","sourceRoot":"","sources":["../../src/lib/not-found-page.test.ts"],"names":[],"mappings":"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"}