summaryrefslogtreecommitdiffstats
path: root/tools/identity-test.js
blob: 179d6406c9176fa4e28cab6727016833acdb25c6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// Who the chat speaks as. If you logged in with a nostr account, everything
// this app signs must be signed by THAT account — the alternative is what
// people actually saw: their own avatar in the header, and their messages
// arriving under the wallet key's pubkey, looking like a stranger.
// Run: bun tools/identity-test.js
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import { messagesFeature } from '../src/features/messages.js';

let fails = 0;
const check = (name, ok, detail = '') => {
  console.log(` ${ok ? '✓' : '✗'} ${name}${detail ? ' — ' + String(detail).slice(0, 120) : ''}`);
  if (!ok) fails++;
};

globalThis.localStorage = {
  _d: {}, getItem(k) { return this._d[k] ?? null; }, setItem(k, v) { this._d[k] = String(v); },
  removeItem(k) { delete this._d[k]; }, key(i) { return Object.keys(this._d)[i] ?? null; },
  get length() { return Object.keys(this._d).length; },
};
globalThis.fetch = async () => { throw new Error('offline'); };

const walletSk = generateSecretKey();
const walletPk = getPublicKey(walletSk);
const loginSk = generateSecretKey();
const loginPk = getPublicKey(loginSk);

// Build the feature with a given login situation and see which key it would
// sign a profile with — publishProfile is the shortest path through identity().
function build({ login, resume }) {
  const store = {};
  const signed = [];
  const wallet = {
    mnemonic: 'x '.repeat(11) + 'y', utxos: [], txs: [],
    nostr: { sk: walletSk, pk: walletPk, ck: new Uint8Array(32) },
    loadFeatureState: (n, fb) => JSON.parse(store[n] || 'null') || fb,
    saveFeatureState: (n, s) => { store[n] = JSON.stringify(s); },
    _cacheKey: () => 'identity-test', saveCache: () => {},
    registerCacheExtension: () => {}, registerLoadHook: () => {},
  };
  const feature = messagesFeature({
    h: (tag, attrs, ...kids) => ({ tag, attrs, kids }),
    ui: {}, render: () => {}, wallet, toast: () => {}, brandHeader: () => null,
    hook: (name) => {
      if (name === 'nostrLoginIdentity') return login;
      if (name === 'nostrLoginResume') return resume;
      return null;
    },
  });
  return { feature, signed };
}

console.log('[logged in with nostr, signer live]');
{
  const signer = { pubkey: loginPk, signEvent: async (e) => ({ ...e, pubkey: loginPk, id: 'x', sig: 'y' }) };
  const { feature } = build({ login: { pubkey: loginPk, signer }, resume: null });
  let signedBy = null;
  try { await feature.publishProfile({ name: 'me' }); } catch (e) { signedBy = e.message; }
  // it fails at the relay (offline), but only AFTER choosing an identity —
  // what matters is that it never refused for want of one
  check('does not refuse for want of an identity', !/signer isn't connected/i.test(signedBy || ''), signedBy || 'reached the relay');
}

console.log('\n[logged in with nostr, signer lost to a reload]');
{
  const { feature } = build({ login: { pubkey: loginPk, signer: null }, resume: null });
  let err = null;
  try { await feature.publishProfile({ name: 'me' }); } catch (e) { err = e.message; }
  check('refuses rather than signing as someone else', /signer isn't connected/i.test(err || ''), err || 'no error!');
}

console.log('\n[logged in with nostr, signer resumes]');
{
  const signer = { pubkey: loginPk, signEvent: async (e) => ({ ...e, pubkey: loginPk, id: 'x', sig: 'y' }) };
  const { feature } = build({ login: { pubkey: loginPk, signer: null }, resume: Promise.resolve(signer) });
  let err = null;
  try { await feature.publishProfile({ name: 'me' }); } catch (e) { err = e.message; }
  check('a resumed signer is accepted', !/signer isn't connected/i.test(err || ''), err || 'reached the relay');
}

console.log('\n[no nostr login at all]');
{
  const { feature } = build({ login: null, resume: null });
  let err = null;
  try { await feature.publishProfile({ name: 'me' }); } catch (e) { err = e.message; }
  check('the wallet key still speaks for a plain seed wallet', !/signer isn't connected/i.test(err || ''), err || 'reached the relay');
}

console.log(fails ? `\n${fails} failing` : '\nall passing');
process.exit(fails ? 1 : 0);