@zigrivers/scaffold 3.6.0 → 3.7.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 +87 -12
- package/content/knowledge/backend/backend-api-design.md +103 -0
- package/content/knowledge/backend/backend-architecture.md +100 -0
- package/content/knowledge/backend/backend-async-patterns.md +101 -0
- package/content/knowledge/backend/backend-auth-patterns.md +100 -0
- package/content/knowledge/backend/backend-conventions.md +105 -0
- package/content/knowledge/backend/backend-data-modeling.md +102 -0
- package/content/knowledge/backend/backend-deployment.md +100 -0
- package/content/knowledge/backend/backend-dev-environment.md +102 -0
- package/content/knowledge/backend/backend-observability.md +102 -0
- package/content/knowledge/backend/backend-project-structure.md +100 -0
- package/content/knowledge/backend/backend-requirements.md +103 -0
- package/content/knowledge/backend/backend-security.md +104 -0
- package/content/knowledge/backend/backend-testing.md +101 -0
- package/content/knowledge/backend/backend-worker-patterns.md +100 -0
- package/content/knowledge/cli/cli-architecture.md +101 -0
- package/content/knowledge/cli/cli-conventions.md +117 -0
- package/content/knowledge/cli/cli-dev-environment.md +121 -0
- package/content/knowledge/cli/cli-distribution-patterns.md +106 -0
- package/content/knowledge/cli/cli-interactivity-patterns.md +116 -0
- package/content/knowledge/cli/cli-output-patterns.md +107 -0
- package/content/knowledge/cli/cli-project-structure.md +124 -0
- package/content/knowledge/cli/cli-requirements.md +101 -0
- package/content/knowledge/cli/cli-shell-integration.md +130 -0
- package/content/knowledge/cli/cli-testing.md +134 -0
- package/content/knowledge/web-app/web-app-api-patterns.md +224 -0
- package/content/knowledge/web-app/web-app-architecture.md +116 -0
- package/content/knowledge/web-app/web-app-auth-patterns.md +256 -0
- package/content/knowledge/web-app/web-app-conventions.md +121 -0
- package/content/knowledge/web-app/web-app-data-patterns.md +218 -0
- package/content/knowledge/web-app/web-app-deployment-workflow.md +143 -0
- package/content/knowledge/web-app/web-app-deployment.md +134 -0
- package/content/knowledge/web-app/web-app-design-system.md +158 -0
- package/content/knowledge/web-app/web-app-dev-environment.md +173 -0
- package/content/knowledge/web-app/web-app-observability.md +221 -0
- package/content/knowledge/web-app/web-app-project-structure.md +160 -0
- package/content/knowledge/web-app/web-app-rendering-strategies.md +133 -0
- package/content/knowledge/web-app/web-app-requirements.md +112 -0
- package/content/knowledge/web-app/web-app-security.md +193 -0
- package/content/knowledge/web-app/web-app-session-patterns.md +214 -0
- package/content/knowledge/web-app/web-app-testing.md +249 -0
- package/content/knowledge/web-app/web-app-ux-patterns.md +162 -0
- package/content/methodology/backend-overlay.yml +73 -0
- package/content/methodology/cli-overlay.yml +69 -0
- package/content/methodology/web-app-overlay.yml +79 -0
- package/dist/cli/commands/init.d.ts +12 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +182 -13
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +136 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/config/schema.d.ts +800 -32
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +48 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +156 -1
- package/dist/config/schema.test.js.map +1 -1
- package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
- package/dist/core/assembly/overlay-loader.js +2 -1
- package/dist/core/assembly/overlay-loader.js.map +1 -1
- package/dist/core/assembly/overlay-loader.test.js +34 -0
- package/dist/core/assembly/overlay-loader.test.js.map +1 -1
- package/dist/e2e/game-pipeline.test.js +1 -0
- package/dist/e2e/game-pipeline.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.d.ts +15 -0
- package/dist/e2e/project-type-overlays.test.d.ts.map +1 -0
- package/dist/e2e/project-type-overlays.test.js +534 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -0
- package/dist/types/config.d.ts +13 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -1
- package/dist/types/index.js.map +1 -1
- package/dist/wizard/questions.d.ts +16 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +87 -3
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +117 -4
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +12 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +16 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
- package/dist/types/wizard.d.ts +0 -14
- package/dist/types/wizard.d.ts.map +0 -1
- package/dist/types/wizard.js +0 -2
- package/dist/types/wizard.js.map +0 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-app-security
|
|
3
|
+
description: XSS prevention, Content Security Policy, CSRF tokens, clickjacking, Subresource Integrity, dependency auditing, and OWASP top 10 for web apps
|
|
4
|
+
topics: [web-app, security, xss, csp, csrf, clickjacking, owasp, dependency-auditing]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Web application security failures are among the most costly and common causes of data breaches. The OWASP Top 10 has catalogued the same categories of vulnerabilities for decades — not because they are new, but because they recur in every generation of frameworks and technologies. Understanding the attack vectors and their mitigations is not optional for engineers building user-facing web applications. Security must be designed in, not bolted on.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
### XSS (Cross-Site Scripting) Prevention
|
|
12
|
+
|
|
13
|
+
XSS occurs when attacker-controlled content is rendered as executable script in a victim's browser. It is the most prevalent web vulnerability class.
|
|
14
|
+
|
|
15
|
+
**Three XSS types:**
|
|
16
|
+
- **Stored XSS** — malicious script is stored in the database and rendered for all users (e.g., a comment containing `<script>`)
|
|
17
|
+
- **Reflected XSS** — malicious script is in the URL and reflected back in the response (e.g., a search page rendering the query parameter unsanitized)
|
|
18
|
+
- **DOM-based XSS** — malicious script is injected via client-side JavaScript manipulation of the DOM (e.g., `element.innerHTML = location.hash`)
|
|
19
|
+
|
|
20
|
+
**Primary defense: output encoding.** Modern React, Vue, and Angular frameworks automatically escape string interpolation. The vulnerabilities arise when developers bypass the framework: `dangerouslySetInnerHTML`, `v-html`, `innerHTML`, `document.write`, `eval()`.
|
|
21
|
+
|
|
22
|
+
**Content Security Policy (CSP):** A `Content-Security-Policy` response header instructs the browser to only execute scripts from trusted sources:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Content-Security-Policy:
|
|
26
|
+
default-src 'self';
|
|
27
|
+
script-src 'self' https://trusted-cdn.com;
|
|
28
|
+
style-src 'self' 'unsafe-inline';
|
|
29
|
+
img-src 'self' data: https:;
|
|
30
|
+
font-src 'self' https://fonts.gstatic.com;
|
|
31
|
+
connect-src 'self' https://api.example.com;
|
|
32
|
+
frame-ancestors 'none';
|
|
33
|
+
upgrade-insecure-requests;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`'unsafe-inline'` in `script-src` defeats the XSS protection benefit. Use nonces (`'nonce-{random}'`) or hashes instead for inline scripts.
|
|
37
|
+
|
|
38
|
+
### Clickjacking Prevention
|
|
39
|
+
|
|
40
|
+
Clickjacking loads your application in a hidden `<iframe>` on an attacker's page. Users believe they are clicking on the attacker's UI but are actually clicking your application's buttons.
|
|
41
|
+
|
|
42
|
+
**Defense:** `Content-Security-Policy: frame-ancestors 'none'` (preferred) or the older `X-Frame-Options: DENY`. `frame-ancestors 'none'` prevents your app from being embedded in any frame. Use `frame-ancestors 'self'` if you need to allow same-origin framing.
|
|
43
|
+
|
|
44
|
+
### CSRF Protection
|
|
45
|
+
|
|
46
|
+
Cross-Site Request Forgery tricks an authenticated user into submitting a request to your application from an attacker's site. The browser automatically sends cookies, making the forged request appear legitimate.
|
|
47
|
+
|
|
48
|
+
**Primary defense:** `SameSite=Strict` or `SameSite=Lax` cookies. With `SameSite=Strict`, the cookie is never sent on cross-site requests, making CSRF impossible.
|
|
49
|
+
|
|
50
|
+
**Secondary defense for legacy or cross-site scenarios:** Synchronizer token pattern — include a CSRF token in every form/request and validate it on the server.
|
|
51
|
+
|
|
52
|
+
### Subresource Integrity (SRI)
|
|
53
|
+
|
|
54
|
+
When loading scripts or stylesheets from CDNs, SRI prevents a compromised CDN from serving malicious content:
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<script
|
|
58
|
+
src="https://cdn.example.com/library.min.js"
|
|
59
|
+
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux8P/C5b2E8x2U6sQ=="
|
|
60
|
+
crossorigin="anonymous"
|
|
61
|
+
></script>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The browser verifies the hash of the loaded resource. If it does not match, the resource is blocked. Generate hashes with: `openssl dgst -sha384 -binary file.js | openssl base64 -A`.
|
|
65
|
+
|
|
66
|
+
### Dependency Auditing
|
|
67
|
+
|
|
68
|
+
Third-party dependencies are the most underestimated attack surface. The 2021 Log4Shell vulnerability and 2022 node-ipc supply chain attack demonstrated that a dependency in a dependency can cause a critical vulnerability.
|
|
69
|
+
|
|
70
|
+
**Baseline practices:**
|
|
71
|
+
- Run `npm audit` or `pnpm audit` in CI — fail the build on critical/high severities
|
|
72
|
+
- Pin exact dependency versions in production builds (lockfile required)
|
|
73
|
+
- Review dependency additions as carefully as code additions
|
|
74
|
+
- Subscribe to security advisories for critical dependencies (GitHub Dependabot, Snyk)
|
|
75
|
+
|
|
76
|
+
## Deep Guidance
|
|
77
|
+
|
|
78
|
+
### Content Security Policy Implementation
|
|
79
|
+
|
|
80
|
+
Deploying CSP in production requires a staged approach — a strict policy will break things:
|
|
81
|
+
|
|
82
|
+
**Phase 1: Report-only mode.** Use `Content-Security-Policy-Report-Only` with a `report-uri` endpoint. This logs violations without blocking anything. Collect violations for 1–2 weeks.
|
|
83
|
+
|
|
84
|
+
**Phase 2: Fix violations.** Address inline scripts (move to external files or use nonces), fix disallowed origins, and handle third-party widget requirements.
|
|
85
|
+
|
|
86
|
+
**Phase 3: Enforce.** Switch to `Content-Security-Policy`. Monitor violation reports for regressions.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// Next.js CSP with nonces (recommended over 'unsafe-inline')
|
|
90
|
+
import { headers } from 'next/headers';
|
|
91
|
+
import crypto from 'crypto';
|
|
92
|
+
|
|
93
|
+
export default function RootLayout({ children }) {
|
|
94
|
+
const nonce = crypto.randomBytes(16).toString('base64');
|
|
95
|
+
|
|
96
|
+
const cspHeader = [
|
|
97
|
+
`default-src 'self'`,
|
|
98
|
+
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
|
|
99
|
+
`style-src 'self' 'nonce-${nonce}'`,
|
|
100
|
+
`img-src 'self' blob: data: https:`,
|
|
101
|
+
`font-src 'self'`,
|
|
102
|
+
`object-src 'none'`,
|
|
103
|
+
`base-uri 'self'`,
|
|
104
|
+
`form-action 'self'`,
|
|
105
|
+
`frame-ancestors 'none'`,
|
|
106
|
+
`upgrade-insecure-requests`,
|
|
107
|
+
].join('; ');
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<html>
|
|
111
|
+
<head>
|
|
112
|
+
<meta httpEquiv="Content-Security-Policy" content={cspHeader} />
|
|
113
|
+
</head>
|
|
114
|
+
<body>{children}</body>
|
|
115
|
+
</html>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Security Headers Checklist
|
|
121
|
+
|
|
122
|
+
Every production web app must serve these response headers:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
# Prevent clickjacking
|
|
126
|
+
Content-Security-Policy: frame-ancestors 'none'
|
|
127
|
+
X-Frame-Options: DENY # Legacy browsers
|
|
128
|
+
|
|
129
|
+
# Prevent MIME sniffing
|
|
130
|
+
X-Content-Type-Options: nosniff
|
|
131
|
+
|
|
132
|
+
# Enable HSTS (HTTP Strict Transport Security)
|
|
133
|
+
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
|
|
134
|
+
|
|
135
|
+
# Control referrer information
|
|
136
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
137
|
+
|
|
138
|
+
# Control browser features
|
|
139
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
140
|
+
|
|
141
|
+
# Prevent IE compatibility mode
|
|
142
|
+
X-UA-Compatible: IE=edge
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Configure these in your CDN (Cloudflare, Vercel, Fastly) or reverse proxy rather than application code — they should apply to all responses including static assets.
|
|
146
|
+
|
|
147
|
+
### HTML Sanitization for User Content
|
|
148
|
+
|
|
149
|
+
When you must render user-supplied HTML (rich text editors, markdown with HTML), use a server-side sanitization library:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
153
|
+
|
|
154
|
+
// GOOD: Sanitize before storage and before rendering
|
|
155
|
+
function sanitizeUserHTML(html: string): string {
|
|
156
|
+
return DOMPurify.sanitize(html, {
|
|
157
|
+
ALLOWED_TAGS: ['p', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'br', 'blockquote'],
|
|
158
|
+
ALLOWED_ATTR: ['href', 'target', 'rel'],
|
|
159
|
+
FORBID_ATTR: ['style', 'class', 'id'],
|
|
160
|
+
ADD_ATTR: ['rel'], // Ensure all links have rel="noopener noreferrer"
|
|
161
|
+
FORCE_BODY: true,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// BAD: Rendering user HTML without sanitization
|
|
166
|
+
function UnsafeComponent({ userContent }) {
|
|
167
|
+
return <div dangerouslySetInnerHTML={{ __html: userContent }} />; // XSS risk
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// GOOD: Sanitize before rendering
|
|
171
|
+
function SafeComponent({ userContent }) {
|
|
172
|
+
return <div dangerouslySetInnerHTML={{ __html: sanitizeUserHTML(userContent) }} />;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Always add `rel="noopener noreferrer"` to user-supplied links.** Without `noopener`, the linked page can access `window.opener` and redirect the original tab.
|
|
177
|
+
|
|
178
|
+
### OWASP Top 10 Mapping
|
|
179
|
+
|
|
180
|
+
| OWASP 2021 | Web App Mitigation |
|
|
181
|
+
|---|---|
|
|
182
|
+
| A01 Broken Access Control | Server-side authz on every endpoint; never trust client-claimed identity |
|
|
183
|
+
| A02 Cryptographic Failures | TLS everywhere; never store passwords in plaintext; use bcrypt/Argon2 |
|
|
184
|
+
| A03 Injection | Parameterized queries; ORM; never concatenate user input into SQL/shell commands |
|
|
185
|
+
| A04 Insecure Design | Threat model before building auth/payment flows |
|
|
186
|
+
| A05 Security Misconfiguration | Headers checklist; disable debug endpoints in production; rotate default credentials |
|
|
187
|
+
| A06 Vulnerable Components | `npm audit` in CI; Dependabot alerts; pin lockfile |
|
|
188
|
+
| A07 Auth Failures | PKCE; rate limiting on login; MFA; session timeout |
|
|
189
|
+
| A08 Software Integrity | SRI for CDN assets; verify npm package checksums; signed commits |
|
|
190
|
+
| A09 Logging Failures | Log auth events; alert on anomalies; never log passwords or tokens |
|
|
191
|
+
| A10 SSRF | Validate and restrict URLs in server-side fetch calls; block private IP ranges |
|
|
192
|
+
|
|
193
|
+
Run a security review against this checklist before the first production deployment and after any major auth or data flow change.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-app-session-patterns
|
|
3
|
+
description: Session management architecture, JWT vs cookie sessions, refresh token rotation, session storage, and hijacking prevention
|
|
4
|
+
topics: [web-app, auth, sessions, jwt, cookies, security, redis]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Session management is the mechanism by which a web application recognizes a returning user between HTTP requests. Because HTTP is stateless, sessions are an application-level construct — and the design decisions here directly affect security, scalability, and user experience. The wrong session architecture causes token theft, session fixation attacks, memory exhaustion on the server, and logout failures that leave users permanently authenticated even after they believe they've signed out.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
### JWT vs Cookie Sessions
|
|
12
|
+
|
|
13
|
+
The two primary session patterns have fundamentally different security models:
|
|
14
|
+
|
|
15
|
+
**Cookie-based sessions (server-authoritative):**
|
|
16
|
+
- Server stores session state (database or Redis); client holds an opaque session ID in a cookie
|
|
17
|
+
- Immediate revocation: delete the session record and the user is logged out
|
|
18
|
+
- Scales horizontally when session storage is centralized (Redis cluster)
|
|
19
|
+
- HttpOnly + Secure + SameSite=Strict cookies resist XSS and CSRF simultaneously
|
|
20
|
+
- Each request requires a session store lookup — adds latency (~1–5 ms for Redis)
|
|
21
|
+
|
|
22
|
+
**JWT (stateless):**
|
|
23
|
+
- Server stores no session state; the signed token is the complete session
|
|
24
|
+
- No revocation without a token denylist (which re-introduces statefulness)
|
|
25
|
+
- Zero session store lookups per request — appropriate for stateless microservices
|
|
26
|
+
- Tokens can be stolen and reused until expiry — short expiry (15 minutes) is essential
|
|
27
|
+
- Refresh tokens enable long sessions without long-lived access tokens
|
|
28
|
+
|
|
29
|
+
**Rule of thumb:** Use cookie-based sessions for user-facing web apps where revocation matters. Use JWTs for service-to-service auth or APIs where clients are trusted and revocation is not required. Hybrid: short-lived JWTs + server-side refresh token rotation is the most common production pattern.
|
|
30
|
+
|
|
31
|
+
### Refresh Token Rotation
|
|
32
|
+
|
|
33
|
+
Refresh token rotation is the critical security mechanism for long-lived JWT sessions:
|
|
34
|
+
|
|
35
|
+
1. Access token expires in 15 minutes
|
|
36
|
+
2. Client uses refresh token to obtain a new access token + new refresh token
|
|
37
|
+
3. The old refresh token is immediately invalidated
|
|
38
|
+
4. If the old refresh token is ever presented again, all sessions for that user are terminated (token reuse detection indicates theft)
|
|
39
|
+
|
|
40
|
+
This pattern detects token theft: if an attacker steals a refresh token and uses it, the legitimate user's next refresh attempt will fail and force re-authentication, alerting the user and invalidating the attacker's session.
|
|
41
|
+
|
|
42
|
+
### Session Storage Backends
|
|
43
|
+
|
|
44
|
+
| Backend | Best For | Limits |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| Redis (single node) | Fast sessions, moderate scale | Single point of failure |
|
|
47
|
+
| Redis Cluster | High availability, large scale | Operational complexity |
|
|
48
|
+
| Redis Sentinel | Automatic failover for single-node Redis | Less scale than Cluster |
|
|
49
|
+
| Database (PostgreSQL) | Simpler stack, queryable sessions | Higher latency than Redis |
|
|
50
|
+
| In-process memory | Development only | Lost on restart, no horizontal scale |
|
|
51
|
+
|
|
52
|
+
For production, Redis is the standard choice: sub-millisecond lookups, TTL-based expiry is native, and session invalidation is an O(1) DEL command.
|
|
53
|
+
|
|
54
|
+
### Token Expiry Strategy
|
|
55
|
+
|
|
56
|
+
Define expiry per token type based on risk tolerance:
|
|
57
|
+
|
|
58
|
+
- **Access token**: 15 minutes — short enough to limit exposure if stolen
|
|
59
|
+
- **Refresh token**: 7–30 days — rotate on each use; invalidate on logout
|
|
60
|
+
- **Remember-me token**: 90 days — separate from standard refresh, requires stronger audit
|
|
61
|
+
- **Email verification token**: 24–48 hours — single-use, invalidate on consumption
|
|
62
|
+
- **Password reset token**: 1 hour — single-use, invalidate on consumption, invalidate all sessions on use
|
|
63
|
+
|
|
64
|
+
## Deep Guidance
|
|
65
|
+
|
|
66
|
+
### Secure Cookie Configuration
|
|
67
|
+
|
|
68
|
+
Every session cookie must be configured with all three security attributes:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// Express.js session cookie configuration
|
|
72
|
+
app.use(session({
|
|
73
|
+
name: '__Host-sessionId', // __Host- prefix requires Secure + path=/ + no domain
|
|
74
|
+
secret: process.env.SESSION_SECRET,
|
|
75
|
+
resave: false,
|
|
76
|
+
saveUninitialized: false,
|
|
77
|
+
cookie: {
|
|
78
|
+
httpOnly: true, // Inaccessible to JavaScript — prevents XSS token theft
|
|
79
|
+
secure: true, // HTTPS only — prevents transmission over HTTP
|
|
80
|
+
sameSite: 'strict', // Not sent on cross-site requests — prevents CSRF
|
|
81
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
|
|
82
|
+
path: '/',
|
|
83
|
+
},
|
|
84
|
+
store: new RedisStore({ client: redisClient }),
|
|
85
|
+
}));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The `__Host-` cookie name prefix is a browser-enforced security policy that prevents subdomain hijacking — it requires `Secure`, `path=/`, and no `Domain` attribute. Use it for session cookies in production.
|
|
89
|
+
|
|
90
|
+
### Refresh Token Rotation Implementation
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface TokenPair {
|
|
94
|
+
accessToken: string;
|
|
95
|
+
refreshToken: string;
|
|
96
|
+
accessTokenExpiresAt: Date;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function rotateRefreshToken(
|
|
100
|
+
incomingRefreshToken: string
|
|
101
|
+
): Promise<TokenPair> {
|
|
102
|
+
// 1. Look up the incoming refresh token
|
|
103
|
+
const storedToken = await db.refreshToken.findUnique({
|
|
104
|
+
where: { token: incomingRefreshToken },
|
|
105
|
+
include: { user: true },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!storedToken) {
|
|
109
|
+
// Token not found — could be expired, already rotated, or forged
|
|
110
|
+
throw new AuthError('INVALID_REFRESH_TOKEN');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (storedToken.usedAt !== null) {
|
|
114
|
+
// REUSE DETECTED: token was already rotated — potential theft
|
|
115
|
+
// Invalidate ALL refresh tokens for this user
|
|
116
|
+
await db.refreshToken.updateMany({
|
|
117
|
+
where: { userId: storedToken.userId },
|
|
118
|
+
data: { revokedAt: new Date() },
|
|
119
|
+
});
|
|
120
|
+
throw new AuthError('TOKEN_REUSE_DETECTED');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (storedToken.revokedAt !== null || storedToken.expiresAt < new Date()) {
|
|
124
|
+
throw new AuthError('REFRESH_TOKEN_EXPIRED');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. Mark old token as used (not deleted — needed for reuse detection)
|
|
128
|
+
await db.refreshToken.update({
|
|
129
|
+
where: { id: storedToken.id },
|
|
130
|
+
data: { usedAt: new Date() },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 3. Issue new token pair
|
|
134
|
+
const newAccessToken = signAccessToken({ sub: storedToken.userId });
|
|
135
|
+
const newRefreshToken = await db.refreshToken.create({
|
|
136
|
+
data: {
|
|
137
|
+
userId: storedToken.userId,
|
|
138
|
+
token: generateSecureToken(),
|
|
139
|
+
expiresAt: addDays(new Date(), 30),
|
|
140
|
+
parentTokenId: storedToken.id, // Track rotation chain
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
accessToken: newAccessToken,
|
|
146
|
+
refreshToken: newRefreshToken.token,
|
|
147
|
+
accessTokenExpiresAt: addMinutes(new Date(), 15),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Session Hijacking Prevention
|
|
153
|
+
|
|
154
|
+
Beyond secure cookies, defend against session hijacking at the application layer:
|
|
155
|
+
|
|
156
|
+
**IP binding (optional, use carefully):**
|
|
157
|
+
- Store the client IP at session creation and validate on each request
|
|
158
|
+
- Breaks for legitimate users on mobile networks (IP changes per cell tower)
|
|
159
|
+
- Use only for high-security flows (admin panels, bank-grade apps), not general user sessions
|
|
160
|
+
|
|
161
|
+
**User-Agent fingerprint:**
|
|
162
|
+
- Store a hash of the User-Agent string at session creation
|
|
163
|
+
- Validate on each request; suspicious changes invalidate the session
|
|
164
|
+
- Not a strong defense (UA is spoofable) but raises the cost of attack
|
|
165
|
+
|
|
166
|
+
**Session fixation prevention:**
|
|
167
|
+
- Always regenerate the session ID on successful authentication
|
|
168
|
+
- Never allow the session ID to be set via URL parameters (cookie-only)
|
|
169
|
+
- In Express: call `req.session.regenerate()` after successful login
|
|
170
|
+
|
|
171
|
+
**Concurrent session limits:**
|
|
172
|
+
- Track active session count per user in Redis
|
|
173
|
+
- On new login, offer the user the choice to log out other sessions or enforce a maximum
|
|
174
|
+
- Essential for compliance in regulated industries
|
|
175
|
+
|
|
176
|
+
### Redis Session Store with TTL
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import Redis from 'ioredis';
|
|
180
|
+
import RedisStore from 'connect-redis';
|
|
181
|
+
|
|
182
|
+
const redisClient = new Redis({
|
|
183
|
+
host: process.env.REDIS_HOST,
|
|
184
|
+
port: 6379,
|
|
185
|
+
// Lazy connect — don't block startup if Redis is temporarily unavailable
|
|
186
|
+
lazyConnect: true,
|
|
187
|
+
// Retry strategy — exponential backoff
|
|
188
|
+
retryStrategy: (times: number) => Math.min(times * 50, 2000),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Sessions expire automatically via Redis TTL — no cleanup job needed
|
|
192
|
+
const sessionStore = new RedisStore({
|
|
193
|
+
client: redisClient,
|
|
194
|
+
prefix: 'sess:',
|
|
195
|
+
ttl: 7 * 24 * 60 * 60, // 7 days in seconds
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Invalidate a specific session (logout)
|
|
199
|
+
async function invalidateSession(sessionId: string): Promise<void> {
|
|
200
|
+
await redisClient.del(`sess:${sessionId}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Invalidate all sessions for a user (force logout everywhere)
|
|
204
|
+
async function invalidateAllUserSessions(userId: string): Promise<void> {
|
|
205
|
+
// Requires storing a user→sessions index in Redis
|
|
206
|
+
const sessionIds = await redisClient.smembers(`user:sessions:${userId}`);
|
|
207
|
+
if (sessionIds.length > 0) {
|
|
208
|
+
await redisClient.del(...sessionIds.map(id => `sess:${id}`));
|
|
209
|
+
await redisClient.del(`user:sessions:${userId}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Maintain a `user:sessions:{userId}` Redis set to enable "logout everywhere" — essential for security incident response.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-app-testing
|
|
3
|
+
description: Component testing with Testing Library, SSR testing, E2E testing with Playwright, visual regression, accessibility testing with axe-core, and Lighthouse CI
|
|
4
|
+
topics: [web-app, testing, playwright, testing-library, accessibility, visual-regression, lighthouse, e2e]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Web application testing requires covering multiple distinct layers: component behavior, server rendering correctness, end-to-end user flows, visual appearance, accessibility compliance, and performance budgets. Each layer catches different classes of bugs and has different cost/value characteristics. The correct strategy invests heavily in component and integration tests (fast feedback, high coverage), uses E2E tests for critical user journeys (slow but comprehensive), and enforces visual and performance budgets in CI (catches regressions before users do).
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
### Testing Layers
|
|
12
|
+
|
|
13
|
+
**Component tests (Testing Library):** Test components in isolation from the DOM's perspective — interactions, rendering, and state changes. These form the bulk of the test suite. Use `@testing-library/user-event` for realistic interactions, not `fireEvent`.
|
|
14
|
+
|
|
15
|
+
**Integration tests:** Test pages or feature slices with real data flow but mocked network calls. Verify that components interact correctly. In Next.js: test pages as React components with a mocked router.
|
|
16
|
+
|
|
17
|
+
**SSR tests (render-to-string):** Verify server-rendered HTML is correct and does not throw. Check that SSR and hydration produce consistent output (no hydration mismatches). Use `@testing-library/react`'s `renderToStaticMarkup` or framework-specific utilities.
|
|
18
|
+
|
|
19
|
+
**E2E tests (Playwright):** Test complete user flows in a real browser against a running application. Reserve for high-value, business-critical flows: registration, login, checkout, core product value proposition. Keep the E2E suite small and fast (target under 10 minutes for the critical path).
|
|
20
|
+
|
|
21
|
+
**Visual regression tests:** Screenshot comparison against committed baselines. Catch unintended UI changes from CSS refactors, component updates, or dependency upgrades. Run against a static Storybook deployment for fast, stable comparisons.
|
|
22
|
+
|
|
23
|
+
**Accessibility tests (axe-core):** Automated WCAG compliance checking. Catches ~30–40% of accessibility issues automatically. Not a substitute for manual testing with a screen reader, but essential for catching regressions.
|
|
24
|
+
|
|
25
|
+
**Performance tests (Lighthouse CI):** Enforce performance budgets in CI. Fail the build if LCP, CLS, or INP regressions are detected.
|
|
26
|
+
|
|
27
|
+
### Testing Library Principles
|
|
28
|
+
|
|
29
|
+
Testing Library is designed around the principle that tests should resemble how users interact with the application:
|
|
30
|
+
|
|
31
|
+
- **Query by role first** — `getByRole('button', { name: 'Submit' })` over `getByTestId`
|
|
32
|
+
- **Query by label text** — `getByLabelText('Email address')` for form inputs
|
|
33
|
+
- **Never query by CSS class** — classes are implementation details, not behavior
|
|
34
|
+
- **Use `userEvent` not `fireEvent`** — `userEvent.type()` simulates real keystrokes including focus, blur, and change events; `fireEvent` dispatches synthetic events that skip browser behaviors
|
|
35
|
+
|
|
36
|
+
### Playwright for E2E
|
|
37
|
+
|
|
38
|
+
Playwright supports Chromium, Firefox, and WebKit. Configure it to run against your staging environment in CI and your local dev server locally. Key features:
|
|
39
|
+
- Auto-waiting: Playwright automatically waits for elements to be ready before interacting
|
|
40
|
+
- Network mocking: intercept and mock API responses in tests
|
|
41
|
+
- Trace viewer: visual debugging of test failures with full network and DOM timeline
|
|
42
|
+
- Component testing: Playwright can also test components in isolation (alternative to Testing Library for teams that want one tool)
|
|
43
|
+
|
|
44
|
+
## Deep Guidance
|
|
45
|
+
|
|
46
|
+
### Component Testing Patterns
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// UserProfile.test.tsx — Testing Library patterns
|
|
50
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
51
|
+
import userEvent from '@testing-library/user-event';
|
|
52
|
+
import { UserProfile } from './UserProfile';
|
|
53
|
+
import { server } from '@/mocks/server'; // MSW mock server
|
|
54
|
+
import { http, HttpResponse } from 'msw';
|
|
55
|
+
|
|
56
|
+
describe('UserProfile', () => {
|
|
57
|
+
it('displays user data after loading', async () => {
|
|
58
|
+
render(<UserProfile userId="user-123" />);
|
|
59
|
+
|
|
60
|
+
// Loading state
|
|
61
|
+
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
62
|
+
|
|
63
|
+
// Data loads
|
|
64
|
+
await screen.findByText('Jane Smith'); // Awaits element appearance
|
|
65
|
+
expect(screen.getByText('jane@example.com')).toBeInTheDocument();
|
|
66
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('allows editing the display name', async () => {
|
|
70
|
+
const user = userEvent.setup();
|
|
71
|
+
render(<UserProfile userId="user-123" />);
|
|
72
|
+
|
|
73
|
+
await screen.findByText('Jane Smith');
|
|
74
|
+
await user.click(screen.getByRole('button', { name: 'Edit profile' }));
|
|
75
|
+
|
|
76
|
+
const nameInput = screen.getByLabelText('Display name');
|
|
77
|
+
await user.clear(nameInput);
|
|
78
|
+
await user.type(nameInput, 'Jane Doe');
|
|
79
|
+
await user.click(screen.getByRole('button', { name: 'Save changes' }));
|
|
80
|
+
|
|
81
|
+
await screen.findByText('Profile updated successfully');
|
|
82
|
+
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('shows an error message when the save fails', async () => {
|
|
86
|
+
// Override the default MSW handler for this test
|
|
87
|
+
server.use(
|
|
88
|
+
http.patch('/api/users/:id', () =>
|
|
89
|
+
HttpResponse.json({ error: 'Server error' }, { status: 500 })
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const user = userEvent.setup();
|
|
94
|
+
render(<UserProfile userId="user-123" />);
|
|
95
|
+
|
|
96
|
+
await screen.findByText('Jane Smith');
|
|
97
|
+
await user.click(screen.getByRole('button', { name: 'Edit profile' }));
|
|
98
|
+
await user.click(screen.getByRole('button', { name: 'Save changes' }));
|
|
99
|
+
|
|
100
|
+
await screen.findByRole('alert');
|
|
101
|
+
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use Mock Service Worker (MSW) for network mocking — it intercepts requests at the network level, making tests work identically in browser and Node environments.
|
|
107
|
+
|
|
108
|
+
### Playwright E2E Tests
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// tests/e2e/auth.spec.ts — Playwright E2E for critical auth flow
|
|
112
|
+
import { test, expect } from '@playwright/test';
|
|
113
|
+
|
|
114
|
+
test.describe('Authentication', () => {
|
|
115
|
+
test('user can register and log in', async ({ page }) => {
|
|
116
|
+
const email = `test-${Date.now()}@example.com`;
|
|
117
|
+
|
|
118
|
+
// Registration
|
|
119
|
+
await page.goto('/register');
|
|
120
|
+
await page.getByLabel('Email address').fill(email);
|
|
121
|
+
await page.getByLabel('Password').fill('SecureP@ss123');
|
|
122
|
+
await page.getByLabel('Confirm password').fill('SecureP@ss123');
|
|
123
|
+
await page.getByRole('button', { name: 'Create account' }).click();
|
|
124
|
+
|
|
125
|
+
// Should redirect to onboarding
|
|
126
|
+
await expect(page).toHaveURL('/onboarding');
|
|
127
|
+
await expect(page.getByText('Welcome')).toBeVisible();
|
|
128
|
+
|
|
129
|
+
// Log out
|
|
130
|
+
await page.getByRole('button', { name: 'Account menu' }).click();
|
|
131
|
+
await page.getByRole('menuitem', { name: 'Sign out' }).click();
|
|
132
|
+
await expect(page).toHaveURL('/');
|
|
133
|
+
|
|
134
|
+
// Log in
|
|
135
|
+
await page.goto('/login');
|
|
136
|
+
await page.getByLabel('Email address').fill(email);
|
|
137
|
+
await page.getByLabel('Password').fill('SecureP@ss123');
|
|
138
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
139
|
+
|
|
140
|
+
await expect(page).toHaveURL('/dashboard');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('shows error for invalid credentials', async ({ page }) => {
|
|
144
|
+
await page.goto('/login');
|
|
145
|
+
await page.getByLabel('Email address').fill('nonexistent@example.com');
|
|
146
|
+
await page.getByLabel('Password').fill('wrongpassword');
|
|
147
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
148
|
+
|
|
149
|
+
await expect(page.getByRole('alert')).toContainText('Invalid email or password');
|
|
150
|
+
await expect(page).toHaveURL('/login');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Accessibility Testing with axe-core
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Integrate axe-core into component tests
|
|
159
|
+
import { render } from '@testing-library/react';
|
|
160
|
+
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
161
|
+
|
|
162
|
+
expect.extend(toHaveNoViolations);
|
|
163
|
+
|
|
164
|
+
describe('Accessibility', () => {
|
|
165
|
+
it('LoginForm has no accessibility violations', async () => {
|
|
166
|
+
const { container } = render(<LoginForm />);
|
|
167
|
+
const results = await axe(container);
|
|
168
|
+
expect(results).toHaveNoViolations();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Playwright accessibility snapshot
|
|
173
|
+
test('dashboard is accessible', async ({ page }) => {
|
|
174
|
+
await page.goto('/dashboard');
|
|
175
|
+
const accessibilityScanResults = await new AxeBuilder({ page })
|
|
176
|
+
.withTags(['wcag2a', 'wcag2aa'])
|
|
177
|
+
.analyze();
|
|
178
|
+
expect(accessibilityScanResults.violations).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Axe-core checks: missing alt text, insufficient color contrast, missing form labels, improper heading hierarchy, keyboard navigation issues, and ARIA attribute misuse. Run it against every page in CI.
|
|
183
|
+
|
|
184
|
+
### Lighthouse CI Configuration
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
# lighthouserc.yml
|
|
188
|
+
ci:
|
|
189
|
+
collect:
|
|
190
|
+
url:
|
|
191
|
+
- 'http://localhost:3000/'
|
|
192
|
+
- 'http://localhost:3000/dashboard'
|
|
193
|
+
numberOfRuns: 3 # Average across multiple runs for stability
|
|
194
|
+
settings:
|
|
195
|
+
preset: 'desktop'
|
|
196
|
+
throttling:
|
|
197
|
+
rttMs: 40
|
|
198
|
+
throughputKbps: 10240
|
|
199
|
+
cpuSlowdownMultiplier: 1
|
|
200
|
+
|
|
201
|
+
assert:
|
|
202
|
+
preset: 'lighthouse:no-pwa'
|
|
203
|
+
assertions:
|
|
204
|
+
# Core Web Vitals
|
|
205
|
+
largest-contentful-paint:
|
|
206
|
+
- error
|
|
207
|
+
- maxNumericValue: 2500
|
|
208
|
+
aggregationMethod: optimistic # Use best of N runs
|
|
209
|
+
cumulative-layout-shift:
|
|
210
|
+
- error
|
|
211
|
+
- maxNumericValue: 0.1
|
|
212
|
+
total-blocking-time:
|
|
213
|
+
- warn
|
|
214
|
+
- maxNumericValue: 300
|
|
215
|
+
|
|
216
|
+
# Performance budget
|
|
217
|
+
uses-optimized-images: ['warn', { minScore: 0.9 }]
|
|
218
|
+
uses-text-compression: ['error', { minScore: 1 }]
|
|
219
|
+
render-blocking-resources: ['warn', { minScore: 0.8 }]
|
|
220
|
+
|
|
221
|
+
# Accessibility
|
|
222
|
+
categories:accessibility: ['error', { minScore: 0.9 }]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Run Lighthouse CI in a separate CI step after deployment to your staging environment. Fail only on errors (hard regressions); warn on improvements.
|
|
226
|
+
|
|
227
|
+
### SSR Hydration Testing
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// Verify SSR output and hydration consistency
|
|
231
|
+
import { renderToString } from 'react-dom/server';
|
|
232
|
+
import { render } from '@testing-library/react';
|
|
233
|
+
|
|
234
|
+
test('ProductCard renders consistently in SSR and client', () => {
|
|
235
|
+
const props = { name: 'Widget', price: 29.99 };
|
|
236
|
+
|
|
237
|
+
// SSR render
|
|
238
|
+
const ssrHTML = renderToString(<ProductCard {...props} />);
|
|
239
|
+
|
|
240
|
+
// Client render
|
|
241
|
+
const { container } = render(<ProductCard {...props} />);
|
|
242
|
+
|
|
243
|
+
// Compare — normalize whitespace differences
|
|
244
|
+
const normalize = (html: string) => html.replace(/\s+/g, ' ').trim();
|
|
245
|
+
expect(normalize(container.innerHTML)).toBe(normalize(ssrHTML));
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Hydration mismatches produce React warnings in development and can cause visual flicker in production. Test SSR/client consistency for any component that uses `Date.now()`, `Math.random()`, browser APIs, or dynamic imports.
|