Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 135 additions & 4 deletions server/public/brand-builder.html
Original file line number Diff line number Diff line change
Expand Up @@ -1200,10 +1200,11 @@ <h4>brand.json for <span id="domain-display"></span></h4>
<a id="view-brand-link" class="btn btn-view-brand" href="#" target="_blank" style="display:none;">View Brand</a>
<button class="btn btn-secondary" onclick="copyJson()">Copy JSON</button>
<button class="btn btn-secondary" onclick="downloadJson()">Download</button>
<div style="display: flex; flex-direction: column; align-items: flex-end;">
<div id="save-registry-group" style="display: flex; flex-direction: column; align-items: flex-end;">
<button class="btn" id="save-registry-btn" onclick="saveToRegistry()" disabled title="Log in to save">Save setup</button>
<span id="save-registry-hint" class="btn-disabled-hint">Log in as a member to save setup</span>
</div>
<button class="btn hidden" id="host-with-aao-btn" onclick="hostWithAao()" title="Continue on AgenticAdvertising.org, which can host this brand.json for you">Host with AgenticAdvertising.org</button>
</div>
</div>

Expand Down Expand Up @@ -1658,6 +1659,17 @@ <h4>Next steps</h4>
<p id="next-step-2-copy">Place the file at this exact location on your server:</p>
<code id="deploy-path">https://example.com/.well-known/brand.json</code>
<p style="margin-top: var(--space-2);">Create the <code>.well-known</code> folder if it doesn't exist.</p>
<details id="delegate-hosting" style="margin-top: var(--space-3);">
<summary style="cursor: pointer; font-weight: var(--font-medium);">Your agency or platform hosts it instead?</summary>
<p style="margin-top: var(--space-2);">Send them the brand.json above. Then publish this small pointer at the same location on your domain, so agents follow it to their copy.</p>
<div class="form-group" style="margin-bottom: var(--space-2);">
<label for="delegate-url">URL where they host your brand.json</label>
<input type="url" id="delegate-url" placeholder="https://agency.example/brands/example.com/brand.json" oninput="updateDelegatePointer()" />
<p class="hint" id="delegate-url-hint">Must be an HTTPS URL that serves the file directly, without redirects.</p>
</div>
<pre class="pointer-snippet" id="delegate-pointer">{}</pre>
<button class="btn btn-secondary" id="delegate-download-btn" onclick="downloadDelegatePointer()" disabled style="padding: var(--space-2) var(--space-4);">Download pointer file</button>
</details>
</div>
</li>
<li>
Expand Down Expand Up @@ -3933,6 +3945,117 @@ <h4>${escapeHtml(brand.name || brand.brand_id || 'Untitled Brand')}</h4>
.catch(() => alert('Failed to copy instructions.'));
}

// ============================================================================
// Delegated hosting (agency or platform hosts the file)
// ============================================================================

function getDelegatePointer() {
const raw = (document.getElementById('delegate-url')?.value || '').trim();
let url;
try { url = new URL(raw); } catch { return null; }
if (url.protocol !== 'https:') return null;
return {
"$schema": "https://adcontextprotocol.org/schemas/latest/brand.json",
"authoritative_location": url.href
};
}

function updateDelegatePointer() {
const pointer = getDelegatePointer();
document.getElementById('delegate-pointer').textContent = pointer ? JSON.stringify(pointer, null, 2) : '{}';
document.getElementById('delegate-download-btn').disabled = !pointer;
}

function downloadDelegatePointer() {
const pointer = getDelegatePointer();
if (!pointer) return;
const blob = new Blob([JSON.stringify(pointer, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'brand.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}

// ============================================================================
// Handoff from brandjson.org to AgenticAdvertising.org hosting
// ============================================================================
// brandjson.org has no AgenticAdvertising.org session, so hosting there
// continues on agenticadvertising.org. The draft travels in the URL
// fragment (never sent to a server) and is only loaded into the form; the
// user still reviews and saves it explicitly.

const HANDOFF_MAX_JSON_CHARS = 100000;

function toBase64Url(bytes) {
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function fromBase64Url(text) {
let b64 = text.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
const bin = atob(b64);
return Uint8Array.from(bin, c => c.charCodeAt(0));
}

async function encodeDraft(json) {
const bytes = new TextEncoder().encode(JSON.stringify(json));
if (typeof CompressionStream === 'function') {
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream('gzip'));
return 'v1.gz.' + toBase64Url(new Uint8Array(await new Response(stream).arrayBuffer()));
}
return 'v1.raw.' + toBase64Url(bytes);
}

async function decodeDraft(token) {
const match = /^v1\.(gz|raw)\.([A-Za-z0-9_-]+)$/.exec(token);
if (!match) return null;
let bytes = fromBase64Url(match[2]);
if (match[1] === 'gz') {
if (typeof DecompressionStream !== 'function') return null;
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
bytes = new Uint8Array(await new Response(stream).arrayBuffer());
}
const text = new TextDecoder().decode(bytes);
if (text.length > HANDOFF_MAX_JSON_CHARS) return null;
const data = JSON.parse(text);
return data && typeof data === 'object' && !Array.isArray(data) ? data : null;
}

function aaoOrigin() {
const host = window.location.hostname;
return host === 'localhost' || host === '127.0.0.1' ? window.location.origin : 'https://agenticadvertising.org';
}

async function hostWithAao() {
if (!currentDomain) return;
try {
const token = await encodeDraft(finalizeJson());
window.location.href = `${aaoOrigin()}/brand/builder?domain=${encodeURIComponent(currentDomain)}#draft=${token}`;
} catch {
alert('Could not prepare your draft. Download it instead and upload it on AgenticAdvertising.org.');
}
}

async function applyHandoffDraft(token, domain) {
let data = null;
try { data = await decodeDraft(token); } catch { data = null; }
history.replaceState(null, '', window.location.pathname + window.location.search);
if (!data) {
alert('The draft from brandjson.org could not be loaded. Please rebuild it here.');
return;
}
document.getElementById('builder-form').classList.remove('hidden');
populateFromBrandJson(data, domain, domain);
document.getElementById('file-status').textContent = 'Imported from brandjson.org. Review, then save your setup.';
updateRegistryButton();
}

async function loadFromRegistry(domain) {
try {
const response = await fetch(`/api/brands/hosted/${encodeURIComponent(domain)}`);
Expand Down Expand Up @@ -3965,13 +4088,21 @@ <h4>${escapeHtml(brand.name || brand.brand_id || 'Untitled Brand')}</h4>
// Initialize
// ============================================================================

document.addEventListener('DOMContentLoaded', () => {
// Check for domain in URL params
document.addEventListener('DOMContentLoaded', async () => {
// On brandjson.org, hosting continues on AgenticAdvertising.org.
if (window.__ADCP_SITE__ === 'brandjson') {
document.getElementById('save-registry-group').style.display = 'none';
document.getElementById('host-with-aao-btn').classList.remove('hidden');
}

// Check for domain in URL params, and a draft handed off from brandjson.org
const params = new URLSearchParams(window.location.search);
const domain = params.get('domain');
const draftToken = window.location.hash.startsWith('#draft=') ? window.location.hash.slice('#draft='.length) : null;
if (domain) {
document.getElementById('domain-input').value = domain;
startBuilding();
await startBuilding();
if (draftToken) await applyHandoffDraft(draftToken, domain);
}
});
</script>
Expand Down
58 changes: 58 additions & 0 deletions server/public/nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,64 @@
return;
}

// Standalone standard sites (brandjson.org, trustjson.org) get neutral
// chrome: the standard's own wordmark and links, with AgenticAdvertising.org
// credited as steward rather than as the site owner.
if (window.__ADCP_SITE__ === 'brandjson' || window.__ADCP_SITE__ === 'trustjson') {
renderStandaloneChrome(window.__ADCP_SITE__);
return;
}

function renderStandaloneChrome(site) {
const sites = {
brandjson: {
name: 'brand.json',
links: [
{ href: '/builder', label: 'Builder' },
{ href: '/spec', label: 'Specification' },
{ href: '/schemas/v3/brand.json', label: 'Schema' },
{ href: 'https://github.com/adcontextprotocol/adcp', label: 'GitHub' },
],
},
trustjson: {
name: 'trust.json',
links: [
{ href: 'https://github.com/adcontextprotocol/adcp/issues/7809', label: 'RFC' },
{ href: 'https://github.com/adcontextprotocol/adcp/pull/7819', label: 'Draft spec' },
{ href: 'https://github.com/adcontextprotocol/adcp', label: 'GitHub' },
],
},
};
const cfg = sites[site];
const style = `
.std-site-nav { position: fixed; top: 0; left: 0; right: 0; height: var(--nav-height); z-index: 100;
display: flex; align-items: center; justify-content: space-between; padding: 0 var(--space-5);
background: var(--color-bg-card); border-bottom: 1px solid var(--color-border); }
.std-site-nav a { color: var(--color-text-secondary); text-decoration: none; font-size: var(--text-sm); }
.std-site-nav a:hover { color: var(--color-text); }
.std-site-wordmark { font-family: var(--font-mono); font-weight: var(--font-bold); font-size: var(--text-lg); color: var(--color-text) !important; }
.std-site-links { display: flex; gap: var(--space-5); }
.std-site-footer { border-top: 1px solid var(--color-border); padding: var(--space-6) var(--space-5); background: var(--color-bg-card);
text-align: center; color: var(--color-text-muted); font-size: var(--text-sm); }
.std-site-footer a { color: var(--color-text-secondary); }
@media (max-width: 640px) { .std-site-links { gap: var(--space-3); } }
`;
const links = cfg.links.map(l => `<a href="${l.href}">${l.label}</a>`).join('');
const navHTML = `<nav class="std-site-nav" aria-label="${cfg.name}"><a class="std-site-wordmark" href="/">${cfg.name}</a><div class="std-site-links">${links}</div></nav>`;
const footerHTML = `<footer class="std-site-footer">${cfg.name} is an open standard stewarded by <a href="https://agenticadvertising.org">AgenticAdvertising.org</a>.</footer>`;
function mount() {
document.head.insertAdjacentHTML('beforeend', `<style>${style}</style>`);
const navPlaceholder = document.getElementById('adcp-nav');
if (navPlaceholder) navPlaceholder.outerHTML = navHTML;
else document.body.insertAdjacentHTML('afterbegin', navHTML);
const footerPlaceholder = document.getElementById('adcp-footer');
if (footerPlaceholder) footerPlaceholder.outerHTML = footerHTML;
else if (!document.querySelector('footer')) document.body.insertAdjacentHTML('beforeend', footerHTML);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount);
else mount();
}

function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
Expand Down
3 changes: 3 additions & 0 deletions server/public/registry-nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
(function () {
'use strict';

// Standalone standard sites have no registry sub-navigation.
if (window.__ADCP_SITE__) return;

var path = window.location.pathname;
var params = new URLSearchParams(window.location.search);
var tab = params.get('tab');
Expand Down
66 changes: 66 additions & 0 deletions server/public/sites/brandjson/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>brand.json: publish your brand identity for AI agents</title>
<meta name="description" content="brand.json is an open standard for publishing your brand identity at /.well-known/brand.json, so AI agents can find your name, logos, colors, and voice.">
<link rel="canonical" href="https://brandjson.org/">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/design-system.css">
<link rel="stylesheet" href="/sites/standard-site.css">
</head>
<body>
<div id="adcp-nav"></div>
<script src="/nav.js"></script>

<header class="std-hero">
<h1>brand.json</h1>
<p>An open standard for publishing your brand identity where AI agents can find it: one file at <code>/.well-known/brand.json</code>.</p>
<div class="std-actions">
<a class="btn btn-primary" href="/builder">Build or check your brand.json</a>
<a class="btn btn-secondary-on-dark" href="/spec">Read the specification</a>
</div>
</header>

<main class="std-main">
<h2>What it is</h2>
<p>brand.json tells agents who your brand is and how it wants to be represented: names, logos, colors, fonts, tone, visual guidelines, and the properties you own. Agents read it the same way crawlers read <code>robots.txt</code>: from a well-known location on your own domain.</p>

<h2>Who hosts it</h2>
<div class="std-cards">
<div class="std-card">
<h3>You</h3>
<p>Recommended. Put the file at <code>https://yourdomain/.well-known/brand.json</code>. The builder generates it and checks it once it's live.</p>
</div>
<div class="std-card">
<h3>Your agency or platform</h3>
<p>They host the full file; you publish a small pointer on your domain with <code>authoritative_location</code>.</p>
</div>
<div class="std-card">
<h3>A hosting service</h3>
<p>AgenticAdvertising.org can host it for you. Your domain still publishes the pointer, so you stay in control.</p>
</div>
</div>

<h2>A minimal example</h2>
<pre>{
"$schema": "https://brandjson.org/schemas/v3/brand.json",
"house": { "domain": "acme.example", "name": "Acme Corp" },
"brands": [
{
"id": "acme",
"names": [{ "en": "Acme" }],
"logos": [{ "url": "https://acme.example/logo.svg" }],
"colors": { "primary": "#1A4D8F" }
}
]
}</pre>

<h2>Schema</h2>
<p>Validate against <a href="/schemas/v3/brand.json"><code>https://brandjson.org/schemas/v3/brand.json</code></a>. The standard's agent and trust fields are moving to a separate record, <a href="https://trustjson.org">trust.json</a>, so brand.json can stay focused on identity.</p>
</main>

<div id="adcp-footer"></div>
</body>
</html>
18 changes: 18 additions & 0 deletions server/public/sites/standard-site.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading