@linkforty/core 1.20.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +38 -0
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/link-safety.d.ts +95 -0
- package/dist/lib/link-safety.d.ts.map +1 -0
- package/dist/lib/link-safety.js +158 -0
- package/dist/lib/link-safety.js.map +1 -0
- package/dist/lib/link-safety.test.d.ts +2 -0
- package/dist/lib/link-safety.test.d.ts.map +1 -0
- package/dist/lib/link-safety.test.js +117 -0
- package/dist/lib/link-safety.test.js.map +1 -0
- package/dist/routes/redirect.d.ts +8 -1
- package/dist/routes/redirect.d.ts.map +1 -1
- package/dist/routes/redirect.js +66 -1
- package/dist/routes/redirect.js.map +1 -1
- package/dist/routes/redirect.safety.test.d.ts +2 -0
- package/dist/routes/redirect.safety.test.d.ts.map +1 -0
- package/dist/routes/redirect.safety.test.js +181 -0
- package/dist/routes/redirect.safety.test.js.map +1 -0
- package/dist/routes/sdk-cache-bypass.test.d.ts +2 -0
- package/dist/routes/sdk-cache-bypass.test.d.ts.map +1 -0
- package/dist/routes/sdk-cache-bypass.test.js +167 -0
- package/dist/routes/sdk-cache-bypass.test.js.map +1 -0
- package/dist/routes/sdk.d.ts.map +1 -1
- package/dist/routes/sdk.js +60 -4
- package/dist/routes/sdk.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export * from './lib/client-ip.js';
|
|
|
30
30
|
export * from './lib/database.js';
|
|
31
31
|
export * from './lib/fingerprint.js';
|
|
32
32
|
export * from './lib/webhook.js';
|
|
33
|
+
export * from './lib/link-safety.js';
|
|
33
34
|
export * from './lib/event-emitter.js';
|
|
34
35
|
export * from './types/index.js';
|
|
35
36
|
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes, healthRoutes } from './routes/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAAsB,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAWxE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oJAAoJ;IACpJ,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,aAAkB,kTAiC7D;AAGD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAAsB,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAWxE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oJAAoJ;IACpJ,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,aAAkB,kTAiC7D;AAGD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -55,6 +55,7 @@ export * from './lib/client-ip.js';
|
|
|
55
55
|
export * from './lib/database.js';
|
|
56
56
|
export * from './lib/fingerprint.js';
|
|
57
57
|
export * from './lib/webhook.js';
|
|
58
|
+
export * from './lib/link-safety.js';
|
|
58
59
|
export * from './lib/event-emitter.js';
|
|
59
60
|
export * from './types/index.js';
|
|
60
61
|
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes, healthRoutes } from './routes/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAA4B,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAmB,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAkBlD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAyB,EAAE;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;QAC5D,UAAU,EAAE,OAAO,CAAC,UAAU;KAC/B,CAAC,CAAC;IAEH,OAAO;IACP,MAAM,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,IAAI,GAAG;KACpC,CAAC,CAAC;IAEH,mBAAmB;IACnB,IAAI,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE;YAC5B,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG;SACvB,CAAC,CAAC;IACL,CAAC;IAED,WAAW;IACX,MAAM,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE3C,SAAS;IACT,MAAM,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACrC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,MAAM,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;IACtC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,MAAM,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,gCAAgC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAA4B,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAmB,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAkBlD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAyB,EAAE;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;QAC5D,UAAU,EAAE,OAAO,CAAC,UAAU;KAC/B,CAAC,CAAC;IAEH,OAAO;IACP,MAAM,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,IAAI,GAAG;KACpC,CAAC,CAAC;IAEH,mBAAmB;IACnB,IAAI,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE;YAC5B,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG;SACvB,CAAC,CAAC;IACL,CAAC;IAED,WAAW;IACX,MAAM,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE3C,SAAS;IACT,MAAM,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACrC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,MAAM,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;IACtC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,MAAM,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,gCAAgC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;AA6BvB,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;AA6BvB,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,iBA6jBrE"}
|
package/dist/lib/database.js
CHANGED
|
@@ -220,6 +220,44 @@ export async function initializeDatabase(options = {}) {
|
|
|
220
220
|
ALTER TABLE links ADD COLUMN template_id UUID REFERENCES link_templates(id) ON DELETE SET NULL;
|
|
221
221
|
END IF;
|
|
222
222
|
END $$;
|
|
223
|
+
`);
|
|
224
|
+
// Link safety states.
|
|
225
|
+
//
|
|
226
|
+
// `is_active = false` already removes a link from resolution entirely. These
|
|
227
|
+
// add a softer state and an explanation:
|
|
228
|
+
//
|
|
229
|
+
// warn_at — resolve to an interstitial warning instead of a 302, so a
|
|
230
|
+
// visitor sees the destination and chooses. Useful when a
|
|
231
|
+
// link is suspected rather than confirmed unsafe, since a
|
|
232
|
+
// hard block on a false positive takes a legitimate link
|
|
233
|
+
// offline with no recourse for the visitor.
|
|
234
|
+
// disabled_at — when the link was taken out of resolution.
|
|
235
|
+
// disabled_reason — why, so the decision can be explained or reversed later.
|
|
236
|
+
//
|
|
237
|
+
// All nullable and additive: an existing deployment that sets none of them
|
|
238
|
+
// behaves exactly as before.
|
|
239
|
+
await client.query(`
|
|
240
|
+
DO $$
|
|
241
|
+
BEGIN
|
|
242
|
+
IF NOT EXISTS (
|
|
243
|
+
SELECT 1 FROM information_schema.columns
|
|
244
|
+
WHERE table_name='links' AND column_name='warn_at'
|
|
245
|
+
) THEN
|
|
246
|
+
ALTER TABLE links ADD COLUMN warn_at TIMESTAMPTZ;
|
|
247
|
+
END IF;
|
|
248
|
+
IF NOT EXISTS (
|
|
249
|
+
SELECT 1 FROM information_schema.columns
|
|
250
|
+
WHERE table_name='links' AND column_name='disabled_at'
|
|
251
|
+
) THEN
|
|
252
|
+
ALTER TABLE links ADD COLUMN disabled_at TIMESTAMPTZ;
|
|
253
|
+
END IF;
|
|
254
|
+
IF NOT EXISTS (
|
|
255
|
+
SELECT 1 FROM information_schema.columns
|
|
256
|
+
WHERE table_name='links' AND column_name='disabled_reason'
|
|
257
|
+
) THEN
|
|
258
|
+
ALTER TABLE links ADD COLUMN disabled_reason TEXT;
|
|
259
|
+
END IF;
|
|
260
|
+
END $$;
|
|
223
261
|
`);
|
|
224
262
|
// Add description column to existing links table if it doesn't exist
|
|
225
263
|
await client.query(`
|
package/dist/lib/database.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link safety states for the redirect path.
|
|
3
|
+
*
|
|
4
|
+
* Three outcomes are possible for a link that exists:
|
|
5
|
+
*
|
|
6
|
+
* - `allow` — resolve normally.
|
|
7
|
+
* - `warn` — serve an interstitial that shows the destination and requires an
|
|
8
|
+
* explicit click. For a link that is *suspected* unsafe rather than
|
|
9
|
+
* confirmed, this is strictly better than a hard block: a false
|
|
10
|
+
* positive still lets the visitor through, while a true positive
|
|
11
|
+
* still breaks the one-click flow a malicious link depends on.
|
|
12
|
+
* - `block` — behave as if the link does not exist.
|
|
13
|
+
*
|
|
14
|
+
* `block` deliberately produces the same response as an unknown short code. A
|
|
15
|
+
* distinct "this link was disabled" response would confirm to whoever is probing
|
|
16
|
+
* that the code was real, and would leak that its owner is under a restriction.
|
|
17
|
+
*/
|
|
18
|
+
export type LinkSafetyOutcome = 'allow' | 'warn' | 'block';
|
|
19
|
+
export interface LinkSafetyInput {
|
|
20
|
+
/** Whether the link is active. Absent/undefined is treated as active. */
|
|
21
|
+
isActive?: boolean | null;
|
|
22
|
+
/** Set when the link should serve a warning instead of resolving. */
|
|
23
|
+
warnAt?: Date | string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Set when the link's owner is restricted. Optional because the owning table is
|
|
26
|
+
* not part of this package's schema — deployments that do not model owner
|
|
27
|
+
* restriction simply never pass it.
|
|
28
|
+
*/
|
|
29
|
+
ownerSuspendedAt?: Date | string | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Decide what to do with a link that was found.
|
|
33
|
+
*
|
|
34
|
+
* Order matters: owner restriction and inactivity both beat `warn`. A link whose
|
|
35
|
+
* owner is restricted must be unreachable even if it was only flagged to warn.
|
|
36
|
+
*/
|
|
37
|
+
export declare function evaluateLinkSafety(input: LinkSafetyInput): LinkSafetyOutcome;
|
|
38
|
+
/**
|
|
39
|
+
* A destination is only safe to put in an `href` if it is http(s).
|
|
40
|
+
*
|
|
41
|
+
* `escapeHtml` is not sufficient on its own: a URL scheme contains none of the
|
|
42
|
+
* characters it neutralises, so `javascript:alert(1)` passes through untouched and
|
|
43
|
+
* becomes executable on click. That matters here more than almost anywhere else —
|
|
44
|
+
* this page is shown *only* for links already flagged as suspicious, so the
|
|
45
|
+
* destinations reaching it are the most likely in the system to be hostile.
|
|
46
|
+
*
|
|
47
|
+
* Reachable in practice, not just in theory: zod's `.url()` accepts `javascript:`
|
|
48
|
+
* and `data:` URLs, so a stored destination can already hold one. Tightening
|
|
49
|
+
* validation at write time is worth doing separately, but this page must not
|
|
50
|
+
* depend on that having happened.
|
|
51
|
+
*
|
|
52
|
+
* Returns null when there is nothing safe to link to — including a bare deep-link
|
|
53
|
+
* path, which would otherwise render as a relative link to the redirect host
|
|
54
|
+
* rather than the real destination. Callers render that as inert text.
|
|
55
|
+
*/
|
|
56
|
+
export declare function safeHref(destination: string): string | null;
|
|
57
|
+
/** Minimal HTML escaping for interpolating a URL into markup and an href. */
|
|
58
|
+
export declare function escapeHtml(value: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Interstitial shown for a `warn` outcome.
|
|
61
|
+
*
|
|
62
|
+
* Shows the destination in full — the point is to hand back the information the
|
|
63
|
+
* short link hid, so the visitor can judge for themselves. Continuing takes a
|
|
64
|
+
* deliberate click, which is what breaks the one-click flow.
|
|
65
|
+
*
|
|
66
|
+
* Contains no JavaScript and no external assets: it must render on a bare
|
|
67
|
+
* redirect host and under a strict content-security policy.
|
|
68
|
+
*/
|
|
69
|
+
export declare function generateWarningLinkHTML(destination: string, options?: {
|
|
70
|
+
reportUrl?: string;
|
|
71
|
+
}): string;
|
|
72
|
+
/**
|
|
73
|
+
* Build a memoised probe for owner-restriction support.
|
|
74
|
+
*
|
|
75
|
+
* A factory rather than module state: each registration gets its own, so two servers
|
|
76
|
+
* in one process pointed at different databases cannot share an answer, and no
|
|
77
|
+
* test-only reset has to be exported.
|
|
78
|
+
*
|
|
79
|
+
* Shared by every path that resolves a link and caches the result, and that sharing
|
|
80
|
+
* is the whole point. The redirect and the SDK resolve endpoint write the SAME Redis
|
|
81
|
+
* key, so if one selects `owner_suspended_at` and the other does not, the second
|
|
82
|
+
* silently caches a row that makes the first one's gate pass. Keeping the SELECT
|
|
83
|
+
* fragment in one place is what stops them drifting apart again.
|
|
84
|
+
*
|
|
85
|
+
* The in-flight promise is memoised, not just the result, so concurrent cold requests
|
|
86
|
+
* issue one probe. A failure resolves to "unsupported", so a probe error can never
|
|
87
|
+
* take a resolution path down.
|
|
88
|
+
*/
|
|
89
|
+
export declare function createOwnerSuspensionSelect(deps: {
|
|
90
|
+
query: (sql: string) => Promise<{
|
|
91
|
+
rows: unknown[];
|
|
92
|
+
}>;
|
|
93
|
+
onSupported?: () => void;
|
|
94
|
+
}): () => Promise<string>;
|
|
95
|
+
//# sourceMappingURL=link-safety.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-safety.d.ts","sourceRoot":"","sources":["../../src/lib/link-safety.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,qEAAqE;IACrE,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;CACzC;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,iBAAiB,CAK5E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ3D;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,MAAM,CA4DR;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAC;IACrD,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAkBxB"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide what to do with a link that was found.
|
|
3
|
+
*
|
|
4
|
+
* Order matters: owner restriction and inactivity both beat `warn`. A link whose
|
|
5
|
+
* owner is restricted must be unreachable even if it was only flagged to warn.
|
|
6
|
+
*/
|
|
7
|
+
export function evaluateLinkSafety(input) {
|
|
8
|
+
if (input.ownerSuspendedAt != null)
|
|
9
|
+
return 'block';
|
|
10
|
+
if (input.isActive === false)
|
|
11
|
+
return 'block';
|
|
12
|
+
if (input.warnAt != null)
|
|
13
|
+
return 'warn';
|
|
14
|
+
return 'allow';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A destination is only safe to put in an `href` if it is http(s).
|
|
18
|
+
*
|
|
19
|
+
* `escapeHtml` is not sufficient on its own: a URL scheme contains none of the
|
|
20
|
+
* characters it neutralises, so `javascript:alert(1)` passes through untouched and
|
|
21
|
+
* becomes executable on click. That matters here more than almost anywhere else —
|
|
22
|
+
* this page is shown *only* for links already flagged as suspicious, so the
|
|
23
|
+
* destinations reaching it are the most likely in the system to be hostile.
|
|
24
|
+
*
|
|
25
|
+
* Reachable in practice, not just in theory: zod's `.url()` accepts `javascript:`
|
|
26
|
+
* and `data:` URLs, so a stored destination can already hold one. Tightening
|
|
27
|
+
* validation at write time is worth doing separately, but this page must not
|
|
28
|
+
* depend on that having happened.
|
|
29
|
+
*
|
|
30
|
+
* Returns null when there is nothing safe to link to — including a bare deep-link
|
|
31
|
+
* path, which would otherwise render as a relative link to the redirect host
|
|
32
|
+
* rather than the real destination. Callers render that as inert text.
|
|
33
|
+
*/
|
|
34
|
+
export function safeHref(destination) {
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(destination);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? destination : null;
|
|
43
|
+
}
|
|
44
|
+
/** Minimal HTML escaping for interpolating a URL into markup and an href. */
|
|
45
|
+
export function escapeHtml(value) {
|
|
46
|
+
return String(value)
|
|
47
|
+
.replace(/&/g, '&')
|
|
48
|
+
.replace(/</g, '<')
|
|
49
|
+
.replace(/>/g, '>')
|
|
50
|
+
.replace(/"/g, '"')
|
|
51
|
+
.replace(/'/g, ''');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Interstitial shown for a `warn` outcome.
|
|
55
|
+
*
|
|
56
|
+
* Shows the destination in full — the point is to hand back the information the
|
|
57
|
+
* short link hid, so the visitor can judge for themselves. Continuing takes a
|
|
58
|
+
* deliberate click, which is what breaks the one-click flow.
|
|
59
|
+
*
|
|
60
|
+
* Contains no JavaScript and no external assets: it must render on a bare
|
|
61
|
+
* redirect host and under a strict content-security policy.
|
|
62
|
+
*/
|
|
63
|
+
export function generateWarningLinkHTML(destination, options = {}) {
|
|
64
|
+
const shown = destination && destination.trim() ? escapeHtml(destination) : '(no destination recorded)';
|
|
65
|
+
const href = destination ? safeHref(destination) : null;
|
|
66
|
+
// No anchor at all when there is nothing safe to link to. An href="" would
|
|
67
|
+
// re-request the warning page, so "Continue anyway" would just reload it.
|
|
68
|
+
const continueButton = href
|
|
69
|
+
? `<a class="go" href="${escapeHtml(href)}" rel="nofollow noopener noreferrer">Continue anyway</a>`
|
|
70
|
+
: `<p class="inert">This link has no usable web destination, so there is nothing to continue to.</p>`;
|
|
71
|
+
const reportLink = options.reportUrl
|
|
72
|
+
? `<p class="report"><a href="${escapeHtml(options.reportUrl)}" rel="nofollow noopener">Report this link</a></p>`
|
|
73
|
+
: '';
|
|
74
|
+
return `<!doctype html>
|
|
75
|
+
<html lang="en">
|
|
76
|
+
<head>
|
|
77
|
+
<meta charset="utf-8">
|
|
78
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
79
|
+
<meta name="robots" content="noindex,nofollow">
|
|
80
|
+
<title>Check this link before continuing</title>
|
|
81
|
+
<style>
|
|
82
|
+
:root { color-scheme: light dark; }
|
|
83
|
+
body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
|
|
84
|
+
background:#f6f7f8; color:#16191d; padding:24px;
|
|
85
|
+
font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }
|
|
86
|
+
.card { max-width:34rem; width:100%; background:#fff; border:1px solid #e3e6ea;
|
|
87
|
+
border-radius:10px; padding:28px; }
|
|
88
|
+
h1 { margin:0 0 12px; font-size:1.35rem; line-height:1.25; }
|
|
89
|
+
p { margin:0 0 14px; }
|
|
90
|
+
.dest { display:block; word-break:break-all; background:#f0f2f4; border-radius:6px;
|
|
91
|
+
padding:10px 12px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
92
|
+
font-size:.9rem; margin-bottom:18px; }
|
|
93
|
+
a.go { display:inline-block; text-decoration:none; border:1px solid #c9ced4; color:#16191d;
|
|
94
|
+
border-radius:6px; padding:9px 15px; font-size:.95rem; font-weight:600; }
|
|
95
|
+
.inert { margin:0; font-size:.9rem; color:#5b636d; }
|
|
96
|
+
.report { margin:16px 0 0; font-size:.85rem; }
|
|
97
|
+
.report a { color:#5b636d; }
|
|
98
|
+
@media (prefers-color-scheme: dark) {
|
|
99
|
+
body { background:#14171a; color:#e8eaed; }
|
|
100
|
+
.card { background:#1d2126; border-color:#2c3238; }
|
|
101
|
+
.dest { background:#14171a; }
|
|
102
|
+
a.go { border-color:#3a4149; color:#e8eaed; }
|
|
103
|
+
.report a { color:#98a1ab; }
|
|
104
|
+
.inert { color:#98a1ab; }
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
<main class="card">
|
|
110
|
+
<h1>Check this link before continuing</h1>
|
|
111
|
+
<p>This short link has been flagged as possibly unsafe, so we have not sent you
|
|
112
|
+
straight there. It leads to:</p>
|
|
113
|
+
<span class="dest">${shown}</span>
|
|
114
|
+
<p>If you were not expecting this link, or it claims to be from a bank, a government
|
|
115
|
+
service, or a company you do business with, close this page. Do not enter any
|
|
116
|
+
password or personal details.</p>
|
|
117
|
+
${continueButton}
|
|
118
|
+
${reportLink}
|
|
119
|
+
</main>
|
|
120
|
+
</body>
|
|
121
|
+
</html>`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build a memoised probe for owner-restriction support.
|
|
125
|
+
*
|
|
126
|
+
* A factory rather than module state: each registration gets its own, so two servers
|
|
127
|
+
* in one process pointed at different databases cannot share an answer, and no
|
|
128
|
+
* test-only reset has to be exported.
|
|
129
|
+
*
|
|
130
|
+
* Shared by every path that resolves a link and caches the result, and that sharing
|
|
131
|
+
* is the whole point. The redirect and the SDK resolve endpoint write the SAME Redis
|
|
132
|
+
* key, so if one selects `owner_suspended_at` and the other does not, the second
|
|
133
|
+
* silently caches a row that makes the first one's gate pass. Keeping the SELECT
|
|
134
|
+
* fragment in one place is what stops them drifting apart again.
|
|
135
|
+
*
|
|
136
|
+
* The in-flight promise is memoised, not just the result, so concurrent cold requests
|
|
137
|
+
* issue one probe. A failure resolves to "unsupported", so a probe error can never
|
|
138
|
+
* take a resolution path down.
|
|
139
|
+
*/
|
|
140
|
+
export function createOwnerSuspensionSelect(deps) {
|
|
141
|
+
let probe = null;
|
|
142
|
+
return () => {
|
|
143
|
+
if (!probe) {
|
|
144
|
+
probe = deps
|
|
145
|
+
.query(`SELECT 1 FROM information_schema.columns
|
|
146
|
+
WHERE table_name = 'organizations' AND column_name = 'suspended_at'`)
|
|
147
|
+
.then((r) => {
|
|
148
|
+
const supported = r.rows.length > 0;
|
|
149
|
+
if (supported)
|
|
150
|
+
deps.onSupported?.();
|
|
151
|
+
return supported ? ', o.suspended_at AS owner_suspended_at' : '';
|
|
152
|
+
})
|
|
153
|
+
.catch(() => '');
|
|
154
|
+
}
|
|
155
|
+
return probe;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=link-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-safety.js","sourceRoot":"","sources":["../../src/lib/link-safety.ts"],"names":[],"mappings":"AAgCA;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAsB;IACvD,IAAI,KAAK,CAAC,gBAAgB,IAAI,IAAI;QAAE,OAAO,OAAO,CAAC;IACnD,IAAI,KAAK,CAAC,QAAQ,KAAK,KAAK;QAAE,OAAO,OAAO,CAAC;IAC7C,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI;QAAE,OAAO,MAAM,CAAC;IACxC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,QAAQ,CAAC,WAAmB;IAC1C,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;AACpF,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,MAAM,CAAC,KAAK,CAAC;SACjB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CACrC,WAAmB,EACnB,UAAkC,EAAE;IAEpC,MAAM,KAAK,GAAG,WAAW,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,2BAA2B,CAAC;IACxG,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxD,2EAA2E;IAC3E,0EAA0E;IAC1E,MAAM,cAAc,GAAG,IAAI;QACzB,CAAC,CAAC,uBAAuB,UAAU,CAAC,IAAI,CAAC,0DAA0D;QACnG,CAAC,CAAC,mGAAmG,CAAC;IACxG,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS;QAClC,CAAC,CAAC,8BAA8B,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,oDAAoD;QACjH,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAuCgB,KAAK;;;;MAIxB,cAAc;MACd,UAAU;;;QAGR,CAAC;AACT,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,2BAA2B,CAAC,IAG3C;IACC,IAAI,KAAK,GAA2B,IAAI,CAAC;IACzC,OAAO,GAAG,EAAE;QACV,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,KAAK,GAAG,IAAI;iBACT,KAAK,CACJ;+EACqE,CACtE;iBACA,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;gBACV,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;gBACpC,IAAI,SAAS;oBAAE,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACpC,OAAO,SAAS,CAAC,CAAC,CAAC,wCAAwC,CAAC,CAAC,CAAC,EAAE,CAAC;YACnE,CAAC,CAAC;iBACD,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-safety.test.d.ts","sourceRoot":"","sources":["../../src/lib/link-safety.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { evaluateLinkSafety, generateWarningLinkHTML, escapeHtml, safeHref } 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('<script>alert("x")</script>');
|
|
34
|
+
expect(escapeHtml("it's & more")).toBe('it's & 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('<script>');
|
|
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
|
+
//# sourceMappingURL=link-safety.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-safety.test.js","sourceRoot":"","sources":["../../src/lib/link-safety.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,5 +29,12 @@ export declare function pickMobileFallbackUrl(device: 'ios' | 'android', userAge
|
|
|
29
29
|
url: string;
|
|
30
30
|
reason: string;
|
|
31
31
|
} | null;
|
|
32
|
-
export
|
|
32
|
+
export interface RedirectRouteOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Absolute URL of an abuse-reporting page. When set, the interstitial warning
|
|
35
|
+
* page links to it. Optional — deployments without one simply omit the link.
|
|
36
|
+
*/
|
|
37
|
+
abuseReportUrl?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function redirectRoutes(fastify: FastifyInstance, options?: RedirectRouteOptions): Promise<void>;
|
|
33
40
|
//# sourceMappingURL=redirect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAa1C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAa5D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAiBhE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,KAAK,GAAG,SAAS,EACzB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,cAAc,EAAE,MAAM,GAAG,IAAI,GAC5B;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAexC;AAkDD,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,oBAAyB,iBA0hBnC"}
|