Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/accounts.js

6.3 KiB, 1 run

created by r2519314175:1339, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* accounts.js — several people, one browser, one at a time.
2 *
3 * An account is a passphrase identity with its own chats, provider keys, credits, mail and
4 * workspace. Nobody sees another's. That isolation is achieved by NAMESPACING storage: every
5 * `daimond-*` localStorage key, the FSA handle store, and the OPFS workspace are prefixed with the
6 * current account — with ONE exception. The first account (the "primary") keeps the raw keys and
7 * the OPFS root exactly as a single-user install always had them, so adopting an existing install
8 * as account one moves not a single byte, and a browser with one account behaves byte-for-byte as
9 * it did before accounts existed. Only a SECOND account brings a prefix into being.
10 *
11 * This script loads before every other, so the shim over `localStorage` is in place before any
12 * module reads or writes. Switching account is a reload: modules read their account's data once,
13 * at load, so the clean way to hand them a different account is to start them again.
14 *
15 * What is NOT namespaced (browser-wide, shared by everyone at this browser): the account registry
16 * itself, the theme, the language, the display currency, the panel layout, and the line-number
17 * toggle. These are preferences of the device, not facts about a person.
18 */
19(function () {
20 'use strict';
21
22 var REG = 'daimond-accounts'; // [{ id, name, fp, primary }] — the registry, raw.
23 var CUR = 'daimond-current'; // the current account's id, raw.
24
25 // Device-wide, never namespaced: the registry, and pure UI preferences.
26 var GLOBAL = {
27 'daimond-accounts': 1, 'daimond-current': 1,
28 'daimond-theme': 1, 'daimond-layout': 1, 'daimond-files-lineno': 1,
29 // The language and the display currency sit with the theme: they say how
30 // this browser presents Daimond, not who is using it.
31 'daimond-locale': 1, 'daimond-currency': 1,
32 };
33
34 // The unshimmed methods, captured before the shim shadows them. All raw access below goes
35 // through these, so the registry and the sweep in remove() see true, un-prefixed keys.
36 var proto = window.Storage.prototype;
37 var rawGet = proto.getItem, rawSet = proto.setItem, rawDel = proto.removeItem, rawKey = proto.key;
38 var LS = window.localStorage;
39 function rget(k) { return rawGet.call(LS, k); }
40 function rset(k, v){ return rawSet.call(LS, k, v); }
41 function rdel(k) { return rawDel.call(LS, k); }
42
43 function list() { try { return JSON.parse(rget(REG) || '[]') || []; } catch (e) { return []; } }
44 function save(a) { rset(REG, JSON.stringify(a)); }
45 function find(id) {
46 var a = list();
47 for (var i = 0; i < a.length; i++) if (a[i].id === id) return a[i];
48 return null;
49 }
50
51 /// A device-unique account id: 16 hex chars from the CSPRNG.
52 function mint() {
53 var b = new Uint8Array(8);
54 crypto.getRandomValues(b);
55 return Array.prototype.map.call(b, function (x) { return (x + 256).toString(16).slice(1); }).join('');
56 }
57
58 // Ensure there is at least one account. An existing single-user install becomes the primary,
59 // carrying its name and fingerprint across so the registry can show it without unlocking; a
60 // fresh install gets a primary too. Either way the primary uses the RAW keys, so nothing moves.
61 (function ensure() {
62 if (list().length) return;
63 var primary = { id: mint(), name: rget('daimond-id-name') || '', fp: rget('daimond-id-fp') || '', primary: true };
64 save([primary]);
65 rset(CUR, primary.id);
66 })();
67
68 function currentId() {
69 var c = rget(CUR);
70 if (c && find(c)) return c;
71 var a = list();
72 var id = a.length ? a[0].id : null;
73 if (id) rset(CUR, id);
74 return id;
75 }
76 function account() { return find(currentId()); }
77
78 // The primary keeps raw keys and the OPFS root; every other account is prefixed.
79 function prefix() { var x = account(); return (x && !x.primary) ? ('d~' + x.id + '~') : ''; }
80 /// The OPFS subdirectory for the current account ('' for the primary, i.e. the root).
81 function opfsNs() { var x = account(); return (x && !x.primary) ? ('d~' + x.id) : ''; }
82
83 function nsKey(k) {
84 return (typeof k === 'string' && k.indexOf('daimond-') === 0 && !GLOBAL[k]) ? prefix() + k : k;
85 }
86
87 // The shim. Shadows the prototype methods on the instance, so every daimond-* read and write in
88 // every module lands in the current account's namespace with no call site aware of it.
89 LS.getItem = function (k) { return rget(nsKey(k)); };
90 LS.setItem = function (k, v) { return rset(nsKey(k), v); };
91 LS.removeItem = function (k) { return rdel(nsKey(k)); };
92
93 // ── Managing accounts ───────────────────────────────────────────
94
95 /// Add a new (non-primary) account and make it current. The caller reloads.
96 function add(name) {
97 var a = list();
98 var na = { id: mint(), name: String(name || '').trim(), fp: '', primary: false };
99 a.push(na); save(a); rset(CUR, na.id);
100 return na.id;
101 }
102 function setCurrent(id) { if (find(id)) { rset(CUR, id); return true; } return false; }
103 function rename(id, name) {
104 var a = list();
105 for (var i = 0; i < a.length; i++) if (a[i].id === id) a[i].name = String(name || '').trim();
106 save(a);
107 }
108 /// Record an account's fingerprint, so the picker can show it without unlocking.
109 function setFp(id, fp) {
110 var a = list();
111 for (var i = 0; i < a.length; i++) if (a[i].id === id) a[i].fp = fp || '';
112 save(a);
113 }
114
115 /// Remove a non-primary account and ALL of its namespaced localStorage. Its OPFS subdirectory
116 /// and its FSA handle store are the caller's to clear (they live outside localStorage). The
117 /// primary cannot be removed here — it owns the raw keys and the OPFS root, and dropping it is
118 /// "Forget this identity", which wipes the raw keys directly.
119 function remove(id) {
120 var x = find(id);
121 if (!x || x.primary) return false;
122 var pre = 'd~' + id + '~';
123 var kills = [];
124 for (var i = 0; i < LS.length; i++) {
125 var k = rawKey.call(LS, i);
126 if (k && k.indexOf(pre) === 0) kills.push(k);
127 }
128 kills.forEach(function (k) { rdel(k); });
129 var a = list().filter(function (y) { return y.id !== id; });
130 save(a);
131 if (rget(CUR) === id) rset(CUR, a.length ? a[0].id : '');
132 return true;
133 }
134
135 window.DaimondAccounts = {
136 list: list,
137 current: currentId,
138 account: account,
139 count: function () { return list().length; },
140 add: add,
141 setCurrent: setCurrent,
142 rename: rename,
143 setFp: setFp,
144 remove: remove,
145 prefix: prefix,
146 opfsNs: opfsNs,
147 };
148})();