@patricio0312rev/skillset 0.1.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/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/bin/cli.js +37 -0
- package/package.json +55 -0
- package/src/commands/init.js +301 -0
- package/src/index.js +168 -0
- package/src/lib/config.js +200 -0
- package/src/lib/generator.js +166 -0
- package/src/utils/display.js +95 -0
- package/src/utils/readme.js +196 -0
- package/src/utils/tool-specific.js +233 -0
- package/templates/ai-engineering/agent-orchestration-planner/ SKILL.md +266 -0
- package/templates/ai-engineering/cost-latency-optimizer/ SKILL.md +270 -0
- package/templates/ai-engineering/doc-to-vector-dataset-generator/ SKILL.md +239 -0
- package/templates/ai-engineering/evaluation-harness/ SKILL.md +219 -0
- package/templates/ai-engineering/guardrails-safety-filter-builder/ SKILL.md +226 -0
- package/templates/ai-engineering/llm-debugger/ SKILL.md +283 -0
- package/templates/ai-engineering/prompt-regression-tester/ SKILL.md +216 -0
- package/templates/ai-engineering/prompt-template-builder/ SKILL.md +393 -0
- package/templates/ai-engineering/rag-pipeline-builder/ SKILL.md +244 -0
- package/templates/ai-engineering/tool-function-schema-designer/ SKILL.md +219 -0
- package/templates/architecture/adr-writer/ SKILL.md +250 -0
- package/templates/architecture/api-versioning-deprecation-planner/ SKILL.md +331 -0
- package/templates/architecture/domain-model-boundaries-mapper/ SKILL.md +300 -0
- package/templates/architecture/migration-planner/ SKILL.md +376 -0
- package/templates/architecture/performance-budget-setter/ SKILL.md +318 -0
- package/templates/architecture/reliability-strategy-builder/ SKILL.md +286 -0
- package/templates/architecture/rfc-generator/ SKILL.md +362 -0
- package/templates/architecture/scalability-playbook/ SKILL.md +279 -0
- package/templates/architecture/system-design-generator/ SKILL.md +339 -0
- package/templates/architecture/tech-debt-prioritizer/ SKILL.md +329 -0
- package/templates/backend/api-contract-normalizer/ SKILL.md +487 -0
- package/templates/backend/api-endpoint-generator/ SKILL.md +415 -0
- package/templates/backend/auth-module-builder/ SKILL.md +99 -0
- package/templates/backend/background-jobs-designer/ SKILL.md +166 -0
- package/templates/backend/caching-strategist/ SKILL.md +190 -0
- package/templates/backend/error-handling-standardizer/ SKILL.md +174 -0
- package/templates/backend/rate-limiting-abuse-protection/ SKILL.md +147 -0
- package/templates/backend/rbac-permissions-builder/ SKILL.md +158 -0
- package/templates/backend/service-layer-extractor/ SKILL.md +269 -0
- package/templates/backend/webhook-receiver-hardener/ SKILL.md +211 -0
- package/templates/ci-cd/artifact-sbom-publisher/ SKILL.md +236 -0
- package/templates/ci-cd/caching-strategy-optimizer/ SKILL.md +195 -0
- package/templates/ci-cd/deployment-checklist-generator/ SKILL.md +381 -0
- package/templates/ci-cd/github-actions-pipeline-creator/ SKILL.md +348 -0
- package/templates/ci-cd/monorepo-ci-optimizer/ SKILL.md +298 -0
- package/templates/ci-cd/preview-environments-builder/ SKILL.md +187 -0
- package/templates/ci-cd/quality-gates-enforcer/ SKILL.md +342 -0
- package/templates/ci-cd/release-automation-builder/ SKILL.md +281 -0
- package/templates/ci-cd/rollback-workflow-builder/ SKILL.md +372 -0
- package/templates/ci-cd/secrets-env-manager/ SKILL.md +242 -0
- package/templates/db-management/backup-restore-runbook-generator/ SKILL.md +505 -0
- package/templates/db-management/data-integrity-auditor/ SKILL.md +505 -0
- package/templates/db-management/data-retention-archiving-planner/ SKILL.md +430 -0
- package/templates/db-management/data-seeding-fixtures-builder/ SKILL.md +375 -0
- package/templates/db-management/db-performance-watchlist/ SKILL.md +425 -0
- package/templates/db-management/etl-sync-job-builder/ SKILL.md +457 -0
- package/templates/db-management/multi-tenant-safety-checker/ SKILL.md +398 -0
- package/templates/db-management/prisma-migration-assistant/ SKILL.md +379 -0
- package/templates/db-management/schema-consistency-checker/ SKILL.md +440 -0
- package/templates/db-management/sql-query-optimizer/ SKILL.md +324 -0
- package/templates/foundation/changelog-writer/ SKILL.md +431 -0
- package/templates/foundation/code-formatter-installer/ SKILL.md +320 -0
- package/templates/foundation/codebase-summarizer/ SKILL.md +360 -0
- package/templates/foundation/dependency-doctor/ SKILL.md +163 -0
- package/templates/foundation/dev-environment-bootstrapper/ SKILL.md +259 -0
- package/templates/foundation/dev-onboarding-builder/ SKILL.md +556 -0
- package/templates/foundation/docs-starter-kit/ SKILL.md +574 -0
- package/templates/foundation/explaining-code/SKILL.md +13 -0
- package/templates/foundation/git-hygiene-enforcer/ SKILL.md +455 -0
- package/templates/foundation/project-scaffolder/ SKILL.md +65 -0
- package/templates/foundation/project-scaffolder/references/templates.md +126 -0
- package/templates/foundation/repo-structure-linter/ SKILL.md +0 -0
- package/templates/foundation/repo-structure-linter/references/conventions.md +98 -0
- package/templates/frontend/animation-micro-interaction-pack/ SKILL.md +41 -0
- package/templates/frontend/component-scaffold-generator/ SKILL.md +562 -0
- package/templates/frontend/design-to-component-translator/ SKILL.md +547 -0
- package/templates/frontend/form-wizard-builder/ SKILL.md +553 -0
- package/templates/frontend/frontend-refactor-planner/ SKILL.md +37 -0
- package/templates/frontend/i18n-frontend-implementer/ SKILL.md +44 -0
- package/templates/frontend/modal-drawer-system/ SKILL.md +377 -0
- package/templates/frontend/page-layout-builder/ SKILL.md +630 -0
- package/templates/frontend/state-ux-flow-builder/ SKILL.md +23 -0
- package/templates/frontend/table-builder/ SKILL.md +350 -0
- package/templates/performance/alerting-dashboard-builder/ SKILL.md +162 -0
- package/templates/performance/backend-latency-profiler-helper/ SKILL.md +108 -0
- package/templates/performance/caching-cdn-strategy-planner/ SKILL.md +150 -0
- package/templates/performance/capacity-planning-helper/ SKILL.md +242 -0
- package/templates/performance/core-web-vitals-tuner/ SKILL.md +126 -0
- package/templates/performance/incident-runbook-generator/ SKILL.md +162 -0
- package/templates/performance/load-test-scenario-builder/ SKILL.md +256 -0
- package/templates/performance/observability-setup/ SKILL.md +232 -0
- package/templates/performance/postmortem-writer/ SKILL.md +203 -0
- package/templates/performance/structured-logging-standardizer/ SKILL.md +122 -0
- package/templates/security/auth-security-reviewer/ SKILL.md +428 -0
- package/templates/security/dependency-vulnerability-triage/ SKILL.md +495 -0
- package/templates/security/input-validation-sanitization-auditor/ SKILL.md +76 -0
- package/templates/security/pii-redaction-logging-policy-builder/ SKILL.md +65 -0
- package/templates/security/rbac-policy-tester/ SKILL.md +80 -0
- package/templates/security/secrets-scanner/ SKILL.md +462 -0
- package/templates/security/secure-headers-csp-builder/ SKILL.md +404 -0
- package/templates/security/security-incident-playbook-generator/ SKILL.md +76 -0
- package/templates/security/security-pr-checklist-skill/ SKILL.md +62 -0
- package/templates/security/threat-model-generator/ SKILL.md +394 -0
- package/templates/testing/contract-testing-builder/ SKILL.md +492 -0
- package/templates/testing/coverage-strategist/ SKILL.md +436 -0
- package/templates/testing/e2e-test-builder/ SKILL.md +382 -0
- package/templates/testing/flaky-test-detective/ SKILL.md +416 -0
- package/templates/testing/integration-test-builder/ SKILL.md +525 -0
- package/templates/testing/mocking-assistant/ SKILL.md +383 -0
- package/templates/testing/snapshot-test-refactorer/ SKILL.md +375 -0
- package/templates/testing/test-data-factory-builder/ SKILL.md +449 -0
- package/templates/testing/test-reporting-triage-skill/ SKILL.md +469 -0
- package/templates/testing/unit-test-generator/ SKILL.md +548 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: service-layer-extractor
|
|
3
|
+
description: Refactors route handlers into service layer with clean boundaries, dependency injection, testability, and separation of concerns. Provides service interfaces, folder structure, testing strategy, and migration plan. Use when refactoring "fat controllers", "business logic", "service layer", or "architecture cleanup".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Service Layer Extractor
|
|
7
|
+
|
|
8
|
+
Extract business logic from controllers into a testable service layer.
|
|
9
|
+
|
|
10
|
+
## Architecture Layers
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
routes/ → Define endpoints, parse requests
|
|
14
|
+
controllers/ → Validate input, call services, format responses
|
|
15
|
+
services/ → Business logic, orchestration
|
|
16
|
+
repositories/ → Database queries
|
|
17
|
+
models/ → Data structures
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Before: Fat Controller
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// ❌ Business logic mixed with HTTP concerns
|
|
24
|
+
router.post("/users", async (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
// Validation
|
|
27
|
+
if (!req.body.email) {
|
|
28
|
+
return res.status(400).json({ error: "Email required" });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check duplicate
|
|
32
|
+
const existing = await db.query("SELECT * FROM users WHERE email = $1", [
|
|
33
|
+
req.body.email,
|
|
34
|
+
]);
|
|
35
|
+
if (existing.rows.length > 0) {
|
|
36
|
+
return res.status(409).json({ error: "Email already exists" });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Hash password
|
|
40
|
+
const hashedPassword = await bcrypt.hash(req.body.password, 10);
|
|
41
|
+
|
|
42
|
+
// Create user
|
|
43
|
+
const result = await db.query(
|
|
44
|
+
"INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *",
|
|
45
|
+
[req.body.email, hashedPassword, req.body.name]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Send welcome email
|
|
49
|
+
await sendEmail(req.body.email, "Welcome!", "Thanks for joining");
|
|
50
|
+
|
|
51
|
+
res.status(201).json(result.rows[0]);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
res.status(500).json({ error: err.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## After: Service Layer
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// ✅ Separated concerns
|
|
62
|
+
|
|
63
|
+
// services/user.service.ts
|
|
64
|
+
export class UserService {
|
|
65
|
+
constructor(
|
|
66
|
+
private userRepository: UserRepository,
|
|
67
|
+
private emailService: EmailService
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
async createUser(dto: CreateUserDto): Promise<User> {
|
|
71
|
+
// Business logic only
|
|
72
|
+
const existing = await this.userRepository.findByEmail(dto.email);
|
|
73
|
+
if (existing) {
|
|
74
|
+
throw new ConflictError("Email already exists");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
|
78
|
+
|
|
79
|
+
const user = await this.userRepository.create({
|
|
80
|
+
...dto,
|
|
81
|
+
password: hashedPassword,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await this.emailService.sendWelcome(user.email);
|
|
85
|
+
|
|
86
|
+
return user;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// controllers/user.controller.ts
|
|
91
|
+
export class UserController {
|
|
92
|
+
constructor(private userService: UserService) {}
|
|
93
|
+
|
|
94
|
+
create = asyncHandler(async (req, res) => {
|
|
95
|
+
const user = await this.userService.createUser(req.body);
|
|
96
|
+
res.status(201).json({ success: true, data: user });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// repositories/user.repository.ts
|
|
101
|
+
export class UserRepository {
|
|
102
|
+
async create(data: CreateUserData): Promise<User> {
|
|
103
|
+
const result = await db.query(
|
|
104
|
+
"INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *",
|
|
105
|
+
[data.email, data.password, data.name]
|
|
106
|
+
);
|
|
107
|
+
return result.rows[0];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
111
|
+
const result = await db.query("SELECT * FROM users WHERE email = $1", [
|
|
112
|
+
email,
|
|
113
|
+
]);
|
|
114
|
+
return result.rows[0] || null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Dependency Injection
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// container.ts (using tsyringe or manual)
|
|
123
|
+
import { UserService } from "./services/user.service";
|
|
124
|
+
import { UserRepository } from "./repositories/user.repository";
|
|
125
|
+
import { EmailService } from "./services/email.service";
|
|
126
|
+
|
|
127
|
+
export class Container {
|
|
128
|
+
private static instances = new Map();
|
|
129
|
+
|
|
130
|
+
static get<T>(constructor: new (...args: any[]) => T): T {
|
|
131
|
+
if (!this.instances.has(constructor)) {
|
|
132
|
+
// Create dependencies
|
|
133
|
+
const deps = this.resolveDependencies(constructor);
|
|
134
|
+
this.instances.set(constructor, new constructor(...deps));
|
|
135
|
+
}
|
|
136
|
+
return this.instances.get(constructor);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static resolveDependencies(constructor: any): any[] {
|
|
140
|
+
// Resolve constructor dependencies
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Usage
|
|
146
|
+
const userService = Container.get(UserService);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Testing Services
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// user.service.test.ts
|
|
153
|
+
describe("UserService", () => {
|
|
154
|
+
let service: UserService;
|
|
155
|
+
let mockRepository: jest.Mocked<UserRepository>;
|
|
156
|
+
let mockEmailService: jest.Mocked<EmailService>;
|
|
157
|
+
|
|
158
|
+
beforeEach(() => {
|
|
159
|
+
mockRepository = {
|
|
160
|
+
create: jest.fn(),
|
|
161
|
+
findByEmail: jest.fn(),
|
|
162
|
+
} as any;
|
|
163
|
+
|
|
164
|
+
mockEmailService = {
|
|
165
|
+
sendWelcome: jest.fn(),
|
|
166
|
+
} as any;
|
|
167
|
+
|
|
168
|
+
service = new UserService(mockRepository, mockEmailService);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("creates user successfully", async () => {
|
|
172
|
+
mockRepository.findByEmail.mockResolvedValue(null);
|
|
173
|
+
mockRepository.create.mockResolvedValue({
|
|
174
|
+
id: "1",
|
|
175
|
+
email: "test@example.com",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const user = await service.createUser({
|
|
179
|
+
email: "test@example.com",
|
|
180
|
+
password: "password123",
|
|
181
|
+
name: "Test User",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(user.id).toBe("1");
|
|
185
|
+
expect(mockEmailService.sendWelcome).toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("throws error if email exists", async () => {
|
|
189
|
+
mockRepository.findByEmail.mockResolvedValue({ id: "1" } as User);
|
|
190
|
+
|
|
191
|
+
await expect(
|
|
192
|
+
service.createUser({
|
|
193
|
+
email: "existing@example.com",
|
|
194
|
+
password: "pass",
|
|
195
|
+
name: "Test",
|
|
196
|
+
})
|
|
197
|
+
).rejects.toThrow(ConflictError);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Folder Structure
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
src/
|
|
206
|
+
├── routes/
|
|
207
|
+
│ └── users.routes.ts
|
|
208
|
+
├── controllers/
|
|
209
|
+
│ └── user.controller.ts
|
|
210
|
+
├── services/
|
|
211
|
+
│ ├── user.service.ts
|
|
212
|
+
│ ├── email.service.ts
|
|
213
|
+
│ └── payment.service.ts
|
|
214
|
+
├── repositories/
|
|
215
|
+
│ └── user.repository.ts
|
|
216
|
+
├── models/
|
|
217
|
+
│ └── user.model.ts
|
|
218
|
+
├── types/
|
|
219
|
+
│ └── user.types.ts
|
|
220
|
+
└── middleware/
|
|
221
|
+
└── validate.ts
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Migration Strategy
|
|
225
|
+
|
|
226
|
+
```markdown
|
|
227
|
+
## Phase 1: Create Service Layer (Week 1-2)
|
|
228
|
+
|
|
229
|
+
- [ ] Create service classes
|
|
230
|
+
- [ ] Move business logic to services
|
|
231
|
+
- [ ] Keep controllers thin
|
|
232
|
+
- [ ] No breaking changes
|
|
233
|
+
|
|
234
|
+
## Phase 2: Add Tests (Week 3-4)
|
|
235
|
+
|
|
236
|
+
- [ ] Write service unit tests
|
|
237
|
+
- [ ] Mock dependencies
|
|
238
|
+
- [ ] Achieve 80%+ coverage
|
|
239
|
+
|
|
240
|
+
## Phase 3: Extract Repositories (Week 5-6)
|
|
241
|
+
|
|
242
|
+
- [ ] Create repository layer
|
|
243
|
+
- [ ] Move DB queries from services
|
|
244
|
+
- [ ] Services depend on repositories
|
|
245
|
+
|
|
246
|
+
## Phase 4: Dependency Injection (Week 7-8)
|
|
247
|
+
|
|
248
|
+
- [ ] Set up DI container
|
|
249
|
+
- [ ] Remove manual instantiation
|
|
250
|
+
- [ ] Wire up dependencies
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Benefits
|
|
254
|
+
|
|
255
|
+
- **Testability**: Services testable without HTTP
|
|
256
|
+
- **Reusability**: Logic reused across endpoints
|
|
257
|
+
- **Separation**: Clear boundaries between layers
|
|
258
|
+
- **Maintainability**: Easier to locate and modify logic
|
|
259
|
+
|
|
260
|
+
## Output Checklist
|
|
261
|
+
|
|
262
|
+
- [ ] Service classes created
|
|
263
|
+
- [ ] Business logic extracted from controllers
|
|
264
|
+
- [ ] Repository layer for data access
|
|
265
|
+
- [ ] Dependency injection setup
|
|
266
|
+
- [ ] Unit tests for services
|
|
267
|
+
- [ ] Folder structure reorganized
|
|
268
|
+
- [ ] Migration plan documented
|
|
269
|
+
- [ ] Team trained on new patterns
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webhook-receiver-hardener
|
|
3
|
+
description: Secures webhook receivers with signature verification, retry handling, deduplication, idempotency keys, and error responses. Provides verification code, dedupe storage strategy, runbook for incidents. Use when implementing "webhooks", "webhook security", "event receivers", or "third-party integrations".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Webhook Receiver Hardener
|
|
7
|
+
|
|
8
|
+
Build secure, reliable webhook endpoints that handle failures gracefully.
|
|
9
|
+
|
|
10
|
+
## Core Security
|
|
11
|
+
|
|
12
|
+
**Signature Verification**: HMAC validation before processing
|
|
13
|
+
**Deduplication**: Track processed webhook IDs
|
|
14
|
+
**Idempotency**: Safe to process same webhook multiple times
|
|
15
|
+
**Retries**: Handle provider retry attempts
|
|
16
|
+
**Rate Limiting**: Prevent abuse
|
|
17
|
+
|
|
18
|
+
## Signature Verification
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import crypto from "crypto";
|
|
22
|
+
|
|
23
|
+
export const verifyWebhookSignature = (
|
|
24
|
+
payload: string,
|
|
25
|
+
signature: string,
|
|
26
|
+
secret: string
|
|
27
|
+
): boolean => {
|
|
28
|
+
const hmac = crypto
|
|
29
|
+
.createHmac("sha256", secret)
|
|
30
|
+
.update(payload)
|
|
31
|
+
.digest("hex");
|
|
32
|
+
|
|
33
|
+
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hmac));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Stripe example
|
|
37
|
+
router.post(
|
|
38
|
+
"/webhooks/stripe",
|
|
39
|
+
express.raw({ type: "application/json" }),
|
|
40
|
+
async (req, res) => {
|
|
41
|
+
const sig = req.headers["stripe-signature"];
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const event = stripe.webhooks.constructEvent(
|
|
45
|
+
req.body,
|
|
46
|
+
sig,
|
|
47
|
+
process.env.STRIPE_WEBHOOK_SECRET
|
|
48
|
+
);
|
|
49
|
+
await processStripeEvent(event);
|
|
50
|
+
res.json({ received: true });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return res.status(400).send(`Webhook Error: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Deduplication
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// Redis-based dedupe
|
|
62
|
+
const WEBHOOK_TTL = 60 * 60 * 24; // 24 hours
|
|
63
|
+
|
|
64
|
+
export const isDuplicate = async (webhookId: string): Promise<boolean> => {
|
|
65
|
+
const key = `webhook:${webhookId}`;
|
|
66
|
+
const exists = await redis.exists(key);
|
|
67
|
+
|
|
68
|
+
if (exists) return true;
|
|
69
|
+
|
|
70
|
+
await redis.setex(key, WEBHOOK_TTL, "1");
|
|
71
|
+
return false;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Usage
|
|
75
|
+
if (await isDuplicate(webhook.id)) {
|
|
76
|
+
return res.status(200).json({ received: true }); // Already processed
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Idempotent Processing
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
export const processWebhook = async (webhook: Webhook) => {
|
|
84
|
+
// Use database transaction with unique constraint
|
|
85
|
+
try {
|
|
86
|
+
await db.transaction(async (trx) => {
|
|
87
|
+
// Insert webhook record (unique constraint on webhook_id)
|
|
88
|
+
await trx("processed_webhooks").insert({
|
|
89
|
+
webhook_id: webhook.id,
|
|
90
|
+
processed_at: new Date(),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Do actual processing
|
|
94
|
+
await performWebhookAction(webhook, trx);
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err.code === "23505") {
|
|
98
|
+
// Unique violation
|
|
99
|
+
console.log("Webhook already processed");
|
|
100
|
+
return; // Idempotent - already processed
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Retry Handling
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Acknowledge immediately, process async
|
|
111
|
+
router.post("/webhooks/provider", async (req, res) => {
|
|
112
|
+
// Verify signature
|
|
113
|
+
if (!verifySignature(req.body, req.headers["signature"])) {
|
|
114
|
+
return res.status(401).send("Invalid signature");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Return 200 immediately
|
|
118
|
+
res.status(200).json({ received: true });
|
|
119
|
+
|
|
120
|
+
// Process async
|
|
121
|
+
processWebhookAsync(req.body).catch((err) => {
|
|
122
|
+
console.error("Webhook processing failed:", err);
|
|
123
|
+
// Will be retried by provider
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Exponential backoff for provider retries
|
|
128
|
+
// Attempt 1: immediate
|
|
129
|
+
// Attempt 2: +5 minutes
|
|
130
|
+
// Attempt 3: +15 minutes
|
|
131
|
+
// Attempt 4: +1 hour
|
|
132
|
+
// Attempt 5: +6 hours
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Error Responses
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Return appropriate status codes
|
|
139
|
+
const webhookHandler = async (req, res) => {
|
|
140
|
+
// 400: Malformed payload (won't retry)
|
|
141
|
+
if (!isValidPayload(req.body)) {
|
|
142
|
+
return res.status(400).json({ error: "Invalid payload" });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 401: Invalid signature (won't retry)
|
|
146
|
+
if (!verifySignature(req.body, req.headers["signature"])) {
|
|
147
|
+
return res.status(401).json({ error: "Invalid signature" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 200: Already processed (idempotent)
|
|
151
|
+
if (await isDuplicate(req.body.id)) {
|
|
152
|
+
return res.status(200).json({ received: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 500: Processing error (will retry)
|
|
156
|
+
try {
|
|
157
|
+
await processWebhook(req.body);
|
|
158
|
+
return res.status(200).json({ received: true });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error("Processing error:", err);
|
|
161
|
+
return res.status(500).json({ error: "Processing failed" });
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Monitoring & Runbook
|
|
167
|
+
|
|
168
|
+
```markdown
|
|
169
|
+
## Webhook Incidents
|
|
170
|
+
|
|
171
|
+
### High Error Rate
|
|
172
|
+
|
|
173
|
+
1. Check provider status page
|
|
174
|
+
2. Review recent code deploys
|
|
175
|
+
3. Check signature secret rotation
|
|
176
|
+
4. Verify database connectivity
|
|
177
|
+
|
|
178
|
+
### Missing Webhooks
|
|
179
|
+
|
|
180
|
+
1. Check provider sending (their dashboard)
|
|
181
|
+
2. Verify endpoint is accessible
|
|
182
|
+
3. Check rate limiting rules
|
|
183
|
+
4. Review dedupe cache TTL
|
|
184
|
+
|
|
185
|
+
### Duplicate Processing
|
|
186
|
+
|
|
187
|
+
1. Check dedupe cache connectivity
|
|
188
|
+
2. Verify unique constraints
|
|
189
|
+
3. Review idempotency logic
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Best Practices
|
|
193
|
+
|
|
194
|
+
- Verify signature BEFORE any processing
|
|
195
|
+
- Return 200 quickly, process async
|
|
196
|
+
- Dedupe with Redis + database constraints
|
|
197
|
+
- Log all webhook attempts
|
|
198
|
+
- Monitor processing latency
|
|
199
|
+
- Set up alerts for failures
|
|
200
|
+
- Document expected payload schemas
|
|
201
|
+
|
|
202
|
+
## Output Checklist
|
|
203
|
+
|
|
204
|
+
- [ ] Signature verification
|
|
205
|
+
- [ ] Deduplication mechanism
|
|
206
|
+
- [ ] Idempotent processing
|
|
207
|
+
- [ ] Async processing pattern
|
|
208
|
+
- [ ] Proper status codes
|
|
209
|
+
- [ ] Error logging
|
|
210
|
+
- [ ] Monitoring/alerts
|
|
211
|
+
- [ ] Incident runbook
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: artifact-sbom-publisher
|
|
3
|
+
description: Produces build artifacts with Software Bill of Materials (SBOM) and supply chain metadata for security and compliance. Use for "artifact publishing", "SBOM generation", "supply chain security", or "build provenance".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Artifact & SBOM Publisher
|
|
7
|
+
|
|
8
|
+
Generate and publish artifacts with supply chain security metadata.
|
|
9
|
+
|
|
10
|
+
## Build Artifacts
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: "20"
|
|
21
|
+
|
|
22
|
+
- run: npm ci
|
|
23
|
+
- run: npm run build
|
|
24
|
+
|
|
25
|
+
- name: Upload artifacts
|
|
26
|
+
uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: dist-${{ github.sha }}
|
|
29
|
+
path: |
|
|
30
|
+
dist/
|
|
31
|
+
!dist/**/*.map
|
|
32
|
+
retention-days: 30
|
|
33
|
+
if-no-files-found: error
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## SBOM Generation (CycloneDX)
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
sbom:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- name: Generate SBOM
|
|
45
|
+
uses: CycloneDX/gh-node-module-generatebom@master
|
|
46
|
+
with:
|
|
47
|
+
path: ./
|
|
48
|
+
output: ./sbom.json
|
|
49
|
+
|
|
50
|
+
- name: Upload SBOM
|
|
51
|
+
uses: actions/upload-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: sbom-${{ github.sha }}
|
|
54
|
+
path: sbom.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## SBOM with Syft
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
- name: Generate SBOM with Syft
|
|
61
|
+
run: |
|
|
62
|
+
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
|
63
|
+
syft . -o spdx-json > sbom-spdx.json
|
|
64
|
+
syft . -o cyclonedx-json > sbom-cyclonedx.json
|
|
65
|
+
|
|
66
|
+
- name: Upload SBOMs
|
|
67
|
+
uses: actions/upload-artifact@v4
|
|
68
|
+
with:
|
|
69
|
+
name: sboms
|
|
70
|
+
path: |
|
|
71
|
+
sbom-spdx.json
|
|
72
|
+
sbom-cyclonedx.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Docker Image SBOM
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
- name: Build image
|
|
79
|
+
uses: docker/build-push-action@v5
|
|
80
|
+
with:
|
|
81
|
+
context: .
|
|
82
|
+
push: true
|
|
83
|
+
tags: myapp:${{ github.sha }}
|
|
84
|
+
sbom: true
|
|
85
|
+
provenance: true
|
|
86
|
+
|
|
87
|
+
- name: Generate SBOM for image
|
|
88
|
+
run: |
|
|
89
|
+
syft myapp:${{ github.sha }} -o spdx-json > image-sbom.json
|
|
90
|
+
|
|
91
|
+
- name: Scan SBOM for vulnerabilities
|
|
92
|
+
uses: anchore/scan-action@v3
|
|
93
|
+
with:
|
|
94
|
+
sbom: image-sbom.json
|
|
95
|
+
fail-build: true
|
|
96
|
+
severity-cutoff: high
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Build Provenance (SLSA)
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
provenance:
|
|
103
|
+
runs-on: ubuntu-latest
|
|
104
|
+
permissions:
|
|
105
|
+
actions: read
|
|
106
|
+
id-token: write
|
|
107
|
+
contents: write
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/checkout@v4
|
|
110
|
+
|
|
111
|
+
- name: Build
|
|
112
|
+
run: npm run build
|
|
113
|
+
|
|
114
|
+
- name: Generate provenance
|
|
115
|
+
uses: actions/attest-build-provenance@v1
|
|
116
|
+
with:
|
|
117
|
+
subject-path: "dist/**"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Artifact Metadata
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
- name: Create artifact metadata
|
|
124
|
+
run: |
|
|
125
|
+
cat > artifact-metadata.json << EOF
|
|
126
|
+
{
|
|
127
|
+
"version": "${{ github.ref_name }}",
|
|
128
|
+
"commit": "${{ github.sha }}",
|
|
129
|
+
"branch": "${{ github.ref }}",
|
|
130
|
+
"build_time": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
131
|
+
"builder": "GitHub Actions",
|
|
132
|
+
"workflow": "${{ github.workflow }}",
|
|
133
|
+
"run_id": "${{ github.run_id }}",
|
|
134
|
+
"actor": "${{ github.actor }}"
|
|
135
|
+
}
|
|
136
|
+
EOF
|
|
137
|
+
|
|
138
|
+
- name: Upload metadata
|
|
139
|
+
uses: actions/upload-artifact@v4
|
|
140
|
+
with:
|
|
141
|
+
name: metadata
|
|
142
|
+
path: artifact-metadata.json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Package & Release
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
release:
|
|
149
|
+
runs-on: ubuntu-latest
|
|
150
|
+
needs: [build, sbom]
|
|
151
|
+
if: github.event_name == 'release'
|
|
152
|
+
steps:
|
|
153
|
+
- name: Download artifacts
|
|
154
|
+
uses: actions/download-artifact@v4
|
|
155
|
+
with:
|
|
156
|
+
path: artifacts/
|
|
157
|
+
|
|
158
|
+
- name: Create release package
|
|
159
|
+
run: |
|
|
160
|
+
cd artifacts
|
|
161
|
+
tar -czf ../release.tar.gz dist-* sbom-* metadata/
|
|
162
|
+
|
|
163
|
+
- name: Upload to release
|
|
164
|
+
uses: actions/upload-release-asset@v1
|
|
165
|
+
env:
|
|
166
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
167
|
+
with:
|
|
168
|
+
upload_url: ${{ github.event.release.upload_url }}
|
|
169
|
+
asset_path: ./release.tar.gz
|
|
170
|
+
asset_name: release-${{ github.ref_name }}.tar.gz
|
|
171
|
+
asset_content_type: application/gzip
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Vulnerability Scanning
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
- name: Scan SBOM for vulnerabilities
|
|
178
|
+
uses: aquasecurity/trivy-action@master
|
|
179
|
+
with:
|
|
180
|
+
scan-type: "sbom"
|
|
181
|
+
format: "sarif"
|
|
182
|
+
output: "trivy-results.sarif"
|
|
183
|
+
sbom-sources: "sbom.json"
|
|
184
|
+
|
|
185
|
+
- name: Upload scan results
|
|
186
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
187
|
+
with:
|
|
188
|
+
sarif_file: "trivy-results.sarif"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Artifact Attestation
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
- name: Attest artifact
|
|
195
|
+
uses: actions/attest@v1
|
|
196
|
+
with:
|
|
197
|
+
subject-path: "dist/myapp.tar.gz"
|
|
198
|
+
predicate-type: "https://slsa.dev/provenance/v1"
|
|
199
|
+
predicate: |
|
|
200
|
+
{
|
|
201
|
+
"buildType": "https://github.com/actions/workflow",
|
|
202
|
+
"builder": {
|
|
203
|
+
"id": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
204
|
+
},
|
|
205
|
+
"metadata": {
|
|
206
|
+
"buildInvocationId": "${{ github.run_id }}",
|
|
207
|
+
"completeness": {
|
|
208
|
+
"parameters": true,
|
|
209
|
+
"environment": false,
|
|
210
|
+
"materials": true
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Best Practices
|
|
217
|
+
|
|
218
|
+
1. **Generate SBOMs**: For all releases
|
|
219
|
+
2. **Multiple formats**: SPDX and CycloneDX
|
|
220
|
+
3. **Scan vulnerabilities**: Before release
|
|
221
|
+
4. **Sign artifacts**: For verification
|
|
222
|
+
5. **Include provenance**: SLSA attestation
|
|
223
|
+
6. **Retention policy**: Keep artifacts 30 days
|
|
224
|
+
7. **Metadata**: Version, commit, timestamp
|
|
225
|
+
8. **Automate**: Part of every build
|
|
226
|
+
|
|
227
|
+
## Output Checklist
|
|
228
|
+
|
|
229
|
+
- [ ] Build artifacts uploaded
|
|
230
|
+
- [ ] SBOM generated (SPDX or CycloneDX)
|
|
231
|
+
- [ ] Vulnerability scanning configured
|
|
232
|
+
- [ ] Build provenance generated
|
|
233
|
+
- [ ] Artifact metadata included
|
|
234
|
+
- [ ] Release packaging automated
|
|
235
|
+
- [ ] Attestation/signing (optional)
|
|
236
|
+
- [ ] Retention policy set
|