feat(ueps): initial push

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-01 22:42:11 +01:00
commit 38087671de
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 196 additions and 0 deletions

187
consent.js Normal file
View file

@ -0,0 +1,187 @@
#!/usr/bin/env node
/**
* UEPS Consent Token wire-level consent for LetherNet traffic
*
* A consent token is an Ed25519-signed message that authorises a specific
* data flow between two parties for a limited time. Every hop in LetherNet
* checks the consent token before forwarding.
*
* Token format:
* {
* version: 1,
* issuer: "iTHN...", // wallet address of consent giver
* subject: "iTHN...", // wallet address of consent recipient
* level: 2, // consent level (0-4)
* capabilities: ["vpn","dns"], // what the subject can do
* issued: 1712000000, // unix timestamp
* expires: 1712003600, // unix timestamp (1 hour default)
* scope: "charon.lthn", // gateway scope
* nonce: "random-hex", // replay protection
* signature: "ed25519-sig" // signed by issuer's chain key
* }
*
* Consent levels:
* 0 = None (reject all)
* 1 = Minimal (routing only, no data retention)
* 2 = Standard (service provision, ephemeral data)
* 3 = Extended (analytics, scoped retention)
* 4 = Full (complete trust, persistent data)
*/
const crypto = require('crypto');
class ConsentToken {
/**
* Create a new consent token
* @param {Object} opts
* @param {string} opts.issuer - iTHN address of consent giver
* @param {string} opts.subject - iTHN address of consent recipient
* @param {number} opts.level - Consent level (0-4)
* @param {string[]} opts.capabilities - Allowed capabilities
* @param {string} opts.scope - Gateway scope (e.g., "charon.lthn")
* @param {number} opts.ttl - Time to live in seconds (default: 3600)
*/
constructor(opts) {
this.version = 1;
this.issuer = opts.issuer;
this.subject = opts.subject;
this.level = Math.max(0, Math.min(4, opts.level || 0));
this.capabilities = opts.capabilities || [];
this.scope = opts.scope || '';
this.issued = Math.floor(Date.now() / 1000);
this.expires = this.issued + (opts.ttl || 3600);
this.nonce = crypto.randomBytes(16).toString('hex');
this.signature = null;
}
/**
* Get the signable payload (everything except signature)
*/
payload() {
return JSON.stringify({
v: this.version,
iss: this.issuer,
sub: this.subject,
lvl: this.level,
cap: this.capabilities,
scp: this.scope,
iat: this.issued,
exp: this.expires,
non: this.nonce,
});
}
/**
* Sign the token with an Ed25519 private key
* In production, this uses the chain wallet's spend key
*/
sign(privateKey) {
const payload = Buffer.from(this.payload());
this.signature = crypto.sign(null, payload, privateKey).toString('hex');
return this;
}
/**
* Verify the token signature against a public key
* In production, the public key comes from the chain alias lookup
*/
verify(publicKey) {
if (!this.signature) return false;
const payload = Buffer.from(this.payload());
const sig = Buffer.from(this.signature, 'hex');
return crypto.verify(null, payload, publicKey, sig);
}
/**
* Check if token is currently valid (not expired, not future-dated)
*/
isValid() {
const now = Math.floor(Date.now() / 1000);
return now >= this.issued && now < this.expires;
}
/**
* Check if token grants a specific capability
*/
hasCapability(cap) {
return this.capabilities.includes(cap);
}
/**
* Serialise to compact wire format
*/
toWire() {
return Buffer.from(JSON.stringify({
...JSON.parse(this.payload()),
sig: this.signature,
})).toString('base64');
}
/**
* Deserialise from wire format
*/
static fromWire(wire) {
const data = JSON.parse(Buffer.from(wire, 'base64').toString());
const token = new ConsentToken({
issuer: data.iss,
subject: data.sub,
level: data.lvl,
capabilities: data.cap,
scope: data.scp,
ttl: data.exp - data.iat,
});
token.issued = data.iat;
token.expires = data.exp;
token.nonce = data.non;
token.signature = data.sig;
return token;
}
/**
* Human-readable display
*/
toString() {
const levelNames = ['None', 'Minimal', 'Standard', 'Extended', 'Full'];
return `ConsentToken{${this.issuer?.slice(0,10)}${this.subject?.slice(0,10)} L${this.level}(${levelNames[this.level]}) caps=[${this.capabilities}] scope=${this.scope} valid=${this.isValid()}}`;
}
}
// === Test / Demo ===
if (require.main === module) {
// Generate test keypair (in production, these come from the chain wallet)
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
// Create a consent token: user authorises gateway for VPN + DNS
const token = new ConsentToken({
issuer: 'iTHNUNiuu3VP1yy8xH2y5iQaABKXurdjqZmzFiBiyR4dKG3j6534e9jMriY6SM7PH8NibVwVWW1DWJfQEWnSjS8n3Wgx86pQpY',
subject: 'iTHNQGNCbu8BH7FhQuzsv5CVsE3veDX1CFCxk9jsqRfA4tFcUPNwQa4DWLFvyaKvs3HGb8xfGDGQq871c9P4iRpt8SFusoqSzJ',
level: 2,
capabilities: ['vpn', 'dns'],
scope: 'charon.lthn',
ttl: 3600,
});
// Sign it
token.sign(privateKey);
console.log('Created:', token.toString());
console.log('');
// Verify it
const valid = token.verify(publicKey);
console.log('Signature valid:', valid);
console.log('Token valid (not expired):', token.isValid());
console.log('Has VPN capability:', token.hasCapability('vpn'));
console.log('Has compute capability:', token.hasCapability('compute'));
console.log('');
// Wire format roundtrip
const wire = token.toWire();
console.log('Wire format:', wire.slice(0, 60) + '...');
console.log('Wire size:', wire.length, 'bytes');
const restored = ConsentToken.fromWire(wire);
console.log('Roundtrip:', restored.toString());
console.log('Roundtrip verify:', restored.verify(publicKey));
}
module.exports = { ConsentToken };

9
package.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "lthn-ueps",
"version": "0.1.0",
"description": "UEPS consent tokens for LetherNet wire-level consent",
"main": "consent.js",
"scripts": {
"test": "node consent.js"
}
}