Origin filtering (host side). Drop every MessageEvent whose event.origin !== PRIVATEKIT_ORIGIN. The iframe currently sends with targetOrigin: '*'; the host is the only side that can enforce origin safety today.
Origin filtering (host → iframe). When posting to the iframe via iframeRef.current.contentWindow.postMessage(...), pass PRIVATEKIT_ORIGIN as the second argument in production builds. '*' is acceptable only for local development.
connectionId discipline. Always echo the latest connectionId from PRIVATE_KIT_INIT. Never reuse a stored connectionId after the iframe has been unmounted — a new mount generates a new one.
authToken exposure. The token is sent inside a postMessage envelope addressed to the iframe. With targetOrigin: '*' any other window listening on the parent's message channel could in theory observe it; this is the main reason to lock down targetOrigin to PRIVATEKIT_ORIGIN in production. The iframe itself never forwards the parent-supplied token back to the parent.
Tokens in EMAIL_CONFIRMED / PHONE_CONFIRMED. Both email and phone confirmation rotate the user's session JWTs server-side, and the iframe forwards the new token / refreshToken to the parent so the host can persist them. The same targetOrigin discipline applies in the other direction: the iframe posts with targetOrigin: '*' today, so until that is tightened the host must filter inbound messages strictly by event.origin === PRIVATEKIT_ORIGIN before reading those token fields.
Token freshness. Treat PRIVATE_KIT_AUTH_TOKEN_401 as the canonical "refresh now" signal. Do not pre-validate the token on the host — let the iframe do the round-trip; otherwise you risk the local clock and the backend disagreeing about expiry.
Multiple iframes. Each mounted iframe has its own connectionId. If you embed several PrivateKit instances simultaneously (e.g. one for username, one for a future action), route inbound messages by connectionId to the right host-side handler.
Iframe sandbox attribute. If you add sandbox, include at least allow-scripts allow-same-origin — otherwise the iframe cannot call the auth API.
Replay. The contract has no per-message nonce. A malicious host that captures a valid UPDATE_USERNAME envelope can resend it — but the backend itself is the source of truth: a replayed envelope simply produces the same outcome (idempotent rename, "same username", or 401). Do not treat the contract as a defence against a compromised host page.
Supported actions. Today PrivateKit supports username changes, the email-change + verification flow, the phone-change + verification flow (each with code resend), and single-step password change. The message shape is extensible — additional *_UPDATE_* / *_UPDATED / *_VALIDATION_ERROR triples can be added without renaming the existing ones.
*_VALIDATION_ERROR.reason granularity. Reasons are coarse: required, invalid, exist, unknown for USERNAME_VALIDATION_ERROR; plus limitReached for EMAIL_VALIDATION_ERROR and PHONE_VALIDATION_ERROR; required, max, invalid, invalidCode, unknown for the *_CONFIRMATION_ERROR events; requiredCurrent, requiredNew, min, uppercase, special, number, invalidCurrent, unknown for PASSWORD_VALIDATION_ERROR. Other backend 400 conditions are collapsed into unknown. If new specific error codes appear, expose them as new reasons rather than overloading unknown.
Password confirmation is host-side. The iframe contract for password change carries only currentPassword and newPassword. The confirmation field that the standalone ChangePasswordForm shows is a UX-only check; the host must enforce confirmation === newPassword before posting. Don't send the confirmation across postMessage — it doubles the exposure of the plaintext password without any backend benefit.
targetOrigin: '*' on iframe side. Temporary, pending the agreement on an allowlist of accepted host origins.
No refresh inside the iframe. PrivateKit deliberately does not refresh the token itself, because it does not have access to the host's refresh-token cookie. Refreshing is always the host's job, signalled by AUTH_TOKEN_401.
Logging. Both sides log only when the auth-central app is built with appConfig.demo.available = true. In production builds the loggers are no-ops; rely on browser DevTools network/console for diagnostics or stand up a dedicated demo build.
Single contract source. The enums are exported from the iframe page's source file. A consumer in another repo must keep its own mirror in sync. A future refactor will extract the contract into a publishable package shared with Web3Kit.