vax-sdk
v1.0.0
Published
VAX JSON Canonicalization Scheme (JCS) - TypeScript implementation
Downloads
14
Readme
VAX TypeScript SDK
Pure TypeScript implementation of VAX cryptographic primitives.
Zero dependencies. Works in Node.js, Bun, Deno, and browsers.
Installation
npm install vaxPhilosophy
VAX provides primitives, not a complete system.
You control:
- Storage structure
- Whether to add signatures
- Authorization logic
- Transport protocol
VAX provides:
- SAI chain computation
- JCS canonicalization
- Schema validation
Quick Start
import { computeGenesisSAI, computeSAI, generateGenesisSalt } from 'vax';
// 1. Genesis
const actorID = 'user123:device456';
const genesisSalt = generateGenesisSalt();
const genesisSAI = await computeGenesisSAI(actorID, genesisSalt);
// 2. Build action
const saeBytes = buildAction('transfer', data);
// 3. Compute SAI
const sai = await computeSAI(genesisSAI, saeBytes);
// 4. Store (your structure)
store({ sai, sae: saeBytes, prevSAI: genesisSAI });Core API
Genesis
async function computeGenesisSAI(
actorID: string,
genesisSalt: Uint8Array
): Promise<Uint8Array>Compute genesis SAI for an Actor.
Formula:
SAI_0 = SHA256("VAX-GENESIS" || actorID || genesisSalt)Example:
const actorID = 'user123:device456';
const salt = generateGenesisSalt(); // 16 random bytes
const sai = await computeGenesisSAI(actorID, salt);Chain
async function computeSAI(
prevSAI: Uint8Array,
saeBytes: Uint8Array
): Promise<Uint8Array>Compute SAI for an action.
Formula:
SAI_n = SHA256("VAX-SAI" || prevSAI || SHA256(SAE))Example:
const sae = buildSAE('transfer', data);
const sai = await computeSAI(prevSAI, sae);Verification
async function verifyChain(
expectedPrevSAI: Uint8Array,
saeBytes: Uint8Array,
clientSAI: Uint8Array
): Promise<void>Verify SAI chain integrity.
Throws:
InvalidPrevSAIError- Chain discontinuitySAIMismatchError- SAI computation mismatch
Example:
try {
await verifyChain(expectedPrevSAI, saeBytes, clientSAI);
// Chain valid
} catch (error) {
if (error instanceof SAIMismatchError) {
// SAI mismatch
}
}Schema-Driven Validation (SDTO)
Define Schema
import { newSchemaBuilder } from 'vax';
const schema = newSchemaBuilder()
.setActionStringLength('username', '3', '20')
.setActionNumberRange('amount', '0', '1000000')
.setActionEnum('currency', ['USD', 'EUR', 'TWD'])
.buildSchema();Build Validated Action
import { newAction } from 'vax';
const saeBytes = newAction('transfer', schema)
.set('username', 'alice')
.set('amount', 500.0)
.set('currency', 'USD')
.finalize(); // Returns Buffer with canonical JSONValidation happens at .set() time. Errors collected and thrown at .finalize().
Server-Side Validation
import { validateData } from 'vax';
// Backend validates SDTO against schema
try {
validateData(action.sdto, schema);
} catch (error) {
// Schema violation
}JCS (JSON Canonicalization)
import { marshal } from 'vax';
// Marshal to canonical JSON
const canonical = marshal(obj);
// Always produces identical output
const obj1 = { b: 2, a: 1 };
const obj2 = { a: 1, b: 2 };
const bytes1 = marshal(obj1); // {"a":1,"b":2}
const bytes2 = marshal(obj2); // {"a":1,"b":2}
// bytes1.equals(bytes2) ✅Never use JSON.stringify() for SAE. Always use marshal().
SAE (Semantic Action Envelope)
import { buildSAE } from 'vax';
const saeBytes = buildSAE('transfer', {
username: 'alice',
amount: 500.0,
currency: 'USD',
});
// Returns Buffer with canonical JSON:
// {"action_type":"transfer","sdto":{...},"timestamp":1704672000000}Structure:
interface Envelope {
action_type: string;
timestamp: number;
sdto: Record<string, unknown>;
}No signature field. If you need signatures, add them yourself.
Complete Workflow
Client Side
import { computeSAI, newAction } from 'vax';
// 1. Get schema from backend
const schema = await backend.getSchema('transfer');
// 2. Build validated action
const saeBytes = newAction('transfer', schema)
.set('username', 'alice')
.set('amount', 500.0)
.set('currency', 'USD')
.finalize();
// 3. Compute SAI
const prevSAI = getLastSAI();
const sai = await computeSAI(prevSAI, saeBytes);
// 4. Submit to backend
await backend.submit({
sai,
sae: saeBytes,
prevSAI,
});Backend Side
import { verifyChain, validateData } from 'vax';
// 1. Verify chain
await verifyChain(expectedPrevSAI, req.sae, req.sai);
// 2. Validate schema
const env = JSON.parse(req.sae.toString());
validateData(env.sdto, schema);
// 3. Optional: Add signature (using @noble/ed25519 or similar)
const signature = await ed25519.sign(req.sae, privateKey);
// 4. Store (your structure)
await db.store({
sai: req.sai,
sae: req.sae,
prevSAI: req.prevSAI,
signature, // optional
timestamp: Date.now(),
});Optional: Signatures
VAX doesn't handle signatures. Use standard libraries:
Node.js 18+ (Web Crypto)
// Note: Ed25519 support requires Node.js 18+ with experimental flag
// For production, use @noble/ed25519
const keyPair = await crypto.subtle.generateKey(
'Ed25519',
true,
['sign', 'verify']
);
const signature = await crypto.subtle.sign(
'Ed25519',
keyPair.privateKey,
saeBytes
);Using @noble/ed25519
import * as ed25519 from '@noble/ed25519';
// Generate key pair
const privateKey = ed25519.utils.randomPrivateKey();
const publicKey = await ed25519.getPublicKey(privateKey);
// Sign
const signature = await ed25519.sign(saeBytes, privateKey);
// Verify
const valid = await ed25519.verify(signature, saeBytes, publicKey);Storage is your choice:
// Option A: Separate field
interface ActionRecord {
sai: Uint8Array;
sae: Uint8Array;
prevSAI: Uint8Array;
signature?: Uint8Array; // Independent
}
// Option B: Wrap in metadata
interface ActionWithMeta {
action: {
sai: Uint8Array;
sae: Uint8Array;
prevSAI: Uint8Array;
};
metadata: {
signature?: Uint8Array;
timestamp: number;
author: string;
};
}
// Your choice!Helpers
// Generate random genesis salt
const salt = generateGenesisSalt(); // 16 bytes
// Hex encoding
const hex = toHex(sai);
const sai = fromHex(hex);
// Constants
SAI_SIZE // 32
GENESIS_SALT_SIZE // 16Error Handling
import {
VaxError,
InvalidInputError,
InvalidPrevSAIError,
SAIMismatchError
} from 'vax';
try {
await computeSAI(prevSAI, saeBytes);
} catch (error) {
if (error instanceof InvalidInputError) {
// Invalid input parameters
} else if (error instanceof SAIMismatchError) {
// SAI computation mismatch
}
}Platform Compatibility
Node.js
node --version # 18+ recommended
npm testBun
bun testDeno
import { computeSAI } from './src/index.ts';Browser
Works in modern browsers supporting:
crypto.subtle(Web Crypto API)TextEncoder/TextDecoder- ES2020+
Testing
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # With coverageExamples
See examples/ for complete workflows:
- minimal - Simplest usage, no signatures
- with-signature - Add backend signature
- browser - Browser usage
- node - Node.js usage
Key Principles
1. Always use marshal()
// ✅ Correct
import { marshal } from 'vax';
const bytes = marshal(obj);
// ❌ Wrong
const bytes = Buffer.from(JSON.stringify(obj));2. Validate early
// Validation at .set() time
const action = newAction('transfer', schema)
.set('amount', -100); // ❌ Fails immediately
// Errors collected at .finalize()
const saeBytes = action.finalize();3. Backend verifies, never repairs
// ✅ Backend verifies
await verifyChain(...);
validateData(...);
// ❌ Backend never "fixes" client data
// Don't do: data.amount = Math.abs(data.amount)Performance
Pure TypeScript with no dependencies:
ComputeSAI: ~25 µs
ComputeGenesisSAI: ~12 µs
JCS Marshal: ~8 µsCross-Language Compatibility
All implementations produce identical output:
# Test vector
actorID: "user123:device456"
genesisSalt: a1a2a3a4a5a6a7a8a9aaabacadaeafb0
# Expected
genesisSAI: afc50728cd79e805a8ae06875a1ddf78ca11b0d56ec300b160fb71f50ce658c3
# Verify
npm testDocumentation
License
MIT License
Philosophy
VAX is a tool, not a system.
We provide primitives. You decide how to use them.
