@kya-os/consent
v0.2.2
Published
Canonical consent page types, styles, and utilities for MCP-I and Checkpoint
Readme
Consent components
@kya-os/consent provides consent configuration, Cedar capability types, templates, and Lit web components.
Import @kya-os/consent/bundle to register the components, or serve CONSENT_BUNDLE as an ES module for a standalone page.
A standalone scope grant
consent-capabilities-screen is presentational and emits the human's selection to its host.
Its capabilities property accepts ConsentScopeCapability[], which has the same fields as Capability with optional cedar.
The canonical authored Capability schema still requires Cedar.
Literal scopes are displayed on each row; a policy disclosure appears only when Cedar is supplied.
screen.capabilities = [
{
id: "coffee-order",
label: "Order coffee",
description: "Within the product and order limits below.",
icon: "cart",
riskLevel: "low",
defaultOn: true,
scopes: ["place_order:https://id.gs1.org/01/09506000134352"],
},
];
screen.description = "Review the grant before using your passkey.";
screen.submitLabel = "Approve grant";
screen.cancelLabel = "Deny";
screen.addEventListener("capabilities-allow", async (event) => {
// event.detail: { selectedCapabilityIds: string[], selectedScopes: string[] }
screen.loading = true;
try {
await hostApprove(event.detail);
} finally {
screen.loading = false;
}
});
screen.addEventListener("capabilities-deny", hostDeny);hostApprove and hostDeny are application callbacks.
The host verifies identity and any passkey assertion, validates the selection against its stored request, and enforces the resulting grant.
The screen does not perform authentication or issue credentials.
disabled blocks approval and selection while preserving the decline action.
loading freezes selection and both actions until the host finishes or cancels its ceremony.
Empty selections cannot emit approval.
allowDisabled (allow-disabled) blocks approval while leaving selection and decline available for external authentication gates.
requireAll (require-all) blocks approval if any capability is unchecked, including synthetic action events.
allSelected (all-selected) seeds every capability as checked; rerendering the same capability IDs preserves the human's subsequent selection.
HTML attributes for custom labels are submit-label and cancel-label; JavaScript properties are submitLabel and cancelLabel.
Host content and styling
Named slots place host content in the existing screen layout:
identity: before the agent identity tile.auth: after the capability rows, preserving the existing credential form integration.details: after the authentication slot.notice: replaces the default revocation and inactivity notice.footer: replaces the default Checkpoint footer.
Use the notice slot whenever the host has different expiry or revocation behavior.
An omitted slot preserves the existing Cedar screen's default content.
The public parts are connector, card, eyebrow, headline, subhead, agent, permissions, actions, submit, and cancel.
Public CSS variables include --cs-screen-max-width, --cs-card-padding, --cs-card-padding-mobile, --cs-card-gap, --cs-headline-font-size, --cs-subhead-font-size, --cs-scope-font-size, --cs-action-font-size, and --cs-action-min-height.
Set the mobile padding variable separately when overriding padding below 480px.
--cs-agent-tile-bg sets the agent logo tile background (default #FFFFFF in both themes).
--cs-agent-tile-border sets the agent logo tile border color (default rgba(15, 23, 42, 0.08) in both themes).
--cs-agent-tile-text sets the agent monogram color shown when there is no logo (default #0F172A in both themes).
These hooks style the actual components without reaching into their shadow roots.
The agent pill
agentMetadata.verification picks the pill beside the agent's name:
verified: proven by a verified Entity Card. The pill says Verified.domain: bound to the https host that served the client's metadata (a CIMD client). Setdomainto that host, such aschatgpt.com; the pill shows it and never says Verified, and the agent stays capped at probe.unverified: self-asserted, such as a dynamically registered client. The pill says UNVERIFIED.
The deprecated verified boolean is read only when verification is absent.
The domain pill is tinted from --consent-primary and takes the theme's text color; --cs-pill-domain-bg, --cs-pill-domain-border, and --cs-pill-domain-text override it.
Brand colors
Text drawn on or in a brand color reads its own variable, so it reaches WCAG AA (4.5:1) on any brand:
--consent-on-primary: text and checkmarks on a primary fill (fallbackwhite).--consent-on-secondary: text on a secondary fill, such as the scope badges (fallback#0e2249).--consent-link: links in the primary color on the white card (fallback: the primary).
<mcp-consent> and <consent-shell> set them along with the brand colors they apply.
A host that sets --consent-primary itself should set them too; generateCSSVariables(branding) returns all of them.
CONSENT_CAPABILITIES_FEATURES is exported from the package root, constants entry point, and browser bundle.
ConsentCapabilitiesScreen.features exposes the same versioned object for hosts loading a script dynamically.
Version 1 supports scope-only rows and the identity, details, notice, and footer slots.
Main consent component selection
In consent-only mode without a bound identity assertion, mcp-consent submits only scopes belonging to the capabilities the human selected.
Selected capability IDs and their derived scope set must agree.
Registry or project capabilities are intersected with the canonical requested scopes before disclosure, and unrelated rows are hidden.
Every requested scope must be covered by a capability; unmapped or duplicate requested scopes fail closed.
Broader Cedar templates are omitted from narrowed rows so the policy preview cannot imply a broader grant.
Credential mode and requests with a signed identity assertion use a fixed scope set, so every relevant disclosed capability must remain checked before approval.
These paths explain that requirement and preserve the existing credential and assertion checks.
The server must still validate all submitted scopes against its stored authorization request.
Passkey mode
<consent-passkey> runs one WebAuthn ceremony for a consent step.
It ships from its own entry point, @kya-os/consent/passkey (React: @kya-os/consent/passkey/react), because it imports @kya-os/id/wallet/passkey.
@kya-os/id is an optional peer dependency: install it only if you use this entry point.
The package root, ./components, ./bundle and ./react never import it.
The self-contained browser bundle (consent.js, CONSENT_BUNDLE) includes the element with the passkey code inlined.
import "@kya-os/consent/passkey";
const el = document.createElement("consent-passkey");
el.ceremony = step.props.ceremony; // "register" | "authenticate"
el.options = step.props.options; // PasskeyCreateOptions | PasskeyGetOptions, passed on unmodified
el.config = consentConfig.passkey; // optional PasskeyConfig copy overrides
el.addEventListener("consent-passkey:complete", (event) => {
// event.detail: { ceremony, response } (RegistrationResponseJSON or AuthenticationResponseJSON)
hostPost(event.detail);
});
el.addEventListener("consent-passkey:error", hostShowError); // detail: { code, message }
el.addEventListener("consent-passkey:unsupported", hostBlock); // detail: { ceremony }
document.querySelector("#step").append(el); // attach listeners before connectingThe element makes no network calls. The host posts the result with its own transport and CSRF protection, and the server verifies it.
options reaches createPasskey or getPasskeyAssertion as the same object, because its challenge commits to the consent request.
The ceremony starts only from a click on the element's button, never when the element connects or receives a step.
error.code is PasskeyCeremonyError.code (not_allowed, invalid_state, not_supported, security, aborted, unknown); any other failure is unknown.
A browser without WebAuthn gets an unsupported-device state with no button and no fallback credential.
consent-passkey:unsupported fires as the element connects.
A host that attaches listeners later, such as a React effect, reads the readonly supported property after mount instead: null until the support check answers, then true or false.
Set disabled while the host is busy. Setting new options (a new challenge) drops any result still in flight and offers the button again.
renderConsentPage(config, remoteConfig, "passkey") renders a standalone page for config.passkeyStep.
It loads the element from {serverUrl}/consent.js, and like the element it makes no network call.
It re-dispatches the element's three events to a same-origin opener or parent window as postMessage({ source: "consent-passkey", type, detail }, "/").
Required terms keep the element disabled until accepted, and without a usable passkeyStep the page fails closed.
Validation and preview
Run pnpm test and pnpm build for the package suite and all bundle formats.
The build also verifies that the browser, ESM, CJS, and MCP App inline bundles contain the same UI.
Run pnpm test:browser in a Python environment with Playwright installed for real Chromium tests of the public component contract.
Set CHROMIUM to use an existing Chromium executable; otherwise Playwright's installed Chromium is used.
The browser suite covers scope-only and Cedar rendering, custom content, mobile layout, empty and disabled approval, frozen scope selection while loading, actual main-component form submission, scope mismatch refusal, and fixed-grant authentication gates.
It also runs <consent-passkey> against a CDP virtual authenticator: passkey registration and a user-verified assertion, a failed user verification, and a browser without WebAuthn.
After building, serve this directory and open preview/all-templates.html#scope-passkey-preview for the fictional Google account and passkey host example.
