#!/usr/bin/env node /** * Proof21 offline demonstration v0.1.0 — NOT the production P21 SDK. * Runs entirely in memory. No dependencies, network, wallets, file writes, or * private keys. All payment evidence is synthetic. Never use it to release funds. * JWS compact serialization (RFC 7515), EdDSA/Ed25519 (RFC 8037), Node crypto. * This intentionally restricted reader is NOT a general-purpose JOSE library. */ import { createPublicKey, verify as cryptoVerify } from 'node:crypto'; import { pathToFileURL } from 'node:url'; export const VERSION = '0.1.0'; export const DEMO_CLOCK = '2026-09-07T12:00:00.000Z'; export const EXPECTED = Object.freeze({ issuer: 'proof21-offline-example', requestId: 'demo-payout-001', policyId: 'demo-payment-match-v1', chainId: 'eip155:8453', asset: 'synthetic:USD6', recipient: '0x1111111111111111111111111111111111111111', amountMinor: '100000000', }); export const HEADER = Object.freeze({ alg: 'EdDSA', kid: 'p21-offline-demo-v1', typ: 'p21-demo+jws' }); /* Fixed local trust anchor for these examples; never learned from a receipt URL. */ export const PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEABldgx+ntYeCza82Ts9TPGJ4agR5F8SFdEwZ7yWwqK6g=\n-----END PUBLIC KEY-----\n"; export const FIXTURES = Object.freeze({ "matching": "eyJhbGciOiJFZERTQSIsImtpZCI6InAyMS1vZmZsaW5lLWRlbW8tdjEiLCJ0eXAiOiJwMjEtZGVtbytqd3MifQ.eyJzY2hlbWEiOiJwMjEtb2ZmbGluZS1kZW1vLzEiLCJpc3N1ZXIiOiJwcm9vZjIxLW9mZmxpbmUtZXhhbXBsZSIsInJlcXVlc3RJZCI6ImRlbW8tcGF5b3V0LTAwMSIsInBvbGljeUlkIjoiZGVtby1wYXltZW50LW1hdGNoLXYxIiwib2JzZXJ2ZWRBdCI6IjIwMjYtMDktMDdUMTE6NTk6MDAuMDAwWiIsInNvdXJjZSI6eyJraW5kIjoic3ludGhldGljLWZpeHR1cmUiLCJjaGFpbklkIjoiZWlwMTU1Ojg0NTMifSwicGF5bWVudCI6eyJhc3NldCI6InN5bnRoZXRpYzpVU0Q2IiwicmVjaXBpZW50IjoiMHgxMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExIiwiYW1vdW50TWlub3IiOiIxMDAwMDAwMDAiLCJzdGF0dXMiOiJzdWNjZWVkZWQifX0.MF04E3pIFXyKw6Fr4Q2ozN5PYefLhhP9v47nLFUdNVlQx2qEHIDwl2p2MdJe4XJT6YH9kvyDB3CkmwrJrSO7BA", "wrongRecipient": "eyJhbGciOiJFZERTQSIsImtpZCI6InAyMS1vZmZsaW5lLWRlbW8tdjEiLCJ0eXAiOiJwMjEtZGVtbytqd3MifQ.eyJzY2hlbWEiOiJwMjEtb2ZmbGluZS1kZW1vLzEiLCJpc3N1ZXIiOiJwcm9vZjIxLW9mZmxpbmUtZXhhbXBsZSIsInJlcXVlc3RJZCI6ImRlbW8tcGF5b3V0LTAwMSIsInBvbGljeUlkIjoiZGVtby1wYXltZW50LW1hdGNoLXYxIiwib2JzZXJ2ZWRBdCI6IjIwMjYtMDktMDdUMTE6NTk6MDAuMDAwWiIsInNvdXJjZSI6eyJraW5kIjoic3ludGhldGljLWZpeHR1cmUiLCJjaGFpbklkIjoiZWlwMTU1Ojg0NTMifSwicGF5bWVudCI6eyJhc3NldCI6InN5bnRoZXRpYzpVU0Q2IiwicmVjaXBpZW50IjoiMHgyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyIiwiYW1vdW50TWlub3IiOiIxMDAwMDAwMDAiLCJzdGF0dXMiOiJzdWNjZWVkZWQifX0.WJOU8LnnvVq80gpISZLS5cb0J2HepDmpr6oEuglteKyGgVdCVlEIRedG9WMPF8Sp9aXO9pIvPUfrxupP63ijAg", "missing": "eyJhbGciOiJFZERTQSIsImtpZCI6InAyMS1vZmZsaW5lLWRlbW8tdjEiLCJ0eXAiOiJwMjEtZGVtbytqd3MifQ.eyJzY2hlbWEiOiJwMjEtb2ZmbGluZS1kZW1vLzEiLCJpc3N1ZXIiOiJwcm9vZjIxLW9mZmxpbmUtZXhhbXBsZSIsInJlcXVlc3RJZCI6ImRlbW8tcGF5b3V0LTAwMSIsInBvbGljeUlkIjoiZGVtby1wYXltZW50LW1hdGNoLXYxIiwib2JzZXJ2ZWRBdCI6IjIwMjYtMDktMDdUMTE6NTk6MDAuMDAwWiIsInNvdXJjZSI6eyJraW5kIjoic3ludGhldGljLWZpeHR1cmUiLCJjaGFpbklkIjoiZWlwMTU1Ojg0NTMifSwicGF5bWVudCI6bnVsbH0.3f8HTfrOORm_CSxpCwn0Xmq-efz58boHWKXJ3DiNy3EDSUHddTUvVKf1GrJ1yYHHamuIjZEoKj7vML6EUTIxDA", "tampered": "eyJhbGciOiJFZERTQSIsImtpZCI6InAyMS1vZmZsaW5lLWRlbW8tdjEiLCJ0eXAiOiJwMjEtZGVtbytqd3MifQ.eyJzY2hlbWEiOiJwMjEtb2ZmbGluZS1kZW1vLzEiLCJpc3N1ZXIiOiJwcm9vZjIxLW9mZmxpbmUtZXhhbXBsZSIsInJlcXVlc3RJZCI6ImRlbW8tcGF5b3V0LTAwMSIsInBvbGljeUlkIjoiZGVtby1wYXltZW50LW1hdGNoLXYxIiwib2JzZXJ2ZWRBdCI6IjIwMjYtMDktMDdUMTE6NTk6MDAuMDAwWiIsInNvdXJjZSI6eyJraW5kIjoic3ludGhldGljLWZpeHR1cmUiLCJjaGFpbklkIjoiZWlwMTU1Ojg0NTMifSwicGF5bWVudCI6eyJhc3NldCI6InN5bnRoZXRpYzpVU0Q2IiwicmVjaXBpZW50IjoiMHgxMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExIiwiYW1vdW50TWlub3IiOiI5OTkwMDAwMDAiLCJzdGF0dXMiOiJzdWNjZWVkZWQifX0.MF04E3pIFXyKw6Fr4Q2ozN5PYefLhhP9v47nLFUdNVlQx2qEHIDwl2p2MdJe4XJT6YH9kvyDB3CkmwrJrSO7BA" }); class DemoError extends Error { constructor(code) { super(code); this.code = code; } } function ensure(ok, code) { if (!ok) throw new DemoError(code); } function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function keys(value, required, optional = []) { ensure(object(value), 'BAD_STRUCTURE'); ensure(required.every(k => Object.hasOwn(value, k)), 'MISSING_FIELD'); ensure(Object.keys(value).every(k => required.includes(k) || optional.includes(k)), 'UNSUPPORTED_FIELD'); } function decoded(segment) { ensure(typeof segment === 'string' && /^[A-Za-z0-9_-]+$/.test(segment), 'BAD_BASE64URL'); const bytes = Buffer.from(segment, 'base64url'); ensure(bytes.toString('base64url') === segment, 'BAD_BASE64URL'); return bytes; } function json(bytes) { let raw, value; try { raw = new TextDecoder('utf-8', { fatal: true }).decode(bytes); value = JSON.parse(raw); } catch { throw new DemoError('BAD_JSON'); } // Restricted demo wire encoding: compact JSON.stringify form. This rejects // duplicate keys, alternate numeric spellings, and ambiguous encodings. // It is not a claim to implement RFC 8785 or accept every valid JSON/JWS. ensure(JSON.stringify(value) === raw, 'NON_COMPACT_OR_AMBIGUOUS_JSON'); return value; } function validatePayload(p) { keys(p, ['schema', 'issuer', 'requestId', 'policyId', 'observedAt', 'source', 'payment']); ensure(p.schema === 'p21-offline-demo/1', 'UNSUPPORTED_SCHEMA'); for (const k of ['issuer', 'requestId', 'policyId', 'observedAt']) ensure(typeof p[k] === 'string' && p[k].length <= 100, 'BAD_FIELD_TYPE'); ensure(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(p.observedAt), 'BAD_TIME'); ensure(Number.isFinite(Date.parse(p.observedAt)) && new Date(p.observedAt).toISOString() === p.observedAt, 'BAD_TIME'); keys(p.source, ['kind', 'chainId']); ensure(p.source.kind === 'synthetic-fixture', 'UNSUPPORTED_SOURCE'); ensure(typeof p.source.chainId === 'string' && /^eip155:\d{1,12}$/.test(p.source.chainId), 'BAD_CHAIN_ID'); if (p.payment === null) return; keys(p.payment, ['asset', 'recipient', 'amountMinor', 'status']); ensure(typeof p.payment.asset === 'string' && p.payment.asset.length <= 100, 'BAD_ASSET'); ensure(typeof p.payment.recipient === 'string' && /^0x[0-9a-f]{40}$/.test(p.payment.recipient), 'BAD_RECIPIENT'); ensure(typeof p.payment.amountMinor === 'string' && /^(0|[1-9][0-9]{0,77})$/.test(p.payment.amountMinor), 'BAD_AMOUNT'); ensure(['succeeded', 'failed', 'pending'].includes(p.payment.status), 'BAD_EXECUTION_STATUS'); } /** Verify the restricted demo artifact. It authenticates an issuer's statement, * not a live chain transaction. Signature-verified fields remain untrusted data. */ export function verifyArtifact(compact, publicKey = PUBLIC_KEY_PEM) { let integrity = 'INVALID'; try { ensure(typeof compact === 'string' && compact.length > 0 && compact.length <= 16384, 'SIZE_OR_TYPE'); const segments = compact.split('.'); ensure(segments.length === 3, 'BAD_JWS'); const header = json(decoded(segments[0])); keys(header, ['alg', 'kid', 'typ']); ensure(Object.keys(HEADER).every(k => header[k] === HEADER[k]), 'UNSUPPORTED_HEADER'); const bytes = decoded(segments[1]); const signature = decoded(segments[2]); ensure(signature.length === 64, 'BAD_SIGNATURE_LENGTH'); const key = createPublicKey(publicKey); ensure(key.asymmetricKeyType === 'ed25519', 'UNSUPPORTED_KEY'); ensure(cryptoVerify(null, Buffer.from(segments[0] + '.' + segments[1], 'ascii'), key, signature), 'SIGNATURE_MISMATCH'); integrity = 'VALID'; const payload = json(bytes); validatePayload(payload); return { integrity, supported: true, code: 'SIGNATURE_VALID', payload }; } catch (error) { return { integrity, supported: false, code: error instanceof DemoError ? error.code : 'INVALID_KEY_OR_CRYPTO_INPUT' }; } } /** Compare synthetic evidence to an independently supplied local instruction. * The clock is a FIXED demonstration snapshot, not a freshness claim about today. * No replay store, identity registry, node consensus or production authorization. */ export function evaluateArtifact(compact, expected = EXPECTED, clock = DEMO_CLOCK, publicKey = PUBLIC_KEY_PEM) { const artifact = verifyArtifact(compact, publicKey); const base = { integrity: artifact.integrity, evaluation: 'INDETERMINATE', acceptance: 'REVIEW', reasons: [], source: 'synthetic-fixture', productionReady: false }; if (artifact.integrity !== 'VALID') return { ...base, acceptance: 'REJECT', reasons: [artifact.code] }; if (!artifact.supported) return { ...base, reasons: [artifact.code] }; try { keys(expected, Object.keys(EXPECTED)); ensure(Object.values(expected).every(v => typeof v === 'string' && v.length > 0 && v.length <= 100), 'BAD_LOCAL_POLICY'); ensure(/^(0|[1-9][0-9]{0,77})$/.test(expected.amountMinor), 'BAD_LOCAL_AMOUNT'); ensure(typeof clock === 'string' && Number.isFinite(Date.parse(clock)), 'BAD_LOCAL_CLOCK'); } catch { return { ...base, reasons: ['INVALID_LOCAL_POLICY'] }; } const p = artifact.payload; const reasons = []; for (const k of ['issuer', 'requestId', 'policyId']) if (p[k] !== expected[k]) reasons.push(k.toUpperCase() + '_MISMATCH'); if (p.source.chainId !== expected.chainId) reasons.push('CHAIN_MISMATCH'); const age = Date.parse(clock) - Date.parse(p.observedAt); if (age < 0 || age > 300000) reasons.push('OUTSIDE_DEMO_TIME_WINDOW'); if (reasons.length) return { ...base, evaluation: 'FAIL', acceptance: 'REJECT', reasons }; if (p.payment === null) return { ...base, reasons: ['PAYMENT_EVIDENCE_MISSING'] }; if (p.payment.status === 'pending') return { ...base, reasons: ['EXECUTION_PENDING'] }; if (p.payment.status === 'failed') reasons.push('EXECUTION_FAILED'); for (const k of ['asset', 'recipient', 'amountMinor']) if (p.payment[k] !== expected[k]) reasons.push(k.toUpperCase() + '_MISMATCH'); if (reasons.length) return { ...base, evaluation: 'FAIL', acceptance: 'REJECT', reasons }; // Even a matching example is NOT approved for production payment acceptance. return { ...base, evaluation: 'PASS', acceptance: 'REVIEW', reasons: ['SYNTHETIC_MATCH_NOT_PRODUCTION_APPROVAL'] }; } export function runDemo() { const cases = [ ['matching', 'Matching sample', 'VALID', 'PASS', 'REVIEW'], ['wrongRecipient', 'Wrong recipient', 'VALID', 'FAIL', 'REJECT'], ['missing', 'Missing evidence', 'VALID', 'INDETERMINATE', 'REVIEW'], ['tampered', 'Tampered report', 'INVALID', 'INDETERMINATE', 'REJECT'], ]; return cases.map(([key, name, integrity, evaluation, acceptance]) => { const result = evaluateArtifact(FIXTURES[key]); return { name, ...result, expectedResult: result.integrity === integrity && result.evaluation === evaluation && result.acceptance === acceptance }; }); } const isMain = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url; if (isMain) { const args = process.argv.slice(2); if (args.length > 1 || (args.length === 1 && !['--json', '--help'].includes(args[0]))) { console.error('Usage: node proof21-demo.mjs [--json | --help]'); process.exitCode = 2; } else if (args[0] === '--help') { console.log('Proof21 offline demo v' + VERSION + '\nNode 22+. No installation, wallet, network or file writes.\nRun without options for four synthetic examples; --json for structured output.\nNot the P21 SDK. Not suitable for real funds.'); } else { const results = runDemo(); if (args[0] === '--json') console.log(JSON.stringify({ demo: VERSION, clock: DEMO_CLOCK, liveEvidence: false, results }, null, 2)); else { console.log('\nProof21 · Offline demo v' + VERSION + '\nSynthetic examples only. No wallet, network calls, or live-chain verification.\n'); for (const r of results) console.log(`${r.name.padEnd(19)} signature ${r.integrity.padEnd(7)} check ${r.evaluation.padEnd(13)} decision ${r.acceptance}`); console.log('\nAll matching samples still require REVIEW: synthetic evidence cannot authorize real funds.'); console.log('This is an educational demo, not the production SDK.'); } if (!results.every(r => r.expectedResult)) process.exitCode = 1; } }