UNPKG

spaps

Version:

Sweet Potato Authentication & Payment Service CLI - Docker Compose orchestrator for local Python/FastAPI SPAPS server with built-in admin middleware

1,532 lines 69.7 kB
const fs = require('node:fs');
const path = require('node:path');

const { DEFAULT_PORT } = require('./config');
const { provisionStarterApplication } = require('./local-runtime');
const {
  DSR_DEPENDENCIES,
  DSR_DEV_DEPENDENCIES,
  DSR_THEME,
  componentsJsonSource,
  designSystemManifestSource,
  reactStylesSource,
  themeAdapterSource,
  uiPrimitiveSources,
  utilsSource,
} = require('./starter-design-system');

const PASSKEY_BROWSER_SDK_RANGE = '^1.14.0';

function browserControllerSource() {
  return `import { PasskeyBrowserError } from 'spaps-sdk';
import { passkeyFlow, spaps } from './spaps';

let activeCeremony: AbortController | undefined;
let activeConditional: ReturnType<typeof passkeyFlow.createConditionalSignIn> | undefined;

async function runCeremony<T>(run: (signal: AbortSignal) => Promise<T>): Promise<T> {
  activeCeremony?.abort();
  const controller = new AbortController();
  activeCeremony = controller;
  try { return await run(controller.signal); }
  finally { if (activeCeremony === controller) activeCeremony = undefined; }
}

export function cancelPasskeyAction() { activeCeremony?.abort(); activeConditional?.abort(); }
export function bootstrapPasskeyAccount(email?: string, username?: string) { return runCeremony((signal) => passkeyFlow.bootstrap({ email: email || undefined, username: username || undefined, signal })); }
export function beginPasskeySignIn(email?: string) { return runCeremony((signal) => passkeyFlow.signIn({ email: email || undefined, signal })); }
export function startConditionalSignIn() {
  activeConditional?.abort();
  const controller = passkeyFlow.createConditionalSignIn();
  activeConditional = controller;
  return controller.start().finally(() => {
    controller.dispose();
    if (activeConditional === controller) activeConditional = undefined;
  });
}
export function enrollPasskey(label?: string) { return runCeremony((signal) => passkeyFlow.register({ label: label || undefined, signal })); }
export function requestRecovery(email: string) { return passkeyFlow.requestRecoveryEmail({ email }); }
export function verifyRecoveryProof(token: string, code?: string) { return passkeyFlow.verifyRecoveryEmail({ token, code: code || undefined }); }
export function registerRecoveryPasskey(recovery_grant: string, label?: string) { return runCeremony((signal) => passkeyFlow.registerRecoveryPasskey({ recovery_grant, label: label || undefined, signal })); }
export function completeRecoveryAssertion(recovery_grant: string) { return runCeremony((signal) => passkeyFlow.completeRecoveryAssertion({ recovery_grant, signal })); }
export function listCredentials() { return passkeyFlow.listCredentials(); }
export function renameCredential(id: string, label: string) { return passkeyFlow.renameCredential(id, label); }
export function revokeCredential(id: string) { return passkeyFlow.revokeCredential(id); }
export function completeMfa(challenge: { challenge_id: string; challenge: string }, code: string) { return spaps.auth.mfa.verify({ challenge_id: challenge.challenge_id, challenge: challenge.challenge, code }); }
export function stepUp(action: string) { return runCeremony((signal) => passkeyFlow.stepUp({ action, intent: { source: 'generated-reference-ui' }, signal })); }

export function handlePasskeyResult(result: unknown) {
  if (result && typeof result === 'object') {
    const value = result as Record<string, unknown>;
    if (value.mfa_required === true || value.status === 'mfa-required') return 'Complete the additional verification step.';
    if (value.status === 'fallback') return 'Passkey autofill is unavailable. Use explicit sign-in or another enabled method.';
    if (value.recovery_grant) return 'Recovery proof accepted. Continue every re-hardening step before treating the session as restored.';
    if (Array.isArray(value.credentials)) return 'Credential list refreshed.';
    if (value.step_up_grant) return 'Step-up verified. Send the bound grant only to the protected server action.';
  }
  return 'Passkey action complete.';
}
export function passkeyStatus(error: unknown) {
  if (error instanceof PasskeyBrowserError && error.code === 'PASSKEY_CANCELLED') return 'Passkey action cancelled.';
  if (error instanceof PasskeyBrowserError && error.code === 'PASSKEY_NOT_ALLOWED') return 'The browser or authenticator did not complete the passkey prompt. Try again and approve the browser dialog.';
  if (error instanceof PasskeyBrowserError && error.code === 'PASSKEY_UNSUPPORTED') return 'This browser does not support the required passkey APIs.';
  if (error instanceof PasskeyBrowserError && error.code === 'PASSKEY_INVALID_STATE') return 'That passkey is already registered or cannot be used in this state.';
  if (error && typeof error === 'object' && 'code' in error && error.code === 'LAST_AUTH_FACTOR') return 'Keep this passkey: add another primary factor before revoking it.';
  if (error instanceof Error && error.message) return \`Passkey action failed: \${error.message}\`;
  return 'Passkey action failed. Check the browser console for details.';
}
export const recoveryGuidance = 'Recovery stays restricted until a replacement passkey is registered and freshly asserted.';
`;
}

function authControllerSource() {
  return `import type { AuthMethodResponse, AuthResponse, User } from 'spaps-sdk';
import { TokenManager } from 'spaps-sdk';
import { spaps } from './spaps';

export function rememberAuth(result: AuthMethodResponse | unknown): User | undefined {
  if (!result || typeof result !== 'object' || (result as Record<string, unknown>).mfa_required === true) return undefined;
  const auth = result as Partial<AuthResponse>;
  if (!auth.access_token || !auth.refresh_token || !auth.user) return undefined;
  TokenManager.storeTokens(auth as AuthResponse);
  return auth.user;
}

export async function restoreSession(): Promise<User | undefined> {
  const restored = await TokenManager.autoRefreshToken(spaps);
  return restored ? spaps.auth.getCurrentUser() : undefined;
}

export async function signOut(): Promise<void> {
  await spaps.auth.logout();
  TokenManager.clearTokens();
}

export function errorMessage(error: unknown): string {
  return error instanceof Error && error.message ? error.message : 'The authentication request failed.';
}
`;
}

function onboardingControllerSource() {
  return `import { spapsApiUrl } from './spaps';

export type LoginMethodChoice = 'password' | 'passkeys';

export type ProvisionedApplication = {
  environment: 'development' | 'production';
  id: string;
  name: string;
  slug: string;
  publishable_key: string;
  secret_key: string;
  capability_graph: 'completed' | 'refresh_failed';
};

export type ProvisionedProject = {
  name: string;
  entitlement_key: string;
  login_methods: LoginMethodChoice[];
  applications: ProvisionedApplication[];
};

type Envelope<T> = { success?: boolean; data?: T; error?: { code?: string; message?: string } };

export function slugify(name: string): string {
  return name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 40);
}

export function isValidEntitlementKey(key: string): boolean {
  return /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(key);
}

async function selfServiceRequest<T>(path: string, token: string | undefined, body: unknown): Promise<T> {
  const response = await fetch(spapsApiUrl + path, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: 'Bearer ' + token } : {}),
    },
    body: JSON.stringify(body),
  });
  const payload = (await response.json().catch(() => undefined)) as Envelope<T> | undefined;
  if (!response.ok || payload?.success === false) {
    throw new Error(payload?.error?.message || 'Self-service request failed with HTTP ' + response.status);
  }
  return (payload && 'data' in payload ? payload.data : payload) as T;
}

// Mirrors the browser_auth blueprint scopes plus entitlements_read, and seeds an
// allow policy so the chosen entitlement is server-enforced from day one.
function entitlementBlueprint(entitlementKey: string, environment: string) {
  return {
    publishable_scopes: ['auth', 'webauthn_ceremonies', 'sessions', 'capability_decisions', 'marketing_events_ingest', 'docs', 'entitlements_read'],
    default_policies: [
      {
        name: 'access-' + entitlementKey.replace(/[^a-z0-9]+/g, '-'),
        description: 'Allow admins and users holding the ' + entitlementKey + ' entitlement into gated surfaces.',
        effect: 'allow',
        conditions: {
          any: [
            { has_role: { role: 'admin' } },
            { has_entitlement: { key: entitlementKey } },
          ],
        },
        priority: 100,
        metadata: { source: 'auth-reference-onboarding', environment },
      },
    ],
  };
}

export async function provisionEnvironmentPair(options: {
  name: string;
  password: string;
  entitlementKey: string;
  loginMethods: LoginMethodChoice[];
}): Promise<ProvisionedProject> {
  const baseSlug = slugify(options.name);
  if (!baseSlug) throw new Error('Application name must contain letters or numbers.');
  const auth = await selfServiceRequest<{ token: string }>('/api/self-service/auth', undefined, { password: options.password });

  // Application names are globally unique server-side, so the mirrored pair
  // carries the environment in the name while sharing the product name.
  const environments: Array<{ environment: 'development' | 'production'; slug: string; name: string }> = [
    { environment: 'development', slug: baseSlug + '-dev', name: options.name + ' (Development)' },
    { environment: 'production', slug: baseSlug, name: options.name + ' (Production)' },
  ];

  const applications: ProvisionedApplication[] = [];
  for (const target of environments) {
    const created = await selfServiceRequest<{
      application: { id: string; name: string; slug: string };
      publishable_key: string;
      secret_key: string;
    }>('/api/self-service/applications', auth.token, {
      name: target.name,
      slug: target.slug,
      description: 'Mirrored ' + target.environment + ' environment for ' + options.name + ' (auth reference onboarding).',
      allowed_origins: [window.location.origin],
      blueprint_key: 'browser_auth',
      blueprint: entitlementBlueprint(options.entitlementKey, target.environment),
    });

    // access.decide fails closed until the application has a completed
    // capability graph projection, so build it now while we hold the one-time
    // secret key. A refresh failure degrades to a documented follow-up, not a
    // failed wizard.
    let capabilityGraph: 'completed' | 'refresh_failed' = 'refresh_failed';
    try {
      const refresh = await fetch(spapsApiUrl + '/api/graph/refresh', {
        method: 'POST',
        headers: { 'X-API-Key': created.secret_key },
      });
      const refreshPayload = (await refresh.json().catch(() => undefined)) as
        | Envelope<{ status?: string }>
        | undefined;
      if (refresh.ok && refreshPayload?.data?.status === 'completed') capabilityGraph = 'completed';
    } catch {
      capabilityGraph = 'refresh_failed';
    }

    applications.push({
      environment: target.environment,
      id: created.application.id,
      name: created.application.name,
      slug: created.application.slug,
      publishable_key: created.publishable_key,
      secret_key: created.secret_key,
      capability_graph: capabilityGraph,
    });
  }

  return {
    name: options.name,
    entitlement_key: options.entitlementKey,
    login_methods: options.loginMethods,
    applications,
  };
}

export function buildAgentInstructions(project: ProvisionedProject, apiUrl: string): string {
  const dev = project.applications.find((app) => app.environment === 'development');
  const prod = project.applications.find((app) => app.environment === 'production');
  const methodLines = [
    project.login_methods.includes('password')
      ? '- Email & password: spaps.auth.register({ email, password }) to create accounts, spaps.auth.signInWithPassword to sign in.'
      : null,
    project.login_methods.includes('passkeys')
      ? '- Passkeys: createBrowserClient(publishableKey).auth.passkeys owns every ceremony. Before scaffolding run: npx spaps auth passkeys show, passkeys validate, passkeys doctor, passkeys test.'
      : null,
  ].filter((line): line is string => line !== null);

  const appLines = (label: string, app?: ProvisionedApplication) =>
    app
      ? [
          '### ' + label,
          '- application_id: ' + app.id,
          '- slug: ' + app.slug,
          '- publishable key (browser-safe): ' + app.publishable_key,
          '- secret key (server only, shown once — store in a secrets manager): ' + app.secret_key,
          '- capability graph projection: ' +
            (app.capability_graph === 'completed'
              ? 'completed (access.decide is ready)'
              : 'NOT built — run POST ' + apiUrl + '/api/graph/refresh with X-API-Key: <secret key> before any access.decide call'),
        ]
      : ['### ' + label, '- provisioning incomplete'];

  return [
    '# SPAPS integration handoff for ' + project.name,
    '',
    'You are an AI coding agent wiring an app to SPAPS (Sweet Potato Authentication & Payment Service).',
    'The applications and keys below already exist on the SPAPS server at ' + apiUrl + ' — do not create new ones.',
    '',
    '## Applications (same product, mirrored dev/prod environments)',
    ...appLines('Development', dev),
    ...appLines('Production', prod),
    '',
    'Use the development application for local work and the production application for the deployed app.',
    'They are the same product — one name with an environment suffix — but separate application IDs and keys, so promoting to production is an env-var swap.',
    '',
    '## Install',
    '- npm install spaps-sdk        # browser + server SDK',
    '- npm install --save-dev spaps # CLI: local server, passkey doctor, scaffolding',
    '',
    '## Environment variables',
    '- The browser bundle gets ONLY the publishable key (VITE_SPAPS_PUBLISHABLE_KEY / NEXT_PUBLIC_SPAPS_PUBLISHABLE_KEY).',
    '- Secret keys live in server-side env files only. Never ship a spaps_sec_ key to a browser.',
    '- Point SPAPS_API_URL / VITE_SPAPS_API_URL at ' + apiUrl + ' for development.',
    '',
    '## Login methods to implement',
    ...methodLines,
    '',
    '## Gated access (entitlement)',
    '- entitlement_key: ' + project.entitlement_key,
    '- Both applications carry an allow policy for users holding this entitlement (or the admin role).',
    "- Gate premium UI and routes with spaps.access.decide({ actor, action, resource, controls: { entitlement_key: '" + project.entitlement_key + "' } }).",
    "- Grant it server-side: POST /api/entitlements/manual with X-API-Key (secret) + an admin JWT, body { beneficiary_email or beneficiary_user_id, entitlement_key: '" + project.entitlement_key + "' }. Stripe entitlement mappings project it automatically on checkout. Clients can only read entitlements.",
    '- access.decide fails closed without a completed capability graph projection; after changing policies, entitlement mappings, or blueprints, re-run POST /api/graph/refresh (X-API-Key: secret key).',
    '',
    '## Verify',
    '- If the spaps MCP server or the spaps-integrate / sweet-potato-usage-audit skills are available in your environment, use them to route the integration and audit the result.',
    '- Otherwise: sign in with each login method above, then confirm spaps.access.decide denies before the entitlement is granted and allows after.',
  ].join('\\n');
}
`;
}

function reactAppSource() {
  return `import { useEffect, useState } from 'react';
import type { AccessDecisionResponse, SessionContext, User } from 'spaps-sdk';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field';
import { Input, Select, Textarea } from '@/components/ui/input';
import { ACTIVE_REGISTRY_ITEM, ACTIVE_REGISTRY_VERSION } from '@/design-system/theme-adapter';
import { errorMessage, rememberAuth, restoreSession, signOut } from '@/lib/auth-controller';
import type { LoginMethodChoice, ProvisionedProject } from '@/lib/onboarding-controller';
import { buildAgentInstructions, isValidEntitlementKey, provisionEnvironmentPair, slugify } from '@/lib/onboarding-controller';
import * as passkeys from '@/lib/passkey-controller';
import { spaps, spapsApiUrl } from '@/lib/spaps';

type Method = { method: string; enabled: boolean; config: Record<string, unknown> };
type Chain = 'solana' | 'ethereum' | 'base' | 'bitcoin';

function Stat({ label, value }: { label: string; value: string }) {
  return (
    <div className="flex flex-col gap-1 rounded-sm border border-border/60 px-3 py-2">
      <span className="text-[0.625rem] uppercase tracking-[0.2em] text-muted-foreground">{label}</span>
      <span className="text-sm text-foreground">{value}</span>
    </div>
  );
}

export default function App() {
  const [methods, setMethods] = useState<Method[]>([]);
  const [status, setStatus] = useState('Loading authentication capabilities…');
  const [busy, setBusy] = useState(false);
  const [user, setUser] = useState<User>();
  const [context, setContext] = useState<SessionContext>();
  const [decision, setDecision] = useState<AccessDecisionResponse>();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [magicToken, setMagicToken] = useState('');
  const [chain, setChain] = useState<Chain>('ethereum');
  const [walletAddress, setWalletAddress] = useState('');
  const [walletMessage, setWalletMessage] = useState('');
  const [walletSignature, setWalletSignature] = useState('');
  const [token, setToken] = useState('');
  const [grant, setGrant] = useState('');
  const [credentialId, setCredentialId] = useState('');
  const [label, setLabel] = useState('');
  const [mfaCode, setMfaCode] = useState('');
  const [mfa, setMfa] = useState<{ challenge_id: string; challenge: string }>();
  const [setupStep, setSetupStep] = useState(0);
  const [appName, setAppName] = useState('');
  const [selfServicePassword, setSelfServicePassword] = useState('');
  const [entitlementKey, setEntitlementKey] = useState('premium.access');
  const [loginMethods, setLoginMethods] = useState<LoginMethodChoice[]>(['password', 'passkeys']);
  const [project, setProject] = useState<ProvisionedProject>();
  const [copied, setCopied] = useState(false);

  const enabled = (name: string) => methods.some((method) => method.method === name && method.enabled);
  const toggleLoginMethod = (method: LoginMethodChoice) =>
    setLoginMethods((current) => (current.includes(method) ? current.filter((entry) => entry !== method) : [...current, method]));
  const instructions = project ? buildAgentInstructions(project, spapsApiUrl) : '';

  useEffect(() => {
    Promise.all([spaps.auth.getMethods(), restoreSession()])
      .then(async ([discovery, restored]) => {
        setMethods(discovery.methods);
        setUser(restored);
        if (restored) setContext(await spaps.auth.getSessionContext());
        setStatus(restored ? 'Session restored.' : 'Choose an available authentication method.');
      })
      .catch((error) => setStatus(errorMessage(error)));
  }, []);

  const run = async (name: string, action: () => Promise<unknown>, isPasskey = false) => {
    setBusy(true);
    setStatus(isPasskey ? 'Waiting for the browser passkey prompt…' : name + '…');
    try {
      const result = await action();
      const authenticated = rememberAuth(result);
      if (authenticated) {
        setUser(authenticated);
        setContext(await spaps.auth.getSessionContext());
      }
      const value = result as Record<string, unknown>;
      if (typeof value?.recovery_grant === 'string') setGrant(value.recovery_grant);
      if (value?.mfa_required === true) setMfa(value as unknown as { challenge_id: string; challenge: string });
      setStatus(isPasskey ? passkeys.handlePasskeyResult(result) : name + ' complete.');
      return result;
    } catch (error) {
      setStatus(isPasskey ? passkeys.passkeyStatus(error) : name + ' failed: ' + errorMessage(error));
    } finally {
      setBusy(false);
    }
  };

  const requestWalletChallenge = async () => {
    const challenge = await spaps.auth.getNonce(walletAddress);
    setWalletMessage(String(challenge.message));
    return challenge;
  };
  const verifyWallet = () =>
    spaps.auth.signInWithWallet({ wallet_address: walletAddress, signature: walletSignature, message: walletMessage, chain_type: chain });
  const checkDeveloperAccess = () =>
    spaps.access.decide({
      actor: { actor_type: 'user', user_id: user?.id },
      action: 'developer.application.create',
      resource: { resource_type: 'developer_platform', resource_ref: 'application_provisioning' },
      controls: { entitlement_key: 'developer.application.create' },
    });

  return (
    <div className="relative min-h-svh">
      <div aria-hidden="true" className="vs-scanfield pointer-events-none fixed inset-0 opacity-50" />
      <main className="relative mx-auto flex w-full max-w-6xl flex-col gap-6 px-5 pb-24 pt-12">
        <header className="flex flex-col gap-3">
          <p className="vs-eyebrow">SPAPS browser integration</p>
          <h1 className="vs-glow font-display text-4xl font-semibold tracking-tight text-[var(--vs-text-bright)] sm:text-5xl">
            Authentication reference
          </h1>
          <p className="max-w-2xl text-sm leading-relaxed text-muted-foreground">
            Exercise every configured authentication contract from one browser-safe reference.
          </p>
          <div className="vs-rule mt-2" />
        </header>

        <section className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-card px-4 py-3">
          <div className="flex items-center gap-3">
            <span className={'size-2 rounded-full ' + (user ? 'bg-signal shadow-[0_0_8px_var(--vs-mesh-green)]' : 'bg-muted-foreground')} />
            <strong className="text-xs uppercase tracking-[0.16em]">
              {user ? 'Signed in as ' + (user.email || user.wallet_address || user.id) : 'Signed out'}
            </strong>
          </div>
          <Button
            size="sm"
            disabled={!user || busy}
            onClick={() =>
              run('Sign out', async () => {
                await signOut();
                setUser(undefined);
                setContext(undefined);
                setDecision(undefined);
              })
            }
          >
            Sign out
          </Button>
        </section>

        <p role="status" className="rounded-sm border border-primary/40 bg-signal-faint px-4 py-3 font-mono text-xs text-primary">
          {status}
        </p>

        {user && (
          <Card>
            <CardHeader>
              <CardTitle>Developer platform access</CardTitle>
              <CardDescription>
                This evaluates <code className="text-primary">developer.application.create</code>. Actual application provisioning and one-time
                secret-key reveal remain privileged server operations.
              </CardDescription>
            </CardHeader>
            <CardContent>
              <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
                <Stat label="Tier" value={context?.tier || 'default'} />
                <Stat label="Entitlements" value={String(context?.entitlements_count ?? '—')} />
                <Stat label="Wallets" value={String(context?.wallets.length ?? '—')} />
                <Stat label="MFA" value={context?.mfa_enrolled ? 'enrolled' : 'not enrolled'} />
              </div>
              <div className="flex flex-wrap gap-2">
                {context?.entitlements.length ? (
                  context.entitlements.map((entitlement) => (
                    <Badge key={entitlement.id} variant="focal">
                      {entitlement.entitlement_key}
                    </Badge>
                  ))
                ) : (
                  <span className="text-xs text-muted-foreground">No active entitlements for this application.</span>
                )}
              </div>
              {decision && (
                <p className={'font-mono text-xs ' + (decision.allowed ? 'text-signal' : 'text-destructive')}>
                  <strong>{decision.allowed ? 'Allowed' : 'Denied'}:</strong> {decision.outcome}.{' '}
                  {decision.reasons.map((reason) => reason.message).join(' ')}
                </p>
              )}
            </CardContent>
            <CardFooter>
              <Button
                variant="default"
                disabled={busy}
                onClick={() =>
                  run('Developer access check', async () => {
                    const result = await checkDeveloperAccess();
                    setDecision(result);
                    return result;
                  })
                }
              >
                Check access
              </Button>
            </CardFooter>
          </Card>
        )}

        {user && (
          <Card className="border-primary/60">
            <CardHeader>
              <CardTitle>Set up your application</CardTitle>
              <CardDescription>
                Three questions, then SPAPS provisions mirrored development and production applications and writes the handoff for your coding
                agent.
              </CardDescription>
            </CardHeader>
            <CardContent>
              {setupStep === 0 && (
                <FieldGroup>
                  <Field>
                    <FieldLabel htmlFor="setup-app-name">Application name</FieldLabel>
                    <Input id="setup-app-name" value={appName} onChange={(event) => setAppName(event.target.value)} />
                    {slugify(appName) && (
                      <p className="text-xs text-muted-foreground">
                        Creates two mirrored apps: <code className="text-primary">{slugify(appName)}-dev</code> and{' '}
                        <code className="text-primary">{slugify(appName)}</code> — one product, separate application IDs and keys, so you can
                        test in development and promote with an env swap.
                      </p>
                    )}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="setup-password">Self-service password</FieldLabel>
                    <Input
                      id="setup-password"
                      type="password"
                      value={selfServicePassword}
                      onChange={(event) => setSelfServicePassword(event.target.value)}
                    />
                    <p className="text-xs text-muted-foreground">
                      SELF_SERVICE_PASSWORD from your SPAPS server environment. Provisioning applications is a privileged operation.
                    </p>
                  </Field>
                </FieldGroup>
              )}
              {setupStep === 1 && (
                <FieldGroup>
                  <Field>
                    <FieldLabel htmlFor="setup-entitlement">Entitlement key</FieldLabel>
                    <Input id="setup-entitlement" value={entitlementKey} onChange={(event) => setEntitlementKey(event.target.value)} />
                    <p className="text-xs text-muted-foreground">
                      An entitlement is gated access a user can hold in your app — for example{' '}
                      <code className="text-primary">premium.access</code> unlocking paid features. Both applications are created with a policy
                      that allows users holding this key (lowercase letters, numbers, and . _ - separators).
                    </p>
                  </Field>
                </FieldGroup>
              )}
              {setupStep === 2 && (
                <FieldGroup>
                  {(
                    [
                      ['password', 'Email & password'],
                      ['passkeys', 'Passkeys'],
                    ] as Array<[LoginMethodChoice, string]>
                  ).map(([method, methodLabel]) => (
                    <label key={method} className="flex items-center gap-2 text-sm text-foreground">
                      <input
                        type="checkbox"
                        className="size-4 accent-primary"
                        checked={loginMethods.includes(method)}
                        onChange={() => toggleLoginMethod(method)}
                      />
                      {methodLabel}
                    </label>
                  ))}
                  <p className="text-xs text-muted-foreground">
                    Email & password or passkeys is plenty to start. Every other method on this page stays available once you integrate.
                  </p>
                </FieldGroup>
              )}
              {setupStep === 3 && project && (
                <div className="flex flex-col gap-3">
                  <div className="grid gap-2 sm:grid-cols-2">
                    {project.applications.map((app) => (
                      <Stat key={app.id} label={app.environment} value={app.slug + ' · ' + app.id} />
                    ))}
                  </div>
                  <Field>
                    <FieldLabel htmlFor="agent-handoff">Copy this to your coding agent</FieldLabel>
                    <Textarea id="agent-handoff" readOnly rows={16} className="font-mono text-xs" value={instructions} />
                  </Field>
                  <p className="text-xs text-muted-foreground">
                    Secret keys appear once — store them in a secrets manager. Paste the block into Claude (or any coding agent) and it has
                    everything: keys, packages, login methods, and the entitlement gate.
                  </p>
                </div>
              )}
            </CardContent>
            <CardFooter>
              {setupStep > 0 && setupStep < 3 && (
                <Button variant="ghost" disabled={busy} onClick={() => setSetupStep(setupStep - 1)}>
                  Back
                </Button>
              )}
              {setupStep === 0 && (
                <Button variant="default" disabled={!slugify(appName) || !selfServicePassword || busy} onClick={() => setSetupStep(1)}>
                  Next: entitlement
                </Button>
              )}
              {setupStep === 1 && (
                <Button variant="default" disabled={!isValidEntitlementKey(entitlementKey) || busy} onClick={() => setSetupStep(2)}>
                  Next: login methods
                </Button>
              )}
              {setupStep === 2 && (
                <Button
                  variant="default"
                  disabled={loginMethods.length === 0 || busy}
                  onClick={() =>
                    run('Application provisioning', async () => {
                      const result = await provisionEnvironmentPair({
                        name: appName,
                        password: selfServicePassword,
                        entitlementKey,
                        loginMethods,
                      });
                      setProject(result);
                      setSetupStep(3);
                      return result;
                    })
                  }
                >
                  Create development + production apps
                </Button>
              )}
              {setupStep === 3 && (
                <>
                  <Button
                    variant="default"
                    disabled={!instructions}
                    onClick={() => navigator.clipboard.writeText(instructions).then(() => setCopied(true))}
                  >
                    {copied ? 'Copied' : 'Copy agent instructions'}
                  </Button>
                  <Button
                    variant="ghost"
                    onClick={() => {
                      setSetupStep(0);
                      setProject(undefined);
                      setCopied(false);
                    }}
                  >
                    Start over
                  </Button>
                </>
              )}
            </CardFooter>
          </Card>
        )}

        <nav aria-label="Discovered authentication methods" className="flex flex-wrap gap-2">
          {methods.map((method) => (
            <Badge key={method.method} variant={method.enabled ? 'default' : 'muted'}>
              {method.method}
            </Badge>
          ))}
        </nav>

        <section className="grid gap-4 md:grid-cols-2">
          <Card>
            <CardHeader>
              <CardTitle>Email & password</CardTitle>
            </CardHeader>
            <CardContent>
              <FieldGroup>
                <Field>
                  <FieldLabel htmlFor="pw-email">Email</FieldLabel>
                  <Input id="pw-email" type="email" autoComplete="email" value={email} onChange={(event) => setEmail(event.target.value)} />
                </Field>
                <Field>
                  <FieldLabel htmlFor="pw-password">Password</FieldLabel>
                  <Input id="pw-password" type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
                </Field>
              </FieldGroup>
            </CardContent>
            <CardFooter>
              <Button variant="default" disabled={!enabled('password') || busy} onClick={() => run('Create account', () => spaps.auth.register({ email, password }))}>
                Create account
              </Button>
              <Button disabled={!enabled('password') || busy} onClick={() => run('Password sign-in', () => spaps.auth.signInWithPassword({ email, password }))}>
                Sign in
              </Button>
            </CardFooter>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>Magic link</CardTitle>
            </CardHeader>
            <CardContent>
              <FieldGroup>
                <Field>
                  <FieldLabel htmlFor="magic-email">Email</FieldLabel>
                  <Input id="magic-email" type="email" value={email} onChange={(event) => setEmail(event.target.value)} />
                </Field>
                <Field>
                  <FieldLabel htmlFor="magic-token">Token from link</FieldLabel>
                  <Input id="magic-token" value={magicToken} onChange={(event) => setMagicToken(event.target.value)} />
                </Field>
              </FieldGroup>
            </CardContent>
            <CardFooter>
              <Button
                variant="default"
                disabled={!enabled('magic_link') || busy}
                onClick={() => run('Magic link request', () => spaps.auth.requestMagicLink({ email, redirect_url: window.location.origin }))}
              >
                Send magic link
              </Button>
              <Button disabled={!magicToken || busy} onClick={() => run('Magic link verification', () => spaps.auth.verifyMagicLink({ token: magicToken }))}>
                Verify magic link
              </Button>
            </CardFooter>
          </Card>

          <Card className="border-primary/60 shadow-elevated md:col-span-2">
            <CardHeader>
              <CardTitle className="text-primary">Passkeys</CardTitle>
              <CardDescription>The browser owns every ceremony. This page only transports the SDK result.</CardDescription>
            </CardHeader>
            <CardContent>
              <FieldGroup className="sm:flex-row sm:gap-4">
                <Field className="flex-1">
                  <FieldLabel htmlFor="passkey-email">Email</FieldLabel>
                  <Input id="passkey-email" type="email" autoComplete="username webauthn" value={email} onChange={(event) => setEmail(event.target.value)} />
                </Field>
                <Field className="flex-1">
                  <FieldLabel htmlFor="passkey-label">Passkey label</FieldLabel>
                  <Input id="passkey-label" value={label} onChange={(event) => setLabel(event.target.value)} />
                </Field>
              </FieldGroup>
            </CardContent>
            <CardFooter>
              <Button variant="default" disabled={!enabled('webauthn') || busy} onClick={() => run('Create passkey account', () => passkeys.bootstrapPasskeyAccount(email), true)}>
                Create account with passkey
              </Button>
              <Button disabled={!enabled('webauthn') || busy} onClick={() => run('Passkey sign-in', () => passkeys.beginPasskeySignIn(email), true)}>
                Sign in with passkey
              </Button>
              <Button disabled={!enabled('webauthn') || busy} onClick={() => run('Passkey autofill', () => passkeys.startConditionalSignIn(), true)}>
                Try passkey autofill
              </Button>
              <Button disabled={!user || busy} onClick={() => run('Passkey enrollment', () => passkeys.enrollPasskey(label), true)}>
                Enroll passkey
              </Button>
              <Button variant="ghost" onClick={passkeys.cancelPasskeyAction}>
                Cancel passkey action
              </Button>
            </CardFooter>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>SMS code</CardTitle>
              <CardDescription>SMS endpoints require a secret credential and must be called through your application server.</CardDescription>
            </CardHeader>
            <CardFooter>
              <Button disabled>Server adapter required</Button>
            </CardFooter>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>OIDC providers</CardTitle>
            </CardHeader>
            <CardContent>
              {['google', 'apple', 'github'].map((provider) => (
                <p key={provider} className="text-xs leading-relaxed text-muted-foreground">
                  <Badge variant={enabled('oidc:' + provider) ? 'focal' : 'muted'}>{provider}</Badge>{' '}
                  {enabled('oidc:' + provider)
                    ? 'Configured; connect the provider browser SDK with a nonce-bound ID token.'
                    : 'No provider credentials configured.'}
                </p>
              ))}
            </CardContent>
          </Card>

          <Card className="md:col-span-2">
            <CardHeader>
              <CardTitle>Wallet authentication</CardTitle>
              <CardDescription>Sign the exact challenge message in your wallet, then paste the signature back.</CardDescription>
            </CardHeader>
            <CardContent>
              <FieldGroup className="sm:flex-row sm:gap-4">
                <Field className="sm:w-48">
                  <FieldLabel htmlFor="wallet-chain">Chain</FieldLabel>
                  <Select id="wallet-chain" value={chain} onChange={(event) => setChain(event.target.value as Chain)}>
                    <option value="ethereum">Ethereum</option>
                    <option value="base">Base</option>
                    <option value="solana">Solana</option>
                    <option value="bitcoin">Bitcoin</option>
                  </Select>
                </Field>
                <Field className="flex-1">
                  <FieldLabel htmlFor="wallet-address">Wallet address</FieldLabel>
                  <Input id="wallet-address" value={walletAddress} onChange={(event) => setWalletAddress(event.target.value)} />
                </Field>
              </FieldGroup>
              <FieldGroup className="md:flex-row md:gap-4">
                <Field className="flex-1">
                  <FieldLabel htmlFor="wallet-message">Exact message</FieldLabel>
                  <Textarea id="wallet-message" readOnly value={walletMessage} />
                </Field>
                <Field className="flex-1">
                  <FieldLabel htmlFor="wallet-signature">Signature</FieldLabel>
                  <Textarea id="wallet-signature" value={walletSignature} onChange={(event) => setWalletSignature(event.target.value)} />
                </Field>
              </FieldGroup>
            </CardContent>
            <CardFooter>
              <Button variant="default" disabled={!walletAddress || busy} onClick={() => run('Wallet challenge request', requestWalletChallenge)}>
                Request challenge
              </Button>
              <Button disabled={!walletMessage || !walletSignature || busy} onClick={() => run('Wallet sign-in', verifyWallet)}>
                Verify signature
              </Button>
            </CardFooter>
          </Card>
        </section>

        <details className="group rounded-md border border-border bg-card/60">
          <summary className="cursor-pointer list-none px-4 py-3 text-xs uppercase tracking-[0.18em] text-muted-foreground hover:text-foreground">
            Advanced security and recovery
          </summary>
          <div className="grid gap-4 border-t border-border p-4 md:grid-cols-3">
            <Card className="bg-transparent shadow-none">
              <CardHeader>
                <CardTitle>Recovery</CardTitle>
              </CardHeader>
              <CardContent>
                <Field>
                  <FieldLabel htmlFor="recovery-token">Recovery token</FieldLabel>
                  <Input id="recovery-token" value={token} onChange={(event) => setToken(event.target.value)} />
                </Field>
              </CardContent>
              <CardFooter>
                <Button size="sm" onClick={() => run('Recovery request', () => passkeys.requestRecovery(email), true)}>
                  Request recovery email
                </Button>
                <Button size="sm" onClick={() => run('Recovery verification', () => passkeys.verifyRecoveryProof(token), true)}>
                  Verify recovery proof
                </Button>
                <Button size="sm" onClick={() => run('Replacement registration', () => passkeys.registerRecoveryPasskey(grant, label), true)}>
                  Register replacement passkey
                </Button>
                <Button size="sm" onClick={() => run('Recovery assertion', () => passkeys.completeRecoveryAssertion(grant), true)}>
                  Complete recovery assertion
                </Button>
              </CardFooter>
            </Card>

            <Card className="bg-transparent shadow-none">
              <CardHeader>
                <CardTitle>Credentials</CardTitle>
              </CardHeader>
              <CardContent>
                <Field>
                  <FieldLabel htmlFor="credential-id">Credential ID</FieldLabel>
                  <Input id="credential-id" value={credentialId} onChange={(event) => setCredentialId(event.target.value)} />
                </Field>
              </CardContent>
              <CardFooter>
                <Button size="sm" onClick={() => run('Credential list', passkeys.listCredentials, true)}>
                  List credentials
                </Button>
                <Button size="sm" onClick={() => run('Credential rename', () => passkeys.renameCredential(credentialId, label), true)}>
                  Rename credential
                </Button>
                <Button size="sm" variant="destructive" onClick={() => run('Credential revoke', () => passkeys.revokeCredential(credentialId), true)}>
                  Revoke credential
                </Button>
              </CardFooter>
            </Card>

            <Card className="bg-transparent shadow-none">
              <CardHeader>
                <CardTitle>MFA and step-up</CardTitle>
              </CardHeader>
              <CardContent>
                <Field>
                  <FieldLabel htmlFor="mfa-code">MFA code</FieldLabel>
                  <Input id="mfa-code" inputMode="numeric" value={mfaCode} onChange={(event) => setMfaCode(event.target.value)} />
                </Field>
              </CardContent>
              <CardFooter>
                <Button size="sm" disabled={!mfa} onClick={() => mfa && run('MFA verification', () => passkeys.completeMfa(mfa, mfaCode), true)}>
                  Verify MFA
                </Button>
                <Button size="sm" onClick={() => run('Step-up', () => passkeys.stepUp('account.security.review'), true)}>
                  Run step-up
                </Button>
              </CardFooter>
            </Card>
          </div>
        </details>

        <footer className="flex flex-col gap-2 pt-2">
          <div className="vs-rule" />
          <p className="text-xs leading-relaxed text-muted-foreground">{passkeys.recoveryGuidance}</p>
          <p className="font-mono text-[0.625rem] uppercase tracking-[0.18em] text-muted-foreground/70">
            Design System Registry · {ACTIVE_REGISTRY_ITEM}@{ACTIVE_REGISTRY_VERSION}
          </p>
        </footer>
      </main>
    </div>
  );
}
`;
}

function nextPageSource() {
  return `'use client';
import { useState } from 'react';
import * as passkeys from '../lib/passkey-controller';
export default function Page() {
  const [status, setStatus] = useState('Ready for a passkey.'); const [email, setEmail] = useState(''); const [token, setToken] = useState(''); const [grant, setGrant] = useState(''); const [credentialId, setCredentialId] = useState(''); const [label, setLabel] = useState(''); const [mfaCode, setMfaCode] = useState(''); const [mfa, setMfa] = useState<{ challenge_id: string; challenge: string }>();
  const run = (action: () => Promise<unknown>) => { setStatus('Waiting for the browser passkey prompt…'); return action().then((result) => { const value = result as Record<string, unknown>; const nested = value?.status === 'mfa-required' ? value.result as Record<string, unknown> : value; if (typeof value?.recovery_grant === 'string') setGrant(value.recovery_grant); if (nested?.mfa_required === true) setMfa(nested as unknown as { challenge_id: string; challenge: string }); setStatus(passkeys.handlePasskeyResult(result)); }).catch((error) => setStatus(passkeys.passkeyStatus(error))); };
  return <main><h1>Passkey reference app</h1><p role="status">{status}</p><label>Email<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /></label><label>Passkey label<input value={label} onChange={(e) => setLabel(e.target.value)} /></label><button onClick={() => run(() => passkeys.bootstrapPasskeyAccount(email))}>Create account with passkey</button><button onClick={() => run(() => passkeys.startConditionalSignIn())}>Try passkey autofill</button><button onClick={() => run(() => passkeys.beginPasskeySignIn(email))}>Sign in with passkey</button><button onClick={() => run(() => passkeys.enrollPasskey(label))}>Enroll passkey</button><button onClick={passkeys.cancelPasskeyAction}>Cancel passkey action</button><fieldset><legend>Recovery</legend><button onClick={() => run(() => passkeys.requestRecovery(email))}>Request recovery email</button><label>Recovery token<input value={token} onChange={(e) => setToken(e.target.value)} /></label><button onClick={() => run(() => passkeys.verifyRecoveryProof(token))}>Verify recovery proof</button><button onClick={() => run(() => passkeys.registerRecoveryPasskey(grant, label))}>Register replacement passkey</button><button onClick={() => run(() => passkeys.completeRecoveryAssertion(grant))}>Complete recovery assertion</button></fieldset><fieldset><legend>Credential management</legend><button onClick={() => run(passkeys.listCredentials)}>List credentials</button><label>Credential ID<input value={credentialId} onChange={(e) => setCredentialId(e.target.value)} /></label><button onClick={() => run(() => passkeys.renameCredential(credentialId, label))}>Rename credential</button><button onClick={() => run(() => passkeys.revokeCredential(credentialId))}>Revoke credential</button></fieldset><label>MFA code<input inputMode="numeric" value={mfaCode} onChange={(e) => setMfaCode(e.target.value)} /></label><button disabled={!mfa} onClick={() => mfa && run(() => passkeys.completeMfa(mfa, mfaCode))}>Verify MFA</button><button onClick={() => run(() => passkeys.stepUp('account.security.review'))}>Run step-up</button><p>{passkeys.recoveryGuidance}</p></main>;
}
`;
}

function tsconfigSource(template) {
  const compilerOptions = { target: 'ES2022', lib: ['DOM', 'DOM.Iterable', 'ESNext'], strict: true, module: 'ESNext', moduleResolution: 'Bundler', jsx: 'react-jsx', noEmit: true, isolatedModules: true, esModuleInterop: true, types: ['node'] };
  const config = template === 'nextjs'
    ? { compilerOptions: { ...compilerOptions, allowJs: true, skipLibCheck: true, incremental: true, resolveJsonModule: true, plugins: [{ name: 'next' }] }, include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts', '.next/dev/types/**/*.ts'], exclude: ['node_modules'] }
    : {
        compilerOptions: { ...compilerOptions, baseUrl: '.', paths: { '@/*': ['./src/*'] } },
        include: ['src/**/*.ts', 'src/**/*.tsx', 'vite-env.d.ts', 'vite.config.ts'],
      };
  return JSON.stringify(config, null, 2) + '\n';
}

function validateBrowserConfig(apiUrl, expectedOrigin, actualOrigin = expectedOrigin) {
  if (typeof expectedOrigin !== 'string' || !expectedOrigin.trim()) throw new Error('SPAPS browser origin is required');
  if (typeof actualOrigin !== 'string' || !actualOrigin.trim()) throw new Error('Runtime browser origin is required');
  const api = new URL(apiUrl); const expected = new URL(expectedOrigin); const actual = new URL(actualOrigin);
  // Plain HTTP is allowed only for development hosts that are not routable on
  // the public internet: loopback, RFC1918 private ranges, and the CGNAT range
  // (100.64.0.0/10) that Tailscale and similar mesh VPNs assign. Anything else
  // must use HTTPS so publishable keys and tokens are never sent in the clear.
  const localHost = (hostname) =>
    hostname === 'localhost' ||
    hostname.endsWith('.localhost') ||
    hostname === '::1' ||
    hostname === '[::1]' ||
    /^127\./.test(hostname) ||
    /^10\./.test(hostname) ||
    /^192\.168\./.test(hostname) ||
    /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
    /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(hostname);
  if (api.protocol !== 'https:' && !(api.protocol === 'http:' && localHost(api.hostname))) throw new Error('SPAPS API URL must use HTTPS except local development hosts');
  if (expected.origin !== actual.origin) throw new Error('Configured browser origin must exactly match window.location.origin');
  if (expected.protocol !== 'https:' && !(expected.protocol === 'http:' && localHost(expected.hostname))) throw new Error('Browser origin must use HTTPS except local development hosts');
  return { api, origin: expected.origin };
}

function browserConfigSource() {
  const typedSource = validateBrowserConfig
    .toString()
    .replace(
      'function validateBrowserConfig(apiUrl, expectedOrigin, actualOrigin = expectedOrigin)',
      'function validateBrowserConfig(apiUrl: string, expectedOrigin: string, actualOrigin: string = expectedOrigin)'
    )
    .replace(
      'const localHost = (hostname) =>',
      'const localHost = (hostname: string) =>'
    );
  return `export ${typedSource}`;
}

const SUPPORTED_TEMPLATES = {
  nextjs: {
    label: 'Next.js starter',
    blueprintKey: 'browser_auth',
    allowedOrigins: ['http://localhost:3000'],
    publicApiUrlEnv: 'NEXT_PUBLIC_SPAPS_API_URL',
    publicApiKeyEnv: 'NEXT_PUBLIC_SPAPS_PUBLISHABLE_KEY',
    files: ({ apiUrl }) => ({
      'lib/spaps.ts': `import { createBrowserClient } from 'spaps-sdk';
${browserConfigSource()}

const apiUrl = process.env.NEXT_PUBLIC_SPAPS_API_URL || '${apiUrl}';
const apiKey = process.env.NEXT_PUBLIC_SPAPS_PUBLISHABLE_KEY;
const browserOrigin = process.env.NEXT_PUBLIC_SPAPS_BROWSER_ORIGIN;

if (!apiKey || !apiKey.startsWith('spaps_pub_')) throw new Error('NEXT_PUBLIC_SPAPS_PUBLISHABLE_KEY must be a spaps_pub_ key');
if (!browserOrigin) throw new Error('NEXT_PUBLIC_SPAPS_BROWSER_ORIGIN is required');
validateBrowserConfig(apiUrl, browserOrigin, typeof window === 'undefined' ? browserOrigin : window.location.origin);

export const spaps = createBrowserClient(apiKey, { apiUrl });
export const passkeyFlow = spaps.auth.passkeys;
`,
      'lib/passkey-controller.ts': browserControllerSource(),
      'app/page.tsx': nextPageSource(),
      'app/layout.tsx': "import { PropsWithChildren } from 'react';\nexport default function RootLayout({ children }: PropsWithChildren) { return <html lang=\"en\"><body>{children}</body></html>; }\n",
      'tsconfig.json': tsconfigSource('nextjs'),
      'next-env.d.ts': '/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n',
      'app/providers.tsx': `'use client';

import { PropsWithChildren } from 'react';
import { spaps } from '../lib/spaps';

export function Providers({ children }: PropsWithChildren) {
  void spaps;
  return children;
}
`,
    }),
  },
  react: {
    label: 'React + Vite starter',
    blueprintKey: 'browser_auth',
    allowedOrigins: ['http://localhost:5173'],
    publicApiUrlEnv: 'VITE_SPAPS_API_URL',
    publicApiKeyEnv: 'VITE_SPAPS_PUBLISHABLE_KEY',
    files: ({ apiUrl, installedAt }) => ({
      'src/lib/spaps.ts': `import { createBrowserClient } from 'spaps-sdk';
${browserConfigSource()}

const apiUrl = import.meta.env.VITE_SPAPS_API_URL || '${apiUrl}';
const apiKey = import.meta.env.VITE_SPAPS_PUBLISHABLE_KEY;
const browserOrigin = import.meta.env.VITE_SPAPS_BROWSER_ORIGIN;

if (!apiKey || !apiKey.startsWith('spaps_pub_')) throw new Error('VITE_SPAPS_PUBLISHABLE_KEY must be a spaps_pub_ key');
if (!browserOrigin) throw new Error('VITE_SPAPS_BROWSER_ORIGIN is required');
validateBrowserConfig(apiUrl, browserOrigin, typeof window === 'undefined' ? browserOrigin : window.location.origin);

export const spapsApiUrl = apiUrl;
export const spaps = createBrowserClient(apiKey, { apiUrl });
export const passkeyFlow = spaps.auth.passkeys;
`,
      'src/lib/auth-controller.ts': authControllerSource(),
      'src/lib/onboarding-controller.ts': onboardingControllerSource(),
      'src/lib/passkey-controller.ts': browserControllerSource(),
      'src/lib/utils.ts': utilsSource(),
      'src/design-system/theme-adapter.ts': themeAdapterSource(),
      ...uiPrimitiveSources(),
      'src/App.tsx': reactAppSource(),
      'src/main.tsx': "import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport App from './App';\nimport './app.css';\ncreateRoot(document.getElementById('root')!).render(<React.StrictMode><App /></React.StrictMode>);\n",
      'src/app.css': reactStylesSource(),
      'components.json': componentsJsonSource(),
      'design-system.registry.json': designSystemManifestSource({ installedAt }),
      'index.html': `<!doctype html>
<html lang="en" class="dark">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>SPAPS authentication reference</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
`,
      'tsconfig.json': tsconfigSource('react'),
      'vite.config.ts': `import { fileURLToPath, URL } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
});
`,
      'vite-env.d.ts': '/// <reference types="vite/client" />\n',
    }),
  },
  node: {
    label: 'Node.js starter',
    blueprintKey: 'default',
    allowedOrigins: [],
    publicApiUrlEnv: 'SPAPS_API_URL',
    publicApiKeyEnv: 'SPAPS_API_KEY',
    files: ({ apiUrl }) => ({
      'src/spaps.js': `const { createServerClient, SPAPSClient } = require('spaps-sdk');

const apiUrl = process.env.SPAPS_API_URL || '${apiUrl}';
const apiKey = process.env.SPAPS_API_KEY;

const spaps = apiKey
  ? createServerClient(apiKey, { apiUrl })
  : new SPAPSClient({ apiUrl });

module.exports = { spaps };
`,
    }),
  },
  vanilla: {
    label: 'Vanilla JavaScript starter',
    blueprintKey: 'browser_auth',
    allowedOrigins: ['http://localhost:8080'],
    publicApiUrlEnv: 'SPAPS_API_URL',
    publicApiKeyEnv: 'SPAPS_API_KEY',
    files: ({ apiUrl }) => ({
      'src/spaps.js': `import { createBrowserClient, SPAPSClient } from 'spaps-sdk';

const apiUrl = window.SPAPS_API_URL || '${apiUrl}';
const apiKey = window.SPAPS_API_KEY;

export const spaps = apiKey && apiKey.startsWith('spaps_pub_')
  ? createBrowserClient(apiKey, { apiUrl })
  : new SPAPSClient({
      apiUrl,
      ...(apiKey ? { apiKey } : {}),
    });
`,
    }),
  },
};

function createCliError(code, message) {
  const error = new Error(message);
  error.code = code;
  return error;
}

function slugifyProjectName(name) {
  return String(name)
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '');
}

function ensureSupportedTemplate(template) {
  if (!template) {
    throw createCliError(
      'EINVAL',
      `The --template flag is required. Supported templates: ${Object.keys(SUPPORTED_TEMPLATES).join(', ')}.`
    );
  }

  if (!SUPPORTED_TEMPLATES[template]) {
    throw createCliError(
      'EINVAL',
      `Unsupported template "${template}". Supported templates: ${Object.keys(SUPPORTED_TEMPLATES).join(', ')}.`
    );
  }
}

function ensureWritableTarget(targetDir, force) {
  if (!fs.existsSync(targetDir)) {
    return;
  }

  const entries = fs.readdirSync(targetDir);
  if (entries.length > 0 && !force) {
    throw createCliError(
      'EEXIST',
      `Target directory "${targetDir}" is not empty. Re-run with --force to overwrite managed files.`
    );
  }
}

function prepareWritableTarget(targetDir, force) {
  ensureWritableTarget(targetDir, force);
  fs.mkdirSync(targetDir, { recursive: true });
  fs.accessSync(targetDir, fs.constants.W_OK);
}

function writeManagedFile(targetDir, relativePath, content, bookkeeping) {
  const fullPath = path.join(targetDir, relativePath);
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });

  if (fs.existsSync(fullPath)) {
    bookkeeping.files_overwritten.push(relativePath);
  } else {
    bookkeeping.files_created.push(relativePath);
  }

  fs.writeFileSync(fullPath, content);
}

function buildPackageJson(name, template) {
  const pkg = {
    name,
    private: true,
    version: '0.0.0',
    description: `SPAPS ${SUPPORTED_TEMPLATES[template].label.toLowerCase()} for ${name}`,
    dependencies: {
      'spaps-sdk': template === 'react' || template === 'nextjs'
        ? PASSKEY_BROWSER_SDK_RANGE
        : 'latest',
    },
  };

  if (template === 'react') {
    pkg.scripts = { dev: 'vite', build: 'tsc -b && vite build', typecheck: 'tsc --noEmit', 'test:passkeys': 'spaps auth passkeys test --launch --json' };
    pkg.dependencies.react = '^19.2.0';
    pkg.dependencies['react-dom'] = '^19.2.0';
    // Design System Registry runtime: the starter renders registry-owned tokens
    // through shadcn primitives. See design-system.registry.json.
    Object.assign(pkg.dependencies, DSR_DEPENDENCIES);
    pkg.devDependencies = { '@types/node': '^22.0.0', '@types/react': '^19.2.0', '@types/react-dom': '^19.2.0', '@vitejs/plugin-react': '^5.1.0', spaps: '^0.10.0', typescript: '^5.9.2', vite: '^7.2.0', ...DSR_DEV_DEPENDENCIES };
  } else if (template === 'nextjs') {
    pkg.scripts = { dev: 'next dev', build: 'next build', typecheck: 'tsc --noEmit', 'test:passkeys': 'spaps auth passkeys test --launch --json' };
    pkg.dependencies.next = '^16.1.0';
    pkg.dependencies.react = '^19.2.0';
    pkg.dependencies['react-dom'] = '^19.2.0';
    pkg.devDependencies = { '@types/node': '^22.0.0', '@types/react': '^19.2.0', '@types/react-dom': '^19.2.0', spaps: '^0.10.0', typescript: '^5.9.2' };
  }

  if (template === 'node') {
    pkg.type = 'commonjs';
  }

  return `${JSON.stringify(pkg, null, 2)}\n`;
}

function buildContract({ name, slug, template, version, apiUrl, docsUrl, provisioning }) {
  const templateDef = SUPPORTED_TEMPLATES[template];
  const contract = {
    name,
    slug,
    template,
    created_with: `spaps@${version}`,
    spaps: {
      local: {
        api_url: apiUrl,
        docs_url: docsUrl,
        local_mode_active: provisioning.runtime?.local_mode?.active ?? null,
      },
      application: {
        id: provisioning.application?.id || null,
        slug,
        blueprint_key: templateDef.blueprintKey,
        allowed_origins: templateDef.allowedOrigins,
        provisioning_status: provisioning.status,
        provisioned_via:
          provisioning.status === 'provisioned'
            ? 'self_service'
            : provisioning.status === 'local_mode'
              ? 'local_mode'
              : null,
      },
    },
  };

  return `${JSON.stringify(contract, null, 2)}\n`;
}

function buildEnvFile(template, apiUrl, provisioning) {
  const templateDef = SUPPORTED_TEMPLATES[template];
  const lines = [
    '# SPAPS local development',
    `SPAPS_API_URL=${apiUrl}`,
  ];

  if (templateDef.publicApiUrlEnv !== 'SPAPS_API_URL') {
    lines.push(`${templateDef.publicApiUrlEnv}=${apiUrl}`);
  }
  if (template === 'react') lines.push('VITE_SPAPS_BROWSER_ORIGIN=http://localhost:5173');
  if (template === 'nextjs') lines.push('NEXT_PUBLIC_SPAPS_BROWSER_ORIGIN=http://localhost:3000');

  if (provisioning.status === 'provisioned') {
    const keyValue = template === 'node' ? provisioning.keys?.secret : provisioning.keys?.publishable;
    if (keyValue) {
      lines.push(`${templateDef.publicApiKeyEnv}=${keyValue}`);
    } else {
      lines.push(`# ${templateDef.publicApiKeyEnv}=`);
    }
  } else {
    lines.push(`# ${templateDef.publicApiKeyEnv}=`);
  }

  if (template !== 'node' && templateDef.publicApiKeyEnv !== 'SPAPS_API_KEY') {
    lines.push('# SPAPS_API_KEY=');
  }

  return `${lines.join('\n')}\n`;
}

function buildProvisioningNote({ provisioning, targetDir, template }) {
  if (provisioning.status === 'provisioned') {
    return [
      '## Provisioning Status',
      '',
      `This starter was provisioned against \`${provisioning.runtime.url}\`. The generated \`.env.local\` includes a working ${template === 'node' ? 'server' : 'browser'} key for this template.`,
      '',
    ].join('\n');
  }

  if (provisioning.status === 'local_mode') {
    return [
      '## Provisioning Status',
      '',
      `The server at \`${provisioning.runtime.url}\` is currently in local mode, so authenticated flows can run without provisioning while that mode stays enabled.`,
      '',
      'Use `npx spaps quickstart --json` if you need the current local-mode hints and test personas.',
      '',
    ].join('\n');
  }

  const rerunCommand = `SELF_SERVICE_PASSWORD=your-password npx spaps create ${path.basename(targetDir)} --template ${template} --dir ${targetDir} --force`;

  return [
    '## Provisioning Status',
    '',
    'This run only scaffolded files. Authenticated flows will not work until you provision a real SPAPS application or enable server local mode.',
    '',
    'To provision automatically:',
    '',
    '```bash',
    rerunCommand,
    '```',
    '',
  ].join('\n');
}

function buildDesignSystemNote() {
  return `- \`components.json\`: shadcn project config, including the \`@design-system\` registry namespace
- \`design-system.registry.json\`: Design System Registry adoption contract for this app
- \`src/app.css\`: Tailwind v4 entry carrying the pinned \`${DSR_THEME.item}\` token payload
- \`src/design-system/theme-adapter.ts\`: the only place the active registry item is named

## Design System

The authentication reference page is styled by **${DSR_THEME.title}**
(\`${DSR_THEME.item}@${DSR_THEME.version}\`) from the
[Design System Registry](${DSR_THEME.sourceRepo}).

Page components consume shadcn semantic tokens only. Do not add a local palette,
radius scale, or shadow system: change the theme instead.

Re-pull or swap the active item:

\`\`\`bash
${DSR_THEME.installCommand}
\`\`\`

Approved alternates carrying the same \`ds-*\` token contract:
${DSR_THEME.switchableItems.map((item) => `- \`${item}\``).join('\n')}

After swapping, update \`activeItem\` in \`design-system.registry.json\` and
\`ACTIVE_REGISTRY_ITEM\` in \`src/design-system/theme-adapter.ts\`.
`;
}

function buildReadme({ name, template, apiUrl, targetDir, provisioning }) {
  const templateDef = SUPPORTED_TEMPLATES[template];

  return `# ${name}

This directory is a SPAPS ${templateDef.label.toLowerCase()}.

It gives you three things immediately:

- a machine-readable SPAPS app contract in \`spaps.app.json\`
- local env wiring in \`.env.local\`
- a small template-specific integration starter you can drop into a real app

This is not a full framework generator. It does not run \`create-next-app\`, Vite, or Express setup for you.

${buildProvisioningNote({ provisioning, targetDir, template })}## Next Steps

1. Install dependencies in this project with \`npm install\`
2. Copy the generated starter files into your real ${templateDef.label.toLowerCase()} or keep extending this directory
3. Point your app at \`${apiUrl}\`
4. Run \`npm run test:passkeys\` locally to launch the managed Chromium virtual-authenticator proof

## Generated Files

- \`spaps.app.json\`: local app contract and provisioning metadata
- \`.env.local\`: SPAPS API URL wiring and, when available, a working key for this template
- \`package.json\`: minimal dependency declaration for \`spaps-sdk\`
${template === 'react' ? buildDesignSystemNote() : ''}`;
}

function buildNextSteps({ targetDir, provisioning, port, name, template }) {
  const steps = [
    `cd ${targetDir}`,
    'npm install',
  ];

  if (provisioning.status === 'provisioned') {
    steps.push('Review .env.local and start your app');
    return steps;
  }

  if (provisioning.status === 'local_mode') {
    steps.push(`Use the starter against http://localhost:${port} while local mode stays enabled`);
    return steps;
  }

  if (provisioning.reason === 'server_unreachable') {
    steps.push(`npx spaps local --port ${port}`);
  } else {
    steps.push(
      `SELF_SERVICE_PASSWORD=your-password npx spaps create ${name} --template ${template} --dir ${targetDir} --force`
    );
  }

  return steps;
}

async function createProjectStarter({
  name,
  template,
  dir = null,
  force = false,
  version = '0.0.0',
  port = DEFAULT_PORT,
}) {
  if (!name || !String(name).trim()) {
    throw createCliError('EINVAL', 'Project name is required.');
  }

  ensureSupportedTemplate(template);

  const normalizedName = String(name).trim();
  const slug = slugifyProjectName(normalizedName);
  const targetDir = path.resolve(dir || path.join(process.cwd(), slug));
  const apiUrl = `http://localhost:${port}`;
  const docsUrl = `${apiUrl}/docs`;
  const bookkeeping = {
    files_created: [],
    files_overwritten: [],
  };
  const templateDef = SUPPORTED_TEMPLATES[template];
  prepareWritableTarget(targetDir, force);
  const provisioning = await provisionStarterApplication({
    port,
    name: normalizedName,
    slug,
    blueprintKey: templateDef.blueprintKey,
    allowedOrigins: templateDef.allowedOrigins,
  });

  writeManagedFile(
    targetDir,
    'spaps.app.json',
    buildContract({
      name: normalizedName,
      slug,
      template,
      version,
      apiUrl,
      docsUrl,
      provisioning,
    }),
    bookkeeping
  );
  writeManagedFile(targetDir, '.env.local', buildEnvFile(template, apiUrl, provisioning), bookkeeping);
  writeManagedFile(
    targetDir,
    'README.md',
    buildReadme({ name: normalizedName, template, apiUrl, targetDir, provisioning }),
    bookkeeping
  );
  writeManagedFile(targetDir, 'package.json', buildPackageJson(normalizedName, template), bookkeeping);
  writeManagedFile(
    targetDir,
    '.gitignore',
    'node_modules\n.env\n.env.local\n',
    bookkeeping
  );

  const templateFiles = templateDef.files({
    name: normalizedName,
    slug,
    apiUrl,
    installedAt: new Date().toISOString().slice(0, 10),
  });

  for (const [relativePath, content] of Object.entries(templateFiles)) {
    writeManagedFile(targetDir, relativePath, content, bookkeeping);
  }

  return {
    success: true,
    command: 'create',
    project_name: normalizedName,
    template,
    target_dir: targetDir,
    contract_path: path.join(targetDir, 'spaps.app.json'),
    files_created: bookkeeping.files_created,
    files_overwritten: bookkeeping.files_overwritten,
    provisioning: {
      status: provisioning.status,
      reason: provisioning.reason || null,
      application_id: provisioning.application?.id || null,
      application_slug: provisioning.application?.slug || slug,
      local_mode_active: provisioning.runtime?.local_mode?.active ?? null,
      server_url: provisioning.runtime?.url || apiUrl,
    },
    warnings: provisioning.warnings || [],
    next_steps: buildNextSteps({
      targetDir,
      provisioning,
      port,
      name: normalizedName,
      template,
    }),
  };
}

module.exports = {
  PASSKEY_BROWSER_SDK_RANGE,
  SUPPORTED_TEMPLATES,
  createProjectStarter,
  validateBrowserConfig,
};