Deploying JSO AI Phase 1
Hand-off document for whoever flips Phase 1 on. The code is in production today as preview-mode stubs; this page lists everything that has to happen outside the source tree to switch real LLM responses on. The site is engineered so this is the only ritual — no further C# changes are required.
Pre-flip state (what's already shipped)
- Four endpoints:
/v1/ai/preset-suggest.ashx, /v1/ai/compat-check.ashx, /v1/ai/explain-error.ashx, /v1/ai/usage.ashx. All POST-only, 405 elsewhere. Wire envelope locked by ai-wire-format.schema.json and the polyglot smoke harness (8/8 mock + 4/4 live).
- Auth:
AIService.ResolveAccountId(base64ApiKey) parses the standard JSO APIKey format and walks JSODB.APIKey. Same code path as HttpApi.ashx.
- Quota enforcement:
AIService.CheckQuota reads AIDisable, walks vw_AIMonthlyUsage, applies the in-code Tier caps in action / token / cost priority order.
- Usage read:
AIService.GetMonthlyUsage shared by the endpoint, the dashboard widget, and the Prometheus exporter (jso-ai-quota-exporter.js).
- LLM client:
LlmProviderClient.SendChat dispatches to real Anthropic Messages or OpenAI Chat Completions clients. Both inline; both gated by API key config.
- Browser previews: PresetAssistant, CompatCheck, ExplainError — mirror the rule-based engines client-side, run without auth.
- Dashboard widget: /dashboard/AIUsage.aspx with three states (schema absent / no subscription / active), plus Subscribe-now buttons that POST to checkout-create and a post-checkout success banner driven by
?subscribed=1.
- Stripe wiring:
/v1/ai/checkout-create.ashx (Stripe Checkout Sessions), /v1/ai/stripe-webhook.ashx (HMAC-SHA256-verified event receiver, routes checkout.session.completed + customer.subscription.* events into AISubscription and captures the Customer ID), and /v1/ai/portal-create.ashx (Stripe Customer Portal sessions for self-service subscription management: payment method updates, invoices, cancel). All three use the same dual auth (APIKey body field OR dashboard session cookie); webhook is idempotent via a Stripe event-ID ledger; all rate-limited per-IP.
- Admin: /jsomanage/AISubscriptions.aspx for support to create subscriptions manually for the early-access cohort before Stripe pricing is fully provisioned.
- Health: /v1/health.ashx surfaces
ai.globallyEnabled + ai.provider so external monitors detect the go-live moment without an API key.
- Regression coverage:
npm run verify:ai-live (in packages/jso-protector) runs a 7-case live POST harness covering preset-suggest, compat-check, explain-error, usage, checkout-create (auth-fail path), stripe-webhook (405), and the per-IP rate limiter.
Deploy-time steps
1. Provision the AI schema
Run App_Data/CreateAITables.sql against the production database. The script is idempotent (uses IF NOT EXISTS guards) so re-running is safe. Creates:
AISubscription — one row per active AI tier subscription.
AIUsage — per-action audit row, written by AIService.RecordSuccess.
AIDisable — account-level kill switch (one row = AI off for that account, with a Reason string).
vw_AIMonthlyUsage — aggregated view over AIUsage grouped by AccountID + first-of-month.
Verify: hit /dashboard/AIUsage.aspx as any logged-in user. Banner should switch from "AI schema not yet provisioned" to "no active subscription" (the schema-present-no-subscription state).
2. Configure the LLM provider
Add to Web.config <appSettings>:
<add key="AIProvider" value="claude" />
<add key="ClaudeApiKey" value="sk-ant-..." />
<!-- Optional: model override; defaults to claude-3-5-sonnet-latest. -->
<add key="ClaudeModel" value="claude-3-5-sonnet-latest" />
For OpenAI:
<add key="AIProvider" value="openai" />
<add key="OpenAIApiKey" value="sk-..." />
<add key="OpenAIModel" value="gpt-4o" />
If you want to test the wiring without an API key, leave AIProvider on preview (or remove the key entirely). The endpoints stay up; they just keep returning the rule-based preview envelopes.
3. Flip the global enable flag
In AIService.cs there is a single IsGloballyEnabled property. Today it returns false via the AIGloballyEnabled appSetting (default off). Add:
<add key="AIGloballyEnabled" value="true" />
This is the only flag that gates CheckQuota. Until it flips on, every quota check returns AIError.GloballyDisabled regardless of the API key state — intentional belt-and-braces against accidentally going live before steps 1+2 are confirmed.
4. Configure Stripe billing
The handlers /v1/ai/checkout-create.ashx and /v1/ai/stripe-webhook.ashx ship as Phase 1 code. Configure four Web.config <appSettings>:
<add key="StripeApiKey" value="sk_live_..." />
<add key="StripeAiBasicPriceId" value="price_..." />
<add key="StripeAiCorporatePriceId" value="price_..." />
<add key="StripeAiEnterprisePriceId" value="price_..." />
<add key="StripeAiWebhookSecret" value="whsec_..." />
In the Stripe dashboard:
- Create three recurring Products (
JSO AI Basic / Corporate / Enterprise) at $19, $79, $299 per month. Copy each Price ID into the corresponding appSetting above.
- Configure a webhook endpoint at
https://www.javascriptobfuscator.com/v1/ai/stripe-webhook.ashx. Subscribe to checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted. Copy the signing secret into StripeAiWebhookSecret.
Until all five appSettings land, checkout-create.ashx returns 503 checkout_not_configured and the dashboard Subscribe buttons fall back to "contact support for early access." This is intended — nothing breaks if the deploy ships before the Stripe products exist.
5. (Optional) Seed early-access subscriptions
While Stripe billing is being provisioned, the early-access cohort gets manual subscriptions via /jsomanage/AISubscriptions.aspx (admin only). Direct SQL works too:
INSERT INTO AISubscription (AccountID, Tier, MonthlyActionsCap, MonthlyTokensCap, MonthlyCostCapCents)
VALUES (12345, 'Corporate', 500, 2000000, 30000);
6. Confirm via the config-check page
Admin-only at /jsomanage/AIConfigCheck.aspx — live mirror of this checklist. Reads every appSetting the AI / Stripe surface consumes and reports which are wired (secrets masked). One green pill = "all required appSettings configured" before you flip the global flag.
7. Run the live smoke
cd packages/jso-protector
JSO_BASE_URL=https://www.javascriptobfuscator.com npm run verify:ai-live
Expected: 7/7 PASS (preset-suggest, compat-check, explain-error, checkout-create auth-fail, stripe-webhook 405, usage, rate-limit). The 429 case is localhost-guarded and SKIPs on prod targets so we don't burn a real visitor's rate-limit bucket. If any endpoint fails, the harness message includes the upstream provider error or HTTP status — debug from there. The smoke harness uses "x" as the APIKey, so passing is "wire format intact"; confirm "real LLM calls work" by hitting the endpoint with a real Corporate APIKey and observing actionsUsed tick up.
Rollback plan
Three independent kill switches in order of severity:
- Remove the API key from Web.config. Endpoints fall back to the preview envelope; preview-mode is the original state. Customers still get useful (rule-based) responses; no quota is consumed. Recovery time: 1 minute.
- Set
AIGloballyEnabled=false. CheckQuota short-circuits to GloballyDisabled; the endpoints return ok: false with the preview-and-waitlist message. Recovery time: 1 minute.
- Insert into
AIDisable for specific accounts. Surgical: leaves AI on for everyone else while taking it off for one or more abusive accounts. INSERT INTO AIDisable (AccountID, Reason, DisabledUtc) VALUES (...).
The dashboard widget and the Prometheus exporter both pick up the rollback automatically — no separate UI revert needed.
Observability after the flip
- The dashboard widget at /dashboard/AIUsage.aspx shows live counters and the green/amber/red threshold gradient.
- The Prometheus exporter at
/download/jso-ai-quota-exporter.js exposes per-account counters that customers can pipe into their own stacks. Internally, point one instance at a service account and dashboard the global numbers in our Grafana.
- The
RecordFailure path in AIService writes a QuotaRejections increment to AIUsage. Spike alert at >50/min suggests a Stripe webhook regression or a quota arithmetic bug.
- RSS feed + /changelog.aspx — customers and watchers see every shipped change. Drop a CHANGELOG entry when you flip the flag so subscribers know they can start using AI.
What's not in Phase 1
- Stripe pricing setup — the webhook handler (
/v1/ai/stripe-webhook.ashx) and the checkout-session creator (/v1/ai/checkout-create.ashx) both ship in Phase 1. Set StripeApiKey + StripeAiBasicPriceId / StripeAiCorporatePriceId / StripeAiEnterprisePriceId in Web.config appSettings; create matching products + recurring prices in the Stripe dashboard; configure the webhook in Stripe to point at https://www.javascriptobfuscator.com/v1/ai/stripe-webhook.ashx with the same signing secret in StripeAiWebhookSecret. Until those four settings are configured, manual AISubscription provisioning via /jsomanage/AISubscriptions.aspx for the early-access cohort.
- Resistance Score (Phase 2, 2026-Q4). Separate concept; separate Phase. See the blog post.
- Deobfuscation benchmark (Phase 3, 2027-Q1). Quarterly publication, includes our own output.
One-page summary for the deployer: run the SQL script → drop in the LLM provider + key → flip AIGloballyEnabled → provision Stripe products + webhook → npm run verify:ai-live. 7/7 PASS = live. Rollback is one config edit away (remove key, flip flag off, or per-account AIDisable insert). Every link in the chain — APIKey resolution, quota enforcement, LLM call, usage write, dashboard widget, Prometheus exporter, schema validator, checkout flow, webhook reconciliation, success-banner UX, public release-notes RSS — is production code today.