@certrev/cert-block 1.1.0 → 1.1.1
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/CHANGELOG.md +41 -0
- package/README.md +20 -6
- package/dist/components/render-cert-block.d.ts.map +1 -1
- package/dist/components/render-cert-block.js +3 -6
- package/dist/components/render-cert-block.js.map +1 -1
- package/dist/modal/certrev-cert-modal.d.ts +34 -1
- package/dist/modal/certrev-cert-modal.d.ts.map +1 -1
- package/dist/modal/certrev-cert-modal.js +40 -2
- package/dist/modal/certrev-cert-modal.js.map +1 -1
- package/dist/modal/certrev-cert.entry.d.ts.map +1 -1
- package/dist/modal/certrev-cert.entry.js +2 -4
- package/dist/modal/certrev-cert.entry.js.map +1 -1
- package/dist/modal/certrev-cert.js +4 -4
- package/dist/modal/deprecated.d.ts +15 -0
- package/dist/modal/deprecated.d.ts.map +1 -0
- package/dist/modal/deprecated.js +22 -0
- package/dist/modal/deprecated.js.map +1 -0
- package/dist/modal/ftc-disclosure.d.ts +7 -3
- package/dist/modal/ftc-disclosure.d.ts.map +1 -1
- package/dist/modal/ftc-disclosure.js +7 -3
- package/dist/modal/ftc-disclosure.js.map +1 -1
- package/dist/modal/index.d.ts +4 -3
- package/dist/modal/index.d.ts.map +1 -1
- package/dist/modal/index.js +4 -3
- package/dist/modal/index.js.map +1 -1
- package/dist/modal/interactions.d.ts +20 -4
- package/dist/modal/interactions.d.ts.map +1 -1
- package/dist/modal/interactions.js +34 -7
- package/dist/modal/interactions.js.map +1 -1
- package/package.json +1 -1
- package/src/components/render-cert-block.ts +3 -6
- package/src/modal/certrev-cert-modal.ts +45 -2
- package/src/modal/certrev-cert.entry.ts +2 -4
- package/src/modal/deprecated.ts +25 -0
- package/src/modal/ftc-disclosure.ts +7 -3
- package/src/modal/index.ts +4 -3
- package/src/modal/interactions.ts +37 -7
- package/dist/modal/ftc-guard.d.ts +0 -85
- package/dist/modal/ftc-guard.d.ts.map +0 -1
- package/dist/modal/ftc-guard.js +0 -150
- package/dist/modal/ftc-guard.js.map +0 -1
- package/src/modal/ftc-guard.ts +0 -174
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FTC disclosure tamper guard for the NATIVE Shopify cert render (iframe→native parity).
|
|
3
|
-
*
|
|
4
|
-
* In the cross-origin iframe model the expert-memo embed inlined the portal's
|
|
5
|
-
* `ftcTamperGuardScript()`: a kill switch that blanks the memo if the verbatim FTC
|
|
6
|
-
* material-connection disclosure is stripped, hidden, or altered — making the MSA §3
|
|
7
|
-
* anti-stripping covenant TECHNICALLY enforced, not just contractual. The native memo renders
|
|
8
|
-
* in the host theme's DOM (no sandbox), so without this it would lose that runtime enforcement.
|
|
9
|
-
* This module restores it: on load + on later DOM mutation it verifies the in-DOM
|
|
10
|
-
* `[data-ftc-line]` is present, carries the EXACT verbatim text, and is actually visible; if not,
|
|
11
|
-
* it neutralizes the memo with a neutral notice so the memo NEVER renders without its disclosure.
|
|
12
|
-
*
|
|
13
|
-
* Native-specific semantics (differ from the iframe, which only existed when a memo did):
|
|
14
|
-
* - Enforcement is keyed on the MEMO element (`.certrev-memo`). A certified article with NO
|
|
15
|
-
* memo legitimately has no `.certrev-memo` and no disclosure — the guard does NOT fire
|
|
16
|
-
* (no false positive on the no-memo path).
|
|
17
|
-
* - On tamper it neutralizes ONLY the memo (never `document.body` — we're in the host page).
|
|
18
|
-
* - The contributors card is governed by the badge re-verify (`revalidate.ts`), not this guard;
|
|
19
|
-
* the card carries only the one-word "Compensated" cue, never the guarded disclosure.
|
|
20
|
-
* - COVERAGE, and it is narrower than it reads: `.certrev-memo` / `.certrev-memo-wrap` are portal
|
|
21
|
-
* LIQUID markup — NOTHING in this package emits either. So the kill switch covers the Liquid
|
|
22
|
-
* memo surface and nothing else. `<CertBadge>` and `render-badge-html.ts` emit the same
|
|
23
|
-
* `[data-ftc-line]` hook inside a `.certrev-badge` (no memo), and `renderCertBlock` emits no
|
|
24
|
-
* hook at all; on those surfaces a pass verifies nothing and says so (`'unguarded'`) rather
|
|
25
|
-
* than reporting success, and no observer is installed at all. Widening enforcement to the
|
|
26
|
-
* badge is a live design decision, not an omission — see `FtcEnforcementResult`.
|
|
27
|
-
*
|
|
28
|
-
* Pure DOM, no crypto, no network. The expected text is the single source of truth
|
|
29
|
-
* `FTC_DISCLOSURE_LINE` (imported, not copied — so a reword can never desync this guard).
|
|
30
|
-
*/
|
|
31
|
-
export declare const FTC_LINE_SELECTOR = "[data-ftc-line]";
|
|
32
|
-
export declare const MEMO_SELECTOR = ".certrev-memo";
|
|
33
|
-
export declare const MEMO_WRAP_SELECTOR = ".certrev-memo-wrap";
|
|
34
|
-
/** Marker on the neutral notice node that replaces a tampered memo (testable / idempotent). */
|
|
35
|
-
export declare const FTC_NEUTRALIZED_ATTR = "data-certrev-ftc-neutralized";
|
|
36
|
-
/** True when a memo's disclosure line is present, exactly verbatim, and visible. */
|
|
37
|
-
export declare function isDisclosureIntact(memo: Element): boolean;
|
|
38
|
-
/**
|
|
39
|
-
* Replace a tampered memo with a neutral, self-contained notice — no reviewer attribution, no
|
|
40
|
-
* seal — so the surface can never read as an endorsement without its required disclosure.
|
|
41
|
-
*/
|
|
42
|
-
export declare function neutralizeMemo(memo: Element): void;
|
|
43
|
-
/**
|
|
44
|
-
* What ONE enforcement pass actually established:
|
|
45
|
-
* - `'intact'` — at least one memo was checked, and every one carried its verbatim, visible line.
|
|
46
|
-
* - `'tampered'` — at least one memo failed the check and was neutralized.
|
|
47
|
-
* - `'unguarded'` — the document holds no `.certrev-memo`, so the pass checked nothing.
|
|
48
|
-
*
|
|
49
|
-
* `'unguarded'` used to be reported as `true`, indistinguishable from `'intact'`, and that
|
|
50
|
-
* conflation was a correctness bug, not a cosmetic one: `.certrev-memo` is portal Liquid markup
|
|
51
|
-
* this package never emits, so on every React / Web Component / `renderCertBlock` surface the pass
|
|
52
|
-
* finds nothing and answered "all intact" about a disclosure it had never looked at — the kill
|
|
53
|
-
* switch silently reporting success on exactly the surfaces it does not protect.
|
|
54
|
-
*
|
|
55
|
-
* The legitimate no-memo certified article (card only, no memo, no disclosure owed) lands here too,
|
|
56
|
-
* and the guard genuinely cannot tell the two apart from the DOM alone. That is the point of a
|
|
57
|
-
* third state rather than a second boolean: "nothing was verified" is the honest answer to both,
|
|
58
|
-
* and it is the caller — which knows which surface it rendered — that can tell whether that is
|
|
59
|
-
* expected or a coverage gap.
|
|
60
|
-
*/
|
|
61
|
-
export type FtcEnforcementResult = 'intact' | 'tampered' | 'unguarded';
|
|
62
|
-
/**
|
|
63
|
-
* Enforce the disclosure across every memo in the doc: neutralize any whose disclosure isn't
|
|
64
|
-
* intact. A doc with no `.certrev-memo` is a no-op — see `FtcEnforcementResult` for why that is
|
|
65
|
-
* `'unguarded'` and not success.
|
|
66
|
-
*/
|
|
67
|
-
export declare function enforceFtcDisclosure(doc: Document): FtcEnforcementResult;
|
|
68
|
-
/**
|
|
69
|
-
* Install the guard once per document: enforce now (on DOM ready) and re-enforce on later DOM
|
|
70
|
-
* mutations within the guarded subtree(s) — so a host script that strips / hides / edits the
|
|
71
|
-
* disclosure after load still trips the kill switch. Idempotent.
|
|
72
|
-
*
|
|
73
|
-
* The observer scope is the memo wrap(s) — placement.ts relocates a wrap as a node, so the observer
|
|
74
|
-
* tracks it wherever it moves — plus any memo sitting outside a wrap. When the document has
|
|
75
|
-
* NEITHER, NO observer is installed at all. That bail-out is the whole storefront case, not an edge
|
|
76
|
-
* one: this package emits neither selector (both are portal Liquid), so the old `<body>` fallback
|
|
77
|
-
* was the only branch a React / Web Component / `renderCertBlock` page could take, and it pinned a
|
|
78
|
-
* subtree + attributes + characterData observer — no `attributeFilter`, no coalescing — on the
|
|
79
|
-
* host's entire page for the life of the tab, re-running an enforce pass on EVERY mutation a busy
|
|
80
|
-
* storefront makes, to query a selector that could never match. A guard watching a page it cannot
|
|
81
|
-
* act on is pure cost; `enforceFtcDisclosure`'s `'unguarded'` result is the correctness half of the
|
|
82
|
-
* same gap.
|
|
83
|
-
*/
|
|
84
|
-
export declare function installFtcGuard(doc: Document): void;
|
|
85
|
-
//# sourceMappingURL=ftc-guard.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ftc-guard.d.ts","sourceRoot":"","sources":["../../src/modal/ftc-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,eAAO,MAAM,iBAAiB,oBAAoB,CAAA;AAClD,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAC5C,eAAO,MAAM,kBAAkB,uBAAuB,CAAA;AACtD,+FAA+F;AAC/F,eAAO,MAAM,oBAAoB,iCAAiC,CAAA;AAgClE,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAKzD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAUlD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,CAAA;AAEtE;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,QAAQ,GAAG,oBAAoB,CAWxE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CA0BnD"}
|
package/dist/modal/ftc-guard.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FTC disclosure tamper guard for the NATIVE Shopify cert render (iframe→native parity).
|
|
3
|
-
*
|
|
4
|
-
* In the cross-origin iframe model the expert-memo embed inlined the portal's
|
|
5
|
-
* `ftcTamperGuardScript()`: a kill switch that blanks the memo if the verbatim FTC
|
|
6
|
-
* material-connection disclosure is stripped, hidden, or altered — making the MSA §3
|
|
7
|
-
* anti-stripping covenant TECHNICALLY enforced, not just contractual. The native memo renders
|
|
8
|
-
* in the host theme's DOM (no sandbox), so without this it would lose that runtime enforcement.
|
|
9
|
-
* This module restores it: on load + on later DOM mutation it verifies the in-DOM
|
|
10
|
-
* `[data-ftc-line]` is present, carries the EXACT verbatim text, and is actually visible; if not,
|
|
11
|
-
* it neutralizes the memo with a neutral notice so the memo NEVER renders without its disclosure.
|
|
12
|
-
*
|
|
13
|
-
* Native-specific semantics (differ from the iframe, which only existed when a memo did):
|
|
14
|
-
* - Enforcement is keyed on the MEMO element (`.certrev-memo`). A certified article with NO
|
|
15
|
-
* memo legitimately has no `.certrev-memo` and no disclosure — the guard does NOT fire
|
|
16
|
-
* (no false positive on the no-memo path).
|
|
17
|
-
* - On tamper it neutralizes ONLY the memo (never `document.body` — we're in the host page).
|
|
18
|
-
* - The contributors card is governed by the badge re-verify (`revalidate.ts`), not this guard;
|
|
19
|
-
* the card carries only the one-word "Compensated" cue, never the guarded disclosure.
|
|
20
|
-
* - COVERAGE, and it is narrower than it reads: `.certrev-memo` / `.certrev-memo-wrap` are portal
|
|
21
|
-
* LIQUID markup — NOTHING in this package emits either. So the kill switch covers the Liquid
|
|
22
|
-
* memo surface and nothing else. `<CertBadge>` and `render-badge-html.ts` emit the same
|
|
23
|
-
* `[data-ftc-line]` hook inside a `.certrev-badge` (no memo), and `renderCertBlock` emits no
|
|
24
|
-
* hook at all; on those surfaces a pass verifies nothing and says so (`'unguarded'`) rather
|
|
25
|
-
* than reporting success, and no observer is installed at all. Widening enforcement to the
|
|
26
|
-
* badge is a live design decision, not an omission — see `FtcEnforcementResult`.
|
|
27
|
-
*
|
|
28
|
-
* Pure DOM, no crypto, no network. The expected text is the single source of truth
|
|
29
|
-
* `FTC_DISCLOSURE_LINE` (imported, not copied — so a reword can never desync this guard).
|
|
30
|
-
*/
|
|
31
|
-
import { FTC_DISCLOSURE_LINE } from './ftc-disclosure.js';
|
|
32
|
-
export const FTC_LINE_SELECTOR = '[data-ftc-line]';
|
|
33
|
-
export const MEMO_SELECTOR = '.certrev-memo';
|
|
34
|
-
export const MEMO_WRAP_SELECTOR = '.certrev-memo-wrap';
|
|
35
|
-
/** Marker on the neutral notice node that replaces a tampered memo (testable / idempotent). */
|
|
36
|
-
export const FTC_NEUTRALIZED_ATTR = 'data-certrev-ftc-neutralized';
|
|
37
|
-
/**
|
|
38
|
-
* Link qualification for the notice's CertREV anchor — a CertREV link on a brand's page that the
|
|
39
|
-
* brand did not author, so it carries the same qualification as every other anchor this package
|
|
40
|
-
* emits. Deliberately a LOCAL literal rather than an import of `CERTREV_LINK_REL`
|
|
41
|
-
* (`../components/rel.ts`): `src/modal/` imports NOTHING from `src/components/` by design — every
|
|
42
|
-
* shared string here (`ftc-disclosure.ts`, `logo-paths.ts`, `display-strings.ts`) is a modal-local
|
|
43
|
-
* copy so the browser IIFE never drags the SSR renderer in. `ftc-guard.test.ts` imports the
|
|
44
|
-
* components constant and asserts this value stays a superset of it, so the copies cannot drift.
|
|
45
|
-
*
|
|
46
|
-
* `noreferrer` rides ON TOP of the shared tokens (which omit it): this is the only anchor the
|
|
47
|
-
* package opens with `target="_blank"`, and it fires from a page whose operator has just stripped
|
|
48
|
-
* the disclosure — withholding the referrer there is worth the token.
|
|
49
|
-
*/
|
|
50
|
-
const NOTICE_LINK_REL = 'nofollow sponsored noopener noreferrer';
|
|
51
|
-
/** Documents already guarded, so a double script-load can't double-bind. */
|
|
52
|
-
const guardedDocs = new WeakSet();
|
|
53
|
-
/** Visible = not display:none / visibility:hidden / opacity:0 and a non-zero box. */
|
|
54
|
-
function isVisible(el) {
|
|
55
|
-
const win = el.ownerDocument.defaultView;
|
|
56
|
-
// No view (detached / SSR): we can't measure, so don't treat as hidden — the text/presence
|
|
57
|
-
// checks still apply; only the layout-based hide vector is skipped.
|
|
58
|
-
if (!win)
|
|
59
|
-
return true;
|
|
60
|
-
const s = win.getComputedStyle(el);
|
|
61
|
-
if (s.display === 'none' || s.visibility === 'hidden' || Number.parseFloat(s.opacity || '1') === 0)
|
|
62
|
-
return false;
|
|
63
|
-
const r = el.getBoundingClientRect();
|
|
64
|
-
return r.width > 0 && r.height > 0;
|
|
65
|
-
}
|
|
66
|
-
/** True when a memo's disclosure line is present, exactly verbatim, and visible. */
|
|
67
|
-
export function isDisclosureIntact(memo) {
|
|
68
|
-
const line = memo.querySelector(FTC_LINE_SELECTOR);
|
|
69
|
-
if (!line)
|
|
70
|
-
return false;
|
|
71
|
-
if ((line.textContent || '').trim() !== FTC_DISCLOSURE_LINE)
|
|
72
|
-
return false;
|
|
73
|
-
return isVisible(line);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Replace a tampered memo with a neutral, self-contained notice — no reviewer attribution, no
|
|
77
|
-
* seal — so the surface can never read as an endorsement without its required disclosure.
|
|
78
|
-
*/
|
|
79
|
-
export function neutralizeMemo(memo) {
|
|
80
|
-
const doc = memo.ownerDocument;
|
|
81
|
-
const notice = doc.createElement('div');
|
|
82
|
-
notice.setAttribute(FTC_NEUTRALIZED_ATTR, '');
|
|
83
|
-
notice.style.cssText =
|
|
84
|
-
'font-family:system-ui,-apple-system,sans-serif;font-size:12px;color:#555;padding:12px;line-height:1.4';
|
|
85
|
-
notice.innerHTML =
|
|
86
|
-
'This CertREV verification cannot be displayed because its required disclosure was not shown. ' +
|
|
87
|
-
`<a href="https://certrev.com" style="color:#555" target="_blank" rel="${NOTICE_LINK_REL}">Learn more</a>.`;
|
|
88
|
-
memo.replaceWith(notice);
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Enforce the disclosure across every memo in the doc: neutralize any whose disclosure isn't
|
|
92
|
-
* intact. A doc with no `.certrev-memo` is a no-op — see `FtcEnforcementResult` for why that is
|
|
93
|
-
* `'unguarded'` and not success.
|
|
94
|
-
*/
|
|
95
|
-
export function enforceFtcDisclosure(doc) {
|
|
96
|
-
const memos = Array.from(doc.querySelectorAll(MEMO_SELECTOR));
|
|
97
|
-
if (!memos.length)
|
|
98
|
-
return 'unguarded';
|
|
99
|
-
let result = 'intact';
|
|
100
|
-
for (const memo of memos) {
|
|
101
|
-
if (!isDisclosureIntact(memo)) {
|
|
102
|
-
result = 'tampered';
|
|
103
|
-
neutralizeMemo(memo);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Install the guard once per document: enforce now (on DOM ready) and re-enforce on later DOM
|
|
110
|
-
* mutations within the guarded subtree(s) — so a host script that strips / hides / edits the
|
|
111
|
-
* disclosure after load still trips the kill switch. Idempotent.
|
|
112
|
-
*
|
|
113
|
-
* The observer scope is the memo wrap(s) — placement.ts relocates a wrap as a node, so the observer
|
|
114
|
-
* tracks it wherever it moves — plus any memo sitting outside a wrap. When the document has
|
|
115
|
-
* NEITHER, NO observer is installed at all. That bail-out is the whole storefront case, not an edge
|
|
116
|
-
* one: this package emits neither selector (both are portal Liquid), so the old `<body>` fallback
|
|
117
|
-
* was the only branch a React / Web Component / `renderCertBlock` page could take, and it pinned a
|
|
118
|
-
* subtree + attributes + characterData observer — no `attributeFilter`, no coalescing — on the
|
|
119
|
-
* host's entire page for the life of the tab, re-running an enforce pass on EVERY mutation a busy
|
|
120
|
-
* storefront makes, to query a selector that could never match. A guard watching a page it cannot
|
|
121
|
-
* act on is pure cost; `enforceFtcDisclosure`'s `'unguarded'` result is the correctness half of the
|
|
122
|
-
* same gap.
|
|
123
|
-
*/
|
|
124
|
-
export function installFtcGuard(doc) {
|
|
125
|
-
if (guardedDocs.has(doc))
|
|
126
|
-
return;
|
|
127
|
-
guardedDocs.add(doc);
|
|
128
|
-
const run = () => {
|
|
129
|
-
enforceFtcDisclosure(doc);
|
|
130
|
-
};
|
|
131
|
-
const start = () => {
|
|
132
|
-
run();
|
|
133
|
-
const MO = doc.defaultView?.MutationObserver;
|
|
134
|
-
if (!MO)
|
|
135
|
-
return;
|
|
136
|
-
// The guarded roots: the stable wrap(s), plus any memo not inside one (in the Liquid shape a
|
|
137
|
-
// wrap always contains its memo, so this is normally just the wraps). No root ⇒ no observer.
|
|
138
|
-
const wraps = Array.from(doc.querySelectorAll(MEMO_WRAP_SELECTOR));
|
|
139
|
-
const looseMemos = Array.from(doc.querySelectorAll(MEMO_SELECTOR)).filter((memo) => !memo.closest(MEMO_WRAP_SELECTOR));
|
|
140
|
-
for (const node of [...wraps, ...looseMemos]) {
|
|
141
|
-
const mo = new MO(run);
|
|
142
|
-
mo.observe(node, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
if (doc.readyState === 'loading')
|
|
146
|
-
doc.addEventListener('DOMContentLoaded', start, { once: true });
|
|
147
|
-
else
|
|
148
|
-
start();
|
|
149
|
-
}
|
|
150
|
-
//# sourceMappingURL=ftc-guard.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ftc-guard.js","sourceRoot":"","sources":["../../src/modal/ftc-guard.ts"],"names":[],"mappings":"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"}
|
package/src/modal/ftc-guard.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FTC disclosure tamper guard for the NATIVE Shopify cert render (iframe→native parity).
|
|
3
|
-
*
|
|
4
|
-
* In the cross-origin iframe model the expert-memo embed inlined the portal's
|
|
5
|
-
* `ftcTamperGuardScript()`: a kill switch that blanks the memo if the verbatim FTC
|
|
6
|
-
* material-connection disclosure is stripped, hidden, or altered — making the MSA §3
|
|
7
|
-
* anti-stripping covenant TECHNICALLY enforced, not just contractual. The native memo renders
|
|
8
|
-
* in the host theme's DOM (no sandbox), so without this it would lose that runtime enforcement.
|
|
9
|
-
* This module restores it: on load + on later DOM mutation it verifies the in-DOM
|
|
10
|
-
* `[data-ftc-line]` is present, carries the EXACT verbatim text, and is actually visible; if not,
|
|
11
|
-
* it neutralizes the memo with a neutral notice so the memo NEVER renders without its disclosure.
|
|
12
|
-
*
|
|
13
|
-
* Native-specific semantics (differ from the iframe, which only existed when a memo did):
|
|
14
|
-
* - Enforcement is keyed on the MEMO element (`.certrev-memo`). A certified article with NO
|
|
15
|
-
* memo legitimately has no `.certrev-memo` and no disclosure — the guard does NOT fire
|
|
16
|
-
* (no false positive on the no-memo path).
|
|
17
|
-
* - On tamper it neutralizes ONLY the memo (never `document.body` — we're in the host page).
|
|
18
|
-
* - The contributors card is governed by the badge re-verify (`revalidate.ts`), not this guard;
|
|
19
|
-
* the card carries only the one-word "Compensated" cue, never the guarded disclosure.
|
|
20
|
-
* - COVERAGE, and it is narrower than it reads: `.certrev-memo` / `.certrev-memo-wrap` are portal
|
|
21
|
-
* LIQUID markup — NOTHING in this package emits either. So the kill switch covers the Liquid
|
|
22
|
-
* memo surface and nothing else. `<CertBadge>` and `render-badge-html.ts` emit the same
|
|
23
|
-
* `[data-ftc-line]` hook inside a `.certrev-badge` (no memo), and `renderCertBlock` emits no
|
|
24
|
-
* hook at all; on those surfaces a pass verifies nothing and says so (`'unguarded'`) rather
|
|
25
|
-
* than reporting success, and no observer is installed at all. Widening enforcement to the
|
|
26
|
-
* badge is a live design decision, not an omission — see `FtcEnforcementResult`.
|
|
27
|
-
*
|
|
28
|
-
* Pure DOM, no crypto, no network. The expected text is the single source of truth
|
|
29
|
-
* `FTC_DISCLOSURE_LINE` (imported, not copied — so a reword can never desync this guard).
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import { FTC_DISCLOSURE_LINE } from './ftc-disclosure.js'
|
|
33
|
-
|
|
34
|
-
export const FTC_LINE_SELECTOR = '[data-ftc-line]'
|
|
35
|
-
export const MEMO_SELECTOR = '.certrev-memo'
|
|
36
|
-
export const MEMO_WRAP_SELECTOR = '.certrev-memo-wrap'
|
|
37
|
-
/** Marker on the neutral notice node that replaces a tampered memo (testable / idempotent). */
|
|
38
|
-
export const FTC_NEUTRALIZED_ATTR = 'data-certrev-ftc-neutralized'
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Link qualification for the notice's CertREV anchor — a CertREV link on a brand's page that the
|
|
42
|
-
* brand did not author, so it carries the same qualification as every other anchor this package
|
|
43
|
-
* emits. Deliberately a LOCAL literal rather than an import of `CERTREV_LINK_REL`
|
|
44
|
-
* (`../components/rel.ts`): `src/modal/` imports NOTHING from `src/components/` by design — every
|
|
45
|
-
* shared string here (`ftc-disclosure.ts`, `logo-paths.ts`, `display-strings.ts`) is a modal-local
|
|
46
|
-
* copy so the browser IIFE never drags the SSR renderer in. `ftc-guard.test.ts` imports the
|
|
47
|
-
* components constant and asserts this value stays a superset of it, so the copies cannot drift.
|
|
48
|
-
*
|
|
49
|
-
* `noreferrer` rides ON TOP of the shared tokens (which omit it): this is the only anchor the
|
|
50
|
-
* package opens with `target="_blank"`, and it fires from a page whose operator has just stripped
|
|
51
|
-
* the disclosure — withholding the referrer there is worth the token.
|
|
52
|
-
*/
|
|
53
|
-
const NOTICE_LINK_REL = 'nofollow sponsored noopener noreferrer'
|
|
54
|
-
|
|
55
|
-
/** Documents already guarded, so a double script-load can't double-bind. */
|
|
56
|
-
const guardedDocs = new WeakSet<Document>()
|
|
57
|
-
|
|
58
|
-
/** Visible = not display:none / visibility:hidden / opacity:0 and a non-zero box. */
|
|
59
|
-
function isVisible(el: Element): boolean {
|
|
60
|
-
const win = el.ownerDocument.defaultView
|
|
61
|
-
// No view (detached / SSR): we can't measure, so don't treat as hidden — the text/presence
|
|
62
|
-
// checks still apply; only the layout-based hide vector is skipped.
|
|
63
|
-
if (!win) return true
|
|
64
|
-
const s = win.getComputedStyle(el)
|
|
65
|
-
if (s.display === 'none' || s.visibility === 'hidden' || Number.parseFloat(s.opacity || '1') === 0) return false
|
|
66
|
-
const r = el.getBoundingClientRect()
|
|
67
|
-
return r.width > 0 && r.height > 0
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** True when a memo's disclosure line is present, exactly verbatim, and visible. */
|
|
71
|
-
export function isDisclosureIntact(memo: Element): boolean {
|
|
72
|
-
const line = memo.querySelector(FTC_LINE_SELECTOR)
|
|
73
|
-
if (!line) return false
|
|
74
|
-
if ((line.textContent || '').trim() !== FTC_DISCLOSURE_LINE) return false
|
|
75
|
-
return isVisible(line)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Replace a tampered memo with a neutral, self-contained notice — no reviewer attribution, no
|
|
80
|
-
* seal — so the surface can never read as an endorsement without its required disclosure.
|
|
81
|
-
*/
|
|
82
|
-
export function neutralizeMemo(memo: Element): void {
|
|
83
|
-
const doc = memo.ownerDocument
|
|
84
|
-
const notice = doc.createElement('div')
|
|
85
|
-
notice.setAttribute(FTC_NEUTRALIZED_ATTR, '')
|
|
86
|
-
notice.style.cssText =
|
|
87
|
-
'font-family:system-ui,-apple-system,sans-serif;font-size:12px;color:#555;padding:12px;line-height:1.4'
|
|
88
|
-
notice.innerHTML =
|
|
89
|
-
'This CertREV verification cannot be displayed because its required disclosure was not shown. ' +
|
|
90
|
-
`<a href="https://certrev.com" style="color:#555" target="_blank" rel="${NOTICE_LINK_REL}">Learn more</a>.`
|
|
91
|
-
memo.replaceWith(notice)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* What ONE enforcement pass actually established:
|
|
96
|
-
* - `'intact'` — at least one memo was checked, and every one carried its verbatim, visible line.
|
|
97
|
-
* - `'tampered'` — at least one memo failed the check and was neutralized.
|
|
98
|
-
* - `'unguarded'` — the document holds no `.certrev-memo`, so the pass checked nothing.
|
|
99
|
-
*
|
|
100
|
-
* `'unguarded'` used to be reported as `true`, indistinguishable from `'intact'`, and that
|
|
101
|
-
* conflation was a correctness bug, not a cosmetic one: `.certrev-memo` is portal Liquid markup
|
|
102
|
-
* this package never emits, so on every React / Web Component / `renderCertBlock` surface the pass
|
|
103
|
-
* finds nothing and answered "all intact" about a disclosure it had never looked at — the kill
|
|
104
|
-
* switch silently reporting success on exactly the surfaces it does not protect.
|
|
105
|
-
*
|
|
106
|
-
* The legitimate no-memo certified article (card only, no memo, no disclosure owed) lands here too,
|
|
107
|
-
* and the guard genuinely cannot tell the two apart from the DOM alone. That is the point of a
|
|
108
|
-
* third state rather than a second boolean: "nothing was verified" is the honest answer to both,
|
|
109
|
-
* and it is the caller — which knows which surface it rendered — that can tell whether that is
|
|
110
|
-
* expected or a coverage gap.
|
|
111
|
-
*/
|
|
112
|
-
export type FtcEnforcementResult = 'intact' | 'tampered' | 'unguarded'
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Enforce the disclosure across every memo in the doc: neutralize any whose disclosure isn't
|
|
116
|
-
* intact. A doc with no `.certrev-memo` is a no-op — see `FtcEnforcementResult` for why that is
|
|
117
|
-
* `'unguarded'` and not success.
|
|
118
|
-
*/
|
|
119
|
-
export function enforceFtcDisclosure(doc: Document): FtcEnforcementResult {
|
|
120
|
-
const memos = Array.from(doc.querySelectorAll(MEMO_SELECTOR))
|
|
121
|
-
if (!memos.length) return 'unguarded'
|
|
122
|
-
let result: FtcEnforcementResult = 'intact'
|
|
123
|
-
for (const memo of memos) {
|
|
124
|
-
if (!isDisclosureIntact(memo)) {
|
|
125
|
-
result = 'tampered'
|
|
126
|
-
neutralizeMemo(memo)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return result
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Install the guard once per document: enforce now (on DOM ready) and re-enforce on later DOM
|
|
134
|
-
* mutations within the guarded subtree(s) — so a host script that strips / hides / edits the
|
|
135
|
-
* disclosure after load still trips the kill switch. Idempotent.
|
|
136
|
-
*
|
|
137
|
-
* The observer scope is the memo wrap(s) — placement.ts relocates a wrap as a node, so the observer
|
|
138
|
-
* tracks it wherever it moves — plus any memo sitting outside a wrap. When the document has
|
|
139
|
-
* NEITHER, NO observer is installed at all. That bail-out is the whole storefront case, not an edge
|
|
140
|
-
* one: this package emits neither selector (both are portal Liquid), so the old `<body>` fallback
|
|
141
|
-
* was the only branch a React / Web Component / `renderCertBlock` page could take, and it pinned a
|
|
142
|
-
* subtree + attributes + characterData observer — no `attributeFilter`, no coalescing — on the
|
|
143
|
-
* host's entire page for the life of the tab, re-running an enforce pass on EVERY mutation a busy
|
|
144
|
-
* storefront makes, to query a selector that could never match. A guard watching a page it cannot
|
|
145
|
-
* act on is pure cost; `enforceFtcDisclosure`'s `'unguarded'` result is the correctness half of the
|
|
146
|
-
* same gap.
|
|
147
|
-
*/
|
|
148
|
-
export function installFtcGuard(doc: Document): void {
|
|
149
|
-
if (guardedDocs.has(doc)) return
|
|
150
|
-
guardedDocs.add(doc)
|
|
151
|
-
|
|
152
|
-
const run = (): void => {
|
|
153
|
-
enforceFtcDisclosure(doc)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const start = (): void => {
|
|
157
|
-
run()
|
|
158
|
-
const MO = doc.defaultView?.MutationObserver
|
|
159
|
-
if (!MO) return
|
|
160
|
-
// The guarded roots: the stable wrap(s), plus any memo not inside one (in the Liquid shape a
|
|
161
|
-
// wrap always contains its memo, so this is normally just the wraps). No root ⇒ no observer.
|
|
162
|
-
const wraps = Array.from(doc.querySelectorAll(MEMO_WRAP_SELECTOR))
|
|
163
|
-
const looseMemos = Array.from(doc.querySelectorAll(MEMO_SELECTOR)).filter(
|
|
164
|
-
(memo) => !memo.closest(MEMO_WRAP_SELECTOR),
|
|
165
|
-
)
|
|
166
|
-
for (const node of [...wraps, ...looseMemos]) {
|
|
167
|
-
const mo = new MO(run)
|
|
168
|
-
mo.observe(node, { childList: true, subtree: true, characterData: true, attributes: true })
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (doc.readyState === 'loading') doc.addEventListener('DOMContentLoaded', start, { once: true })
|
|
173
|
-
else start()
|
|
174
|
-
}
|