@polymorphism-tech/morph-spec 4.17.0 → 4.19.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.
Files changed (70) hide show
  1. package/CLAUDE.md +36 -0
  2. package/README.md +2 -2
  3. package/bin/task-manager.js +12 -0
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/agents/architecture/architecture-expert.md +86 -0
  8. package/framework/agents/architecture/morph-spec-architect.md +66 -0
  9. package/framework/agents/architecture/packages-validator.md +77 -0
  10. package/framework/agents/architecture/security-expert.md +67 -0
  11. package/framework/agents/backend/ms-agent-expert.md +1 -1
  12. package/framework/agents/backend/neon-expert.md +9 -27
  13. package/framework/agents/frontend/design-system-validator.md +88 -0
  14. package/framework/agents/frontend/nextjs-component-validator.md +85 -0
  15. package/framework/agents/frontend/{ui-ux-designer.md → ui-designer.md} +31 -0
  16. package/framework/agents/integrations/neon-auth.md +151 -0
  17. package/framework/agents.json +22 -14
  18. package/framework/hooks/claude-code/post-tool-use/dispatch.js +39 -0
  19. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +78 -44
  20. package/framework/hooks/claude-code/post-tool-use/loop-detect.js +103 -0
  21. package/framework/hooks/claude-code/session-start/inject-morph-context.js +21 -2
  22. package/framework/hooks/claude-code/stop/validate-completion.js +48 -1
  23. package/framework/hooks/shared/activity-logger.js +56 -1
  24. package/framework/hooks/shared/build-check.js +79 -0
  25. package/framework/hooks/shared/payload-utils.js +39 -0
  26. package/framework/hooks/shared/session-snapshot.js +105 -0
  27. package/framework/phases.json +1 -1
  28. package/framework/rules/neon-auth-standards.md +13 -0
  29. package/framework/rules/neon-standards.md +1 -0
  30. package/framework/skills/level-0-meta/mcp-registry.json +34 -1
  31. package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +3 -3
  32. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
  33. package/framework/skills/level-0-meta/morph-init/SKILL.md +4 -4
  34. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
  35. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +11 -1
  36. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +12 -1
  37. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
  38. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
  39. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +119 -11
  40. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  41. package/framework/standards/STANDARDS.json +13 -9
  42. package/framework/standards/ai-agents/production.md +5 -2
  43. package/framework/standards/ai-agents/setup.md +13 -14
  44. package/framework/standards/ai-agents/workflows.md +8 -6
  45. package/framework/standards/backend/database/postgresql/database.md +5 -5
  46. package/framework/standards/backend/database/vector-search-rag.md +1 -1
  47. package/framework/standards/backend/integrations/asaas/asaas-api.md +1 -1
  48. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +364 -0
  49. package/framework/standards/frontend/nextjs/nextjs-patterns.md +9 -9
  50. package/framework/templates/README.md +2 -2
  51. package/framework/templates/REGISTRY.json +5 -5
  52. package/framework/templates/code/dotnet/jobs/agent.cs +0 -1
  53. package/framework/templates/code/sql/postgresql-migration.sql +10 -10
  54. package/framework/templates/code/sql/postgresql-migration.template.sql +12 -14
  55. package/framework/templates/code/sql/postgresql-rls-policy.sql +6 -9
  56. package/framework/templates/feature/decisions.md +8 -7
  57. package/framework/templates/integrations/neon-auth-config.cs +133 -0
  58. package/framework/workflows/docs/full-morph.md +1 -1
  59. package/package.json +1 -1
  60. package/src/commands/mcp/mcp-setup.js +21 -0
  61. package/src/commands/project/init.js +11 -0
  62. package/src/lib/contracts/sprint-contract-generator.js +137 -0
  63. package/src/lib/detectors/index.js +2 -2
  64. package/src/lib/generators/architecture-scanner.js +2 -3
  65. package/src/lib/installers/mcp-installer.js +25 -0
  66. package/src/utils/hooks-installer.js +12 -1
  67. package/framework/agents/integrations/clerk-auth.md +0 -109
  68. package/framework/rules/clerk-standards.md +0 -13
  69. package/framework/standards/backend/integrations/clerk/clerk-auth.md +0 -292
  70. package/framework/templates/integrations/clerk-config.cs +0 -217
@@ -0,0 +1,133 @@
1
+ // ==============================================================================
2
+ // MORPH-SPEC - Neon Auth Configuration Template
3
+ // Configuracao de autenticacao com Neon Auth (Better Auth) para .NET
4
+ // ==============================================================================
5
+
6
+ using Microsoft.AspNetCore.Authentication;
7
+ using Microsoft.AspNetCore.Authentication.JwtBearer;
8
+ using Microsoft.IdentityModel.Tokens;
9
+ using System.Security.Claims;
10
+
11
+ namespace {{Namespace}}.Infrastructure.Auth;
12
+
13
+ // ==============================================================================
14
+ // OPTIONS
15
+ // ==============================================================================
16
+
17
+ public class NeonAuthOptions
18
+ {
19
+ public const string SectionName = "NeonAuth";
20
+
21
+ /// <summary>
22
+ /// Neon Auth Base URL (e.g., https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth)
23
+ /// </summary>
24
+ public string BaseUrl { get; set; } = string.Empty;
25
+ }
26
+
27
+ // ==============================================================================
28
+ // SERVICE EXTENSIONS
29
+ // ==============================================================================
30
+
31
+ public static class NeonAuthServiceExtensions
32
+ {
33
+ /// <summary>
34
+ /// Adds Neon Auth JWT Bearer authentication to the project.
35
+ /// Validates JWTs issued by Neon Auth service.
36
+ /// </summary>
37
+ public static IServiceCollection AddNeonAuthentication(
38
+ this IServiceCollection services,
39
+ IConfiguration configuration)
40
+ {
41
+ services.Configure<NeonAuthOptions>(configuration.GetSection(NeonAuthOptions.SectionName));
42
+
43
+ var neonAuthOptions = configuration.GetSection(NeonAuthOptions.SectionName).Get<NeonAuthOptions>()!;
44
+
45
+ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
46
+ .AddJwtBearer(options =>
47
+ {
48
+ options.Authority = neonAuthOptions.BaseUrl;
49
+ options.TokenValidationParameters = new TokenValidationParameters
50
+ {
51
+ ValidateIssuer = true,
52
+ ValidateAudience = false,
53
+ ValidateLifetime = true,
54
+ ValidateIssuerSigningKey = true,
55
+ NameClaimType = "sub"
56
+ };
57
+ });
58
+
59
+ services.AddAuthorization();
60
+
61
+ return services;
62
+ }
63
+ }
64
+
65
+ // ==============================================================================
66
+ // USER CONTEXT SERVICE
67
+ // ==============================================================================
68
+
69
+ public interface ICurrentUserService
70
+ {
71
+ string? UserId { get; }
72
+ string? Email { get; }
73
+ bool IsAuthenticated { get; }
74
+ }
75
+
76
+ public class CurrentUserService(IHttpContextAccessor httpContextAccessor) : ICurrentUserService
77
+ {
78
+ public string? UserId =>
79
+ httpContextAccessor.HttpContext?.User.FindFirstValue("sub");
80
+
81
+ public string? Email =>
82
+ httpContextAccessor.HttpContext?.User.FindFirstValue("email");
83
+
84
+ public bool IsAuthenticated =>
85
+ httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false;
86
+ }
87
+
88
+ // ==============================================================================
89
+ // MIDDLEWARE PIPELINE
90
+ // ==============================================================================
91
+
92
+ public static class NeonAuthMiddlewareExtensions
93
+ {
94
+ public static IApplicationBuilder UseNeonAuthentication(this IApplicationBuilder app)
95
+ {
96
+ app.UseAuthentication();
97
+ app.UseAuthorization();
98
+ return app;
99
+ }
100
+ }
101
+
102
+ // ==============================================================================
103
+ // APPSETTINGS EXAMPLE
104
+ // ==============================================================================
105
+
106
+ /*
107
+ {
108
+ "NeonAuth": {
109
+ "BaseUrl": "https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth"
110
+ }
111
+ }
112
+ */
113
+
114
+ // ==============================================================================
115
+ // PROGRAM.CS EXAMPLE
116
+ // ==============================================================================
117
+
118
+ /*
119
+ var builder = WebApplication.CreateBuilder(args);
120
+
121
+ // Add Neon Auth authentication
122
+ builder.Services.AddNeonAuthentication(builder.Configuration);
123
+ builder.Services.AddHttpContextAccessor();
124
+ builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
125
+
126
+ var app = builder.Build();
127
+
128
+ app.UseNeonAuthentication();
129
+
130
+ app.MapControllers().RequireAuthorization();
131
+
132
+ app.Run();
133
+ */
@@ -63,7 +63,7 @@ proposal.md, design-system.md, mockups.md, components.md, flows.md, spec.md, con
63
63
 
64
64
  ## Exemplos
65
65
  - "Sistema de relatórios com jobs agendados Hangfire"
66
- - "Autenticação completa com Clerk (login, signup, OAuth)"
66
+ - "Autenticação completa com Neon Auth (login, signup, OAuth)"
67
67
  - "Dashboard com gráficos, tabelas e filtros complexos"
68
68
  - "RAG pipeline com AI Search e embeddings"
69
69
  - "Infraestrutura completa com Docker/Easypanel"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "4.17.0",
3
+ "version": "4.19.0",
4
4
  "description": "MORPH-SPEC: AI-First development framework with validation pipeline and multi-stack support",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -16,6 +16,7 @@ import ora from 'ora';
16
16
  import {
17
17
  orchestrateMcpSetup,
18
18
  installAutoMcps,
19
+ installCompanionSkills,
19
20
  installMcpWithCredentials,
20
21
  generateSetupInstructions,
21
22
  formatMcpStatusTable,
@@ -120,6 +121,16 @@ async function autoInstall(targetPath, stack, existingMcps) {
120
121
  console.log(chalk.gray(` Skipped (already configured): ${result.skipped.join(', ')}`));
121
122
  }
122
123
 
124
+ // Install companion skills for MCPs that define them
125
+ const skillResults = installCompanionSkills(orchestration.autoInstallable);
126
+ for (const r of skillResults) {
127
+ if (r.success) {
128
+ console.log(chalk.green(` ✓ Companion skills installed: ${r.repo}`));
129
+ } else {
130
+ console.log(chalk.yellow(` ⚠ Failed to install companion skills: ${r.repo}`));
131
+ }
132
+ }
133
+
123
134
  console.log('');
124
135
  }
125
136
 
@@ -305,6 +316,16 @@ async function interactiveWizard(targetPath, stack, existingMcps) {
305
316
  const spinner = ora('Installing...').start();
306
317
  const result = await installAutoMcps(targetPath, orchestration.autoInstallable);
307
318
  spinner.succeed(`Installed: ${result.added.join(', ')}`);
319
+
320
+ // Install companion skills for MCPs that define them
321
+ const skillResults = installCompanionSkills(orchestration.autoInstallable);
322
+ for (const r of skillResults) {
323
+ if (r.success) {
324
+ console.log(chalk.green(` ✓ Companion skills: ${r.repo}`));
325
+ } else {
326
+ console.log(chalk.yellow(` ⚠ Companion skills failed: ${r.repo}`));
327
+ }
328
+ }
308
329
  }
309
330
  }
310
331
 
@@ -20,6 +20,7 @@ import { generateSettings, saveIntegrations } from '../../lib/generators/setting
20
20
  import {
21
21
  orchestrateMcpSetup,
22
22
  installAutoMcps,
23
+ installCompanionSkills,
23
24
  installMcpWithCredentials,
24
25
  generateSetupInstructions,
25
26
  formatMcpStatusTable,
@@ -256,6 +257,16 @@ export async function initCommand(options) {
256
257
  for (const name of result.added) {
257
258
  autoInstalled[name] = orchestration.autoInstallable[name];
258
259
  }
260
+
261
+ // Install companion skills for MCPs that define them (e.g., Stitch agent skills)
262
+ const skillResults = installCompanionSkills(orchestration.autoInstallable);
263
+ for (const r of skillResults) {
264
+ if (r.success) {
265
+ logger.info(`✓ Companion skills installed: ${r.repo}`);
266
+ } else {
267
+ logger.warn(`Failed to install companion skills: ${r.repo}`);
268
+ }
269
+ }
259
270
  }
260
271
  }
261
272
 
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Sprint Contract Generator
3
+ *
4
+ * Generates sprint contracts from tasks.md + spec.md for the evaluator.
5
+ * Contracts define objective "definition of done" criteria.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { getBuildCommand } from '../../../framework/hooks/shared/build-check.js';
11
+
12
+ /**
13
+ * Parse a task entry from tasks.md markdown.
14
+ *
15
+ * Expected format: ## T001: Task Title [FR001, FR003]
16
+ *
17
+ * @param {string} content - tasks.md content
18
+ * @param {string} taskId - Task ID (e.g., 'T001')
19
+ * @returns {{ id: string, title: string, frs: string[], description: string }|null}
20
+ */
21
+ export function parseTaskFromMarkdown(content, taskId) {
22
+ const pattern = new RegExp(
23
+ `##\\s+${taskId}:\\s+(.+?)(?:\\s+\\[([^\\]]+)\\])?\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`,
24
+ 'i'
25
+ );
26
+ const match = content.match(pattern);
27
+ if (!match) return null;
28
+
29
+ const title = match[1].trim();
30
+ const frString = match[2] || '';
31
+ const description = match[3].trim();
32
+
33
+ const frs = frString
34
+ ? frString.split(',').map(fr => fr.trim()).filter(Boolean)
35
+ : [];
36
+
37
+ return { id: taskId, title, frs, description };
38
+ }
39
+
40
+ /**
41
+ * Generate a sprint contract for a task.
42
+ *
43
+ * @param {string} projectPath - Project root
44
+ * @param {string} featureName - Feature name
45
+ * @param {string} taskId - Task ID
46
+ * @returns {Object|null} Contract object or null if unable to generate
47
+ */
48
+ export function generateSprintContract(projectPath, featureName, taskId) {
49
+ const featureDir = join(projectPath, '.morph/features', featureName);
50
+ const tasksPath = join(featureDir, '4-tasks/tasks.md');
51
+
52
+ if (!existsSync(tasksPath)) return null;
53
+
54
+ const tasksContent = readFileSync(tasksPath, 'utf-8');
55
+ const task = parseTaskFromMarkdown(tasksContent, taskId);
56
+ if (!task) return null;
57
+
58
+ // Read spec for FR-based criteria
59
+ const specPath = join(featureDir, '2-design/spec.md');
60
+ let definitionOfDone = [];
61
+ if (task.frs.length > 0 && existsSync(specPath)) {
62
+ const specContent = readFileSync(specPath, 'utf-8');
63
+ for (const fr of task.frs) {
64
+ const frPattern = new RegExp(`##\\s+${fr}[:\\s]([\\s\\S]*?)(?=\\n##\\s|$)`, 'i');
65
+ const frMatch = specContent.match(frPattern);
66
+ if (frMatch) {
67
+ const bullets = frMatch[1].match(/^[\s]*[-*]\s+(.+)/gm);
68
+ if (bullets) {
69
+ definitionOfDone.push(...bullets.map(b => b.replace(/^[\s]*[-*]\s+/, '').trim()));
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ if (definitionOfDone.length === 0) {
76
+ definitionOfDone = [task.description || `Complete ${task.title}`];
77
+ }
78
+
79
+ // Read stack for build commands
80
+ let stack = '';
81
+ try {
82
+ const configPath = join(projectPath, '.morph/config/config.json');
83
+ if (existsSync(configPath)) {
84
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
85
+ stack = config?.project?.stack || '';
86
+ }
87
+ } catch { /* ignore */ }
88
+
89
+ const hasTsConfig = existsSync(join(projectPath, 'tsconfig.json'));
90
+ const buildCmd = getBuildCommand(stack, hasTsConfig);
91
+
92
+ const contract = {
93
+ task: taskId,
94
+ title: task.title,
95
+ functionalRequirements: task.frs,
96
+ definitionOfDone,
97
+ verification: {
98
+ build: buildCmd || 'echo "No build command configured"',
99
+ test: stack.includes('dotnet') ? `dotnet test --filter ${task.title.replace(/\s+/g, '')}` : 'npm test',
100
+ e2e: null
101
+ },
102
+ maxAttempts: 3,
103
+ generatedAt: new Date().toISOString()
104
+ };
105
+
106
+ // Write contract to disk
107
+ const contractsDir = join(featureDir, '5-implement/contracts');
108
+ mkdirSync(contractsDir, { recursive: true });
109
+ const yamlContent = contractToYaml(contract);
110
+ writeFileSync(join(contractsDir, `${taskId}-contract.yaml`), yamlContent, 'utf-8');
111
+
112
+ return contract;
113
+ }
114
+
115
+ /**
116
+ * Simple YAML serializer for contracts (no dependency needed).
117
+ *
118
+ * @param {Object} contract - Contract object
119
+ * @returns {string} YAML string
120
+ */
121
+ function contractToYaml(contract) {
122
+ const lines = [
123
+ `task: ${contract.task}`,
124
+ `title: "${contract.title}"`,
125
+ `functionalRequirements:`,
126
+ ...contract.functionalRequirements.map(fr => ` - ${fr}`),
127
+ `definitionOfDone:`,
128
+ ...contract.definitionOfDone.map(d => ` - "${d}"`),
129
+ `verification:`,
130
+ ` build: "${contract.verification.build}"`,
131
+ ` test: "${contract.verification.test}"`,
132
+ ` e2e: ${contract.verification.e2e ? `"${contract.verification.e2e}"` : 'null'}`,
133
+ `maxAttempts: ${contract.maxAttempts}`,
134
+ `generatedAt: "${contract.generatedAt}"`,
135
+ ];
136
+ return lines.join('\n') + '\n';
137
+ }
@@ -74,8 +74,8 @@ export function detectProject(targetPath, opts = {}) {
74
74
  if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
75
75
  structure.integrations.push('neon');
76
76
  }
77
- if (depNames.includes('@clerk/nextjs') || depNames.includes('@clerk/clerk-sdk-node')) {
78
- structure.integrations.push('clerk');
77
+ if (depNames.includes('@neondatabase/auth')) {
78
+ structure.integrations.push('neon-auth');
79
79
  }
80
80
  if (depNames.includes('stripe')) {
81
81
  structure.integrations.push('stripe');
@@ -223,7 +223,7 @@ export async function scanIntegrations(projectPath) {
223
223
  );
224
224
 
225
225
  const knownServices = {
226
- 'Clerk': 'auth',
226
+ 'NeonAuth': 'auth',
227
227
  'AsaaS': 'payment',
228
228
  'Resend': 'email',
229
229
  'Hangfire': 'background-jobs',
@@ -266,8 +266,7 @@ export async function scanIntegrations(projectPath) {
266
266
 
267
267
  // Scan .env files for known prefixes
268
268
  const envPrefixes = {
269
- 'NEXT_PUBLIC_CLERK': 'Clerk',
270
- 'CLERK_': 'Clerk',
269
+ 'NEON_AUTH_': 'NeonAuth',
271
270
  'DATABASE_URL': 'Neon',
272
271
  'NEON_': 'Neon',
273
272
  'RESEND_': 'Resend',
@@ -148,6 +148,31 @@ export async function installAutoMcps(targetPath, mcpsToInstall) {
148
148
  return installMcpServers(targetPath, servers);
149
149
  }
150
150
 
151
+ /**
152
+ * Install companion skills for MCPs that define them (e.g., Stitch agent skills).
153
+ * Reads the `companionSkills` field from registry entries and runs the install command.
154
+ * @param {Object} mcpsInstalled - Map of mcpName → mcpEntry that were just installed
155
+ * @param {Function} exec - execSync function (for testing)
156
+ * @returns {Array<{name: string, repo: string, success: boolean}>}
157
+ */
158
+ export function installCompanionSkills(mcpsInstalled, exec = execSync) {
159
+ const results = [];
160
+
161
+ for (const [name, entry] of Object.entries(mcpsInstalled)) {
162
+ const companion = entry.companionSkills;
163
+ if (!companion?.installCommand) continue;
164
+
165
+ try {
166
+ exec(companion.installCommand, { stdio: 'inherit', timeout: 30000 });
167
+ results.push({ name, repo: companion.repo, success: true });
168
+ } catch {
169
+ results.push({ name, repo: companion.repo, success: false });
170
+ }
171
+ }
172
+
173
+ return results;
174
+ }
175
+
151
176
  /**
152
177
  * Install a single MCP with provided credentials
153
178
  * @param {string} targetPath - Project root directory
@@ -15,7 +15,7 @@ import { homedir } from 'os';
15
15
  import { execSync } from 'child_process';
16
16
 
17
17
  /** Current hooks schema version — bump when hook definitions change */
18
- const HOOKS_VERSION = '2.14.0';
18
+ const HOOKS_VERSION = '3.0.0';
19
19
 
20
20
  /** Marker for old dispatch.js (v1) */
21
21
  const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
@@ -155,6 +155,17 @@ Otherwise respond: {"ok": true}`
155
155
  ]
156
156
  },
157
157
 
158
+ // === PostToolUse: All — loop detection ===
159
+ {
160
+ event: 'PostToolUse',
161
+ matcher: null,
162
+ hooks: [{
163
+ type: 'command',
164
+ command: 'node framework/hooks/claude-code/post-tool-use/loop-detect.js',
165
+ timeout: 5000
166
+ }]
167
+ },
168
+
158
169
  // === Stop ===
159
170
  {
160
171
  event: 'Stop',
@@ -1,109 +0,0 @@
1
- ---
2
- name: clerk-auth
3
- description: Clerk Auth specialist for Clerk SDK integration, social login, JWT session management, and user management in Next.js and .NET projects. Use when integrating Clerk authentication, implementing social login flows, managing user sessions with Clerk JWT, or protecting routes with Clerk middleware.
4
- allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
- stacks:
6
- - clerk
7
- color: teal
8
- effort: medium
9
- ---
10
-
11
- # Clerk Auth
12
-
13
- Autenticação SaaS com Clerk para .NET e Next.js. SDK: `Clerk.Net.AspNetCore.Security` (.NET) / `@clerk/nextjs` (Next.js).
14
-
15
- ## Setup
16
-
17
- ```csharp
18
- // appsettings.json
19
- { "Clerk": { "SecretKey": "${CLERK_SECRET_KEY}", "PublishableKey": "pk_test_xxx" } }
20
-
21
- // Program.cs
22
- builder.Services.AddClerk(builder.Configuration);
23
- builder.Services.AddAuthentication(ClerkAuthenticationDefaults.AuthenticationScheme)
24
- .AddClerk(o => { o.Authority = "https://clerk.{instance}.com"; o.ValidAudiences = ["your-app-id"]; });
25
- builder.Services.AddAuthorization();
26
- app.UseAuthentication();
27
- app.UseAuthorization();
28
- ```
29
-
30
- ## Protected Endpoints
31
-
32
- ```csharp
33
- // Minimal API
34
- app.MapGet("/api/profile", async (ClaimsPrincipal user, IClerkClient clerk) =>
35
- {
36
- var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
37
- if (userId is null) return Results.Unauthorized();
38
- var u = await clerk.Users.GetUserAsync(userId);
39
- return Results.Ok(new { u.Id, Email = u.EmailAddresses.FirstOrDefault()?.EmailAddress });
40
- }).RequireAuthorization();
41
-
42
- // Controller: same pattern with [Authorize] + User.FindFirstValue()
43
- // Role-based: [Authorize(Roles = "admin")]
44
- ```
45
-
46
- ## Next.js Integration
47
-
48
- ```tsx
49
- // middleware.ts
50
- import { clerkMiddleware } from '@clerk/nextjs/server';
51
- export default clerkMiddleware();
52
- export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/'] };
53
-
54
- // Protected page
55
- import { auth } from '@clerk/nextjs/server';
56
- export default async function DashboardPage() {
57
- const { userId } = await auth();
58
- if (!userId) redirect('/sign-in');
59
- return <Dashboard />;
60
- }
61
-
62
- // Client component
63
- 'use client';
64
- import { useUser } from '@clerk/nextjs';
65
- const { user, isLoaded } = useUser();
66
- ```
67
-
68
- ## Client Operations (.NET)
69
-
70
- ```csharp
71
- var user = await _clerk.Users.GetUserAsync(userId); // Get
72
- var users = await _clerk.Users.GetUserListAsync(new() { Limit = 10 }); // List
73
- await _clerk.Users.UpdateUserMetadataAsync(userId, new() // Update metadata
74
- { PublicMetadata = new Dictionary<string, object> { ["plan"] = "pro" } });
75
- await _clerk.Users.DeleteUserAsync(userId); // Delete
76
- ```
77
-
78
- ## Webhooks
79
-
80
- ```csharp
81
- [ApiController, Route("api/webhooks/clerk")]
82
- public class ClerkWebhookController(IUserService users) : ControllerBase
83
- {
84
- [HttpPost]
85
- public async Task<IActionResult> Handle([FromBody] ClerkWebhookPayload payload)
86
- {
87
- switch (payload.Type)
88
- {
89
- case "user.created": await users.SyncUserAsync(payload.Data); break;
90
- case "user.updated": await users.UpdateUserAsync(payload.Data); break;
91
- case "user.deleted": await users.DeleteUserAsync(payload.Data.Id); break;
92
- }
93
- return Ok();
94
- }
95
- }
96
- ```
97
-
98
- ## Checklist
99
-
100
- - [ ] Secret Key in environment variables (not hardcoded)
101
- - [ ] Authentication scheme configured
102
- - [ ] Authorization policies defined
103
- - [ ] Webhook endpoint + signature validation
104
- - [ ] Redirect after login configured
105
- - [ ] Error handling for expired sessions
106
-
107
- ---
108
-
109
- *MORPH-SPEC by Polymorphism Tech*
@@ -1,13 +0,0 @@
1
- ---
2
- paths:
3
- - "**/Auth/**"
4
- - "**/auth/**"
5
- - "**/Identity/**"
6
- - "**/Clerk/**"
7
- stacks:
8
- - dotnet
9
- ---
10
-
11
- # Clerk Authentication Standards
12
-
13
- @.morph/framework/standards/backend/integrations/clerk/clerk-auth.md