KoniKodeKnowledge BaseMy AccountOpen website

KNOWLEDGE BASE

Documentation

DOCUMENTATION
CURRENT APPLICATIONClean Framework
DocumentationClean Framework

Clean Framework · DOCUMENTATION

Provider infrastructure, OAuth, and webhooks

Provider infrastructure, OAuth, and webhooks

Bounded candidate reference — 2026-10-20: 0.2.0-rc.1 is committed locally through 84f415b3098af5653577b2c65eea31fca21df79c, unpublished and not production-approved; stable PHP >=8.5.11 <8.6.0 is required. This is documentation, not adoption: this knowledge base retains its installed 0.1.0-rc.1 dependency and PHP 8.2.11 local profile. Dated PHP 8.2 evidence is historical; earlier dirty-source reports retain their original test provenance, not retroactive clean-commit proof. Candidate examples are illustrative: API signatures were reviewed, but candidate code was not executed by this editorial update. See current checkpoint, open criteria and adoption boundary.

Updated atomicity: DatabaseWebhookHandoff, queue/recovery and the SQL-only processor now supply opt-in shared-connection transactions with local MySQL proof; INT-09/INT-10 are scoped PASS. The legacy split receipt/dispatcher remains non-atomic. No provider/module is enabled by this text. See atomic adoption.

Scope and ownership

Phase 6 supplies provider-neutral transport, admission, credential-protection, and webhook mechanics. Applications still own provider accounts, endpoints, client registration, secrets, scopes, response schemas, domain actions, provider-specific signature formats, alert thresholds, and durable shared-store adapters. No provider is contacted during framework or application boot.

The neutral runtime is divided into four packages:

  • Integration/Http provides bounded requests/responses, cURL/fake transports, safe error categories, rate metadata, and one explicit retry owner;
  • Integration/Provider provides separate read/write circuits, single-probe half-open recovery, concurrency backpressure, and redacted health values;
  • Integration/OAuth provides state/PKCE, environment-bound metadata, versioned AEAD credential storage, lifecycle, rotation, and refresh locking;
  • Integration/Webhook provides raw-body verification, timestamp/replay protection, atomic event deduplication, private payload references, queue dispatch, unknown-event retention, lifecycle, replay, and metrics.

These are mechanisms, not ready-made Square, Eventbrite, payment, mail, or AI adapters. Provider-specific adapters remain under an application's app/Providers/ directory.

HTTP transport and retry ownership

HttpRequest requires a reviewed absolute HTTPS URL by default, bounded connect/total timeouts, a response byte ceiling, TLS verification, and TLS 1.2 or 1.3. Redirects are disabled unless deliberately enabled and remain limited to three. Embedded URL/proxy credentials are rejected. Headers, bodies, query values, and multipart content are validated and bounded.

CurlHttpTransport maps DNS, timeout, TLS, malformed response, 4xx, 5xx, 429, and response-size failures into ProviderTransportException. It never retains or reports cURL messages or provider response bodies. Rate-limit headers and provider operation IDs are normalized when present. Bind FakeHttpTransport in deterministic adapter tests; it verifies requests in order without network access.

RetryingHttpTransport is the sole synchronous HTTP retry owner. It retries only typed transient failures and only GET/HEAD/OPTIONS or a request carrying an explicit idempotency key. It respects a bounded Retry-After and uses the injected RetryDelay. Do not add an SDK retry loop, controller retry loop, or queue-handler retry loop around the same call. Consequential writes with an unknown outcome belong in the outbox reconciliation flow, not blind retry.

Circuit, concurrency, rate, and health policy

ProviderCircuitBreaker keys state by provider, capability, and read or write, so a failing mutation path does not automatically disable safe reads. Transient failures are incremented through the store's atomic mutation boundary. When cooldown ends, a shared lease admits only one half-open probe. Successful work closes the circuit; manual reset requires actor/reason context and appends a redacted audit event.

ProviderConcurrencyGate uses bounded lease slots and immediately applies backpressure when all slots are held. Put waiting/retry behavior in the queue or an explicit caller policy. ProviderHealth exposes only available, degraded, unavailable, disabled, or misconfigured, a safe code, optional latency, and normalized rate budget. Health probes must be bounded and non-mutating.

The included circuit store is process-local and the file lease store is single-host. Multi-node applications must bind transactional/shared adapters and prove concurrent failure increments, probe leasing, and slot recovery.

OAuth authorization and credentials

Each environment constructs its own OAuthProviderMetadata. Do not reuse a PROD client ID, callback URI, token endpoint, keyring, or stored account record in DEV or STAGE. Endpoints and callbacks require HTTPS. Requested scopes must be the application's least-privilege set.

Beginning a flow generates 256-bit state and an S256 PKCE verifier/challenge, then stores only a state digest plus the private verifier. The application must bind the returned state to the initiating browser session and pass that exact SecretValue during callback completion:

use Clean\Framework\Config\SecretValue;

// The flow, metadata, and framework SessionManager are injected by the app.
$authorization = $flow->begin($metadata);
$session->put('oauth_state', $authorization->state()->reveal());

// Redirect to $authorization->url(). At callback, recover the same
// session-owned state; never trust state copied only from the query string.
$expectedState = $session->get('oauth_state');
$session->remove('oauth_state');
if (!is_string($expectedState) || $expectedState === '') {
    throw new \RuntimeException('OAuth callback was rejected.');
}

$callback = $flow->complete(
    $metadata,
    $request->query(),
    new SecretValue($expectedState)
);

SessionManager supplies get() and remove(), not pull(). This example allows one outstanding flow per browser session; concurrent authorization flows need application-owned, separately keyed session state. Removing the browser value means a rejected callback must start a new flow. These two session operations are not a claim of atomic consumption in a shared store.

The framework's OAuthStateStore atomically consumes successfully matched callback state before the callback is returned. That state is time-limited, and is bound to the originating provider/environment. Provider errors, malformed fields, state mismatch, expiry, and reuse fail closed.

OAuthCredentialCipher uses XChaCha20-Poly1305 with a fresh 192-bit nonce and additional authenticated data containing environment, provider, and account. The OAuthEncryptionKeyring receives raw 32-byte keys from application secret injection. Keys never belong in configuration PHP/JSON, .env committed to source control, database rows, logs, or credential files. A stored envelope contains only ciphertext, nonce, key version, identity metadata, revision, and update time.

To rotate keys, deploy a keyring containing the old and new versions with the new version current, call OAuthCredentialManager::rotate() for each account, confirm every record moved, then remove the old key in a later deployment. Unknown key versions and authentication/decryption failures fail closed.

The manager requires active lifecycle state and all configured scopes. It serializes refresh with a shared lease, re-reads after acquiring the lease, and publishes through compare-and-swap. Revoked, disconnected, and reauthorization-required states never release token material. The supplied file stores are private single-host adapters; a multi-node application needs shared atomic state/credential stores and an operations-owned rotation job.

Webhook receipt and asynchronous handling

Current stabilization candidate: adopt the explicitly optional SQL path in atomic webhook adoption for locally qualified receipt/outbox/queue mechanics. The legacy sequence below is retained for API compatibility/nonconsequential fixtures, NOT an atomic receipt+handoff guarantee. Production consequential processing remains disabled until deployment, recovery, independent review and owner gates are qualified.

For the opted-in SQL path, DatabaseWebhookHandoff::receive() verifies raw input before private staging, then confirms receipt/outbox intent on one injected Connection/transaction. Duplicate success requires canonical payload and durable handoff/completion proof; conflicting bytes or an uncertain commit do not produce success. A signed unsupported event is retained UNKNOWN under explicit policy, not silently treated as supported processing. DatabaseWebhookQueueTransport bridges the committed intent to the stable SQL queue identity; DatabaseWebhookProcessor can couple a local SQL callback and completion proof on that same Connection. It does not make a remote provider mutation transactional or exactly once. Use adoption/recovery for the opt-in migration, UNKNOWN holds, account/environment scope and bounded orphan cleanup.

A webhook route must preserve exact raw request bytes. Do not decode/re-encode JSON before signature verification. Construct a WebhookRequest, invoke one provider-specific WebhookSignatureVerifier, and map WebhookVerificationException to the provider's documented rejection status. HmacWebhookSignatureVerifier is a configurable timestamped HMAC-SHA256 strategy for compatible providers and deterministic tests; use another implementation when a provider specifies a different canonical format.

The retained nonconsequential WebhookReceiver performs this legacy sequence; its receipt and dispatch are NOT one SQL transaction:

  • verify signature and bounded timestamp freshness over exact bytes;
  • extract provider event ID/type only after verification;
  • place verified bytes in non-public WebhookPayloadStorage;
  • atomically insert the environment/provider/event-ID receipt;
  • delete the new payload copy on a duplicate and reject ID/body conflicts;
  • enqueue a versioned job containing only metadata and an opaque payload reference, then acknowledge;
  • retain and acknowledge unknown event types without running domain code.

The legacy duplicate path suppresses another dispatch; it does NOT prove that a prior dispatch succeeded after a receipt-only crash. Do not acknowledge a consequential duplicate on that receipt alone. Unknown types are visible in status/metrics, with explicit lifecycle operations. A legacy worker calls WebhookLifecycle::processed() only after its application transaction commits, or failed() with a safe category. replay() is an explicit operator action; the stable queue key may suppress duplicate delivery, not prove a domain or external effect occurred exactly once. Handlers own effect idempotency because delivery is at least once. The legacy path remains excluded from consequential use.

FileWebhookEventStore and PrivateWebhookPayloadStorage are single-host durable examples, not a shared receipt/outbox transaction. Consequential use, even on one host, requires the qualified opt-in database coordinator and its explicit adoption gates. Retention must delete payload objects and metadata under application privacy policy; raw bodies, signatures, OAuth values, and secrets must never enter logs or ordinary audit metadata.

Sample adapter and verification status

skeleton/app/Providers/ExampleCatalogProvider.php demonstrates an application-owned provider adapter over HttpTransport. The deterministic suite runs it only with FakeHttpTransport and fake values. It is not a live provider integration and contains no account configuration.

Local deterministic tests cover error classes, safe/unsafe retry behavior, read/write circuits, half-open admission, concurrency backpressure, PKCE/state tamper and reuse, encrypted at-rest records, key-version failure and rotation, refresh serialization, environment isolation, signatures/timestamps, duplicates/conflicts, unknown events, private references, durable metadata, queue dispatch, terminal lifecycle, and replay.

Actual isolated PHP8.5.11 loopback cURL/TLS trust/hostname/deadline and bounded HTTP failure tests pass. Opt-in same-Connection MySQL8.4.11 receipt/outbox atomicity and independent-process recovery pass 22 fault tests; these do not qualify the legacy split receiver or a provider's external effects. Provider sandboxes, registration/revocation, distributed host/network/DNS controls, actual deployment profile, clock skew and provider-specific signature/retry variations remain TO VERIFY. See Major integration boundaries and atomic webhook adoption.

Editorial provenance

Source: docs/PROVIDERS_AND_WEBHOOKS.md. Source SHA-256: fb3b02fa3cb84cb25135eb833af217940d0a96511c31cffba040668152d871cb.

Framework candidate reference; editorial checkpoint 2026-10-20. See candidate status and adoption boundary.

EXPLORE FURTHER

Drag a divider or use Left and Right Arrow keys to resize. Hold Shift for larger steps. Enter hides or restores the sidebar; Home and End select its minimum or maximum width. Double-click resets the width.