Files
kandev/apps/web/lib/state/slices/auth/auth-slice.ts
T
João Salavisa b20047b3eb feat(plugins): add auth capability for plugin-provided OIDC/SAML login (#1964)
* feat(plugins): add auth capability for plugin-provided OIDC/SAML login

Plugins can now authenticate a visitor against an external IdP (OIDC/SAML)
and contribute a login button to the pre-auth screen:

- new `auth` manifest capability + a capability-gated X-Kandev-Auth-Login
  webhook-response directive; the host mints the kandev_session (ADR 0050),
  the plugin never receives the raw token. Plugin-set Set-Cookie is dropped.
- auth.Service.AuthenticateExternal: existing-identity / link-by-email / JIT
  member provisioning; refuses to auto-link to an admin; verified-email is
  the plugin's contract.
- auth_providers manifest block + plugins.SSOProviders() populate the
  anonymous boot payload (auth.ssoProviders); the login page renders a
  "Continue with <provider>" button per provider.
- ADR 0050 + plugin-manifest / plugin-authoring / auth spec docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: exclude publish_review_findings_kandev from public-docs coverage

The native code-review agent (#1957) added the publish_review_findings_kandev
MCP tool but did not update docs/public/coverage.json; that PR did not touch
docs/public so the path-filtered validator never ran. It's an internal review
reporting tool, not a user-facing feature, so record it as an exclusion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins,auth): address review on SSO directive and JIT provisioning

- takeAuthLoginDirective strips every case variant of X-Kandev-Auth-Login and
  rejects (403) when a plugin sends more than one, so no variant leaks to the
  browser and login never uses a nondeterministically-picked assertion.
- provisionExternalUser rolls back the just-created user when the identity
  insert fails (transient error or a lost UNIQUE(provider,subject) race) and
  resolves to the winning identity, so no account is left without a login and
  no email is reserved by an unusable row. Adds userstore.DeleteUser.
- auth spec: mark the login-page SSO buttons + provider discovery as landed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: drop redundant coverage exclusion; main now covers publish_review_findings_kandev

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 18:20:39 +01:00

34 lines
1.1 KiB
TypeScript

import type { StateCreator } from "zustand";
import type { AuthSlice, AuthSliceState } from "./types";
export const defaultAuthState: AuthSliceState = {
// IMPORTANT: authenticated defaults to true (with mode "disabled") so that
// deployments/tests whose boot payload has no auth block at all (older
// backend, or a test harness that never sets initialState.auth) preserve
// today's behavior: the app shell renders immediately, no login gate.
auth: {
mode: "disabled",
authenticated: true,
user: null,
ssoProviders: [],
},
};
type ImmerSet = Parameters<StateCreator<AuthSlice, [["zustand/immer", never]], [], AuthSlice>>[0];
// The auth slice only needs `set` (no cross-slice reads), so it is typed as a
// plain single-arg creator rather than the full 3-arg StateCreator. `set`
// keeps the immer-enabled recipe typing via ImmerSet.
export const createAuthSlice = (set: ImmerSet): AuthSlice => ({
...defaultAuthState,
setAuthState: (state) =>
set((draft) => {
draft.auth = state;
}),
clearAuthenticated: () =>
set((draft) => {
draft.auth.authenticated = false;
draft.auth.user = null;
}),
});