# License-Only API Configuration Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
>
> Audience: AI coding agents first.

**Goal:** Remove user-configurable backend API settings, hardcode `https://api.press.zone`, and authenticate International Press Zone backend requests with the activated license key only.

**Architecture:** Define one plugin API-base constant and one decrypted license credential accessor. Remove API URL/API key UI, REST fields, browser credential handling, legacy storage, and generated API keys from license checkout. Backend license authentication resolves an active license, activated site, owner/principal, and plugin subscription before attaching the same request identity used by translation and credit services.

**Tech Stack:** WordPress PHP 8+, vanilla JavaScript admin SPA, Node.js/Express/TypeScript, Prisma/PostgreSQL, PHPUnit-style standalone regression scripts, Jest/Supertest, Playwright.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|---|---|---|---|
| 1 | Task 1, Task 2 | Plugin tests; backend tests | ✅ no overlap |
| 2 | Task 3, Task 4, Task 5 | Backend schema/services; plugin credential core; settings UI/REST | ✅ no overlap |
| 3 | Task 6, Task 7, Task 8, Task 9 | Backend auth; backend issuance; plugin request consumers; plugin onboarding | ✅ no overlap |
| 4 | Task 10 | Docs + generated admin bundle | single task |
| 5 | Task 11 | Integrated gates, browser proof, deployment | single task |

## File Map

### Plugin

- `international-press-zone.php` — canonical `IPZ_API_BASE_URL` constant.
- `includes/Translation/Settings.php` — hardcoded API URL and decrypted license credential facade; no API-key storage/state.
- `includes/Licensing/ActivationManager.php` — canonical encrypted license storage and license lifecycle signal.
- `includes/Migrations/Migration007RemoveLegacyApiCredentials.php` — one-time deletion of obsolete API URL/API-key options.
- `includes/API/SettingsController.php` — settings REST contract without API configuration or connection-test route.
- `includes/Admin/SettingsController.php` — localized admin settings without backend credentials.
- `admin/src/pages/settings.js` — settings page without API tab/state/actions/payload.
- `admin/src/utils/api.js` — no browser-to-backend credential method.
- Translation request consumers listed in Task 8 — bearer license authentication.
- `includes/Translation/OnboardingApi.php`, `SiteRegistrar.php`, `ServiceRegistrar.php` — license-only onboarding and lifecycle.
- `tests/unit/LicenseOnlyApiConfigurationTest.php` — PHP credential/configuration regression.
- `tests/e2e/settings.spec.js` — browser assertion that API Configuration does not exist.
- `admin/dist/js/main.js` and build-selected chunks — generated deployable admin assets.

### Backend

- `press-zone-backend/api/prisma/schema.prisma` + new migration — optional-to-required-during-rollout license owner relation.
- `press-zone-backend/api/src/services/multilingualLicenseService.ts` — issue/link license principal without API-key provisioning.
- `press-zone-backend/api/src/middleware/auth.ts` — validate API keys for legacy platform clients and license keys for plugin requests; plugin path uses license only.
- `press-zone-backend/api/src/types/index.ts` or existing Express request augmentation file — typed license identity.
- `press-zone-backend/api/src/routes/account.ts`, `translateLicense.ts`, `multilingualLicense.ts` — stop returning/provisioning plugin API keys; link licenses to owners.
- New focused backend tests — license authentication, site/plugin isolation, checkout response contract.

## Decisions Frozen

- Canonical base URL: `https://api.press.zone`; no option, filter, request parameter, or browser override.
- Canonical plugin credential: decrypted `ipz_license_key` managed by `ActivationManager`.
- Plugin outbound header: `Authorization: Bearer <license-key>`, plus existing `X-Plugin`, `X-Plugin-Version`, and `X-Site-URL` headers.
- Backend MUST verify license hash, `active` status, non-expiry, matching plugin, and an active activation for normalized `X-Site-URL`. Fail closed on missing/ambiguous owner or site.
- API keys remain supported only for unrelated legacy/platform API consumers. International Press Zone MUST neither receive, persist, expose, nor send one.
- Legacy plugin options are deleted, not migrated: `ipz_api_url`, `ipz_api_key`, `presszone_international_api_key_encrypted`, `presszone_international_api_key_valid`.
- No human decisions or publication gates.

### Task 1: Add plugin regression contracts

**Wave:** 1  
**Blocks:** Tasks 4, 5, 8, 9  
**Blocked by:** —

**Files:**
- Create: `tests/unit/LicenseOnlyApiConfigurationTest.php` — standalone WordPress-stubbed credential/configuration contract.
- Modify: `tests/e2e/settings.spec.js` — API tab and fields absence.

**Contract:**
- `Settings::get_api_url(): string` returns exactly `https://api.press.zone` regardless of stored `ipz_api_url`.
- `Settings::get_license_key(): string` returns the decrypted canonical activated license.
- No public API-key getter/save/validity methods remain.
- Settings REST GET omits `api_url`, `api_key`, `api_key_set`; update ignores obsolete inputs and does not write options.
- Browser settings page contains no tab/name/field/button matching `API Configuration`, `API URL`, `API Key`, or `Test Connection`.

**Acceptance:**
- Run: `php tests/unit/LicenseOnlyApiConfigurationTest.php`
- Expected: PASS with one concise success line.
- Run through `e2e-remote`: `npx playwright test settings.spec.js --grep "API Configuration"`
- Expected: PASS; before implementation, at least one assertion fails.

- [ ] Write failing runtime assertions.
- [ ] Run minimal checks and retain failing receipt.
- [ ] Commit only these test paths: `Test license-only API configuration`.

### Task 2: Add backend license-authentication contracts

**Wave:** 1  
**Blocks:** Tasks 3, 6, 7  
**Blocked by:** —

**Files:**
- Create: `press-zone-backend/api/src/__tests__/unit/middleware/licenseAuth.test.ts` — focused middleware tests; do not modify existing untracked `auth.test.ts`.
- Modify: `press-zone-backend/api/src/__tests__/integration/routes/account.test.ts` — checkout response contract.
- Modify: `press-zone-backend/api/src/__tests__/integration/routes/translate.test.ts` — license bearer request contract.

**Behavior:**
- Valid active license + matching plugin + exact activated site authenticates and supplies user/subscription identity.
- Missing, malformed, unknown, inactive, expired, wrong-plugin, unactivated-site, or missing-owner license fails with stable 401/403 error codes and no `next()`.
- Checkout retrieval returns `license_key` and never `api_key`/`api_key_prefix`.
- Existing `sk_live_`/`sk_test_` API-key behavior stays green for unrelated consumers.

**Acceptance:**
- Run backend targeted Jest command from `press-zone-backend/api` for the three test files.
- Expected: new license cases fail before implementation; legacy API-key cases remain PASS.

- [ ] Write failing tests with Prisma/request mocks matching current conventions.
- [ ] Run once; retain receipt.
- [ ] Commit only listed test paths: `Test license-only plugin authentication`.

### Task 3: Link backend licenses to an authenticated principal

**Wave:** 2  
**Blocks:** Tasks 6, 7  
**Blocked by:** Task 2

**Files:**
- Modify: `press-zone-backend/api/prisma/schema.prisma` — nullable rollout-safe `License.user_id` relation + indexes.
- Create: `press-zone-backend/api/prisma/migrations/20260730XXXXXX_link_license_owner/migration.sql` — additive relation and deterministic backfill where uniquely provable.
- Modify: `press-zone-backend/api/src/services/multilingualLicenseService.ts` — owner-aware license creation/linking contract.

**Contract:**
- `createLicense(...)` accepts owner user ID when issuance occurs in an authenticated checkout.
- Add service seam that resolves/creates a license principal for legacy/admin-issued licenses without generating an API key; it links exactly one user and plugin subscription, transactionally.
- Backfill MUST NOT guess ambiguous ownership. Unresolved rows stay nullable and authenticate fail closed until activation/issuance links them.
- Existing license hashes and plaintext secrecy remain unchanged.

**Acceptance:**
- Run Prisma validate and focused license-service tests.
- Expected: schema validates; owner link is atomic, plugin-scoped, and idempotent.

- [ ] Implement schema and migration after checking current untracked migration IDs; choose a unique later timestamp.
- [ ] Implement owner-aware service seam.
- [ ] Run targeted checks.
- [ ] Commit exact schema/migration/service paths: `Link licenses to owners`.

### Task 4: Establish plugin hardcoded URL and license credential core

**Wave:** 2  
**Blocks:** Tasks 8, 9  
**Blocked by:** Task 1

**Files:**
- Modify: `international-press-zone.php` — define `IPZ_API_BASE_URL` exactly once.
- Modify: `includes/Translation/Settings.php` — license getter; remove API-key encryption/storage/validity methods and bulk fields.
- Modify: `includes/Licensing/ActivationManager.php` — expose canonical decrypted key and emit license lifecycle action after durable activation.
- Create: `includes/Migrations/Migration007RemoveLegacyApiCredentials.php` — obsolete-option cleanup.

**Contract:**
- `define('IPZ_API_BASE_URL', 'https://api.press.zone');`
- `Settings::get_api_url(): string` returns `rtrim(IPZ_API_BASE_URL, '/')` only.
- `Settings::get_license_key(): string` delegates to canonical encrypted license storage; no duplicate encryption implementation.
- Action name: `presszone_international_license_activated`; emit only after key + activation metadata persist.
- Migration deletes only four frozen obsolete options and records itself through existing `AbstractMigration` contract.

**Acceptance:**
- Run: `php tests/unit/LicenseOnlyApiConfigurationTest.php`
- Expected: core URL/credential/migration assertions PASS.

- [ ] Implement minimal core contract.
- [ ] Run targeted test.
- [ ] Commit exact paths: `Use one license credential`.

### Task 5: Remove API Configuration UI and settings REST surface

**Wave:** 2  
**Blocks:** Task 10  
**Blocked by:** Task 1

**Files:**
- Modify: `admin/src/pages/settings.js` — delete API tab/state/rendering/validation/connection test/payload fields; update page description.
- Modify: `admin/src/utils/api.js` — delete unused browser-side `requestExternal()` credential path.
- Modify: `includes/Admin/SettingsController.php` — stop localizing API URL/key.
- Modify: `includes/API/SettingsController.php` — remove API response fields, update handlers/schema, and `/settings/test-connection` route.

**Behavior:**
- Remaining tab order: General, Cache, Performance, Advanced.
- Cached/legacy clients may send obsolete fields; endpoint ignores them and never writes them, while valid unrelated settings still save.
- No license key is localized to JavaScript or exposed by settings endpoints.
- Remove now-unused imports from `settings.js`; no dead API status components remain.

**Acceptance:**
- Run admin unit/build checks plus source-level regression.
- Expected: build emits no warnings; `admin/src` has no translation-backend API-key token or configurable API URL.

- [ ] Remove UI and REST surface.
- [ ] Run source regression and admin tests.
- [ ] Commit exact source/controller paths: `Remove API configuration settings`.

### Task 6: Authenticate backend plugin requests with licenses

**Wave:** 3  
**Blocks:** Task 11  
**Blocked by:** Tasks 2, 3

**Files:**
- Modify: `press-zone-backend/api/src/middleware/auth.ts` — credential dispatch and license validation.
- Modify: existing request augmentation/type file under `press-zone-backend/api/src/types/` — typed `req.license` identity.
- Modify: `press-zone-backend/api/src/middleware/rateLimiter.ts` only if required to key license requests without fabricating API-key identity.
- Modify: `press-zone-backend/api/src/middleware/errorHandler.ts` only if required for safe license audit context.

**Contract:**
- Keep exported middleware names/routes stable unless all callers update atomically.
- Credential classifier recognizes `sk_live_`/`sk_test_` as API key; recognized license formats as license; rejects everything else before DB work.
- License lookup uses SHA-256 hash and verifies status, expiry, plugin, activated site, owner, active subscription.
- Attach `req.user`; attach separate `req.license`, never fake `req.apiKey`.
- Rate limit key uses license ID, never plaintext/hash in logs.
- Compare normalized site origins; reject arbitrary host aliases and absent `X-Site-URL`.

**Acceptance:**
- Run Task 2 middleware and translate integration tests.
- Expected: all license failure modes and legacy API-key compatibility PASS.

- [ ] Implement fail-closed middleware path.
- [ ] Run targeted tests once after implementation.
- [ ] Commit exact auth/type/support paths: `Authenticate plugins with licenses`.

### Task 7: Stop backend API-key issuance for plugin licenses

**Wave:** 3  
**Blocks:** Task 11  
**Blocked by:** Tasks 2, 3

**Files:**
- Modify: `press-zone-backend/api/src/routes/account.ts` — link issued license to authenticated user; remove API-key mint/response.
- Modify: `press-zone-backend/api/src/routes/translateLicense.ts` — remove `provisionApiKeyForLicense`; link/ensure license principal instead.
- Modify: `press-zone-backend/api/src/routes/multilingualLicense.ts` — ensure owner/principal on activation without API-key issuance.

**Behavior:**
- Checkout retrieval remains race-safe and single-use.
- Response contains license and subscription metadata only.
- Activation never succeeds with an unusable credential: principal linking is transactional or returns a clear server error; no swallowed provisioning error.
- Do not delete general API-key platform routes/data; stop only plugin-license issuance.

**Acceptance:**
- Run Task 2 account/translate route tests and existing license route tests.
- Expected: no plugin license response includes API-key material; activation/checkout remains idempotent.

- [ ] Remove issuance and link license principal.
- [ ] Run targeted routes.
- [ ] Commit exact route paths: `Stop issuing plugin API keys`.

### Task 8: Convert plugin translation consumers to license auth

**Wave:** 3  
**Blocks:** Task 11  
**Blocked by:** Task 4

**Files:**
- Modify: `includes/Integration/TranslationAPI.php`
- Modify: `includes/API/TranslateJobsController.php`
- Modify: `includes/API/StringTranslateController.php`
- Modify: `includes/API/TranslateController.php`
- Modify: `includes/Translation/CharacterEstimator.php`
- Modify: `includes/Translation/BulkActions.php`
- Modify: `includes/Translation/MetaBox.php`
- Modify: `includes/Translation/JobSender.php`
- Modify: `includes/Translation/TranslationService.php`

**Behavior:**
- Rename credential variables/method calls/errors from API key to license key.
- Every Press.Zone request uses hardcoded base URL and bearer license plus exact plugin/site headers.
- Availability checks require activated license only.
- 401/403 must not mutate a cached “API key valid” flag; return/log license-specific failure without exposing key.
- Leave CDN provider API keys in `includes/Performance/CDNIntegration.php` unchanged.

**Acceptance:**
- Run PHP regression plus targeted translation tests.
- Expected: no listed file references `get_api_key`, `api_key_valid`, `ipz_api_url`, or `ipz_api_key`; requests carry license bearer.

- [ ] Convert all listed consumers consistently.
- [ ] Run targeted checks.
- [ ] Commit exact paths: `Send license credentials to translation API`.

### Task 9: Convert onboarding and service lifecycle to license only

**Wave:** 3  
**Blocks:** Task 11  
**Blocked by:** Task 4

**Files:**
- Modify: `includes/Translation/OnboardingApi.php`
- Modify: `includes/Translation/SiteRegistrar.php`
- Modify: `includes/Translation/ServiceRegistrar.php`

**Behavior:**
- Onboarding status depends on canonical license activation/status, not API-key validity.
- Checkout/payment completion stores canonical encrypted license through `ActivationManager`; ignores any legacy backend API-key field.
- Upgrade and site registration authenticate with license key.
- Service custom fields expose no credential input; license is managed only on Licensing page.
- Listen to `presszone_international_license_activated`; delete old API-key action.
- Reconcile duplicate `presszone_international_license_*` options to canonical `ipz_license_*` fields only where this code writes/reads them.

**Acceptance:**
- Run plugin license activation and onboarding targeted tests.
- Expected: activation leads to available translation service and site registration with no API-key option.

- [ ] Implement lifecycle conversion.
- [ ] Run targeted checks.
- [ ] Commit exact paths: `Use license-only onboarding`.

### Task 10: Update docs and build deployable admin assets

**Wave:** 4  
**Blocks:** Task 11  
**Blocked by:** Tasks 5, 8, 9

**Files:**
- Modify: `docs/user-guide/getting-started.md` and other user-facing docs found by exact API-configuration/API-key search.
- Modify: `admin/SETTINGS-PAGE-DELIVERABLE.md`, `admin/TESTING-CHECKLIST.md` only where they describe removed UI.
- Modify: generated files selected by `cd admin && npm run build`, including `admin/dist/js/main.js`.

**Behavior:**
- User docs direct users to Licensing and never instruct API URL/key setup.
- Internal generic skills and CDN provider API-key documentation remain untouched.
- Build output must be the artifact enqueued by WordPress; no stale source/bundle mismatch.

**Acceptance:**
- Run: `cd admin && npm run build`
- Expected: exit 0 with no warnings.
- Search built/source settings UI for `API Configuration`, `Enter your API key`, and `/settings/test-connection`.
- Expected: zero matches.

- [ ] Update only affected docs.
- [ ] Build once and consume its receipt; do not rebuild to verify.
- [ ] Commit exact docs/generated outputs: `Build license-only settings interface`.

### Task 11: Integrated verification, review, land, deploy

**Wave:** 5  
**Blocks:** —  
**Blocked by:** Tasks 6, 7, 8, 9, 10

**Files:**
- No planned source edits. Fix verified findings in owning task files before proceeding.

**Acceptance:**
- Plugin PHP syntax/checks clean.
- Backend targeted tests, Prisma validation, typecheck, and build clean with no warnings; consume current-tree receipts and do not repeat identical broad checks.
- Required browser run through `~/.claude/bin/e2e-remote` proves settings page has only General/Cache/Performance/Advanced and no API configuration controls; capture screenshot under repo test artifacts.
- Activate an `INTL-…` license in dev, then perform one authenticated backend request; verify backend accepts license bearer and wrong site/license fails.
- Fresh code-reviewer and security-guard passes cover credential exposure, site binding, IDOR, auth fail-open, timing/log leakage, migration safety, and credit identity.
- Local commits land using project delivery policy; backend deploys before plugin bundle to avoid breaking live translations; plugin deploy follows; live/dev smoke verifies exact deployed versions.

- [ ] Run targeted gates once per immutable candidate.
- [ ] Run required remote browser verification.
- [ ] Run independent correctness and security reviews; repair every confirmed finding.
- [ ] Land and deploy backend first, plugin second.
- [ ] Verify live observable result: API Configuration tab absent; activated license alone authorizes translation.

## Self-Review

- Spec coverage: page removal, hardcoded URL, API-key elimination, license-only plugin/backend authentication, legacy cleanup, generated bundle, deployment all map to tasks.
- No unresolved graph-changing decisions.
- Same-wave tasks have no file overlap; semantic dependencies are later waves.
- Contracts use consistent names: `IPZ_API_BASE_URL`, `Settings::get_api_url()`, `Settings::get_license_key()`, `presszone_international_license_activated`, `req.license`.
- No substantial implementation bodies included.
