# QR Service architecture (V1)

## Existing platform contracts confirmed before implementation

Platform applications are Python packages exporting an `APP_MANIFEST` and a
`handle_request(context, action)` entry point. Actions are versioned with a
`_v1` suffix and advertise stable capabilities, schemas, errors, side effects,
and idempotency in `platform.apps.contracts`. Internal calls resolve capability
allowlists from both manifests and pass through the same API gateway as remote
calls.

The gateway authenticates the signed caller, resolves the tenant from that
trusted identity, creates the request context, derives `requesting_app` from
verified claims, and enforces the live subscription, tenant app binding,
invoke scope, and quota before dispatch. An in-process trusted invocation uses
an internal identity but retains all tenant, entitlement, enablement, quota,
and app dispatch checks. WordPress calls the gateway only through Bridge Core's
`wp_flask_bridge_invoke()`; optional PHP is delivered as a signed App Pack.

Cross-app coarse permissions are outbound capabilities in manifests. Existing
transversal services additionally use tenant-aware permission rows for fine
grained policy. Idempotent mutations use a database unique key scoped by
tenant, requesting app, operation, and idempotency key. Structured audit uses
`AuditLogger`; provider secrets use encrypted `SecretsManager` references.
Models inherit `config.db.Base`, repositories use the control-plane session,
Alembic migrations live under `bridge_platform/migrations/alembic/versions`, and app
tests live beside each transversal app.

`messaging_service` is the current transversal reference: manifest and handler
in `__init__.py`, SQLAlchemy models, repository/service boundaries, an Alembic
migration, App Pack wrappers/admin UI, tests, README, and APP_MANUAL. It renders
and delivers allowlisted SMS/email templates and deliberately does not own OTP
generation or validation.

Repository inspection found no stable identity/security challenge service.
The five-minute verification code exists only as the `verification_code`
message template. `messaging_service/APP_MANUAL.md` explicitly places OTP
generation, hashing, expiry, attempts, and validation outside messaging. QR V1
therefore defines a challenge port that invokes the stable conceptual
operations `create_verification_challenge_v1` and `verify_challenge_v1` on a
configured identity app. It never generates, stores, logs, or validates an OTP.
Authenticated resolution returns `challenge_service_unavailable` until that
provider exists; this is safer than silently coupling QR to transport.

## Boundary and data model

`qr_service` stores generic `resource_type`, opaque `resource_reference`,
registered `owner_app`, allowlisted `action_key`, controlled metadata, policy,
state, counters, replacements, temporary resolutions, fine permissions,
idempotency results, and audit events. It stores only SHA-256 hashes of public
QR and resolution tokens plus short support prefixes. It never stores private
resource content, OTP values, arbitrary URLs, callbacks, or transport secrets.

Policy kinds are composable presets (`permanent_revocable`, `fixed_expiry`,
`relative_expiry`, `single_use`, `limited_use`, `limited_scan`, and
`dynamic_session`) with bounded overrides. Scanning means a token was presented
and accepted for policy evaluation. Successful use means the owner app consumed
an authorized resolution. Single-use consumption therefore occurs on successful
consumption unless `consume_on=scan` is explicitly configured.

Allowed state transitions are draft→active; active→suspended/expired/consumed/
revoked/replaced; and suspended→active/revoked/replaced. Revoked, replaced,
consumed, and expired records cannot be reactivated. Replacement creates a new
record/token and preserves bidirectional lineage.

## Resolution and routing

The public route receives only a high-entropy opaque token, rate-limits before
lookup, hashes it, and returns the same generic response for unavailable QR
states. A valid unauthenticated scan creates a short-lived server-side
resolution and returns its one-time opaque token. An authenticated scan creates
a pending resolution and delegates challenge creation through the challenge
port. Continuation delegates verification, re-evaluates QR policy, and issues a
short-lived resolution token. Only the registered owner app may inspect or
consume that context; arbitrary redirects and callbacks are not accepted.

Administrative and trusted actions remain gateway-only. The public Flask route
is limited to beginning resolution and carries defensive cache/frame/content
headers. The WordPress App Pack contains thin wrappers around Bridge invocation;
it does not implement cryptography, counters, OTP, or authorization.

## Implementation phases

1. Manifest, models, migration, schemas, token hashing.
2. Policy/state engine, create/read/list, render, idempotency.
3. Public resolution, pending challenge references, temporary contexts, consume.
4. Permissions, lifecycle actions, replacement, audit, rate limits.
5. Bridge/App Pack wrappers and public route registration.
6. Unit/integration/security tests and operator/integration documentation.
