Conversation
|
Re-homed to |
|
Hi @yxyxy — thanks for the enormous amount of careful work here; the fail-closed instincts (multi_user off by default, registration disabled by default, 404-without-existence-leak on cross-org models, scrypt + encrypted SMTP secrets) are exactly right. That said, we can't take the bundle as-is: it introduces a whole new identity model (users/orgs/SMTP/GitHub OAuth) that needs an owner-approved RFC before landing, it collides with migrations already on the branch (your 161_organization_quotas.sql vs the existing 161_config_audit_log.sql — the runner aborts on duplicate version prefixes), and it's currently conflicting with the base. Two asks: (1) please extract the client-abort guard (commits 83a4797/bcef7f0 + its test) into its own small PR — that part is clean, self-contained, and we want it this cycle; (2) let's move the org layer discussion to #11288 as an RFC (scope, rollout plan, migration numbering strategy) and re-base it on a fresh cycle branch. Happy to help structure the RFC. |
…in role, key linkage PHASE 0 VERDICT approved (see _tasks/P0-architecture/PHASE0_VERDICT.md). P1.01 user-model: src/lib/db/users.ts + migration 155_users.sql (CRUD, role enum, status). P1.02 user-session-principal: src/lib/org/principal.ts resolveDashboardUserPrincipal (JWT sub -> user, legacy null). P1.03 platform-admin-role: isPlatformAdmin/resolvePlatformRole in principal.ts. P1.04 inference-key-principal: api_keys.user_id (migration 156) + setApiKeyUserId/getApiKeyUserId/getApiKeyUser (auth semantics unchanged). Re-export users + key linkage in src/lib/localDb.ts (re-export only). Tests: tests/unit/org-users.test.ts, org-principal.test.ts, org-identity.test.ts (18 passing). Backward compatible: NULL org/user columns = personal; legacy keys validate unchanged.
…variant, migration 157)
…ation, active-listing contract)
… target validation
…t-token independence P6.01 model-parser: src/lib/org/qualifiedRoute.ts parseQualifiedModel — splits `<organization>/<route>`; provider-prefixed ids (openai/gpt-4) stay personal. P6.02 route-resolution: resolveQualifiedRoute — fail-closed (non-member/unknown slug => null, no existence reveal); personal models unchanged. P6.03 chat-completions: buildOrgRoutingContext (member => org scope + scoped connectionIds; non-member => denied). Minimal scope-only wiring in src/app/api/v1/chat/completions/route.ts: denied qualified route => 404 (no tenant leak). Reuses existing combo/auto machinery (Invariant diegosouzapw#3) — no second routing engine. P6.04 management-token-compatibility: requireManagementAuth (token scopes) and requireOrganizationAccess (membership) are independent; org role != mgmt scope. Tests: tests/unit/org-qualified-route.test.ts, org-route-resolution.test.ts, org-chat-qualified.test.ts, org-mgmt-token-compat.test.ts (23 passing). All org unit tests 134/134 green. Backward compatible: personal models/combo/auto behavior unchanged.
…o org connections P7.02: in createVirtualAutoComboFromPrepared, apply filterCandidatesByAllowedConnections(scopeAllowedConnectionIds) BEFORE any other narrowing, so no downstream filter/fallback/failover can reintroduce an out-of-scope connection. Personal scope (null) is the identity function; a denied scope (empty set) yields an empty pool (fail-closed — never widens back to personal). Wiring: - autoScope.ts: scopedConnectionIdSet(scope) -> Set<string>|null (null personal, empty denied, else org connectionIds); scopeAllowsConnection() fail-closed. - candidateOverrides.ts: filterCandidatesByAllowedConnections() (fail-closed allowlist, mirrors existing exclusion filter). - virtualFactory.ts: createVirtualAutoComboFromPrepared 6th param scopeOptions.allowedConnectionIds; public createVirtual passes it through. - autoRouting.ts: createVirtualAutoCombo(state, combo, apiKeyId, scope) threads scopedConnectionIdSet(scope) + autoScopeKey(scope); namespaced virtualCombo.id via buildScopedAutoComboId() (P7.03 foundation). Tests: tests/unit/org-auto-candidate-scope.test.ts (12 passing) — org auto returns only org connections; personal auto unchanged; org A never leaks org B connections; non-member fail-closed. All org unit tests still green. Backward compatible: personal auto routing byte-identical (no scope => null => no filter).
P7.03 cache-scope: virtual auto-combo ids are namespaced by organization via buildScopedAutoComboId() (implemented in P7.02 autoRouting.ts). Distinct orgs get distinct combo/cache/cooldown/lockout keys; personal routes keep their existing un-namespaced ids (backward compatible); denied scope isolated. P7.04 failover-isolation: the existing intra-request auto failover selects from the candidate pool it receives, so scoping the pool scopes failover. filterCandidatesByAllowedConnections() (P7.02) restricts the pool to the org's connectionIds BEFORE any downstream selection; org-A scope can never reach org-B / personal / null candidates. Personal scope (null) is the identity function — legacy failover byte-identical. Denied scope yields an empty pool (fail-closed — never widens to personal). Tests: tests/unit/org-auto-cache-scope.test.ts (5), org-auto-failover-isolation.test.ts (5). All org unit tests green.
P8.01: full REST API under /api/v1/organizations/* backed by the P2/P3 data + authz layers. Service logic lives in src/lib/org/orgApiService.ts (pure handlers returning Response objects, fully unit-testable); thin route.ts files under src/app/api/v1/organizations/ forward to it (Next.js App Router pattern). Routes: - POST/GET / create (owner=principal) / list principal's orgs - GET/PATCH/DELETE /[id] read / manage / archive (fail-closed by capability) - GET/POST /[id]/members list / add (manageResource) - DELETE/PATCH /[id]/members/[userId] remove / promote|demote (manageMembership) - GET/POST /[id]/invitations list / create (manageResource) - DELETE /[id]/invitations/[token] revoke (manageResource) - GET /invitations/[token] public token view (accept UI, no membership) - GET /[id]/connections, /[id]/combos org-scoped surfaces (P8.04/05/06 data) Authn: resolveDashboardUserPrincipal (JWT cookie) → 401 when absent. Authz: requireOrganizationAccess (P3) re-resolves org from membership (never trusts client id), fail-closed 403/404. DB domain errors mapped to sanitized HTTP. Also adds listUserOrganizations() to src/lib/db/organizations.ts + re-export (re-export ONLY in localDb.ts). Tests: tests/unit/org-api.test.ts (6) — create/list, 401 no-auth, 409 dup slug, fail-closed 404 for non-member (no existence reveal), owner update/archive, owner invite/revoke. All green. Backward compatible: no existing routes touched; personal routing untouched.
Expose githubOAuthEnabled via GET /api/settings/require-login; render a 'Continue with GitHub' button on the login page only when OAuth is configured (gated server-side, mirrors the OIDC button). Add i18n key (en/ru). TDD: tests/integration/github-login-button.test.ts (2). All pass; eslint clean.
Phase 08 Task 06. Add tests/unit/github-security.test.ts (3): idempotent existing-link resolution, single-use CSRF state rejected on replay, expired state rejected. Hardening is enforced by UNIQUE(provider,sub) + single-use/ expiring state + idempotent resolveOrProvisionGitHubUser from Task 04. All pass; eslint clean.
… regression suite Phase 08 Task 07 (compatibility checklist). Add tests/integration/github-task07.test.ts (5): unknown state -> 403, server-bound redirect_uri, linking-attack UNIQUE backstop, identity-mismatch safe-link never clobbers existing user, single-use state replay rejected. Full Phase 08 = 27/27 green; eslint clean.
…cret-leak, rate-limit, CSRF, audit events, benchmark Phase 09 (8 tasks). Adds 24 passing tests covering: - T01 session lifecycle (sub+30d exp, expiry rejection, legacy admin resolve, tamper reject) - T02 authorization matrix (isPlatformAdmin/resolvePlatformRole) - T03 cross-user resource isolation (API keys + org membership scoping) - T04 secret-leak guards (reset tokens hashed, OAuth/SMTP secrets encrypted + masked on read) - T05 login brute-force guard lockout/reset/disable - T06 CSRF token bound to session (cross-session replay rejected) - T07 security-safe audit events on forgot/reset/github-callback (no token/code/password leakage) - T08 benchmark: principal resolution + org dashboard load < 50ms avg/iter; guard map bounded Also wires logAuditEvent into forgot-password, reset-password and github/callback routes (fail-closed, metadata carries only safe flags). All green; eslint clean.
…al+management tokens, org routing, inference keys Phase 10 (8 tasks). Adds compatibility regression suite (4 tests) verifying: - T01/T02 fresh/pre-multi-user defaults (multi-user OFF, registration disabled, requireLogin ON, OIDC OFF) - T03/T06 personal inference API key (user_id NULL) still resolves without forced registration - T05 management token mode retained (manage-scoped key preferred for internal use) - T04 organization routing + role enforcement unchanged (non-member rejected, member allowed) Documentation: COMPATIBILITY.md (migration/auth modes/registration/SMTP/recovery/GitHub OAuth) and FINAL_REVIEW.md (security/migration/rollback/lockout-prevention). All Phase 9+10 = 28/28 green; eslint clean.
- Replace shadcn-style class names (bg-background, text-muted-foreground, bg-primary/text-primary-foreground) with the project's Tailwind v4 theme tokens (bg-card, bg-surface, text-text-main, border-border, bg-primary + text-white) so the form is no longer transparent on the canvas. - Fix setTesting(false) call that referenced the state value instead of the setter (runtime crash in testConnection finally block). - Add frontmatter title to ORGANIZATIONS-SECURITY.md so the MDX build passes.
Rebase onto release/v3.8.50 surfaced migration-version collisions: the base branch added 155-159 (agentic_conversations, conversation_turn_nodes, exclusive_connection_leases, call_logs_error_type, combos_org + remove_mimocode_provider) on the same prefixes my org/auth roadmap used. Renumber my migrations to free slots above the base max (172): - users 155 -> 165 (must run before 162 login_identifier) - api_keys_user 156 -> 174 - organizations 157 -> 175 - provider_connections_org 158 -> 176 - combos_org / remove_mimocode_provider 159 -> 177 / 178 - login_identifier 162 -> 179, login_identifier_unique 163 -> 180, email_unique 167 -> 181 (depend on users table, run after 165) Add retroactive idempotency guards in migrationRunner for the new ALTER migrations (users/login_identifier/api_keys_user/provider_connections_org/ combos_org) and update the stale unit tests that hard-coded the old migration file paths and the old acceptInvitation contract (it returns the invitation record; the membership join is performed by the accept-invitation route via addMember).
… policy The /login page's Register-control visibility depends on the registration policy, but the GET handler required authentication — so an unauthenticated visitor could never read it and the button never appeared (Task 02 regression). GET now returns only the non-sensitive policy flags (multiUserEnabled, registrationPolicy) without auth; POST (policy mutation) stays admin-only. Added TDD guard (instance-settings-public-get.test.ts); updated existing authorization test to cover the public-read / admin-only-mutation split.
…allback - registrationConfig.ts (server): resolveRegistrationVisibility() reads OMNIROUTE_MULTI_USER_ENABLED + OMNIROUTE_REGISTRATION_POLICY (config-primary), falls back to instance_auth_settings DB row. - registrationPolicy.ts (client-safe): isRegistrationAllowed() pure derivation, no DB/server imports (avoids better-sqlite3 in client bundle). - useRegistrationPolicy hook now reads the public /api/settings/require-login endpoint (no auth) instead of the auth-gated /api/auth/instance-settings, so the Register button shows for unauthenticated visitors. - require-login GET now returns multiUserEnabled + registrationPolicy. - Fix transparent Users/UserDetail panels: replace non-existent --color-bg-surface with --color-surface (defined in globals.css). - Remove dead registrationVisibility.ts; add unit test. TDD: tests/unit/registrationPolicy.test.ts (2/2 pass), ESLint clean.
…, login/register toggle + styled form - publicApiRoutes: mark /api/auth/register (POST+OPTIONS) as exact-method public route so the self-service signup is not gated by management auth (was 401 Authentication required). - registrationService: resolveRegistrationVisibility() is now config(env)-primary, DB-fallback — env OMNIROUTE_REGISTRATION_POLICY overrides the DB row (matches the login-page button visibility). - login/page.tsx + RegistrationForm.tsx: hide login fields in register mode, add 'Back to sign in' link, give registration inputs a surface background (no longer transparent). - i18n: add backToSignIn key (en/ru). Verified live on webpack dev server: POST /api/auth/register without cookie -> 201 with invite code, 400 without, never 401.
…ddMember with owner actor
acceptInvitation() (per P7.05) does NOT create the membership row; callers wire
membership creation at the API layer. The test modeled a non-manager membership via
the invitation flow, which left the member with no DB membership, so resolveOrgAccess
returned 404 instead of the expected 403. Use addMember({ actorUserId: owner.id })
to model a real non-manager membership.
…gate + post-flight consume Previously org quota was declarative only: enforceOrgQuotaScope existed but was never called from the chat execution path, and nothing wrote to the org quota pool, so setting an org limit had zero runtime effect (cross-tenant isolation gap, security-relevant). - orgQuotaEnforcement.consumeOrgQuota(): post-flight accounting that writes the realized request count into the shared QuotaStore pool org:<orgId>, mirroring the legacy personal recordConsumption flow. Fail-open. - Fix latent bug: enforceOrgQuotaScope/consumeOrgQuota called getQuotaStore() without await (async) so the store was a Promise and the call threw silently (swallowed by fail-open) — pool never read or written. Now awaited. - Build a proper DimensionKey (poolId+unit+window) so the enforce read and the consume write share the same store key (previously the read passed a bare unit string, mismatching the write key — masked by the getUsage test seam). - Wire into src/sse/handlers/chat.ts: pre-flight enforceOrgQuotaScope gate before handleComboChat (429 on block, fail-open on error) and post-flight consumeOrgQuota for non-streaming success, both threaded by routingScope. Personal/denied scopes are untouched (legacy path unchanged). TDD: org-quota-enforcement.test.ts extended (7 tests) proves consume writes to the real pool, a follow-up pre-flight blocks, cross-tenant isolation, and fail-open on degenerate input. Org unit suite 239/239 green; ESLint clean.
…e isolation (P6)
The dashboard sends org combos as qualified routes (e.g. team1/combo:dev —
OrgModelPicker.tsx). But chat.ts only resolved combos via the legacy
getComboForModel, which returns null for qualified names, so an org member
requesting their own org combo got a fail-closed 404 — the explicit-org-combo
feature was non-functional.
Now, when the request resolved to an organization routing scope (a verified
member) and the model is org-qualified, chat.ts resolves the combo via
resolveComboInScope({name, organizationId}, routingScope.ctx) — the SAME
verified OrganizationContext the route already produced. This is the
execution-time isolation boundary: a member of teamA can only resolve teamA's
combo, never teamB's (fail-closed on orgId mismatch). Personal / denied /
unqualified models are untouched (legacy path unchanged).
TDD: org-combo-resolution.test.ts extended with a chat-path test asserting a
member resolves their own org combo and a cross-tenant member gets null.
Org unit suite 239/239 green; ESLint clean.
Documents that the branch introduces no new test regressions; the ~12 pre-existing unit failures on this Windows/git-bash host are environmental (Windows temp-dir EPERM in isolateDataDir cleanup hook, command-line length limit on single-invocation full-suite globs, aggregate-runner hang), not logic regressions. Confirms org/quota/combo suites are green and ESLint clean for the P6/P9 changes.
- Add admin API /api/admin/github-oauth (GET/POST) gated by requirePlatformAdminUser; persists via setGithubOAuthConfig and masks the client secret on the read path. - Fix missing decrypt import in githubOAuthConfig.ts (callback would have thrown ReferenceError on token exchange). - Add GithubOAuthSettings UI panel mounted in dashboard settings/general, mirroring SmtpSettings. - Add login-page wiring already present reads githubOAuthEnabled from /api/settings/require-login, so the Continue-with-GitHub button now appears once an admin configures the OAuth app. - Add registrationVisibility.ts (deriveRegistrationAllowed) referenced by the registration-visibility unit test. - Add i18n keys (en/ru) and integration tests (7/7 green).
- Add email_verified column (users) + email_verification_tokens table (182/183 migrations). - emailVerification.ts: one-time, expiring verification tokens (reuses passwordReset pattern, SHA-256 hashed). - emailVerificationService.ts: dispatch via existing EmailService + renderVerificationEmail (already present, was orphaned). - registrationService: when SMTP configured, new accounts start unverified + receive a verification token/email; without SMTP they are verified immediately (preserves P3). - login: block unverified accounts (403 needsVerification). - verify-email + resend-verification endpoints (fail-closed, anti-enumeration). - Tests (TDD): pending, successful, expired/replayed, resend, login blocking, anti-enumeration (10/10). Auth regression + org suites green.
…/prod server A browser closing the TCP socket during navigation/HMR/Back-Forward cache (or nulling the request while the dashboard polls after login) emits Error: aborted / ECONNRESET on the http.IncomingMessage. With no 'error' listener attached, Node re-throws it as an uncaughtException that kills the whole server process — surfacing as a wall of net::ERR_CONNECTION_REFUSED after login (see app.log uncaughtException: aborted). - scripts/dev/httpClientAbortGuard.mjs: reusable guard that attaches req/res 'error' listeners (absorbing client-abort errors) and a process-level uncaughtException/unhandledRejection safety net that swallows benign aborts while preserving crash semantics for real errors. - scripts/dev/run-next.mjs: install the guard in start() and attach per-request stream guards in the request listener. - tests/unit/httpClientAbortGuard.test.mjs: TDD coverage for the exact abortIncoming crash signature + idempotency + crash-semantics preservation.
…, liveWS, embedWS) Extends #fix-dev-server-aborted beyond the Next dev/prod server to the other Node http servers that are equally exposed to a client aborting the TCP socket (navigation/HMR/bfcache) while a response is in flight: - src/lib/apiBridgeServer.ts — the OpenAI-compatible API bridge server. - src/server/ws/liveServer.ts — the live-dashboard WebSocket daemon (20132). - src/lib/services/embedWsProxy.ts — the embedded-service WS proxy (20131). Each now calls attachRequestStreamGuards(req, res) inside its http.createServer request listener, and installProcessCrashGuard() once at start (idempotent process-level safety net). The guard implementation is promoted to a single source of truth at src/shared/utils/httpClientAbortGuard.mjs (importable from both the Node-only dev server and the TypeScript servers; tsconfig allowJs:true). scripts/dev/httpClientAbortGuard.mjs is now a pure re-export so the dev server's existing import path is unchanged. TDD: adds a test asserting the dev re-export is the SAME function objects as the shared module (guards against future drift). Full guard suite stays green (8/8).
Resolve MISSING_MESSAGE IntlError console spam on ru locale: - auth: enabled, save, saved, saveError, adminRequired (GithubOAuthSettings) - common: bypassProviderQuota, bypassProviderQuotaDescription - home: recentRequests, recentRequestsEmpty, recentRequestsModel, recentRequestsTokens, recentRequestsWhen - sidebar: logout, quickNavigation, quickNavigationTitle, openQuickNavigation
…races) Corrects two duplicate closing braces left after merging the LOCAL_ONLY_OAUTH_IMPORT_ROUTES block with isPublicExactMethodRoute during the rebase onto release/v3.8.51.
|
Re-homed to |
Summary
This PR bundles two improvements to OmniRoute:
1. Organizations (multi-tenant) layer
Organizationsand API routes under/api/v1/organizations/*.2. Client-abort server resilience (bugfix)
aborted/ECONNRESETon the incoming socket) from crashing the dev/prod HTTP server via an unhandleduncaughtException.httpClientAbortGuard(shared module + dev-server integration) that swallows client-abort errors while preserving genuine crash semantics.createServercall sites:apiBridgeServer,liveServer(WS),embedWsProxy.tests/unit/httpClientAbortGuard.test.mjs, 8/8 passing).3. i18n fix
auth.*,common.*,sidebar.*,cheaperInferenceSponsorBanner) toen.json/ru.jsonto resolveMISSING_MESSAGEIntlError console spam on therulocale.Test plan
node --test tests/unit/httpClientAbortGuard.test.mjs> 8/8 passing.uncaughtException: aborted.Base branch
Targets
release/v3.8.50(current active release branch).