-
Notifications
You must be signed in to change notification settings - Fork 0
AP2 mandate-chain developer surface — spec + runnable prototypes (#92) #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
64cd3e7
spec(#92,008): AP2 mandate-chain developer surface (MandateBundle)
dzuluaga c26cde2
spec(#92,008): lock the consent SDK surface after 9-round DX council
dzuluaga be12f18
spec(#92,008): completion via webhook (no poll loop) + Intent Mandate…
dzuluaga 32b0e12
proto(#92,008): runnable orders.* prototype alongside its spec
dzuluaga 6da92dc
proto(#92,008): increment A — real mandateBundle on the orders door
dzuluaga b43117d
proto(#92,008): increment B — runnable grants.* (human-not-present) s…
dzuluaga f4f957c
docs(#92,008): overnight morning brief — orders + grants prototypes
dzuluaga 02d9b07
spec(#92,008): reconcile the page-less wrapper to credentagent.gate()
dzuluaga 810165e
chore(#92): renumber feature 008 → 009-ap2-mandate-chain-dx
dzuluaga a3a0fd9
chore(#92): drop MORNING-BRIEF.md from the PR (scratch handoff doc)
dzuluaga e5c5d7c
proto(#92): address Codex P1s on #95
dzuluaga 29046dc
docs(#92): mark the prototypes as validation demos → graduate in #97
dzuluaga a965901
Merge remote-tracking branch 'origin/main' into 009-ap2-mandate-chain-dx
dzuluaga 51e79d8
spec(#95): address review — scope age gate, add allow-bounds, typed R…
dzuluaga 3aa8136
Merge remote-tracking branch 'origin/main' into 009-ap2-mandate-chain-dx
dzuluaga File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| { | ||
| "feature_directory": "specs/007-quickstart-ladder" | ||
| "feature_directory": "specs/009-ap2-mandate-chain-dx" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| // ⚠ PROTOTYPE — a validation demo, NOT the shipping library API. It graduates into the | ||
| // real credentagent.orders.* / credentagent.grants.* API in #97 (the demo is rewired to it). | ||
| // grants.mjs — a RUNNABLE prototype of the v10 `grants.*` surface (#92 / spec 008), | ||
| // the human-NOT-present half, over the REAL DelegatedGate engine. | ||
| // | ||
| // grants.create → authorize (the Intent Mandate is produced) → grant.spend loop | ||
| // (budget / perSpend enforcement, remaining, idempotent replay) → grant.revoke. | ||
| // | ||
| // REAL: DelegatedGate.preApprove/spend/revoke — the bounds check, the single-use ledger, | ||
| // the revocation, the dev-sealed Intent Mandate (sealIntent) are the actual engine. | ||
| // FACADE polish demonstrating the v10 design: usd() Money, { ok | code } door, idempotent | ||
| // replay ({ ok:true, replayed:true }), split reason codes (per-spend vs budget), mandateBundle. | ||
| // HONEST: today preApprove seals the intent SERVER-side (presence "delegated-demo", | ||
| // trust "server-issued-demo"); the human-signs-on-the-phone ceremony is the roadmap (#71). | ||
|
|
||
| import { DelegatedGate, issueCartMandate } from "@openmobilehub/credentagent-gate"; | ||
|
|
||
| const SIGNING_KEY = "grants-proto-secret"; | ||
| const b64u = (o) => Buffer.from(JSON.stringify(o)).toString("base64url"); | ||
|
|
||
| // Engine RefusalCode → the v10 door's `code` (spec 008). | ||
| const REASON_MAP = { | ||
| "over-cap": "per-spend-exceeded", // per-draw ceiling (TS12 max_amount) | ||
| "over-total": "budget-exceeded", // cumulative budget (TS12 total_amount) | ||
| "revoked": "revoked", | ||
| "consumed": "revoked", | ||
| "out-of-scope": "wrong-merchant", | ||
| "expired": "expired", | ||
| }; | ||
|
|
||
| export function usd(minor) { | ||
| return Object.freeze({ | ||
| currency: "usd", _minor: minor, | ||
| lt(o) { return minor < o._minor; }, gte(o) { return minor >= o._minor; }, eq(o) { return minor === o._minor; }, | ||
| serialize() { return { amount: minor, currency: "usd" }; }, | ||
| toString() { return `$${(minor / 100).toFixed(2)}`; }, | ||
| }); | ||
| } | ||
| usd.dollars = (d) => usd(Math.round(d * 100)); | ||
| usd.cents = (c) => usd(c); | ||
|
|
||
| export class GrantsProto { | ||
| constructor({ catalog }) { | ||
| this.catalogMinor = catalog; // { sku: minorUnits } | ||
| this.gate = new DelegatedGate({ catalog }); // the REAL delegated engine | ||
| this._grants = new Map(); | ||
| } | ||
|
|
||
| // grants.create({ merchant, budget, perSpend, policy }) — authorize once; the Intent Mandate is produced. | ||
| async create({ merchant, budget, perSpend, policy = [], description }) { | ||
| const dg = await this.gate.preApprove({ | ||
| merchant, | ||
| perOrder: perSpend._minor, | ||
| total: budget._minor, | ||
| description: description ?? `Up to ${budget} at ${merchant}, ${perSpend}/purchase`, | ||
| }); | ||
| const intentMandate = { | ||
| type: "ap2.IntentMandate", | ||
| intentId: dg.id, | ||
| presence: dg.presence, // "delegated-demo" | ||
| trustLevel: dg.trustLevel, // "server-issued-demo" | ||
| bounds: { merchant, perSpend: perSpend.serialize(), budget: budget.serialize(), policy: policy.map((p) => p.credential?.id ?? p.id ?? "credential") }, | ||
| serialize() { return b64u({ ...this, serialize: undefined }); }, | ||
| }; | ||
| const rec = { id: dg.id, dg, merchant, budget, perSpend, status: "authorized", intentMandate, cache: new Map() }; | ||
| this._grants.set(dg.id, rec); | ||
| return this._view(rec); | ||
| } | ||
|
|
||
| // grants.retrieve(id) — rehydrate the grant handle. | ||
| retrieve(id) { | ||
| const rec = this._grants.get(id); | ||
| return rec ? this._view(rec) : null; | ||
| } | ||
|
|
||
| _view(rec) { | ||
| return { | ||
| id: rec.id, | ||
| status: rec.status, | ||
| approveUrl: `about:blank#authorize-${rec.id}`, // roadmap: the wallet ceremony that key-signs the intent | ||
| intentMandate: rec.intentMandate, | ||
| budget: rec.budget.serialize(), | ||
| perSpend: rec.perSpend.serialize(), | ||
| spend: (purchase) => this._spend(rec, purchase), | ||
| revoke: () => this._revoke(rec), | ||
| }; | ||
| } | ||
|
|
||
| async _spend(rec, { idempotencyKey, items }) { | ||
| if (rec.cache.has(idempotencyKey)) return { ...rec.cache.get(idempotencyKey), replayed: true }; // v10 idempotent replay | ||
| const { sku, qty = 1 } = items[0]; | ||
| const r = await rec.dg.spend({ idempotencyKey, item: sku, quantity: qty }); | ||
| let door; | ||
| if (r.ok) { | ||
| door = { | ||
| ok: true, | ||
| amount: usd(r.amount).serialize(), | ||
| remaining: usd(r.remaining).serialize(), | ||
| replayed: false, | ||
| authorization: "delegated", | ||
| trustLevel: "presence-only-demo", | ||
| mandateBundle: this._bundle(rec, sku, qty, r.amount), | ||
| }; | ||
| } else { | ||
| // Map the engine's RefusalCode → the v10 door's `code` vocabulary. The engine already | ||
| // distinguishes the two caps: "over-cap" = the per-draw (per-spend) ceiling, "over-total" | ||
| // = the cumulative budget. | ||
| const code = REASON_MAP[r.reason] ?? r.reason; | ||
| door = { ok: false, code, remaining: usd(r.remaining).serialize(), retryable: r.retryable, trustLevel: "presence-only-demo" }; | ||
| } | ||
| rec.cache.set(idempotencyKey, door); | ||
| return door; | ||
| } | ||
|
|
||
| async _revoke(rec) { | ||
| await rec.dg.revoke(); | ||
| rec.status = "revoked"; | ||
| return { revoked: true, status: "revoked" }; | ||
| } | ||
|
|
||
| _bundle(rec, sku, qty, amountMinor) { | ||
| // A CartMandateLine is { id, quantity, unitPrice, lineTotal } — so a recipient can reconcile the | ||
| // signed mandate to the priced purchase (Codex P1). Price from the catalog, never the caller. | ||
| const unitPrice = this.catalogMinor[sku] ?? Math.round(amountMinor / qty); | ||
| const line = { id: sku, quantity: qty, unitPrice, lineTotal: unitPrice * qty }; | ||
| const cart = issueCartMandate( | ||
| { orderId: `${rec.id}-${sku}-${rec.cache.size}`, lines: [line], currency: "usd", total: amountMinor }, | ||
| SIGNING_KEY, | ||
| ); | ||
| const pay = { | ||
| type: "ap2.PaymentMandate", amount: { amount: amountMinor, currency: "usd" }, | ||
| presenceMode: "human_not_present", authorization: "delegated", cart: cart.id, | ||
| intentId: rec.id, trust_level: "presence-only-demo", | ||
| }; | ||
| return { | ||
| intentMandate: { type: rec.intentMandate.type, intentId: rec.intentMandate.intentId, trustLevel: rec.intentMandate.trustLevel }, | ||
| cartMandate: { type: cart.type, id: cart.id, total: cart.total, trust_level: cart.trust_level, serialized: b64u(cart) }, | ||
| paymentMandate: { ...pay, serialized: b64u(pay) }, | ||
| trustLevel: "presence-only-demo", | ||
| }; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| // ⚠ PROTOTYPE — a validation demo, NOT the shipping library API. It graduates into the | ||
| // real credentagent.orders.* / credentagent.grants.* API in #97 (the demo is rewired to it). | ||
| // server.mjs — runnable demo of the v10 `grants.*` surface (human-not-present) with a live UI. | ||
| // | ||
| // (npm run build --workspaces) # once, if not built | ||
| // node examples/grants-proto/server.mjs # → http://localhost:4020 | ||
| // | ||
| // Left pane = human PRESENT: authorize once (the Intent Mandate is produced). | ||
| // Right pane = human AWAY: the agent's spend loop (budget/perSpend, remaining, replay) + revoke. | ||
|
|
||
| import { createServer } from "node:http"; | ||
| import { GrantsProto, usd } from "./grants.mjs"; | ||
|
|
||
| const PORT = Number(process.env.PORT ?? 4020); | ||
| const BASE = `http://localhost:${PORT}`; | ||
|
|
||
| const grants = new GrantsProto({ catalog: { wine: 2000, case: 5000 } }); // wine=$20, case=$50 (minor units; case > $30/spend) | ||
| const log = []; | ||
|
|
||
| const json = (res, code, body) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(body)); }; | ||
| const read = (req) => new Promise((r) => { let d = ""; req.on("data", (c) => (d += c)); req.on("end", () => r(d ? JSON.parse(d) : {})); }); | ||
|
|
||
| const server = createServer(async (req, res) => { | ||
| const url = new URL(req.url, BASE); | ||
| const p = url.pathname; | ||
|
|
||
| if (p === "/") { res.writeHead(200, { "content-type": "text/html" }); res.end(PAGE); return; } | ||
|
|
||
| // grants.create — authorize once (human present) | ||
| if (p === "/api/grant" && req.method === "POST") { | ||
| const g = await grants.create({ merchant: "utopia", budget: usd.dollars(100), perSpend: usd.dollars(30), policy: [{ id: "age" }] }); | ||
| log.length = 0; | ||
| log.push(`→ grants.create → ${g.id.slice(0, 14)}… · Intent Mandate sealed (${g.intentMandate.presence} · ${g.intentMandate.trustLevel})`); | ||
| return json(res, 200, { grant: pub(g), log }); | ||
| } | ||
|
|
||
| // grant.spend — human away | ||
| if (p.match(/^\/api\/grant\/[^/]+\/spend$/) && req.method === "POST") { | ||
| const id = p.split("/")[3]; | ||
| const { idempotencyKey, sku = "wine" } = await read(req); | ||
| const g = grants.retrieve(id); | ||
| if (!g) return json(res, 404, { error: "unknown grant" }); | ||
| const s = await g.spend({ idempotencyKey, items: [{ sku, qty: 1 }] }); | ||
| log.push(s.ok | ||
| ? ` spend ${idempotencyKey} (${sku})${s.replayed ? " · replayed" : ""} → ok · $${(s.amount.amount / 100).toFixed(2)} · remaining $${(s.remaining.amount / 100).toFixed(2)}` | ||
| : ` spend ${idempotencyKey} (${sku}) → refused: ${s.code} · remaining $${(s.remaining.amount / 100).toFixed(2)}`); | ||
| return json(res, 200, { result: s, log }); | ||
| } | ||
|
|
||
| // grant.revoke | ||
| if (p.match(/^\/api\/grant\/[^/]+\/revoke$/) && req.method === "POST") { | ||
| const id = p.split("/")[3]; | ||
| const g = grants.retrieve(id); | ||
| if (!g) return json(res, 404, { error: "unknown grant" }); | ||
| const r = await g.revoke(); | ||
| log.push(`✗ grant.revoke → ${r.status} · next spend fails closed`); | ||
| return json(res, 200, { result: r, log }); | ||
| } | ||
|
|
||
| res.writeHead(404); res.end("not found"); | ||
| }); | ||
|
|
||
| server.listen(PORT, () => { | ||
| console.log(`\n grants.* prototype (human not present) → ${BASE}`); | ||
| console.log(` Left: authorize once (Intent Mandate produced). Right: the spend loop + revoke.`); | ||
| console.log(` Real engine: DelegatedGate (dev-sealed intent, real bounds/ledger/revocation).\n Open ${BASE}.\n`); | ||
| }); | ||
|
|
||
| const pub = (g) => ({ id: g.id, status: g.status, intentMandate: g.intentMandate, budget: g.budget, perSpend: g.perSpend }); | ||
|
|
||
| // ──────────────────────────────────────────────────────────────────── | ||
| const CSS = ` | ||
| :root{--bg:#0b0f17;--surface:#121826;--surface2:#171f30;--border:#273043;--ink:#eaeff8;--muted:#9aa6bd;--accent:#6d93ff;--ok:#3ed89a;--pend:#e7a73c;--rf:#f1637c;--mono:ui-monospace,"SF Mono",Menlo,monospace} | ||
| @media(prefers-color-scheme:light){:root{--bg:#f4f6fa;--surface:#fff;--surface2:#eef2f8;--border:#dce3ec;--ink:#101827;--muted:#59637a;--accent:#2b54d6;--ok:#0e9e6a;--pend:#c67c0a;--rf:#d63e57}} | ||
| *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.55} | ||
| .top{padding:1.1rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:baseline;gap:.7rem} | ||
| .top h1{font-size:1.05rem;margin:0}.top .tag{font-family:var(--mono);font-size:.7rem;color:var(--accent);letter-spacing:.08em;text-transform:uppercase} | ||
| .grid{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--border);min-height:calc(100vh - 58px)} | ||
| .pane{background:var(--bg);padding:1.4rem 1.5rem} | ||
| .pane h2{font-size:.78rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin:0 0 1rem;font-family:var(--mono)} | ||
| button{font:inherit;font-weight:600;border:1px solid var(--accent);background:var(--accent);color:#fff;padding:.55rem 1rem;border-radius:9px;cursor:pointer;margin:.2rem .35rem .2rem 0} | ||
| button.ghost{background:transparent;color:var(--accent)}button.deny{border-color:var(--rf);color:var(--rf);background:transparent} | ||
| button:disabled{opacity:.4;cursor:not-allowed} | ||
| .card{background:var(--surface);border:1px solid var(--border);border-radius:11px;padding:1.1rem;margin-top:1rem} | ||
| .row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;font-family:var(--mono);font-size:.82rem;margin:.25rem 0} | ||
| .k{color:var(--muted)}.pill{font-family:var(--mono);font-weight:700;font-size:.72rem;padding:.12rem .5rem;border-radius:999px} | ||
| .p-ok{background:color-mix(in srgb,var(--ok) 18%,transparent);color:var(--ok)} | ||
| .p-rf{background:color-mix(in srgb,var(--rf) 18%,transparent);color:var(--rf)} | ||
| .p-pend{background:color-mix(in srgb,var(--pend) 20%,transparent);color:var(--pend)} | ||
| pre{font-family:var(--mono);font-size:.74rem;background:var(--surface2);border:1px solid var(--border);border-radius:9px;padding:.8rem;overflow:auto;margin:.55rem 0 0} | ||
| .bar{height:10px;border-radius:6px;background:var(--surface2);border:1px solid var(--border);overflow:hidden;margin:.4rem 0} | ||
| .bar>span{display:block;height:100%;background:var(--ok)} | ||
| .log{font-family:var(--mono);font-size:.75rem;color:var(--muted)}.log div{padding:.18rem 0;border-bottom:1px solid var(--border)} | ||
| code{font-family:var(--mono)} | ||
| `; | ||
|
|
||
| const PAGE = `<!doctype html><html><head><meta charset="utf8"><meta name=viewport content="width=device-width,initial-scale=1"><title>grants.* prototype</title><style>${CSS}</style></head><body> | ||
| <div class=top><h1>grants.* — authorize once, spend later (human not present)</h1><span class=tag>credentagent · prototype</span></div> | ||
| <div class=grid> | ||
| <div class=pane> | ||
| <h2>① Human present — authorize once</h2> | ||
| <button id=create>Create grant — $100 budget · $30/spend · Utopia</button> | ||
| <div id=grant></div> | ||
| <div class=card><div class=k style="font-family:var(--mono);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;margin-bottom:.5rem">event log</div><div class=log id=log></div></div> | ||
| </div> | ||
| <div class=pane> | ||
| <h2>② Human away — the agent spends</h2> | ||
| <div id=spend class=k style="font-size:.85rem">Create a grant on the left; the spend controls appear here.</div> | ||
| <div id=rows></div> | ||
| </div> | ||
| </div> | ||
| <script> | ||
| let ID=null,n=0,lastKey=null,lastSku='wine',budgetMinor=10000; | ||
| const el=id=>document.getElementById(id); | ||
| el('create').onclick=async()=>{ | ||
| const {grant,log}=await(await fetch('/api/grant',{method:'POST'})).json(); | ||
| ID=grant.id;n=0;lastKey=null;budgetMinor=grant.budget.amount; | ||
| el('grant').innerHTML=\`<div class=card> | ||
| <div class=row><span class=k>grant</span><code>\${grant.id.slice(0,16)}…</code><span class="pill p-ok">\${grant.status}</span></div> | ||
| <div class=row><span class=k>Intent Mandate</span><span class="pill p-pend">\${grant.intentMandate.presence}</span><span class="pill p-pend">\${grant.intentMandate.trustLevel}</span></div> | ||
| <pre>intentMandate = \${JSON.stringify({type:grant.intentMandate.type,intentId:grant.intentMandate.intentId.slice(0,20)+'…',bounds:grant.intentMandate.bounds},null,2)}</pre> | ||
| <p class=k style="font-size:.78rem;margin:.5rem 0 0">Today the intent is sealed server-side (\${grant.intentMandate.trustLevel}). The phone-wallet key-signing ceremony is the roadmap.</p></div>\`; | ||
| el('spend').innerHTML=\`<div class=row><span class=k>budget</span><b id=rem>$\${(budgetMinor/100).toFixed(2)}</b><span class=k>left</span></div> | ||
| <div class=bar><span id=barfill style="width:100%"></span></div> | ||
| <div style="margin-top:.6rem"> | ||
| <button id=buy>Spend 1× wine ($20)</button> | ||
| <button id=buycase class=ghost>Spend 1× case ($50 · over per-spend)</button> | ||
| <button id=retry class=ghost disabled>Retry last (same key)</button> | ||
| <button id=revoke class=deny>Revoke grant</button> | ||
| </div>\`; | ||
| el('rows').innerHTML=''; | ||
| paintLog(log); | ||
| el('buy').onclick=()=>doSpend('buy-'+(++n),'wine'); | ||
| el('buycase').onclick=()=>doSpend('buy-'+(++n),'case'); | ||
| el('retry').onclick=()=>lastKey&&doSpend(lastKey,lastSku); | ||
| el('revoke').onclick=async()=>{const {log}=await(await fetch('/api/grant/'+ID+'/revoke',{method:'POST'})).json();paintLog(log);}; | ||
| }; | ||
| async function doSpend(key,sku){ | ||
| lastKey=key;lastSku=sku;el('retry').disabled=false; | ||
| const {result,log}=await(await fetch('/api/grant/'+ID+'/spend',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({idempotencyKey:key,sku})})).json(); | ||
| const remMinor=result.remaining.amount; | ||
| el('rem').textContent='$'+(remMinor/100).toFixed(2);el('barfill').style.width=(100*remMinor/budgetMinor)+'%'; | ||
| el('barfill').style.background=result.ok?'var(--ok)':'var(--rf)'; | ||
| const pill=result.ok?\`<span class="pill p-ok">ok\${result.replayed?' · replayed':''}</span>\`:\`<span class="pill p-rf">\${result.code}</span>\`; | ||
| const detail=result.ok?\`$\${(result.amount.amount/100).toFixed(2)} · authorization=\${result.authorization} · presenceMode=\${result.mandateBundle.paymentMandate.presenceMode}\`:\`retryable=\${result.retryable||'—'}\`; | ||
| const row=document.createElement('div');row.className='row';row.innerHTML=\`<code>\${key}</code>\${pill}<span class=k>\${detail}</span>\`; | ||
| el('rows').prepend(row);paintLog(log); | ||
| } | ||
| function paintLog(log){el('log').innerHTML=log.slice(-10).map(l=>'<div>'+l+'</div>').join('');} | ||
| </script></body></html>`; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.