diff --git a/api/verify.js b/api/verify.js index a1fe9c4..e4ce6f2 100644 --- a/api/verify.js +++ b/api/verify.js @@ -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); @@ -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.' }); } diff --git a/docs/extension/chrome-receipt-inspector.md b/docs/extension/chrome-receipt-inspector.md new file mode 100644 index 0000000..53189dd --- /dev/null +++ b/docs/extension/chrome-receipt-inspector.md @@ -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. diff --git a/extension/chrome-receipt-inspector/README.md b/extension/chrome-receipt-inspector/README.md new file mode 100644 index 0000000..ac8b636 --- /dev/null +++ b/extension/chrome-receipt-inspector/README.md @@ -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. diff --git a/extension/chrome-receipt-inspector/background.js b/extension/chrome-receipt-inspector/background.js new file mode 100644 index 0000000..e5b27de --- /dev/null +++ b/extension/chrome-receipt-inspector/background.js @@ -0,0 +1 @@ +chrome.runtime.onInstalled.addListener(()=>{console.log('CommandLayer Receipt Inspector developer preview installed');}); diff --git a/extension/chrome-receipt-inspector/content.css b/extension/chrome-receipt-inspector/content.css new file mode 100644 index 0000000..916ad23 --- /dev/null +++ b/extension/chrome-receipt-inspector/content.css @@ -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} diff --git a/extension/chrome-receipt-inspector/content.js b/extension/chrome-receipt-inspector/content.js new file mode 100644 index 0000000..cd7fef7 --- /dev/null +++ b/extension/chrome-receipt-inspector/content.js @@ -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));} diff --git a/extension/chrome-receipt-inspector/manifest.json b/extension/chrome-receipt-inspector/manifest.json new file mode 100644 index 0000000..9494e58 --- /dev/null +++ b/extension/chrome-receipt-inspector/manifest.json @@ -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"] + } + ] +} diff --git a/extension/chrome-receipt-inspector/popup.html b/extension/chrome-receipt-inspector/popup.html new file mode 100644 index 0000000..ccab275 --- /dev/null +++ b/extension/chrome-receipt-inspector/popup.html @@ -0,0 +1 @@ +

Receipt Inspector

Developer preview. Paste full receipt JSON.

diff --git a/extension/chrome-receipt-inspector/popup.js b/extension/chrome-receipt-inspector/popup.js new file mode 100644 index 0000000..db4bee3 --- /dev/null +++ b/extension/chrome-receipt-inspector/popup.js @@ -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); +}); diff --git a/public/playground.html b/public/playground.html new file mode 100644 index 0000000..9093e96 --- /dev/null +++ b/public/playground.html @@ -0,0 +1,51 @@ + + + + + +CommandLayer Playground + + + + + + +
+

Playground

+

Try a CommandLayer-style flow: choose a verb, submit text, and verify a sample receipt.

+
+
+
+
+
+

Live runtime unavailable. Use sample receipt.

+
+
+

Receipt JSON

No receipt yet.
+ +
+
+ + + diff --git a/tests/api-verify.test.js b/tests/api-verify.test.js index 78a014f..5b75314 100644 --- a/tests/api-verify.test.js +++ b/tests/api-verify.test.js @@ -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); });