# KYE Partner Widgets™ — postMessage Protocol (v1)

> **Status:** locked 2026-05-12.
> **Source:** `constitution/19-WIDGETS-EXPANDED.md §2.2` and
> `constitution/09-WIDGETS.md §6`.
> **Audience:** developers integrating KYE Partner Widgets™ into a
> host page (web app, mobile webview, partner dashboard).

This document specifies the **exact** messages a host page receives
from a KYE Partner Widget™ and may optionally send back. The
protocol is identical across iframe and `<kye-widget-*>` web
component forms.

The web-component form re-broadcasts every postMessage as a
`CustomEvent` of the same name on the element itself, so a host page
that uses the inline form may listen with `addEventListener` rather
than `window.addEventListener("message", ...)`.

---

## 1. Wire format

Three message types only. Every message is a JSON object with a
`type` field whose value is one of:

| `type` | Direction | When |
|---|---|---|
| `kye.widget.ready` | widget → host | once, on first paint complete |
| `kye.widget.action` | widget → host | every interactive action the principal takes |
| `kye.widget.revoke` | widget → host | once, when a revocation has been executed and the widget will go dark |
| `kye.widget.ack` | host → widget | optional acknowledgement of a `kye.widget.action` |

Any other `type` value is silently dropped. The protocol has no
extension surface. New types require an amendment to both
`19-WIDGETS-EXPANDED.md §6` and `01-NAMING.md §15`.

---

## 2. Message schemas

### 2.1 `kye.widget.ready`

```json
{
  "type": "kye.widget.ready",
  "widget": "<widget-slug>",
  "version": "v1",
  "deployment_id": "kye:widget:gb:partner:dep_abc123",
  "required_purpose_class": "kye:purpose-class:authority.gate.evaluate",
  "host_origin": "https://partner.example.com",
  "rendered_at": "2026-05-12T14:33:00Z"
}
```

The widget MUST emit this exactly once, after the identity strip
and first interactive control have painted. The host MAY use this
to remove a loading placeholder, focus the widget, or fire its own
analytics.

### 2.2 `kye.widget.action`

```json
{
  "type": "kye.widget.action",
  "widget": "<widget-slug>",
  "version": "v1",
  "deployment_id": "kye:widget:gb:partner:dep_abc123",
  "action": {
    "kind": "<per-widget enum>",
    "payload": { /* widget-specific, MUST be JSON-serialisable */ }
  },
  "purpose_permission_ref": "kye:purpose:gb:principal:grant_xyz",
  "audit_event_id": "kye:event:gb:ledger:evt_def456",
  "occurred_at": "2026-05-12T14:34:00Z"
}
```

- The `action.kind` value MUST be present in the widget's
  descriptor `audit_events` (CI gate
  `widget-postmessage-conformance`).
- `purpose_permission_ref` MUST resolve to an unrevoked Purpose
  Permission of class `required_purpose_class` for the current
  principal (CI gate `widget-purpose-permission-required`).
- `audit_event_id` is the ID the widget assigned when it appended
  the matching `kye.partner.widget.event.v1` record to the AI Call
  Ledger before emitting the message.

#### Action kinds (per widget)

| Widget | `action.kind` values |
|---|---|
| `partner-registry` | `select_partner`, `query` |
| `widget-licence` | `verify_chain`, `export_chain` |
| `claims-policy` | `compare_versions`, `view_clause` |
| `consent-receipt` | `revoke_grant`, `print_receipt`, `view_grant` |
| `report-builder` | `assemble_pack`, `export_pack`, `add_evidence` |
| `deal-registration` | `submit_deal`, `collision_detected`, `accept_collision_resolution` |
| `widget-analytics` | `drill_into_period`, `export_csv` |
| `widget-revocation` | `preview_cascade`, `confirm_revocation` |
| `sector-pack` | `view_overlay`, `view_obligation` |
| `training-integration` | `launch_training`, `acknowledge_overdue` |
| `authority-gate` | `request_reconfirmation`, `view_reason` |
| `decision-map` | `expand_node`, `export_map`, `view_signal` |
| `replay-proof` | `verify`, `view_signal_diff` |
| `investigation-console` | `pull_evidence`, `expand_blast_radius`, `export_packet` |

### 2.3 `kye.widget.revoke`

```json
{
  "type": "kye.widget.revoke",
  "widget": "<widget-slug>",
  "version": "v1",
  "deployment_id": "kye:widget:gb:partner:dep_abc123",
  "cascade_count": 7,
  "executed_at": "2026-05-12T14:33:00Z",
  "audit_event_id": "kye:event:gb:ledger:evt_revoke_001"
}
```

Only the `Consent Receipt Widget`, the `Widget Revocation Widget`,
and (in extreme cases) any widget whose deployment is revoked
remotely MAY emit this. After emission the widget MUST clear its
DOM, render a permanent "revoked" state, and stop accepting input.

### 2.4 `kye.widget.ack` (host → widget, optional)

```json
{
  "type": "kye.widget.ack",
  "audit_event_id": "kye:event:gb:ledger:evt_def456",
  "ack_at": "2026-05-12T14:34:01Z"
}
```

The host MAY send this to confirm receipt and ledger append. The
widget treats absence of ACK as fire-and-forget — it does **not**
retry on timeout, because the widget already appended its own
ledger entry before emitting.

---

## 3. Origin check (mandatory)

The widget MUST `postMessage(msg, targetOrigin)` against the host
origin registered in the deployment record's `host_origins` array
(`kye.partner.widget.deployment.v1`). Wildcard targets (`"*"`) are
forbidden and cause `widget-postmessage-conformance` CI failure.

A host page receiving a widget message MUST verify
`event.origin === "https://widgets.kyeprotocol.com"` (or the
configured widget origin) before trusting the payload.

```js
window.addEventListener("message", (e) => {
  if (e.origin !== "https://widgets.kyeprotocol.com") return;
  const msg = e.data;
  if (!msg || typeof msg !== "object") return;
  if (!msg.type || !msg.type.startsWith("kye.widget.")) return;
  switch (msg.type) {
    case "kye.widget.ready":  onReady(msg);  break;
    case "kye.widget.action": onAction(msg); break;
    case "kye.widget.revoke": onRevoke(msg); break;
  }
});
```

For the web-component form the equivalent listener is on the
element instance:

```js
const el = document.querySelector("kye-authority-gate");
el.addEventListener("kye.widget.ready",  (e) => onReady(e.detail));
el.addEventListener("kye.widget.action", (e) => onAction(e.detail));
el.addEventListener("kye.widget.revoke", (e) => onRevoke(e.detail));
```

---

## 4. Ledger append ordering

Every `kye.widget.action` MUST be preceded by a successful append
of a `kye.partner.widget.event.v1` record. The `audit_event_id`
field on the emitted message MUST reference that record. This
ordering means the host page never receives an action that has not
already been audited — a property the
`widget-postmessage-conformance` CI gate relies on for replay tests.

---

## 5. CSP `frame-ancestors` policy

Every widget MUST advertise a strict `frame-ancestors` allowlist in
both its `<meta http-equiv="Content-Security-Policy">` tag AND its
`descriptor.json` `csp.frame_ancestors` field. The two MUST agree;
CI gate `widget-csp-strict` rejects drift.

The canonical allowlist for general-purpose widgets is:

```
frame-ancestors 'self' https://app.kyeprotocol.com https://admin.kyeprotocol.com https://kyeprotocol.com https://*.partners.kyeprotocol.com
```

- `'self'` permits the widget origin to host its own preview.
- `https://app.kyeprotocol.com` and `https://admin.kyeprotocol.com`
  are the first-party consoles.
- `https://kyeprotocol.com` is the marketing site (live previews).
- `https://*.partners.kyeprotocol.com` covers licensed partner
  deployments; each partner is provisioned a subdomain under
  `partners.kyeprotocol.com` and the wildcard scopes embedding to
  those tenants only. Embedding from any other origin is blocked.

The `investigation-console` widget is auditor-only and uses a
narrower allowlist — `'self'`, `app.kyeprotocol.com`, and
`admin.kyeprotocol.com` only. It is never embedded on a public
surface.

`frame-ancestors *` is forbidden on every widget; CI fails the
`widget-csp-strict` gate if it appears.

---

## 6. Versioning

The protocol version is the widget's MAJOR version. A change to
this document is a breaking change for every widget in the
catalogue (`19-WIDGETS-EXPANDED.md §9.2`). The 24-month old-major
support window from `09-WIDGETS.md §3` applies.

The current version is **v1** and is locked.
