@polymorphism-tech/morph-spec 7.9.1 → 7.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -5
- package/docs/QUICKSTART.md +4 -4
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +2 -2
- package/framework/agents.json +4 -2
- package/framework/evals/rubrics/architecture.md +27 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +13 -8
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +13 -8
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +19 -7
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +14 -3
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +350 -19
- package/framework/standards/frontend/nextjs/app-router.md +6 -1
- package/framework/standards/frontend/nextjs/data-fetching.md +17 -3
- package/framework/templates/REGISTRY.json +2 -2
- package/framework/templates/integrations/neon-auth-config.cs +144 -11
- package/package.json +1 -1
- package/src/commands/project/doctor.js +1 -1
- package/src/utils/version-checker.js +2 -2
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
// ==============================================================================
|
|
2
2
|
// MORPH-SPEC - Neon Auth Configuration Template
|
|
3
3
|
// Configuracao de autenticacao com Neon Auth (Better Auth) para .NET
|
|
4
|
+
//
|
|
5
|
+
// Neon Auth tokens are signed with EdDSA (Ed25519, JWK kty "OKP"). There is no
|
|
6
|
+
// OIDC discovery document (/.well-known/openid-configuration) — only the raw
|
|
7
|
+
// /.well-known/jwks.json — and Microsoft.IdentityModel.Tokens cannot verify
|
|
8
|
+
// OKP/EdDSA signatures on its own. See:
|
|
9
|
+
// .morph/framework/standards/backend/integrations/neon-auth/neon-auth.md
|
|
10
|
+
// → "## .NET Backend Integration" for the full reference and rationale.
|
|
11
|
+
//
|
|
12
|
+
// NuGet (add both):
|
|
13
|
+
// <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
|
|
14
|
+
// <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" /> <!-- pin >= 2.3.1, CVE-2024-30172 -->
|
|
4
15
|
// ==============================================================================
|
|
5
16
|
|
|
17
|
+
using System.Security.Claims;
|
|
18
|
+
using System.Text;
|
|
6
19
|
using Microsoft.AspNetCore.Authentication;
|
|
7
20
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
21
|
+
using Microsoft.Extensions.Options;
|
|
22
|
+
using Microsoft.IdentityModel.JsonWebTokens;
|
|
8
23
|
using Microsoft.IdentityModel.Tokens;
|
|
9
|
-
using
|
|
24
|
+
using Org.BouncyCastle.Crypto.Parameters;
|
|
25
|
+
using Org.BouncyCastle.Crypto.Signers;
|
|
10
26
|
|
|
11
27
|
namespace {{Namespace}}.Infrastructure.Auth;
|
|
12
28
|
|
|
@@ -19,11 +35,65 @@ public class NeonAuthOptions
|
|
|
19
35
|
public const string SectionName = "NeonAuth";
|
|
20
36
|
|
|
21
37
|
/// <summary>
|
|
22
|
-
/// Neon Auth Base URL (e.g., https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth)
|
|
38
|
+
/// Neon Auth Base URL (e.g., https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth).
|
|
39
|
+
/// MUST be present in appsettings.Development.json — it's a public value, not a secret.
|
|
40
|
+
/// Missing it produces the same symptom as a broken signature validator: every request
|
|
41
|
+
/// gets a bare 401 with nothing useful in the logs.
|
|
23
42
|
/// </summary>
|
|
24
43
|
public string BaseUrl { get; set; } = string.Empty;
|
|
25
44
|
}
|
|
26
45
|
|
|
46
|
+
// ==============================================================================
|
|
47
|
+
// JWKS PROVIDER (cached — Neon Auth has no OIDC discovery to piggyback on)
|
|
48
|
+
// ==============================================================================
|
|
49
|
+
|
|
50
|
+
/// <summary>
|
|
51
|
+
/// Fetches and caches Neon Auth's raw JWKS. Cached for 15 minutes; a forced refresh
|
|
52
|
+
/// happens once when an unknown `kid` is seen, to survive key rotation without
|
|
53
|
+
/// waiting out the TTL.
|
|
54
|
+
/// </summary>
|
|
55
|
+
public sealed class NeonAuthJwksProvider(HttpClient httpClient, IOptions<NeonAuthOptions> options)
|
|
56
|
+
{
|
|
57
|
+
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(15);
|
|
58
|
+
private readonly SemaphoreSlim _refreshLock = new(1, 1);
|
|
59
|
+
private JsonWebKeySet? _cached;
|
|
60
|
+
private DateTimeOffset _cachedAt;
|
|
61
|
+
|
|
62
|
+
public async Task<JsonWebKey?> GetKeyAsync(string kid, CancellationToken cancellationToken)
|
|
63
|
+
{
|
|
64
|
+
var keySet = await GetKeySetAsync(forceRefresh: false, cancellationToken);
|
|
65
|
+
var key = keySet.Keys.FirstOrDefault(k => k.Kid == kid);
|
|
66
|
+
if (key is not null) return key;
|
|
67
|
+
|
|
68
|
+
// Unknown kid — could be key rotation. Force a single refresh before giving up.
|
|
69
|
+
keySet = await GetKeySetAsync(forceRefresh: true, cancellationToken);
|
|
70
|
+
return keySet.Keys.FirstOrDefault(k => k.Kid == kid);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async Task<JsonWebKeySet> GetKeySetAsync(bool forceRefresh, CancellationToken cancellationToken)
|
|
74
|
+
{
|
|
75
|
+
if (!forceRefresh && _cached is not null && DateTimeOffset.UtcNow - _cachedAt < CacheTtl)
|
|
76
|
+
return _cached;
|
|
77
|
+
|
|
78
|
+
await _refreshLock.WaitAsync(cancellationToken);
|
|
79
|
+
try
|
|
80
|
+
{
|
|
81
|
+
if (!forceRefresh && _cached is not null && DateTimeOffset.UtcNow - _cachedAt < CacheTtl)
|
|
82
|
+
return _cached;
|
|
83
|
+
|
|
84
|
+
var jwksUri = $"{options.Value.BaseUrl.TrimEnd('/')}/.well-known/jwks.json";
|
|
85
|
+
var json = await httpClient.GetStringAsync(jwksUri, cancellationToken);
|
|
86
|
+
_cached = new JsonWebKeySet(json);
|
|
87
|
+
_cachedAt = DateTimeOffset.UtcNow;
|
|
88
|
+
return _cached;
|
|
89
|
+
}
|
|
90
|
+
finally
|
|
91
|
+
{
|
|
92
|
+
_refreshLock.Release();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
27
97
|
// ==============================================================================
|
|
28
98
|
// SERVICE EXTENSIONS
|
|
29
99
|
// ==============================================================================
|
|
@@ -31,28 +101,91 @@ public class NeonAuthOptions
|
|
|
31
101
|
public static class NeonAuthServiceExtensions
|
|
32
102
|
{
|
|
33
103
|
/// <summary>
|
|
34
|
-
/// Adds Neon Auth JWT Bearer authentication to the project.
|
|
35
|
-
///
|
|
104
|
+
/// Adds Neon Auth JWT Bearer authentication to the project. Validates EdDSA-signed
|
|
105
|
+
/// JWTs issued by Neon Auth via a manually-fetched, cached JWKS — see
|
|
106
|
+
/// NeonAuthJwksProvider above and the standard's "## .NET Backend Integration" section
|
|
107
|
+
/// for why options.Authority / the default IssuerSigningKeyResolver do not work here.
|
|
36
108
|
/// </summary>
|
|
37
109
|
public static IServiceCollection AddNeonAuthentication(
|
|
38
110
|
this IServiceCollection services,
|
|
39
111
|
IConfiguration configuration)
|
|
40
112
|
{
|
|
41
113
|
services.Configure<NeonAuthOptions>(configuration.GetSection(NeonAuthOptions.SectionName));
|
|
42
|
-
|
|
43
|
-
var neonAuthOptions = configuration.GetSection(NeonAuthOptions.SectionName).Get<NeonAuthOptions>()!;
|
|
114
|
+
services.AddHttpClient<NeonAuthJwksProvider>();
|
|
44
115
|
|
|
45
116
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
46
117
|
.AddJwtBearer(options =>
|
|
47
118
|
{
|
|
48
|
-
options.Authority
|
|
119
|
+
// Do NOT set options.Authority — Neon Auth serves no
|
|
120
|
+
// /.well-known/openid-configuration, so Authority-driven metadata refresh
|
|
121
|
+
// has nothing valid to fetch. ValidIssuer is set explicitly instead.
|
|
49
122
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
50
123
|
{
|
|
51
124
|
ValidateIssuer = true,
|
|
125
|
+
ValidIssuer = configuration["NeonAuth:BaseUrl"],
|
|
52
126
|
ValidateAudience = false,
|
|
53
127
|
ValidateLifetime = true,
|
|
54
|
-
|
|
55
|
-
|
|
128
|
+
NameClaimType = "sub",
|
|
129
|
+
// Trust in the signing key is established entirely inside
|
|
130
|
+
// SignatureValidator below (fetched from our own cached JWKS) —
|
|
131
|
+
// this flag doesn't gate that path.
|
|
132
|
+
ValidateIssuerSigningKey = false,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// A second Configure<TDep> pass with DI available — AddJwtBearer's own setup has
|
|
137
|
+
// already run, so this only *adds* the SignatureValidator to the
|
|
138
|
+
// TokenValidationParameters instance built above.
|
|
139
|
+
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
|
|
140
|
+
.Configure<NeonAuthJwksProvider>((options, jwks) =>
|
|
141
|
+
{
|
|
142
|
+
options.TokenValidationParameters.SignatureValidator = (token, _) =>
|
|
143
|
+
{
|
|
144
|
+
var jwt = new JsonWebToken(token);
|
|
145
|
+
|
|
146
|
+
// Pin the algorithm — refuse anything that isn't EdDSA to block
|
|
147
|
+
// alg-substitution attacks.
|
|
148
|
+
if (!string.Equals(jwt.Alg, "EdDSA", StringComparison.Ordinal))
|
|
149
|
+
throw new SecurityTokenInvalidSignatureException(
|
|
150
|
+
$"Unsupported alg '{jwt.Alg}' — Neon Auth tokens must be EdDSA.");
|
|
151
|
+
|
|
152
|
+
var kid = jwt.Kid;
|
|
153
|
+
if (string.IsNullOrEmpty(kid))
|
|
154
|
+
throw new SecurityTokenInvalidSignatureException("Token is missing 'kid'.");
|
|
155
|
+
|
|
156
|
+
// Blocking call: JWKS is cached in-memory, so this is I/O-free on the hot
|
|
157
|
+
// path. SignatureValidator has no async overload — only a cold-start /
|
|
158
|
+
// key-rotation miss ever awaits real I/O here.
|
|
159
|
+
var jwk = jwks.GetKeyAsync(kid, CancellationToken.None).GetAwaiter().GetResult()
|
|
160
|
+
?? throw new SecurityTokenInvalidSignatureException(
|
|
161
|
+
$"No JWKS key found for kid '{kid}'.");
|
|
162
|
+
|
|
163
|
+
if (jwk.Kty != "OKP" || jwk.Crv != "Ed25519")
|
|
164
|
+
throw new SecurityTokenInvalidSignatureException(
|
|
165
|
+
"JWKS key is not an Ed25519 (OKP) key.");
|
|
166
|
+
|
|
167
|
+
var parts = token.Split('.');
|
|
168
|
+
if (parts.Length != 3)
|
|
169
|
+
throw new SecurityTokenInvalidSignatureException(
|
|
170
|
+
"Malformed JWT — expected 3 segments.");
|
|
171
|
+
|
|
172
|
+
var signingInput = Encoding.ASCII.GetBytes($"{parts[0]}.{parts[1]}");
|
|
173
|
+
var signature = Base64UrlEncoder.DecodeBytes(parts[2]);
|
|
174
|
+
var publicKey = new Ed25519PublicKeyParameters(Base64UrlEncoder.DecodeBytes(jwk.X), 0);
|
|
175
|
+
|
|
176
|
+
var verifier = new Ed25519Signer();
|
|
177
|
+
verifier.Init(forSigning: false, publicKey);
|
|
178
|
+
verifier.BlockUpdate(signingInput, 0, signingInput.Length);
|
|
179
|
+
|
|
180
|
+
if (!verifier.VerifySignature(signature))
|
|
181
|
+
throw new SecurityTokenInvalidSignatureException(
|
|
182
|
+
"Ed25519 signature verification failed.");
|
|
183
|
+
|
|
184
|
+
// MUST return Microsoft.IdentityModel.JsonWebTokens.JsonWebToken — ASP.NET
|
|
185
|
+
// Core 8+ uses JsonWebTokenHandler internally. Returning a
|
|
186
|
+
// System.IdentityModel.Tokens.Jwt.JwtSecurityToken here fails with
|
|
187
|
+
// IDX10506 ("SignatureValidator returned a token of an unexpected type").
|
|
188
|
+
return jwt;
|
|
56
189
|
};
|
|
57
190
|
});
|
|
58
191
|
|
|
@@ -100,7 +233,7 @@ public static class NeonAuthMiddlewareExtensions
|
|
|
100
233
|
}
|
|
101
234
|
|
|
102
235
|
// ==============================================================================
|
|
103
|
-
// APPSETTINGS
|
|
236
|
+
// APPSETTINGS.DEVELOPMENT.JSON — mandatory, public value, safe to commit
|
|
104
237
|
// ==============================================================================
|
|
105
238
|
|
|
106
239
|
/*
|
|
@@ -118,7 +251,7 @@ public static class NeonAuthMiddlewareExtensions
|
|
|
118
251
|
/*
|
|
119
252
|
var builder = WebApplication.CreateBuilder(args);
|
|
120
253
|
|
|
121
|
-
// Add Neon Auth authentication
|
|
254
|
+
// Add Neon Auth authentication (registers the JWKS provider + EdDSA signature validator)
|
|
122
255
|
builder.Services.AddNeonAuthentication(builder.Configuration);
|
|
123
256
|
builder.Services.AddHttpContextAccessor();
|
|
124
257
|
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
|
package/package.json
CHANGED
|
@@ -974,7 +974,7 @@ export async function doctorCommand(options = {}) {
|
|
|
974
974
|
|
|
975
975
|
if (cliCheck.isOutdated || projectCheck.isOutdated) {
|
|
976
976
|
logger.info('To update:');
|
|
977
|
-
logger.dim(' 1. Update the CLI: npm install -g @polymorphism-tech/morph-spec@latest');
|
|
977
|
+
logger.dim(' 1. Update the CLI: npm install -g @polymorphism-tech/morph-spec@latest --prefer-online');
|
|
978
978
|
logger.dim(' 2. Update the project: morph-spec update');
|
|
979
979
|
logger.blank();
|
|
980
980
|
}
|
|
@@ -121,13 +121,13 @@ export function getUpdateInstructions(method) {
|
|
|
121
121
|
switch (method) {
|
|
122
122
|
case 'global':
|
|
123
123
|
return [
|
|
124
|
-
'npm install -g @polymorphism-tech/morph-spec@latest',
|
|
124
|
+
'npm install -g @polymorphism-tech/morph-spec@latest --prefer-online',
|
|
125
125
|
'morph-spec update'
|
|
126
126
|
];
|
|
127
127
|
case 'npx':
|
|
128
128
|
default:
|
|
129
129
|
return [
|
|
130
|
-
'npm install -g @polymorphism-tech/morph-spec@latest',
|
|
130
|
+
'npm install -g @polymorphism-tech/morph-spec@latest --prefer-online',
|
|
131
131
|
'morph-spec update'
|
|
132
132
|
];
|
|
133
133
|
}
|