@polymorphism-tech/morph-spec 2.3.0 → 3.0.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/CLAUDE.md +446 -1730
- package/README.md +515 -516
- package/bin/morph-spec.js +366 -294
- package/bin/task-manager.js +429 -368
- package/bin/validate.js +369 -268
- package/content/.claude/commands/morph-apply.md +221 -158
- package/content/.claude/commands/morph-deploy.md +529 -0
- package/content/.claude/commands/morph-preflight.md +227 -0
- package/content/.claude/commands/morph-proposal.md +122 -101
- package/content/.claude/commands/morph-status.md +86 -86
- package/content/.claude/commands/morph-troubleshoot.md +122 -0
- package/content/.claude/skills/infra/azure-deploy-specialist.md +699 -0
- package/content/.claude/skills/level-0-meta/README.md +7 -0
- package/content/.claude/skills/level-0-meta/code-review.md +226 -0
- package/content/.claude/skills/level-0-meta/morph-checklist.md +117 -0
- package/content/.claude/skills/level-0-meta/simulation-checklist.md +77 -0
- package/content/.claude/skills/level-1-workflows/README.md +7 -0
- package/content/.claude/skills/level-1-workflows/morph-replicate.md +213 -0
- package/content/.claude/{commands/morph-clarify.md → skills/level-1-workflows/phase-clarify.md} +131 -184
- package/content/.claude/{commands/morph-design.md → skills/level-1-workflows/phase-design.md} +213 -275
- package/content/.claude/skills/level-1-workflows/phase-setup.md +106 -0
- package/content/.claude/skills/level-1-workflows/phase-tasks.md +164 -0
- package/content/.claude/{commands/morph-uiux.md → skills/level-1-workflows/phase-uiux.md} +169 -211
- package/content/.claude/skills/level-2-domains/README.md +14 -0
- package/content/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +192 -0
- package/content/.claude/skills/{specialists → level-2-domains/architecture}/po-pm-advisor.md +197 -197
- package/content/.claude/skills/level-2-domains/architecture/standards-architect.md +156 -0
- package/content/.claude/skills/level-2-domains/backend/dotnet-senior.md +287 -0
- package/content/.claude/skills/level-2-domains/backend/ef-modeler.md +113 -0
- package/content/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +126 -0
- package/content/.claude/skills/level-2-domains/backend/ms-agent-expert.md +109 -0
- package/content/.claude/skills/level-2-domains/frontend/blazor-builder.md +210 -0
- package/content/.claude/skills/level-2-domains/frontend/nextjs-expert.md +154 -0
- package/content/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +191 -0
- package/content/.claude/skills/{specialists → level-2-domains/infrastructure}/azure-architect.md +142 -142
- package/content/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +126 -0
- package/content/.claude/skills/level-2-domains/infrastructure/container-specialist.md +131 -0
- package/content/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +119 -0
- package/content/.claude/skills/level-2-domains/integrations/asaas-financial.md +130 -0
- package/content/.claude/skills/level-2-domains/integrations/azure-identity.md +142 -0
- package/content/.claude/skills/level-2-domains/integrations/clerk-auth.md +108 -0
- package/content/.claude/skills/level-2-domains/integrations/resend-email.md +119 -0
- package/content/.claude/skills/level-2-domains/quality/code-analyzer.md +235 -0
- package/content/.claude/skills/level-2-domains/quality/testing-specialist.md +126 -0
- package/content/.claude/skills/level-3-technologies/README.md +7 -0
- package/content/.claude/skills/level-4-patterns/README.md +7 -0
- package/content/.claude/skills/specialists/prompt-engineer.md +189 -0
- package/content/.claude/skills/specialists/seo-growth-hacker.md +320 -0
- package/content/.morph/config/agents.json +762 -242
- package/content/.morph/config/config.template.json +122 -108
- package/content/.morph/docs/workflows/design-impl.md +37 -0
- package/content/.morph/docs/workflows/enforcement-pipeline.md +668 -0
- package/content/.morph/docs/workflows/fast-track.md +29 -0
- package/content/.morph/docs/workflows/full-morph.md +76 -0
- package/content/.morph/docs/workflows/standard.md +44 -0
- package/content/.morph/docs/workflows/ui-refresh.md +39 -0
- package/content/.morph/examples/scheduled-reports/decisions.md +158 -0
- package/content/.morph/examples/scheduled-reports/proposal.md +95 -0
- package/content/.morph/examples/scheduled-reports/spec.md +267 -0
- package/content/.morph/hooks/README.md +348 -239
- package/content/.morph/hooks/pre-commit-agents.sh +24 -24
- package/content/.morph/hooks/task-completed.js +73 -0
- package/content/.morph/hooks/teammate-idle.js +68 -0
- package/content/.morph/schemas/tasks.schema.json +220 -0
- package/content/.morph/standards/agent-framework-blazor-ui.md +359 -0
- package/content/.morph/standards/agent-framework-production.md +410 -0
- package/content/.morph/standards/agent-framework-setup.md +413 -453
- package/content/.morph/standards/agent-framework-workflows.md +349 -0
- package/content/.morph/standards/agent-teams-workflow.md +474 -0
- package/content/.morph/standards/architecture.md +325 -325
- package/content/.morph/standards/azure.md +605 -379
- package/content/.morph/standards/dotnet10-migration.md +520 -494
- package/content/.morph/templates/CONTEXT-FEATURE.md +276 -0
- package/content/.morph/templates/CONTEXT.md +170 -0
- package/content/.morph/templates/agent.cs +163 -172
- package/content/.morph/templates/clarify-questions.md +159 -0
- package/content/.morph/templates/contracts/Commands.cs +74 -0
- package/content/.morph/templates/contracts/Entities.cs +25 -0
- package/content/.morph/templates/contracts/Queries.cs +74 -0
- package/content/.morph/templates/contracts/README.md +74 -0
- package/content/.morph/templates/decisions.md +123 -106
- package/content/.morph/templates/infra/azure-pipelines-deploy.yml +480 -0
- package/content/.morph/templates/infra/deploy-checklist.md +426 -0
- package/content/.morph/templates/proposal.md +141 -155
- package/content/.morph/templates/recap.md +94 -105
- package/content/.morph/templates/simulation.md +353 -0
- package/content/.morph/templates/spec.md +149 -148
- package/content/.morph/templates/state.template.json +222 -222
- package/content/.morph/templates/tasks.md +257 -235
- package/content/.morph/templates/ui-components.md +362 -276
- package/content/CLAUDE.md +150 -442
- package/detectors/structure-detector.js +245 -250
- package/docs/README.md +144 -149
- package/docs/getting-started.md +301 -302
- package/docs/installation.md +361 -361
- package/docs/validation-checklist.md +265 -266
- package/package.json +80 -80
- package/src/commands/advance-phase.js +266 -0
- package/src/commands/analyze-blazor-concurrency.js +193 -0
- package/src/commands/deploy.js +780 -0
- package/src/commands/detect-agents.js +167 -0
- package/src/commands/doctor.js +356 -280
- package/src/commands/generate-context.js +40 -0
- package/src/commands/init.js +258 -245
- package/src/commands/lint-fluent.js +352 -0
- package/src/commands/rollback-phase.js +185 -0
- package/src/commands/session-summary.js +291 -0
- package/src/commands/task.js +78 -75
- package/src/commands/troubleshoot.js +222 -0
- package/src/commands/update.js +192 -159
- package/src/commands/validate-blazor-state.js +210 -0
- package/src/commands/validate-blazor.js +156 -0
- package/src/commands/validate-css.js +84 -0
- package/src/commands/validate-phase.js +221 -0
- package/src/lib/blazor-concurrency-analyzer.js +288 -0
- package/src/lib/blazor-state-validator.js +291 -0
- package/src/lib/blazor-validator.js +374 -0
- package/src/lib/complexity-analyzer.js +441 -292
- package/src/lib/context-generator.js +513 -0
- package/src/lib/continuous-validator.js +421 -440
- package/src/lib/css-validator.js +352 -0
- package/src/lib/decision-constraint-loader.js +109 -0
- package/src/lib/design-system-detector.js +187 -0
- package/src/lib/design-system-scaffolder.js +299 -0
- package/src/lib/hook-executor.js +256 -0
- package/src/lib/recap-generator.js +205 -0
- package/src/lib/spec-validator.js +258 -0
- package/src/lib/standards-context-injector.js +287 -0
- package/src/lib/state-manager.js +397 -340
- package/src/lib/team-orchestrator.js +322 -0
- package/src/lib/troubleshoot-grep.js +194 -0
- package/src/lib/troubleshoot-index.js +144 -0
- package/src/lib/validation-runner.js +283 -0
- package/src/lib/validators/contract-compliance-validator.js +273 -0
- package/src/lib/validators/design-system-validator.js +231 -0
- package/src/utils/file-copier.js +187 -139
- package/content/.claude/commands/morph-costs.md +0 -206
- package/content/.claude/commands/morph-setup.md +0 -100
- package/content/.claude/commands/morph-tasks.md +0 -319
- package/content/.claude/skills/infra/bicep-architect.md +0 -419
- package/content/.claude/skills/infra/container-specialist.md +0 -437
- package/content/.claude/skills/infra/devops-engineer.md +0 -405
- package/content/.claude/skills/integrations/asaas-financial.md +0 -333
- package/content/.claude/skills/integrations/azure-identity.md +0 -309
- package/content/.claude/skills/integrations/clerk-auth.md +0 -290
- package/content/.claude/skills/specialists/ai-system-architect.md +0 -604
- package/content/.claude/skills/specialists/cost-guardian.md +0 -110
- package/content/.claude/skills/specialists/ef-modeler.md +0 -211
- package/content/.claude/skills/specialists/hangfire-orchestrator.md +0 -255
- package/content/.claude/skills/specialists/ms-agent-expert.md +0 -263
- package/content/.claude/skills/specialists/standards-architect.md +0 -78
- package/content/.claude/skills/specialists/ui-ux-designer.md +0 -1100
- package/content/.claude/skills/stacks/dotnet-blazor.md +0 -606
- package/content/.claude/skills/stacks/dotnet-nextjs.md +0 -402
- package/content/.claude/skills/stacks/shopify.md +0 -445
- package/content/.morph/config/azure-pricing.json +0 -70
- package/content/.morph/config/azure-pricing.schema.json +0 -50
- package/content/.morph/hooks/pre-commit-costs.sh +0 -91
- package/docs/api/cost-calculator.js.html +0 -513
- package/docs/api/design-system-generator.js.html +0 -382
- package/docs/api/global.html +0 -5263
- package/docs/api/index.html +0 -96
- package/docs/api/state-manager.js.html +0 -423
- package/src/commands/cost.js +0 -181
- package/src/commands/update-pricing.js +0 -206
- package/src/lib/cost-calculator.js +0 -429
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Asaas Financial
|
|
2
|
+
|
|
3
|
+
> **Layer:** 2 | **Load:** on-keyword | **Keywords:** asaas, payment, pix, boleto, cobranca, subscription, billing, pagamento
|
|
4
|
+
|
|
5
|
+
Integração com Asaas para pagamentos no Brasil. API REST, sem SDK .NET oficial. Suporta PIX, Boleto, Cartão.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```csharp
|
|
10
|
+
// appsettings.json
|
|
11
|
+
{ "Asaas": { "BaseUrl": "https://sandbox.asaas.com/api/v3", "ApiKey": "${ASAAS_API_KEY}" } }
|
|
12
|
+
|
|
13
|
+
// Program.cs
|
|
14
|
+
builder.Services.Configure<AsaasOptions>(builder.Configuration.GetSection("Asaas"));
|
|
15
|
+
builder.Services.AddHttpClient<IAsaasClient, AsaasClient>((sp, client) =>
|
|
16
|
+
{
|
|
17
|
+
var options = sp.GetRequiredService<IOptions<AsaasOptions>>().Value;
|
|
18
|
+
client.BaseAddress = new Uri(options.BaseUrl);
|
|
19
|
+
client.DefaultRequestHeaders.Add("access_token", options.ApiKey);
|
|
20
|
+
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Client Interface
|
|
25
|
+
|
|
26
|
+
```csharp
|
|
27
|
+
public interface IAsaasClient
|
|
28
|
+
{
|
|
29
|
+
Task<AsaasCustomer> CreateCustomerAsync(CreateCustomerRequest request);
|
|
30
|
+
Task<AsaasPayment> CreatePaymentAsync(CreatePaymentRequest request);
|
|
31
|
+
Task<AsaasPayment> GetPaymentAsync(string paymentId);
|
|
32
|
+
Task<AsaasSubscription> CreateSubscriptionAsync(CreateSubscriptionRequest request);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Implementation: standard `HttpClient.PostAsJsonAsync` / `GetAsync` + `ReadFromJsonAsync` pattern. Log errors, throw `AsaasException` on failure.
|
|
37
|
+
|
|
38
|
+
## DTOs (all use `[JsonPropertyName]`)
|
|
39
|
+
|
|
40
|
+
```csharp
|
|
41
|
+
public record CreateCustomerRequest
|
|
42
|
+
{
|
|
43
|
+
[JsonPropertyName("name")] public required string Name { get; init; }
|
|
44
|
+
[JsonPropertyName("cpfCnpj")] public required string CpfCnpj { get; init; } // REQUIRED even in sandbox
|
|
45
|
+
[JsonPropertyName("email")] public string? Email { get; init; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public record CreatePaymentRequest
|
|
49
|
+
{
|
|
50
|
+
[JsonPropertyName("customer")] public required string Customer { get; init; }
|
|
51
|
+
[JsonPropertyName("billingType")] public required string BillingType { get; init; } // BOLETO, PIX, CREDIT_CARD
|
|
52
|
+
[JsonPropertyName("value")] public required decimal Value { get; init; }
|
|
53
|
+
[JsonPropertyName("dueDate")] public required string DueDate { get; init; } // yyyy-MM-dd
|
|
54
|
+
[JsonPropertyName("description")] public string? Description { get; init; }
|
|
55
|
+
[JsonPropertyName("externalReference")] public string? ExternalReference { get; init; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public record AsaasPayment
|
|
59
|
+
{
|
|
60
|
+
[JsonPropertyName("id")] public string Id { get; init; } = "";
|
|
61
|
+
[JsonPropertyName("status")] public string Status { get; init; } = ""; // PENDING, RECEIVED, CONFIRMED, OVERDUE
|
|
62
|
+
[JsonPropertyName("invoiceUrl")] public string? InvoiceUrl { get; init; }
|
|
63
|
+
[JsonPropertyName("bankSlipUrl")] public string? BankSlipUrl { get; init; }
|
|
64
|
+
[JsonPropertyName("pixQrCode")] public AsaasPixQrCode? PixQrCode { get; init; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public record AsaasPixQrCode
|
|
68
|
+
{
|
|
69
|
+
[JsonPropertyName("encodedImage")] public string? EncodedImage { get; init; } // Base64
|
|
70
|
+
[JsonPropertyName("payload")] public string? Payload { get; init; } // PIX copia-e-cola
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Follow same pattern for `CreateSubscriptionRequest` (add `cycle`: MONTHLY/WEEKLY/YEARLY, `nextDueDate`).
|
|
75
|
+
|
|
76
|
+
## Webhooks
|
|
77
|
+
|
|
78
|
+
```csharp
|
|
79
|
+
[ApiController, Route("api/webhooks/asaas")]
|
|
80
|
+
public class AsaasWebhookController(IPaymentService payments, ILogger<AsaasWebhookController> logger) : ControllerBase
|
|
81
|
+
{
|
|
82
|
+
[HttpPost]
|
|
83
|
+
public async Task<IActionResult> Handle([FromBody] AsaasWebhookPayload payload)
|
|
84
|
+
{
|
|
85
|
+
logger.LogInformation("Asaas webhook: {Event} payment {Id}", payload.Event, payload.Payment?.Id);
|
|
86
|
+
switch (payload.Event)
|
|
87
|
+
{
|
|
88
|
+
case "PAYMENT_CONFIRMED": case "PAYMENT_RECEIVED":
|
|
89
|
+
await payments.ConfirmPaymentAsync(payload.Payment!.Id); break;
|
|
90
|
+
case "PAYMENT_OVERDUE":
|
|
91
|
+
await payments.MarkOverdueAsync(payload.Payment!.Id); break;
|
|
92
|
+
case "PAYMENT_REFUNDED":
|
|
93
|
+
await payments.RefundPaymentAsync(payload.Payment!.Id); break;
|
|
94
|
+
}
|
|
95
|
+
return Ok();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Environments
|
|
101
|
+
|
|
102
|
+
| Environment | Base URL |
|
|
103
|
+
|-------------|----------|
|
|
104
|
+
| Sandbox | `https://sandbox.asaas.com/api/v3` |
|
|
105
|
+
| Production | `https://www.asaas.com/api/v3` |
|
|
106
|
+
|
|
107
|
+
## Gotchas
|
|
108
|
+
|
|
109
|
+
| Issue | Fix |
|
|
110
|
+
|-------|-----|
|
|
111
|
+
| PIX QR Code returns Base64, NOT URL | `$"data:image/png;base64,{response.EncodedImage}"` |
|
|
112
|
+
| CPF required even in sandbox | Always include `cpfCnpj` in CreateCustomer |
|
|
113
|
+
| Missing `User-Agent` header | Add to HttpClient defaults |
|
|
114
|
+
| Date format must be `yyyy-MM-dd` | `DateTime.Today.AddDays(1).ToString("yyyy-MM-dd")` |
|
|
115
|
+
|
|
116
|
+
## Checklist
|
|
117
|
+
|
|
118
|
+
- [ ] API Key configured (not hardcoded)
|
|
119
|
+
- [ ] HttpClient with retry policy (Polly)
|
|
120
|
+
- [ ] Headers: `access_token` + `User-Agent`
|
|
121
|
+
- [ ] CPF/CNPJ always included for customers
|
|
122
|
+
- [ ] PIX QR Code → data URI conversion
|
|
123
|
+
- [ ] Dates in `yyyy-MM-dd` format
|
|
124
|
+
- [ ] Webhook endpoint + signature validation
|
|
125
|
+
- [ ] ExternalReference for reconciliation
|
|
126
|
+
- [ ] Tests with sandbox
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Azure Identity (Microsoft Identity)
|
|
2
|
+
|
|
3
|
+
> **Layer:** 2 | **Load:** on-keyword | **Keywords:** identity, entra, azure ad, microsoft auth, msal, oauth, oidc, microsoft identity
|
|
4
|
+
|
|
5
|
+
Microsoft Identity Platform for .NET/Blazor. SDK: `Microsoft.Identity.Web`.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
dotnet add package Microsoft.Identity.Web
|
|
11
|
+
dotnet add package Microsoft.Identity.Web.UI # For Blazor
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
// appsettings.json
|
|
16
|
+
{ "AzureAd": {
|
|
17
|
+
"Instance": "https://login.microsoftonline.com/",
|
|
18
|
+
"Domain": "yourdomain.onmicrosoft.com",
|
|
19
|
+
"TenantId": "your-tenant-id",
|
|
20
|
+
"ClientId": "your-client-id",
|
|
21
|
+
"ClientSecret": "${AZURE_AD_CLIENT_SECRET}",
|
|
22
|
+
"CallbackPath": "/signin-oidc"
|
|
23
|
+
} }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```csharp
|
|
27
|
+
// Program.cs
|
|
28
|
+
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
29
|
+
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
|
|
30
|
+
builder.Services.AddControllersWithViews().AddMicrosoftIdentityUI();
|
|
31
|
+
builder.Services.AddAuthorization();
|
|
32
|
+
app.UseAuthentication();
|
|
33
|
+
app.UseAuthorization();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Blazor Server
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
// Additional setup
|
|
40
|
+
builder.Services.AddServerSideBlazor().AddMicrosoftIdentityConsentHandler();
|
|
41
|
+
|
|
42
|
+
// App.razor
|
|
43
|
+
<CascadingAuthenticationState>
|
|
44
|
+
<Router AppAssembly="@typeof(App).Assembly">
|
|
45
|
+
<Found Context="routeData">
|
|
46
|
+
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
|
47
|
+
<NotAuthorized>
|
|
48
|
+
@if (!context.User.Identity?.IsAuthenticated ?? true) { <RedirectToLogin /> }
|
|
49
|
+
else { <p>No permission.</p> }
|
|
50
|
+
</NotAuthorized>
|
|
51
|
+
</AuthorizeRouteView>
|
|
52
|
+
</Found>
|
|
53
|
+
</Router>
|
|
54
|
+
</CascadingAuthenticationState>
|
|
55
|
+
|
|
56
|
+
// RedirectToLogin.razor
|
|
57
|
+
@inject NavigationManager Nav
|
|
58
|
+
@code {
|
|
59
|
+
protected override void OnInitialized() =>
|
|
60
|
+
Nav.NavigateTo($"MicrosoftIdentity/Account/SignIn?redirectUri={Uri.EscapeDataString(Nav.Uri)}", forceLoad: true);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Protected Pages
|
|
65
|
+
|
|
66
|
+
```razor
|
|
67
|
+
@page "/secure"
|
|
68
|
+
@attribute [Authorize]
|
|
69
|
+
|
|
70
|
+
<AuthorizeView>
|
|
71
|
+
<Authorized>Welcome, @context.User.Identity?.Name!</Authorized>
|
|
72
|
+
</AuthorizeView>
|
|
73
|
+
|
|
74
|
+
<AuthorizeView Roles="Admin"><Authorized><AdminPanel /></Authorized></AuthorizeView>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## API Protection
|
|
78
|
+
|
|
79
|
+
```csharp
|
|
80
|
+
// API Program.cs
|
|
81
|
+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
82
|
+
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
|
|
83
|
+
|
|
84
|
+
// Controller
|
|
85
|
+
[ApiController, Route("api/[controller]"), Authorize]
|
|
86
|
+
public class ProfileController : ControllerBase
|
|
87
|
+
{
|
|
88
|
+
[HttpGet]
|
|
89
|
+
public IActionResult GetProfile() => Ok(new {
|
|
90
|
+
UserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
|
|
91
|
+
Name = User.Identity?.Name
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
[HttpGet("admin"), Authorize(Roles = "Admin")]
|
|
95
|
+
public IActionResult AdminOnly() => Ok("Admin access");
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Downstream APIs (Microsoft Graph)
|
|
100
|
+
|
|
101
|
+
```csharp
|
|
102
|
+
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
|
103
|
+
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
|
|
104
|
+
.EnableTokenAcquisitionToCallDownstreamApi()
|
|
105
|
+
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
|
|
106
|
+
.AddInMemoryTokenCaches();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Authorization Policies
|
|
110
|
+
|
|
111
|
+
```csharp
|
|
112
|
+
builder.Services.AddAuthorization(o => {
|
|
113
|
+
o.AddPolicy("RequireAdmin", p => p.RequireRole("Admin"));
|
|
114
|
+
o.AddPolicy("RequireManager", p => p.RequireAssertion(c =>
|
|
115
|
+
c.User.IsInRole("Admin") || c.User.IsInRole("Manager")));
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Multi-tenant & B2C
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
// Multi-tenant: TenantId = "common" or "organizations"
|
|
123
|
+
// Then validate allowed tenants in TokenValidationParameters.IssuerValidator
|
|
124
|
+
|
|
125
|
+
// B2C: Use "AzureAdB2C" section with SignUpSignInPolicyId, ResetPasswordPolicyId
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Checklist
|
|
129
|
+
|
|
130
|
+
- [ ] App registered in Azure Portal
|
|
131
|
+
- [ ] Client ID + Tenant ID configured
|
|
132
|
+
- [ ] Client Secret in Key Vault
|
|
133
|
+
- [ ] Redirect URIs configured
|
|
134
|
+
- [ ] API permissions defined
|
|
135
|
+
- [ ] Token caching configured
|
|
136
|
+
- [ ] Authorization policies created
|
|
137
|
+
- [ ] Logout flow implemented
|
|
138
|
+
- [ ] Token expiry error handling
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Clerk Auth
|
|
2
|
+
|
|
3
|
+
> **Layer:** 2 | **Load:** on-keyword | **Keywords:** clerk, auth, login, signup, authentication, session, jwt, user
|
|
4
|
+
|
|
5
|
+
Autenticação SaaS com Clerk para .NET/Blazor. SDK: `Clerk.Net.AspNetCore.Security`.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```csharp
|
|
10
|
+
// appsettings.json
|
|
11
|
+
{ "Clerk": { "SecretKey": "${CLERK_SECRET_KEY}", "PublishableKey": "pk_test_xxx" } }
|
|
12
|
+
|
|
13
|
+
// Program.cs
|
|
14
|
+
builder.Services.AddClerk(builder.Configuration);
|
|
15
|
+
builder.Services.AddAuthentication(ClerkAuthenticationDefaults.AuthenticationScheme)
|
|
16
|
+
.AddClerk(o => { o.Authority = "https://clerk.{instance}.com"; o.ValidAudiences = ["your-app-id"]; });
|
|
17
|
+
builder.Services.AddAuthorization();
|
|
18
|
+
app.UseAuthentication();
|
|
19
|
+
app.UseAuthorization();
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Protected Endpoints
|
|
23
|
+
|
|
24
|
+
```csharp
|
|
25
|
+
// Minimal API
|
|
26
|
+
app.MapGet("/api/profile", async (ClaimsPrincipal user, IClerkClient clerk) =>
|
|
27
|
+
{
|
|
28
|
+
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
29
|
+
if (userId is null) return Results.Unauthorized();
|
|
30
|
+
var u = await clerk.Users.GetUserAsync(userId);
|
|
31
|
+
return Results.Ok(new { u.Id, Email = u.EmailAddresses.FirstOrDefault()?.EmailAddress });
|
|
32
|
+
}).RequireAuthorization();
|
|
33
|
+
|
|
34
|
+
// Controller: same pattern with [Authorize] + User.FindFirstValue()
|
|
35
|
+
// Role-based: [Authorize(Roles = "admin")]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Blazor Server
|
|
39
|
+
|
|
40
|
+
> **Ref:** Same `CascadingAuthenticationState` + `AuthorizeRouteView` pattern as `azure-identity.md`
|
|
41
|
+
|
|
42
|
+
```razor
|
|
43
|
+
@* RedirectToLogin.razor *@
|
|
44
|
+
@inject NavigationManager Nav
|
|
45
|
+
@code {
|
|
46
|
+
protected override void OnInitialized() =>
|
|
47
|
+
Nav.NavigateTo($"/sign-in?redirect_url={Uri.EscapeDataString(Nav.Uri)}", forceLoad: true);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@* Protected page *@
|
|
51
|
+
@page "/dashboard"
|
|
52
|
+
@attribute [Authorize]
|
|
53
|
+
<AuthorizeView><Authorized>Welcome, @context.User.Identity?.Name!</Authorized></AuthorizeView>
|
|
54
|
+
<AuthorizeView Roles="admin"><Authorized><AdminPanel /></Authorized></AuthorizeView>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Client Operations
|
|
58
|
+
|
|
59
|
+
```csharp
|
|
60
|
+
var user = await _clerk.Users.GetUserAsync(userId); // Get
|
|
61
|
+
var users = await _clerk.Users.GetUserListAsync(new() { Limit = 10 }); // List
|
|
62
|
+
await _clerk.Users.UpdateUserMetadataAsync(userId, new() // Update metadata
|
|
63
|
+
{ PublicMetadata = new Dictionary<string, object> { ["plan"] = "pro" } });
|
|
64
|
+
await _clerk.Users.DeleteUserAsync(userId); // Delete
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Webhooks
|
|
68
|
+
|
|
69
|
+
```csharp
|
|
70
|
+
[ApiController, Route("api/webhooks/clerk")]
|
|
71
|
+
public class ClerkWebhookController(IUserService users) : ControllerBase
|
|
72
|
+
{
|
|
73
|
+
[HttpPost]
|
|
74
|
+
public async Task<IActionResult> Handle([FromBody] ClerkWebhookPayload payload)
|
|
75
|
+
{
|
|
76
|
+
switch (payload.Type)
|
|
77
|
+
{
|
|
78
|
+
case "user.created": await users.SyncUserAsync(payload.Data); break;
|
|
79
|
+
case "user.updated": await users.UpdateUserAsync(payload.Data); break;
|
|
80
|
+
case "user.deleted": await users.DeleteUserAsync(payload.Data.Id); break;
|
|
81
|
+
}
|
|
82
|
+
return Ok();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Clerk vs Azure Identity
|
|
88
|
+
|
|
89
|
+
| Aspecto | Clerk | Azure Identity |
|
|
90
|
+
|---------|-------|----------------|
|
|
91
|
+
| Setup | Faster | More complex |
|
|
92
|
+
| Cost | Freemium (5k MAU free) | Free (Azure AD) |
|
|
93
|
+
| Social login | 20+ providers | Limited |
|
|
94
|
+
| UI components | Pre-built | Build your own |
|
|
95
|
+
| Best for | SaaS B2C, MVPs | Enterprise/Azure |
|
|
96
|
+
|
|
97
|
+
## Checklist
|
|
98
|
+
|
|
99
|
+
- [ ] Secret Key in Key Vault (not hardcoded)
|
|
100
|
+
- [ ] Authentication scheme configured
|
|
101
|
+
- [ ] Authorization policies defined
|
|
102
|
+
- [ ] Webhook endpoint + signature validation
|
|
103
|
+
- [ ] Redirect after login configured
|
|
104
|
+
- [ ] Error handling for expired sessions
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Resend Email
|
|
2
|
+
|
|
3
|
+
> **Layer:** 2 | **Load:** on-keyword | **Keywords:** resend, email, envio, transactional, notification, send email, template
|
|
4
|
+
|
|
5
|
+
Transactional email via Resend for .NET. REST API, no official SDK. Simple, developer-friendly.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```csharp
|
|
10
|
+
// appsettings.json
|
|
11
|
+
{ "Resend": { "BaseUrl": "https://api.resend.com", "ApiKey": "${RESEND_API_KEY}", "FromEmail": "noreply@yourdomain.com" } }
|
|
12
|
+
|
|
13
|
+
// Program.cs
|
|
14
|
+
builder.Services.Configure<ResendOptions>(builder.Configuration.GetSection("Resend"));
|
|
15
|
+
builder.Services.AddHttpClient<IResendClient, ResendClient>((sp, client) =>
|
|
16
|
+
{
|
|
17
|
+
var options = sp.GetRequiredService<IOptions<ResendOptions>>().Value;
|
|
18
|
+
client.BaseAddress = new Uri(options.BaseUrl);
|
|
19
|
+
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.ApiKey);
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Client Interface
|
|
24
|
+
|
|
25
|
+
```csharp
|
|
26
|
+
public interface IResendClient
|
|
27
|
+
{
|
|
28
|
+
Task<EmailResponse> SendAsync(SendEmailRequest request, CancellationToken ct = default);
|
|
29
|
+
Task<EmailResponse> SendBatchAsync(IEnumerable<SendEmailRequest> requests, CancellationToken ct = default);
|
|
30
|
+
Task<EmailDetails> GetEmailAsync(string emailId, CancellationToken ct = default);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Implementation: `HttpClient.PostAsJsonAsync("/emails")` + `ReadFromJsonAsync<EmailResponse>`. Log errors, throw `ResendException` on failure.
|
|
35
|
+
|
|
36
|
+
## DTOs
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
public record SendEmailRequest
|
|
40
|
+
{
|
|
41
|
+
[JsonPropertyName("from")] public required string From { get; init; }
|
|
42
|
+
[JsonPropertyName("to")] public required string[] To { get; init; }
|
|
43
|
+
[JsonPropertyName("subject")] public required string Subject { get; init; }
|
|
44
|
+
[JsonPropertyName("html")] public string? Html { get; init; }
|
|
45
|
+
[JsonPropertyName("text")] public string? Text { get; init; }
|
|
46
|
+
[JsonPropertyName("reply_to")] public string? ReplyTo { get; init; }
|
|
47
|
+
[JsonPropertyName("tags")] public Tag[]? Tags { get; init; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public record EmailResponse
|
|
51
|
+
{
|
|
52
|
+
[JsonPropertyName("id")] public string Id { get; init; } = "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public record Tag
|
|
56
|
+
{
|
|
57
|
+
[JsonPropertyName("name")] public required string Name { get; init; }
|
|
58
|
+
[JsonPropertyName("value")] public required string Value { get; init; }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Webhooks
|
|
63
|
+
|
|
64
|
+
```csharp
|
|
65
|
+
[ApiController, Route("api/webhooks/resend")]
|
|
66
|
+
public class ResendWebhookController(IEmailTrackingService tracking, ILogger<ResendWebhookController> logger) : ControllerBase
|
|
67
|
+
{
|
|
68
|
+
[HttpPost]
|
|
69
|
+
public async Task<IActionResult> Handle([FromBody] ResendWebhookPayload payload)
|
|
70
|
+
{
|
|
71
|
+
logger.LogInformation("Resend webhook: {Type} for {EmailId}", payload.Type, payload.Data?.EmailId);
|
|
72
|
+
switch (payload.Type)
|
|
73
|
+
{
|
|
74
|
+
case "email.delivered": await tracking.MarkDeliveredAsync(payload.Data!.EmailId); break;
|
|
75
|
+
case "email.bounced": await tracking.MarkBouncedAsync(payload.Data!.EmailId); break;
|
|
76
|
+
case "email.complained": await tracking.MarkComplainedAsync(payload.Data!.EmailId); break;
|
|
77
|
+
}
|
|
78
|
+
return Ok();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Abstraction Pattern
|
|
84
|
+
|
|
85
|
+
```csharp
|
|
86
|
+
// Use IEmailService abstraction over IResendClient for testability
|
|
87
|
+
public interface IEmailService
|
|
88
|
+
{
|
|
89
|
+
Task<string> SendTransactionalAsync(string to, string subject, string htmlBody, CancellationToken ct = default);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ResendEmailService implements IEmailService using IResendClient
|
|
93
|
+
// FakeEmailClient implements IEmailService for simulation mode
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Gotchas
|
|
97
|
+
|
|
98
|
+
| Issue | Fix |
|
|
99
|
+
|-------|-----|
|
|
100
|
+
| Rate limit: 100 req/sec (free), 1000 (pro) | Queue emails via Hangfire for bulk sends |
|
|
101
|
+
| Domain verification required for production | Verify in Resend dashboard, add DNS records |
|
|
102
|
+
| `from` must match verified domain | Use `noreply@yourdomain.com` |
|
|
103
|
+
| HTML email rendering varies | Test with Litmus/Email on Acid, use MJML |
|
|
104
|
+
| Webhook signature validation | Verify `svix-signature` header in production |
|
|
105
|
+
|
|
106
|
+
## Checklist
|
|
107
|
+
|
|
108
|
+
- [ ] API Key in Key Vault (not hardcoded)
|
|
109
|
+
- [ ] HttpClient with `Authorization: Bearer` header
|
|
110
|
+
- [ ] Domain verified in Resend dashboard
|
|
111
|
+
- [ ] `IEmailService` abstraction for testability
|
|
112
|
+
- [ ] Webhook endpoint + signature validation
|
|
113
|
+
- [ ] Rate limiting handled (queue for bulk)
|
|
114
|
+
- [ ] Tags used for tracking/analytics
|
|
115
|
+
- [ ] Simulation mode (`FakeEmailClient`) for dev
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
*MORPH-SPEC by Polymorphism Tech*
|