Repository navigation
v0.27.0: production hardening and billing on payforge - #71
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fosite reads a code or refresh token before it writes, and this storage does not run the two in one transaction, so two concurrent requests with the same code or refresh token could both read it as valid and both receive tokens. Invalidating a code now only matches an unused code, and rotating a refresh token first retires the presented token only while it is active. The request that loses gets invalid_grant or invalid_request and no tokens. A later reuse of a rotated refresh token still revokes the family through the read path. The memory store also returned no request for a used code, so a replayed code answered server_error and left the tokens it had issued active; it now returns the request and Fosite revokes them. Refs: RMI-SYSTEMFORGE-200 Refs: RMI-SYSTEMFORGE-201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he address A new GitHub or Google identity was linked to any existing account whose verified email matched the upstream one. A provider can keep an address marked verified after the person loses the mailbox, such as a former work address, so whoever held that stale verification could sign in to the account of the person who now owns the address. An identity now links by email only when the login is authoritative for the address: Google for gmail.com and googlemail.com, and for a Workspace account's own domain (the hd claim, which the Google connector now reads). Other verified emails create an account as before, and an email that already belongs to an account is refused with ErrEmailConflict; the person signs in the way they signed in before. GitHub sign-in uses the primary email only, never another verified address on the account. Refs: RMI-SYSTEMFORGE-202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Report vulnerabilities through GitHub private vulnerability reporting, with response targets, the supported-versions policy and how fixes are disclosed. Link it from the README and the documentation site. Refs: RMI-SYSTEMFORGE-210 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A checklist an operator walks before an application that embeds SystemAuth goes live: keys and secrets, the fail-closed production checks, shared stores, the edge and origin, logging, the database, billing, monitoring and response, and branch protection. Link it from the security policy and the documentation site. Refs: RMI-SYSTEMFORGE-211 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… policy production.allow names the production checks a deployment waives (ephemeral_hmac_secret, client_secret_client_credentials, bearer_token_clients); an unknown name is a configuration error. dpop.single_instance declares that one instance serves SystemAuth. WithProductionPolicy makes the token endpoint answer unauthorized_client, once the client is authenticated, to the client_credentials grant from a client using a shared secret and to clients registered with allow_bearer_tokens. Checking per request covers clients registered at runtime. Confidential clients keep using their secret for the authorization_code and refresh_token grants, and discovery stops listing client_credentials when it is refused. Refs: RMI-SYSTEMFORGE-203 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd replay cache Outside development mode the service now refuses to start without tokens.hmac_secret, with a statically configured client that sets allow_bearer_tokens, or without a DPoP replay cache every instance shares (dpop.replay_cache_redis_url or Options.DPoPReplayCache) or dpop.single_instance. It applies the token endpoint's production policy unless production.allow waives a check, and logs every waiver and the single-instance setting as a warning at startup. Refs: RMI-SYSTEMFORGE-203 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New logs a warning when it falls back to the in-memory replay cache for a public URL. Options.SingleInstance declares that one instance serves the API and silences it. Refs: RMI-SYSTEMFORGE-203 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-203 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-203 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One middleware sets a content security policy (default-src 'self', base-uri 'none', frame-ancestors 'none', object-src 'none', form-action 'self'), X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy same-origin, and HSTS when the issuer is https, on the login, consent, sign-up, sign-up code, waitlist, age question and logout pages, their error pages, and the JSON and redirect responses. Browsers check form-action on the redirects that follow a submission, so the consent page adds its client's redirect URI origins, the logout page the allowed redirect origins, and the age question https:. The pages need no inline script or style. same-origin replaces no-referrer on the pages: page URLs still never reach another origin as a referrer, and a same-origin form post keeps the Origin header the form handlers check, which no-referrer turns into null. Refs: RMI-SYSTEMFORGE-204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-204 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A provisioned user signing in through the organization's Google Workspace still links to the provisioned principal; a GitHub sign-in with the same verified address is refused with ErrEmailConflict. Refs: RMI-SYSTEMFORGE-202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-211 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The billing package supplies the ports payforge leaves to the host. Store implements payforge.Store on the Ent client: processed event IDs in sf_billing_events, whose unique event_id makes processing once hold across instances, and the customer-to-organization mapping in sf_billing_customers. Both tables are created by the Ent schema migration. billing/pglock implements payforge.Locker with a PostgreSQL transaction-scoped advisory lock held on its own connection until unlock, so it cannot outlive its connection and is never left held on a pooled one. It is a package of its own so the rest of billing runs on any Ent dialect. payforge is not yet tagged, so it resolves through a relative replace until a release is available. Refs: RMI-SYSTEMFORGE-205 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NewWebhookHandler verifies a request with payforge's reader, keeping its body limit, and passes the event to the lifecycle, which holds the organization's lock. It answers 400 to a signature that does not verify, 413 to an oversized body, 500 only when reading or handling fails, a lock included, so the provider redelivers, and 200 to duplicate, ignored, other-mode and foreign events, which are logged rather than retried. Response bodies are fixed words; causes are logged through the configured slog.Logger. A one-time checkout, which the lifecycle leaves to the host, is passed to an optional CheckoutCompleter under the same per-organization lock and recorded as processed. Refs: RMI-SYSTEMFORGE-206 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sink implements payforge's EntitlementSink, SaleSink, SellerSink and DisputeSink, and the webhook's CheckoutCompleter, on the Ent client. The subject is the buying organization. - A plan entitlement sets the organization's platform subscription: plan_tier is the plan it is on, the spec's default when nothing grants. OnEntitlement and WithPlanSyncer run after every apply. - A paid one-time checkout grants a license, matched to the listing, organization and purchaser through the order recorded at checkout (sf_billing_orders), never through provider metadata. - A full refund (sf_billing_refunds), or an open or lost dispute (sf_billing_disputes), suspends that license; a dispute that is won restores it. Refunds only grow and closed disputes stay closed, so events may arrive in any order, before the checkout included. - A seller's connected account (sf_billing_sellers) records whether it can take charges. SellerGate implements the new marketplace.PublishGate, which EntListingService.WithPublishGate uses to refuse publishing a paid listing with ErrSellerNotReady. Each apply changes the database in one transaction, then writes the authorization graph again from the stored state whether or not anything changed, so a sync that failed is repaired when the provider redelivers. A subject that is not a known organization is logged and acknowledged. Refs: RMI-SYSTEMFORGE-207 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checkout serves, on plain net/http, a plan checkout, a one-time listing checkout and the customer portal for the caller's organization. The organization comes from the caller's credential (configurable), the caller must pass a marketplace.PurchaseAuthorizer for it, which NewCheckout requires, and managed principals are refused. The provider customer is found from the organization on the server and created and linked the first time. Redirect URLs are configured, never read from the request, and must be https on a host in AllowedURLHosts. A listing checkout runs under the organization's lock and records its order before the buyer sees it. Errors are fixed JSON messages; causes are logged. NewStripe builds payforge's Stripe client and webhook reader with what production needs: the startup checks, the allowed redirect hosts, the shared Locker, and the reader's mode set from the key. Refs: RMI-SYSTEMFORGE-208 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Payments are taken by package billing on payforge. marketplace/stripe, marketplace.CheckoutService with its request and session types, EntService.WithCheckout, Service.Checkout, the marketplace API's POST /checkout endpoint and the WithPurchaseAuthorizer option that only it read are removed, with the stripe-go v84 dependency. PurchaseAuthorizer and OrgRolePurchaseAuthorizer stay and are passed to billing.CheckoutConfig. docs/marketplace/billing.md describes the wiring, the endpoints and tables, and how to upgrade from marketplace/stripe. BREAKING CHANGE: marketplace/stripe, marketplace.CheckoutService and POST /checkout are removed; use billing.Checkout and its endpoints. Refs: RMI-SYSTEMFORGE-209 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…retries Billing applies payforge's rule that a past-due subscription still grants its plan while the provider retries the payment, but Subscription.IsActive treated past due as inactive, so the organization's entitlements dropped the paid tier during the retries. IsActive now grants while active, trialing or past due. Refs: RMI-SYSTEMFORGE-207 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-212 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: RMI-SYSTEMFORGE-202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checkout accepted redirect URLs on localhost and loopback addresses with any key, so a live configuration pointing at a developer's machine started. Loopback hosts, and http with them, now pass only when the payment provider reports a test-mode key; a provider that reports no mode counts as live, so NewCheckout refuses such URLs at startup. Refs: RMI-SYSTEMFORGE-208 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cookie-write check required a JSON body on every state-changing request, so a signed-in browser could not upload a file with multipart/form-data. A browser sends Origin on such requests and a page cannot forge it, so when Origin is present and allowed, any body is now accepted; a cross-site form still arrives with its own site's Origin and is refused. Without an Origin, the JSON body is still required, with an allowed Referer when one is sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An application that mounts the organization console with its BFF session needs the console's SignOut: end the session, revoke its SystemAuth refresh token and clear the cookie, without the logout endpoint's response. BFF.SignOut does that, and /bff/auth/logout now calls it after its cross-origin check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A browser whose sign-in failed at the BFF's login or callback routes saw a raw JSON error. With BFFConfig.LoginErrorPath (also appauth.Options.LoginErrorPath) set, it is redirected there with ?error=<CODE> instead; requests that ask for JSON still get JSON, and the upstream's error text is never copied into the URL. The path must pass the return_to checks. Without it the errors stay JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…v0.27.0 Refs: RMI-SYSTEMFORGE-212 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 156 files, which is 56 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.
|
…estart test The test starts the server outside dev mode, which now requires the token HMAC secret and a declared replay cache. Refs: RMI-SYSTEMFORGE-203 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed billing request logged r.URL.Path, which the client controls; it now logs the route pattern the request matched. Refs: RMI-SYSTEMFORGE-208 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hardening for running SystemAuth and the relying-party stack with outside users, and billing moved onto payforge. Release notes:
docs/releases/v0.27.0.md; roadmap:docs/specs/initiatives/INIT-SYSTEMFORGE-015/ROADMAP.md(Phases 1–3).Security
invalid_grant/invalid_requestand no tokens. The memory store now returns the request for a used code, so a replayed code revokes its tokens.hddomain). GitHub uses the primary email only and never auto-links.tokens.hmac_secret, staticallow_bearer_tokensclients, and a per-instance replay cache withoutdpop.single_instanceor Redis stop startup. The token endpoint refusesclient_credentialswith a shared secret andallow_bearer_tokensclients. Waivers are listed underproduction.allowand logged.frame-ancestors 'none',X-Frame-Options,nosniff,Referrer-Policy: same-origin, and HSTS on https issuers.billingreplacesmarketplace/stripe. It has a Postgres store and cross-instance advisory lock, a verified, size-limited, idempotent webhook handler, sinks that re-sync authorization on every apply, and checkout/portal handlers that take the organization from the session and the redirect URLs from config. Loopback redirects are allowed only with a test-mode key.Also
relyingparty: cookie writes with an allowedOriginaccept any body (file uploads);BFF.SignOut;LoginErrorPathredirects failed sign-ins to the app's sign-in page.SECURITY.mdanddocs/security/production-checklist.md.Breaking
marketplace/striperemoved; production startup needstokens.hmac_secretanddpop.single_instance(or Redis);client_credentialswith a client secret refused in production; narrower email linking;Referrer-Policychanged tosame-origin. See the upgrade guide in the release notes.Tests and lint pass locally. The Postgres-backed billing and lock tests run only in CI (
SF_TEST_PG_DSN).🤖 Generated with Claude Code