@runhalo/engine 0.1.0 → 0.3.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/README.md +58 -0
- package/dist/fixer.d.ts +99 -0
- package/dist/fixer.js +274 -0
- package/dist/fixer.js.map +1 -0
- package/dist/framework-detect.d.ts +15 -0
- package/dist/framework-detect.js +108 -0
- package/dist/framework-detect.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +94 -3
- package/dist/index.js.map +1 -1
- package/dist/scaffold-engine.d.ts +67 -0
- package/dist/scaffold-engine.js +104 -0
- package/dist/scaffold-engine.js.map +1 -0
- package/dist/scaffolds/index.d.ts +23 -0
- package/dist/scaffolds/index.js +19 -0
- package/dist/scaffolds/index.js.map +1 -0
- package/dist/scaffolds/templates/age-gate-auth.d.ts +7 -0
- package/dist/scaffolds/templates/age-gate-auth.js +254 -0
- package/dist/scaffolds/templates/age-gate-auth.js.map +1 -0
- package/dist/scaffolds/templates/consent-cookies.d.ts +7 -0
- package/dist/scaffolds/templates/consent-cookies.js +253 -0
- package/dist/scaffolds/templates/consent-cookies.js.map +1 -0
- package/dist/scaffolds/templates/pii-sanitizer.d.ts +7 -0
- package/dist/scaffolds/templates/pii-sanitizer.js +263 -0
- package/dist/scaffolds/templates/pii-sanitizer.js.map +1 -0
- package/dist/scaffolds/templates/retention-policy.d.ts +7 -0
- package/dist/scaffolds/templates/retention-policy.js +247 -0
- package/dist/scaffolds/templates/retention-policy.js.map +1 -0
- package/dist/scoring.d.ts +52 -0
- package/dist/scoring.js +104 -0
- package/dist/scoring.js.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Age Gate Authentication Scaffold
|
|
4
|
+
* Addresses: coppa-auth-001 (CRITICAL)
|
|
5
|
+
* Generates an age verification gate that blocks users under 13
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ageGateAuthTemplate = void 0;
|
|
9
|
+
function generateReact(typescript) {
|
|
10
|
+
const ext = typescript ? 'tsx' : 'jsx';
|
|
11
|
+
const typeAnnotation = typescript ? ': string' : '';
|
|
12
|
+
const stateType = typescript ? '<boolean>' : '';
|
|
13
|
+
const eventType = typescript ? ': React.FormEvent<HTMLFormElement>' : '';
|
|
14
|
+
const changeType = typescript ? ': React.ChangeEvent<HTMLInputElement>' : '';
|
|
15
|
+
const propsType = typescript
|
|
16
|
+
? `\ninterface AgeGateProps {\n children: React.ReactNode;\n minimumAge?: number;\n onVerified?: () => void;\n onBlocked?: () => void;\n}\n`
|
|
17
|
+
: '';
|
|
18
|
+
const propsArg = typescript ? '{ children, minimumAge = 13, onVerified, onBlocked }: AgeGateProps' : '{ children, minimumAge = 13, onVerified, onBlocked }';
|
|
19
|
+
const component = `/**
|
|
20
|
+
* AgeGate — COPPA-compliant age verification component
|
|
21
|
+
*
|
|
22
|
+
* Blocks access for users under the minimum age (default: 13).
|
|
23
|
+
* Uses date-of-birth verification (not simple yes/no buttons).
|
|
24
|
+
* Stores verification in sessionStorage (not persistent — COPPA best practice).
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* <AgeGate>
|
|
28
|
+
* <YourProtectedContent />
|
|
29
|
+
* </AgeGate>
|
|
30
|
+
*
|
|
31
|
+
* Generated by Halo (runhalo.dev) — COPPA compliance scaffold
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import React, { useState, useCallback } from 'react';
|
|
35
|
+
${propsType}
|
|
36
|
+
const AGE_GATE_KEY = 'halo_age_verified';
|
|
37
|
+
|
|
38
|
+
export function AgeGate(${propsArg}) {
|
|
39
|
+
const [verified, setVerified] = useState${stateType}(() => {
|
|
40
|
+
// Check sessionStorage (not localStorage — don't persist across sessions)
|
|
41
|
+
if (typeof window !== 'undefined') {
|
|
42
|
+
return sessionStorage.getItem(AGE_GATE_KEY) === 'true';
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
const [dob, setDob] = useState${typescript ? '<string>' : ''}('');
|
|
47
|
+
const [error, setError] = useState${typescript ? '<string>' : ''}('');
|
|
48
|
+
const [blocked, setBlocked] = useState${stateType}(false);
|
|
49
|
+
|
|
50
|
+
const handleSubmit = useCallback((e${eventType}) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
setError('');
|
|
53
|
+
|
|
54
|
+
if (!dob) {
|
|
55
|
+
setError('Please enter your date of birth.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const birthDate = new Date(dob);
|
|
60
|
+
const today = new Date();
|
|
61
|
+
let age = today.getFullYear() - birthDate.getFullYear();
|
|
62
|
+
const monthDiff = today.getMonth() - birthDate.getMonth();
|
|
63
|
+
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
|
64
|
+
age--;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (age >= minimumAge) {
|
|
68
|
+
sessionStorage.setItem(AGE_GATE_KEY, 'true');
|
|
69
|
+
setVerified(true);
|
|
70
|
+
onVerified?.();
|
|
71
|
+
} else {
|
|
72
|
+
setBlocked(true);
|
|
73
|
+
onBlocked?.();
|
|
74
|
+
}
|
|
75
|
+
}, [dob, minimumAge, onVerified, onBlocked]);
|
|
76
|
+
|
|
77
|
+
if (verified) {
|
|
78
|
+
return <>{children}</>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (blocked) {
|
|
82
|
+
return (
|
|
83
|
+
<div style={{ textAlign: 'center', padding: '2rem', maxWidth: '400px', margin: '0 auto' }}>
|
|
84
|
+
<h2>Access Restricted</h2>
|
|
85
|
+
<p>
|
|
86
|
+
Sorry, you must be at least {minimumAge} years old to access this content.
|
|
87
|
+
If you believe this is an error, please contact your parent or guardian.
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div style={{ textAlign: 'center', padding: '2rem', maxWidth: '400px', margin: '0 auto' }}>
|
|
95
|
+
<h2>Age Verification Required</h2>
|
|
96
|
+
<p>Please enter your date of birth to continue.</p>
|
|
97
|
+
<form onSubmit={handleSubmit} style={{ marginTop: '1rem' }}>
|
|
98
|
+
<label htmlFor="halo-dob" style={{ display: 'block', marginBottom: '0.5rem' }}>
|
|
99
|
+
Date of Birth
|
|
100
|
+
</label>
|
|
101
|
+
<input
|
|
102
|
+
id="halo-dob"
|
|
103
|
+
type="date"
|
|
104
|
+
value={dob}
|
|
105
|
+
onChange={(e${changeType}) => setDob(e.target.value)}
|
|
106
|
+
required
|
|
107
|
+
max={new Date().toISOString().split('T')[0]}
|
|
108
|
+
style={{ padding: '0.5rem', fontSize: '1rem', width: '100%', boxSizing: 'border-box' }}
|
|
109
|
+
/>
|
|
110
|
+
{error && <p style={{ color: 'red', marginTop: '0.5rem' }}>{error}</p>}
|
|
111
|
+
<button
|
|
112
|
+
type="submit"
|
|
113
|
+
style={{
|
|
114
|
+
marginTop: '1rem',
|
|
115
|
+
padding: '0.75rem 2rem',
|
|
116
|
+
fontSize: '1rem',
|
|
117
|
+
cursor: 'pointer',
|
|
118
|
+
width: '100%',
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
Verify Age
|
|
122
|
+
</button>
|
|
123
|
+
</form>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default AgeGate;
|
|
129
|
+
`;
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
relativePath: `components/AgeGate.${ext}`,
|
|
133
|
+
content: component,
|
|
134
|
+
description: 'COPPA-compliant age gate component with DOB verification',
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
function generatePlainJS() {
|
|
139
|
+
const code = `/**
|
|
140
|
+
* AgeGate — COPPA-compliant age verification
|
|
141
|
+
*
|
|
142
|
+
* Blocks access for users under 13.
|
|
143
|
+
* Uses date-of-birth verification (not simple yes/no).
|
|
144
|
+
* Stores verification in sessionStorage (not persistent).
|
|
145
|
+
*
|
|
146
|
+
* Usage:
|
|
147
|
+
* const ageGate = new AgeGate(document.getElementById('app'));
|
|
148
|
+
* ageGate.onVerified = () => { showProtectedContent(); };
|
|
149
|
+
*
|
|
150
|
+
* Generated by Halo (runhalo.dev) — COPPA compliance scaffold
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
class AgeGate {
|
|
154
|
+
constructor(container, options = {}) {
|
|
155
|
+
this.container = container;
|
|
156
|
+
this.minimumAge = options.minimumAge || 13;
|
|
157
|
+
this.onVerified = options.onVerified || null;
|
|
158
|
+
this.onBlocked = options.onBlocked || null;
|
|
159
|
+
this.storageKey = 'halo_age_verified';
|
|
160
|
+
|
|
161
|
+
// Check if already verified this session
|
|
162
|
+
if (sessionStorage.getItem(this.storageKey) === 'true') {
|
|
163
|
+
if (this.onVerified) this.onVerified();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.render();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
render() {
|
|
171
|
+
const today = new Date().toISOString().split('T')[0];
|
|
172
|
+
this.container.innerHTML = \`
|
|
173
|
+
<div style="text-align: center; padding: 2rem; max-width: 400px; margin: 0 auto;">
|
|
174
|
+
<h2>Age Verification Required</h2>
|
|
175
|
+
<p>Please enter your date of birth to continue.</p>
|
|
176
|
+
<form id="halo-age-form" style="margin-top: 1rem;">
|
|
177
|
+
<label for="halo-dob" style="display: block; margin-bottom: 0.5rem;">Date of Birth</label>
|
|
178
|
+
<input id="halo-dob" type="date" required max="\${today}"
|
|
179
|
+
style="padding: 0.5rem; font-size: 1rem; width: 100%; box-sizing: border-box;" />
|
|
180
|
+
<div id="halo-age-error" style="color: red; margin-top: 0.5rem; display: none;"></div>
|
|
181
|
+
<button type="submit"
|
|
182
|
+
style="margin-top: 1rem; padding: 0.75rem 2rem; font-size: 1rem; cursor: pointer; width: 100%;">
|
|
183
|
+
Verify Age
|
|
184
|
+
</button>
|
|
185
|
+
</form>
|
|
186
|
+
</div>
|
|
187
|
+
\`;
|
|
188
|
+
|
|
189
|
+
this.container.querySelector('#halo-age-form').addEventListener('submit', (e) => {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
this.verify();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
verify() {
|
|
196
|
+
const dobInput = this.container.querySelector('#halo-dob');
|
|
197
|
+
const errorDiv = this.container.querySelector('#halo-age-error');
|
|
198
|
+
|
|
199
|
+
if (!dobInput.value) {
|
|
200
|
+
errorDiv.textContent = 'Please enter your date of birth.';
|
|
201
|
+
errorDiv.style.display = 'block';
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const birthDate = new Date(dobInput.value);
|
|
206
|
+
const today = new Date();
|
|
207
|
+
let age = today.getFullYear() - birthDate.getFullYear();
|
|
208
|
+
const monthDiff = today.getMonth() - birthDate.getMonth();
|
|
209
|
+
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
|
210
|
+
age--;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (age >= this.minimumAge) {
|
|
214
|
+
sessionStorage.setItem(this.storageKey, 'true');
|
|
215
|
+
if (this.onVerified) this.onVerified();
|
|
216
|
+
} else {
|
|
217
|
+
this.container.innerHTML = \`
|
|
218
|
+
<div style="text-align: center; padding: 2rem; max-width: 400px; margin: 0 auto;">
|
|
219
|
+
<h2>Access Restricted</h2>
|
|
220
|
+
<p>Sorry, you must be at least \${this.minimumAge} years old to access this content.</p>
|
|
221
|
+
</div>
|
|
222
|
+
\`;
|
|
223
|
+
if (this.onBlocked) this.onBlocked();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// CommonJS + ESM compatibility
|
|
229
|
+
if (typeof module !== 'undefined') module.exports = { AgeGate };
|
|
230
|
+
`;
|
|
231
|
+
return [
|
|
232
|
+
{
|
|
233
|
+
relativePath: 'age-gate.js',
|
|
234
|
+
content: code,
|
|
235
|
+
description: 'COPPA-compliant age gate with DOB verification (vanilla JS)',
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
exports.ageGateAuthTemplate = {
|
|
240
|
+
scaffoldId: 'age-gate-auth',
|
|
241
|
+
name: 'Age Gate Authentication',
|
|
242
|
+
description: 'COPPA-compliant age verification gate that blocks users under 13 using date-of-birth verification',
|
|
243
|
+
ruleIds: ['coppa-auth-001'],
|
|
244
|
+
generate(framework, typescript) {
|
|
245
|
+
switch (framework) {
|
|
246
|
+
case 'react':
|
|
247
|
+
case 'nextjs':
|
|
248
|
+
return generateReact(typescript);
|
|
249
|
+
default:
|
|
250
|
+
return generatePlainJS();
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
//# sourceMappingURL=age-gate-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"age-gate-auth.js","sourceRoot":"","sources":["../../../src/scaffolds/templates/age-gate-auth.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAKH,SAAS,aAAa,CAAC,UAAmB;IACxC,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;IACvC,MAAM,cAAc,GAAG,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IACpD,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IAChD,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzE,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,uCAAuC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7E,MAAM,SAAS,GAAG,UAAU;QAC1B,CAAC,CAAC,8IAA8I;QAChJ,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,oEAAoE,CAAC,CAAC,CAAC,sDAAsD,CAAC;IAE5J,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;EAgBlB,SAAS;;;0BAGe,QAAQ;4CACU,SAAS;;;;;;;kCAOnB,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;sCACxB,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;0CACxB,SAAS;;uCAEZ,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAuDxB,UAAU;;;;;;;;;;;;;;;;;;;;;;;;CAwBjC,CAAC;IAEA,OAAO;QACL;YACE,YAAY,EAAE,sBAAsB,GAAG,EAAE;YACzC,OAAO,EAAE,SAAS;YAClB,WAAW,EAAE,0DAA0D;SACxE;KACF,CAAC;AACJ,CAAC;AAED,SAAS,eAAe;IACtB,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2Fd,CAAC;IAEA,OAAO;QACL;YACE,YAAY,EAAE,aAAa;YAC3B,OAAO,EAAE,IAAI;YACb,WAAW,EAAE,6DAA6D;SAC3E;KACF,CAAC;AACJ,CAAC;AAEY,QAAA,mBAAmB,GAAqB;IACnD,UAAU,EAAE,eAAe;IAC3B,IAAI,EAAE,yBAAyB;IAC/B,WAAW,EAAE,mGAAmG;IAChH,OAAO,EAAE,CAAC,gBAAgB,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,UAAU;QAC5B,QAAQ,SAAS,EAAE,CAAC;YAClB,KAAK,OAAO,CAAC;YACb,KAAK,QAAQ;gBACX,OAAO,aAAa,CAAC,UAAU,CAAC,CAAC;YACnC;gBACE,OAAO,eAAe,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cookie Consent Banner Scaffold
|
|
4
|
+
* Addresses: coppa-cookies-016
|
|
5
|
+
* Generates a privacy-first cookie consent banner (deny by default)
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.consentCookiesTemplate = void 0;
|
|
9
|
+
function generateReact(typescript) {
|
|
10
|
+
const ext = typescript ? 'tsx' : 'jsx';
|
|
11
|
+
const propsType = typescript
|
|
12
|
+
? `\ninterface CookieConsentProps {\n onAccept?: () => void;\n onDecline?: () => void;\n privacyPolicyUrl?: string;\n}\n`
|
|
13
|
+
: '';
|
|
14
|
+
const propsArg = typescript
|
|
15
|
+
? '{ onAccept, onDecline, privacyPolicyUrl = "/privacy" }: CookieConsentProps'
|
|
16
|
+
: '{ onAccept, onDecline, privacyPolicyUrl = "/privacy" }';
|
|
17
|
+
const component = `/**
|
|
18
|
+
* CookieConsentBanner — COPPA-compliant cookie consent
|
|
19
|
+
*
|
|
20
|
+
* Privacy-first: all non-essential cookies are OFF by default.
|
|
21
|
+
* No cookies are set until the user explicitly accepts.
|
|
22
|
+
* Consent is stored in localStorage (not a cookie itself).
|
|
23
|
+
*
|
|
24
|
+
* COPPA requirement: Children's sites must not use tracking cookies
|
|
25
|
+
* without verifiable parental consent.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* <CookieConsentBanner
|
|
29
|
+
* onAccept={() => enableAnalytics()}
|
|
30
|
+
* onDecline={() => console.log('User declined cookies')}
|
|
31
|
+
* />
|
|
32
|
+
*
|
|
33
|
+
* Generated by Halo (runhalo.dev) — COPPA compliance scaffold
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import React, { useState, useEffect } from 'react';
|
|
37
|
+
${propsType}
|
|
38
|
+
const CONSENT_KEY = 'halo_cookie_consent';
|
|
39
|
+
|
|
40
|
+
export function CookieConsentBanner(${propsArg}) {
|
|
41
|
+
const [visible, setVisible] = useState(false);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
// Only show if user hasn't made a choice yet
|
|
45
|
+
const stored = localStorage.getItem(CONSENT_KEY);
|
|
46
|
+
if (stored === null) {
|
|
47
|
+
setVisible(true);
|
|
48
|
+
}
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const handleAccept = () => {
|
|
52
|
+
localStorage.setItem(CONSENT_KEY, 'accepted');
|
|
53
|
+
setVisible(false);
|
|
54
|
+
onAccept?.();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleDecline = () => {
|
|
58
|
+
localStorage.setItem(CONSENT_KEY, 'declined');
|
|
59
|
+
setVisible(false);
|
|
60
|
+
onDecline?.();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (!visible) return null;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
role="dialog"
|
|
68
|
+
aria-label="Cookie consent"
|
|
69
|
+
style={{
|
|
70
|
+
position: 'fixed',
|
|
71
|
+
bottom: 0,
|
|
72
|
+
left: 0,
|
|
73
|
+
right: 0,
|
|
74
|
+
backgroundColor: '#1a1a2e',
|
|
75
|
+
color: '#e0e0e0',
|
|
76
|
+
padding: '1rem 1.5rem',
|
|
77
|
+
display: 'flex',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
justifyContent: 'space-between',
|
|
80
|
+
flexWrap: 'wrap',
|
|
81
|
+
gap: '1rem',
|
|
82
|
+
zIndex: 9999,
|
|
83
|
+
boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.3)',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<p style={{ margin: 0, flex: 1, minWidth: '200px', fontSize: '0.9rem' }}>
|
|
87
|
+
We use cookies to improve your experience.{' '}
|
|
88
|
+
<a href={privacyPolicyUrl} style={{ color: '#818cf8', textDecoration: 'underline' }}>
|
|
89
|
+
Privacy Policy
|
|
90
|
+
</a>
|
|
91
|
+
</p>
|
|
92
|
+
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
93
|
+
<button
|
|
94
|
+
onClick={handleDecline}
|
|
95
|
+
style={{
|
|
96
|
+
padding: '0.5rem 1.25rem',
|
|
97
|
+
border: '1px solid #555',
|
|
98
|
+
backgroundColor: 'transparent',
|
|
99
|
+
color: '#e0e0e0',
|
|
100
|
+
cursor: 'pointer',
|
|
101
|
+
borderRadius: '4px',
|
|
102
|
+
fontSize: '0.85rem',
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
Decline
|
|
106
|
+
</button>
|
|
107
|
+
<button
|
|
108
|
+
onClick={handleAccept}
|
|
109
|
+
style={{
|
|
110
|
+
padding: '0.5rem 1.25rem',
|
|
111
|
+
border: 'none',
|
|
112
|
+
backgroundColor: '#6366f1',
|
|
113
|
+
color: '#fff',
|
|
114
|
+
cursor: 'pointer',
|
|
115
|
+
borderRadius: '4px',
|
|
116
|
+
fontSize: '0.85rem',
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
Accept
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if cookies have been consented to.
|
|
128
|
+
* Use this before initializing any analytics or tracking.
|
|
129
|
+
*
|
|
130
|
+
* Example:
|
|
131
|
+
* if (hasCookieConsent()) {
|
|
132
|
+
* initializeAnalytics();
|
|
133
|
+
* }
|
|
134
|
+
*/
|
|
135
|
+
export function hasCookieConsent()${typescript ? ': boolean' : ''} {
|
|
136
|
+
if (typeof window === 'undefined') return false;
|
|
137
|
+
return localStorage.getItem(CONSENT_KEY) === 'accepted';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default CookieConsentBanner;
|
|
141
|
+
`;
|
|
142
|
+
return [
|
|
143
|
+
{
|
|
144
|
+
relativePath: `components/CookieConsentBanner.${ext}`,
|
|
145
|
+
content: component,
|
|
146
|
+
description: 'COPPA-compliant cookie consent banner with deny-by-default behavior',
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
function generatePlainJS() {
|
|
151
|
+
const code = `/**
|
|
152
|
+
* CookieConsentBanner — COPPA-compliant cookie consent
|
|
153
|
+
*
|
|
154
|
+
* Privacy-first: all non-essential cookies are OFF by default.
|
|
155
|
+
* No cookies are set until the user explicitly accepts.
|
|
156
|
+
*
|
|
157
|
+
* Usage:
|
|
158
|
+
* const banner = new CookieConsentBanner({
|
|
159
|
+
* onAccept: () => enableAnalytics(),
|
|
160
|
+
* onDecline: () => console.log('User declined'),
|
|
161
|
+
* });
|
|
162
|
+
*
|
|
163
|
+
* Generated by Halo (runhalo.dev) — COPPA compliance scaffold
|
|
164
|
+
*/
|
|
165
|
+
|
|
166
|
+
class CookieConsentBanner {
|
|
167
|
+
constructor(options = {}) {
|
|
168
|
+
this.onAccept = options.onAccept || null;
|
|
169
|
+
this.onDecline = options.onDecline || null;
|
|
170
|
+
this.privacyPolicyUrl = options.privacyPolicyUrl || '/privacy';
|
|
171
|
+
this.storageKey = 'halo_cookie_consent';
|
|
172
|
+
|
|
173
|
+
// Only show if user hasn't made a choice
|
|
174
|
+
const stored = localStorage.getItem(this.storageKey);
|
|
175
|
+
if (stored === null) {
|
|
176
|
+
this.render();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
render() {
|
|
181
|
+
const banner = document.createElement('div');
|
|
182
|
+
banner.setAttribute('role', 'dialog');
|
|
183
|
+
banner.setAttribute('aria-label', 'Cookie consent');
|
|
184
|
+
banner.style.cssText = \`
|
|
185
|
+
position: fixed; bottom: 0; left: 0; right: 0;
|
|
186
|
+
background: #1a1a2e; color: #e0e0e0;
|
|
187
|
+
padding: 1rem 1.5rem; display: flex; align-items: center;
|
|
188
|
+
justify-content: space-between; flex-wrap: wrap; gap: 1rem;
|
|
189
|
+
z-index: 9999; box-shadow: 0 -2px 10px rgba(0,0,0,0.3);
|
|
190
|
+
\`;
|
|
191
|
+
|
|
192
|
+
banner.innerHTML = \`
|
|
193
|
+
<p style="margin:0; flex:1; min-width:200px; font-size:0.9rem;">
|
|
194
|
+
We use cookies to improve your experience.
|
|
195
|
+
<a href="\${this.privacyPolicyUrl}" style="color:#818cf8; text-decoration:underline;">Privacy Policy</a>
|
|
196
|
+
</p>
|
|
197
|
+
<div style="display:flex; gap:0.5rem;">
|
|
198
|
+
<button id="halo-cookie-decline" style="padding:0.5rem 1.25rem; border:1px solid #555; background:transparent; color:#e0e0e0; cursor:pointer; border-radius:4px; font-size:0.85rem;">Decline</button>
|
|
199
|
+
<button id="halo-cookie-accept" style="padding:0.5rem 1.25rem; border:none; background:#6366f1; color:#fff; cursor:pointer; border-radius:4px; font-size:0.85rem;">Accept</button>
|
|
200
|
+
</div>
|
|
201
|
+
\`;
|
|
202
|
+
|
|
203
|
+
banner.querySelector('#halo-cookie-decline').addEventListener('click', () => {
|
|
204
|
+
localStorage.setItem(this.storageKey, 'declined');
|
|
205
|
+
banner.remove();
|
|
206
|
+
if (this.onDecline) this.onDecline();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
banner.querySelector('#halo-cookie-accept').addEventListener('click', () => {
|
|
210
|
+
localStorage.setItem(this.storageKey, 'accepted');
|
|
211
|
+
banner.remove();
|
|
212
|
+
if (this.onAccept) this.onAccept();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
document.body.appendChild(banner);
|
|
216
|
+
this.element = banner;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if cookies have been consented to.
|
|
222
|
+
* Use before initializing analytics or tracking.
|
|
223
|
+
*/
|
|
224
|
+
function hasCookieConsent() {
|
|
225
|
+
return localStorage.getItem('halo_cookie_consent') === 'accepted';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (typeof module !== 'undefined') module.exports = { CookieConsentBanner, hasCookieConsent };
|
|
229
|
+
`;
|
|
230
|
+
return [
|
|
231
|
+
{
|
|
232
|
+
relativePath: 'cookie-consent.js',
|
|
233
|
+
content: code,
|
|
234
|
+
description: 'COPPA-compliant cookie consent banner (vanilla JS)',
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
exports.consentCookiesTemplate = {
|
|
239
|
+
scaffoldId: 'consent-cookies',
|
|
240
|
+
name: 'Cookie Consent Banner',
|
|
241
|
+
description: 'Privacy-first cookie consent banner with deny-by-default behavior',
|
|
242
|
+
ruleIds: ['coppa-cookies-016'],
|
|
243
|
+
generate(framework, typescript) {
|
|
244
|
+
switch (framework) {
|
|
245
|
+
case 'react':
|
|
246
|
+
case 'nextjs':
|
|
247
|
+
return generateReact(typescript);
|
|
248
|
+
default:
|
|
249
|
+
return generatePlainJS();
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
//# sourceMappingURL=consent-cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent-cookies.js","sourceRoot":"","sources":["../../../src/scaffolds/templates/consent-cookies.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAKH,SAAS,aAAa,CAAC,UAAmB;IACxC,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;IACvC,MAAM,SAAS,GAAG,UAAU;QAC1B,CAAC,CAAC,0HAA0H;QAC5H,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,QAAQ,GAAG,UAAU;QACzB,CAAC,CAAC,4EAA4E;QAC9E,CAAC,CAAC,wDAAwD,CAAC;IAE7D,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;EAoBlB,SAAS;;;sCAG2B,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oCA+FV,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE;;;;;;CAMhE,CAAC;IAEA,OAAO;QACL;YACE,YAAY,EAAE,kCAAkC,GAAG,EAAE;YACrD,OAAO,EAAE,SAAS;YAClB,WAAW,EAAE,qEAAqE;SACnF;KACF,CAAC;AACJ,CAAC;AAED,SAAS,eAAe;IACtB,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8Ed,CAAC;IAEA,OAAO;QACL;YACE,YAAY,EAAE,mBAAmB;YACjC,OAAO,EAAE,IAAI;YACb,WAAW,EAAE,oDAAoD;SAClE;KACF,CAAC;AACJ,CAAC;AAEY,QAAA,sBAAsB,GAAqB;IACtD,UAAU,EAAE,iBAAiB;IAC7B,IAAI,EAAE,uBAAuB;IAC7B,WAAW,EAAE,mEAAmE;IAChF,OAAO,EAAE,CAAC,mBAAmB,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,UAAU;QAC5B,QAAQ,SAAS,EAAE,CAAC;YAClB,KAAK,OAAO,CAAC;YACb,KAAK,QAAQ;gBACX,OAAO,aAAa,CAAC,UAAU,CAAC,CAAC;YACnC;gBACE,OAAO,eAAe,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;CACF,CAAC"}
|