@runhalo/engine 0.2.0 → 0.3.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/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 +10 -0
- package/dist/index.js +326 -14
- 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/package.json +1 -1
|
@@ -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"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PII Sanitizer Scaffold
|
|
4
|
+
* Addresses: coppa-data-002 (HIGH)
|
|
5
|
+
* Generates middleware/utility to strip PII from URL parameters
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.piiSanitizerTemplate = void 0;
|
|
9
|
+
function generateReact(typescript) {
|
|
10
|
+
const ext = typescript ? 'ts' : 'js';
|
|
11
|
+
const typeAnnotations = typescript;
|
|
12
|
+
// Utility file (framework-agnostic, used by both React and Next.js)
|
|
13
|
+
const utility = `/**
|
|
14
|
+
* PII Sanitizer — strips personally identifiable information from URLs
|
|
15
|
+
*
|
|
16
|
+
* COPPA requires that children's personal information is not transmitted
|
|
17
|
+
* in URL parameters, query strings, or referrer headers. This utility
|
|
18
|
+
* detects and removes PII patterns from URLs before they are used.
|
|
19
|
+
*
|
|
20
|
+
* Common PII fields detected: email, name, phone, dob, ssn, address,
|
|
21
|
+
* birthday, age, school, parent, guardian.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* import { sanitizeUrl, containsPII } from './pii-sanitizer';
|
|
25
|
+
*
|
|
26
|
+
* const cleanUrl = sanitizeUrl('https://api.example.com/users?email=child@test.com&name=Bobby');
|
|
27
|
+
* // => 'https://api.example.com/users'
|
|
28
|
+
*
|
|
29
|
+
* if (containsPII(url)) {
|
|
30
|
+
* console.warn('URL contains PII — sanitizing before request');
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* Generated by Halo (runhalo.dev) — COPPA compliance scaffold
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** PII parameter patterns — case-insensitive matching */
|
|
37
|
+
const PII_PATTERNS${typeAnnotations ? ': readonly string[]' : ''} = [
|
|
38
|
+
'email', 'e-mail', 'mail',
|
|
39
|
+
'name', 'first_name', 'last_name', 'firstname', 'lastname', 'full_name', 'fullname',
|
|
40
|
+
'username', 'user_name', 'display_name',
|
|
41
|
+
'phone', 'telephone', 'tel', 'mobile', 'cell',
|
|
42
|
+
'dob', 'date_of_birth', 'dateofbirth', 'birthday', 'birth_date', 'birthdate',
|
|
43
|
+
'age',
|
|
44
|
+
'ssn', 'social_security', 'national_id',
|
|
45
|
+
'address', 'street', 'city', 'zip', 'zipcode', 'zip_code', 'postal',
|
|
46
|
+
'school', 'school_name',
|
|
47
|
+
'parent', 'guardian', 'parent_email', 'guardian_email',
|
|
48
|
+
'child', 'child_name', 'kid',
|
|
49
|
+
'password', 'passwd', 'pwd',
|
|
50
|
+
'token', 'api_key', 'apikey', 'secret',
|
|
51
|
+
]${typeAnnotations ? ' as const' : ''};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a URL contains PII in its query parameters
|
|
55
|
+
*/
|
|
56
|
+
export function containsPII(url${typeAnnotations ? ': string' : ''})${typeAnnotations ? ': boolean' : ''} {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = new URL(url, 'https://placeholder.local');
|
|
59
|
+
const params = Array.from(parsed.searchParams.keys());
|
|
60
|
+
return params.some(param =>
|
|
61
|
+
PII_PATTERNS.some(pattern =>
|
|
62
|
+
param.toLowerCase().includes(pattern.toLowerCase())
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remove PII parameters from a URL, preserving non-PII params
|
|
72
|
+
*/
|
|
73
|
+
export function sanitizeUrl(url${typeAnnotations ? ': string' : ''})${typeAnnotations ? ': string' : ''} {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = new URL(url, 'https://placeholder.local');
|
|
76
|
+
const toRemove${typeAnnotations ? ': string[]' : ''} = [];
|
|
77
|
+
|
|
78
|
+
parsed.searchParams.forEach((_, key) => {
|
|
79
|
+
if (PII_PATTERNS.some(pattern => key.toLowerCase().includes(pattern.toLowerCase()))) {
|
|
80
|
+
toRemove.push(key);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
toRemove.forEach(key => parsed.searchParams.delete(key));
|
|
85
|
+
|
|
86
|
+
// If original was relative, return relative
|
|
87
|
+
if (!url.startsWith('http')) {
|
|
88
|
+
return parsed.pathname + parsed.search + parsed.hash;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return parsed.toString();
|
|
92
|
+
} catch {
|
|
93
|
+
return url;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract PII fields from a URL (for logging/auditing)
|
|
99
|
+
* Returns the parameter names (NOT values) that match PII patterns
|
|
100
|
+
*/
|
|
101
|
+
export function detectPIIFields(url${typeAnnotations ? ': string' : ''})${typeAnnotations ? ': string[]' : ''} {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = new URL(url, 'https://placeholder.local');
|
|
104
|
+
return Array.from(parsed.searchParams.keys()).filter(key =>
|
|
105
|
+
PII_PATTERNS.some(pattern => key.toLowerCase().includes(pattern.toLowerCase()))
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Middleware-style sanitizer for fetch/axios interceptors
|
|
114
|
+
*
|
|
115
|
+
* Usage with fetch:
|
|
116
|
+
* const originalFetch = window.fetch;
|
|
117
|
+
* window.fetch = (url, options) => {
|
|
118
|
+
* return originalFetch(sanitizeFetchUrl(url), options);
|
|
119
|
+
* };
|
|
120
|
+
*/
|
|
121
|
+
export function sanitizeFetchUrl(input${typeAnnotations ? ': string | URL | Request' : ''})${typeAnnotations ? ': string | URL | Request' : ''} {
|
|
122
|
+
if (typeof input === 'string') {
|
|
123
|
+
return sanitizeUrl(input);
|
|
124
|
+
}
|
|
125
|
+
if (input instanceof URL) {
|
|
126
|
+
return new URL(sanitizeUrl(input.toString()));
|
|
127
|
+
}
|
|
128
|
+
// Request object — sanitize the URL
|
|
129
|
+
if (input instanceof Request) {
|
|
130
|
+
const cleanUrl = sanitizeUrl(input.url);
|
|
131
|
+
return new Request(cleanUrl, input);
|
|
132
|
+
}
|
|
133
|
+
return input;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { PII_PATTERNS };
|
|
137
|
+
`;
|
|
138
|
+
return [
|
|
139
|
+
{
|
|
140
|
+
relativePath: `utils/pii-sanitizer.${ext}`,
|
|
141
|
+
content: utility,
|
|
142
|
+
description: 'PII detection and URL sanitization utility for COPPA compliance',
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
function generatePlainJS() {
|
|
147
|
+
const code = `/**
|
|
148
|
+
* PII Sanitizer — strips personally identifiable information from URLs
|
|
149
|
+
*
|
|
150
|
+
* COPPA requires that children's personal information is not transmitted
|
|
151
|
+
* in URL parameters, query strings, or referrer headers.
|
|
152
|
+
*
|
|
153
|
+
* Usage:
|
|
154
|
+
* const { sanitizeUrl, containsPII } = require('./pii-sanitizer');
|
|
155
|
+
* const cleanUrl = sanitizeUrl('https://api.example.com/users?email=child@test.com');
|
|
156
|
+
*
|
|
157
|
+
* Express middleware:
|
|
158
|
+
* app.use(piiMiddleware());
|
|
159
|
+
*
|
|
160
|
+
* Generated by Halo (runhalo.dev) — COPPA compliance scaffold
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
const PII_PATTERNS = [
|
|
164
|
+
'email', 'e-mail', 'mail',
|
|
165
|
+
'name', 'first_name', 'last_name', 'firstname', 'lastname', 'full_name', 'fullname',
|
|
166
|
+
'username', 'user_name', 'display_name',
|
|
167
|
+
'phone', 'telephone', 'tel', 'mobile', 'cell',
|
|
168
|
+
'dob', 'date_of_birth', 'dateofbirth', 'birthday', 'birth_date', 'birthdate',
|
|
169
|
+
'age',
|
|
170
|
+
'ssn', 'social_security', 'national_id',
|
|
171
|
+
'address', 'street', 'city', 'zip', 'zipcode', 'zip_code', 'postal',
|
|
172
|
+
'school', 'school_name',
|
|
173
|
+
'parent', 'guardian', 'parent_email', 'guardian_email',
|
|
174
|
+
'child', 'child_name', 'kid',
|
|
175
|
+
'password', 'passwd', 'pwd',
|
|
176
|
+
'token', 'api_key', 'apikey', 'secret',
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
function containsPII(url) {
|
|
180
|
+
try {
|
|
181
|
+
const parsed = new URL(url, 'https://placeholder.local');
|
|
182
|
+
const params = Array.from(parsed.searchParams.keys());
|
|
183
|
+
return params.some(param =>
|
|
184
|
+
PII_PATTERNS.some(pattern =>
|
|
185
|
+
param.toLowerCase().includes(pattern.toLowerCase())
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function sanitizeUrl(url) {
|
|
194
|
+
try {
|
|
195
|
+
const parsed = new URL(url, 'https://placeholder.local');
|
|
196
|
+
const toRemove = [];
|
|
197
|
+
|
|
198
|
+
parsed.searchParams.forEach((_, key) => {
|
|
199
|
+
if (PII_PATTERNS.some(pattern => key.toLowerCase().includes(pattern.toLowerCase()))) {
|
|
200
|
+
toRemove.push(key);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
toRemove.forEach(key => parsed.searchParams.delete(key));
|
|
205
|
+
|
|
206
|
+
if (!url.startsWith('http')) {
|
|
207
|
+
return parsed.pathname + parsed.search + parsed.hash;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return parsed.toString();
|
|
211
|
+
} catch {
|
|
212
|
+
return url;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Express middleware — strips PII from query parameters
|
|
218
|
+
*
|
|
219
|
+
* Usage:
|
|
220
|
+
* const { piiMiddleware } = require('./pii-sanitizer');
|
|
221
|
+
* app.use(piiMiddleware());
|
|
222
|
+
*/
|
|
223
|
+
function piiMiddleware() {
|
|
224
|
+
return function (req, res, next) {
|
|
225
|
+
const piiKeys = Object.keys(req.query).filter(key =>
|
|
226
|
+
PII_PATTERNS.some(pattern => key.toLowerCase().includes(pattern.toLowerCase()))
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (piiKeys.length > 0) {
|
|
230
|
+
console.warn('[Halo PII Guard] Stripped PII params from request:', piiKeys.join(', '));
|
|
231
|
+
piiKeys.forEach(key => delete req.query[key]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
next();
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { containsPII, sanitizeUrl, piiMiddleware, PII_PATTERNS };
|
|
239
|
+
`;
|
|
240
|
+
return [
|
|
241
|
+
{
|
|
242
|
+
relativePath: 'pii-sanitizer.js',
|
|
243
|
+
content: code,
|
|
244
|
+
description: 'PII sanitizer with Express middleware (vanilla JS/Node.js)',
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
exports.piiSanitizerTemplate = {
|
|
249
|
+
scaffoldId: 'pii-sanitizer',
|
|
250
|
+
name: 'PII Sanitizer',
|
|
251
|
+
description: 'Strips personally identifiable information from URL parameters to prevent COPPA violations',
|
|
252
|
+
ruleIds: ['coppa-data-002'],
|
|
253
|
+
generate(framework, typescript) {
|
|
254
|
+
switch (framework) {
|
|
255
|
+
case 'react':
|
|
256
|
+
case 'nextjs':
|
|
257
|
+
return generateReact(typescript);
|
|
258
|
+
default:
|
|
259
|
+
return generatePlainJS();
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
//# sourceMappingURL=pii-sanitizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pii-sanitizer.js","sourceRoot":"","sources":["../../../src/scaffolds/templates/pii-sanitizer.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAKH,SAAS,aAAa,CAAC,UAAmB;IACxC,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACrC,MAAM,eAAe,GAAG,UAAU,CAAC;IAEnC,oEAAoE;IACpE,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;oBAwBE,eAAe,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;;;;GAc7D,eAAe,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE;;;;;iCAKJ,eAAe,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,eAAe,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;;;;;;;iCAiBvE,eAAe,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,eAAe,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;;;oBAGnF,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;qCAyBlB,eAAe,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;wCAoBrE,eAAe,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,EAAE,IAAI,eAAe,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;;;;;;CAgB7I,CAAC;IAEA,OAAO;QACL;YACE,YAAY,EAAE,uBAAuB,GAAG,EAAE;YAC1C,OAAO,EAAE,OAAO;YAChB,WAAW,EAAE,iEAAiE;SAC/E;KACF,CAAC;AACJ,CAAC;AAED,SAAS,eAAe;IACtB,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4Fd,CAAC;IAEA,OAAO;QACL;YACE,YAAY,EAAE,kBAAkB;YAChC,OAAO,EAAE,IAAI;YACb,WAAW,EAAE,4DAA4D;SAC1E;KACF,CAAC;AACJ,CAAC;AAEY,QAAA,oBAAoB,GAAqB;IACpD,UAAU,EAAE,eAAe;IAC3B,IAAI,EAAE,eAAe;IACrB,WAAW,EAAE,4FAA4F;IACzG,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,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Retention Policy Scaffold
|
|
3
|
+
* Addresses: coppa-retention-005
|
|
4
|
+
* Generates data retention utility with cleanup script and deletion handler
|
|
5
|
+
*/
|
|
6
|
+
import type { ScaffoldTemplate } from '../index';
|
|
7
|
+
export declare const retentionPolicyTemplate: ScaffoldTemplate;
|