Skip to content

Secure Mode

Turn on Yedric secure mode to sign chat users with HMAC-SHA256 so sessions and history follow a verified identity across visits and devices.

Secure mode ties chat sessions to a verified user in your app. Without it, the widget uses an anonymous browser-local sessionId. With it, the widget sends a verifiable username plus a cryptographic signature (HMAC-SHA256), and the server scopes the user’s sessions and history to that identity, so prior messages can reappear across visits and devices.

  • You know who the end user is (logged-in app, Shopify admin, your dashboard’s customer).
  • You want chat history to persist per-user across page loads, devices, or browsers.
  • You want to protect sessions from being impersonated by changing the username attribute.

If none of that matters, you don’t need secure mode.

Every assistant has a widget API key with two parts:

Value Prefix Who sees it
Public key yk_… Embedded in HTML as the key attribute. Sent as X-Yedric-Key.
Secret yks_… Server-side only. Never should be sent the browser.

Get the pair from Agents → Widget → Code in the dashboard, or create/refresh via the API.

A widget key can be limited to a list of allowed domains. That check now fails closed: a request carrying no Origin or Referer header at all is rejected, where previously the check was skipped.

Browsers always send one of those headers, so ordinary embeds are unaffected. This does matter for anything calling the widget API outside a browser, such as a server-side script or an integration test, since those send no origin by default. Either send an Origin header that is on the list, or use a key with no domain restrictions for that caller. A key with an empty domain list stays unrestricted, so this only affects keys you have actually limited.

See Yedric API Keys and Agents for the full key management flow.

The signature is HMAC-SHA256(secret, username) encoded as lowercase hex.

signature = hex( HMAC-SHA256( key = "yks_your_secret", message = "jannette.parks@yedric.ai" ) )

The username is whatever stable identifier makes sense for your app: an email, a UUID, a Shopify shop UUID, a numeric user id, as long as the exact same string goes into the HMAC on the server and the username attribute on the widget.

import { createHmac } from 'node:crypto';
function signYedricUsername(secret, username) {
return createHmac('sha256', secret).update(username).digest('hex');
}
const signature = signYedricUsername(
process.env.YEDRIC_WIDGET_SECRET,
req.user.email,
);
res.render('page', { yedricKey: process.env.YEDRIC_WIDGET_KEY, username: req.user.email, signature });
$signature = hash_hmac('sha256', $username, $yedricSecret);

A real Shopify embed computes the signature from the shop UUID and passes the full config to the view:

$uuid = Request::Shop()->uuid;
$yedricSecret = Config::get('yedric.widget_secret'); // load from env/config, never commit
$yedricKey = Config::get('yedric.widget_key');
$yedricAgent = Config::get('yedric.agent_id');
$yedric = [
'enabled' => !empty($yedricSecret) && !empty($yedricKey) && !empty($yedricAgent),
'key' => $yedricKey,
'agent_id' => $yedricAgent,
'username' => $uuid,
'signature' => hash_hmac('sha256', $uuid, $yedricSecret),
];

Then the view renders:

<?php if (!empty($yedric['enabled'])): ?>
<script src="https://cdn.yedric.ai/widget.js?agent=<?= htmlspecialchars($yedric['agent_id'], ENT_QUOTES, 'UTF-8') ?>"></script>
<yedric-widget
key="<?= htmlspecialchars($yedric['key'], ENT_QUOTES, 'UTF-8') ?>"
agent="<?= htmlspecialchars($yedric['agent_id'], ENT_QUOTES, 'UTF-8') ?>"
username="<?= htmlspecialchars($yedric['username'], ENT_QUOTES, 'UTF-8') ?>"
signature="<?= htmlspecialchars($yedric['signature'], ENT_QUOTES, 'UTF-8') ?>"
></yedric-widget>
<?php endif; ?>

Browser SubtleCrypto (edge functions, workers)

Section titled “Browser SubtleCrypto (edge functions, workers)”

Useful when your “server” is a Cloudflare Worker, Deno Deploy function, or anywhere without node:crypto.

async function signYedricUsername(secret, username) {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
enc.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const sigBytes = await crypto.subtle.sign('HMAC', key, enc.encode(username));
return Array.from(new Uint8Array(sigBytes))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}

Never run this in the end-user’s browser, because it needs the raw secret.

Render the signed bundle into HTML attributes:

<script src="https://cdn.yedric.ai/widget.js?agent=agt_123"></script>
<yedric-widget
key="yk_abc…"
agent="agt_123"
username="jannette.parks@yedric.ai"
signature="a1b2c3…"
></yedric-widget>

Carrying a conversation across widget instances

Section titled “Carrying a conversation across widget instances”

Each <yedric-widget> is its own instance. A second widget (on another page, or in a separate iframe such as a fullscreen editor opened from a list view) loads the user’s default thread on mount, not whatever conversation was active in the first instance. To carry the active conversation across, hand off its session id.

  1. Read the active session id on the source page:

    const sessionId = window.Yedric.getSessionId(); // secure session id, or null before a session exists
  2. Mount the second instance with that id via the resume-session-id attribute. Because it’s an attribute, you can server-render it into the iframe URL or set it before the element connects:

    <yedric-widget
    key="yk_abc…"
    agent="agt_123"
    username="user@example.com"
    signature="a1b2c3…"
    resume-session-id="widget-<derived>-<thread>"
    ></yedric-widget>

    The second instance resumes that exact thread instead of the user’s default thread. The server still validates that the thread belongs to the verified user.

  3. Re-sync the first instance when the second one closes, so messages added in it appear:

    window.Yedric.refresh(); // re-fetch the active secure thread's messages

getSessionId() and refresh() are also available on the element (document.querySelector('yedric-widget')). All three are secure-mode features: getSessionId() falls back to the anonymous session id outside secure mode, and refresh() is a no-op when there is no active secure session.

  • Never ship yks_… to the browser. Only the HMAC output (signature) goes into HTML. If your secret is ever exposed, rotate it via POST /api/agents/:id/key/refresh.
  • Username must match exactly. A trailing space, different capitalization, or a different field (id vs email) will produce a different HMAC and a 401 Invalid signature error. Log the exact string you pass to the HMAC and to the username attribute. It must be byte-for-byte identical.
  • One signature per username, not per session. You don’t re-sign for every page or session; the signature is static until the username changes.
  • Storing the secret in version control. Prefer env vars or a secrets manager. Even in a private repo, committing yks_… makes rotation painful and broadens the blast radius of a leak.
  • Session prefix matters. session-id-prefix="preview" scopes secure sessions under a different prefix than the default widget. A user’s “widget” history and “preview” history are separate, so don’t mix them.
  • Key/secret pair mismatch. If you rotate the assistant’s key but forget to redeploy the new secret on your signing server, every request will return 401 Invalid signature. Keep them in sync.