start-vibing-stacks 2.6.0 → 2.7.3
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 +83 -135
- package/dist/index.js +16 -2
- package/dist/migrate.d.ts +27 -0
- package/dist/migrate.js +217 -0
- package/dist/setup.js +10 -0
- package/package.json +1 -1
- package/stacks/_shared/agents/claude-md-compactor.md +1 -0
- package/stacks/_shared/agents/commit-manager.md +1 -0
- package/stacks/_shared/agents/documenter.md +1 -0
- package/stacks/_shared/agents/domain-updater.md +1 -0
- package/stacks/_shared/agents/research-web.md +1 -0
- package/stacks/_shared/agents/security-auditor.md +168 -0
- package/stacks/_shared/agents/tester.md +1 -0
- package/stacks/_shared/hooks/final-check.ts +205 -0
- package/stacks/_shared/hooks/stop-validator.ts +77 -1
- package/stacks/_shared/skills/accessibility-wcag22/SKILL.md +284 -0
- package/stacks/_shared/skills/ci-pipelines/SKILL.md +166 -0
- package/stacks/_shared/skills/codebase-knowledge/SKILL.md +5 -0
- package/stacks/_shared/skills/database-migrations/SKILL.md +256 -0
- package/stacks/_shared/skills/debugging-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/docker-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/docs-tracker/SKILL.md +5 -0
- package/stacks/_shared/skills/error-handling/SKILL.md +335 -0
- package/stacks/_shared/skills/final-check/SKILL.md +74 -37
- package/stacks/_shared/skills/git-workflow/SKILL.md +5 -0
- package/stacks/_shared/skills/hook-development/SKILL.md +5 -0
- package/stacks/_shared/skills/observability/SKILL.md +351 -0
- package/stacks/_shared/skills/performance-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/playwright-automation/SKILL.md +5 -0
- package/stacks/_shared/skills/quality-gate/SKILL.md +5 -0
- package/stacks/_shared/skills/research-cache/SKILL.md +5 -0
- package/stacks/_shared/skills/secrets-management/SKILL.md +245 -0
- package/stacks/_shared/skills/security-baseline/SKILL.md +202 -0
- package/stacks/_shared/skills/test-coverage/SKILL.md +5 -0
- package/stacks/_shared/skills/ui-ux-audit/SKILL.md +5 -0
- package/stacks/frontend/react/skills/preline-ui/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-standards/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/shadcn-ui/SKILL.md +5 -0
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/zod-validation/SKILL.md +5 -0
- package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +5 -0
- package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +5 -0
- package/stacks/nodejs/skills/api-security-node/SKILL.md +275 -0
- package/stacks/nodejs/skills/bun-runtime/SKILL.md +5 -0
- package/stacks/nodejs/skills/mongoose-patterns/SKILL.md +5 -0
- package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +5 -0
- package/stacks/nodejs/skills/trpc-api/SKILL.md +5 -0
- package/stacks/nodejs/skills/typescript-strict/SKILL.md +5 -0
- package/stacks/nodejs/stack.json +2 -1
- package/stacks/nodejs/workflows/ci.yml +90 -0
- package/stacks/nodejs/workflows/security.yml +45 -0
- package/stacks/php/skills/api-design/SKILL.md +5 -0
- package/stacks/php/skills/api-security/SKILL.md +5 -0
- package/stacks/php/skills/composer-workflow/SKILL.md +5 -0
- package/stacks/php/skills/external-api-patterns/SKILL.md +5 -0
- package/stacks/php/skills/inertia-react/SKILL.md +5 -0
- package/stacks/php/skills/laravel-inertia-i18n/SKILL.md +5 -0
- package/stacks/php/skills/laravel-octane/SKILL.md +5 -0
- package/stacks/php/skills/laravel-patterns/SKILL.md +5 -0
- package/stacks/php/skills/mariadb-octane/SKILL.md +5 -0
- package/stacks/php/skills/php-patterns/SKILL.md +5 -0
- package/stacks/php/skills/phpstan-analysis/SKILL.md +5 -0
- package/stacks/php/skills/phpunit-testing/SKILL.md +5 -0
- package/stacks/php/skills/security-scan-php/SKILL.md +5 -0
- package/stacks/php/workflows/ci.yml +106 -0
- package/stacks/php/workflows/security.yml +36 -0
- package/stacks/python/skills/api-security-python/SKILL.md +312 -0
- package/stacks/python/skills/async-patterns/SKILL.md +5 -0
- package/stacks/python/skills/django-patterns/SKILL.md +5 -0
- package/stacks/python/skills/fastapi-patterns/SKILL.md +5 -0
- package/stacks/python/skills/pydantic-validation/SKILL.md +5 -0
- package/stacks/python/skills/pytest-testing/SKILL.md +5 -0
- package/stacks/python/skills/python-patterns/SKILL.md +5 -0
- package/stacks/python/skills/python-performance/SKILL.md +5 -0
- package/stacks/python/skills/scripting-automation/SKILL.md +5 -0
- package/stacks/python/stack.json +2 -1
- package/stacks/python/workflows/ci.yml +76 -0
- package/stacks/python/workflows/security.yml +56 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-security-node
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: Production-grade API hardening for Node.js (Express, Fastify, Next.js Route Handlers, Server Actions). Rate limit, CORS, JWT, secure cookies, CSRF, headers.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# API Security — Node.js
|
|
8
|
+
|
|
9
|
+
**ALWAYS invoke when building API endpoints, auth flows, Server Actions, or Route Handlers.**
|
|
10
|
+
|
|
11
|
+
> Pair this with `security-baseline` for OWASP Top 10. This skill is stack-specific hardening.
|
|
12
|
+
|
|
13
|
+
## Layered Defense
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Edge (CDN/WAF) → Rate Limit → CORS → Headers → Auth → Authz → Validate → Logic → Encode → Audit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Security Headers
|
|
22
|
+
|
|
23
|
+
### Express / Fastify
|
|
24
|
+
```ts
|
|
25
|
+
import helmet from 'helmet';
|
|
26
|
+
app.use(helmet({
|
|
27
|
+
contentSecurityPolicy: {
|
|
28
|
+
directives: {
|
|
29
|
+
defaultSrc: ["'self'"],
|
|
30
|
+
scriptSrc: ["'self'", "'strict-dynamic'", (_, res) => `'nonce-${res.locals.nonce}'`],
|
|
31
|
+
styleSrc: ["'self'", "'unsafe-inline'"], // Tailwind needs inline; otherwise remove
|
|
32
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
33
|
+
connectSrc: ["'self'"],
|
|
34
|
+
frameAncestors: ["'none'"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
|
|
38
|
+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
39
|
+
crossOriginOpenerPolicy: { policy: 'same-origin' },
|
|
40
|
+
}));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Next.js — `next.config.ts`
|
|
44
|
+
```ts
|
|
45
|
+
const securityHeaders = [
|
|
46
|
+
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
|
|
47
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
48
|
+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
49
|
+
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
|
50
|
+
{ key: 'X-Frame-Options', value: 'DENY' },
|
|
51
|
+
];
|
|
52
|
+
export default {
|
|
53
|
+
async headers() { return [{ source: '/(.*)', headers: securityHeaders }]; },
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
CSP for Next.js is best done in middleware with per-request nonces (script-src `'strict-dynamic'`).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 2. CORS — Strict Allowlist
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import cors from 'cors';
|
|
65
|
+
const ALLOW = (process.env['CORS_ORIGINS'] ?? '').split(',').filter(Boolean);
|
|
66
|
+
app.use(cors({
|
|
67
|
+
origin: (origin, cb) => {
|
|
68
|
+
if (!origin) return cb(null, true); // server-to-server
|
|
69
|
+
if (ALLOW.includes(origin)) return cb(null, true);
|
|
70
|
+
return cb(new Error('CORS blocked'));
|
|
71
|
+
},
|
|
72
|
+
credentials: true, // required for cookies
|
|
73
|
+
methods: ['GET', 'POST', 'PATCH', 'DELETE'],
|
|
74
|
+
maxAge: 600,
|
|
75
|
+
}));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Never** use `origin: '*'` with `credentials: true` — browsers will reject and you'll silently break auth.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 3. Rate Limiting
|
|
83
|
+
|
|
84
|
+
### Express — `express-rate-limit` + Redis
|
|
85
|
+
```ts
|
|
86
|
+
import rateLimit from 'express-rate-limit';
|
|
87
|
+
import RedisStore from 'rate-limit-redis';
|
|
88
|
+
|
|
89
|
+
const authLimiter = rateLimit({
|
|
90
|
+
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
|
|
91
|
+
windowMs: 15 * 60 * 1000,
|
|
92
|
+
max: 5, // 5 attempts / 15 min / IP
|
|
93
|
+
standardHeaders: true,
|
|
94
|
+
legacyHeaders: false,
|
|
95
|
+
skipSuccessfulRequests: true, // only count failures on /login
|
|
96
|
+
});
|
|
97
|
+
app.post('/auth/login', authLimiter, loginHandler);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Next.js — `@upstash/ratelimit`
|
|
101
|
+
```ts
|
|
102
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
103
|
+
import { Redis } from '@upstash/redis';
|
|
104
|
+
|
|
105
|
+
const limiter = new Ratelimit({
|
|
106
|
+
redis: Redis.fromEnv(),
|
|
107
|
+
limiter: Ratelimit.slidingWindow(10, '60s'),
|
|
108
|
+
analytics: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export async function POST(req: Request) {
|
|
112
|
+
const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';
|
|
113
|
+
const { success } = await limiter.limit(ip);
|
|
114
|
+
if (!success) return new Response('Too Many Requests', { status: 429 });
|
|
115
|
+
// ...
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Limits to set:** auth (5/15min), password reset (3/hour), signup (3/hour/IP), generic write (60/min/user).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 4. Cookies
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
res.cookie('session', token, {
|
|
127
|
+
httpOnly: true, // JS cannot read
|
|
128
|
+
secure: process.env['NODE_ENV'] === 'production',
|
|
129
|
+
sameSite: 'lax', // 'strict' if no cross-site flows
|
|
130
|
+
path: '/',
|
|
131
|
+
maxAge: 1000 * 60 * 60 * 24 * 7, // 7d
|
|
132
|
+
domain: process.env['COOKIE_DOMAIN'], // e.g. .example.com
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
For sensitive ops, also set a CSRF token cookie (readable) + require it in `X-CSRF-Token` header.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 5. JWT / Session Tokens
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
144
|
+
|
|
145
|
+
const SECRET = new TextEncoder().encode(process.env['JWT_SECRET']);
|
|
146
|
+
|
|
147
|
+
// Issue access token (short-lived) + refresh token (rotated)
|
|
148
|
+
const accessToken = await new SignJWT({ sub: user.id, role: user.role })
|
|
149
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
150
|
+
.setIssuedAt()
|
|
151
|
+
.setExpirationTime('15m')
|
|
152
|
+
.setJti(crypto.randomUUID())
|
|
153
|
+
.sign(SECRET);
|
|
154
|
+
|
|
155
|
+
// Verify
|
|
156
|
+
const { payload } = await jwtVerify(token, SECRET, {
|
|
157
|
+
algorithms: ['HS256'], // pin algorithm — never accept 'none'
|
|
158
|
+
clockTolerance: 5,
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Rules:
|
|
163
|
+
- Access tokens: ≤ 15 min. Refresh tokens: rotate on use, store hash in DB, revocable.
|
|
164
|
+
- Pin algorithm. The `alg: 'none'` and key-confusion attacks are real.
|
|
165
|
+
- Include `jti` for revocation lists.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 6. CSRF — Next.js Server Actions / Route Handlers
|
|
170
|
+
|
|
171
|
+
Server Actions: Next.js 14+ verifies origin automatically when called via form/`useFormState`. For Route Handlers and any cross-origin form, implement double-submit cookie:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// middleware.ts
|
|
175
|
+
import { NextResponse } from 'next/server';
|
|
176
|
+
import type { NextRequest } from 'next/server';
|
|
177
|
+
|
|
178
|
+
export function middleware(req: NextRequest) {
|
|
179
|
+
const res = NextResponse.next();
|
|
180
|
+
if (!req.cookies.get('csrf-token')) {
|
|
181
|
+
res.cookies.set('csrf-token', crypto.randomUUID(), {
|
|
182
|
+
sameSite: 'lax', secure: true, path: '/',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
186
|
+
const cookie = req.cookies.get('csrf-token')?.value;
|
|
187
|
+
const header = req.headers.get('x-csrf-token');
|
|
188
|
+
if (!cookie || cookie !== header) {
|
|
189
|
+
return new NextResponse('CSRF', { status: 403 });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return res;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 7. Input Validation Boundary (Zod)
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { z } from 'zod';
|
|
202
|
+
|
|
203
|
+
const Body = z.object({
|
|
204
|
+
email: z.string().email().max(254),
|
|
205
|
+
age: z.number().int().min(13).max(120),
|
|
206
|
+
}).strict(); // .strict() rejects unknown keys → blocks mass assignment
|
|
207
|
+
|
|
208
|
+
export async function POST(req: Request) {
|
|
209
|
+
const parsed = Body.safeParse(await req.json());
|
|
210
|
+
if (!parsed.success) {
|
|
211
|
+
return Response.json({ errors: parsed.error.flatten() }, { status: 422 });
|
|
212
|
+
}
|
|
213
|
+
// parsed.data is type-safe and clean
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 8. File Upload
|
|
220
|
+
|
|
221
|
+
- Cap size at the proxy AND in the handler.
|
|
222
|
+
- Validate MIME by **magic bytes** (`file-type` package), not by extension or `Content-Type`.
|
|
223
|
+
- Store outside webroot. Serve via signed URLs.
|
|
224
|
+
- Never use the user-supplied filename on disk — generate a UUID.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
228
|
+
const ft = await fileTypeFromBuffer(buf);
|
|
229
|
+
if (!ft || !['image/jpeg', 'image/png', 'image/webp'].includes(ft.mime)) {
|
|
230
|
+
throw new BadRequestError('Invalid file type');
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 9. Password Hashing
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { hash, verify } from '@node-rs/argon2';
|
|
240
|
+
const hashed = await hash(password, { memoryCost: 19456, timeCost: 2, parallelism: 1 });
|
|
241
|
+
const ok = await verify(hashed, attempt); // constant-time
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Never use `bcryptjs` (pure JS, slow); prefer native `bcrypt` or `argon2`. Argon2id is the modern default.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 10. Server Action / Route Handler Checklist
|
|
249
|
+
|
|
250
|
+
- [ ] `auth()` called first; reject if no session for protected routes
|
|
251
|
+
- [ ] User ID from session, **never** from body
|
|
252
|
+
- [ ] Zod schema with `.strict()`
|
|
253
|
+
- [ ] Authz check (RBAC/ABAC) on the resource
|
|
254
|
+
- [ ] Rate limit applied
|
|
255
|
+
- [ ] No `console.log(req.body)` (PII leak — see `observability`)
|
|
256
|
+
- [ ] Errors return generic message; details to logs only
|
|
257
|
+
|
|
258
|
+
## FORBIDDEN
|
|
259
|
+
|
|
260
|
+
| Anti-pattern | Reason |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `cors({ origin: '*', credentials: true })` | Browsers reject; you're disabling auth |
|
|
263
|
+
| `jwt.verify(token)` without `algorithms` | `alg: none` and key confusion attacks |
|
|
264
|
+
| Storing JWT in `localStorage` | XSS exfiltration trivial — use HttpOnly cookies |
|
|
265
|
+
| `app.use(express.json({ limit: '50mb' }))` everywhere | DoS — set per-route |
|
|
266
|
+
| `req.body.userId` for ownership | A01 violation — use session |
|
|
267
|
+
| Logging `req.body` or `req.headers` | Logs passwords, cookies, tokens |
|
|
268
|
+
| `bcrypt.compare(a, b)` with non-string | Type coercion bugs leak info |
|
|
269
|
+
|
|
270
|
+
## See Also
|
|
271
|
+
|
|
272
|
+
- `security-baseline` — OWASP Top 10
|
|
273
|
+
- `secrets-management` — env vars, rotation
|
|
274
|
+
- `zod-validation` — schema patterns
|
|
275
|
+
- `observability` — structured logs without PII
|
package/stacks/nodejs/stack.json
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
ci:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
timeout-minutes: 15
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- uses: oven-sh/setup-bun@v2
|
|
23
|
+
with:
|
|
24
|
+
bun-version: latest
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: bun install --frozen-lockfile
|
|
28
|
+
|
|
29
|
+
- name: Typecheck
|
|
30
|
+
run: bun run typecheck
|
|
31
|
+
|
|
32
|
+
- name: Lint
|
|
33
|
+
run: bun run lint
|
|
34
|
+
|
|
35
|
+
- name: Unit tests
|
|
36
|
+
run: bun run test
|
|
37
|
+
|
|
38
|
+
- name: Build
|
|
39
|
+
run: bun run build
|
|
40
|
+
|
|
41
|
+
security:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
timeout-minutes: 10
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
with:
|
|
47
|
+
fetch-depth: 0
|
|
48
|
+
|
|
49
|
+
- uses: oven-sh/setup-bun@v2
|
|
50
|
+
with:
|
|
51
|
+
bun-version: latest
|
|
52
|
+
|
|
53
|
+
- name: Install dependencies
|
|
54
|
+
run: bun install --frozen-lockfile
|
|
55
|
+
|
|
56
|
+
- name: Audit dependencies
|
|
57
|
+
run: bun audit --audit-level=high
|
|
58
|
+
continue-on-error: false
|
|
59
|
+
|
|
60
|
+
- name: Gitleaks (secret scan)
|
|
61
|
+
uses: gitleaks/gitleaks-action@v2
|
|
62
|
+
env:
|
|
63
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
64
|
+
|
|
65
|
+
e2e:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
timeout-minutes: 30
|
|
68
|
+
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v4
|
|
71
|
+
|
|
72
|
+
- uses: oven-sh/setup-bun@v2
|
|
73
|
+
with:
|
|
74
|
+
bun-version: latest
|
|
75
|
+
|
|
76
|
+
- name: Install dependencies
|
|
77
|
+
run: bun install --frozen-lockfile
|
|
78
|
+
|
|
79
|
+
- name: Install Playwright browsers
|
|
80
|
+
run: bunx playwright install --with-deps chromium
|
|
81
|
+
|
|
82
|
+
- name: Run Playwright tests
|
|
83
|
+
run: bunx playwright test
|
|
84
|
+
|
|
85
|
+
- uses: actions/upload-artifact@v4
|
|
86
|
+
if: failure()
|
|
87
|
+
with:
|
|
88
|
+
name: playwright-report
|
|
89
|
+
path: playwright-report/
|
|
90
|
+
retention-days: 14
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Security
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 4 * * 1' # Mondays 04:00 UTC
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
security-events: write
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
audit:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 10
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: oven-sh/setup-bun@v2
|
|
21
|
+
with:
|
|
22
|
+
bun-version: latest
|
|
23
|
+
- run: bun install --frozen-lockfile
|
|
24
|
+
- run: bun audit --audit-level=high
|
|
25
|
+
|
|
26
|
+
gitleaks:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
timeout-minutes: 5
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
fetch-depth: 0
|
|
33
|
+
- uses: gitleaks/gitleaks-action@v2
|
|
34
|
+
env:
|
|
35
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
36
|
+
|
|
37
|
+
codeql:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
timeout-minutes: 20
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
- uses: github/codeql-action/init@v3
|
|
43
|
+
with:
|
|
44
|
+
languages: javascript-typescript
|
|
45
|
+
- uses: github/codeql-action/analyze@v3
|