Conversation
…d smoke Verified live against beta with the test1@dos.me test account (provided by JOY; same credentials on prod and beta): - login via DOS ID works (in-tab or popup navigation both handled) - first-login onboarding is handled: when the user has no organization the app shows a Company + Create Account screen; the test creates the 'E2E Test Workspace' organization (verified: lands on /launches) - a fresh workspace then shows the DOS shared-billing plan gate (only Plus / Pro checkout, no free/skip path) with no connected channel, so the compose -> schedule -> calendar leg is gated: the test SKIPS with a clear reason instead of failing. Enabling the full leg needs either a DOS plan entitlement for the test account (DOS.Me admin action) or a free/skip path on the gate page (product decision).
test(e2e): working DOS ID login + onboarding handling in authenticated smoke
…subscription
Incident 2026-09-22: a free-plan member login (test1@dos.me, ADMIN in the
org) triggered the DOS shared-billing sync on the users endpoint and
clearDosSyncedSubscription - deleteMany({ organizationId }) - wiped the org's
ULTIMATE stripe subscription. Any free-plan member of any paid org could do
this on every page load.
- syncOrg now resolves the caller's membership role and only proceeds to
clear/sync when the role is SUPERADMIN (the owner role this codebase
assigns to org creators). Members get a read-only mapped view of their own
DOS plan instead; the org subscription is untouched.
- tests/bootstrap-dos-sync-guard.spec.ts: 5 pure unit cases (owner free
clears, owner plus syncs, member free/plus read-only, non-DOS user no
write). Repository modules are stubbed with explicit jest.mock factories -
their real prisma import graph cannot load in the CJS jest context.
Prod data was restored separately (subscription recreated, isLifetime
flipped back on the 3 orgs).
- generateAuthUrl redirects to api.dos.me/oauth/reddit/authorize with a per-env product label (crove-post-prod/beta, override REDDIT_BROKER_PRODUCT) and a >=128-bit state instead of Reddit directly with REDDIT_CLIENT_ID - authenticate exchanges the broker's one-time delivery handle for the token bundle via POST /oauth/reddit/token-delivery/:handle (X-API-Key = DOS_ME_INTERNAL_API_KEY); the state/login Redis contract is unchanged - REDDIT_CLIENT_ID/SECRET stay in the env: the broker does not cover the refresh grant, which 401 recovery for scheduled posts older than an hour depends on - frontend continue page maps the callback's handle param to the connect body's code field
The reviewer verified the real repository modules do in fact load in this CJS jest context (the file-type ESM claim was wrong); the stubs exist for isolation so interaction assertions stay on the injected instances. State that accurately.
fix(billing): only the org owner's DOS entitlement may write the org subscription
… silent signup fallback Two commissioned auth UX fixes for the DOS ID single-provider flow: 1. Auto-redirect: when SSO is the only auth method (isGeneral + genericOauth), the /auth and /auth/login pages now start the DOS ID flow immediately (Redirecting to DOS ID... loader) instead of showing a page whose only content is one button. OauthProvider gains an autoStart prop: it fires gotoLogin on mount, falls back to the manual button if the link fetch fails, and clearing the error-page retry budget on a deliberately initiated flow. 2. Honest error state: a failed /oauth/:provider/exists exchange (state cookie mismatch, upstream token error) previously fell through to the signup form, so a broken sign-in looked like a fresh registration - the exact confusion reported on 2026-09-21 during the prod login outage. AuthErrorState now shows what happened (HTTP status, error message) with a loop-guarded retry: up to 2 automatic SSO restarts via sessionStorage budget, then a manual link so a persistent failure cannot ping-pong. The i18n strings use the fork's t(key, english-default) pattern consistent with the existing SSO strings on these pages.
feat(reddit): connect through the dos.me OAuth broker
Review round: an IdP error bounce can return to /auth WITHOUT a code, which skips the exists-exchange error path entirely and re-fires the auto-start - an uncontrolled /auth <-> IdP redirect storm is possible if the IdP ever auto-returns. Cap it: OauthProvider's autoStart writes a sessionStorage timestamp before navigating and refuses to auto-redirect again within 10s (falls back to the manual button). Manual clicks are exempt. Also per review: drop the dead useT in Register and the never-called onRetry prop of AuthErrorState.
fix(auth): SSO auto-redirect + honest error page instead of the silent signup fallback
| const retry = useCallback(async () => { | ||
| try { | ||
| window.sessionStorage.setItem(DOS_OAUTH_RETRY_KEY, String(attempts + 1)); | ||
| const response = await fetch('/auth/oauth/GENERIC'); | ||
| if (response.ok) { | ||
| window.location.href = await response.text(); | ||
| return; | ||
| } | ||
| } catch (e) { | ||
| console.error('Failed to restart the SSO flow:', e); | ||
| } | ||
| window.location.href = '/auth/login'; | ||
| }, [attempts]); |
| console.error('Failed to restart the SSO flow:', e); | ||
| } | ||
| window.location.href = '/auth/login'; | ||
| }, [attempts]); |
There was a problem hiding this comment.
Code Review
This pull request introduces several enhancements to the authentication and billing systems, including an auto-start feature for OAuth, a loop-guarded retry state for failed OAuth exchanges, integration with the dos.me Reddit OAuth broker, and a security guard that restricts organization subscription synchronization to organization owners (SUPERADMIN). Additionally, unit and E2E tests have been updated to cover these changes. The review feedback suggests optimizing the database query in DosSharedBillingService by adding a targeted method in OrganizationRepository to fetch only the specific organization and user role, rather than filtering all of a user's organizations in memory.
| const membership = await this.organizations | ||
| .getOrgsByUserId(user.id) | ||
| .then((orgs) => orgs.find((o) => o.id === organizationId)); |
There was a problem hiding this comment.
Fetching all of a user's organizations and then filtering in memory to find their role in a specific organization can be inefficient. This could lead to performance issues for users who are members of a large number of organizations.
A more performant approach would be to add a dedicated method in OrganizationRepository to fetch only the specific organization and the user's role within it.
For example, a method like findUserRoleInOrg(userId: string, organizationId: string): Promise<{ role: Role } | null> could execute a more targeted database query. Here is an example of how that might be implemented in your repository:
// In OrganizationRepository
async findUserRoleInOrg(userId: string, organizationId: string) {
const org = await this.prisma.organization.findUnique({
where: { id: organizationId },
select: {
users: {
where: { userId },
select: { role: true },
},
},
});
return org?.users?.[0] || null;
}This would significantly reduce the amount of data fetched from the database and improve the performance of this critical path.
What kind of change does this PR introduce?
Release promotion: dev -> main. Merges everything validated on dev into main so the production image (ghcr.io/dos/crove-post:latest, built from main) picks it up. No new code authored in this PR - it is the dev branch as reviewed and merged PR-by-PR.
Why was this change needed?
The promotion ships: (1) the platform minimal batch (docs/ADR suite, upstream sync, eslint 9 green CI, vitest + e2e test tracks, prod compose corrections), (2) the prod incident fixes - the DOS billing owner guard (PR #53: only the org owner's DOS entitlement may write the org subscription, after a free-plan member login wiped an ULTIMATE subscription) and the auth UX fixes (PR #55: SSO auto-redirect + honest error page with loop-guarded retry), (3) the e2e verified DOS ID login flow.
Technical Details & Scope
Full dev history since the last promote. Highlights:
Verification & Testing
Every constituent PR was CI-gated (build.yml: Postgres/Redis bootstrap suites, frontend vitest, full build; branding-guard) and reviewed. Auth login verified live against beta (e2e) and prod (post-incident verification script). Prod data integrity verified via SQL before/after the incident remediation.
QA
Checklist: