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
23 changes: 22 additions & 1 deletion api/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
const { verifyReceipt } = require('../lib/verifyReceipt');
const MAX_JSON_BODY_BYTES = 1024 * 1024; // 1 MiB


function resolveCorsOrigin(originHeader) {
if (!originHeader || typeof originHeader !== 'string') return null;
if (originHeader === 'https://www.commandlayer.org' || originHeader === 'https://commandlayer.org') return originHeader;
if (originHeader.startsWith('chrome-extension://')) return originHeader;
return null;
}


function isOversizedJsonBody(req) {
const contentLengthHeader = req?.headers?.['content-length'] || req?.headers?.['Content-Length'];
const parsedContentLength = Number.parseInt(String(contentLengthHeader || ''), 10);
Expand All @@ -22,8 +31,20 @@ module.exports = async function handler(req, res) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');

const allowedOrigin = resolveCorsOrigin(req?.headers?.origin || req?.headers?.Origin);
if (allowedOrigin) {
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
res.setHeader('Vary', 'Origin');
}
res.setHeader('Access-Control-Allow-Methods', 'POST,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

if (req.method === 'OPTIONS') {
return res.status(204).end();
}

if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
res.setHeader('Allow', 'POST,OPTIONS');
return res.status(405).json({ ok: false, status: 'INVALID', reason: 'Method not allowed. Use POST.' });
}

Expand Down
34 changes: 34 additions & 0 deletions docs/extension/chrome-receipt-inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Chrome Receipt Inspector (Developer Preview Foundation)

Status: planned / developer preview. Not published to Chrome Web Store.

## Goal
Provide a browser-side helper that can detect potential CommandLayer receipt IDs on pages and help users verify receipts using the public verifier API.

## Detection
The extension content script scans page text for IDs matching:

- `clrcpt_[a-f0-9]{32}`

## Verification path
- Verify endpoint: `https://www.commandlayer.org/api/verify`
- Method: `POST` with JSON body containing a receipt object.

## CORS requirement
`/api/verify` must allow:
- Origins:
- `https://www.commandlayer.org`
- `https://commandlayer.org`
- `chrome-extension://*`
- Methods: `POST`, `OPTIONS`
- Headers: `Content-Type`

## Receipt lookup options
1. Preferred (when implemented): fetch by ID via:
- `/receipts/{receiptId}.json`
2. Current fallback: popup requires users to paste full receipt JSON.

## Security and scope
- No private keys in extension code.
- No admin or payment endpoints.
- No claims about production store release until launch readiness.
7 changes: 7 additions & 0 deletions extension/chrome-receipt-inspector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# CommandLayer Receipt Inspector (Developer Preview)

This is a developer-preview scaffold only.

- Detects `clrcpt_[a-f0-9]{32}` in page text via content script.
- Supports paste-and-verify flow against `https://www.commandlayer.org/api/verify`.
- Receipt ID lookup via `/receipts/{id}.json` is pending.
1 change: 1 addition & 0 deletions extension/chrome-receipt-inspector/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
chrome.runtime.onInstalled.addListener(()=>{console.log('CommandLayer Receipt Inspector developer preview installed');});
1 change: 1 addition & 0 deletions extension/chrome-receipt-inspector/content.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
body{min-width:320px;font-family:Arial,sans-serif}textarea{width:100%;min-height:120px}pre{white-space:pre-wrap;word-break:break-word}
4 changes: 4 additions & 0 deletions extension/chrome-receipt-inspector/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const re=/clrcpt_[a-f0-9]{32}/g;
const txt=document.body?.innerText||'';
const matches=[...new Set(txt.match(re)||[])];
if(matches.length){console.debug('CommandLayer receipt IDs detected',matches.slice(0,20));}
24 changes: 24 additions & 0 deletions extension/chrome-receipt-inspector/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"manifest_version": 3,
"name": "CommandLayer Receipt Inspector (Developer Preview)",
"version": "0.0.1",
"description": "Developer preview foundation for scanning potential CommandLayer receipt IDs and verifying pasted receipts.",
"permissions": ["storage"],
"host_permissions": [
"https://www.commandlayer.org/*",
"https://commandlayer.org/*"
],
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"],
"css": ["content.css"]
}
]
}
1 change: 1 addition & 0 deletions extension/chrome-receipt-inspector/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="stylesheet" href="content.css"></head><body><main><h3>Receipt Inspector</h3><p>Developer preview. Paste full receipt JSON.</p><textarea id="receipt" placeholder='{"signer":"..."}'></textarea><button id="verify">Verify</button><pre id="out"></pre></main><script src="popup.js"></script></body></html>
6 changes: 6 additions & 0 deletions extension/chrome-receipt-inspector/popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const out=document.getElementById('out');
document.getElementById('verify').addEventListener('click',async()=>{
let payload;try{payload=JSON.parse(document.getElementById('receipt').value);}catch(e){out.textContent='Invalid JSON';return;}
const r=await fetch('https://www.commandlayer.org/api/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
out.textContent=JSON.stringify(await r.json(),null,2);
});
51 changes: 51 additions & 0 deletions public/playground.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CommandLayer Playground</title>
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/site.css" />
<style>
.play-wrap{padding:64px 0 88px;background:linear-gradient(180deg,#F9FAFF 0%,#F3F6FF 100%)}
.card{background:#fff;border:1px solid var(--border);border-radius:16px;padding:18px;box-shadow:0 1px 2px rgba(15,23,42,.04)}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.row{margin-bottom:12px}
textarea,select,input{width:100%;border:1px solid rgba(15,23,42,.18);border-radius:10px;padding:10px;font:14px/1.5 Inter,sans-serif}
textarea{min-height:140px}.btns{display:flex;gap:8px;flex-wrap:wrap}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;white-space:pre-wrap;word-break:break-word}
@media(max-width:960px){.grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<nav><div class="container nav-inner"><a href="/" class="brand"><img src="/commandlayer-logo.png" alt="CommandLayer" /><span>CommandLayer</span></a><ul class="nav-links"><li><a href="/">Home</a></li><li><a href="/proof.html">Live Proof</a></li><li><a href="/verify.html">Verifier</a></li><li><a href="/docs.html">Docs</a></li></ul></div></nav>
<main class="play-wrap"><div class="container">
<h1 class="section-h2">Playground</h1>
<p class="section-p">Try a CommandLayer-style flow: choose a verb, submit text, and verify a sample receipt.</p>
<div class="grid" style="margin-top:16px;">
<section class="card">
<div class="row"><label>Verb</label><select id="verb"><option>verify</option><option>attest</option><option>authorize</option><option>approve</option></select></div>
<div class="row"><label>Input text</label><textarea id="input" placeholder="Paste text"></textarea></div>
<div class="btns"><button id="runBtn" class="btn btn-primary" type="button">Run</button><button id="sampleBtn" type="button">Use sample receipt</button></div>
<p id="runtimeNote" style="margin-top:10px;color:#334155;">Live runtime unavailable. Use sample receipt.</p>
</section>
<section class="card">
<h3>Receipt JSON</h3><pre id="receipt" class="mono">No receipt yet.</pre>
<div class="btns" style="margin-top:10px"><a class="btn btn-secondary" id="openVerify" href="/verify.html">Open verifier</a></div>
</section>
</div></div></main>
<script>
const sampleEndpoint='/receipts/demo-valid-receipt.json';
const receiptEl=document.getElementById('receipt');
const openVerify=document.getElementById('openVerify');
let latestReceipt=null;
async function loadSample(){
const r=await fetch(sampleEndpoint,{cache:'no-store'});
if(!r.ok) throw new Error('Sample receipt unavailable');
latestReceipt=await r.json();
receiptEl.textContent=JSON.stringify(latestReceipt,null,2);
openVerify.href='/verify.html#manual-verifier';
}
document.getElementById('sampleBtn').addEventListener('click',async()=>{try{await loadSample()}catch(e){receiptEl.textContent=e.message;}});
document.getElementById('runBtn').addEventListener('click',async()=>{receiptEl.textContent='Live runtime unavailable. Use sample receipt.';});
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion tests/api-verify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ test('GET /api/verify => 405', async () => {
await handler(req, res);

assert.equal(res.statusCode, 405);
assert.equal(res.headers.allow, 'POST');
assert.equal(res.headers.allow, 'POST,OPTIONS');
assert.equal(res.body.ok, false);
});

Expand Down
Loading