diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java index e7040c38f..8148ce90a 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java @@ -24,6 +24,7 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.OAuth2ProtectedResourceMetadata; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; @@ -201,40 +202,9 @@ public class McpSecurityConfig { .protectedResourceMetadata( prm -> prm.protectedResourceMetadataCustomizer( - builder -> { - if (!auth.getResourceId() - .isBlank()) { - builder.resource( - auth - .getResourceId()); - } - if (!auth.getIssuerUri() - .isBlank()) { - builder.authorizationServer( - auth - .getIssuerUri()); - } - // Only advertise the granular - // tool scopes when we actually - // enforce them. When scopes are - // disabled (e.g. the IdP only - // mints coarse tokens, like - // Supabase), advertising scopes - // the authorization server - // can't - // issue makes spec-compliant - // clients request them and get - // rejected with - // invalid_request. - if (applicationProperties - .getMcp() - .isScopesEnabled()) { - builder.scope( - "mcp.tools.read"); - builder.scope( - "mcp.tools.write"); - } - })) + builder -> + buildResourceMetadata( + builder, auth))) .jwt( jwt -> jwt.decoder(mcpJwtDecoder) @@ -243,6 +213,25 @@ public class McpSecurityConfig { return http.build(); } + /** Populate the RFC 9728 protected-resource metadata document from the configured auth. */ + private void buildResourceMetadata( + OAuth2ProtectedResourceMetadata.Builder builder, ApplicationProperties.Mcp.Auth auth) { + if (!auth.getResourceId().isBlank()) { + builder.resource(auth.getResourceId()); + } + if (!auth.getIssuerUri().isBlank()) { + builder.authorizationServer(auth.getIssuerUri()); + } + // Only advertise the granular tool scopes when we actually enforce them. When scopes are + // disabled (e.g. the IdP only mints coarse tokens, like Supabase), advertising scopes the + // authorization server can't issue makes spec-compliant clients request them and get + // rejected with invalid_request. + if (applicationProperties.getMcp().isScopesEnabled()) { + builder.scope("mcp.tools.read"); + builder.scope("mcp.tools.write"); + } + } + @Bean JwtDecoder mcpJwtDecoder() { ApplicationProperties.Mcp.Auth auth = applicationProperties.getMcp().getAuth(); diff --git a/app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java b/app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java index ee2de60c1..041eaa8f4 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java @@ -303,9 +303,9 @@ public class PaygWalletController { } else { long capMinor = CapMoneyUnits.usdToCents(req.capUsd()); policy.setCapSourceMoney(capMinor); - // Derived document allowance (design §10: store both the money intent and the unit - // translation). The live snapshot recomputes from cap_source_money + current rate; - // this stored value is the enforcement fallback when the rate is unreachable. + // Derived document allowance: store both the money intent and the unit translation. + // The live snapshot recomputes from cap_source_money + current rate; this stored value + // is the enforcement fallback when the rate is unreachable. TeamBillingContext billing = billingService.forTeam(teamId); Optional docCap = billingService.docCapForMoney(billing, capMinor); if (docCap.isPresent()) { diff --git a/app/saas/src/main/java/stirling/software/saas/payg/api/WalletSnapshotResponse.java b/app/saas/src/main/java/stirling/software/saas/payg/api/WalletSnapshotResponse.java index 3a70cecb8..0b29347c8 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/api/WalletSnapshotResponse.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/api/WalletSnapshotResponse.java @@ -82,8 +82,7 @@ public record WalletSnapshotResponse( /** * One row of the team-members table on the leader's Plan page — display-only per-member usage. - * (Per-member sub-caps aren't enforced yet; see the follow-ups note. When they ship, a cap - * field returns here.) + * (Per-member sub-caps aren't enforced yet. When they ship, a cap field returns here.) */ public record MemberRow(String userId, String name, String email, int spendUnits) {} diff --git a/app/saas/src/main/java/stirling/software/saas/payg/cap/CapEvaluator.java b/app/saas/src/main/java/stirling/software/saas/payg/cap/CapEvaluator.java index 43819f3a3..d47b6e736 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/cap/CapEvaluator.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/cap/CapEvaluator.java @@ -12,7 +12,7 @@ import stirling.software.saas.payg.model.FeatureSet; * effect, and the corresponding enabled {@link FeatureGate}s. No DB access, no caches — the caller * (entitlement service) supplies the inputs. * - *

State transitions (matching {@code notes/PAYG_DESIGN.md} §3.6): + *

State transitions: * *