Verify widget identity

Verified identity proves that your server signed in the person named by the widget. An owner or administrator with settings.manage generates the widget identity secret in Settings → Widget → Identity verification. Ordinary reads return only configured and rotated_at; the secret is delivered after generate/rotate. Copy it into your server's protected configuration before dismissing the result. A lost secret must be rotated.

The help center has a separate signing secret in Branding. Never interchange these credentials. Cairn currently has no setting to require verified identity for all visitors. Anonymous and unverified visitors can chat; verified identity additionally enables the permitted customer audience and signed targeting attributes.

Choose HMAC or JWT

HMAC is the hex HMAC-SHA256 of the lowercased email, signed with the widget secret. It proves the email only. JWT uses HS256 with that same secret and can carry a name and targeting attributes. Use JWT for AccordRx because its audience and actor surface must be signed. Send either hmac or token, not both.

Use a one-hour lifetime. Cairn rejects an expired token or an expiry more than 24 hours ahead. Tokens may carry at most 30 attributes, and the decoded attributes are limited to 64 KiB. Attribute names are at most 100 characters; string values are at most 1,000 characters. Values must be scalars, not nested arrays or objects. These limits are also shown in the portal.

PHP example

Add firebase/php-jwt as a direct Composer requirement. Read the account from your application's authenticated session, then build an explicit allowlist:

use Firebase\JWT\JWT;

$email = strtolower(trim($account->email)); // Authenticated server account.
$claims = [
    'email' => $email,
    'name' => $account->name,
    'exp' => time() + 3600,
    'attributes' => [
        'account_id' => 'platform-account:'.$account->id,
        'actor_surface' => $resolvedSurface, // Trusted customer/seller/admin guard.
        'role' => $resolvedRole,
    ],
];
$secret = config('cairn.widget_identity_secret');
abort_unless(is_string($secret) && $secret !== '', 503);
$identity = ['email' => $email, 'name' => $account->name, 'token' => JWT::encode($claims, $secret, 'HS256')];
return response()->json($identity)->header('Cache-Control', 'no-store');

For email-only verification: hash_hmac('sha256', $email, $secret), returned as hmac instead of token. Do not sign an email supplied by a form, URL parameter or request body.

Node.js example

Keep the secret in server configuration. This example uses the standard crypto library and signs a small, server-derived claims object:

import { createHmac } from 'node:crypto';
const secret = process.env.CAIRN_WIDGET_IDENTITY_SECRET;
if (!secret) throw new Error('Widget identity is not configured');
const email = authenticatedAccount.email.trim().toLowerCase();
const claims = { email, name: authenticatedAccount.name, exp: Math.floor(Date.now() / 1000) + 3600,
  attributes: { account_id: `platform-account:${authenticatedAccount.id}`, actor_surface: resolvedSurface, role: resolvedRole } };
const part = value => Buffer.from(JSON.stringify(value)).toString('base64url');
const input = `${part({ alg: 'HS256', typ: 'JWT' })}.${part(claims)}`;
const token = `${input}.${createHmac('sha256', secret).update(input).digest('base64url')}`;
// Return { email, name: authenticatedAccount.name, token } with Cache-Control: no-store.

For HMAC: createHmac('sha256', secret).update(email).digest('hex').

Identify in the browser

After the issued globals tag, queue the server response:

window.$cairn.push(['user:set', identity]);

Refresh this response after reauthentication or account switching. Do not place tokens in URLs or console logs. Clear cairn: storage and complete a full navigation on sign-out, as described in installation.

AccordRx's attribute boundary

The pilot allowlist is the platform account identity (platform-account:<id>), signup time, audience (pharmacy or buying_group), actor surface, role, pharmacy ID/name, buying-group ID/name/status, pharmacy_approved and onboarding_completed_*. Implement an allowlist builder around AccordRx's OnboardingContextResolver; ambiguous account links fail closed. The exact attribute keys and their approval belong in AccordRx's governance record before transmission.

Never send DEA number/class code, NPI, NCPDP number, state license number/expiry, Stripe accounts or payment methods, phone numbers, addresses, order/lot/serial data, DSCSA/EPCIS documents or PHI. Do not serialize an account or pharmacy model wholesale. Names and email are ordinary identity fields, not permission to include unrelated personal data. Support captures, screenshots, PDFs and chat messages must follow the same no-PHI boundary.

Rotate and verify

Prepare the server configuration change first, generate/rotate in the portal, then update the stored server secret immediately. Rotation invalidates old HMACs/JWTs and makes earlier visitor proofs unverified until the server signs again. There is no overlap grace period. A retry with the same Idempotency-Key within 24 hours replays encrypted delivery material without rotating twice; a new deliberate rotation uses a new key. Current permission and membership checks apply to retries.

Verify a current proof succeeds, an old proof fails, anonymous readers cannot obtain customer/internal knowledge, and no secret appears in browser source, logs, audits or ordinary GET responses. Repeat as a settings manager, a member without permission and a member of another workspace.