@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 +0,0 @@
1
- {"version":3,"file":"link-resolution-cache.test.js","sourceRoot":"","sources":["../../src/lib/link-resolution-cache.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=link-safety.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"link-safety.test.d.ts","sourceRoot":"","sources":["../../src/lib/link-safety.test.ts"],"names":[],"mappings":""}
@@ -1,211 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { evaluateLinkSafety, generateWarningLinkHTML, escapeHtml, safeHref, evaluateLinkSafetyDecision, blockCauseIsAbuse, generateBlockedLinkHTML, } from './link-safety.js';
3
- describe('evaluateLinkSafety', () => {
4
- it('allows a plain healthy link', () => {
5
- expect(evaluateLinkSafety({ isActive: true })).toBe('allow');
6
- expect(evaluateLinkSafety({})).toBe('allow');
7
- });
8
- it('treats an absent isActive as active, so existing rows are unaffected', () => {
9
- expect(evaluateLinkSafety({ isActive: undefined })).toBe('allow');
10
- expect(evaluateLinkSafety({ isActive: null })).toBe('allow');
11
- });
12
- it('blocks an inactive link', () => {
13
- expect(evaluateLinkSafety({ isActive: false })).toBe('block');
14
- });
15
- it('warns when warn_at is set', () => {
16
- expect(evaluateLinkSafety({ isActive: true, warnAt: new Date() })).toBe('warn');
17
- expect(evaluateLinkSafety({ isActive: true, warnAt: '2026-08-10T00:00:00Z' })).toBe('warn');
18
- });
19
- it('blocks when the owner is restricted, even if the link itself is fine', () => {
20
- expect(evaluateLinkSafety({ isActive: true, ownerSuspendedAt: new Date() })).toBe('block');
21
- });
22
- it('prefers block over warn — a restricted owner outranks a mere warning', () => {
23
- expect(evaluateLinkSafety({ isActive: true, warnAt: new Date(), ownerSuspendedAt: new Date() })).toBe('block');
24
- });
25
- it('ignores owner restriction when it is not modelled at all', () => {
26
- // Deployments without an owner table never pass the field.
27
- expect(evaluateLinkSafety({ isActive: true, ownerSuspendedAt: null })).toBe('allow');
28
- expect(evaluateLinkSafety({ isActive: true, ownerSuspendedAt: undefined })).toBe('allow');
29
- });
30
- });
31
- describe('escapeHtml', () => {
32
- it('neutralises markup and quote characters', () => {
33
- expect(escapeHtml('<script>alert("x")</script>')).toBe('&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;');
34
- expect(escapeHtml("it's & more")).toBe('it&#39;s &amp; more');
35
- });
36
- });
37
- describe('generateWarningLinkHTML', () => {
38
- it('shows the destination so the visitor can judge it', () => {
39
- const html = generateWarningLinkHTML('https://example.com/login');
40
- expect(html).toContain('https://example.com/login');
41
- expect(html).toContain('Check this link before continuing');
42
- });
43
- it('escapes a hostile destination rather than injecting it', () => {
44
- const html = generateWarningLinkHTML('https://evil.test/"><script>alert(1)</script>');
45
- expect(html).not.toContain('<script>alert(1)</script>');
46
- expect(html).toContain('&lt;script&gt;');
47
- });
48
- it('asks search engines not to index it', () => {
49
- expect(generateWarningLinkHTML('https://example.com')).toContain('noindex');
50
- });
51
- it('carries no JavaScript of its own, so it survives a strict CSP', () => {
52
- const html = generateWarningLinkHTML('https://example.com');
53
- expect(html).not.toContain('<script');
54
- expect(html).not.toMatch(/\son[a-z]+=/i);
55
- });
56
- // The CSP case above used a benign destination, so it could never have failed for
57
- // the reason it appeared to cover. These pass the hostile input instead.
58
- describe('hostile destinations', () => {
59
- it('never emits a javascript: href', () => {
60
- const html = generateWarningLinkHTML('javascript:alert(document.domain)');
61
- expect(html).not.toMatch(/href="javascript:/i);
62
- });
63
- it('never emits a data: href', () => {
64
- const html = generateWarningLinkHTML('data:text/html,<script>alert(1)</script>');
65
- expect(html).not.toMatch(/href="data:/i);
66
- });
67
- it('offers no continue button when the scheme is not http(s)', () => {
68
- const html = generateWarningLinkHTML('javascript:alert(1)');
69
- expect(html).not.toContain('class="go"');
70
- expect(html).toContain('nothing to continue to');
71
- });
72
- it('still shows the destination as inert text so the visitor can see it', () => {
73
- const html = generateWarningLinkHTML('javascript:alert(1)');
74
- expect(html).toContain('javascript:alert(1)');
75
- });
76
- it('emits no empty href for a bare path or an absent destination', () => {
77
- for (const d of ['/deep/link/path', '']) {
78
- const html = generateWarningLinkHTML(d);
79
- expect(html).not.toContain('href=""');
80
- expect(html).not.toContain('class="go"');
81
- }
82
- });
83
- it('still links a legitimate http(s) destination', () => {
84
- const html = generateWarningLinkHTML('https://example.com/x');
85
- expect(html).toContain('class="go"');
86
- expect(html).toContain('href="https://example.com/x"');
87
- });
88
- });
89
- it('marks the outbound link nofollow/noopener so we pass no reputation to it', () => {
90
- const html = generateWarningLinkHTML('https://example.com');
91
- expect(html).toContain('rel="nofollow noopener noreferrer"');
92
- });
93
- it('includes a report link only when one is configured', () => {
94
- expect(generateWarningLinkHTML('https://example.com')).not.toContain('Report this link');
95
- const withReport = generateWarningLinkHTML('https://example.com', {
96
- reportUrl: 'https://example.org/abuse',
97
- });
98
- expect(withReport).toContain('Report this link');
99
- expect(withReport).toContain('https://example.org/abuse');
100
- });
101
- });
102
- describe('safeHref', () => {
103
- it('allows http and https', () => {
104
- expect(safeHref('http://example.com')).toBe('http://example.com');
105
- expect(safeHref('https://example.com/a?b=c')).toBe('https://example.com/a?b=c');
106
- });
107
- it('rejects every other scheme', () => {
108
- for (const d of ['javascript:alert(1)', 'data:text/html,x', 'file:///etc/passwd', 'vbscript:x']) {
109
- expect(safeHref(d), d).toBeNull();
110
- }
111
- });
112
- it('rejects a relative path, which would point at the redirect host', () => {
113
- expect(safeHref('/deep/link/path')).toBeNull();
114
- expect(safeHref('')).toBeNull();
115
- });
116
- });
117
- describe('evaluateLinkSafetyDecision — why a link was blocked', () => {
118
- const T = '2026-08-10T00:00:00Z';
119
- it('reports owner restriction as an abuse cause', () => {
120
- const d = evaluateLinkSafetyDecision({ ownerSuspendedAt: T });
121
- expect(d).toEqual({ outcome: 'block', cause: 'owner_suspended' });
122
- expect(blockCauseIsAbuse(d.cause)).toBe(true);
123
- });
124
- it('reports an explicit disable as an abuse cause', () => {
125
- const d = evaluateLinkSafetyDecision({ isActive: false, disabledAt: T });
126
- expect(d).toEqual({ outcome: 'block', cause: 'disabled' });
127
- expect(blockCauseIsAbuse(d.cause)).toBe(true);
128
- });
129
- /**
130
- * The distinction the whole notice rests on. An expiry sweep and a person
131
- * switching their own link off both land here, and neither of their visitors was
132
- * targeted by anything.
133
- */
134
- it('reports a merely inactive link as NOT an abuse cause', () => {
135
- const d = evaluateLinkSafetyDecision({ isActive: false });
136
- expect(d).toEqual({ outcome: 'block', cause: 'inactive' });
137
- expect(blockCauseIsAbuse(d.cause)).toBe(false);
138
- });
139
- /**
140
- * Ordering guard. An abuse disable sets `isActive: false` as well, so testing
141
- * `isActive` first would collapse every abuse disable into `inactive` and the
142
- * notice would never be served to anyone. Every route test would still pass on
143
- * the owner-restriction path, so this is the only place that catches it.
144
- */
145
- it('keeps the disable cause even though the link is also inactive', () => {
146
- expect(evaluateLinkSafetyDecision({ isActive: false, disabledAt: T }).cause).toBe('disabled');
147
- });
148
- it('lets owner restriction outrank an explicit disable', () => {
149
- expect(evaluateLinkSafetyDecision({ ownerSuspendedAt: T, isActive: false, disabledAt: T }).cause).toBe('owner_suspended');
150
- });
151
- it('carries no cause when the link is fine or only warned', () => {
152
- expect(evaluateLinkSafetyDecision({})).toEqual({ outcome: 'allow' });
153
- expect(evaluateLinkSafetyDecision({ warnAt: T })).toEqual({ outcome: 'warn' });
154
- });
155
- it('agrees with evaluateLinkSafety on every outcome', () => {
156
- for (const input of [
157
- {},
158
- { warnAt: T },
159
- { isActive: false },
160
- { disabledAt: T },
161
- { ownerSuspendedAt: T },
162
- { warnAt: T, ownerSuspendedAt: T },
163
- ]) {
164
- expect(evaluateLinkSafety(input)).toBe(evaluateLinkSafetyDecision(input).outcome);
165
- }
166
- });
167
- });
168
- describe('generateBlockedLinkHTML', () => {
169
- const html = generateBlockedLinkHTML();
170
- it('says only that the link is gone', () => {
171
- expect(html).toContain('This link is no longer available');
172
- expect(html).toContain('It was removed and no longer goes anywhere.');
173
- });
174
- /**
175
- * The guard on the decision, not on the wording.
176
- *
177
- * The page must not characterise the link, its destination, or whoever made it.
178
- * An earlier draft explained that the link had been reported for trying to obtain
179
- * passwords and payment details — useful to a victim, but it is an accusation
180
- * about a user's content published on a domain we serve, and it would be false
181
- * the first time a link is disabled in error. If any of these words come back,
182
- * that decision is being reversed and it should be a deliberate edit here first.
183
- */
184
- it('makes no claim about the link or whoever created it', () => {
185
- for (const word of [
186
- 'phishing', 'fraud', 'fraudulent', 'scam', 'malicious', 'malware', 'abuse',
187
- 'reported', 'trick', 'steal', 'suspended', 'restricted', 'violat', 'blocked',
188
- 'password', 'bank', 'card details',
189
- ]) {
190
- expect(html.toLowerCase(), `page must not mention "${word}"`).not.toContain(word);
191
- }
192
- });
193
- it('contains no link out at all', () => {
194
- expect(html).not.toMatch(/<a\s/i);
195
- });
196
- it('contains no script and no external asset', () => {
197
- expect(html).not.toMatch(/<script/i);
198
- expect(html).not.toMatch(/https?:\/\//);
199
- });
200
- it('asks not to be indexed', () => {
201
- expect(html).toContain('noindex');
202
- });
203
- /**
204
- * Arity is the real containment. Adding a parameter is how a destination or an
205
- * owner ends up rendered to a stranger, so it fails here before it fails in review.
206
- */
207
- it('takes no input, so it cannot carry anything from the link', () => {
208
- expect(generateBlockedLinkHTML.length).toBe(0);
209
- });
210
- });
211
- //# sourceMappingURL=link-safety.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"link-safety.test.js","sourceRoot":"","sources":["../../src/lib/link-safety.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,UAAU,EACV,QAAQ,EACR,0BAA0B,EAC1B,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAE1B,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7D,MAAM,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAClE,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChF,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,CACJ,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,EAAE,gBAAgB,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CACzF,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,2DAA2D;QAC3D,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrF,MAAM,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,UAAU,CAAC,6BAA6B,CAAC,CAAC,CAAC,IAAI,CACpD,mDAAmD,CACpD,CAAC;QACF,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,IAAI,GAAG,uBAAuB,CAAC,2BAA2B,CAAC,CAAC;QAClE,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;QACpD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,mCAAmC,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,IAAI,GAAG,uBAAuB,CAAC,+CAA+C,CAAC,CAAC;QACtF,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;QACxD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,uBAAuB,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,IAAI,GAAG,uBAAuB,CAAC,qBAAqB,CAAC,CAAC;QAC5D,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,kFAAkF;IAClF,yEAAyE;IACzE,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;QACpC,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;YACxC,MAAM,IAAI,GAAG,uBAAuB,CAAC,mCAAmC,CAAC,CAAC;YAC1E,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;YAClC,MAAM,IAAI,GAAG,uBAAuB,CAAC,0CAA0C,CAAC,CAAC;YACjF,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;YAClE,MAAM,IAAI,GAAG,uBAAuB,CAAC,qBAAqB,CAAC,CAAC;YAC5D,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;YACzC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;YAC7E,MAAM,IAAI,GAAG,uBAAuB,CAAC,qBAAqB,CAAC,CAAC;YAC5D,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;YACtE,KAAK,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC,EAAE,CAAC;gBACxC,MAAM,IAAI,GAAG,uBAAuB,CAAC,CAAC,CAAC,CAAC;gBACxC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;gBACtC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;YACtD,MAAM,IAAI,GAAG,uBAAuB,CAAC,uBAAuB,CAAC,CAAC;YAC9D,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;YACrC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,IAAI,GAAG,uBAAuB,CAAC,qBAAqB,CAAC,CAAC;QAC5D,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,oCAAoC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,uBAAuB,CAAC,qBAAqB,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;QACzF,MAAM,UAAU,GAAG,uBAAuB,CAAC,qBAAqB,EAAE;YAChE,SAAS,EAAE,2BAA2B;SACvC,CAAC,CAAC;QACH,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;QACjD,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAGH,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QAClE,MAAM,CAAC,QAAQ,CAAC,2BAA2B,CAAC,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,KAAK,MAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,YAAY,CAAC,EAAE,CAAC;YAChG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACpC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC/C,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,qDAAqD,EAAE,GAAG,EAAE;IACnE,MAAM,CAAC,GAAG,sBAAsB,CAAC;IAEjC,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,GAAG,0BAA0B,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9D,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,GAAG,0BAA0B,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QACzE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;QAC3D,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH;;;;OAIG;IACH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,CAAC,GAAG,0BAA0B,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1D,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;QAC3D,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH;;;;;OAKG;IACH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,CAAC,0BAA0B,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAChG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CACJ,0BAA0B,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAC1F,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,CAAC,0BAA0B,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QACrE,MAAM,CAAC,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IACjF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,KAAK,MAAM,KAAK,IAAI;YAClB,EAAE;YACF,EAAE,MAAM,EAAE,CAAC,EAAE;YACb,EAAE,QAAQ,EAAE,KAAK,EAAE;YACnB,EAAE,UAAU,EAAE,CAAC,EAAE;YACjB,EAAE,gBAAgB,EAAE,CAAC,EAAE;YACvB,EAAE,MAAM,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE;SACnC,EAAE,CAAC;YACF,MAAM,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC;QACpF,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,MAAM,IAAI,GAAG,uBAAuB,EAAE,CAAC;IAEvC,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAC;QAC3D,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,6CAA6C,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH;;;;;;;;;OASG;IACH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,KAAK,MAAM,IAAI,IAAI;YACjB,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO;YAC1E,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS;YAC5E,UAAU,EAAE,MAAM,EAAE,cAAc;SACnC,EAAE,CAAC;YACF,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,0BAA0B,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACpF,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH;;;OAGG;IACH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=not-found-page.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"not-found-page.test.d.ts","sourceRoot":"","sources":["../../src/lib/not-found-page.test.ts"],"names":[],"mappings":""}
@@ -1,151 +0,0 @@
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
@@ -1 +0,0 @@
1
- {"version":3,"file":"not-found-page.test.js","sourceRoot":"","sources":["../../src/lib/not-found-page.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=cli.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../../../src/lib/portable/cli.test.ts"],"names":[],"mappings":""}
@@ -1,98 +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 { runCli } from './cli.js';
8
- /**
9
- * The `linkforty` command: arguments, exit codes and what it prints. The
10
- * export and import runs need a real database and skip without DATABASE_URL.
11
- */
12
- function capture() {
13
- const lines = [];
14
- const errors = [];
15
- return { lines, errors, log: (l) => lines.push(l), logError: (l) => errors.push(l) };
16
- }
17
- describe('linkforty usage', () => {
18
- it('prints usage and exits 2 for an unknown command or a missing file', async () => {
19
- for (const argv of [[], ['backup'], ['export']]) {
20
- const out = capture();
21
- expect(await runCli(argv, {}, out.log, out.logError)).toBe(2);
22
- expect(out.errors.join('\n')).toContain('Usage:');
23
- }
24
- });
25
- it('exits 2 for a malformed flag', async () => {
26
- const out = capture();
27
- expect(await runCli(['import', 'x.lfbundle', '--organization'], {}, out.log, out.logError)).toBe(2);
28
- expect(out.errors[0]).toContain('--organization needs a value');
29
- });
30
- it('exits 2 without DATABASE_URL', async () => {
31
- const out = capture();
32
- expect(await runCli(['export', 'x.lfbundle'], {}, out.log, out.logError)).toBe(2);
33
- expect(out.errors[0]).toContain('DATABASE_URL');
34
- });
35
- });
36
- const ADMIN_URL = process.env.DATABASE_URL;
37
- describe.skipIf(!ADMIN_URL)('linkforty export and import (integration)', () => {
38
- const stamp = Date.now();
39
- const names = { source: `lf_core_cli_src_${stamp}`, target: `lf_core_cli_dst_${stamp}` };
40
- let admin;
41
- let dir;
42
- let file;
43
- const urlFor = (name) => {
44
- const url = new URL(ADMIN_URL);
45
- url.pathname = `/${name}`;
46
- return url.toString();
47
- };
48
- const run = async (argv, database) => {
49
- const out = capture();
50
- const code = await runCli(argv, { DATABASE_URL: urlFor(names[database]) }, out.log, out.logError);
51
- return { code, text: out.lines.join('\n'), errors: out.errors.join('\n') };
52
- };
53
- beforeAll(async () => {
54
- admin = new pg.Client({ connectionString: ADMIN_URL });
55
- await admin.connect();
56
- for (const name of Object.values(names))
57
- await admin.query(`CREATE DATABASE ${name}`);
58
- dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lf-cli-'));
59
- file = path.join(dir, 'server.lfbundle');
60
- // An export creates the schema; then seed one link and one webhook.
61
- await run(['export', path.join(dir, 'empty.lfbundle')], 'source');
62
- const source = new pg.Client({ connectionString: urlFor(names.source) });
63
- await source.connect();
64
- await source.query(`INSERT INTO links (short_code, original_url) VALUES ('hello', 'https://example.com')`);
65
- await source.query(`INSERT INTO webhooks (name, url, secret, events) VALUES ('w', 'https://hooks.example', 's', '{click_event}')`);
66
- await source.end();
67
- }, 60000);
68
- afterAll(async () => {
69
- for (const name of Object.values(names))
70
- await admin.query(`DROP DATABASE IF EXISTS ${name} WITH (FORCE)`);
71
- await admin.end();
72
- fs.rmSync(dir, { recursive: true, force: true });
73
- }, 30000);
74
- it('exports, dry-runs, imports and then reports conflicts', async () => {
75
- const exported = await run(['export', file], 'source');
76
- expect(exported.code).toBe(0);
77
- expect(exported.text).toContain('Exported 2 rows');
78
- const dry = await run(['import', file, '--dry-run'], 'target');
79
- expect(dry).toMatchObject({ code: 0 });
80
- expect(dry.text).toContain('Dry run: no conflicts');
81
- const imported = await run(['import', file], 'target');
82
- expect(imported.code).toBe(0);
83
- expect(imported.text).toContain('Imported.');
84
- expect(imported.text).toMatch(/https:\/\/hooks\.example {2}[0-9a-f]{64}/);
85
- const again = await run(['import', file], 'target');
86
- expect(again.code).toBe(1);
87
- expect(again.text).toContain('conflict(s); nothing was imported');
88
- expect(again.text).toContain('short_code_taken: hello');
89
- }, 60000);
90
- it('exits 1 with the reason for a file that is not a bundle', async () => {
91
- const bogus = path.join(dir, 'bogus.lfbundle');
92
- fs.writeFileSync(bogus, zlib.gzipSync('{"hello":"world"}\n'));
93
- const result = await run(['import', bogus], 'target');
94
- expect(result.code).toBe(1);
95
- expect(result.errors).toContain('Not a LinkForty bundle');
96
- }, 60000);
97
- });
98
- //# sourceMappingURL=cli.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.test.js","sourceRoot":"","sources":["../../../src/lib/portable/cli.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC;;;GAGG;AAEH,SAAS,OAAO;IACd,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;AACvG,CAAC;AAED,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;QACjF,KAAK,MAAM,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YAChD,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;YACtB,MAAM,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC9D,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;QAC5C,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,MAAM,CAAC,MAAM,MAAM,CAAC,CAAC,QAAQ,EAAE,YAAY,EAAE,gBAAgB,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;QAC5C,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,MAAM,CAAC,MAAM,MAAM,CAAC,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClF,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAE3C,QAAQ,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,2CAA2C,EAAE,GAAG,EAAE;IAC5E,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,EAAE,MAAM,EAAE,mBAAmB,KAAK,EAAE,EAAE,MAAM,EAAE,mBAAmB,KAAK,EAAE,EAAE,CAAC;IACzF,IAAI,KAAgB,CAAC;IACrB,IAAI,GAAW,CAAC;IAChB,IAAI,IAAY,CAAC;IAEjB,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,EAAE;QAC9B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAmB,CAAC,CAAC;QACzC,GAAG,CAAC,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC;QAC1B,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,KAAK,EAAE,IAAc,EAAE,QAA4B,EAAE,EAAE;QACjE,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;QAClG,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC7E,CAAC,CAAC;IAEF,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,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAC,KAAK,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC;QACtF,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC;QACxD,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;QAEzC,oEAAoE;QACpE,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACzE,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;QACvB,MAAM,MAAM,CAAC,KAAK,CAAC,sFAAsF,CAAC,CAAC;QAC3G,MAAM,MAAM,CAAC,KAAK,CAAC,8GAA8G,CAAC,CAAC;QACnI,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC;IACrB,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,QAAQ,CAAC,KAAK,IAAI,EAAE;QAClB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAC,KAAK,CAAC,2BAA2B,IAAI,eAAe,CAAC,CAAC;QAC3G,MAAM,KAAK,CAAC,GAAG,EAAE,CAAC;QAClB,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACrE,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;QACvD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAEnD,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC/D,MAAM,CAAC,GAAG,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QACvC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;QAEpD,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;QACvD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAC7C,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,0CAA0C,CAAC,CAAC;QAE1E,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;QACpD,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,mCAAmC,CAAC,CAAC;QAClE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAC;IAC1D,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;QAC/C,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,qBAAqB,CAAC,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;QACtD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC;IAC5D,CAAC,EAAE,KAAM,CAAC,CAAC;AACb,CAAC,CAAC,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=portable.integration.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"portable.integration.test.d.ts","sourceRoot":"","sources":["../../../src/lib/portable/portable.integration.test.ts"],"names":[],"mappings":""}