403Webshell
Server IP : 35.80.110.71  /  Your IP : 216.73.216.221
Web Server : Apache/2.4.58 (Ubuntu)
System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64
User : ubuntu ( 1000)
PHP Version : 8.3.31
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /tmp/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /tmp//websites.php.new
<?php

declare(strict_types=1);

/**
 * ScopeForged Websites Product Configuration
 *
 * Configuration for websites.scopeforged.com. Phase 0 ships a single-page
 * landing with a Calendly inline embed and lead form. Later phases extend
 * this config (pricing engine, builder, provisioning).
 *
 * @see docs/plans/website-hosting/270-scopeforged-websites-engineering-plan.md
 */

return [

    // Plan 325 §Phase 6 — global kill-switch for the builder's live preview
    // patch + persistent-selection behavior. Fleet-wide off (migration-free
    // rollback) when false; when true, the per-Website `preview_live_patch`
    // feature flag (default off) is the real gate. Defaults true so the
    // per-tenant flag governs rollout.
    'preview_live_patch' => filter_var(env('WEBSITES_PREVIEW_LIVE_PATCH', true), FILTER_VALIDATE_BOOL),

    // Plan 324 §Phase 1 — global kill-switch for on-canvas inline text editing
    // (click a heading/subhead/intro in the preview and type). Fleet-wide off
    // (migration-free rollback) when false; when true, the per-Website
    // `on_canvas_inline_edit` feature flag (default off) is the real gate.
    // Defaults true so the per-tenant flag governs rollout.
    'on_canvas_inline_edit' => filter_var(env('WEBSITES_ON_CANVAS_INLINE_EDIT', true), FILTER_VALIDATE_BOOL),

    // Plan 300 §Phase 300-E-ii — store order PII retention. Terminal orders
    // older than this are anonymized by websites:anonymize-expired-store-orders.
    'store' => [
        'order_pii_retention_days' => (int) env('WEBSITES_STORE_ORDER_PII_RETENTION_DAYS', 365),
    ],

    // Plan 344 §B.4 — DIY self-serve "build free" guardrails.
    // A draft is unbilled and never publicly served, so its cost is bounded by
    // (a) the per-account concurrent-draft cap and (b) the lowest-tier asset
    // quota (AssetQuotaPolicy). Abandoned drafts are reclaimed after the TTL by
    // ReclaimAbandonedDraftsJob so the subsidy substrate isn't held hostage.
    'diy' => [
        'max_drafts_per_account' => (int) env('WEBSITES_DIY_MAX_DRAFTS_PER_ACCOUNT', 3),
        'draft_ttl_days' => (int) env('WEBSITES_DIY_DRAFT_TTL_DAYS', 30),

        // Plan 344 §F.1 — subsidize-with-cap (Decision #9). DIY sits on CF
        // Enterprise; the subsidy is bounded by a HARD cap on concurrent live
        // DIY tenants (active/past_due diy-* subscriptions). When the cap is
        // reached, new go-lives are waitlisted rather than provisioned. The
        // number is PROVISIONAL — sized to the subsidy budget (pricing decision,
        // still open) — and overridable per-env.
        'max_live_tenants' => (int) env('WEBSITES_DIY_MAX_LIVE_TENANTS', 25),

        // Plan 344 §C.2 — metered usage pass-through. Each logical meter maps
        // to a Stripe Billing Meter (`event_name` matches the meter configured
        // in the Stripe Dashboard; the metered Price feeding that meter is added
        // to the subscription as an addon item). Usage is recorded locally then
        // pushed to Stripe's Meter Events API and reconciled independently.
        //
        // `event_name`  — Stripe Billing Meter event_name (server → Stripe).
        // `label`       — human label for portal usage surfaces.
        'metered_meters' => [
            'sms' => [
                'event_name' => env('WEBSITES_METER_SMS_EVENT', 'website_sms_sent'),
                'label' => 'SMS messages',
            ],
            'email' => [
                'event_name' => env('WEBSITES_METER_EMAIL_EVENT', 'website_email_sent'),
                'label' => 'Emails sent',
            ],
        ],

        // Reconciler: rows unreported this many minutes after occurring are
        // considered stuck and retried; rows exceeding max_report_attempts are
        // surfaced (never silently dropped — the ledger stays the source of truth).
        'usage_stuck_after_minutes' => (int) env('WEBSITES_USAGE_STUCK_AFTER_MINUTES', 15),
        'usage_max_report_attempts' => (int) env('WEBSITES_USAGE_MAX_REPORT_ATTEMPTS', 8),
    ],

    // Plan 301 §Phase 301-C — AI chat runtime defaults.
    'ai_chat' => [
        'model' => env('WEBSITES_AI_CHAT_MODEL', 'claude-haiku-4-5-20251001'),
        'temperature' => (float) env('WEBSITES_AI_CHAT_TEMPERATURE', 0.4),
        'max_output_tokens' => (int) env('WEBSITES_AI_CHAT_MAX_TOKENS', 600),
        // Per-conversation cap on visitor turns — blunts budget runaway
        // from a chatty (or abusive) visitor.
        'max_visitor_messages' => (int) env('WEBSITES_AI_CHAT_MAX_MESSAGES', 40),
        // How many recent messages to include as context.
        'history_window' => (int) env('WEBSITES_AI_CHAT_HISTORY', 20),
        // Retrieval-augmented grounding (Plan 301-D).
        'embeddings' => [
            'driver' => env('WEBSITES_AI_CHAT_EMBEDDINGS_DRIVER', 'fake'), // fake | openai
            'api_key' => env('WEBSITES_AI_CHAT_EMBEDDINGS_KEY', ''),
            'model' => env('WEBSITES_AI_CHAT_EMBEDDINGS_MODEL', 'text-embedding-3-small'),
        ],
        'retrieval_top_k' => (int) env('WEBSITES_AI_CHAT_TOP_K', 4),
        'chunk_tokens' => (int) env('WEBSITES_AI_CHAT_CHUNK_TOKENS', 200),
        // Visitor-conversation retention: conversations idle longer than this
        // are pruned by websites:prune-chat-conversations (falls back to each
        // store's ai_chat_settings.retention_days when set).
        'conversation_retention_days' => (int) env('WEBSITES_AI_CHAT_RETENTION_DAYS', 90),
    ],
    /*
    |--------------------------------------------------------------------------
    | Calendly Inline Embed URL
    |--------------------------------------------------------------------------
    |
    | Public Calendly event URL embedded on the landing. Leave empty to hide
    | the embed entirely (the "book a call" CTA falls back to a plain mailto
    | link). Setting WEBSITES_CALENDLY_URL=https://calendly.com/... enables it.
    |
    */
    'calendly_url' => env('WEBSITES_CALENDLY_URL', ''),

    /*
    |--------------------------------------------------------------------------
    | Contact Email
    |--------------------------------------------------------------------------
    |
    | Fallback contact address shown on the landing when Calendly is disabled
    | and as a "prefer email" option alongside the form.
    |
    */
    'contact_email' => env('WEBSITES_CONTACT_EMAIL', env('MARKETING_CONTACT_EMAIL', 'hello@scopeforged.com')),

    /*
    |--------------------------------------------------------------------------
    | Marketing Copy
    |--------------------------------------------------------------------------
    |
    | Strings rendered on the landing. Centralized so non-engineering edits
    | live in one place and don't require touching the Blade view.
    |
    */
    /*
    |--------------------------------------------------------------------------
    | Builder assets
    |--------------------------------------------------------------------------
    |
    | Storage disk for builder-session asset uploads (logos, etc.). Plan 270
    | §Phase 2 stores binaries under tenants/anon/{session}/... or
    | tenants/{client_id}/builder/{session}/... on S3 in production. The
    | `scan_inline_for_dev` flag controls whether the asset row is marked
    | clean at upload time (dev/scaffolding) or stays pending until a
    | future virus-scan job flips it (production).
    |
    */
    'assets' => [
        'disk' => env('WEBSITES_ASSETS_DISK', 'public'),
        // Plan 309 Phase 1a — non-embedded assets (kind = other: downloadable
        // files, admin-only previews) land here instead of the public disk and
        // are served only through an ownership-scoped signed route. Embedded
        // images/logos stay on `disk` so their baked-into-HTML URLs keep working.
        // `local` (serve=true) yields working temporaryUrl()s in dev/test; point
        // this at `s3` in production for real presigned access.
        'private_disk' => env('WEBSITES_ASSETS_PRIVATE_DISK', 'local'),
        'signed_url_ttl' => (int) env('WEBSITES_ASSETS_SIGNED_URL_TTL', 15),
        'scan_inline_for_dev' => env('WEBSITES_ASSETS_SCAN_INLINE_FOR_DEV', true),
    ],

    /*
    |--------------------------------------------------------------------------
    | Proposals
    |--------------------------------------------------------------------------
    |
    | Storage disk for rendered proposal PDFs. The PDF is persisted so the
    | customer-facing page can stream it without re-rendering on every hit.
    | Default validity window applies when admin doesn't specify one.
    |
    */
    'proposals' => [
        'disk' => env('WEBSITES_PROPOSALS_DISK', env('FILESYSTEM_DISK', 'local')),
        'default_validity_days' => env('WEBSITES_PROPOSAL_VALIDITY_DAYS', 14),
    ],

    /*
    |--------------------------------------------------------------------------
    | Data exports (plan 270 §Phase 4)
    |--------------------------------------------------------------------------
    |
    | Per GDPR Article 20 every paying customer can download all data we
    | hold about them. The job lands in the `client-portal-default` queue
    | per the plan; storage disk defaults to the global filesystem disk so
    | S3 in prod / local in dev "just works" without per-feature env vars.
    |
    */
    'exports' => [
        'disk' => env('WEBSITES_EXPORTS_DISK', env('FILESYSTEM_DISK', 'local')),
        'queue' => env('WEBSITES_EXPORTS_QUEUE', 'client-portal-default'),
    ],

    /*
    |--------------------------------------------------------------------------
    | AI content generation (plan 270 §Phase 6)
    |--------------------------------------------------------------------------
    |
    | `provider` chooses the bound ContentGenerator implementation:
    |   - "fake"       (default for dev + CI)
    |   - "anthropic"  primary in production
    |   - "openai"     documented fallback
    |
    | Setting `WEBSITES_AI_PROVIDER=anthropic` in env switches production.
    | `default_monthly_token_budget` is the per-tenant hard cap unless an
    | ai_usage_quotas row overrides it for the (client_id, year_month).
    |
    */
    'ai' => [
        'provider' => env('WEBSITES_AI_PROVIDER', 'fake'),
        'fallback_provider' => env('WEBSITES_AI_FALLBACK_PROVIDER', 'openai'),
        'default_monthly_token_budget' => env('WEBSITES_AI_DEFAULT_MONTHLY_BUDGET', 200_000),

        // Plan 323 — AI authoring in the builder (per-field Rewrite/Generate +
        // SEO meta). Free to all tenants; the cost model is platform-key +
        // per-client token quota (above) + the per-request/rate caps below.
        //
        // `authoring_enabled` is a fleet-wide kill-switch; the per-Website
        // `ai_authoring` feature flag (default off) is the real rollout gate.
        'authoring_enabled' => filter_var(env('WEBSITES_AI_AUTHORING_ENABLED', true), FILTER_VALIDATE_BOOL),
        'authoring' => [
            // Per-request denial-of-wallet caps (Plan 323 §Phase 2.1) — on top
            // of the monthly token quota.
            'max_output_tokens' => (int) env('WEBSITES_AI_AUTHORING_MAX_TOKENS', 600),
            'rate_limit_per_minute' => (int) env('WEBSITES_AI_AUTHORING_RATE_LIMIT', 10),
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Cost monitoring (plan 272 §A.2)
    |--------------------------------------------------------------------------
    |
    | `ops_alert_email` is the address that receives the 50% rung
    | TenantCostGuard alert. Empty disables the ops rung (the alert
    | still records in tenant_cost_alerts_sent so duplicate hourly
    | runs don't refire).
    |
    */
    'costs' => [
        'ops_alert_email' => env('WEBSITES_COSTS_OPS_ALERT_EMAIL', ''),
    ],

    /*
    |--------------------------------------------------------------------------
    | Security (plan 272 §B.3, §B.4)
    |--------------------------------------------------------------------------
    |
    | `require_mfa_for_new_customers` (default true) makes
    | WebsiteOnboardingCompletionService stamp a `requires_mfa_setup_by`
    | deadline on the User row; the `RequireMfaSetup` middleware
    | redirects past the deadline.
    |
    | Session idle-timeout minutes are role-derived; the
    | `EnforceSessionIdleTimeout` middleware logs the session out
    | after the threshold.
    |
    */
    'security' => [
        'require_mfa_for_new_customers' => env('WEBSITES_REQUIRE_MFA_FOR_NEW_CUSTOMERS', true),
        'mfa_setup_grace_days' => env('WEBSITES_MFA_SETUP_GRACE_DAYS', 7),
        'session_idle_timeout_customer_minutes' => env('WEBSITES_SESSION_IDLE_CUSTOMER_MIN', 720),
        'session_idle_timeout_admin_minutes' => env('WEBSITES_SESSION_IDLE_ADMIN_MIN', 240),
    ],

    /*
    |--------------------------------------------------------------------------
    | Mail compliance (plan 270 §Phase 7)
    |--------------------------------------------------------------------------
    |
    | Physical address rendered in every customer-facing email's CAN-SPAM
    | footer. CanSpamFooterEnforcer asserts the address is present in
    | every rendered body.
    |
    */
    'mail' => [
        'physical_address' => env('WEBSITES_MAIL_PHYSICAL_ADDRESS', 'ScopeForged Inc., PO Box change-me, City, ST'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Legal-process intake (Plan 295 §12)
    |--------------------------------------------------------------------------
    |
    | Where valid subpoenas / court orders / warrants are served. Counsel-
    | defined — the placeholder must be replaced before launch.
    |
    */
    'legal' => [
        'process_address' => env('WEBSITES_LEGAL_PROCESS_ADDRESS', 'ScopeForged Inc., c/o Registered Agent, PO Box change-me, City, ST'),
        'process_email' => env('WEBSITES_LEGAL_PROCESS_EMAIL', 'legal@scopeforged.com'),
    ],

    /*
    |--------------------------------------------------------------------------
    | CSP (plan 270 §Phase 7)
    |--------------------------------------------------------------------------
    |
    | Platform-level baseline merged with per-template extensions by
    | CspHeaderComposer. Source allowlists prevent templates from
    | silently introducing new third-party dependencies.
    |
    */
    'csp' => [
        'baseline' => [
            'default-src' => ["'self'"],
            'script-src' => ["'self'"],
            'frame-src' => ["'none'"],
            'img-src' => ["'self'", 'data:', 'https://cdn.scopeforged.com'],
            'style-src' => ["'self'", "'unsafe-inline'"],
            'connect-src' => ["'self'"],
            'font-src' => ["'self'", 'data:'],
            'object-src' => ["'none'"],
        ],
        'report_uri' => env('WEBSITES_CSP_REPORT_URI', '/csp-report'),
        'report_only_by_default' => env('WEBSITES_CSP_REPORT_ONLY', true),

        /*
        | Add-on CSP requirements (plan 308 §Phase 1)
        |
        | An add-on module can declare the CSP sources it needs, keyed by
        | `addon_key`. When the add-on is active, CspHeaderComposer merges
        | these between the template layer and the tenant-override layer:
        |
        |   baseline ← template extensions ← ACTIVE ADD-ONS ← tenant overrides
        |
        | Add-on sources are subject to the exact same allowlist +
        | forbidden-token guards as every other layer, so an add-on can no
        | more introduce 'unsafe-inline' or a non-allowlisted host than a
        | template can. Shape:
        |
        |   'addon_requirements' => [
        |       'live-chat' => [
        |           'script-src'  => ['https://widget.example.com'],
        |           'connect-src' => ['https://api.example.com'],
        |           'frame-src'   => ['https://widget.example.com'],
        |       ],
        |   ],
        */
        'addon_requirements' => [],
    ],

    /*
    |--------------------------------------------------------------------------
    | Analytics
    |--------------------------------------------------------------------------
    |
    | Server-side event capture per plan 270 §Phase 1. The salt is used to
    | hash IP and User-Agent so the analytics table never stores raw PII;
    | rotating the salt invalidates the ability to link future events to
    | past hashed identifiers (an intentional privacy property).
    |
    */
    'analytics' => [
        'enabled' => env('WEBSITES_ANALYTICS_ENABLED', true),
        'hash_salt' => env('WEBSITES_ANALYTICS_HASH_SALT', env('APP_KEY', 'change-me-in-env')),
        'excluded_paths' => [
            'sitemap.xml',
            'robots.txt',
            'leads',
        ],

        // Plan 294 §Phase G — customer-facing analytics (CF Web Analytics +
        // GA4 passthrough). `driver` selects the CF stats client: `cloudflare`
        // hits the GraphQL API, anything else binds the in-memory fake (which
        // returns an empty summary), so CI never touches CF.
        'customer' => [
            'driver' => env('WEBSITES_CUSTOMER_ANALYTICS_DRIVER', 'fake'), // cloudflare|fake
        ],
    ],

    'copy' => [
        'hero_eyebrow' => 'ScopeForged Websites',
        'hero_headline' => 'Done-for-you websites for service businesses.',
        'hero_subhead' => 'Conversion-focused sites, transparent pricing, no plugin rabbit holes. We design it, build it, and keep it running so you can stay focused on the work.',
        'primary_cta' => 'Book a 20-minute call',
        'secondary_cta' => 'Send us a brief',
        'response_promise' => 'We reply within one business day.',
    ],

    /*
    |--------------------------------------------------------------------------
    | DNS + Cache driver wiring (Plan 273 §A)
    |--------------------------------------------------------------------------
    |
    | Production binds the real Cloudflare classes; tests + dev fall back to
    | the in-memory fakes. Switching is a one-line env change (`fake` vs
    | `cloudflare`) — no code edits.
    |
    */
    'dns' => [
        'driver' => env('WEBSITES_DNS_DRIVER', 'fake'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Custom-domain self-service (Plan 279)
    |--------------------------------------------------------------------------
    |
    | `self_serve_enabled` kill-switch — flip to false to block new attaches
    | without breaking existing Domain rows. Per-Client granularity is a
    | follow-up. `public_ipv4`/`public_ipv6` are surfaced to customers in
    | the wizard's "Required DNS records" panel. `default_grace_days` is
    | the pre-selected option in the remove modal. The reverify ladder
    | (30d / 60d / 90d) is enforced by ReverifyDomainOwnershipJob.
    |
    */
    'custom_domain' => [
        'self_serve_enabled' => env('WEBSITES_CUSTOM_DOMAIN_SELF_SERVE', true),
        'public_ipv4' => array_values(array_filter(array_map('trim', explode(',', (string) env('WEBSITES_PUBLIC_IPV4', ''))))),
        'public_ipv6' => array_values(array_filter(array_map('trim', explode(',', (string) env('WEBSITES_PUBLIC_IPV6', ''))))),
        'default_grace_days' => (int) env('WEBSITES_CUSTOM_DOMAIN_DEFAULT_GRACE_DAYS', 30),
        'reverify_interval_days' => (int) env('WEBSITES_REVERIFY_INTERVAL_DAYS', 30),
        'reverify_failure_threshold_days' => (int) env('WEBSITES_REVERIFY_FAILURE_THRESHOLD_DAYS', 60),
        'reverify_stale_threshold_days' => (int) env('WEBSITES_REVERIFY_STALE_THRESHOLD_DAYS', 90),
        'restart_cap_per_24h' => (int) env('WEBSITES_DOMAIN_RESTART_CAP', 10),
    ],

    'cache_purge' => [
        'driver' => env('WEBSITES_CACHE_PURGE_DRIVER', 'fake'),
        'zone_id' => env('WEBSITES_CLOUDFLARE_ZONE_ID', ''),
    ],

    // Plan 294 line 376/769 — Stripe webhook idempotency-table hygiene.
    'webhooks' => [
        // Retain processed webhook-event rows this long (covers Stripe's 3-day
        // retry plus a generous incident-replay window), then prune.
        'event_retention_days' => (int) env('WEBSITES_WEBHOOK_EVENT_RETENTION_DAYS', 90),
        // Chunk size for the pruning delete so we don't hold long lock waits.
        'cleanup_chunk' => (int) env('WEBSITES_WEBHOOK_CLEANUP_CHUNK', 5000),
        // Sudden row-count growth ⇒ likely a handler regression backing rows up.
        'row_count_alert_threshold' => (int) env('WEBSITES_WEBHOOK_ROW_COUNT_ALERT', 100000),
    ],

    /*
    |--------------------------------------------------------------------------
    | Apache SNI snippet regeneration (Plan 273 §D.1)
    |--------------------------------------------------------------------------
    |
    | Path the catch-all vhost includes for per-host cert directives. Dry-run
    | is on by default so a misconfigured stub can't take Apache down on the
    | first deploy; flip via env once the rendered output looks right.
    |
    */
    'apache' => [
        'ssl_domains_conf_path' => env('WEBSITES_APACHE_SSL_DOMAINS_CONF', '/etc/apache2/ssl-domains.conf'),
        'reload_command' => env('WEBSITES_APACHE_RELOAD_COMMAND', '/usr/sbin/apachectl graceful'),
        'reload_dry_run' => env('WEBSITES_APACHE_RELOAD_DRY_RUN', true),
        'queue' => env('WEBSITES_APACHE_QUEUE', 'client-portal-default'),
    ],

    /*
    |--------------------------------------------------------------------------
    | SMS gateway routing (Plan 273 §B.3)
    |--------------------------------------------------------------------------
    */
    'sms' => [
        'per_tenant_rate_per_minute' => env('WEBSITES_SMS_PER_TENANT_RPM', 60),
    ],

    /*
    |--------------------------------------------------------------------------
    | Backup integrity (Plan 273 §F.1)
    |--------------------------------------------------------------------------
    */
    'backup' => [
        'inventory_disk' => env('WEBSITES_BACKUP_INVENTORY_DISK', 'backups'),
        'asset_disk' => env('WEBSITES_BACKUP_ASSET_DISK', 'assets'),
        'queue' => env('WEBSITES_BACKUP_QUEUE', 'client-portal-default'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Per-tenant content search (Plan 273 §G.2)
    |--------------------------------------------------------------------------
    |
    | MysqlSearchBackend is the primary; Meilisearch is the mirror during
    | the FULLTEXT -> Meilisearch transition. Once SearchParityCheckJob
    | (§G.3) confirms >= 99% top-K parity for 7 days per tenant, the
    | enabled_clients list flips reads to Meilisearch for that client_id.
    |
    */
    'search' => [
        'meilisearch' => [
            'url' => env('WEBSITES_MEILISEARCH_URL', 'http://127.0.0.1:7700'),
            'master_key' => env('WEBSITES_MEILISEARCH_MASTER_KEY', ''),
            'mirror_writes' => (bool) env('WEBSITES_MEILISEARCH_MIRROR_WRITES', false),
            'enabled_clients' => array_filter(array_map('intval', explode(',', (string) env('WEBSITES_MEILISEARCH_CLIENTS', '')))),
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Subdomain provisioner (Plan 294 Phase B-1 / Decision Lock #10)
    |--------------------------------------------------------------------------
    |
    | The driver behind the SubdomainProvisioner contract — writes A records
    | on `{slug}.{parent}` against the parent's Route53 hosted zone.
    |
    |   - 'fake'    (default): in-memory FakeSubdomainProvisioner for tests
    |                          and local development.
    |   - 'route53'          : production Route53SubdomainProvisioner. Requires
    |                          aws/aws-sdk-php installed (composer require ...).
    |
    | Defaulting to 'fake' is intentional — an environment without the AWS
    | SDK installed should fail loudly at boot if it's misconfigured to
    | route53, not silently NXDOMAIN on the first customer signup.
    |
    */
    'subdomain_provisioner' => [
        'driver' => env('WEBSITES_SUBDOMAIN_PROVISIONER_DRIVER', 'fake'),
        'aws_region' => env('WEBSITES_SUBDOMAIN_PROVISIONER_AWS_REGION', 'us-east-1'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Parent domains we own (Plan 294 Phase B-1)
    |--------------------------------------------------------------------------
    |
    | The domains under which we offer subdomains via the zero-touch path.
    | Each parent has its own Route53 hosted zone + ACM (Phase A) or Certbot
    | (Stage 0) wildcard cert. At Stage 0 we ship with one parent and add
    | more once the funnel is proven.
    |
    | Each entry mirrors the parent_domains table seed. Adding a new parent
    | is config + one wildcard cert + one IAM-policy line, not code.
    |
    */
    'parent_domains' => [
        // Note: `scopeforged.com` itself is NOT a tenant parent - it's
        // the operational apex (marketing + portal + admin live on it).
        // Tenants on the scopeforged.* surface go under the explicit
        // `site.scopeforged.com` subdomain instead.
        [
            'name' => 'site.scopeforged.com',
            // site.scopeforged.com lives inside the scopeforged.com Route53
            // zone (no separate zone). The provisioner writes A records
            // for `{slug}.site.scopeforged.com` into the same hosted zone.
            'route53_hosted_zone_id' => env('WEBSITES_PARENT_SITE_SCOPEFORGED_ZONE_ID', env('WEBSITES_PARENT_SCOPEFORGED_ZONE_ID', '')),
            'is_active' => true,
            'is_default' => true,
        ],
        [
            'name' => 'dcsuniverse.com',
            'route53_hosted_zone_id' => env('WEBSITES_PARENT_DCSUNIVERSE_ZONE_ID', ''),
            'is_active' => true,
            'is_default' => false,
        ],
        [
            'name' => 'femalelarrybird.com',
            'route53_hosted_zone_id' => env('WEBSITES_PARENT_FEMALELARRYBIRD_ZONE_ID', ''),
            'is_active' => true,
            'is_default' => false,
        ],
        // Staging-only tenant parent — needed so the CSP frame-src allowlist
        // in SecurityHeaders includes the staging wildcard, otherwise the
        // builder preview iframe on staging is blocked from framing
        // `<slug>.staging.websites.scopeforged.com`. Gated on
        // WEBSITES_STAGING_TENANTS_ENABLED so it stays inert in prod.
        [
            'name' => 'staging.websites.scopeforged.com',
            'route53_hosted_zone_id' => env('WEBSITES_PARENT_SITE_SCOPEFORGED_ZONE_ID', env('WEBSITES_PARENT_SCOPEFORGED_ZONE_ID', '')),
            'is_active' => (bool) env('WEBSITES_STAGING_TENANTS_ENABLED', false),
            'is_default' => false,
        ],
        // Customer-friendly staging tenant parent. Same env gate as above.
        // Prod does NOT set this flag, so the entry stays inactive on prod
        // and doesn't collide with real prod tenants.
        [
            'name' => 'sites.scopeforged.com',
            'route53_hosted_zone_id' => env('WEBSITES_PARENT_SITE_SCOPEFORGED_ZONE_ID', env('WEBSITES_PARENT_SCOPEFORGED_ZONE_ID', '')),
            'is_active' => (bool) env('WEBSITES_STAGING_TENANTS_ENABLED', false),
            'is_default' => false,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Reserved subdomain slugs (Plan 294 Phase B-1)
    |--------------------------------------------------------------------------
    |
    | Slugs a customer can never claim because they collide with operational
    | hostnames. Enforced by ReservedSlugValidator at the wizard AND inside
    | ProvisionSiteJob (defense in depth - a bypass at one layer doesn't put
    | a tenant on a reserved name).
    |
    | Anything matching the underscore-leading regex below is reserved as a
    | CLASS (_dmarc, _domainkey, _acme-challenge, etc. are DNS-convention
    | TXT/CNAME hosts; a tenant claiming _dmarc.scopeforged.com would break
    | DMARC for the parent). The validator applies both checks.
    |
    */
    'reserved_slugs' => [
        'admin', 'api', 'app', 'apps', 'auth',
        'assets', 'beta', 'blog', 'cdn', 'console',
        'dashboard', 'dev', 'devops', 'docs', 'download', 'downloads',
        'email', 'files', 'forum', 'ftp', 'git', 'go',
        'help', 'host', 'images', 'img', 'info', 'js', 'mail',
        'media', 'mobile', 'mx', 'news', 'ns', 'ns1', 'ns2',
        'pay', 'portal', 'preview', 'public', 'private', 'pro',
        'qa', 'release', 'releases', 'review', 'reviews', 'root',
        'sale', 'sales', 'shop', 'signup', 'site', 'sites',
        'smtp', 'staging', 'static', 'stats', 'status', 'store',
        'support', 'system', 'team', 'test', 'tests', 'tools',
        'training', 'try', 'uat', 'updates', 'upgrade', 'uploads',
        'user', 'users', 'vpn', 'webdav', 'webmail', 'www', 'www2',
        // Internal product surface
        'scopeforged', 'websites', 'forge',
    ],

    /*
    |--------------------------------------------------------------------------
    | Reserved slug regex class (Plan 294 Phase B-1)
    |--------------------------------------------------------------------------
    |
    | Anything matching this regex is reserved as a class. Underscore-leading
    | hosts are DNS-convention sentinels (_dmarc, _acme-challenge, _domainkey,
    | _mta-sts, _smtp._tls, ...) - a tenant claiming `_dmarc.scopeforged.com`
    | would break DMARC for the parent.
    |
    */
    'reserved_slug_pattern' => '/^_/',

    /*
    |--------------------------------------------------------------------------
    | Reputation pre-check on first publish (Plan 294 line 278)
    |--------------------------------------------------------------------------
    |
    | Lightweight content scan that runs on the first publish for subdomain
    | tenants — protects the parent-domain reputation from a new customer's
    | first page hosting phishing, malware download links, or content in
    | the highest-risk verticals.
    |
    | Pass = saga proceeds and the customer goes live. Fail = the publish
    | is held in draft + admin notified. The Cloudflare URL Scanner API
    | check is deferred; the v1 implementation uses just the local
    | blocklist + keyword screen below.
    |
    | BYO-domain tenants skip the check — they own their own domain
    | reputation.
    |
    */
    'reputation_check' => [
        'enabled' => (bool) env('WEBSITES_REPUTATION_CHECK_ENABLED', true),

        // Domains/URLs we never want a tenant linking to. Match is
        // substring (lowercased) on each href value in the content.
        'url_blocklist' => [
            // Known phishing-cluster patterns; expand as we learn.
            'login-paypal',
            'verify-account',
            'secure-bank-login',
            'wallet-connect',
        ],

        // Words/phrases that flag the page for admin review. Substring
        // match (lowercased, word-boundary not required). Lean toward
        // false-positives — admin can dismiss quickly; a single false-
        // negative on day 1 can blacklist the parent domain across
        // major spam filters.
        'keywords' => [
            'cashapp giveaway',
            'send your seed phrase',
            'connect your wallet to claim',
            'irs final notice',
            'paypal account suspended',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Per-tenant origin in-flight budgets (Plan 294 Phase B-1 / Phase A)
    |--------------------------------------------------------------------------
    |
    | OriginTenantRateLimiter middleware uses these per-tier in-flight budgets
    | as a Redis token bucket keyed on the tenant id (resolved from Host:).
    | Over-quota requests get a 503 with Retry-After: 1; the counter
    | `tenant.origin.throttled{tenant_id}` is emitted for dashboards.
    |
    | These are the PHASE A defaults. Stage 0 halves them via the
    | `stage_0.origin_throttle.tier_budgets` override below.
    |
    */
    'origin_throttle' => [
        'tier_budgets' => [
            'starter' => 4,
            'growth' => 12,
            'enterprise' => 0, // 0 = unlimited / contract
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Stage 0 starting-posture overrides (Plan 294 §Stage 0, ADR-0021)
    |--------------------------------------------------------------------------
    |
    | These knobs make Stage 0 explicit in config. They're consumed by:
    |   - OriginTenantRateLimiter (tier_budgets override)
    |   - The wizard host-choice controller (signup cap waitlist)
    |   - PayingCustomerCountGauge job (alarm + signup cap trigger thresholds)
    |   - The capacity-ceiling check that fires the Phase A trigger
    |
    | Stage 0 ends when Phase A cutover completes. At that point this whole
    | section is removed (not just set to false) so the production config is
    | as honest as Phase A's operating model.
    |
    */
    'stage_0' => [
        'active' => (bool) env('WEBSITES_STAGE_0_ACTIVE', true),

        // Halved tier budgets to protect the shared FPM pool from any one
        // tenant's burst (the shared pool serves portal + admin + render at
        // Stage 0).
        'origin_throttle' => [
            'tier_budgets' => [
                'starter' => 2,
                'growth' => 6,
                'enterprise' => 12,
            ],
        ],

        // PhaseAReadinessAlarm fires at this paying-customer count (one
        // customer of headroom). Triggers ClickUp task + on-call page; does
        // not auto-flip Phase A.
        'phase_a_readiness_alarm_count' => (int) env('WEBSITES_STAGE_0_PHASE_A_ALARM_COUNT', 4),

        // Hard signup cap. When the paying-customer count reaches this,
        // the wizard goes to a waitlist view behind Pennant flag
        // `waas_stage_0_capacity_full` until Phase A cutover completes.
        'signup_cap_count' => (int) env('WEBSITES_STAGE_0_SIGNUP_CAP_COUNT', 5),

        // Plan 294 line 277 - signup-velocity limits at the wizard.
        // Per-IP + (Phase B-1.5) per-card-fingerprint daily caps to deter
        // bulk-abuse signups. On limit hit, the customer is shown the
        // "let's chat" page instead of a 429 so legit edge cases
        // (office NAT, small agency signing up multiple sites) get a
        // human touchpoint. Card-fingerprint check lands with the Stripe
        // direct-checkout wiring (Phase B-1 line 244 completion).
        'signup_velocity' => [
            'per_ip_per_day' => (int) env('WEBSITES_STAGE_0_SIGNUP_VELOCITY_PER_IP_PER_DAY', 3),
            'per_card_fingerprint_per_day' => (int) env('WEBSITES_STAGE_0_SIGNUP_VELOCITY_PER_CARD_PER_DAY', 5),
        ],

        // Measured ceilings on the shared host. Any one breached fires
        // the Phase A trigger regardless of paying-customer count.
        // Working assumptions from 294-capacity-model.md §Stage 0; replace
        // with measured values in Stage 0 week 1.
        'capacity_ceiling' => [
            'concurrent_renders' => (int) env('WEBSITES_STAGE_0_MAX_CONCURRENT_RENDERS', 20),
            'active_byo_certs' => (int) env('WEBSITES_STAGE_0_MAX_BYO_CERTS', 50),
            'paying_tenants' => (int) env('WEBSITES_STAGE_0_MAX_PAYING_TENANTS', 5),
        ],

        // Customer-facing SLA banner copy. The 2-4h RTO is the published
        // Stage 0 RTO from ADR-0021; honestly stated. Removed on Phase A
        // cutover completion.
        'sla_banner' => [
            'enabled' => (bool) env('WEBSITES_STAGE_0_SLA_BANNER', true),
            'published_rto_hours' => '2-4',
        ],

        // The target IP the ProvisionSiteJob saga writes into the Route53
        // A record for `{slug}.{parent}` at step 2. At Stage 0 there is no
        // ALB — CF origin-pulls direct to the shared EC2's public IP per
        // Decision Lock #11's Stage 0 simplification. At Phase A cutover
        // this value flips to the ALB DNS name (resolved to ALB anycast).
        'subdomain_target_ip' => env('WEBSITES_STAGE_0_SUBDOMAIN_TARGET_IP', '127.0.0.1'),

        // CloudWatch region for the Scopeforged/WaaS namespace. The
        // EmitPayingCustomerCountGauge command writes the PayingCustomers
        // metric here; the PhaseAReadinessAlarm + dashboard widget read
        // from the same region. EC2 sits in us-west-2 by default.
        'cloudwatch_region' => env('WEBSITES_STAGE_0_CLOUDWATCH_REGION', 'us-west-2'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Content retention (Plan 333 §Phase 1)
    |--------------------------------------------------------------------------
    |
    | Builder deletes soft-delete the page/section/block (recoverable via the
    | Undo toast). `websites:purge-trashed-content` force-deletes rows trashed
    | longer than this window through TrashCascadeService (media-ref-safe).
    |
    */
    'retention' => [
        'trashed_builder_content_hours' => (int) env('WEBSITES_TRASHED_CONTENT_RETENTION_HOURS', 24),
    ],

    /*
    |--------------------------------------------------------------------------
    | Backup retention / paths (Plan 294 §Stage 0)
    |--------------------------------------------------------------------------
    |
    | Used by the backup scripts in scripts/backup/. The bucket itself has
    | a 7-day lifecycle policy on the /mysql/ and /letsencrypt/ prefixes
    | so retention is enforced server-side, not client-side.
    |
    */
    'stage_0_backups' => [
        'bucket' => env('WEBSITES_STAGE_0_BACKUP_BUCKET', 'scopeforged-websites-prod-backups'),
        'mysql_prefix' => 'mysql/',
        'letsencrypt_prefix' => 'letsencrypt/',
        'retention_days' => 7,
        'mysql_cron' => '17 * * * *', // matches /etc/cron.d/scopeforged-stage-0-backups
        'letsencrypt_cron' => '23 * * * *',
    ],

    /*
    |--------------------------------------------------------------------------
    | Dunning (Plan 294 §Phase C)
    |--------------------------------------------------------------------------
    |
    | Branded reminder emails during the past_due grace window. `offsets`
    | are days-since-entering-past_due on which a reminder is sent;
    | `suspend_after_days` is when an `unpaid` subscription past grace is
    | suspended by the daily orchestrator.
    */
    'dunning' => [
        'offsets' => [0, 3, 7, 10, 13],
        'suspend_after_days' => (int) env('WEBSITES_DUNNING_SUSPEND_AFTER_DAYS', 14),
    ],

    /*
    |--------------------------------------------------------------------------
    | Tenant email — SES/DKIM (Plan 294 §Phase G, Lock #20)
    |--------------------------------------------------------------------------
    |
    | Per-tenant BYO-domain sending identities. `driver` selects the SES
    | client: `ses` uses the AWS SDK (production), anything else binds the
    | in-memory fake so CI never touches AWS. `dmarc_rua` is the aggregate
    | report mailbox we recommend when a tenant has no DMARC record.
    */
    'email' => [
        'driver' => env('WEBSITES_EMAIL_DRIVER', 'fake'), // ses|fake
        'dmarc_rua' => env('WEBSITES_EMAIL_DMARC_RUA', 'dmarc-aggregate@scopeforged.com'),
    ],
];

Youez - 2016 - github.com/yon3zu
LinuXploit