UNPKG

spaps

Version:

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

2,413 lines 75.5 kB
const fs = require('node:fs');
const path = require('node:path');
const axios = require('axios');

const { DEFAULT_PORT } = require('./config');
const { buildBaseUrl, getServerRuntime } = require('./local-runtime');

const FIXTURE_SCHEMA_VERSION = 1;
const FIXTURE_DIRNAME = '.spaps';
const BROWSER_DIRNAME = 'browser';
const DEFAULT_BROWSER_BASE_URL = 'http://localhost:3000';
const DEFAULT_PUBLIC_DIR = 'public';
const DEFAULT_BRIDGE_SCRIPT_NAME = 'spaps-dev-auth.js';
const FIXTURE_KEYS = {
  active_persona: 'spaps.fixture.active_persona',
  persona: 'spaps.fixture.persona',
  profile: 'spaps.fixture.profile',
  roles: 'spaps.fixture.roles',
  entitlements: 'spaps.fixture.entitlements',
  scenario: 'spaps.fixture.scenario',
  application: 'spaps.fixture.application',
  runtime: 'spaps.fixture.runtime',
  selector: 'spaps.fixture.selector',
};
const COMPAT_STORAGE_KEYS = {
  sdk_user: 'sweet_potato_user',
  sdk_access_token: 'sweet_potato_access_token',
  sdk_refresh_token: 'sweet_potato_refresh_token',
  legacy_user: 'spaps_user',
};

function cloneValue(value) {
  return JSON.parse(JSON.stringify(value));
}

const BASE_PERSONAS = [
  {
    code: 'user',
    display_name: 'Local User',
    selector: {
      query_param: { _user: 'user' },
      headers: { 'X-Test-User': 'user' },
    },
    profile: {
      user_id: '00000000-0000-0000-0000-000000000001',
      email: 'user@localhost',
      username: 'dev-user',
      tier: 'free',
    },
    permissions: ['view_products'],
    browser: {
      local_storage: {},
    },
  },
  {
    code: 'admin',
    display_name: 'Local Admin',
    selector: {
      query_param: { _user: 'admin' },
      headers: { 'X-Test-User': 'admin' },
    },
    profile: {
      user_id: '5bdb0db2-5ab1-4e2c-999b-1153cc329477',
      email: 'buildooor@gmail.com',
      username: 'admin',
      tier: 'enterprise',
    },
    permissions: [
      'view_products',
      'manage_products',
      'view_users',
      'manage_users',
      'view_orders',
      'manage_orders',
      'access_admin',
      'view_analytics',
      'manage_subscriptions',
      'manage_system_settings',
    ],
    browser: {
      local_storage: {},
    },
  },
  {
    code: 'premium',
    display_name: 'Premium User',
    selector: {
      query_param: { _user: 'premium' },
      headers: { 'X-Test-User': 'premium' },
    },
    profile: {
      user_id: '00000000-0000-0000-0000-000000000002',
      email: 'premium@localhost',
      username: 'premium-user',
      tier: 'premium',
    },
    permissions: ['view_products'],
    browser: {
      local_storage: {},
    },
  },
];

function getBasePersona(code) {
  const persona = BASE_PERSONAS.find((entry) => entry.code === code);
  if (!persona) {
    throw new Error(`Unknown base persona "${code}"`);
  }
  return persona;
}

function makeDerivedPersona(code, displayName, fromCode, overrides = {}) {
  const base = getBasePersona(fromCode);
  const {
    selector,
    profile,
    permissions,
    browser,
    ...rest
  } = overrides;

  return {
    ...cloneValue(base),
    ...cloneValue(rest),
    code,
    display_name: displayName,
    selector: selector ? cloneValue(selector) : cloneValue(base.selector),
    profile: {
      ...cloneValue(base.profile),
      ...(profile ? cloneValue(profile) : {}),
    },
    permissions: permissions ? [...permissions] : [...(base.permissions || [])],
    browser: {
      local_storage: {
        ...(base.browser?.local_storage || {}),
        ...((browser && browser.local_storage) || {}),
      },
    },
  };
}

const DEFAULT_PERSONAS = [
  ...BASE_PERSONAS,
  makeDerivedPersona('dayrate-guest', 'Dayrate Guest', 'user', {
    profile: {
      username: 'dayrate-guest',
    },
    scenario: {
      dayrate: {
        mode: 'paid',
        policy_key: null,
        sample_request: {
          date: '2026-05-01',
          slot: 'AM',
          clientEmail: 'guest-fixture@example.com',
          clientName: 'Fixture Guest',
          successUrl: 'https://example.com/dayrate/success',
          cancelUrl: 'https://example.com/dayrate/cancel',
        },
      },
    },
  }),
  makeDerivedPersona('dayrate-entitled', 'Dayrate Entitled', 'user', {
    permissions: ['view_products', 'book_dayrate_free'],
    profile: {
      username: 'dayrate-entitled',
    },
    scenario: {
      dayrate: {
        mode: 'free',
        policy_key: 'fixture-dayrate-free-{{global.run_id}}',
        entitlement_key: 'fixture.dayrate.free.{{global.run_id}}',
        free_bookings_remaining: 2,
        sample_request: {
          date: '2026-05-02',
          slot: 'AM',
          clientEmail: 'entitled-fixture@example.com',
          clientName: 'Fixture Entitled',
          successUrl: 'https://example.com/dayrate/success',
          cancelUrl: 'https://example.com/dayrate/cancel',
          policyKey: 'fixture-dayrate-free-{{global.run_id}}',
          userId: '{{persona.profile.user_id}}',
        },
      },
    },
    seed: [
      {
        as: 'admin',
        method: 'PUT',
        path: '/api/dayrate/admin/config',
        body: {
          base_rate: 12000,
          available_days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
          horizon_weeks: 8,
          min_notice_hours: 1,
          hold_duration_minutes: 15,
          timezone: 'America/Toronto',
          currency: 'usd',
          product_name: 'Fixture Dayrate',
          slot_definitions: {
            AM: { start: '09:00', end: '12:00' },
            PM: { start: '13:00', end: '16:00' },
          },
        },
      },
      {
        as: 'admin',
        method: 'POST',
        path: '/api/dayrate/admin/policies',
        expected_statuses: [201, 409],
        body: {
          policyKey: 'fixture-dayrate-free-{{global.run_id}}',
          entitlementKey: 'fixture.dayrate.free.{{global.run_id}}',
          freeBookingsPerPeriod: 2,
          periodDays: 28,
          overageRate: null,
          overageCurrency: 'usd',
          overageProductName: 'Fixture Dayrate Overage',
          rescheduleIsFree: true,
          metadata: {
            source: 'spaps-fixture-seed',
            persona: 'dayrate-entitled',
          },
        },
      },
      {
        as: 'admin',
        method: 'POST',
        path: '/api/entitlements/manual',
        expected_statuses: [201, 409],
        body: {
          beneficiary_user_id: '{{persona.profile.user_id}}',
          beneficiary_email: '{{persona.profile.email}}',
          entitlement_key: 'fixture.dayrate.free.{{global.run_id}}',
          resource_type: 'user',
          reason: 'spaps fixture seed',
          metadata: {
            source: 'spaps-fixture-seed',
            persona: 'dayrate-entitled',
          },
        },
      },
    ],
  }),
  makeDerivedPersona('dayrate-overage', 'Dayrate Overage', 'premium', {
    permissions: ['view_products', 'book_dayrate_free'],
    profile: {
      username: 'dayrate-overage',
    },
    scenario: {
      dayrate: {
        mode: 'overage',
        policy_key: 'fixture-dayrate-overage-{{global.run_id}}',
        entitlement_key: 'fixture.dayrate.overage.{{global.run_id}}',
        overage_rate: 1500,
        free_bookings_remaining: 0,
        sample_request: {
          date: '2026-05-03',
          slot: 'PM',
          clientEmail: 'overage-fixture@example.com',
          clientName: 'Fixture Overage',
          successUrl: 'https://example.com/dayrate/success',
          cancelUrl: 'https://example.com/dayrate/cancel',
          policyKey: 'fixture-dayrate-overage-{{global.run_id}}',
          userId: '{{persona.profile.user_id}}',
        },
      },
    },
    seed: [
      {
        as: 'admin',
        method: 'POST',
        path: '/api/dayrate/admin/policies',
        expected_statuses: [201, 409],
        body: {
          policyKey: 'fixture-dayrate-overage-{{global.run_id}}',
          entitlementKey: 'fixture.dayrate.overage.{{global.run_id}}',
          freeBookingsPerPeriod: 0,
          periodDays: 28,
          overageRate: 1500,
          overageCurrency: 'usd',
          overageProductName: 'Fixture Dayrate Overage',
          rescheduleIsFree: true,
          metadata: {
            source: 'spaps-fixture-seed',
            persona: 'dayrate-overage',
          },
        },
      },
      {
        as: 'admin',
        method: 'POST',
        path: '/api/entitlements/manual',
        expected_statuses: [201, 409],
        body: {
          beneficiary_user_id: '{{persona.profile.user_id}}',
          beneficiary_email: '{{persona.profile.email}}',
          entitlement_key: 'fixture.dayrate.overage.{{global.run_id}}',
          resource_type: 'user',
          reason: 'spaps fixture seed',
          metadata: {
            source: 'spaps-fixture-seed',
            persona: 'dayrate-overage',
          },
        },
      },
    ],
  }),
  makeDerivedPersona('cfo-billing-demo', 'CFO Billing Demo', 'admin', {
    permissions: ['view_products', 'manage_company_billing', 'view_company_reports'],
    profile: {
      username: 'cfo-billing-demo',
    },
    scenario: {
      cfo: {
        pack: 'cfo.billing_demo',
        company_id: '00000000-0000-4000-8000-00000000c001',
        company_slug: 'fixture-cfo-company',
        expected_entitlements: [
          'fixture.cfo.company.billing_admin',
          'fixture.cfo.company.reports',
        ],
      },
    },
  }),
  makeDerivedPersona('htma-local-auth', 'HTMA Local Auth', 'user', {
    permissions: ['view_products', 'manage_patient_protocols'],
    profile: {
      username: 'htma-local-auth',
    },
    scenario: {
      htma: {
        pack: 'htma.local_auth',
        invite_template_key: 'fixture-htma-invite-{{global.run_id}}',
        invite_action_url: 'http://localhost:5173/htma/invite/{{global.run_id}}',
        expected_entitlements: ['fixture.htma.local_auth'],
      },
    },
    seed: [
      {
        as: 'admin',
        method: 'POST',
        path: '/api/email/templates',
        expected_statuses: [201, 409],
        body: {
          name: 'Fixture HTMA Invite',
          template_key: 'fixture-htma-invite-{{global.run_id}}',
          subject: 'HTMA invite for {{name}}',
          html_body: '<p><a href="{{action_url}}">Open HTMA invite</a></p>',
          text_body: 'Open HTMA invite: {{action_url}}',
          from_email: 'noreply@example.com',
          category: 'fixture',
          sample_context: {
            name: 'Fixture HTMA User',
            action_url: 'http://localhost:5173/htma/invite/{{global.run_id}}',
          },
        },
      },
    ],
  }),
  makeDerivedPersona('email-local-safe', 'Email Local Safe', 'user', {
    profile: {
      username: 'email-local-safe',
    },
    scenario: {
      email: {
        template_key: 'fixture-email-safe-{{global.run_id}}',
        send_request: {
          template_key: 'fixture-email-safe-{{global.run_id}}',
          to: 'recipient@example.com',
          context: {
            name: 'Fixture Recipient',
          },
        },
        expected_status: 'short_circuited',
      },
    },
    seed: [
      {
        as: 'admin',
        method: 'POST',
        path: '/api/email/templates',
        expected_statuses: [201, 409],
        body: {
          name: 'Fixture Local Safe Email',
          template_key: 'fixture-email-safe-{{global.run_id}}',
          subject: 'Hello {{name}}',
          html_body: '<p>Hello {{name}}</p>',
          text_body: 'Hello {{name}}',
          from_email: 'noreply@example.com',
          category: 'fixture',
          sample_context: {
            name: 'Fixture Recipient',
          },
        },
      },
    ],
  }),
  makeDerivedPersona('webhook-signature-invalid', 'Webhook Signature Invalid', 'admin', {
    profile: {
      username: 'webhook-signature-invalid',
    },
    scenario: {
      webhooks: {
        invalid_request: {
          path: '/api/webhooks/mailgun/events',
          expected_status: 401,
          body: {
            signature: {
              timestamp: '{{global.epoch}}',
              token: 'fixture-invalid-token',
              signature: 'not-a-real-signature',
            },
            'event-data': {
              event: 'delivered',
              timestamp: '2026-05-04T09:00:00Z',
              message: {
                headers: {
                  'message-id': 'fixture-invalid-msg',
                },
              },
            },
          },
        },
      },
    },
  }),
  makeDerivedPersona('webhook-replay', 'Webhook Replay', 'admin', {
    profile: {
      username: 'webhook-replay',
    },
    scenario: {
      webhooks: {
        replay_request: {
          path: '/api/webhooks/mailgun/events',
          expected_status: 409,
          signing_key: 'test-key',
          token: 'fixture-replay-token',
          body_template: {
            signature: {
              timestamp: '{{global.epoch}}',
              token: 'fixture-replay-token',
              signature: '{{scenario_signature}}',
            },
            'event-data': {
              event: 'delivered',
              timestamp: '2026-05-04T09:05:00Z',
              message: {
                headers: {
                  'message-id': 'fixture-replay-msg',
                },
              },
            },
          },
        },
      },
    },
  }),
  makeDerivedPersona('policy-admin-allow', 'Policy Admin Allow', 'admin', {
    scenario: {
      policies: {
        policy_name: 'fixture-admin-read-{{global.run_id}}',
        authorize_request: {
          policy_name: 'fixture-admin-read-{{global.run_id}}',
        },
      },
    },
    seed: [
      {
        as: 'admin',
        method: 'POST',
        path: '/api/policies',
        expected_statuses: [201, 409],
        body: {
          name: 'fixture-admin-read-{{global.run_id}}',
          description: 'Allow admin-only fixture policy',
          effect: 'allow',
          conditions: {
            has_role: {
              role: 'admin',
            },
          },
          priority: 100,
          metadata: {
            source: 'spaps-fixture-seed',
            persona: 'policy-admin-allow',
          },
        },
      },
    ],
  }),
  makeDerivedPersona('policy-denied-no-wallet', 'Policy Denied No Wallet', 'user', {
    permissions: ['view_products'],
    scenario: {
      policies: {
        policy_name: 'fixture-requires-wallet-{{global.run_id}}',
        expected_decision: 'deny',
        authorize_request: {
          policy_name: 'fixture-requires-wallet-{{global.run_id}}',
        },
      },
    },
    seed: [
      {
        as: 'admin',
        method: 'POST',
        path: '/api/policies',
        expected_statuses: [201, 409],
        body: {
          name: 'fixture-requires-wallet-{{global.run_id}}',
          description: 'Require a Solana wallet for access',
          effect: 'allow',
          conditions: {
            has_wallet: {
              chain: 'solana',
            },
          },
          priority: 100,
          metadata: {
            source: 'spaps-fixture-seed',
            persona: 'policy-denied-no-wallet',
          },
        },
      },
    ],
  }),
  makeDerivedPersona('issue-reporter', 'Issue Reporter', 'user', {
    permissions: ['view_products'],
    scenario: {
      issue_reporting: {
        expected_status: 201,
        create_request: {
          target: {
            component_key: 'feedback-button',
            component_label: 'Feedback Button',
            page_url: 'https://example.com/app',
            surface_ref: 'feedback-fab',
            metadata: {
              screenshots: [
                {
                  url: 'https://example.com/screenshots/fixture-1.png',
                  timestamp: '2026-05-05T09:00:00Z',
                },
              ],
            },
          },
          note: 'Fixture issue report with screenshots for local issue-reporting flows.',
          reporter_role_hint: 'member',
        },
        reply_request: {
          note: 'Fixture follow-up after a support response.',
          reporter_role_hint: 'member',
        },
      },
    },
    seed: [
      {
        as: 'persona',
        method: 'POST',
        path: '/api/v1/issue-reports',
        expected_statuses: [201],
        body: {
          target: {
            component_key: 'feedback-button',
            component_label: 'Feedback Button',
            page_url: 'https://example.com/app',
            surface_ref: 'feedback-fab',
            metadata: {
              screenshots: [
                {
                  url: 'https://example.com/screenshots/fixture-1.png',
                  timestamp: '2026-05-05T09:00:00Z',
                },
              ],
            },
          },
          note: 'Fixture issue report with screenshots for local issue-reporting flows.',
          reporter_role_hint: 'member',
        },
        capture: {
          existing_issue_report_id: 'id',
          existing_issue_case_id: 'linked_case.case_id',
        },
      },
    ],
  }),
  makeDerivedPersona('issue-reporter-blocked', 'Issue Reporter Blocked', 'user', {
    permissions: [],
    selector: {
      query_param: { _user: 'user' },
      headers: {
        'X-Test-User': 'user',
        'X-API-Key': '{{seed.blocked_app_api_key}}',
      },
    },
    application: {
      application_id: '{{seed.blocked_app_id}}',
      application_slug: '{{seed.blocked_app_slug}}',
      api_key: '{{seed.blocked_app_api_key}}',
    },
    scenario: {
      issue_reporting: {
        expected_status: 403,
        required_capability: 'issue_reporting',
        create_request: {
          target: {
            component_key: 'feedback-button',
            component_label: 'Feedback Button',
            page_url: 'https://example.com/app',
            surface_ref: 'feedback-fab',
            metadata: {},
          },
          note: 'Blocked issue reporter fixture should fail until capability is granted.',
          reporter_role_hint: 'member',
        },
      },
    },
    seed: [
      {
        as: 'admin',
        method: 'POST',
        path: '/api/admin/create-app',
        expected_statuses: [201],
        body: {
          name: 'Fixture Issue Reporting Blocked {{global.run_id}}',
          slug: 'fixture-ir-blocked-{{global.run_id}}',
          settings: {
            issue_reporting_required_capability: 'issue_reporting',
          },
        },
        capture: {
          blocked_app_api_key: 'api_key',
          blocked_app_id: 'application.id',
          blocked_app_slug: 'application.slug',
        },
      },
    ],
  }),
];

const DEFAULT_ROLE_GRANTS = {
  user: ['user'],
  admin: ['admin', 'super_admin'],
  premium: ['user'],
  'dayrate-guest': ['user'],
  'dayrate-entitled': ['user'],
  'dayrate-overage': ['user'],
  'cfo-billing-demo': ['admin', 'super_admin'],
  'htma-local-auth': ['user'],
  'email-local-safe': ['user'],
  'webhook-signature-invalid': ['admin', 'super_admin'],
  'webhook-replay': ['admin', 'super_admin'],
  'policy-admin-allow': ['admin', 'super_admin'],
  'policy-denied-no-wallet': ['user'],
  'issue-reporter': ['user'],
  'issue-reporter-blocked': ['user'],
};

const DEFAULT_ENTITLEMENT_GRANTS = {
  user: [],
  admin: ['paid_access', 'admin_console'],
  premium: ['paid_access'],
  'dayrate-guest': [],
  'dayrate-entitled': ['fixture.dayrate.free.{{global.run_id}}'],
  'dayrate-overage': ['fixture.dayrate.overage.{{global.run_id}}'],
  'cfo-billing-demo': [
    {
      key: 'fixture.cfo.company.billing_admin',
      resource_type: 'company',
      resource_id: '00000000-0000-4000-8000-00000000c001',
      metadata: {
        pack: 'cfo.billing_demo',
        company_slug: 'fixture-cfo-company',
      },
    },
    {
      key: 'fixture.cfo.company.reports',
      resource_type: 'company',
      resource_id: '00000000-0000-4000-8000-00000000c001',
      metadata: {
        pack: 'cfo.billing_demo',
        company_slug: 'fixture-cfo-company',
      },
    },
  ],
  'htma-local-auth': ['fixture.htma.local_auth'],
  'email-local-safe': [],
  'webhook-signature-invalid': [],
  'webhook-replay': [],
  'policy-admin-allow': [],
  'policy-denied-no-wallet': [],
  'issue-reporter': [],
  'issue-reporter-blocked': [],
};

function normalizeEntitlementGrant(grant) {
  if (typeof grant === 'string') {
    return { key: grant };
  }
  if (!grant || typeof grant !== 'object') {
    return { key: String(grant || '') };
  }
  return {
    key: grant.key || grant.entitlement_key || '',
    entitlement_type: grant.entitlement_type || grant.type || 'access',
    resource_type: grant.resource_type || 'user',
    resource_id: grant.resource_id || null,
    metadata: grant.metadata || {},
  };
}

function entitlementGrantKey(grant) {
  return normalizeEntitlementGrant(grant).key;
}

function entitlementGrantKeys(grants = []) {
  return grants.map(entitlementGrantKey).filter(Boolean);
}

function serializeEntitlementGrantForSync(grant) {
  if (typeof grant === 'string') {
    return grant;
  }
  return normalizeEntitlementGrant(grant);
}

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

function resolveRepoRoot(dir = null) {
  return path.resolve(dir || process.cwd());
}

function resolveFixturePaths(rootDir) {
  const fixtureDir = path.join(rootDir, FIXTURE_DIRNAME);
  const browserDir = path.join(fixtureDir, BROWSER_DIRNAME);
  const publicDir = path.join(rootDir, DEFAULT_PUBLIC_DIR);
  return {
    rootDir,
    fixtureDir,
    browserDir,
    publicDir,
    app: path.join(fixtureDir, 'app.json'),
    users: path.join(fixtureDir, 'users.json'),
    roles: path.join(fixtureDir, 'roles.json'),
    entitlements: path.join(fixtureDir, 'entitlements.json'),
    readme: path.join(fixtureDir, 'README.md'),
    browserGitignore: path.join(browserDir, '.gitignore'),
    bridgeScript: path.join(publicDir, DEFAULT_BRIDGE_SCRIPT_NAME),
    lock: path.join(fixtureDir, 'fixtures.lock.json'),
    starterContract: path.join(rootDir, 'spaps.app.json'),
  };
}

function normalizeBaseUrl(baseUrl = null) {
  const candidate = String(baseUrl || process.env.SPAPS_BROWSER_BASE_URL || DEFAULT_BROWSER_BASE_URL).trim();
  return candidate.replace(/\/+$/, '');
}

function resolveStorageOrigin(baseUrl) {
  const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
  try {
    return new URL(normalizedBaseUrl).origin;
  } catch {
    return normalizedBaseUrl;
  }
}

function readJsonFile(filePath, label) {
  try {
    return JSON.parse(fs.readFileSync(filePath, 'utf8'));
  } catch (error) {
    throw createCliError('EINVAL', `Could not read ${label} at "${filePath}": ${error.message}`);
  }
}

function readExistingStarterContract(paths) {
  if (!fs.existsSync(paths.starterContract)) {
    return null;
  }

  try {
    return JSON.parse(fs.readFileSync(paths.starterContract, 'utf8'));
  } catch {
    return null;
  }
}

function buildRuntimeSummary(runtime, port, starterContract = null) {
  const contractLocal = starterContract?.spaps?.local || {};
  const contractApp = starterContract?.spaps?.application || {};
  const apiUrl = runtime?.url || contractLocal.api_url || buildBaseUrl(port);
  const docsUrl = runtime?.docs || contractLocal.docs_url || `${apiUrl}/docs`;

  return {
    api_url: apiUrl,
    docs_url: docsUrl,
    port,
    running: Boolean(runtime?.running),
    local_mode_active:
      runtime?.running
        ? (runtime.local_mode?.known ? Boolean(runtime.local_mode.active) : null)
        : (typeof contractLocal.local_mode_active === 'boolean' ? contractLocal.local_mode_active : null),
    environment: runtime?.running ? runtime.local_mode?.environment || null : null,
    application_id: contractApp.id || runtime?.local_mode?.test_application?.id || null,
    application_slug: contractApp.slug || runtime?.local_mode?.test_application?.slug || null,
    provisioning_status: contractApp.provisioning_status || null,
  };
}

function buildAppConfig({ version, port, baseUrl, runtime, starterContract }) {
  const server = buildRuntimeSummary(runtime, port, starterContract);

  return {
    schema_version: FIXTURE_SCHEMA_VERSION,
    generated_by: `spaps@${version}`,
    server,
    browser: {
      base_url: normalizeBaseUrl(baseUrl),
      default_persona: 'user',
      storage_format: 'playwright',
    },
    bridge: {
      enabled: true,
      public_dir: DEFAULT_PUBLIC_DIR,
      script_name: DEFAULT_BRIDGE_SCRIPT_NAME,
      query_param: 'spaps_persona',
      ui: {
        enabled: true,
      },
    },
    auth: {
      mode: server.local_mode_active === true ? 'local_mode' : runtime?.running ? 'application_key' : 'offline',
      selector: {
        query_param: '_user',
        header: 'X-Test-User',
      },
      api_key_header: 'X-API-Key',
    },
  };
}

function buildUsersConfig() {
  return {
    schema_version: FIXTURE_SCHEMA_VERSION,
    default_persona: 'user',
    personas: DEFAULT_PERSONAS,
  };
}

function buildRolesConfig() {
  return {
    schema_version: FIXTURE_SCHEMA_VERSION,
    grants: DEFAULT_ROLE_GRANTS,
  };
}

function buildEntitlementsConfig() {
  return {
    schema_version: FIXTURE_SCHEMA_VERSION,
    grants: DEFAULT_ENTITLEMENT_GRANTS,
  };
}

function buildFixtureReadme() {
  return `# .spaps

Repo-local SPAPS auth fixtures live here.

What to edit:

- \`app.json\`: local SPAPS server and browser target settings
- \`users.json\`: personas, profile data, scenario metadata, optional seed steps, selector hints, and custom browser storage
- \`roles.json\`: persona-to-role grants
- \`entitlements.json\`: persona-to-entitlement grants

Generated files:

- \`browser/*.storage-state.json\`: Playwright storageState files
- \`browser/*.headers.json\`: extraHTTPHeaders companions for local-mode persona routing
- \`browser/*.context.json\`: merged persona/runtime summary for test harnesses
- \`public/${DEFAULT_BRIDGE_SCRIPT_NAME}\`: optional browser bridge for frontend-only auth/RBAC clicking around

Common workflow:

1. Run \`npx spaps fixtures init\`
2. Edit \`.spaps/users.json\`, \`.spaps/roles.json\`, or \`.spaps/entitlements.json\`
3. Run \`npx spaps fixtures apply\`
4. Run \`npx spaps fixtures apply --sync-server\` to reconcile users, memberships, and entitlements into local SPAPS
5. Run \`npx spaps fixtures apply --seed --persona <code>\` when a domain persona needs extra domain-specific DB state
6. Point Playwright or local scripts at the generated browser artifacts
7. Include \`/${DEFAULT_BRIDGE_SCRIPT_NAME}\` before your app boots if you want frontend-only persona switching

Notes:

- \`--sync-server\` only runs against a reachable localhost SPAPS server with local mode active
- \`--seed\` only runs against a reachable SPAPS server with local mode active
- Seed steps use the persona metadata already in \`.spaps/users.json\`; no extra backend-only fixture routes are required
`;
}

function writeJson(filePath, value) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}

function writeManagedFile(filePath, content, bookkeeping, { overwrite = true } = {}) {
  const relativePath = path.relative(bookkeeping.rootDir, filePath) || path.basename(filePath);
  if (fs.existsSync(filePath) && !overwrite) {
    bookkeeping.files_skipped.push(relativePath);
    return false;
  }

  fs.mkdirSync(path.dirname(filePath), { recursive: true });

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

  fs.writeFileSync(filePath, content);
  return true;
}

function cleanupGeneratedBrowserArtifacts(paths) {
  if (!fs.existsSync(paths.browserDir)) {
    return [];
  }

  const removed = [];
  for (const entry of fs.readdirSync(paths.browserDir)) {
    if (entry === '.gitignore') {
      continue;
    }
    const fullPath = path.join(paths.browserDir, entry);
    fs.rmSync(fullPath, { recursive: true, force: true });
    removed.push(path.relative(paths.rootDir, fullPath));
  }

  if (fs.existsSync(paths.lock)) {
    fs.rmSync(paths.lock, { force: true });
    removed.push(path.relative(paths.rootDir, paths.lock));
  }

  return removed;
}

async function buildFixtureSeed({ rootDir, port, baseUrl, version }) {
  const paths = resolveFixturePaths(rootDir);
  const starterContract = readExistingStarterContract(paths);
  const runtime = await getServerRuntime({ port });

  return {
    runtime,
    appConfig: buildAppConfig({ version, port, baseUrl, runtime, starterContract }),
    usersConfig: buildUsersConfig(),
    rolesConfig: buildRolesConfig(),
    entitlementsConfig: buildEntitlementsConfig(),
  };
}

async function initFixtureKernel({
  dir = null,
  port = DEFAULT_PORT,
  baseUrl = null,
  version = '0.0.0',
  force = false,
} = {}) {
  const rootDir = resolveRepoRoot(dir);
  const paths = resolveFixturePaths(rootDir);
  const bookkeeping = {
    rootDir,
    files_created: [],
    files_overwritten: [],
    files_skipped: [],
  };
  const seed = await buildFixtureSeed({ rootDir, port, baseUrl, version });

  writeManagedFile(paths.app, `${JSON.stringify(seed.appConfig, null, 2)}\n`, bookkeeping, { overwrite: force });
  writeManagedFile(paths.users, `${JSON.stringify(seed.usersConfig, null, 2)}\n`, bookkeeping, { overwrite: force });
  writeManagedFile(paths.roles, `${JSON.stringify(seed.rolesConfig, null, 2)}\n`, bookkeeping, { overwrite: force });
  writeManagedFile(paths.entitlements, `${JSON.stringify(seed.entitlementsConfig, null, 2)}\n`, bookkeeping, { overwrite: force });
  writeManagedFile(paths.readme, buildFixtureReadme(), bookkeeping, { overwrite: force });
  writeManagedFile(paths.browserGitignore, '*.json\n!.gitignore\n', bookkeeping, { overwrite: force });

  return {
    success: true,
    command: 'fixtures',
    subcommand: force ? 'reset' : 'init',
    root_dir: rootDir,
    fixture_dir: paths.fixtureDir,
    runtime: seed.runtime,
    files_created: bookkeeping.files_created,
    files_overwritten: bookkeeping.files_overwritten,
    files_skipped: bookkeeping.files_skipped,
    next_steps: [
      'Edit .spaps/users.json, .spaps/roles.json, or .spaps/entitlements.json as needed',
      'Run npx spaps fixtures apply',
    ],
  };
}

function loadFixtureKernel(rootDir) {
  const paths = resolveFixturePaths(rootDir);
  if (!fs.existsSync(paths.fixtureDir)) {
    throw createCliError('ENOENT', `No .spaps directory found in "${rootDir}". Run "npx spaps fixtures init" first.`);
  }

  const app = readJsonFile(paths.app, '.spaps/app.json');
  const users = readJsonFile(paths.users, '.spaps/users.json');
  const roles = readJsonFile(paths.roles, '.spaps/roles.json');
  const entitlements = readJsonFile(paths.entitlements, '.spaps/entitlements.json');

  if (!Array.isArray(users.personas) || users.personas.length === 0) {
    throw createCliError('EINVAL', '.spaps/users.json must define at least one persona.');
  }

  return { paths, app, users, roles, entitlements };
}

function ensurePersona(usersConfig, code) {
  const persona = usersConfig.personas.find((entry) => entry.code === code);
  if (!persona) {
    throw createCliError('EINVAL', `Unknown persona "${code}". Add it to .spaps/users.json or choose one of: ${usersConfig.personas.map((entry) => entry.code).join(', ')}.`);
  }
  return persona;
}

function buildFixtureRunState(appConfig, runtime) {
  const now = new Date();
  const generatedAt = now.toISOString();
  return {
    global: {
      generated_at: generatedAt,
      epoch: Math.floor(now.getTime() / 1000),
      run_id: generatedAt
        .replace(/[-:]/g, '')
        .replace(/\.\d+Z$/, 'z')
        .replace('T', 't'),
    },
    runtime: {
      api_url: appConfig.server.api_url,
      application_id: appConfig.server.application_id,
      application_slug: appConfig.server.application_slug,
      local_mode_active: appConfig.server.local_mode_active,
      running: appConfig.server.running,
      environment: runtime?.local_mode?.environment || appConfig.server.environment || null,
    },
    seed: {},
  };
}

function getNestedValue(source, dottedPath) {
  if (!source || typeof source !== 'object' || !dottedPath) {
    return undefined;
  }

  return String(dottedPath)
    .split('.')
    .filter(Boolean)
    .reduce((current, segment) => {
      if (current === undefined || current === null) {
        return undefined;
      }

      if (Array.isArray(current)) {
        const index = Number(segment);
        return Number.isInteger(index) ? current[index] : undefined;
      }

      return current[segment];
    }, source);
}

function resolveTemplateLookup(context, expression) {
  const lookup = String(expression || '').trim();
  if (!lookup) {
    return undefined;
  }

  if (!lookup.includes('.') && Object.prototype.hasOwnProperty.call(context, lookup)) {
    return context[lookup];
  }

  return getNestedValue(context, lookup);
}

function renderTemplateValue(value, context) {
  if (typeof value === 'string') {
    const matches = [...value.matchAll(/\{\{\s*([^}]+?)\s*\}\}/g)];
    if (matches.length === 0) {
      return value;
    }

    if (matches.length === 1 && matches[0][0] === value) {
      const resolved = resolveTemplateLookup(context, matches[0][1]);
      return resolved === undefined ? value : cloneValue(resolved);
    }

    return value.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (match, expression) => {
      const resolved = resolveTemplateLookup(context, expression);
      return resolved === undefined ? match : String(resolved);
    });
  }

  if (Array.isArray(value)) {
    return value.map((entry) => renderTemplateValue(entry, context));
  }

  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, entry]) => [key, renderTemplateValue(entry, context)])
    );
  }

  return value;
}

function unwrapEnvelope(payload) {
  if (
    payload &&
    typeof payload === 'object' &&
    payload.success === true &&
    Object.prototype.hasOwnProperty.call(payload, 'data')
  ) {
    return payload.data;
  }

  return payload;
}

function describeStatuses(statuses = []) {
  if (!Array.isArray(statuses) || statuses.length === 0) {
    return '2xx';
  }
  return statuses.join(', ');
}

function buildApplicationSummary(appConfig, persona = {}) {
  const override = persona.application || {};
  return {
    api_url: override.api_url || appConfig.server.api_url,
    application_id: override.application_id || override.id || appConfig.server.application_id || null,
    application_slug: override.application_slug || override.slug || appConfig.server.application_slug || null,
    api_key: override.api_key || null,
    local_mode_active: appConfig.server.local_mode_active,
  };
}

function buildPersonaTemplateContext(appConfig, persona, fixtureRunState) {
  return {
    global: fixtureRunState.global,
    runtime: fixtureRunState.runtime,
    seed: fixtureRunState.seed,
    persona,
    application: buildApplicationSummary(appConfig, persona),
    scenario: persona.scenario || {},
  };
}

function resolvePersonaDefinition(appConfig, persona, fixtureRunState) {
  return renderTemplateValue(cloneValue(persona), buildPersonaTemplateContext(appConfig, persona, fixtureRunState));
}

function resolveConfigTemplates(config, fixtureRunState) {
  return renderTemplateValue(cloneValue(config), fixtureRunState);
}

function resolveSeedActor({ appConfig, usersConfig, persona, actor = 'persona', fixtureRunState }) {
  const actorCode = actor === 'persona' ? persona.code : actor;
  const actorPersona = ensurePersona(usersConfig, actorCode);
  return resolvePersonaDefinition(appConfig, actorPersona, fixtureRunState);
}

function buildSeedUrl(apiUrl, requestPath, queryParams = {}) {
  const url = new URL(requestPath, apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`);
  for (const [key, value] of Object.entries(queryParams || {})) {
    if (value === undefined || value === null || value === '') {
      continue;
    }
    url.searchParams.set(key, String(value));
  }
  return url.toString();
}

function isLocalServerUrl(apiUrl) {
  try {
    const parsed = new URL(apiUrl);
    return ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
  } catch {
    return false;
  }
}

function assertSeedingRuntime(runtime, port) {
  if (!runtime?.running) {
    throw createCliError(
      'ESEED',
      `Fixture seeding requires a running SPAPS server on port ${port}. Start it with "npx spaps local --port ${port}" or omit --seed.`
    );
  }

  if (runtime.local_mode?.active !== true) {
    throw createCliError(
      'ESEED',
      'Fixture seeding requires SPAPS local mode so persona routing can use X-Test-User headers. Enable local mode or omit --seed.'
    );
  }
}

function assertServerSyncRuntime(runtime, appConfig, port) {
  if (!runtime?.running) {
    throw createCliError(
      'ESYNC',
      `Fixture server sync requires a running SPAPS server on port ${port}. Start it with "npx spaps local --port ${port}" or omit --sync-server.`
    );
  }

  if (runtime.local_mode?.active !== true) {
    throw createCliError(
      'ESYNC',
      'Fixture server sync requires SPAPS local mode. Refusing to sync fixtures into a non-local server.'
    );
  }

  if (!isLocalServerUrl(appConfig.server.api_url)) {
    throw createCliError(
      'ESYNC',
      `Fixture server sync refuses non-local SPAPS targets: ${appConfig.server.api_url}`
    );
  }
}

async function runSeedSequence({
  appConfig,
  usersConfig,
  persona,
  fixtureRunState,
}) {
  const steps = Array.isArray(persona.seed) ? persona.seed : [];
  if (steps.length === 0) {
    return null;
  }

  const requests = [];
  const captures = {};

  for (const step of steps) {
    const resolvedPersona = resolvePersonaDefinition(appConfig, persona, fixtureRunState);
    const actor = resolveSeedActor({
      appConfig,
      usersConfig,
      persona,
      actor: step.as || 'persona',
      fixtureRunState,
    });
    const templateContext = buildPersonaTemplateContext(appConfig, resolvedPersona, fixtureRunState);
    const resolvedStep = renderTemplateValue(cloneValue(step), templateContext);
    const application = buildApplicationSummary(appConfig, actor);
    const headers = {
      Accept: 'application/json',
      ...(resolvedStep.body ? { 'Content-Type': 'application/json' } : {}),
      ...(actor.selector?.headers || {}),
    };

    if (application.api_key && !headers['X-API-Key']) {
      headers['X-API-Key'] = application.api_key;
    }

    const url = buildSeedUrl(
      appConfig.server.api_url,
      resolvedStep.path,
      actor.selector?.query_param || {}
    );

    let response;
    try {
      response = await axios({
        method: resolvedStep.method || 'GET',
        url,
        data: resolvedStep.body || null,
        headers,
        timeout: 5000,
        validateStatus: () => true,
      });
    } catch (error) {
      throw createCliError(
        'ESEED',
        `Fixture seed request for persona "${persona.code}" failed at ${resolvedStep.method || 'GET'} ${resolvedStep.path}: ${error.message}`
      );
    }

    const expectedStatuses =
      Array.isArray(resolvedStep.expected_statuses) && resolvedStep.expected_statuses.length > 0
        ? resolvedStep.expected_statuses
        : null;
    const ok = expectedStatuses
      ? expectedStatuses.includes(response.status)
      : response.status >= 200 && response.status < 300;

    if (!ok) {
      throw createCliError(
        'ESEED',
        `Fixture seed request for persona "${persona.code}" failed at ${resolvedStep.method || 'GET'} ${resolvedStep.path}: expected ${describeStatuses(expectedStatuses)}, received ${response.status}.`
      );
    }

    const payload = unwrapEnvelope(response.data);
    const captured = {};
    for (const [key, lookup] of Object.entries(resolvedStep.capture || {})) {
      const capturedValue = getNestedValue(payload, lookup);
      if (capturedValue === undefined) {
        throw createCliError(
          'ESEED',
          `Fixture seed request for persona "${persona.code}" could not capture "${key}" from "${lookup}" at ${resolvedStep.method || 'GET'} ${resolvedStep.path}.`
        );
      }

      fixtureRunState.seed[key] = capturedValue;
      captures[key] = capturedValue;
      captured[key] = capturedValue;
    }

    requests.push({
      as: actor.code,
      method: resolvedStep.method || 'GET',
      path: resolvedStep.path,
      status: response.status,
      expected_statuses: expectedStatuses,
      captured,
    });
  }

  return {
    persona: persona.code,
    request_count: requests.length,
    captures,
    requests,
  };
}

async function runFixtureSeeding({
  appConfig,
  runtime,
  usersConfig,
  personaCode = null,
  fixtureRunState,
  port,
}) {
  assertSeedingRuntime(runtime, port);

  const selectedPersonas = personaCode
    ? [ensurePersona(usersConfig, personaCode)]
    : usersConfig.personas;
  const seeded = [];

  for (const persona of selectedPersonas) {
    const result = await runSeedSequence({
      appConfig,
      usersConfig,
      persona,
      fixtureRunState,
    });

    if (result) {
      seeded.push(result);
    }
  }

  return {
    enabled: true,
    personas: seeded,
    captures: { ...fixtureRunState.seed },
  };
}

function buildFixtureServerSyncPayload({
  appConfig,
  usersConfig,
  rolesConfig,
  entitlementsConfig,
  personaCode = null,
}) {
  const selectedCodes = personaCode
    ? [ensurePersona(usersConfig, personaCode).code]
    : usersConfig.personas.map((entry) => entry.code);

  return {
    source: 'spaps-fixtures',
    application: {
      id: appConfig.server.application_id || null,
      slug: appConfig.server.application_slug || 'local-dev',
      name: appConfig.server.application_slug || 'Local Development',
      api_key: appConfig.server.api_key || null,
      allowed_origins: [resolveStorageOrigin(appConfig.browser.base_url)],
      settings: {
        spaps_fixture_browser_base_url: appConfig.browser.base_url,
      },
    },
    personas: selectedCodes.map((code) => {
      const persona = ensurePersona(usersConfig, code);
      return {
        code,
        display_name: persona.display_name || code,
        profile: persona.profile || {},
        roles: rolesConfig.grants?.[code] || [],
        permissions: persona.permissions || [],
        entitlements: (entitlementsConfig.grants?.[code] || []).map(serializeEntitlementGrantForSync),
        metadata: {
          selector: persona.selector || {},
          scenario: persona.scenario || {},
        },
      };
    }),
  };
}

async function runFixtureServerSync({
  appConfig,
  runtime,
  usersConfig,
  rolesConfig,
  entitlementsConfig,
  personaCode = null,
  port,
}) {
  assertServerSyncRuntime(runtime, appConfig, port);
  const payload = buildFixtureServerSyncPayload({
    appConfig,
    usersConfig,
    rolesConfig,
    entitlementsConfig,
    personaCode,
  });

  let response;
  try {
    response = await axios({
      method: 'POST',
      url: `${appConfig.server.api_url.replace(/\/+$/, '')}/api/dev/fixtures/apply`,
      data: payload,
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      timeout: 10000,
      validateStatus: () => true,
    });
  } catch (error) {
    throw createCliError(
      'ESYNC',
      `Fixture server sync failed at /api/dev/fixtures/apply: ${error.message}`
    );
  }

  const body = unwrapEnvelope(response.data);
  if (response.status < 200 || response.status >= 300) {
    const message = body?.message || body?.error?.message || `status ${response.status}`;
    throw createCliError('ESYNC', `Fixture server sync failed: ${message}`);
  }

  return body;
}

function applyServerSnapshotToUsers(usersConfig, appConfig, serverSync) {
  if (!serverSync?.application || !Array.isArray(serverSync.personas)) {
    return usersConfig;
  }

  const byCode = new Map(serverSync.personas.map((entry) => [entry.code, entry]));
  appConfig.server.application_id = serverSync.application.id;
  appConfig.server.application_slug = serverSync.application.slug;
  appConfig.server.api_key = serverSync.application.api_key;

  return {
    ...usersConfig,
    personas: usersConfig.personas.map((persona) => {
      const synced = byCode.get(persona.code);
      if (!synced) {
        return persona;
      }

      return {
        ...persona,
        profile: {
          ...(persona.profile || {}),
          user_id: synced.user.id,
          email: synced.user.email || persona.profile?.email,
          username: synced.user.username || persona.profile?.username,
          tier: synced.user.tier || persona.profile?.tier,
        },
        application: {
          ...(persona.application || {}),
          application_id: serverSync.application.id,
          application_slug: serverSync.application.slug,
          api_key: serverSync.application.api_key,
        },
        server_sync: {
          membership_created: synced.membership_created,
          entitlements_created: synced.entitlements_created,
        },
      };
    }),
  };
}

function stringifyStorageValue(value) {
  if (typeof value === 'string') {
    return value;
  }
  return JSON.stringify(value);
}

function buildRouteHint(appConfig, persona) {
  const userCode = persona?.selector?.query_param?._user;
  if (appConfig.server.local_mode_active === true && userCode) {
    return `/?_user=${encodeURIComponent(userCode)}`;
  }
  return null;
}

function buildHeaderArtifact(appConfig, persona) {
  const application = buildApplicationSummary(appConfig, persona);
  const headers = {
    ...(persona.selector?.headers || {}),
  };
  if (application.api_key && !headers['X-API-Key']) {
    headers['X-API-Key'] = application.api_key;
  }

  return {
    format: 'playwright-extra-http-headers',
    local_mode_active: appConfig.server.local_mode_active === true,
    headers,
    route_hint: buildRouteHint(appConfig, persona),
    note:
      appConfig.server.local_mode_active === true
        ? 'Use these headers with Playwright context.extraHTTPHeaders when you want SPAPS local-mode persona routing.'
        : 'SPAPS local mode is not active. These headers are still emitted so app-specific test harnesses can choose to consume them.',
  };
}

function base64UrlEncodeJson(value) {
  return Buffer.from(JSON.stringify(value)).toString('base64url');
}

function buildFixtureTokens(appConfig, persona, roles) {
  const now = Math.floor(Date.now() / 1000);
  const primaryRole = roles[0] || 'user';
  const application = buildApplicationSummary(appConfig, persona);
  const header = base64UrlEncodeJson({ alg: 'none', typ: 'JWT' });
  const payload = base64UrlEncodeJson({
    sub: persona.profile?.user_id,
    user_id: persona.profile?.user_id,
    email: persona.profile?.email,
    role: primaryRole,
    roles,
    tier: persona.profile?.tier || 'free',
    app_id: application.application_id,
    aud: application.application_slug || 'spaps-fixture',
    iss: 'spaps-fixture',
    iat: now,
    exp: now + (60 * 60 * 24 * 30),
  });

  return {
    access_token: `${header}.${payload}.fixture`,
    refresh_token: `fixture-refresh-${persona.code}`,
    token_type: 'Bearer',
    expires_in: 60 * 60 * 24 * 30,
  };
}

function buildEntitlementRecords(appConfig, persona, entitlementsConfig) {
  const entitlementGrants = (entitlementsConfig.grants?.[persona.code] || []).map(normalizeEntitlementGrant);
  const application = buildApplicationSummary(appConfig, persona);

  return entitlementGrants.map((grant, index) => ({
    id: `fixture-${persona.code}-${index + 1}`,
    application_id: application.application_id,
    beneficiary_user_id: persona.profile?.user_id || null,
    beneficiary_email: persona.profile?.email || null,
    entitlement_key: grant.key,
    entitlement_type: grant.entitlement_type || 'manual',
    source: 'fixture',
    resource_type: grant.resource_type || 'user',
    resource_id: grant.resource_id || null,
    starts_at: '2026-01-01T00:00:00.000Z',
    ends_at: null,
    revoked_at: null,
    metadata: {
      fixture_persona: persona.code,
      ...(grant.metadata || {}),
    },
  }));
}

function buildCurrentUser(appConfig, persona, rolesConfig, entitlementsConfig) {
  const roles = rolesConfig.grants?.[persona.code] || [];
  const entitlements = entitlementGrantKeys(entitlementsConfig.grants?.[persona.code] || []);
  const permissions = Array.isArray(persona.permissions) ? persona.permissions : [];
  const primaryRole = roles[0] || 'user';
  const application = buildApplicationSummary(appConfig, persona);

  return {
    id: persona.profile?.user_id || null,
    email: persona.profile?.email || null,
    username: persona.profile?.username || null,
    tier: persona.profile?.tier || 'free',
    role: primaryRole,
    roles,
    permissions,
    is_admin: roles.includes('admin') || roles.includes('super_admin'),
    is_super_admin: roles.includes('super_admin'),
    entitlements,
    active_entitlements: entitlements.map((entitlementKey) => ({ key: entitlementKey })),
    application_id: application.application_id || null,
    fixture_persona: persona.code,
  };
}

function buildBridgeConfig(appConfig, users, rolesConfig, entitlementsConfig) {
  const apiOrigin = (() => {
    try {
      return new URL(appConfig.server.api_url).origin;
    } catch {
      return appConfig.server.api_url;
    }
  })();

  return {
    generated_at: new Date().toISOString(),
    api_url: appConfig.server.api_url,
    api_origin: apiOrigin,
    auth_mode: appConfig.auth.mode,
    local_mode_active: appConfig.server.local_mode_active === true,
    default_persona: users.default_persona || appConfig.browser.default_persona || 'user',
    query_param: appConfig.bridge?.query_param || 'spaps_persona',
    storage_keys: {
      ...FIXTURE_KEYS,
      ...COMPAT_STORAGE_KEYS,
    },
    bridge: {
      script_name: appConfig.bridge?.script_name || DEFAULT_BRIDGE_SCRIPT_NAME,
      ui_enabled: appConfig.bridge?.ui?.enabled !== false,
    },
    application: {
      api_url: appConfig.server.api_url,
      application_id: appConfig.server.application_id,
      application_slug: appConfig.server.application_slug,
      local_mode_active: appConfig.server.local_mode_active,
    },
    personas: users.personas.map((persona) => ({
      code: persona.code,
      display_name: persona.display_name,
      selector: persona.selector || {},
      route_hint: buildRouteHint(appConfig, persona),
      application: buildApplicationSummary(appConfig, persona),
      user: buildCurrentUser(appConfig, persona, rolesConfig, entitlementsConfig),
      tokens: buildFixtureTokens(appConfig, persona, rolesConfig.grants?.[persona.code] || []),
      entitlement_keys: entitlementGrantKeys(entitlementsConfig.grants?.[persona.code] || []),
      entitlements: buildEntitlementRecords(appConfig, persona, entitlementsConfig),
      scenario: persona.scenario || {},
      browser: {
        local_storage: persona.browser?.local_storage || {},
      },
    })),
  };
}

function buildDevAuthBridgeScript(config) {
  const serializedConfig = JSON.stringify(config);

  return `;(function () {
  if (typeof window === 'undefined' || typeof localStorage === 'undefined') return;

  const CONFIG = ${serializedConfig};
  const STORAGE_KEYS = CONFIG.storage_keys;
  const ACTIVE_PERSONA_KEY = STORAGE_KEYS.active_persona;
  const TOKEN_USER_KEY = STORAGE_KEYS.sdk_user;
  const TOKEN_ACCESS_KEY = STORAGE_KEYS.sdk_access_token;
  const TOKEN_REFRESH_KEY = STORAGE_KEYS.sdk_refresh_token;
  const LEGACY_USER_KEY = STORAGE_KEYS.legacy_user;
  const PERSONA_LOCAL_STORAGE_KEYS = Array.from(new Set(
    CONFIG.personas.flatMap((persona) => Object.keys(persona.browser?.local_storage || {}))
  ));
  const personaChangeListeners = [];
  const originalFetch = typeof window.fetch === 'function' ? window.fetch.bind(window) : null;

  function clone(value) {
    if (value === undefined || value === null) return value;
    return JSON.parse(JSON.stringify(value));
  }

  function toAbsoluteUrl(input) {
    if (input instanceof Request) return new URL(input.url, window.location.origin);
    return new URL(String(input), window.location.origin);
  }

  function getPersonaByCode(code) {
    return CONFIG.personas.find((persona) => persona.code === code) || null;
  }

  function getInitialPersonaCode() {
    const fromQuery = new URLSearchParams(window.location.search).get(CONFIG.query_param);
    return fromQuery || localStorage.getItem(ACTIVE_PERSONA_KEY) || CONFIG.default_persona;
  }

  function getCurrentPersona() {
    return getPersonaByCode(localStorage.getItem(ACTIVE_PERSONA_KEY)) || getPersonaByCode(getInitialPersonaCode()) || CONFIG.personas[0];
  }

  function getScenario(namespace) {
    const scenario = getCurrentPersona().scenario || {};
    if (!namespace) return clone(scenario);
    return Object.prototype.hasOwnProperty.call(scenario, namespace) ? clone(scenario[namespace]) : null;
  }

  function publicPersona(persona) {
    if (!persona) return null;
    return {
      code: persona.code,
      display_name: persona.display_name,
      selector: clone(persona.selector || {}),
      route_hint: persona.route_hint || null,
      application: clone(persona.application || CONFIG.application),
      user: clone(persona.user),
      roles: clone(persona.user?.roles || []),
      entitlements: clone(persona.entitlement_keys || []),
      scenario: clone(persona.scenario || {})
    };
  }

  function buildPersonaChangeDetail(previousPersona, persona) {
    return {
      persona: publicPersona(persona),
      previous_persona: publicPersona(previousPersona),
      user: clone(persona?.user || null),
      scenario: clone(persona?.scenario || {})
    };
  }

  function emitPersonaChange(previousPersona, persona) {
    const detail = buildPersonaChangeDetail(previousPersona, persona);
    personaChangeListeners.slice().forEach((listener) => {
      try {
        listener(detail);
      } catch (error) {
        console.error('[spaps-dev-auth] persona change listener failed', error);
      }
    });

    if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
      window.dispatchEvent(new CustomEvent('spaps:persona-change', { detail }));
    }
  }

  function onPersonaChange(callback) {
    if (typeof callback !== 'function') {
      return function noopUnsubscribe() {};
    }

    personaChangeListeners.push(callback);
    return function unsubscribe() {
      const index = personaChangeListeners.indexOf(callback);
      if (index >= 0) personaChangeListeners.splice(index, 1);
    };
  }

  function clearPersonaBrowserStorage() {
    PERSONA_LOCAL_STORAGE_KEYS.forEach((key) => {
      localStorage.removeItem(key);
    });
  }

  function persistPersona(persona) {
    if (!persona) return;
    clearPersonaBrowserStorage();
    localStorage.setItem(ACTIVE_PERSONA_KEY, persona.code);
    localStorage.setItem(STORAGE_KEYS.persona, persona.code);
    localStorage.setItem(STORAGE_KEYS.profile, JSON.stringify(persona.user));
    localStorage.setItem(STORAGE_KEYS.roles, JSON.stringify(persona.user.roles || []));
    localStorage.setItem(STORAGE_KEYS.entitlements, JSON.stringify(persona.entitlement_keys || []));
    localStorage.setItem(STORAGE_KEYS.scenario, JSON.stringify(persona.scenario || {}));
    localStorage.setItem(STORAGE_KEYS.application, JSON.stringify(persona.application || CONFIG.application));
    localStorage.setItem(STORAGE_KEYS.runtime, JSON.stringify({
      auth_mode: CONFIG.auth_mode,
      local_mode_active: CONFIG.local_mode_active
    }));
    localStorage.setItem(STORAGE_KEYS.selector, JSON.stringify(persona.selector || {}));
    localStorage.setItem(TOKEN_USER_KEY, JSON.stringify(persona.user));
    localStorage.setItem(TOKEN_ACCESS_KEY, persona.tokens.access_token);
    localStorage.setItem(TOKEN_REFRESH_KEY, persona.tokens.refresh_token);
    localStorage.setItem(LEGACY_USER_KEY, JSON.stringify(persona.user));

    Object.entries(persona.browser?.local_storage || {}).forEach(([key, value]) => {
      localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
    });
  }

  function clearSession() {
    [ACTIVE_PERSONA_KEY, STORAGE_KEYS.persona, STORAGE_KEYS.profile, STORAGE_KEYS.roles, STORAGE_KEYS.entitlements, STORAGE_KEYS.scenario, STORAGE_KEYS.application, STORAGE_KEYS.runtime, STORAGE_KEYS.selector, TOKEN_USER_KEY, TOKEN_ACCESS_KEY, TOKEN_REFRESH_KEY, LEGACY_USER_KEY, ...PERSONA_LOCAL_STORAGE_KEYS].forEach((key) => {
      localStorage.removeItem(key);
    });
  }

  function switchPersona(code, options) {
    const nextPersona = getPersonaByCode(code);
    if (!nextPersona) return null;
    const previousPersona = getCurrentPersona();
    persistPersona(nextPersona);
    emitPersonaChange(previousPersona, nextPersona);
    if (!options || options.reload !== false) window.location.reload();
    return nextPersona;
  }

  function response(body, status) {
    return Promise.resolve(new Response(JSON.stringify(body), {
      status: status || 200,
      headers: { 'Content-Type': 'application/json' }
    }));
  }

  function authEnvelope(persona) {
    return {
      success: true,
      data: {
        access_token: persona.tokens.access_token,
        refresh_token: persona.tokens.refresh_token,
        expires_in: persona.tokens.expires_in,
        token_type: persona.tokens.token_type,
        user: persona.user
      }
    };
  }

  function userEnvelope(persona) {
    return {
      success: true,
      data: {
        user: persona.user
      }
    };
  }

  function entitlementsEnvelope(persona, requestedKey) {
    const matching = requestedKey
      ? persona.entitlements.filter((entry) => entry.entitlement_key === requestedKey)
      : persona.entitlements;

    return {
      success: true,
      data: {
        entitlements: matching,
        count: matching.length
      }
    };
  }

  function entitlementCheckEnvelope(persona, requestedKey) {
    const matching = requestedKey
      ? persona.entitlements.filter((entry) => entry.entitlement_key === requestedKey)
      : persona.entitlements;

    return {
      success: true,
      data: {
        has_entitlement: matching.length > 0,
        entitlements: matching,
        entitlement_key: requestedKey || null
      }
    };
  }

  async function parseJsonBody(input, init) {
    try {
      const request = input instanceof Request ? input.clone() : new Request(String(input), init);
      const text = await request.text();
      return text ? JSON.parse(text) : {};
    } catch {
      return {};
    }
  }

  function resolvePersonaForEmail(email) {
    if (!email) return null;
    return CONFIG.personas.find((persona) => persona.user.email && persona.user.email.toLowerCase() === String(email).toLowerCase()) || null;
  }

  function shouldIntercept(url) {
    const path = url.pathname;
    const supported = path === '/api/auth/login' ||
      path === '/api/auth/register' ||
      path === '/api/auth/logout' ||
      path === '/api/auth/refresh' ||
      path === '/api/auth/user' ||
      path === '/api/entitlements' ||
      path === '/api/entitlements/check';

    if (!supported) return false;
    return url.origin === window.location.origin || url.origin === CONFIG.api_origin;
  }

  function installFetchBridge() {
    if (!originalFetch || window.__spapsDevAuthFetchInstalled) return;

    window.fetch = async function (input, init) {
      const url = toAbsoluteUrl(input);
      if (!shouldIntercept(url)) {
        return originalFetch(input, init);
      }

      const method = String((init && init.method) || (input instanceof Request && input.method) || 'GET').toUpperCase();
      const currentPersona = getCurrentPersona();

      if ((url.pathname === '/api/auth/login' || url.pathname === '/api/auth/register') && method === 'POST') {
        const body = await parseJsonBody(input, init);
        const persona = resolvePersonaForEmail(body.email) || currentPersona;
        persistPersona(persona);
        return response(authEnvelope(persona), 200);
      }

      if (url.pathname === '/api/auth/refresh' && method === 'POST') {
        persistPersona(currentPersona);
        return response(authEnvelope(currentPersona), 200);
      }

      if (url.pathname === '/api/auth/logout' && method === 'POST') {
        clearSession();
        return response({ success: true, message: 'Signed out from dev auth bridge' }, 200);
      }

      if (url.pathname === '/api/auth/user' && method === 'GET') {
        persistPersona(currentPersona);
        return response(userEnvelope(currentPersona), 200);
      }

      if (url.pathname === '/api/entitlements' && method === 'GET') {
        const requestedKey = url.searchParams.get('entitlement_key');
        return response(entitlementsEnvelope(currentPersona, requestedKey), 200);
      }

      if (url.pathname === '/api/entitlements/check' && method === 'GET') {
        const requestedKey = url.searchParams.get('key') || url.searchParams.get('entitlement_key');
        return response(entitlementCheckEnvelope(currentPersona, requestedKey), 200);
      }

      return originalFetch(input, init);
    };

    window.__spapsDevAuthFetchInstalled = true;
  }

  function renderOverlay() {
    if (CONFIG.bridge.ui_enabled === false || document.getElementById('spaps-dev-auth-overlay')) return;
    const container = document.createElement('div');
    container.id = 'spaps-dev-auth-overlay';
    container.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:2147483647;background:#111827;color:#f9fafb;padding:10px 12px;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,0.25);font:12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;display:flex;gap:8px;align-items:center;';

    const label = document.createElement('span');
    label.textContent = 'SPAPS dev auth';
    label.style.fontWeight = '600';

    const select = document.createElement('select');
    select.style.cssText = 'background:#1f2937;color:#f9fafb;border:1px solid #374151;border-radius:8px;padding:4px 8px;';
    CONFIG.personas.forEach((persona) => {
      const option = document.createElement('option');
      option.value = persona.code;
      option.textContent = persona.code;
      select.appendChild(option);
    });
    select.value = getCurrentPersona().code;

    const button = document.createElement('button');
    button.type = 'button';
    button.textContent = 'Switch';
    button.style.cssText = 'background:#f59e0b;color:#111827;border:0;border-radius:8px;padding:4px 8px;cursor:pointer;font-weight:600;';
    button.onclick = function () {
      switchPersona(select.value);
    };

    container.appendChild(label);
    container.appendChild(select);
    container.appendChild(button);
    document.body.appendChild(container);
  }

  const initialPersona = getCurrentPersona();
  persistPersona(initialPersona);
  installFetchBridge();

  window.__SPAPS_DEV_AUTH__ = {
    config: CONFIG,
    listPersonas: function () {
      return CONFIG.personas.map((persona) => ({
        code: persona.code,
        display_name: persona.display_name,
        entitlements: persona.entitlement_keys,
        roles: persona.user.roles || [],
        scenario: persona.scenario || {}
      }));
    },
    getCurrentPersona: getCurrentPersona,
    getCurrentUser: function () {
      return getCurrentPersona().user;
    },
    getScenario: getScenario,
    onPersonaChange: onPersonaChange,
    switchPersona: switchPersona,
    clearSession: clearSession,
    installFetchBridge: installFetchBridge
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', renderOverlay, { once: true });
  } else {
    renderOverlay();
  }
})();\n`;
}

function buildStorageStateArtifact(appConfig, persona, rolesConfig, entitlementsConfig) {
  const user = buildCurrentUser(appConfig, persona, rolesConfig, entitlementsConfig);
  const tokens = buildFixtureTokens(appConfig, persona, rolesConfig.grants?.[persona.code] || []);
  const application = buildApplicationSummary(appConfig, persona);
  const localStorage = [
    {
      name: FIXTURE_KEYS.active_persona,
      value: persona.code,
    },
    {
      name: FIXTURE_KEYS.persona,
      value: persona.code,
    },
    {
      name: FIXTURE_KEYS.profile,
      value: stringifyStorageValue(persona.profile || {}),
    },
    {
      name: FIXTURE_KEYS.roles,
      value: stringifyStorageValue(rolesConfig.grants?.[persona.code] || []),
    },
    {
      name: FIXTURE_KEYS.entitlements,
      value: stringifyStorageValue(entitlementGrantKeys(entitlementsConfig.grants?.[persona.code] || [])),
    },
    {
      name: FIXTURE_KEYS.scenario,
      value: stringifyStorageValue(persona.scenario || {}),
    },
    {
      name: FIXTURE_KEYS.application,
      value: stringifyStorageValue(application),
    },
    {
      name: FIXTURE_KEYS.runtime,
      value: stringifyStorageValue({
        running: appConfig.server.running,
        local_mode_active: appConfig.server.local_mode_active,
        auth_mode: appConfig.auth.mode,
      }),
    },
    {
      name: FIXTURE_KEYS.selector,
      value: stringifyStorageValue(persona.selector || {}),
    },
    {
      name: COMPAT_STORAGE_KEYS.sdk_user,
      value: stringifyStorageValue(user),
    },
    {
      name: COMPAT_STORAGE_KEYS.sdk_access_token,
      value: tokens.access_token,
    },
    {
      name: COMPAT_STORAGE_KEYS.sdk_refresh_token,
      value: tokens.refresh_token,
    },
    {
      name: COMPAT_STORAGE_KEYS.legacy_user,
      value: stringifyStorageValue(user),
    },
  ];

  for (const [key, value] of Object.entries(persona.browser?.local_storage || {})) {
    localStorage.push({
      name: key,
      value: stringifyStorageValue(value),
    });
  }

  return {
    cookies: [],
    origins: [
      {
        origin: resolveStorageOrigin(appConfig.browser.base_url),
        localStorage,
      },
    ],
  };
}

function buildPersonaContext(appConfig, persona, rolesConfig, entitlementsConfig, paths) {
  const user = buildCurrentUser(appConfig, persona, rolesConfig, entitlementsConfig);
  const tokens = buildFixtureTokens(appConfig, persona, rolesConfig.grants?.[persona.code] || []);
  const application = buildApplicationSummary(appConfig, persona);
  return {
    persona: persona.code,
    display_name: persona.display_name,
    base_url: appConfig.browser.base_url,
    route_hint: buildRouteHint(appConfig, persona),
    selector: persona.selector || {},
    profile: persona.profile || {},
    scenario: persona.scenario || {},
    roles: rolesConfig.grants?.[persona.code] || [],
    entitlements: entitlementGrantKeys(entitlementsConfig.grants?.[persona.code] || []),
    user,
    tokens,
    application,
    artifacts: {
      storage_state_path: path.join(paths.browserDir, `${persona.code}.storage-state.json`),
      headers_path: path.join(paths.browserDir, `${persona.code}.headers.json`),
      context_path: path.join(paths.browserDir, `${persona.code}.context.json`),
    },
  };
}

function writePersonaArtifacts({
  rootDir,
  paths,
  appConfig,
  users,
  roles,
  entitlements,
  personaCode = null,
  seedResultsByPersona = {},
  serverSync = null,
}) {
  const selectedCodes = personaCode ? [ensurePersona(users, personaCode).code] : users.personas.map((persona) => persona.code);
  const generated = [];

  for (const code of selectedCodes) {
    const persona = ensurePersona(users, code);
    const storageState = buildStorageStateArtifact(appConfig, persona, roles, entitlements);
    const headers = buildHeaderArtifact(appConfig, persona);
    const context = buildPersonaContext(appConfig, persona, roles, entitlements, paths);
    context.seed = seedResultsByPersona[code] || null;
    context.server_sync = serverSync?.personas?.find((entry) => entry.code === code) || null;

    writeJson(context.artifacts.storage_state_path, storageState);
    writeJson(context.artifacts.headers_path, headers);
    writeJson(context.artifacts.context_path, context);

    generated.push({
      persona: code,
      storage_state_path: context.artifacts.storage_state_path,
      headers_path: context.artifacts.headers_path,
      context_path: context.artifacts.context_path,
      route_hint: context.route_hint,
    });
  }

  writeJson(paths.lock, {
    schema_version: FIXTURE_SCHEMA_VERSION,
    applied_at: new Date().toISOString(),
    personas: generated.map((entry) => entry.persona),
    base_url: appConfig.browser.base_url,
    api_url: appConfig.server.api_url,
    local_mode_active: appConfig.server.local_mode_active,
    ownership: serverSync
      ? {
          source: serverSync.source || 'spaps-fixtures',
          application: {
            id: serverSync.application.id,
            slug: serverSync.application.slug,
          },
          personas: serverSync.personas.map((entry) => ({
            code: entry.code,
            user_id: entry.user.id,
            entitlements: entry.entitlements,
          })),
        }
      : null,
  });

  return generated;
}

function writeDevAuthBridge({ rootDir, paths, appConfig, users, roles, entitlements }) {
  const bridgeConfig = buildBridgeConfig(appConfig, users, roles, entitlements);
  fs.mkdirSync(paths.publicDir, { recursive: true });
  fs.writeFileSync(paths.bridgeScript, buildDevAuthBridgeScript(bridgeConfig));

  return {
    script_path: paths.bridgeScript,
    public_dir: paths.publicDir,
    script_name: appConfig.bridge?.script_name || DEFAULT_BRIDGE_SCRIPT_NAME,
    public_url: `/${appConfig.bridge?.script_name || DEFAULT_BRIDGE_SCRIPT_NAME}`,
  };
}

async function applyFixtures({
  dir = null,
  port = DEFAULT_PORT,
  baseUrl = null,
  version = '0.0.0',
  persona = null,
  seed = false,
  syncServer = false,
  subcommand = 'apply',
} = {}) {
  const rootDir = resolveRepoRoot(dir);
  const paths = resolveFixturePaths(rootDir);
  let bootstrapped = false;

  if (!fs.existsSync(paths.fixtureDir)) {
    await initFixtureKernel({ dir: rootDir, port, baseUrl, version, force: false });
    bootstrapped = true;
  }

  const kernel = loadFixtureKernel(rootDir);
  const runtime = await getServerRuntime({ port });
  const starterContract = readExistingStarterContract(paths);
  const appConfig = buildAppConfig({
    version,
    port,
    baseUrl: baseUrl || kernel.app.browser?.base_url,
    runtime,
    starterContract,
  });

  writeJson(paths.app, appConfig);
  writeManagedFile(paths.browserGitignore, '*.json\n!.gitignore\n', {
    rootDir,
    files_created: [],
    files_overwritten: [],
    files_skipped: [],
  });

  const fixtureRunState = buildFixtureRunState(appConfig, runtime);
  let resolvedUsers = {
    ...kernel.users,
    personas: kernel.users.personas.map((entry) => resolvePersonaDefinition(appConfig, entry, fixtureRunState)),
  };
  const resolvedRoles = resolveConfigTemplates(kernel.roles, fixtureRunState);
  const resolvedEntitlements = resolveConfigTemplates(kernel.entitlements, fixtureRunState);
  const serverSync = syncServer
    ? await runFixtureServerSync({
        appConfig,
        runtime,
        usersConfig: resolvedUsers,
        rolesConfig: resolvedRoles,
        entitlementsConfig: resolvedEntitlements,
        personaCode: persona,
        port,
      })
    : null;
  if (serverSync) {
    resolvedUsers = applyServerSnapshotToUsers(resolvedUsers, appConfig, serverSync);
    writeJson(paths.app, appConfig);
  }
  const seeding = seed
    ? await runFixtureSeeding({
        appConfig,
        runtime,
        usersConfig: resolvedUsers,
        personaCode: persona,
        fixtureRunState,
        port,
      })
    : null;
  if (seeding) {
    resolvedUsers = {
      ...kernel.users,
      personas: kernel.users.personas.map((entry) => resolvePersonaDefinition(appConfig, entry, fixtureRunState)),
    };
    if (serverSync) {
      resolvedUsers = applyServerSnapshotToUsers(resolvedUsers, appConfig, serverSync);
    }
  }
  const seedResultsByPersona = Object.fromEntries(
    (seeding?.personas || []).map((entry) => [entry.persona, entry])
  );

  const generated = writePersonaArtifacts({
    rootDir,
    paths,
    appConfig,
    users: resolvedUsers,
    roles: resolvedRoles,
    entitlements: resolvedEntitlements,
    personaCode: persona,
    seedResultsByPersona,
    serverSync,
  });
  const bridge = writeDevAuthBridge({
    rootDir,
    paths,
    appConfig,
    users: resolvedUsers,
    roles: resolvedRoles,
    entitlements: resolvedEntitlements,
  });

  return {
    success: true,
    command: 'fixtures',
    subcommand,
    root_dir: rootDir,
    fixture_dir: paths.fixtureDir,
    bootstrapped,
    runtime,
    server_sync: serverSync,
    seeding,
    generated: {
      personas: generated,
      bridge,
    },
    next_steps: persona
      ? [
          `Use ${generated[0].storage_state_path} as Playwright storageState`,
          `Use ${generated[0].headers_path} for extraHTTPHeaders when needed`,
          `Include ${bridge.public_url} before your app boots when you want frontend-only persona switching`,
        ]
      : [
          'Wire the generated storage-state and headers files into Playwright or your local harness',
          `Include ${bridge.public_url} before your app boots to get frontend-only auth/RBAC fixtures`,
          'Re-run npx spaps fixtures apply after editing .spaps/*.json',
        ],
  };
}

async function exportStorageState(options = {}) {
  const result = await applyFixtures({ ...options, subcommand: 'storage-state' });
  const personaArtifact = result.generated.personas[0];
  return {
    success: true,
    command: 'fixtures',
    subcommand: 'storage-state',
    root_dir: result.root_dir,
    fixture_dir: result.fixture_dir,
    persona: personaArtifact.persona,
    storage_state_path: personaArtifact.storage_state_path,
    headers_path: personaArtifact.headers_path,
    context_path: personaArtifact.context_path,
    route_hint: personaArtifact.route_hint,
    bridge: result.generated.bridge,
    runtime: result.runtime,
    server_sync: result.server_sync,
    seeding: result.seeding,
    next_steps: result.next_steps,
  };
}

async function resetFixtures({
  dir = null,
  port = DEFAULT_PORT,
  baseUrl = null,
  version = '0.0.0',
  seed = false,
  syncServer = false,
} = {}) {
  const rootDir = resolveRepoRoot(dir);
  const paths = resolveFixturePaths(rootDir);
  const removed = cleanupGeneratedBrowserArtifacts(paths);
  const initResult = await initFixtureKernel({
    dir: rootDir,
    port,
    baseUrl,
    version,
    force: true,
  });
  const applyResult = await applyFixtures({
    dir: rootDir,
    port,
    baseUrl,
    version,
    seed,
    syncServer,
  });

  return {
    success: true,
    command: 'fixtures',
    subcommand: 'reset',
    root_dir: rootDir,
    fixture_dir: paths.fixtureDir,
    removed,
    files_created: initResult.files_created,
    files_overwritten: initResult.files_overwritten,
    generated: applyResult.generated,
    runtime: applyResult.runtime,
    server_sync: applyResult.server_sync,
    seeding: applyResult.seeding,
    next_steps: applyResult.next_steps,
  };
}

module.exports = {
  DEFAULT_BROWSER_BASE_URL,
  FIXTURE_DIRNAME,
  FIXTURE_KEYS,
  FIXTURE_SCHEMA_VERSION,
  applyFixtures,
  exportStorageState,
  initFixtureKernel,
  resetFixtures,
};