# Tuurio ID WordPress plugin provisioning contract (API and CLI 1.1.6)

This document is the reviewed integration contract for a WordPress plugin that provisions Tuurio ID either through the public provisioning API or through `manage-tuurio-id@1.1.6`. Both modes use the same server-side handoff and scaffold operations. The direct API mode is the normal choice on WordPress hosting without Node.js, process execution, or a persistent operating-system home directory. Do not replace the pinned CLI version with `latest` or another version.

## Intended administrator experience

1. A WordPress administrator clicks **Connect Tuurio ID**.
2. The plugin calculates and displays the exact WordPress site, callback, and post-logout URLs.
3. The administrator approves the resolved addresses and provisioning action.
4. The plugin starts either the direct API flow or the pinned CLI and opens the returned `verificationUriComplete` in a popup or new browser tab.
5. The human signs in, chooses or creates an organization, verifies their email when required, and personally completes any current Tuurio terms or AVV/DPA gate.
6. WordPress stays open. The plugin polls the same direct handoff or reruns the exact same CLI command in short, non-overlapping attempts until provisioning completes.
7. The plugin imports the generated server-side OIDC settings and shows a discovery/login test.

The plugin initiates and observes this process. It never creates an organization through a machine-signup shortcut and never accepts contracts for the administrator.

## Supported execution modes

### Direct provisioning API (recommended for ordinary WordPress hosting)

All requests use `https://id.tuurio.com/cli/v1/provisioning`. The `/cli/v1` path is retained for compatibility and does not mean that callers must execute the CLI. Inspect `GET /capabilities` and require `contractVersion: "20260815"` or a later compatible contract before starting.

1. `POST /handoff/start` with `framework: "WORDPRESS"`, a stable `flowId`, `campaign: "wordpress_plugin"`, the exact `deploymentBaseUrl`, `redirectUri`, and `postLogoutRedirectUri`, and `allowExistingClientReuse: true` when the plugin supports retaining an already configured client secret. The opt-in is deliberately absent from CLI 1.1.6 handoffs, so the pinned CLI always provisions a separate client.
2. Store `handoffId` and `deviceCode` only in short-lived protected server-side state. Open `verificationUriComplete` exactly as returned.
3. Poll `POST /handoff/poll` with the same `handoffId` and `deviceCode`, respecting `pollIntervalSeconds`. `PENDING` is not connected. `APPROVED` returns a short-lived provisioning `sessionToken`.
4. Inspect `selectedClientRecordId` in the approved session. If it is absent, call `POST /scaffold` with `Authorization: Bearer SESSION_TOKEN`, a stable `Idempotency-Key`, a non-empty `projectName` (for example the WordPress site name), and the same browser-approved addresses. If it is present, do not call scaffold; call the existing-client update operation described below.
5. Treat `registeredClients` in a scaffold response, or `client` in an existing-client update response, as the authoritative registration proof. Each contains the actual `clientId`, `redirectUris`, and `postLogoutRedirectUris` persisted by Tuurio. Do not infer registration from generated files or local settings.
6. Call `POST /session/logout` and erase the provisioning session and device credentials after the result is imported.

If an administrator selected an existing client in a handoff that explicitly set `allowExistingClientReuse: true`, the approved session returns its opaque database identifier as `selectedClientRecordId`. Only that exact client may be passed to `PUT /clients/{clientRecordId}/redirect-uris`; it matches `existingClients[].clientRecordId`. For backward compatibility, `existingClients[].id` remains the protocol client identifier and is equal to `existingClients[].clientId`; never use either protocol field in the administrative endpoint path. The endpoint accepts no address body: the server applies only the exact addresses approved in the same browser handoff. It requires an existing-tenant session, complete current legal acceptance, organization-admin permission, a client belonging to the selected tenant, and a compatible client type. The operation is idempotent and returns the authoritative registered client URI sets. A WordPress OIDC client secret never authorizes this administrative change. `/scaffold` rejects such a session to prevent accidental duplicate clients and secret loss. If `selectedClientRecordId` is absent, the administrator chose a separate new client and the existing-client update endpoint must not be called.

The direct API flow must use bounded HTTP timeouts, serialized polling, capability and nonce/CSRF checks in WordPress, and protected server-side storage. It must never expose the device code or provisioning session to browser JavaScript, HTML, telemetry, logs, support exports, or AI prompts.

### `POST /handoff/start` request contract and compatibility

Send JSON with `Content-Type: application/json`. These fields form the established WordPress handoff body:

```json
{
  "campaign": "wordpress_plugin",
  "framework": "WORDPRESS",
  "flowId": "stable_non_secret_flow_id",
  "additionalTargets": [],
  "deploymentBaseUrl": "https://wordpress.example/",
  "redirectUri": "https://wordpress.example/wp-login.php?tuurio_callback=1",
  "postLogoutRedirectUri": "https://wordpress.example/"
}
```

`campaign`, `framework`, `flowId`, and the three URL fields are required for the WordPress integration. `additionalTargets` is optional and defaults to an empty array; it may contain at most four complete target objects, each with the same three URL fields. `allowExistingClientReuse` is an optional Boolean opt-in that defaults to `false` when omitted or sent as `null`; set it to `true` only when the plugin can safely retain an existing confidential-client secret. `resumedIdempotencyKey` is optional and only used when resuming a prior handoff. Clients must remain compatible when later optional fields are added: omit fields they do not implement, rather than guessing a value.

Malformed JSON or an incompatible field type returns HTTP 400 with a safe message pointing to this public, versioned contract; it never returns a raw Jackson exception. Record the response's correlation ID when contacting support, but never include handoff IDs, device codes, session tokens, or client secrets.

### Pinned CLI (optional)

Use this mode only where Node.js, process execution, a persistent home directory, and a private persistent work directory are available. The CLI is a maintained client of the same API contract and remains useful for development, managed hosting, and agent workflows.

## Exact command

Resolve every placeholder first and show the resulting values to the administrator:

```bash
npx --yes manage-tuurio-id@1.1.6 init \
  --framework wordpress \
  --project-dir WORDPRESS_PRIVATE_WORK_DIR \
  --app-name WORDPRESS_SITE_NAME \
  --base-url WORDPRESS_SITE_ORIGIN \
  --redirect-uri 'WORDPRESS_EXACT_CALLBACK_URL' \
  --post-logout-redirect-uri WORDPRESS_EXACT_POST_LOGOUT_URL \
  --auth browser \
  --yes \
  --output json \
  --campaign wordpress_plugin \
  --flow-id STABLE_FLOW_ID \
  --no-open \
  --no-wait
```

The first `--yes` belongs to `npx`; the second belongs to `manage-tuurio-id init`. Invoke the process with an argument array rather than concatenating a shell command. User-controlled site names and URLs must never become executable shell syntax.

### WordPress URL calculation

- `WORDPRESS_SITE_ORIGIN`: the canonical HTTPS origin and installation path from WordPress, with no invented host and no wildcard.
- `WORDPRESS_EXACT_CALLBACK_URL`: the URL equivalent of `add_query_arg('tuurio_callback', '1', site_url('wp-login.php', 'login'))`.
- `WORDPRESS_EXACT_POST_LOGOUT_URL`: the canonical `home_url('/')` appropriate for this installation.
- For subdirectory installs, preserve the subdirectory in all three values.
- For multisite, decide explicitly whether the client belongs to one site or the network. Use the matching site/network URL APIs consistently.
- Reject non-HTTPS production URLs. Cleartext is acceptable only for explicit loopback development hosts.

Do not reconstruct or decode these URLs after approval. Pass them as individual process arguments exactly as shown to the human. Tuurio binds them to the handoff and rejects a scaffold request that changes them later.

## Persistent execution state

CLI 1.1.6 implements a two-step, restart-safe handoff:

- It stores the pending handoff privately under the operating-system user's home directory at `~/.tuurio/manage-tuurio-id-init-session.json`.
- The plugin must run both invocations as the same OS user with the same persistent home directory.
- The plugin must use a persistent, private, writable `WORDPRESS_PRIVATE_WORK_DIR`; do not use `/tmp`, a deployment build directory, or a directory deleted between HTTP requests.
- Only one init process may run for that CLI home at a time. Serialize plugin requests and prevent double-clicks, cron jobs, and AJAX retries from launching overlapping processes.
- Persist the exact resolved argument array, `flow-id`, timestamps, and non-secret UI status in the plugin's server-side state. Do not persist the device code or any browser/session credentials in WordPress options or logs.
- Multiple WordPress sites sharing one OS account must be serialized because CLI 1.1.6 has one pending state file per OS home. Do not change `HOME` ad hoc between the first run and resume.

The private work directory receives a `.env` file with confidential server-side OIDC configuration and a root-anchored `/.env` ignore rule. It must not be web-accessible, committed, included in support archives, or returned through AJAX/REST responses.

## State machine

### 1. `IDLE`

Calculate the URLs, create a stable non-secret `flow-id`, build the exact argument array, and request administrator approval. Do not execute before approval.

### 2. `STARTING`

Run the command once. Capture stdout and stderr separately. Stdout is the machine-readable channel; stderr is diagnostic text and may contain the same verification URL for humans.

Expected successful stdout:

```json
{
  "status": "authorization_required",
  "verificationUriComplete": "https://id.tuurio.com/vibe/connect?handoff=...",
  "expiresAt": "...",
  "resumeExpiresAt": "...",
  "projectDir": "...",
  "campaign": "wordpress_plugin",
  "flowId": "...",
  "nextAction": "Complete the browser handoff, then rerun this exact command unchanged."
}
```

Validate that `status`, `verificationUriComplete`, `flowId`, and the expiry values have the expected types. Open `verificationUriComplete` exactly as returned. Require HTTPS and the configured Tuurio apex host; never construct an `/admin/provisioning/cli` URL.

### 3. `AWAITING_HUMAN`

Keep WordPress open and display the expiry time. The popup or new tab is Tuurio-controlled. Passwords, passkeys, email-verification codes, organization choice, terms, and AVV/DPA acceptance remain entirely in that browser context.

The plugin may offer **Check connection** or perform a throttled server-side check. Each check runs the exact same approved command once. Do not keep a PHP request or child process open for the full browser interaction.

- If stdout again returns `authorization_required`, remain in this state.
- If stdout returns `configured`, continue to `IMPORTING`.
- Never start another check while one is running.
- Never add `--fresh-handoff` automatically.

### 4. `IMPORTING`

Expected CLI success output:

```json
{
  "status": "configured",
  "tenantId": "...",
  "framework": "WORDPRESS",
  "envFile": ".../.env",
  "configuredKeys": [
    "TUURIO_CLIENT_ID",
    "TUURIO_CLIENT_SECRET",
    "TUURIO_ISSUER",
    "TUURIO_REDIRECT_URI",
    "TUURIO_POST_LOGOUT_REDIRECT_URI",
    "TUURIO_SCOPES"
  ],
  "registeredClients": [{
    "clientId": "wordpress-...",
    "framework": "WORDPRESS",
    "redirectUris": ["https://wordpress.example/wp-login.php?tuurio_callback=1"],
    "postLogoutRedirectUris": ["https://wordpress.example/"]
  }],
  "campaign": "wordpress_plugin",
  "flowId": "..."
}
```

`configuredKeys` proves only which local environment keys the CLI wrote; it is not proof that an address was registered. `registeredClients` is the authoritative server registration result and must contain the selected WordPress client with the exact approved redirect and post-logout URI sets. Require `framework === "WORDPRESS"`, the expected `flowId`, and an `envFile` within the private work directory before reading it.

Import only allowlisted `TUURIO_*` keys. At minimum, the runtime needs issuer, client ID, client secret, redirect URI, post-logout redirect URI, and scopes when returned. Store confidential values server-side using the plugin's protected settings mechanism or constants. Never render the client secret into HTML or JavaScript, send it through browser telemetry, or log it. After an atomic and verified import, remove the temporary `.env` if the plugin no longer needs it; otherwise retain it with owner-only permissions outside the web root.

Do not infer a commercial package from the CLI output. The selected organization's current plan is authoritative in Tuurio and is not chosen or purchased by this command.

### 5. `CONNECTED`

Run server-side discovery against `${issuer}/.well-known/openid-configuration`. Require the returned issuer to match exactly and all production endpoints to use HTTPS. Then offer one real login/logout test.

The WordPress runtime is a confidential web client. Use Authorization Code with PKCE S256, transaction-bound `state`, `nonce`, one-time callback processing, server-side code exchange, signed ID-token validation against JWKS, exact issuer and audience validation, time-claim validation, and secure handling of the client secret.

## Failure and retry rules

- Non-zero CLI exit: show a sanitized error and keep the exact command/state available for a deliberate retry. Do not expose stdout/stderr containing paths or URLs to unprivileged users.
- Process timeout/interruption: first retry the exact command. CLI 1.1.6 resumes the saved handoff; do not assume an interrupted agent response means the handoff was lost.
- Handoff still pending: retain the same `verificationUriComplete`; do not create a new one.
- Browser handoff expired: rerun the exact command and follow the CLI result. Preserve the server-backed idempotency window; do not delete the state file.
- Option mismatch: stop and show both the previously approved non-secret URL set and the proposed URL set. A human must cancel or approve a replacement.
- `--fresh-handoff`: use only after explicit human approval to replace an unexpired pending request.
- Missing Node.js/npx, unwritable persistent home, or disabled process execution: use the documented direct provisioning API when outbound HTTPS is available. Never fall back to the blocked machine-signup endpoint or ask for secrets.
- Email pending verification: remain awaiting the human/server result. A UI appearing connected before `status: "configured"` is a bug.

## WordPress authorization and request safety

- Only a current user with the plugin's chosen administrator capability may start, inspect, retry, import, or disconnect provisioning.
- Protect every WordPress form, AJAX action, and REST operation with capability checks plus a WordPress nonce/CSRF check.
- Use a per-attempt lock and idempotent server-side state transitions.
- Escape all rendered values. URLs shown to the administrator are display data, never HTML.
- Never place the handoff device code, provisioning session, client secret, password, authorization code, access token, refresh token, session cookie, email-verification code, or `.env` contents into the browser, database logs, PHP logs, support exports, analytics, or an AI-agent prompt.
- The plugin must not send legal-acceptance booleans, authority assertions, or document versions. Those are collected and recorded only by Tuurio's authenticated browser flow.

## Acceptance checklist

1. Exact callback contains `wp-login.php?tuurio_callback=1` and survives start, approval, scaffold, and runtime unchanged.
2. WordPress remains usable while the browser handoff is open.
3. Refreshing WordPress or the agent UI does not create another tenant or client.
4. An unverified new account cannot reach `CONNECTED` before server approval.
5. Existing-organization and new-organization paths both end in one configured client.
6. Terms/AVV/DPA are accepted only by the authenticated human in Tuurio.
7. Discovery, real login, callback, user mapping, logout, cancellation, expiry, and retry are tested.
8. No secret appears in page source, network responses to the browser, logs, Git, or support exports.
9. Repeated **Check connection** calls are serialized and reuse the same handoff or exact CLI command.
10. A callback or site-URL change requires a new visible approval instead of silently updating the client.

## Server contract summary

Provisioning API calls go to `https://id.tuurio.com/cli/v1/provisioning/...`. Direct API and pinned CLI integrations are both official. They use the same endpoint payloads and browser handoff. The plugin opens only the returned `verificationUriComplete`; it never constructs a tenant Admin URL. Tenant-scoped discovery and OIDC endpoints use the issuer returned after provisioning.

RP-initiated logout remains strict: the runtime sends a valid `id_token_hint` and sends `post_logout_redirect_uri` only when that exact URI appears in the authoritative registered client result. Missing or invalid ID-token hints are not silently trusted, and unregistered post-logout redirects fail with `invalid_request`.

The legacy `POST /cli/v1/provisioning/signup` machine-signup shortcut is blocked. New organizations, email verification, organization selection, and current legal gates are human browser operations.
