127 lines
4.8 KiB
JavaScript
127 lines
4.8 KiB
JavaScript
// Offline checks for the parts that have to match the shipped build byte-for-byte:
|
|
// the mrs-sig-v1 canonical string, the device-id derivation, and the header rewriting.
|
|
// Run: ./vendor/node test.mjs
|
|
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import assert from 'node:assert/strict';
|
|
|
|
// Point the config at a scratch dir so a real login is never touched.
|
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mirasim-test-'));
|
|
process.env.MIRASIM_PROXY_HOME = home;
|
|
|
|
const m = await import('./mirasim-relay.mjs');
|
|
|
|
let passed = 0;
|
|
function check(name, fn) {
|
|
try { fn(); process.stdout.write(` ok ${name}\n`); passed++; }
|
|
catch (err) { process.stdout.write(` FAIL ${name}\n ${err.message}\n`); process.exitCode = 1; }
|
|
}
|
|
|
|
check('device identity is stable across calls and persisted', () => {
|
|
const a = m.deviceIdentity();
|
|
const b = m.deviceIdentity();
|
|
assert.equal(a.deviceId, b.deviceId);
|
|
assert.equal(a.deviceId.length, 22);
|
|
const stored = JSON.parse(fs.readFileSync(path.join(home, 'config.json'), 'utf8'));
|
|
assert.ok(stored.device.privateKey.includes('BEGIN PRIVATE KEY'));
|
|
});
|
|
|
|
check('deviceId is sha256(spki-b64) base64url, first 22 chars', () => {
|
|
const id = m.deviceIdentity();
|
|
const expected = crypto.createHash('sha256').update(id.publicKeyB64).digest('base64url').slice(0, 22);
|
|
assert.equal(id.deviceId, expected);
|
|
});
|
|
|
|
check('config file is not world-readable', () => {
|
|
const mode = fs.statSync(path.join(home, 'config.json')).mode & 0o777;
|
|
assert.equal(mode, 0o600, `mode was ${mode.toString(8)}`);
|
|
});
|
|
|
|
check('canonical string is the 6-field newline join', () => {
|
|
const s = m.canonicalString({ method: 'post', path: '/v1/messages', ts: '123', nonce: 'n', bodySha256: 'abc' });
|
|
assert.equal(s, `${m.SIG_SCHEME}\nPOST\n/v1/messages\n123\nn\nabc`);
|
|
assert.equal(s.split('\n').length, 6);
|
|
});
|
|
|
|
check('signature verifies with the device public key', () => {
|
|
const id = m.deviceIdentity();
|
|
const body = Buffer.from(JSON.stringify({ hello: 'world' }));
|
|
const h = m.signatureHeaders(id, { method: 'POST', path: m.DEVICE_SESSION_PATH, body });
|
|
|
|
const canonical = m.canonicalString({
|
|
method: 'POST',
|
|
path: m.DEVICE_SESSION_PATH,
|
|
ts: h['x-mirasim-ts'],
|
|
nonce: h['x-mirasim-nonce'],
|
|
bodySha256: crypto.createHash('sha256').update(body).digest('hex'),
|
|
});
|
|
|
|
const pub = crypto.createPublicKey({
|
|
key: Buffer.from(id.publicKeyB64, 'base64'),
|
|
format: 'der',
|
|
type: 'spki',
|
|
});
|
|
assert.ok(crypto.verify(null, Buffer.from(canonical, 'utf8'), pub,
|
|
Buffer.from(h['x-mirasim-sig'], 'base64url')), 'signature did not verify');
|
|
assert.equal(h['x-mirasim-device'], id.deviceId);
|
|
assert.equal(h['x-mirasim-client'], m.CLIENT_VERSION);
|
|
});
|
|
|
|
check('nonces do not repeat', () => {
|
|
const id = m.deviceIdentity();
|
|
const seen = new Set();
|
|
for (let i = 0; i < 200; i++) {
|
|
seen.add(m.signatureHeaders(id, { method: 'POST', path: '/x', body: Buffer.alloc(0) })['x-mirasim-nonce']);
|
|
}
|
|
assert.equal(seen.size, 200);
|
|
});
|
|
|
|
check('empty body still hashes (no undefined in canonical)', () => {
|
|
const id = m.deviceIdentity();
|
|
const h = m.signatureHeaders(id, { method: 'GET', path: '/v1/models', body: undefined });
|
|
assert.match(h['x-mirasim-sig'], /^[A-Za-z0-9_-]+$/);
|
|
});
|
|
|
|
check('hop-by-hop headers are dropped', () => {
|
|
const out = m.forwardHeaders({
|
|
'content-length': '10', 'transfer-encoding': 'chunked', connection: 'keep-alive',
|
|
host: 'localhost:8787', 'anthropic-version': '2023-06-01',
|
|
});
|
|
assert.deepEqual(Object.keys(out).sort(), ['anthropic-version']);
|
|
});
|
|
|
|
check('anthropic-beta keeps only the relay-honoured value', () => {
|
|
const kept = m.KEPT_BETAS[0];
|
|
assert.equal(m.forwardHeaders({ 'anthropic-beta': `foo,${kept}, bar` })['anthropic-beta'], kept);
|
|
assert.equal('anthropic-beta' in m.forwardHeaders({ 'anthropic-beta': 'foo,bar' }), false);
|
|
});
|
|
|
|
check('array header values are joined', () => {
|
|
assert.equal(m.forwardHeaders({ 'x-thing': ['a', 'b'] })['x-thing'], 'a, b');
|
|
});
|
|
|
|
check('non-ascii header values are percent-encoded', () => {
|
|
assert.equal(m.sanitizeHeader('claude'), 'claude');
|
|
assert.equal(m.sanitizeHeader('中'), '%E4%B8%AD');
|
|
});
|
|
|
|
check('jwt claims are decoded from the payload segment', () => {
|
|
const payload = Buffer.from(JSON.stringify({ sub: 'u1', exp: 42 })).toString('base64url');
|
|
const claims = m.decodeJwt(`h.${payload}.s`);
|
|
assert.equal(claims.sub, 'u1');
|
|
assert.equal(claims.exp, 42);
|
|
assert.equal(m.decodeJwt('garbage'), null);
|
|
});
|
|
|
|
check('ticket manager reports no credential until a mint succeeds', () => {
|
|
const t = new m.TicketManager();
|
|
assert.equal(t.credential(), null);
|
|
assert.equal(t.signingIdentity(), null); // unsigned fallback, matching the desktop build
|
|
});
|
|
|
|
fs.rmSync(home, { recursive: true, force: true });
|
|
process.stdout.write(`\n${passed} passed${process.exitCode ? ', with failures' : ''}\n`);
|