@atlashub/smartstack-cli 2.0.0 → 2.2.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/.documentation/agents.html +147 -40
- package/.documentation/apex.html +1 -1
- package/.documentation/business-analyse.html +3 -3
- package/.documentation/cli-commands.html +2 -2
- package/.documentation/commands.html +14 -14
- package/.documentation/efcore.html +14 -14
- package/.documentation/gitflow.html +12 -12
- package/.documentation/hooks.html +41 -3
- package/.documentation/index.html +1 -1
- package/.documentation/init.html +2 -2
- package/.documentation/installation.html +11 -11
- package/.documentation/js/app.js +1 -1
- package/.documentation/ralph-loop.html +1 -1
- package/.documentation/test-web.html +4 -4
- package/dist/index.js +19 -11
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +57595 -4569
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/agents/ba-reader.md +250 -0
- package/templates/agents/ba-writer.md +210 -0
- package/templates/agents/docs-context-reader.md +51 -33
- package/templates/skills/_shared.md +2 -0
- package/templates/skills/business-analyse/SKILL.md +120 -108
- package/templates/skills/business-analyse/_shared.md +191 -160
- package/templates/skills/business-analyse/patterns/suggestion-catalog.md +478 -0
- package/templates/skills/business-analyse/questionnaire/01-context.md +3 -15
- package/templates/skills/business-analyse/questionnaire/08-performance.md +7 -21
- package/templates/skills/business-analyse/questionnaire/09-constraints.md +0 -13
- package/templates/skills/business-analyse/questionnaire/10-documentation.md +0 -13
- package/templates/skills/business-analyse/questionnaire.md +72 -76
- package/templates/skills/business-analyse/react/components.md +317 -154
- package/templates/skills/business-analyse/react/i18n-template.md +167 -106
- package/templates/skills/business-analyse/react/schema.md +325 -106
- package/templates/skills/business-analyse/schemas/feature-schema.json +690 -0
- package/templates/skills/business-analyse/steps/step-00-init.md +395 -285
- package/templates/skills/business-analyse/steps/step-01-analyse.md +505 -0
- package/templates/skills/business-analyse/steps/step-02-specify.md +833 -0
- package/templates/skills/business-analyse/steps/step-03-validate.md +862 -0
- package/templates/skills/business-analyse/steps/step-04-handoff.md +1593 -0
- package/templates/skills/business-analyse/templates/tpl-handoff.md +95 -43
- package/templates/skills/controller/templates.md +82 -0
- package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -0
- package/templates/skills/efcore/steps/migration/step-03-validate.md +19 -0
- package/templates/skills/review-code/SKILL.md +4 -2
- package/templates/skills/review-code/references/owasp-api-top10.md +243 -0
- package/templates/skills/review-code/references/security-checklist.md +86 -1
- package/templates/skills/review-code/references/smartstack-conventions.md +166 -0
- package/templates/skills/workflow/SKILL.md +27 -0
- package/templates/skills/business-analyse/steps/step-01-discover.md +0 -737
- package/templates/skills/business-analyse/steps/step-02-analyse.md +0 -299
- package/templates/skills/business-analyse/steps/step-03-specify.md +0 -409
- package/templates/skills/business-analyse/steps/step-04-validate.md +0 -313
- package/templates/skills/business-analyse/steps/step-05-handoff.md +0 -658
- package/templates/skills/business-analyse/steps/step-06-doc-html.md +0 -320
- package/templates/skills/business-analyse/templates/00-context.md +0 -105
- package/templates/skills/business-analyse/templates/tpl-brd.md +0 -97
- package/templates/skills/business-analyse/templates/tpl-discovery.md +0 -78
- package/templates/skills/business-analyse/tracking/change-template.md +0 -30
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<overview>
|
|
2
|
+
OWASP API Security Top 10 checklist adapted for SmartStack (.NET/ASP.NET Core). This is DIFFERENT from the OWASP Top 10 (web application vulnerabilities) -- this list targets API-specific threats.
|
|
3
|
+
|
|
4
|
+
Use this reference when reviewing API controllers, especially those exposed to external clients or public-facing APIs.
|
|
5
|
+
</overview>
|
|
6
|
+
|
|
7
|
+
<api1_bola>
|
|
8
|
+
## API1 - Broken Object Level Authorization (BOLA/IDOR)
|
|
9
|
+
|
|
10
|
+
**Risk:** Users access other users' resources by manipulating object IDs in requests.
|
|
11
|
+
|
|
12
|
+
**SmartStack check:**
|
|
13
|
+
- [ ] All queries filter by `TenantId` (EF Core global filters active)
|
|
14
|
+
- [ ] `[RequirePermission]` on every endpoint
|
|
15
|
+
- [ ] No raw `Guid` from URL used directly without ownership verification
|
|
16
|
+
|
|
17
|
+
```csharp
|
|
18
|
+
// BAD: IDOR vulnerability - any authenticated user can access any order
|
|
19
|
+
[HttpGet("{id}")]
|
|
20
|
+
public async Task<ActionResult<OrderDto>> Get(Guid id)
|
|
21
|
+
{
|
|
22
|
+
var order = await _context.Orders.FindAsync(id);
|
|
23
|
+
return Ok(order);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// GOOD: Tenant filter via EF Core global filter + explicit check
|
|
27
|
+
[HttpGet("{id}")]
|
|
28
|
+
[RequirePermission(Permissions.Business.Orders.Read)]
|
|
29
|
+
public async Task<ActionResult<OrderDto>> Get(Guid id)
|
|
30
|
+
{
|
|
31
|
+
var order = await _context.Orders
|
|
32
|
+
.FirstOrDefaultAsync(o => o.Id == id); // Global filter ensures TenantId match
|
|
33
|
+
if (order is null) return NotFound();
|
|
34
|
+
return Ok(order.ToDto());
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Detection pattern:**
|
|
39
|
+
```bash
|
|
40
|
+
grep -rE "FindAsync\(id\)|Find\(id\)" --include="*.cs" | grep -v "TenantId"
|
|
41
|
+
```
|
|
42
|
+
</api1_bola>
|
|
43
|
+
|
|
44
|
+
<api2_broken_auth>
|
|
45
|
+
## API2 - Broken Authentication
|
|
46
|
+
|
|
47
|
+
**Risk:** Weak authentication mechanisms allow attackers to impersonate users.
|
|
48
|
+
|
|
49
|
+
**SmartStack check:**
|
|
50
|
+
- [ ] `[Authorize]` on all controllers (except explicit `[AllowAnonymous]`)
|
|
51
|
+
- [ ] JWT tokens validated with issuer, audience, and expiration
|
|
52
|
+
- [ ] Refresh token rotation implemented
|
|
53
|
+
- [ ] Failed login attempts tracked and rate-limited
|
|
54
|
+
|
|
55
|
+
**Detection pattern:**
|
|
56
|
+
```bash
|
|
57
|
+
grep -rL "\[Authorize\]" --include="*Controller.cs" | grep -v "AuthController"
|
|
58
|
+
```
|
|
59
|
+
</api2_broken_auth>
|
|
60
|
+
|
|
61
|
+
<api3_broken_property_auth>
|
|
62
|
+
## API3 - Broken Object Property Level Authorization
|
|
63
|
+
|
|
64
|
+
**Risk:** Users modify properties they shouldn't have access to (mass assignment).
|
|
65
|
+
|
|
66
|
+
**SmartStack check:**
|
|
67
|
+
- [ ] DTOs separate from entities (no direct entity binding)
|
|
68
|
+
- [ ] `CreateDto` and `UpdateDto` contain only writable fields
|
|
69
|
+
- [ ] Sensitive properties (TenantId, CreatedById, Role) NOT in DTOs
|
|
70
|
+
- [ ] No `[FromBody] Entity` binding (always use DTOs)
|
|
71
|
+
|
|
72
|
+
```csharp
|
|
73
|
+
// BAD: Mass assignment - user can set their own Role
|
|
74
|
+
[HttpPut("{id}")]
|
|
75
|
+
public async Task<ActionResult> Update(Guid id, [FromBody] User user) { ... }
|
|
76
|
+
|
|
77
|
+
// GOOD: DTO limits writable fields
|
|
78
|
+
[HttpPut("{id}")]
|
|
79
|
+
[RequirePermission(Permissions.Business.Users.Update)]
|
|
80
|
+
public async Task<ActionResult> Update(Guid id, [FromBody] UpdateUserDto dto) { ... }
|
|
81
|
+
|
|
82
|
+
public record UpdateUserDto(string Name, string Email); // No Role, no TenantId
|
|
83
|
+
```
|
|
84
|
+
</api3_broken_property_auth>
|
|
85
|
+
|
|
86
|
+
<api4_unrestricted_consumption>
|
|
87
|
+
## API4 - Unrestricted Resource Consumption
|
|
88
|
+
|
|
89
|
+
**Risk:** No rate limiting allows API abuse, DDoS, or resource exhaustion.
|
|
90
|
+
|
|
91
|
+
**SmartStack check:**
|
|
92
|
+
- [ ] Rate limiting middleware configured (`Microsoft.AspNetCore.RateLimiting`)
|
|
93
|
+
- [ ] Pagination enforced on list endpoints (max page size)
|
|
94
|
+
- [ ] File upload size limits set
|
|
95
|
+
- [ ] Query complexity limits (no unbounded `Include()`)
|
|
96
|
+
|
|
97
|
+
```csharp
|
|
98
|
+
// BAD: No pagination limit - can request entire database
|
|
99
|
+
[HttpGet]
|
|
100
|
+
public async Task<ActionResult<List<OrderDto>>> GetAll([FromQuery] int pageSize = 1000000) { ... }
|
|
101
|
+
|
|
102
|
+
// GOOD: Enforced max page size
|
|
103
|
+
[HttpGet]
|
|
104
|
+
public async Task<ActionResult<PagedResult<OrderDto>>> GetAll(
|
|
105
|
+
[FromQuery] int page = 1,
|
|
106
|
+
[FromQuery] int pageSize = 20)
|
|
107
|
+
{
|
|
108
|
+
pageSize = Math.Min(pageSize, 100); // Hard cap
|
|
109
|
+
...
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
</api4_unrestricted_consumption>
|
|
113
|
+
|
|
114
|
+
<api5_broken_function_auth>
|
|
115
|
+
## API5 - Broken Function Level Authorization
|
|
116
|
+
|
|
117
|
+
**Risk:** Regular users access admin-level API functions.
|
|
118
|
+
|
|
119
|
+
**SmartStack check:**
|
|
120
|
+
- [ ] Admin endpoints use `platform.administration.*` permissions
|
|
121
|
+
- [ ] Area-based routing enforced (Admin, Support, Business, User)
|
|
122
|
+
- [ ] System account protection (UserType.System, UserType.LocalAdmin)
|
|
123
|
+
- [ ] No permission bypass via direct URL manipulation
|
|
124
|
+
|
|
125
|
+
```csharp
|
|
126
|
+
// BAD: Missing permission - any authenticated user can delete
|
|
127
|
+
[HttpDelete("{id}")]
|
|
128
|
+
[Authorize]
|
|
129
|
+
public async Task<ActionResult> Delete(Guid id) { ... }
|
|
130
|
+
|
|
131
|
+
// GOOD: Explicit permission check
|
|
132
|
+
[HttpDelete("{id}")]
|
|
133
|
+
[RequirePermission(Permissions.Platform.Administration.Users.Delete)]
|
|
134
|
+
public async Task<ActionResult> Delete(Guid id) { ... }
|
|
135
|
+
```
|
|
136
|
+
</api5_broken_function_auth>
|
|
137
|
+
|
|
138
|
+
<api6_business_flow>
|
|
139
|
+
## API6 - Unrestricted Access to Sensitive Business Flows
|
|
140
|
+
|
|
141
|
+
**Risk:** Automated abuse of business processes (mass account creation, coupon abuse).
|
|
142
|
+
|
|
143
|
+
**SmartStack check:**
|
|
144
|
+
- [ ] Rate limiting on sensitive endpoints (registration, password reset)
|
|
145
|
+
- [ ] CAPTCHA or bot detection on public-facing forms
|
|
146
|
+
- [ ] Business flow monitoring and alerting
|
|
147
|
+
- [ ] Idempotency keys for payment/creation operations
|
|
148
|
+
</api6_business_flow>
|
|
149
|
+
|
|
150
|
+
<api7_ssrf>
|
|
151
|
+
## API7 - Server-Side Request Forgery (SSRF)
|
|
152
|
+
|
|
153
|
+
**Risk:** Attacker makes the server send requests to internal resources.
|
|
154
|
+
|
|
155
|
+
**SmartStack check:**
|
|
156
|
+
- [ ] No user-supplied URLs passed to `HttpClient` without validation
|
|
157
|
+
- [ ] Webhook URLs validated against allowlist
|
|
158
|
+
- [ ] Internal network ranges blocked (127.0.0.1, 10.x, 192.168.x)
|
|
159
|
+
|
|
160
|
+
```csharp
|
|
161
|
+
// BAD: SSRF - user controls the URL
|
|
162
|
+
[HttpPost("fetch")]
|
|
163
|
+
public async Task<ActionResult> Fetch([FromBody] string url)
|
|
164
|
+
{
|
|
165
|
+
var response = await _httpClient.GetAsync(url);
|
|
166
|
+
return Ok(await response.Content.ReadAsStringAsync());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// GOOD: Validate against allowlist
|
|
170
|
+
[HttpPost("webhook")]
|
|
171
|
+
public async Task<ActionResult> ConfigureWebhook([FromBody] WebhookDto dto)
|
|
172
|
+
{
|
|
173
|
+
if (!_webhookValidator.IsAllowedDomain(dto.Url))
|
|
174
|
+
return BadRequest("URL domain not allowed");
|
|
175
|
+
...
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
</api7_ssrf>
|
|
179
|
+
|
|
180
|
+
<api8_misconfiguration>
|
|
181
|
+
## API8 - Security Misconfiguration
|
|
182
|
+
|
|
183
|
+
**Risk:** Default configs, verbose errors, missing security headers expose attack surface.
|
|
184
|
+
|
|
185
|
+
**SmartStack check:**
|
|
186
|
+
- [ ] Security headers configured (see security-checklist.md A02)
|
|
187
|
+
- [ ] Error responses don't expose stack traces in production
|
|
188
|
+
- [ ] CORS restricted to known origins (no `AllowAnyOrigin` in production)
|
|
189
|
+
- [ ] Swagger/OpenAPI disabled in production
|
|
190
|
+
- [ ] Debug mode off in production (`ASPNETCORE_ENVIRONMENT=Production`)
|
|
191
|
+
|
|
192
|
+
**Detection pattern:**
|
|
193
|
+
```bash
|
|
194
|
+
grep -rE "AllowAnyOrigin|EnableDetailedErrors|DeveloperExceptionPage" --include="*.cs"
|
|
195
|
+
```
|
|
196
|
+
</api8_misconfiguration>
|
|
197
|
+
|
|
198
|
+
<api9_inventory>
|
|
199
|
+
## API9 - Improper Inventory Management
|
|
200
|
+
|
|
201
|
+
**Risk:** Old or undocumented API endpoints remain exposed without security.
|
|
202
|
+
|
|
203
|
+
**SmartStack check:**
|
|
204
|
+
- [ ] All controllers have `[NavRoute]` attribute (discoverable)
|
|
205
|
+
- [ ] Deprecated endpoints marked with `[Obsolete]`
|
|
206
|
+
- [ ] `[ProducesResponseType]` on every endpoint (API documentation)
|
|
207
|
+
- [ ] No orphan controllers without matching permissions
|
|
208
|
+
</api9_inventory>
|
|
209
|
+
|
|
210
|
+
<api10_unsafe_consumption>
|
|
211
|
+
## API10 - Unsafe Consumption of APIs
|
|
212
|
+
|
|
213
|
+
**Risk:** Application blindly trusts data from third-party APIs.
|
|
214
|
+
|
|
215
|
+
**SmartStack check:**
|
|
216
|
+
- [ ] External API responses validated/deserialized with typed DTOs
|
|
217
|
+
- [ ] Timeout and retry policies on `HttpClient` (Polly)
|
|
218
|
+
- [ ] Circuit breaker pattern for unreliable external services
|
|
219
|
+
- [ ] External data sanitized before storage
|
|
220
|
+
</api10_unsafe_consumption>
|
|
221
|
+
|
|
222
|
+
<severity_mapping>
|
|
223
|
+
## Mapping to SmartStack SEC-xxx Categories
|
|
224
|
+
|
|
225
|
+
| OWASP API | SmartStack Check | Severity |
|
|
226
|
+
|-----------|-----------------|----------|
|
|
227
|
+
| API1 BOLA | SEC-001: Missing tenant filter | blocking |
|
|
228
|
+
| API2 Auth | SEC-002: Missing [Authorize] | blocking |
|
|
229
|
+
| API3 Property Auth | SEC-003: Entity binding (mass assignment) | critical |
|
|
230
|
+
| API4 Resource | SEC-004: No pagination limit | warning |
|
|
231
|
+
| API5 Function Auth | SEC-005: Missing [RequirePermission] | blocking |
|
|
232
|
+
| API6 Business Flow | SEC-006: No rate limit on sensitive ops | warning |
|
|
233
|
+
| API7 SSRF | SEC-007: Unvalidated URL in HttpClient | blocking |
|
|
234
|
+
| API8 Misconfig | SEC-008: CORS/Debug/Headers | critical |
|
|
235
|
+
| API9 Inventory | SEC-009: Undocumented endpoint | info |
|
|
236
|
+
| API10 Unsafe API | SEC-010: Unvalidated external data | warning |
|
|
237
|
+
</severity_mapping>
|
|
238
|
+
|
|
239
|
+
<sources>
|
|
240
|
+
- [OWASP API Security Top 10 (2023)](https://owasp.org/API-Security/editions/2023/en/0x11-t10/)
|
|
241
|
+
- [OWASP API Security Project](https://owasp.org/www-project-api-security/)
|
|
242
|
+
- SmartStack RBAC and Multi-tenant documentation
|
|
243
|
+
</sources>
|
|
@@ -17,8 +17,35 @@ Configuration hardening:
|
|
|
17
17
|
|
|
18
18
|
- [ ] No default credentials
|
|
19
19
|
- [ ] Debug mode disabled in production
|
|
20
|
-
- [ ] Secure headers present (
|
|
20
|
+
- [ ] Secure headers present (see below)
|
|
21
21
|
- [ ] Error messages don't expose internals
|
|
22
|
+
|
|
23
|
+
**Security headers configuration (ASP.NET Core):**
|
|
24
|
+
```csharp
|
|
25
|
+
// Program.cs
|
|
26
|
+
app.UseHsts(); // HTTP Strict Transport Security (production only)
|
|
27
|
+
|
|
28
|
+
app.Use(async (context, next) =>
|
|
29
|
+
{
|
|
30
|
+
var headers = context.Response.Headers;
|
|
31
|
+
headers["X-Content-Type-Options"] = "nosniff";
|
|
32
|
+
headers["X-Frame-Options"] = "DENY";
|
|
33
|
+
headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
|
|
34
|
+
headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()";
|
|
35
|
+
headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";
|
|
36
|
+
await next();
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Expected headers in responses:**
|
|
41
|
+
| Header | Value | Purpose |
|
|
42
|
+
|--------|-------|---------|
|
|
43
|
+
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Force HTTPS |
|
|
44
|
+
| `X-Content-Type-Options` | `nosniff` | Prevent MIME sniffing |
|
|
45
|
+
| `X-Frame-Options` | `DENY` | Prevent clickjacking |
|
|
46
|
+
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer info |
|
|
47
|
+
| `Permissions-Policy` | `camera=(), microphone=()` | Restrict browser features |
|
|
48
|
+
| `Content-Security-Policy` | `default-src 'self'` | Prevent XSS via inline scripts |
|
|
22
49
|
</a02_security_misconfiguration>
|
|
23
50
|
|
|
24
51
|
<a04_cryptographic_failures>
|
|
@@ -71,6 +98,64 @@ Access control requirements:
|
|
|
71
98
|
✓ No vertical escalation (user → admin functions)
|
|
72
99
|
</authorization>
|
|
73
100
|
|
|
101
|
+
<rate_limiting>
|
|
102
|
+
## Rate Limiting & Throttling
|
|
103
|
+
|
|
104
|
+
**ASP.NET Core built-in middleware** (`Microsoft.AspNetCore.RateLimiting`):
|
|
105
|
+
|
|
106
|
+
```csharp
|
|
107
|
+
// Program.cs
|
|
108
|
+
builder.Services.AddRateLimiter(options =>
|
|
109
|
+
{
|
|
110
|
+
// Global fixed window: 100 requests per minute
|
|
111
|
+
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
|
|
112
|
+
RateLimitPartition.GetFixedWindowLimiter(
|
|
113
|
+
partitionKey: context.User?.FindFirst("tenant_id")?.Value ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
|
|
114
|
+
factory: _ => new FixedWindowRateLimiterOptions
|
|
115
|
+
{
|
|
116
|
+
PermitLimit = 100,
|
|
117
|
+
Window = TimeSpan.FromMinutes(1),
|
|
118
|
+
QueueLimit = 0
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
// Named policy for sensitive endpoints
|
|
122
|
+
options.AddFixedWindowLimiter("auth", opt =>
|
|
123
|
+
{
|
|
124
|
+
opt.PermitLimit = 5;
|
|
125
|
+
opt.Window = TimeSpan.FromMinutes(15);
|
|
126
|
+
opt.QueueLimit = 0;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
app.UseRateLimiter();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Controller usage:**
|
|
136
|
+
```csharp
|
|
137
|
+
[EnableRateLimiting("auth")]
|
|
138
|
+
[HttpPost("login")]
|
|
139
|
+
[AllowAnonymous]
|
|
140
|
+
public async Task<ActionResult> Login([FromBody] LoginDto dto) { ... }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Available strategies:**
|
|
144
|
+
| Strategy | Use case |
|
|
145
|
+
|----------|----------|
|
|
146
|
+
| Fixed Window | General API protection |
|
|
147
|
+
| Sliding Window | Smoother rate distribution |
|
|
148
|
+
| Token Bucket | Burst-tolerant endpoints |
|
|
149
|
+
| Concurrency | Limit parallel requests (file upload, reports) |
|
|
150
|
+
|
|
151
|
+
**Checklist:**
|
|
152
|
+
- [ ] Global rate limiter configured
|
|
153
|
+
- [ ] Partition by tenant (multi-tenant) or IP (anonymous)
|
|
154
|
+
- [ ] Stricter limits on auth endpoints (login, register, password reset)
|
|
155
|
+
- [ ] `429 Too Many Requests` returned with `Retry-After` header
|
|
156
|
+
- [ ] Rate limit headers present (`X-RateLimit-Limit`, `X-RateLimit-Remaining`)
|
|
157
|
+
</rate_limiting>
|
|
158
|
+
|
|
74
159
|
<search_patterns>
|
|
75
160
|
Grep patterns for vulnerability detection:
|
|
76
161
|
|
|
@@ -105,6 +105,172 @@ public class Permission : SystemEntity
|
|
|
105
105
|
</system_entity>
|
|
106
106
|
</entities>
|
|
107
107
|
|
|
108
|
+
<value_objects>
|
|
109
|
+
## Value Objects (DDD Pattern)
|
|
110
|
+
|
|
111
|
+
Value Objects are immutable types defined by their attributes rather than identity. Use them to replace primitive types that carry domain meaning and validation.
|
|
112
|
+
|
|
113
|
+
**When to use a Value Object:**
|
|
114
|
+
| Instead of | Use | Why |
|
|
115
|
+
|------------|-----|-----|
|
|
116
|
+
| `string Email` | `Email` record | Self-validating, prevents invalid state |
|
|
117
|
+
| `decimal Amount` + `string Currency` | `Money` record | Always consistent pair |
|
|
118
|
+
| `string Street` + `string City` + ... | `Address` record | Cohesive group |
|
|
119
|
+
| `DateTime Start` + `DateTime End` | `DateRange` record | Enforces Start < End invariant |
|
|
120
|
+
|
|
121
|
+
**Structure (C# record):**
|
|
122
|
+
```csharp
|
|
123
|
+
// Domain/ValueObjects/Email.cs
|
|
124
|
+
public sealed record Email
|
|
125
|
+
{
|
|
126
|
+
public string Value { get; }
|
|
127
|
+
|
|
128
|
+
public Email(string value)
|
|
129
|
+
{
|
|
130
|
+
if (string.IsNullOrWhiteSpace(value))
|
|
131
|
+
throw new DomainException("Email is required");
|
|
132
|
+
if (!value.Contains('@'))
|
|
133
|
+
throw new DomainException("Invalid email format");
|
|
134
|
+
|
|
135
|
+
Value = value.Trim().ToLowerInvariant();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public static implicit operator string(Email email) => email.Value;
|
|
139
|
+
public override string ToString() => Value;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**EF Core mapping:**
|
|
144
|
+
```csharp
|
|
145
|
+
// Option 1: Owned type (separate columns)
|
|
146
|
+
builder.OwnsOne(e => e.Email, email =>
|
|
147
|
+
{
|
|
148
|
+
email.Property(v => v.Value).HasColumnName("Email").HasMaxLength(256);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Option 2: Value conversion (single column)
|
|
152
|
+
builder.Property(e => e.Email)
|
|
153
|
+
.HasConversion(
|
|
154
|
+
v => v.Value,
|
|
155
|
+
v => new Email(v))
|
|
156
|
+
.HasMaxLength(256);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Entity vs Value Object:**
|
|
160
|
+
| Criteria | Entity | Value Object |
|
|
161
|
+
|----------|--------|-------------|
|
|
162
|
+
| Identity | Has `Id` (Guid) | No identity |
|
|
163
|
+
| Equality | By Id | By all properties |
|
|
164
|
+
| Mutability | Mutable (behavior methods) | Immutable (records) |
|
|
165
|
+
| Lifecycle | Independent | Owned by entity |
|
|
166
|
+
| Example | `User`, `Order` | `Email`, `Money`, `Address` |
|
|
167
|
+
|
|
168
|
+
**Code review detection:** If an entity has `string Email`, `string Phone`, `decimal Amount` + `string Currency` as separate primitives, suggest replacing with Value Objects.
|
|
169
|
+
</value_objects>
|
|
170
|
+
|
|
171
|
+
<domain_events>
|
|
172
|
+
## Domain Events
|
|
173
|
+
|
|
174
|
+
Domain Events capture something that happened in the domain. They enable decoupled communication between aggregates and complement the existing Workflow trigger system.
|
|
175
|
+
|
|
176
|
+
**Relationship with Workflow Triggers:**
|
|
177
|
+
| Mechanism | Scope | Use case |
|
|
178
|
+
|-----------|-------|----------|
|
|
179
|
+
| Domain Events (MediatR) | In-process, synchronous | Cache invalidation, audit logging, read-model updates |
|
|
180
|
+
| Workflow Triggers (`TriggerAsync`) | Cross-process, async | Emails, webhooks, multi-step business processes |
|
|
181
|
+
| **Combined** | Domain Event handler calls `TriggerAsync()` | Best of both worlds |
|
|
182
|
+
|
|
183
|
+
**Structure:**
|
|
184
|
+
```csharp
|
|
185
|
+
// Domain/Events/OrderCreatedEvent.cs
|
|
186
|
+
public sealed record OrderCreatedEvent(
|
|
187
|
+
Guid OrderId,
|
|
188
|
+
Guid TenantId,
|
|
189
|
+
Guid CreatedById,
|
|
190
|
+
DateTime OccurredAt) : IDomainEvent;
|
|
191
|
+
|
|
192
|
+
// Domain/Common/IDomainEvent.cs
|
|
193
|
+
public interface IDomainEvent : INotification { } // MediatR marker
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Publishing from entities:**
|
|
197
|
+
```csharp
|
|
198
|
+
public class Order : BaseEntity, ITenantEntity
|
|
199
|
+
{
|
|
200
|
+
private readonly List<IDomainEvent> _domainEvents = new();
|
|
201
|
+
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
|
|
202
|
+
|
|
203
|
+
public static Order Create(Guid tenantId, string description, Guid createdById)
|
|
204
|
+
{
|
|
205
|
+
var order = new Order
|
|
206
|
+
{
|
|
207
|
+
Id = Guid.NewGuid(),
|
|
208
|
+
TenantId = tenantId,
|
|
209
|
+
Description = description
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
order._domainEvents.Add(new OrderCreatedEvent(
|
|
213
|
+
order.Id, tenantId, createdById, DateTime.UtcNow));
|
|
214
|
+
|
|
215
|
+
return order;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public void ClearDomainEvents() => _domainEvents.Clear();
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Handling events:**
|
|
223
|
+
```csharp
|
|
224
|
+
// Application/EventHandlers/OrderCreatedEventHandler.cs
|
|
225
|
+
public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
|
|
226
|
+
{
|
|
227
|
+
private readonly IWorkflowService _workflowService;
|
|
228
|
+
private readonly ILogger<OrderCreatedEventHandler> _logger;
|
|
229
|
+
|
|
230
|
+
public async Task Handle(OrderCreatedEvent notification, CancellationToken ct)
|
|
231
|
+
{
|
|
232
|
+
_logger.LogInformation("Order {OrderId} created", notification.OrderId);
|
|
233
|
+
|
|
234
|
+
// Bridge to Workflow triggers for cross-process actions
|
|
235
|
+
await _workflowService.TriggerAsync("order.created",
|
|
236
|
+
new Dictionary<string, object>
|
|
237
|
+
{
|
|
238
|
+
["orderId"] = notification.OrderId,
|
|
239
|
+
["tenantId"] = notification.TenantId
|
|
240
|
+
}, ct: ct);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Dispatching (in DbContext):**
|
|
246
|
+
```csharp
|
|
247
|
+
// Infrastructure/Persistence/ApplicationDbContext.cs
|
|
248
|
+
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
|
|
249
|
+
{
|
|
250
|
+
var entities = ChangeTracker.Entries<BaseEntity>()
|
|
251
|
+
.Where(e => e.Entity.DomainEvents.Any())
|
|
252
|
+
.ToList();
|
|
253
|
+
|
|
254
|
+
var events = entities.SelectMany(e => e.Entity.DomainEvents).ToList();
|
|
255
|
+
|
|
256
|
+
var result = await base.SaveChangesAsync(ct);
|
|
257
|
+
|
|
258
|
+
foreach (var @event in events)
|
|
259
|
+
await _mediator.Publish(@event, ct);
|
|
260
|
+
|
|
261
|
+
foreach (var entity in entities)
|
|
262
|
+
entity.Entity.ClearDomainEvents();
|
|
263
|
+
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**When to add Domain Events (code review check):**
|
|
269
|
+
- Entity has side effects in `Create()` or behavior methods → Domain Event candidate
|
|
270
|
+
- Service calls `TriggerAsync()` right after `SaveChangesAsync()` → Should be Domain Event
|
|
271
|
+
- Multiple services react to the same entity change → Domain Event decouples them
|
|
272
|
+
</domain_events>
|
|
273
|
+
|
|
108
274
|
<tables>
|
|
109
275
|
## Table Naming Conventions
|
|
110
276
|
|
|
@@ -156,6 +156,33 @@ workflowsApi.execute(workflowId, variables)
|
|
|
156
156
|
□ Tests: trigger, emails, variables
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
## RELATION WITH DOMAIN EVENTS
|
|
160
|
+
|
|
161
|
+
Workflow triggers (`TriggerAsync`) are the **primary mechanism** for cross-process automation (emails, webhooks, multi-step flows).
|
|
162
|
+
|
|
163
|
+
**Domain Events** (MediatR `INotificationHandler<T>`) are a complementary pattern for **in-process** side effects (audit logging, cache invalidation, read-model updates).
|
|
164
|
+
|
|
165
|
+
**Recommended pattern:** Domain Event handler bridges to Workflow triggers:
|
|
166
|
+
```csharp
|
|
167
|
+
// OrderCreatedEventHandler calls TriggerAsync for cross-process actions
|
|
168
|
+
public async Task Handle(OrderCreatedEvent notification, CancellationToken ct)
|
|
169
|
+
{
|
|
170
|
+
await _workflowService.TriggerAsync("order.created",
|
|
171
|
+
new Dictionary<string, object>{ ["orderId"] = notification.OrderId }, ct: ct);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**When to use which:**
|
|
176
|
+
| Scenario | Mechanism |
|
|
177
|
+
|----------|-----------|
|
|
178
|
+
| Send email after entity creation | Workflow Trigger |
|
|
179
|
+
| Invalidate cache after entity update | Domain Event |
|
|
180
|
+
| Log audit trail | Domain Event |
|
|
181
|
+
| Multi-step business process with delays | Workflow Trigger |
|
|
182
|
+
| Notify multiple services of same change | Domain Event → Workflow Trigger |
|
|
183
|
+
|
|
184
|
+
See `review-code/references/smartstack-conventions.md` (Domain Events section) for the full implementation pattern.
|
|
185
|
+
|
|
159
186
|
## ABSOLUTE RULES
|
|
160
187
|
|
|
161
188
|
| DO | DON'T |
|