pithos/test/theseus-id.test.js

82 lines
4.3 KiB
JavaScript
Raw Permalink Normal View History

// Pithos links the person's Theseus ID to their hosted account by itself,
// once, right after signing in (DESIGN-theseus-id.md §11.3), and does nothing
// on hosts that do not offer Theseus ID. Runs the real gateway and the real
// control server; Theseus is played by the reference library in TheseusID/.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { createHosted } from '../hosted/server.js';
import { createPithos } from '../core/server.js';
const here = path.dirname(fileURLToPath(import.meta.url));
const tidDir = path.join(here, '..', '..', 'TheseusID');
const haveRef = fs.existsSync(path.join(tidDir, 'node_modules', '@noble', 'curves')) && fs.existsSync(path.join(here, '..', 'hosted', 'node_modules', '@bitauth'));
const ref = haveRef ? await import(pathToFileURL(path.join(tidDir, 'lib', 'index.mjs')).href) : null;
const deps = haveRef ? await import(pathToFileURL(path.join(tidDir, 'test', 'deps.mjs')).href) : null;
const iso = (ms) => new Date(ms).toISOString();
// What Theseus's api.theseusId.signIn returns for the Pithos add-on.
const theseusFor = (priv, calls) => async ({ projectId, nonce }) => {
calls.push(projectId);
const now = Date.now();
const message = ref.buildMessage({
kind: 'signin', origin: 'theseus-addon://pithos', account: deps.crypto.account(priv), uri: 'theseus-addon://pithos/',
chainId: 'tid:1', projectId, nonce, issuedAt: iso(now), expirationTime: iso(now + 300_000),
});
return { message, signature: deps.crypto.sign(priv, message) };
};
async function until(fn, ms = 5000) {
const end = Date.now() + ms;
for (;;) {
const v = await fn();
if (v || Date.now() > end) return v;
await new Promise((r) => setTimeout(r, 50));
}
}
test('signing in links the Theseus ID once; without the API nothing happens', { skip: !haveRef && 'TheseusID/node_modules or hosted/node_modules missing' }, async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pithos-tid-client-'));
const port = 19100 + Math.floor(Math.random() * 200);
const gwUrl = `http://127.0.0.1:${port}`;
const h = await createHosted({
listen: `127.0.0.1:${port}`, publicUrl: gwUrl, root: path.join(root, 'gw'), s3dBinary: null, portBase: 23400, maxRunning: 1, idleMinutes: 1,
gate: { invites: ['a', 'b'] },
}, () => {});
t.after(async () => { await h.close(); fs.rmSync(root, { recursive: true, force: true }); });
async function pithosWith(name, theseusIdSignIn) {
const p = await createPithos({
configFile: path.join(root, name, 'pithos.yml'), handoffPort: null, hostName: 'theseus',
secrets: { kind: 'vault', identitySeed: async () => crypto.randomBytes(32), encryptionKey: async () => crypto.randomBytes(32), theseusIdSignIn },
});
t.after(() => p.close());
const cookie = p.internalSession();
return (method, apiPath, body) => fetch(new URL(apiPath, p.url), {
method, headers: { cookie, 'x-pithos': '1', ...(body ? { 'content-type': 'application/json' } : {}) }, body: body ? JSON.stringify(body) : undefined,
}).then(async (r) => ({ status: r.status, data: await r.json().catch(() => null) }));
}
// In Theseus: sign up, and the ID is linked without being asked for.
const tidKey = crypto.randomBytes(32);
const calls = [];
const api = await pithosWith('in-theseus', theseusFor(tidKey, calls));
const signin = await api('POST', '/api/hosted/signin', { url: gwUrl, signup: { type: 'invite', code: 'a' } });
assert.equal(signin.status, 200, JSON.stringify(signin.data));
const linked = await until(async () => (await api('GET', '/api/hosted')).data?.me?.theseusId);
assert.equal(linked, deps.crypto.account(tidKey));
assert.deepEqual(calls, ['pithos'], 'signed for project "pithos", once');
await api('GET', '/api/hosted');
assert.equal(calls.length, 1, 'not again once linked');
// Elsewhere (no Theseus ID API): sign-in works and nothing is linked.
const plain = await pithosWith('elsewhere', null);
assert.equal((await plain('POST', '/api/hosted/signin', { url: gwUrl, signup: { type: 'invite', code: 'b' } })).status, 200);
await new Promise((r) => setTimeout(r, 300));
assert.equal((await plain('GET', '/api/hosted')).data?.me?.theseusId, null);
});