Restructure repo into five-package AFI architecture

Mizan is an Application Framework Interface (AFI) with five
independent packages:

  packages/
    mizan-ast/       Language layer (source → KDL schema)
    mizan-schema/    IR layer (KDL schema definition)
    mizan-rpc/       Protocol layer (client gen + server adapters)
      adapters/django/   ← was django/
      generator/         ← was react/src/generator/
    mizan-csr/       State layer (client state engine)
      adapters/react/    ← was react/
    mizan-ssr/       Rendering layer (server-side rendering)

Each package is independent. The adapter directories contain the
framework-specific implementations. Stub packages (ast, schema, ssr)
establish the structure for future work.

264 Django tests + 33 React tests pass from new locations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 13:31:19 -04:00
parent 01d33173a4
commit b28ee72c67
139 changed files with 25 additions and 16 deletions

View File

@@ -0,0 +1,309 @@
import { OAuthProcess, apiURL } from './defines'
import {
type RegistrationResponseJSON,
type AuthenticationResponseJSON,
} from '@simplewebauthn/browser'
import type {
// Core types
AuthError,
User,
Flow,
Authenticated,
AuthenticationMeta,
// Request types
LoginRequest,
SignupRequest,
ProviderSignupRequest,
ReauthenticateRequest,
ChangePasswordRequest,
ResetPasswordRequest,
MFAAuthenticateRequest,
WebAuthnUpdateRequest,
// Response types
AllauthResponse,
AuthenticatedResponse,
AuthenticationRequiredResponse,
ReauthenticationRequiredResponse,
ConfigurationResponse,
EmailListResponse,
SessionListResponse,
AuthenticatorListResponse,
ProviderAccountListResponse,
TOTPStatusResponse,
RecoveryCodesResponse,
WebAuthnCreationOptionsResponse,
WebAuthnRequestOptionsResponse,
EmailVerificationInfoResponse,
ErrorResponse,
} from './types'
export type { AuthError } from './types'
// Registration = creating new credentials (signup, add)
// Authentication = verifying existing credentials (login, authenticate, reauthenticate)
type RegistrationCredential = RegistrationResponseJSON
type AuthenticationCredential = AuthenticationResponseJSON
/**
* Union of all possible auth responses
*/
export type AuthResponse =
| AuthenticatedResponse
| AuthenticationRequiredResponse
| ReauthenticationRequiredResponse
| ConfigurationResponse
| EmailListResponse
| SessionListResponse
| AuthenticatorListResponse
| ProviderAccountListResponse
| TOTPStatusResponse
| RecoveryCodesResponse
| WebAuthnCreationOptionsResponse
| WebAuthnRequestOptionsResponse
| EmailVerificationInfoResponse
| ErrorResponse
| AllauthResponse
export interface AuthDetails {
isAuthenticated: boolean
requiresReauthentication: boolean
user: User | null
pendingFlow: Flow | undefined
}
export const getAuthDetails = (auth: AllauthResponse | null | undefined): AuthDetails => {
const meta = auth?.meta as AuthenticationMeta | undefined
const isAuthenticated = !!auth && (auth?.status === 200 || (auth?.status === 401 && !!meta?.is_authenticated))
const requiresReauthentication = !!(isAuthenticated && auth?.status === 401)
const data = auth?.data as Authenticated | { flows?: Flow[]; user?: User } | undefined
const pendingFlow = (data as { flows?: Flow[] })?.flows?.find((flow: Flow) => flow.is_pending)
return {
isAuthenticated,
requiresReauthentication,
user: isAuthenticated ? (data as Authenticated)?.user ?? null : null,
pendingFlow
}
}
export type BrowserFormAction = (action: string, data: Record<string, string>) => void
type RequestFn = (method: string, path: string, data?: unknown, headers?: Record<string, string>) => Promise<AllauthResponse>
export const createAPI = (
request: RequestFn,
browserFormAction?: BrowserFormAction
) => {
return {
getConfig: async (): Promise<ConfigurationResponse | ErrorResponse> =>
await request('GET', apiURL.CONFIG) as ConfigurationResponse | ErrorResponse,
session: {
getStatus: async (): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
await request('GET', apiURL.SESSION) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
list: async (): Promise<SessionListResponse | ErrorResponse> =>
await request('GET', apiURL.SESSIONS) as SessionListResponse | ErrorResponse,
logout: async (): Promise<AllauthResponse> =>
await request('DELETE', apiURL.SESSION),
remove: async (ids: number[]): Promise<AllauthResponse> =>
await request('DELETE', apiURL.SESSIONS, { sessions: ids }),
},
account: {
signup: async (data: SignupRequest): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
await request('POST', apiURL.SIGNUP, data) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
login: async (data: LoginRequest): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
await request('POST', apiURL.LOGIN, data) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
reauthenticate: async (data: ReauthenticateRequest): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('POST', apiURL.REAUTHENTICATE, data) as AuthenticatedResponse | ErrorResponse,
emails: {
list: async (): Promise<EmailListResponse | ErrorResponse> =>
await request('GET', apiURL.EMAIL) as EmailListResponse | ErrorResponse,
add: async (email: string): Promise<EmailListResponse | ErrorResponse> =>
await request('POST', apiURL.EMAIL, { email }) as EmailListResponse | ErrorResponse,
remove: async (email: string): Promise<EmailListResponse | ErrorResponse> =>
await request('DELETE', apiURL.EMAIL, { email }) as EmailListResponse | ErrorResponse,
setPrimary: async (email: string): Promise<EmailListResponse | ErrorResponse> =>
await request('PATCH', apiURL.EMAIL, { email, primary: true }) as EmailListResponse | ErrorResponse,
verification: {
dispatch: async (email: string): Promise<AllauthResponse> =>
await request('PUT', apiURL.EMAIL, { email }),
checkKey: async (key: string): Promise<EmailVerificationInfoResponse | ErrorResponse> =>
await request('GET', apiURL.VERIFY_EMAIL, undefined, { 'X-Email-Verification-Key': key }) as EmailVerificationInfoResponse | ErrorResponse,
confirmKey: async (key: string): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('POST', apiURL.VERIFY_EMAIL, { key }) as AuthenticatedResponse | ErrorResponse,
}
},
password: {
set: async (data: ResetPasswordRequest): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('POST', apiURL.RESET_PASSWORD, data) as AuthenticatedResponse | ErrorResponse,
change: async (data: ChangePasswordRequest): Promise<AllauthResponse> =>
await request('POST', apiURL.CHANGE_PASSWORD, data),
reset: {
dispatch: async (email: string): Promise<AllauthResponse> =>
await request('POST', apiURL.REQUEST_PASSWORD_RESET, { email }),
checkKey: async (key: string): Promise<AllauthResponse> =>
await request('GET', apiURL.RESET_PASSWORD, undefined, { 'X-Password-Reset-Key': key }),
}
}
},
loginCodes: {
request: async (email: string): Promise<AuthenticationRequiredResponse | ErrorResponse> =>
await request('POST', apiURL.REQUEST_LOGIN_CODE, { email }) as AuthenticationRequiredResponse | ErrorResponse,
confirm: async (code: string): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
await request('POST', apiURL.CONFIRM_LOGIN_CODE, { code }) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
},
oauth: {
list: async (): Promise<ProviderAccountListResponse | ErrorResponse> =>
await request('GET', apiURL.PROVIDERS) as ProviderAccountListResponse | ErrorResponse,
signup: async (data: ProviderSignupRequest): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('POST', apiURL.PROVIDER_SIGNUP, data) as AuthenticatedResponse | ErrorResponse,
provider: (providerID: string) => {
const buildAuths = (processType: string) => {
return {
withToken: async (token: string): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
await request(
'POST',
apiURL.PROVIDER_TOKEN,
{
provider: providerID,
process: processType,
token: token,
}
) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
withRedirect: (endpoint: string): void => {
if (browserFormAction) {
if (!process.env.NEXT_PUBLIC_HOST_URL) {
throw new Error('NEXT_PUBLIC_HOST_URL environment variable is not set. OAuth redirects require this to be set at build time.')
}
browserFormAction(
apiURL.REDIRECT_TO_PROVIDER,
{
provider: providerID,
process: processType,
callback_url: new URL(`${process.env.NEXT_PUBLIC_HOST_URL}/${endpoint}`).toString(),
}
)
}
}
}
}
return {
removeFrom: async (accountUID: string): Promise<ProviderAccountListResponse | ErrorResponse> =>
await request('DELETE', apiURL.PROVIDERS, { provider: providerID, account: accountUID }) as ProviderAccountListResponse | ErrorResponse,
login: buildAuths(OAuthProcess.LOGIN),
connect: buildAuths(OAuthProcess.CONNECT),
}
}
},
mfa: {
list: async (): Promise<AuthenticatorListResponse | ErrorResponse> =>
await request('GET', apiURL.AUTHENTICATORS) as AuthenticatorListResponse | ErrorResponse,
authenticate: async (code: string): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('POST', apiURL.MFA_AUTHENTICATE, { code } as MFAAuthenticateRequest) as AuthenticatedResponse | ErrorResponse,
reauthenticate: async (code: string): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('POST', apiURL.MFA_REAUTHENTICATE, { code } as MFAAuthenticateRequest) as AuthenticatedResponse | ErrorResponse,
trust: async (trust: boolean): Promise<AllauthResponse> =>
await request('POST', apiURL.MFA_TRUST, { trust }),
totp: {
getStatus: async (): Promise<TOTPStatusResponse | ErrorResponse> =>
await request('GET', apiURL.TOTP_AUTHENTICATOR) as TOTPStatusResponse | ErrorResponse,
activate: async (code: string): Promise<TOTPStatusResponse | ErrorResponse> =>
await request('POST', apiURL.TOTP_AUTHENTICATOR, { code }) as TOTPStatusResponse | ErrorResponse,
deactivate: async (): Promise<AllauthResponse> =>
await request('DELETE', apiURL.TOTP_AUTHENTICATOR),
},
recoveryCodes: {
list: async (): Promise<RecoveryCodesResponse | ErrorResponse> =>
await request('GET', apiURL.RECOVERY_CODES) as RecoveryCodesResponse | ErrorResponse,
regenerate: async (): Promise<RecoveryCodesResponse | ErrorResponse> =>
await request('POST', apiURL.RECOVERY_CODES) as RecoveryCodesResponse | ErrorResponse,
}
},
webauthn: {
signup: async (name: string, credential: RegistrationCredential): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('PUT', apiURL.SIGNUP_WEBAUTHN, { name, credential }) as AuthenticatedResponse | ErrorResponse,
add: async (name: string, credential: RegistrationCredential): Promise<AllauthResponse> =>
await request('POST', apiURL.WEBAUTHN_AUTHENTICATOR, { name, credential }),
login: async (credential: AuthenticationCredential): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
await request('POST', apiURL.LOGIN_WEBAUTHN, { credential }) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
authenticate: async (credential: AuthenticationCredential): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('POST', apiURL.AUTHENTICATE_WEBAUTHN, { credential }) as AuthenticatedResponse | ErrorResponse,
reauthenticate: async (credential: AuthenticationCredential): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('POST', apiURL.REAUTHENTICATE_WEBAUTHN, { credential }) as AuthenticatedResponse | ErrorResponse,
update: async (id: number, data: Omit<WebAuthnUpdateRequest, 'id'>): Promise<AllauthResponse> =>
await request('PUT', apiURL.WEBAUTHN_AUTHENTICATOR, { id, ...data }),
delete: async (ids: number[]): Promise<AllauthResponse> =>
await request('DELETE', apiURL.WEBAUTHN_AUTHENTICATOR, { authenticators: ids }),
passkey: {
signup: async (email: string): Promise<AllauthResponse> =>
await request('POST', apiURL.SIGNUP_WEBAUTHN, { email }),
confirm: async (): Promise<AuthenticatedResponse | ErrorResponse> =>
await request('PUT', apiURL.SIGNUP_WEBAUTHN) as AuthenticatedResponse | ErrorResponse,
},
requestOptions: {
creation: async (passwordless: boolean): Promise<WebAuthnCreationOptionsResponse | ErrorResponse> =>
await request('GET', apiURL.WEBAUTHN_AUTHENTICATOR + (passwordless ? '?passwordless' : '')) as WebAuthnCreationOptionsResponse | ErrorResponse,
creationAtSignup: async (): Promise<WebAuthnCreationOptionsResponse | ErrorResponse> =>
await request('GET', apiURL.SIGNUP_WEBAUTHN) as WebAuthnCreationOptionsResponse | ErrorResponse,
login: async (): Promise<WebAuthnRequestOptionsResponse | ErrorResponse> =>
await request('GET', apiURL.LOGIN_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse,
authentication: async (): Promise<WebAuthnRequestOptionsResponse | ErrorResponse> =>
await request('GET', apiURL.AUTHENTICATE_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse,
reauthentication: async (): Promise<WebAuthnRequestOptionsResponse | ErrorResponse> =>
await request('GET', apiURL.REAUTHENTICATE_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse,
},
}
}
}
export type AllauthAPI = ReturnType<typeof createAPI>