Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/post.js

103 KiB, 23 runs

created by r2519314175:1417, 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/* ============================================================
2 Daimond — private messages (post.js)
3 ------------------------------------------------------------
4 The client half of the relay. `/api/post` is a post box the
5 gateway cannot read: a message is sealed on this device to a
6 key the recipient proved they hold, and what leaves here is
7 ciphertext with a little metadata around it.
8
9 ── THE FOUR RULES THIS FILE EXISTS TO KEEP ─────────────────
10
11 1. THE SEAL IS MADE HERE AND OPENED HERE. Nothing between the
12 two devices sees a body. The gateway carries base64 and
13 cannot do anything else with it.
14
15 2. WASM ENCODES, JAVASCRIPT SIGNS. The device signing key is a
16 non-extractable WebCrypto key, so it cannot be handed to
17 wasm and must not become extractable to make this easier.
18 `DaimondCrypto` hands out a signing input and takes a
19 signature back, and never sees a secret in either direction.
20
21 3. THE ACK COMES AFTER THE COMMIT, NEVER BEFORE. A message is
22 collected when a device has folded it into the account's
23 sync parcel AND THAT PUSH HAS COMMITTED. Then, and only
24 then, the relay is told it may let go. Ack after commit
25 costs a crash one re-collect; ack before commit costs a
26 device wiped in that window the only copy there was.
27
28 4. A ROW THE RELAY WROTE IS NEVER DRAWN AS A PERSON. `kind` is
29 a safety field: anything but "post" carries no envelope and
30 no signature, so it goes in `notes` and can never reach the
31 message list. The relay writes expiry notices; a relay that
32 had been taken over would write whatever it liked.
33
34 ── WHAT IS ENCRYPTED, AND WHERE ────────────────────────────
35 In flight and at the relay: the seal below. At rest on this
36 device: the store is wrapped with `DaimondIdentity.wrap`, the
37 same one scheme that wraps the API key, the mailbox passwords
38 and the forge voice. Not a second scheme — a second way of
39 encrypting a secret at rest is how one of the two stops being
40 reviewed. In the sync parcel: plaintext, because sync.js wraps
41 the whole parcel under the same key before it leaves.
42
43 The consequence is that the store can only be READ while the
44 identity is unlocked, so `snapshot()` answers null while it is
45 locked rather than an empty record. An empty record would read
46 to the merge on the other device as "everything was deleted".
47
48 Attaches one global, `window.DaimondPost`.
49 ============================================================ */
50(function () {
51 'use strict';
52
53 // ── Saying things ──────────────────────────────────────────
54
55 function t(k, v) { return window.DaimondI18n ? DaimondI18n.t(k, v) : k; }
56
57 /// A string from the table, or the English written at the call site where the
58 /// table has no entry for it yet. The same device voice.js and improve.js use.
59 function tOr(k, fallback, v) {
60 var s = t(k, v);
61 if (s !== k) return s;
62 if (!v) return fallback;
63 return String(fallback).replace(/\{(\w+)\}/g, function (whole, name) {
64 return v[name] != null ? String(v[name]) : whole;
65 });
66 }
67
68 function log(/* ...args */) {
69 try {
70 if (!window.DAIMOND_DEBUG) return;
71 console.log.apply(console, ['[post]'].concat([].slice.call(arguments)));
72 } catch (e) { /* no console */ }
73 }
74
75 // ── Where things are ───────────────────────────────────────
76
77 /// The relay. One path, five operations, all on the caller's own account.
78 var PATH = '/api/post';
79
80 /// The store, wrapped. `daimond-` prefixed so accounts.js namespaces it per
81 /// account without this file knowing: two people at one browser have two
82 /// message stores and neither can see the other's.
83 var LS = 'daimond-post';
84
85 /// The record's shape, so a later one can be told from this one.
86 // 2 since the artefact, the envelope and the content key began to be kept on
87 // each incoming message: a record written by version 1 has none of them, so a
88 // build reading one would draw a Report control over evidence that is not
89 // there. `read()` answers a fresh record for any version it does not know,
90 // which is the right trade while nothing is deployed -- the messages a bump
91 // costs are re-collectable from the relay; a report that cannot be checked is
92 // not repairable at all.
93 // 3 since `groups` joined it. A group's roster and the messages sealed under
94 // that roster are ONE account state and must merge together: a device that
95 // adopted the messages and not the roster would hold a message for a group it
96 // does not know it is in, and would refuse to open the next one.
97 var REC_V = 3;
98
99 /// The region the Social panel gives this module: the Messages view's list.
100 /// Everything drawn below lives inside it, and the panel's own head, chips and
101 /// empty line belong to improve.js. `DaimondSocial.filled('messages', n)` is
102 /// how the honest "not switched on" line above it goes away, and it goes away
103 /// only when a row has actually been drawn.
104 var HOST = '#social-messages-list';
105
106 /// Which view of the Social panel this module owns.
107 var VIEW = 'messages';
108
109 // ── The seal ───────────────────────────────────────────────
110 //
111 // One content key, sealed once per recipient slot: the age/PGP shape. That
112 // one choice buys the sender's own Sent copy, groups later, and an offline
113 // recovery slot, for a few lines.
114 //
115 // "DPS1" (4) | epk (32) | n (1) | slot × n (60 each) | iv (12) | ciphertext
116 //
117 // The ephemeral key is per message and is what makes a slot openable: the
118 // recipient computes the SAME shared secret from their own sealing key and
119 // this public one, so nothing about the sender has to travel in the clear for
120 // the seal to work. There is no recipient tag on a slot -- a reader tries
121 // each in turn, which costs microseconds and means the envelope discloses the
122 // NUMBER of recipients and not who they are.
123
124 /// Magic, so a blob that is not one of these is refused rather than decoded.
125 var MAGIC = [0x44, 0x50, 0x53, 0x31]; // "DPS1"
126
127 /// AES-GCM nonce width, matching identity.js.
128 var IV = 12;
129
130 /// A slot: nonce, then the 32-byte content key with its 16-byte tag.
131 var SLOT = IV + 32 + 16;
132
133 /// The domain this seal's key derivation runs in. A tag that is not a prefix
134 /// of any other tag, so two derivations can never collide.
135 var SEAL_INFO = 'daimond.post.seal.v1';
136
137 /// The schema every message is signed under. The purpose tag is inside the
138 /// signing input, so a signature over a card can never be read as one over a
139 /// message.
140 var SCHEMA = 'daimond/post/0';
141
142 /// The most a body may carry, in bytes of UTF-8. Exactly `limit::BODY_BYTES`
143 /// in the schema's own crate: checked here so a person is told before they
144 /// have composed anything, and checked there because that is the authority.
145 var BODY_MAX = 8 * 1024;
146
147 /// The most recipients one envelope may name. The slot count is one byte.
148 var SLOTS_MAX = 255;
149
150 // ── Encoding ───────────────────────────────────────────────
151
152 function utf8(s) { return new TextEncoder().encode(String(s)); }
153
154 function b64enc(buf) {
155 var b = (buf instanceof Uint8Array) ? buf : new Uint8Array(buf);
156 var bin = '';
157 for (var i = 0; i < b.length; i++) bin += String.fromCharCode(b[i]);
158 return btoa(bin);
159 }
160
161 function b64dec(str) {
162 var bin = atob(String(str));
163 var out = new Uint8Array(bin.length);
164 for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
165 return out;
166 }
167
168 /// Standard base64 to the base64url the gateway binds an account by. The two
169 /// encodings differ and mixing them up fails a lookup silently.
170 function b64url(b64) {
171 return String(b64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
172 }
173
174 /// base64url back to raw bytes.
175 function urldec(s) {
176 var b = String(s).replace(/-/g, '+').replace(/_/g, '/');
177 while (b.length % 4) b += '=';
178 return b64dec(b);
179 }
180
181 function hex(bytes) {
182 var s = '';
183 for (var i = 0; i < bytes.length; i++) s += ('0' + bytes[i].toString(16)).slice(-2);
184 return s;
185 }
186
187 function unhex(s) {
188 var str = String(s || '');
189 var out = new Uint8Array(str.length >> 1);
190 for (var i = 0; i < out.length; i++) out[i] = parseInt(str.substr(i * 2, 2), 16);
191 return out;
192 }
193
194 /// A millisecond timestamp, kept whole.
195 ///
196 /// NOT `| 0`, and this is the reason it is its own function. A bitwise
197 /// operator coerces to a SIGNED 32-BIT integer, and a Unix millisecond passed
198 /// 2038 in 1970 -- `Date.now()` is about 1.79e12, so `x | 0` wraps it to
199 /// whatever the low thirty-two bits happen to be. The wrapped values stay
200 /// locally ordered, which is exactly why this survives being looked at: two
201 /// stamps a second apart still compare correctly, and the ordering only
202 /// inverts when the pair straddles a 2^32 boundary, about every fifty days.
203 /// A message list that sorted itself wrongly for one day in fifty, or a
204 /// roster that a later one failed to replace, would be blamed on anything but
205 /// arithmetic.
206 ///
207 /// Seconds-scale stamps -- the gateway's `row.ts` -- are inside the range and
208 /// are left as they were.
209 function ms(v) {
210 var n = Number(v);
211 return isFinite(n) ? Math.trunc(n) : 0;
212 }
213
214 /// Concatenate byte arrays.
215 function cat(parts) {
216 var n = 0, i;
217 for (i = 0; i < parts.length; i++) n += parts[i].length;
218 var out = new Uint8Array(n), at = 0;
219 for (i = 0; i < parts.length; i++) { out.set(parts[i], at); at += parts[i].length; }
220 return out;
221 }
222
223 /// Constant-ish byte equality, for comparing keys. Length first, then every
224 /// byte: equality of keys is always the FULL key, never a fingerprint.
225 function sameBytes(a, b) {
226 if (!a || !b || a.length !== b.length) return false;
227 var d = 0;
228 for (var i = 0; i < a.length; i++) d |= a[i] ^ b[i];
229 return d === 0;
230 }
231
232 // ── The wasm bridge ────────────────────────────────────────
233 //
234 // The same arrangement identity.js uses, and for the same reason: this is a
235 // classic script, the canonical encoding lives in the format's own crate, and
236 // a second encoding written in JavaScript would be a second address for one
237 // message. Nothing here computes what the crate owns.
238
239 /// The bridge, or null before it is up.
240 function bridge() {
241 return (typeof window !== 'undefined' && window.DaimondCrypto) || null;
242 }
243
244 /// Whether the bridge carries everything this file needs.
245 ///
246 /// `postDraft` is the one name identity.js did not need, and it is the message
247 /// encoder. Said out loud when it is missing rather than worked around: a
248 /// message encoded here instead would have a different address from the same
249 /// message encoded by any other build.
250 function cryptoReady() {
251 var b = bridge();
252 return !!(b && typeof b.postDraft === 'function' && typeof b.signingInput === 'function'
253 && typeof b.assemble === 'function' && typeof b.address === 'function'
254 && typeof b.read === 'function');
255 }
256
257 /// Why the bridge cannot be used, in words, or '' when it can.
258 function cryptoWhy() {
259 var b = bridge();
260 if (!b) return tOr('post.err_no_bridge',
261 'This build cannot compose a message: its message format is not loaded.');
262 if (typeof b.postDraft !== 'function') return tOr('post.err_no_draft',
263 'This build cannot compose a message: its message encoder is not loaded.');
264 return cryptoReady() ? '' : tOr('post.err_no_bridge',
265 'This build cannot compose a message: its message format is not loaded.');
266 }
267
268 // ── Sealing ────────────────────────────────────────────────
269
270 /// Derive one slot key from a shared secret.
271 ///
272 /// The raw ECDH output is not uniformly distributed, so it is the INPUT to a
273 /// derivation and never a key. The recipient's own public key is in the salt,
274 /// which binds a slot to the party it was made for: a slot lifted out of one
275 /// envelope and dropped into another derives a different key and does not open.
276 async function slotKey(sharedBits, epk, theirPub) {
277 var base = await crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, ['deriveKey']);
278 return await crypto.subtle.deriveKey(
279 { name: 'HKDF', hash: 'SHA-256', salt: cat([epk, theirPub]), info: utf8(SEAL_INFO) },
280 base,
281 { name: 'AES-GCM', length: 256 },
282 false,
283 ['encrypt', 'decrypt']);
284 }
285
286 /// Seal bytes to a list of 32-byte X25519 public keys.
287 ///
288 /// The sender puts their OWN key in the list to keep a Sent copy; nothing here
289 /// does that for them, because a caller that did not ask for one must not get
290 /// a slot it does not know about.
291 async function seal(recipients, plainBytes) {
292 if (!recipients || !recipients.length) {
293 throw new Error(tOr('post.err_no_recipient',
294 'A sealed message needs at least one recipient key.'));
295 }
296 if (recipients.length > SLOTS_MAX) {
297 throw new Error(tOr('post.err_too_many',
298 'A message can be sealed to at most {n} people at once.', { n: SLOTS_MAX }));
299 }
300 var i;
301 for (i = 0; i < recipients.length; i++) {
302 if (!recipients[i] || recipients[i].length !== 32) {
303 throw new Error(tOr('post.err_bad_key',
304 'One of the recipients has no usable key, so nothing was sent.'));
305 }
306 }
307
308 var pair = await crypto.subtle.generateKey({ name: 'X25519' }, true, ['deriveBits']);
309 var epk = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));
310
311 // The content key: one per message, sealed once per slot.
312 var ck = crypto.getRandomValues(new Uint8Array(32));
313 var aad = cat([new Uint8Array(MAGIC), epk]);
314
315 var slots = [];
316 for (i = 0; i < recipients.length; i++) {
317 var theirs = await crypto.subtle.importKey(
318 'raw', recipients[i], { name: 'X25519' }, false, []);
319 var bits = new Uint8Array(await crypto.subtle.deriveBits(
320 { name: 'X25519', public: theirs }, pair.privateKey, 256));
321 var k = await slotKey(bits, epk, recipients[i]);
322 var iv = crypto.getRandomValues(new Uint8Array(IV));
323 var ct = new Uint8Array(await crypto.subtle.encrypt(
324 { name: 'AES-GCM', iv: iv, additionalData: aad }, k, ck));
325 slots.push(cat([iv, ct]));
326 }
327
328 var head = cat([new Uint8Array(MAGIC), epk, new Uint8Array([recipients.length])]
329 .concat(slots));
330 // The body is bound to the WHOLE head, so a slot cannot be swapped in from
331 // another envelope without the body ceasing to open.
332 var bodyKey = await crypto.subtle.importKey(
333 'raw', ck, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
334 var biv = crypto.getRandomValues(new Uint8Array(IV));
335 var bct = new Uint8Array(await crypto.subtle.encrypt(
336 { name: 'AES-GCM', iv: biv, additionalData: head }, bodyKey, plainBytes));
337 return cat([head, biv, bct]);
338 }
339
340 /// Open a sealed envelope with this device's sealing key, and answer BOTH the
341 /// plaintext artefact and the content key that opened it.
342 ///
343 /// Throws with a sentence a person can read. A slot that does not open is not
344 /// an error -- most slots in a group message are somebody else's -- so the
345 /// refusal comes only when NONE of them does.
346 async function unsealFull(bytes) {
347 var b = (bytes instanceof Uint8Array) ? bytes : new Uint8Array(bytes);
348 if (b.length < 4 + 32 + 1 + SLOT + IV + 16) {
349 throw new Error(tOr('post.err_short', 'That message is too short to be one.'));
350 }
351 for (var m = 0; m < 4; m++) {
352 if (b[m] !== MAGIC[m]) {
353 throw new Error(tOr('post.err_not_sealed',
354 'That is not a sealed Daimond message.'));
355 }
356 }
357 var epk = b.slice(4, 36);
358 var n = b[36];
359 var end = 37 + n * SLOT;
360 if (n === 0 || b.length < end + IV + 16) {
361 throw new Error(tOr('post.err_short', 'That message is too short to be one.'));
362 }
363 var head = b.slice(0, end);
364 var aad = cat([new Uint8Array(MAGIC), epk]);
365
366 var mine = window.DaimondIdentity ? DaimondIdentity.sealingKeyRaw() : null;
367 if (!mine) {
368 throw new Error(tOr('post.err_no_sealing_key',
369 'This device has no sealing key, so it cannot open a sealed message. '
370 + 'Unlock Daimond once and one will be made.'));
371 }
372 // One shared secret, one key, tried against every slot. The recipient does
373 // not know which slot is theirs, and an envelope that said would be an
374 // envelope that names its readers.
375 var bits = await DaimondIdentity.sharedSecret(epk);
376 var k = await slotKey(bits, epk, mine);
377
378 var ck = null;
379 for (var i = 0; i < n; i++) {
380 var at = 37 + i * SLOT;
381 try {
382 ck = new Uint8Array(await crypto.subtle.decrypt(
383 { name: 'AES-GCM', iv: b.slice(at, at + IV), additionalData: aad },
384 k, b.slice(at + IV, at + SLOT)));
385 break;
386 } catch (e) { ck = null; }
387 }
388 if (!ck) {
389 throw new Error(tOr('post.err_not_for_you',
390 'This message was not sealed to any key this device holds.'));
391 }
392 var bodyKey = await crypto.subtle.importKey(
393 'raw', ck, { name: 'AES-GCM' }, false, ['decrypt']);
394 // THE CONTENT KEY COMES BACK OUT WITH THE PLAINTEXT, and that is the whole
395 // of why this function was split in two. It used to be recovered here and
396 // thrown away, so a caller that needed it later -- report.js, which has to
397 // hand an operator the sealed form AND the key that opens it, or the report
398 // is unverifiable -- had only one way to get it: implement the seal a second
399 // time. This file's own header forbids exactly that, for the reason voice.js
400 // states: a second way of doing this is how one of the two stops being
401 // reviewed.
402 return {
403 plain: new Uint8Array(await crypto.subtle.decrypt(
404 { name: 'AES-GCM', iv: b.slice(end, end + IV), additionalData: head },
405 bodyKey, b.slice(end + IV))),
406 ck: ck,
407 };
408 }
409
410 /// The plaintext artefact alone, which is what most callers want.
411 ///
412 /// The published shape, unchanged: `unsealFull` is the one that also answers
413 /// the content key, and nothing outside this file needs both unless it is
414 /// building a report.
415 async function unseal(bytes) {
416 return (await unsealFull(bytes)).plain;
417 }
418
419 // ── Composing ──────────────────────────────────────────────
420
421 /// Build, sign and seal one message. Answers `{ addr, envelope, artefact }`.
422 ///
423 /// THE SEAM, drawn the way §2.5.4 requires it: the crate encodes the payload
424 /// and says what to sign, this signs it with a key that never crosses the
425 /// boundary, and the crate takes the signature back and assembles. A caller
426 /// cannot sign one envelope and assemble a different one, because the envelope
427 /// is a pure function of the four arguments both calls are given.
428 ///
429 /// `to` is the recipient's SIGNING key -- what the relay addresses by and what
430 /// the reader checks the payload against. `toEnc` is their SEALING key, which
431 /// is a different key for a stated reason (see identity.js), and is what the
432 /// slot is made for.
433 ///
434 /// `group` is the other shape, and it changes only what goes in those two
435 /// places: `{ id, enc }` puts the GROUP's 32-byte id in the signed `to` and
436 /// gives the envelope one slot per member. The id is not a public key and
437 /// nothing here treats it as one -- the schema's `to` is thirty-two bytes and
438 /// says nothing about what they are -- so a group needs no second schema, no
439 /// change to the wasm write side and no third field anywhere.
440 ///
441 // ── THE FAN-OUT, AND WHERE IT ACTUALLY STOPS ────────────────
442 //
443 // One envelope, one slot per member. A slot is `SLOT` = 60 bytes: 12 of
444 // nonce, 32 of content key, 16 of tag. So the envelope carries 60n bytes
445 // over what a one-to-one message costs:
446 //
447 // 10 members 600 B nothing
448 // 50 members 3.0 KB nothing
449 // 255 members 15.3 KB THE HARD STOP
450 //
451 // 255 and not the thousand §12.6 estimates, for two reasons that are both
452 // in this file rather than in the plan: the slot count is ONE BYTE
453 // (`SLOTS_MAX`), and a slot is 60 bytes and not the 48 the plan assumed.
454 //
455 // The bytes are not the wall, though. The wall is the DELIVERIES. `send`
456 // posts the same envelope once per member, so one message to a group of
457 // fifty is fifty requests, each taking the relay's single `post_writes`
458 // mutex (gateway/src/schema.rs, `Store::deliver_post`), and it lands fifty
459 // rows against a box cap of 500 (`POST_BOX_MAX_ROWS`). Fifty people sending
460 // ten messages each fills every box in the group. The trigger §12.6 sets
461 // for a real group key -- "roughly a thousand members" -- is therefore
462 // reached at TENS of members and not at a thousand, and it is reached by
463 // request count and box pressure long before it is reached by bytes.
464 async function compose(opts) {
465 var o = opts || {};
466 var body = String(o.body == null ? '' : o.body);
467 await read(); // so `encFor` can see the cards this device holds
468 var why = cryptoWhy();
469 if (why) throw new Error(why);
470 if (!window.DaimondIdentity || !DaimondIdentity.isUnlocked()) {
471 throw new Error(tOr('post.err_locked',
472 'Unlock Daimond to send a message: it is signed with your own key.'));
473 }
474 if (!body.trim()) {
475 throw new Error(tOr('post.err_empty', 'There is nothing to send.'));
476 }
477 if (utf8(body).length > BODY_MAX) {
478 throw new Error(tOr('post.err_long',
479 'That message is longer than {n} characters of text and was not sent. '
480 + 'It is refused rather than cut: half a message is not a shorter message.',
481 { n: BODY_MAX }));
482 }
483 var grp = o.group || null;
484 var toPub = grp ? grp.id : (o.to instanceof Uint8Array ? o.to : urldec(o.to));
485 if (!toPub || toPub.length !== 32) {
486 throw new Error(tOr('post.err_bad_key',
487 'One of the recipients has no usable key, so nothing was sent.'));
488 }
489 var toEnc = null;
490 if (!grp) {
491 // Named by the caller, or looked up. `encFor` asks trust.js first and
492 // falls back to the cards read here; a caller that already holds the
493 // key passes it.
494 toEnc = o.toEnc instanceof Uint8Array ? o.toEnc
495 : (o.toEnc ? b64dec(o.toEnc) : encFor(o.to));
496 if (!toEnc || toEnc.length !== 32) {
497 throw new Error(tOr('post.err_no_card',
498 'There is no sealing key for that person yet, so nothing can be sealed to them. '
499 + 'Scan their code, or ask them to send you theirs.'));
500 }
501 }
502 // A group's slot list MAY be empty, and this is where that was once
503 // refused. A group of one -- made, and nobody added yet -- still has a
504 // roster, and that roster still has to be sealed and stored so it reaches
505 // this account's other devices. The sender's own slot below makes it a
506 // valid envelope with one slot in it. Nothing is lost by allowing it: a
507 // MESSAGE to a group with nobody in it is refused a step earlier, by
508 // `DaimondGroup.sealTo`, in a sentence about the group rather than about
509 // the seal.
510
511 var b = bridge();
512 var nonce = crypto.getRandomValues(new Uint8Array(16));
513 var draft = b.postDraft(body, toPub, nonce);
514 var payload;
515 try {
516 if (o.replyTo) draft.replyTo(unhex(o.replyTo));
517 (o.refs || []).forEach(function (r) { addRef(draft, r); });
518 payload = draft.encode();
519 } finally {
520 // A wasm-bindgen object holds memory on the other side of the boundary
521 // until it is told to let go, and a draft that is not freed is a leak
522 // per message rather than per session.
523 try { if (draft && draft.free) draft.free(); } catch (e) { /* already freed */ }
524 }
525
526 var author = await DaimondIdentity.publicKeyRaw();
527 var when = Date.now();
528 var input = b.signingInput(payload, SCHEMA, author, when);
529 // `sign` answers STANDARD base64, not base64url. The envelope wants the raw
530 // bytes, so it is decoded rather than passed on as text.
531 var sig = b64dec(await DaimondIdentity.sign(input));
532 var artefact = b.assemble(payload, SCHEMA, author, when, sig);
533 var addr = hex(b.address(payload));
534
535 // The sender's own slot, so a Sent copy is readable on this account's other
536 // devices. Left out when this device has no sealing key: better a message
537 // the sender cannot re-read than one that cannot be sent at all.
538 //
539 // One slot each and NO RECIPIENT TAG ON ANY OF THEM, which is what a
540 // group message gets for free from the one-to-one seal: the envelope
541 // discloses how many people are in the group and never which. A reader
542 // trial-decrypts, at microseconds a slot. Nothing below may add a tag to
543 // make that loop shorter -- the loop is the property.
544 var mine = DaimondIdentity.sealingKeyRaw();
545 var to = grp ? grp.enc.slice() : [toEnc];
546 if (mine && !to.some(function (k) { return sameBytes(mine, k); })) to.push(mine);
547
548 return {
549 addr: addr,
550 artefact: artefact,
551 envelope: b64enc(await seal(to, artefact)),
552 ts: when,
553 };
554 }
555
556 /// Hang one reference on a draft. The four kinds the schema admits, named
557 /// rather than passed through: a fifth would be signed and drawn by nobody.
558 function addRef(draft, r) {
559 var fb = String((r && r.fallback) || '');
560 switch (r && r.kind) {
561 case 'proposal':
562 draft.addProposal(String(r.account || ''), String(r.repo || ''), r.number | 0, fb);
563 break;
564 case 'build':
565 draft.addBuild(String(r.id || ''), fb);
566 break;
567 case 'panel':
568 draft.addPanel(String(r.name || ''), fb);
569 break;
570 case 'guide':
571 draft.addGuide(String(r.page || ''), String(r.anchor || ''), fb);
572 break;
573 default:
574 throw new Error(tOr('post.err_bad_ref',
575 'That is not a kind of reference a message can carry.'));
576 }
577 }
578
579 /// Open one collected envelope and say what it turned out to be.
580 ///
581 /// THE READER CHECKS AND NOBODY ELSE. The whole verification -- magic,
582 /// envelope, address, signature -- runs in `DaimondCrypto.read`, on this
583 /// device. Two checks are made here on top of it, and both are about this
584 /// account rather than about the artefact:
585 ///
586 /// - the payload's `to` must be THIS account's key, OR a group this device is
587 /// in. A message sealed to us but addressed to somebody else is a message
588 /// somebody re-slotted, and the signature covers `to`, so this catches it;
589 /// - the address the relay carried must be the address the artefact has, or
590 /// the row and the message are not the same thing.
591 ///
592 /// THE GROUP CASE IS THE SAME CHECK, asked of a different holder. A group id
593 /// is thirty-two signed bytes in exactly the place a signing key sits, and
594 /// group.js answers whether this device is in the group they name AND whether
595 /// the author is in its current roster. Both halves matter: without the first
596 /// anybody could address a message to any thirty-two bytes and have it drawn;
597 /// without the second a member the creator removed would keep being drawn for
598 /// ever, because there is no group key to rotate them out of and the relay
599 /// knows nothing about groups at all. A build with no group module answers no
600 /// to both, and behaves exactly as it did before groups existed.
601 async function openEnvelope(b64, expectAddr) {
602 var opened = await unsealFull(b64dec(b64));
603 var plain = opened.plain;
604 var b = bridge();
605 if (!b || typeof b.read !== 'function') {
606 throw new Error(tOr('post.err_no_bridge',
607 'This build cannot compose a message: its message format is not loaded.'));
608 }
609 var got = JSON.parse(b.read(plain));
610 if (got.kind !== 'post') {
611 throw new Error(tOr('post.err_not_a_post',
612 'That is not a message; it is a {kind}.', { kind: String(got.kind || '?') }));
613 }
614 var mine = await DaimondIdentity.publicKeyRaw();
615 if (!mine || hex(mine) !== String(got.post.to)) {
616 var g = null;
617 try {
618 if (window.DaimondGroup && DaimondGroup.accepts) {
619 g = await DaimondGroup.accepts(String(got.post.to), got);
620 }
621 } catch (e) { g = null; }
622 if (!g) {
623 throw new Error(tOr('post.err_not_addressed',
624 'That message is addressed to a different key from this one.'));
625 }
626 got.gid = g.gid;
627 got.gname = g.name || '';
628 got.gop = !!g.op;
629 }
630 if (expectAddr && String(expectAddr) !== String(got.address)) {
631 throw new Error(tOr('post.err_addr_mismatch',
632 'The message the relay named is not the message it carried.'));
633 }
634 // THE EVIDENCE, carried out beside the reading of it. `art` is the bytes the
635 // signature is over; `ck` is what opened the body. A caller that only wants
636 // the words ignores both, and `collect` keeps them so that a message can
637 // still be reported after the relay has been told to let go -- at which
638 // point this device holds the only copy there is.
639 got.art = plain;
640 got.ck = opened.ck;
641 return got;
642 }
643
644 // ── The store ──────────────────────────────────────────────
645 //
646 // Held in memory while unlocked and wrapped at rest. Read once per unlock;
647 // `null` until it has been, which is what stops a locked device publishing an
648 // empty record into the parcel and deleting the account's mail everywhere.
649
650 /// The record, or null when it has not been read.
651 var _st = null;
652
653 /// A write that has not landed yet, so two writes in a row do not race.
654 var _writing = null;
655
656 /// A fresh, empty record.
657 function blank() {
658 return { v: REC_V, through: 0, acked: 0, tries: 0, msgs: {}, notes: {}, groups: {} };
659 }
660
661 /// Read the store out from under the passphrase. Idempotent.
662 ///
663 /// A record that will not unwrap is NOT replaced with an empty one: that would
664 /// hand the merge an empty record to spread. It is reported, and the module
665 /// stays unread until an unlock that works.
666 async function read() {
667 if (_st) return _st;
668 if (!window.DaimondIdentity || !DaimondIdentity.isUnlocked()) return null;
669 var raw = null;
670 try { raw = localStorage.getItem(LS); } catch (e) { raw = null; }
671 if (!raw) { _st = blank(); return _st; }
672 var plain;
673 try { plain = await DaimondIdentity.unwrap(raw); }
674 catch (e) { log('store will not unwrap under this passphrase'); return null; }
675 var r = null;
676 try { r = JSON.parse(plain); } catch (e) { r = null; }
677 if (!r || r.v !== REC_V) { _st = blank(); return _st; }
678 r.msgs = r.msgs || {};
679 r.notes = r.notes || {};
680 r.groups = r.groups || {};
681 r.through = r.through | 0;
682 r.acked = r.acked | 0;
683 r.tries = r.tries | 0;
684 _st = r;
685 return _st;
686 }
687
688 /// Write the store back, wrapped. Serialised, so an interleaved pair of
689 /// writes cannot leave the older one on disk.
690 async function save() {
691 if (!_st) return;
692 var mine = _writing = (_writing || Promise.resolve()).then(async function () {
693 try {
694 localStorage.setItem(LS, await DaimondIdentity.wrap(JSON.stringify(_st)));
695 } catch (e) { log('store write failed', e); }
696 });
697 await mine;
698 if (_writing === mine) _writing = null;
699 }
700
701 /// Drop what is in memory, for an account switch or a lock.
702 function forget() { _st = null; }
703
704 // ── The groups half of the record ──────────────────────────
705 //
706 // group.js holds NO STORAGE OF ITS OWN and reaches the roster through these
707 // three. The record is already wrapped at rest under the identity key,
708 // already re-sealed by `DaimondRekey` on a passphrase change and already
709 // carried on the sync parcel; a second store would have to repeat all three
710 // and would be the weaker of the two, since nothing would exercise it as
711 // often. It is also the correct factoring rather than only the cheap one: a
712 // device that adopted the messages without the roster would hold a message
713 // for a group it does not know it is in.
714
715 /// Whether this device has JOINED a group, read synchronously off the record.
716 /// A group only invited, or left, answers false.
717 function groupJoined(st, gid) {
718 var g = st && st.groups && st.groups[String(gid)];
719 return !!(g && g.state === 'joined');
720 }
721
722 /// Every group this account knows, as a copy. A copy, so a panel that hangs a
723 /// drawing flag on a row cannot write one into the store.
724 async function groups() {
725 var st = await read();
726 if (!st) return null;
727 return JSON.parse(JSON.stringify(st.groups || {}));
728 }
729
730 /// Write one group's record back. Answers false while the identity is locked.
731 async function putGroup(gid, rec) {
732 var st = await read();
733 if (!st || !rec) return false;
734 // Whatever a panel hung on the copy stays on the panel's copy.
735 delete rec.iAmCreator;
736 st.groups[String(gid)] = rec;
737 await save();
738 return true;
739 }
740
741 /// Take the tray flag off every message of one group, because the invitation
742 /// has been accepted. The same act `connect('accept')` performs for a person,
743 /// and for the same reason: the messages were sealed to this device and are
744 /// its own; the tray was holding them until the invitation was answered.
745 async function untrayGroup(gid) {
746 var st = await read();
747 if (!st) return 0;
748 var n = 0;
749 Object.keys(st.msgs).forEach(function (a) {
750 if (st.msgs[a].gid === String(gid) && st.msgs[a].tray) { st.msgs[a].tray = 0; n++; }
751 });
752 if (n) { await save(); render(); }
753 return n;
754 }
755
756 // ── The unlock boundary ────────────────────────────────────
757 //
758 // `attachPanel` reads the store at `DOMContentLoaded`, which is BEFORE the
759 // passphrase has been typed, so that read got nothing and nothing asked
760 // again. The store then stayed unread for the whole session unless somebody
761 // opened Social -> Messages by hand, and three things followed from it:
762 // `snapshot()` answered null so the record was left off every sync parcel,
763 // `adopt()` dropped an arriving one, and `unread()` answered 0 so the badge
764 // whose only job is to say "open the panel" could not count until the panel
765 // had been opened. identity.js announces the boundary; this listens.
766
767 /// Read the store and redraw, for a caller that has just unlocked.
768 async function wake() {
769 try {
770 await read();
771 await refreshDir();
772 } catch (e) { log('wake failed', e); }
773 try { render(); } catch (e) { /* no panel yet */ }
774 return !!_st;
775 }
776
777 try {
778 window.addEventListener('daimond:unlock', function () { wake(); });
779 window.addEventListener('daimond:lock', function () {
780 forget();
781 try { render(); } catch (e) { /* no panel */ }
782 });
783 } catch (e) { /* no window */ }
784
785 // ── Surviving a passphrase change ──────────────────────────
786 //
787 // The store is sealed under the passphrase, so a change to it has to carry
788 // the store across or the whole message history is orphaned -- silently, and
789 // permanently, since there is no second copy of the read and tray flags.
790 //
791 // BOTH PHASES, and the `read` is the load-bearing one: after
792 // `changePassphrase` swaps the key there is no old key left to open the blob
793 // with, so the record must be in memory before it runs. `read()` is
794 // idempotent, so this costs nothing on the ordinary path where the panel has
795 // already read it.
796 if (window.DaimondRekey) {
797 DaimondRekey.register({
798 name: 'post',
799 /// Bring the record into memory under the OLD key.
800 read: async function () {
801 var st = await read();
802 return { held: st ? 1 : 0, failed: st ? [] : ['messages'] };
803 },
804 /// Write it back under the new one. `save()` returns early on a null
805 /// record, so a store that would not open is never overwritten blank.
806 reseal: async function () {
807 if (!_st) return { failed: [], unread: ['messages'] };
808 await save();
809 return { failed: [], unread: [] };
810 },
811 /// A change that did not happen leaves the blob under the old key, so
812 /// the in-memory copy is the thing to drop.
813 forget: forget,
814 sentence: function (kind) {
815 return kind === 'unread'
816 ? tOr('changepass.post_not_unsealed',
817 'Your private messages could not be read under your old passphrase, '
818 + 'so they have been left as they were.')
819 : tOr('changepass.post_not_resealed',
820 'Your private messages could not be re-encrypted under the new passphrase.');
821 },
822 });
823 }
824
825 // ── The parcel ─────────────────────────────────────────────
826
827 /// What travels between this account's own devices.
828 ///
829 /// SYNCHRONOUS, because sync.js collects a parcel synchronously, and `null`
830 /// while the store is unread. sync.js hangs it on only when it is not null,
831 /// the same rule the pairing look record is carried under.
832 function snapshot() {
833 if (!_st) return null;
834 return JSON.parse(JSON.stringify(_st));
835 }
836
837 /// Merge another device's record into this one. True when this device moved.
838 ///
839 /// EVERY RULE HERE IS MONOTONE, so the result is the same whichever device
840 /// runs it and whichever order the parcels arrive in, and nothing stamps on
841 /// the way in. A message is immutable -- its address is its content -- so only
842 /// the flags merge: `read` and `del` only ever go true, `tray` only ever goes
843 /// false, and the two sequences take the higher.
844 function adopt(rec) {
845 if (!rec || typeof rec !== 'object') return false; // no section on the parcel
846 if (rec.v !== REC_V) {
847 // A record from a build this one cannot read. Not a merge failure --
848 // there is nothing this version could correctly do with it -- but it is
849 // not nothing either, so it is said.
850 log('a message record at version', rec.v, 'was not merged; this build reads', REC_V);
851 return false;
852 }
853 // LOUDLY. A record arrived and there is nowhere to put it, which loses the
854 // other device's read and tray flags outright. Returning false here left
855 // sync.js's `failed` list empty, so the merge counted as complete and the
856 // next push went over the top of the parcel this device had just failed to
857 // read -- the other device's work replaced by a version that never saw it.
858 // A throw puts `post` in `failed`, which jams the sync and refuses that
859 // push (www/js/sync.js:760, :996).
860 if (!_st) {
861 throw new Error('the message store is not read on this device, so an '
862 + 'arriving message record cannot be merged into it');
863 }
864 var moved = false;
865
866 Object.keys(rec.msgs || {}).forEach(function (addr) {
867 var r = rec.msgs[addr];
868 if (!r || typeof r !== 'object') return;
869 var mine = _st.msgs[addr];
870 if (!mine) { _st.msgs[addr] = r; moved = true; return; }
871 if (r.read && !mine.read) { mine.read = 1; moved = true; }
872 if (mine.tray && !r.tray) { mine.tray = 0; moved = true; }
873 if (r.hidden && !mine.hidden) { mine.hidden = 1; moved = true; }
874 if (r.del && !mine.del) { mine.del = r.del; moved = true; }
875 });
876 Object.keys(rec.notes || {}).forEach(function (k) {
877 if (!_st.notes[k]) { _st.notes[k] = rec.notes[k]; moved = true; }
878 });
879 // GROUPS: TWO CLOCKS, EACH WITH EXACTLY ONE WRITER, which is what lets
880 // this converge with no ordering machinery and no tie-break beyond an
881 // address.
882 //
883 // - the ROSTER half (`at`, `salt`, `name`, `members`, `creator`) is
884 // written only by the group's creator, so the higher `at` is simply
885 // the later roster. Equal stamps take the higher address, because a
886 // creator sending two rosters inside one millisecond must still leave
887 // every device holding the same one;
888 // - the LOCAL half (`state`, `stateAt`) is written only by this account,
889 // so the higher `stateAt` is this account's later decision.
890 //
891 // The two are never compared against each other. A rule that took, say,
892 // the whole record on the higher `at` would let a creator's roster undo a
893 // person's own decision to leave.
894 Object.keys(rec.groups || {}).forEach(function (gid) {
895 var r = rec.groups[gid];
896 // A record whose roster is not a list is not a roster. Checked here
897 // rather than where it is drawn: `adopt` is synchronous by contract and
898 // a throw from it jams the whole sync, so a malformed section must be
899 // refused at the merge and not three frames later inside a redraw.
900 if (!r || typeof r !== 'object' || !r.gid || !Array.isArray(r.members)) return;
901 var mine = _st.groups[gid];
902 if (!mine) { _st.groups[gid] = r; moved = true; return; }
903 if (ms(r.at) > ms(mine.at)
904 || (ms(r.at) === ms(mine.at)
905 && String(r.addr || '') > String(mine.addr || ''))) {
906 mine.at = ms(r.at);
907 mine.addr = String(r.addr || '');
908 mine.salt = r.salt;
909 mine.name = r.name;
910 mine.creator = r.creator;
911 mine.members = r.members;
912 moved = true;
913 }
914 if (ms(r.stateAt) > ms(mine.stateAt)) {
915 mine.state = r.state;
916 mine.stateAt = ms(r.stateAt);
917 moved = true;
918 }
919 });
920 if ((rec.through | 0) > _st.through) { _st.through = rec.through | 0; moved = true; }
921 if ((rec.acked | 0) > _st.acked) { _st.acked = rec.acked | 0; moved = true; }
922 // AND WRITTEN DOWN. Nothing else here saves a merge: a device that adopted
923 // the other one's read marks and was then closed came back not having
924 // adopted them, and would re-ack and re-draw what the other device had
925 // already dealt with. Not awaited, because `adopt` is synchronous by
926 // contract -- sync.js collects and merges a parcel synchronously -- and
927 // `save()` serialises its own writes.
928 if (moved) { save(); }
929 return moved;
930 }
931
932 // ── People ─────────────────────────────────────────────────
933 //
934 // Sealing needs the recipient's ENCRYPTION key; the relay addresses by their
935 // SIGNING key. trust.js holds both, in a log it REPLAYS AND RE-VERIFIES on
936 // every read, and it is the only authority here. A second store of cards in
937 // this file would be a second place a key could be wrong -- and the weaker of
938 // the two, since nothing here re-checks a signature at rest.
939 //
940 // The projection is asynchronous and the panel draws synchronously, so it is
941 // cached into `_dir` by `refreshDir` and read from there. The cache decides
942 // nothing on its own: `compose` refreshes before it seals.
943
944 /// key hex -> { pub, keyHex, enc, label, state }. Refreshed, never authored.
945 var _dir = {};
946
947 /// Read the People projection into the cache. Answers how many people there are.
948 async function refreshDir() {
949 var dir = {};
950 try {
951 if (window.DaimondTrust && DaimondTrust.people) {
952 var all = await DaimondTrust.people();
953 (all || []).forEach(function (p) {
954 if (!p || !p.key || !p.enc) return;
955 dir[String(p.key).toLowerCase()] = {
956 keyHex: String(p.key).toLowerCase(),
957 pub: b64url(b64enc(unhex(p.key))),
958 enc: String(p.enc),
959 label: String(p.label || ''),
960 state: String(p.state || 'new'),
961 };
962 });
963 }
964 } catch (e) { log('people projection failed', e); }
965 _dir = dir;
966 return Object.keys(_dir).length;
967 }
968
969 /// The row held for a key, given either spelling of it.
970 function dirFor(pub) {
971 var p = String(pub || '');
972 if (_dir[p.toLowerCase()]) return _dir[p.toLowerCase()];
973 var k;
974 for (k in _dir) {
975 if (Object.prototype.hasOwnProperty.call(_dir, k) && _dir[k].pub === p) return _dir[k];
976 }
977 return null;
978 }
979
980 /// The sealing key held for somebody, as raw bytes, or null.
981 function encFor(pub) {
982 // This account's own key, which needs no card: a Sent copy and a note to
983 // self are both sealed to it, and looking it up in a directory would be
984 // asking somebody else about a key this device holds the other half of.
985 try {
986 if (window.DaimondIdentity && DaimondIdentity.publicKeyB64url() === String(pub)) {
987 return DaimondIdentity.sealingKeyRaw();
988 }
989 } catch (e) { /* no identity */ }
990 var it = dirFor(pub);
991 return it ? unhex(it.enc) : null;
992 }
993
994 /// Everybody this device could seal to. A blocked key is not among them: the
995 /// block is this account's own act and offering to write to them anyway would
996 /// be the interface arguing with the user.
997 function people() {
998 return Object.keys(_dir).map(function (k) { return _dir[k]; })
999 .filter(function (p) { return p.state !== 'blocked'; });
1000 }
1001
1002 /// The groups this device has joined, read synchronously off the record.
1003 /// Empty while the identity is locked, which is the same answer `list()` gives.
1004 function joinedGroups() {
1005 if (!_st || !_st.groups) return [];
1006 return Object.keys(_st.groups).map(function (k) { return _st.groups[k]; })
1007 .filter(function (g) { return g && g.state === 'joined'; });
1008 }
1009
1010 /// One group's record by id, or null.
1011 function groupRec(gid) {
1012 return (_st && _st.groups && _st.groups[String(gid)]) || null;
1013 }
1014
1015 // ── The wire ───────────────────────────────────────────────
1016
1017 /// This tab's wake channel, so the relay taps this device's OTHER tabs and
1018 /// not the one that is already parked.
1019 var WAKE_ID = 'p' + Math.random().toString(36).slice(2, 10);
1020
1021 /// One relay request. Through `DaimondGateway.gwFetch`, which is THE ONE COPY
1022 /// of the session rule -- renew once, retry once -- so nothing here carries a
1023 /// second version of it.
1024 async function call(method, body, query) {
1025 var opts = {
1026 method: method,
1027 credentials: 'same-origin',
1028 headers: { 'x-daimond-api': String(DaimondGateway.clientApi()) },
1029 };
1030 if (body !== undefined) {
1031 opts.headers['content-type'] = 'application/json';
1032 opts.body = JSON.stringify(body);
1033 }
1034 var r = await DaimondGateway.gwFetch(PATH + (query || ''), opts);
1035 var j = null;
1036 try { j = await r.json(); } catch (e) { j = null; }
1037 return { status: r.status, json: j };
1038 }
1039
1040 // ── The doorbell ───────────────────────────────────────────
1041 //
1042 // One email, at most once a day, saying something is waiting. No sender, no
1043 // subject, no count. It is ON BY DEFAULT for a beta account (decision 11):
1044 // those people applied by email and were invited by email, and with push
1045 // declined it is the only thing a closed tab ever hears. A default that
1046 // sends is a default that MUST be reachable, and until this pair of calls had
1047 // a caller it was not: the gateway has answered `?view=doorbell` and
1048 // `?op=doorbell` all along and nothing in the app asked either.
1049 //
1050 // THE READ CARRIES THE REACH AS WELL AS THE STATE, and a screen must draw
1051 // both. "On" and "will ring" are different answers: an account with no
1052 // address on file has the first and not the second, and a switch that showed
1053 // only the first would be lying to the one person who could fix it
1054 // (gateway/src/handlers/post.rs:625, gateway/src/doorbell.rs:155).
1055
1056 /// Whether the doorbell is on, and whether it could actually ring.
1057 ///
1058 /// Answers `{ ok, on, set, reach, why, last_ts, ... }` or `{ ok:false, why }`.
1059 /// `set` is false while nobody has chosen, so a caller can draw a default AS a
1060 /// default rather than as somebody's decision.
1061 async function doorbell() {
1062 var r;
1063 try { r = await call('GET', undefined, '?view=doorbell'); }
1064 catch (e) { return { ok: false, why: 'offline' }; }
1065 if (r.status !== 200 || !r.json || !r.json.ok) {
1066 return { ok: false, why: 'status_' + r.status };
1067 }
1068 return r.json;
1069 }
1070
1071 /// Turn it on or off. Answers the same shape the read does, because the
1072 /// gateway answers the new state rather than an acknowledgement -- so a
1073 /// caller never has to guess what it now is, and a switch cannot draw a
1074 /// state the server did not confirm.
1075 ///
1076 /// TURNING IT OFF TAKES ANY QUEUED RING WITH IT, at the gateway
1077 /// (`requeue_doorbell(.., 0)`), so a bell already armed does not ring once
1078 /// more on its way out. Nothing here needs to do anything about that; it is
1079 /// said because a caller drawing "off" is entitled to mean it.
1080 async function setDoorbell(on) {
1081 var r;
1082 try { r = await call('POST', { on: !!on }, '?op=doorbell'); }
1083 catch (e) { return { ok: false, why: 'offline' }; }
1084 if (r.status !== 200 || !r.json || !r.json.ok) {
1085 return { ok: false, why: 'status_' + r.status };
1086 }
1087 return r.json;
1088 }
1089
1090 /// Send one message. Answers `{ ok, addr }`, or `{ ok:false, why }`.
1091 ///
1092 /// A FULL BOX IS DRAWN HONESTLY. 507 means the message did not arrive, and
1093 /// saying anything else here would be telling somebody their words were
1094 /// delivered when they were not.
1095 async function send(opts) {
1096 var o = opts || {};
1097 var st = await read();
1098 if (!st) return { ok: false, why: tOr('post.err_locked',
1099 'Unlock Daimond to send a message: it is signed with your own key.') };
1100 // THE ONE THING A PERSON MAY NOT WRITE. group.js marks a roster by the
1101 // first line of the body, and a person who typed that line would have
1102 // their words applied as a membership list instead of drawn. Refused here,
1103 // at the one door a person's own text comes through, so the marker never
1104 // has to be a security boundary: the authorisation is the id derivation,
1105 // and this only keeps honest prose out of the roster path.
1106 try {
1107 if (window.DaimondGroup && DaimondGroup.looksLikeOp
1108 && DaimondGroup.looksLikeOp(o.body) && !o.group) {
1109 return { ok: false, why: tOr('post.err_reserved_line',
1110 'A message cannot begin with that line: Daimond uses it to carry a '
1111 + 'group\'s membership list. Put something before it.') };
1112 }
1113 } catch (e) { /* no group module */ }
1114 if (o.group) return await sendGroup(st, o);
1115 var enc = o.toEnc || encFor(o.to);
1116 var made;
1117 try { made = await compose({ body: o.body, to: o.to, toEnc: enc, replyTo: o.replyTo, refs: o.refs }); }
1118 catch (e) { return { ok: false, why: String(e && e.message || e) }; }
1119
1120 // THE SAME TABLE THE FAN-OUT READS, which is the whole of `whyRefused`'s
1121 // reason for existing: one recipient and forty recipients are the same POST
1122 // and must fail in the same words.
1123 var r;
1124 try { r = await call('POST', { to: String(o.to), addr: made.addr, envelope: made.envelope }); }
1125 catch (e) { return { ok: false, status: 0, why: whyRefused(0) }; }
1126
1127 if (r.status !== 200 || !r.json || !r.json.ok) {
1128 return { ok: false, status: r.status | 0, why: whyRefused(r.status) };
1129 }
1130
1131 // The sender's own copy. Kept only after the relay accepted it, so a Sent
1132 // list never shows something that did not leave.
1133 st.msgs[made.addr] = {
1134 addr: made.addr, dir: 'out', to: String(o.to), body: String(o.body),
1135 ts: made.ts, read: 1, tray: 0,
1136 };
1137 await save();
1138 render();
1139 return { ok: true, addr: made.addr };
1140 }
1141
1142 // ── The raw put, for the persistent desktop peer ───────────
1143 //
1144 // PEER STEP 2 (dev/PEER_DESIGN.md §1.4). An errand and a report ride this same
1145 // `/api/post` door a message does, but they are NOT messages: they are sealed
1146 // by DaimondPeer -- raw JSON under the account's own seal -- and handed here as
1147 // a finished `{ to, addr, envelope }` body. `send` stays the message-shaped
1148 // door (compose -> seal -> post); this is the one raw put, and it composes
1149 // nothing and stores no Sent copy, because a peer envelope is not a message and
1150 // must never reach the message list. Status is read through the SAME
1151 // `whyRefused` table `send` uses, so a full box or a refused put fails in the
1152 // same words wherever it is posted from.
1153
1154 /// Put an already-sealed `{ to, addr, envelope }` in the box. Answers
1155 /// `{ ok, status, addr, why }`. `to` is the account's OWN public address for a
1156 /// self-post, so the gateway wakes the account's OTHER devices (wake.rs
1157 /// `Sub.origin` does not wake the poster).
1158 async function post(body) {
1159 var b = body || {};
1160 if (!b.to || !b.addr || !b.envelope) {
1161 return { ok: false, status: 0, why: tOr('post.err_bad_put',
1162 'A post needs a recipient, an address and a sealed body.') };
1163 }
1164 var r;
1165 try { r = await call('POST', { to: String(b.to), addr: String(b.addr), envelope: String(b.envelope) }); }
1166 catch (e) { return { ok: false, status: 0, why: whyRefused(0) }; }
1167 if (r.status !== 200 || !r.json || !r.json.ok) {
1168 return { ok: false, status: r.status | 0, why: whyRefused(r.status) };
1169 }
1170 return { ok: true, status: 200, addr: String(b.addr) };
1171 }
1172
1173 /// Hand a roster to group.js, and say whether it moved anything.
1174 ///
1175 /// Its own function rather than four lines inside `collect`, so that a
1176 /// verifier drives the same door a collect does. A test that opened an
1177 /// envelope and then reached into group.js by hand would be measuring less
1178 /// than the run it is standing in for: it would still pass on a build where
1179 /// `collect` had stopped calling this at all.
1180 async function absorbRoster(got) {
1181 try {
1182 if (!window.DaimondGroup || !DaimondGroup.consume) return false;
1183 return await DaimondGroup.consume(got);
1184 } catch (e) { log('a roster would not apply', e); return false; }
1185 }
1186
1187 /// Seal one message to a group: ask group.js who, then compose once.
1188 ///
1189 /// The half of `sendGroup` that involves no relay, split out because it is the
1190 /// half a group's cryptography actually lives in and it must be provable
1191 /// between three devices WITH NO SERVER IN THE PATH AT ALL -- which is how the
1192 /// two-party seal was proved and is the shape a group needs. A verifier that
1193 /// reimplemented these two calls would pass on a build where `sendGroup` had
1194 /// stopped making them.
1195 ///
1196 /// Answers `{ ok, made, who }` or `{ ok:false, why, skipped }`.
1197 async function sealGroup(gid, opts) {
1198 var o = opts || {};
1199 if (!window.DaimondGroup) {
1200 return { ok: false, why: tOr('post.err_no_groups',
1201 'This build cannot send to a group.') };
1202 }
1203 var who = await DaimondGroup.sealTo(gid);
1204 if (!who.ok) return { ok: false, why: who.why, skipped: who.skipped || [] };
1205 var made;
1206 try {
1207 made = await compose({ body: o.body, replyTo: o.replyTo, refs: o.refs,
1208 group: { id: unhex(gid), enc: who.enc } });
1209 } catch (e) { return { ok: false, why: String(e && e.message || e) }; }
1210 return { ok: true, made: made, who: who };
1211 }
1212
1213 /// One message to a group: sealed once, delivered once per member.
1214 ///
1215 /// WHAT THIS DOES NOT PROMISE. The relay answers a blocked delivery exactly
1216 /// as it answers an accepted one, deliberately -- otherwise Block would be
1217 /// distinguishable from Ignore and the tray would be a presence oracle
1218 /// (gateway/src/handlers/post.rs, `deliver`). So `sent` is the number of
1219 /// members this device SENT to and never the number who received it, and the
1220 /// wording on screen has to say the first. A "delivered to 12" line would be a
1221 /// claim the transport was built not to be able to make.
1222 ///
1223 /// A FULL BOX IS STILL DRAWN. 507 from one member is that member's box, not
1224 /// the message's failure, so it is counted into `refused` and named -- and the
1225 /// rest of the group still gets it. Refusing the whole send because one person
1226 /// has not collected their mail for a month would be one absent reader
1227 /// silencing a group.
1228 async function sendGroup(st, o) {
1229 if (!window.DaimondGroup) {
1230 return { ok: false, why: tOr('post.err_no_groups',
1231 'This build cannot send to a group.') };
1232 }
1233 var sealed = await sealGroup(o.group, o);
1234 if (!sealed.ok) return sealed;
1235 var made = sealed.made, who = sealed.who;
1236
1237 var out = await fanout(made, who.to);
1238 if (!out.sent) {
1239 return { ok: false, why: tOr('post.err_group_none',
1240 'The message reached nobody in that group, so nothing was sent.'),
1241 skipped: who.skipped, refused: out.refused };
1242 }
1243 // The sender's own copy, kept only for the members the relay took it for.
1244 st.msgs[made.addr] = {
1245 addr: made.addr, dir: 'out', gid: o.group, body: String(o.body),
1246 ts: made.ts, read: 1, tray: 0, sent: out.sent,
1247 };
1248 await save();
1249 render();
1250 return { ok: true, addr: made.addr, sent: out.sent, refused: out.refused,
1251 skipped: who.skipped };
1252 }
1253
1254 /// Deliver ONE already-sealed envelope to each of a list of signing keys.
1255 ///
1256 /// A loop of ordinary deliveries, and no batched route on the gateway, for two
1257 /// reasons that both survive being argued with:
1258 ///
1259 /// - a batch endpoint would hand the gateway a single request SAYING these N
1260 /// accounts are one group. The loop leaves it to infer that from N rows
1261 /// sharing an `addr`, which it can do today -- but the inference is what a
1262 /// blinded mailbox id (§12.7) removes, and a stored assertion is not;
1263 /// - it would buy nothing in correctness. The store has no transaction across
1264 /// two boxes (`Store::deliver_post` takes one lock per box), so a batch that
1265 /// failed halfway would leave exactly the partial delivery this does, with
1266 /// less said about which half.
1267 ///
1268 /// Answers `{ sent, refused }`, where `refused` is `[{ to, why }]` and every
1269 /// entry is drawn rather than counted.
1270 async function fanout(made, tos) {
1271 var sent = 0, refused = [], i;
1272 for (i = 0; i < tos.length; i++) {
1273 var r;
1274 try {
1275 r = await call('POST', { to: String(tos[i]), addr: made.addr,
1276 envelope: made.envelope });
1277 } catch (e) {
1278 refused.push({ to: String(tos[i]), status: 0, why: whyRefused(0) });
1279 continue;
1280 }
1281 if (r.status === 200 && r.json && r.json.ok) { sent++; continue; }
1282 refused.push({ to: String(tos[i]), status: r.status | 0,
1283 why: whyRefused(r.status) });
1284 }
1285 return { sent: sent, refused: refused };
1286 }
1287
1288 /// What a delivery status means, in words a person can act on.
1289 ///
1290 /// ONE PLACE, because there were two and the second one had no words at all.
1291 /// The one-to-one send mapped 507, 404 and 413 onto four sentences inline, and
1292 /// `fanout` -- which is the same POST, once per member -- wrote `'status_507'`
1293 /// and `'offline'` instead: machine text no locale holds and nothing drew. So a
1294 /// group of ten where nine boxes were full reported "Sent to 1 people." and
1295 /// said nothing whatever about the nine. The words being in the one-to-one
1296 /// branch is WHY the fan-out invented codes, so they are moved out of it rather
1297 /// than copied.
1298 ///
1299 /// `status` is 0 where the relay could not be reached at all.
1300 ///
1301 /// TWO REGISTERS FOR THE SAME FACT, and which one a caller wants depends on
1302 /// where it is going to be read. `whole` is the sentence a one-to-one send
1303 /// shows on its own, and `clause` is what goes inside a list of members --
1304 /// "Left out: Bob (their mailbox is full)" -- pitched at `group.skip_blocked`
1305 /// rather than at `post.err_box_full`, whose full sentence is right for one
1306 /// recipient and far too long once ten are named on one line.
1307 function whyRefused(status, clause) {
1308 var s = status | 0;
1309 if (!s) {
1310 return clause
1311 ? tOr('post.refused_offline', 'the relay could not be reached')
1312 : tOr('post.err_offline',
1313 'Daimond could not reach the relay, so the message has not been sent.');
1314 }
1315 if (s === 507) {
1316 return clause
1317 ? tOr('post.refused_full', 'their mailbox is full')
1318 : tOr('post.err_box_full',
1319 'That mailbox is full, so the message did not arrive. '
1320 + 'They have to collect what is already in it before another will fit.');
1321 }
1322 if (s === 404) {
1323 return clause
1324 ? tOr('post.refused_no_account', 'no account holds their key')
1325 : tOr('post.err_no_account',
1326 'No account holds that key, so the message has not been sent.');
1327 }
1328 if (s === 413) {
1329 return clause
1330 ? tOr('post.refused_too_big', 'too large for the relay to carry')
1331 : tOr('post.err_too_big',
1332 'That message is too large for the relay to carry.');
1333 }
1334 return clause
1335 ? tOr('post.refused_other', 'the relay refused it')
1336 : tOr('post.err_refused',
1337 'The relay would not take that message, so it has not been sent.');
1338 }
1339
1340 /// Take ONE row the relay handed over: open it, and put it where it belongs.
1341 ///
1342 /// Its own function, and the only place a collected envelope becomes a record,
1343 /// so that a verifier proving what happens to a message drives the door
1344 /// `collect` drives. A test that opened an envelope and then wrote the record
1345 /// itself would still pass on a build where this had stopped being called --
1346 /// which is the shape of a check that measures less than the run it stands in
1347 /// for.
1348 ///
1349 /// Answers the three counters `collect` keeps, so that the caller adds rather
1350 /// than branches.
1351 async function takeRow(st, row) {
1352 // THE SAFETY FIELD. A row the relay wrote carries no envelope and no
1353 // signature. It is recorded, and it can never reach the message list.
1354 if (String(row.kind) !== 'post') {
1355 st.notes['n' + row.seq] = {
1356 seq: row.seq | 0, kind: String(row.kind),
1357 addr: String(row.addr || ''), ts: row.ts | 0,
1358 };
1359 return ROSTER; // a note, counted the same way
1360 }
1361 // A tombstone: the row survives so a gap is never silent, and the body is
1362 // gone. Drawn as an expiry, never as an empty message.
1363 if (row.expired) {
1364 st.notes['n' + row.seq] = {
1365 seq: row.seq | 0, kind: 'expired',
1366 addr: String(row.addr || ''), ts: row.ts | 0,
1367 };
1368 return ROSTER;
1369 }
1370 // Already held. A message is immutable -- its address is its content -- so
1371 // a second sighting of one is a re-collect and not news.
1372 if (st.msgs[String(row.addr)]) return NOTHING;
1373 // THE PERSISTENT DESKTOP PEER'S OWN ENVELOPES (dev/PEER_DESIGN.md §4.3). An
1374 // errand or a report rides this same box but is raw JSON, not a message
1375 // artefact -- `openEnvelope` below would reject it as "not a message". So it
1376 // is peeked for and routed FIRST: `DaimondPeer.peek` unseals and classifies,
1377 // `absorb` verifies the account signature and hands it to the runner. A row
1378 // that is not a peer envelope -- every ordinary message -- peeks to null and
1379 // falls straight through to the message read below, UNCHANGED. A build with
1380 // no peer module skips the block entirely.
1381 if (window.DaimondPeer && DaimondPeer.peek) {
1382 var peer = null;
1383 try { peer = await DaimondPeer.peek(row.envelope); } catch (e) { peer = null; }
1384 if (peer) {
1385 // OUR OWN dispatch: leave it on the relay for the peer to run and ack.
1386 // The sender collecting its own post must NOT advance the ack cursor past
1387 // it -- acking it here drops it from the shared relay before the peer
1388 // collects, which is the awake-sender hand-off failure. HOLD tells
1389 // collect() to keep the ack watermark just below this row.
1390 //
1391 // But VERIFY the account signature before honouring the HOLD. `peer` is
1392 // the UNVERIFIED peek object, and `dispatchedBy` rides inside it; a
1393 // forgery sealed to this account's public sealing key could set it to
1394 // our own device id purely to force a HOLD and stall the ack cursor
1395 // (an availability nuisance -- it is never run, the signature stops
1396 // that). Only a row that verifies as ours may hold; an unverified
1397 // "own dispatch" falls through to absorb, which drops it and lets the
1398 // cursor advance.
1399 if (DaimondPeer.isOwnDispatch && DaimondPeer.isOwnDispatch(peer)) {
1400 var ours = false;
1401 try { ours = await DaimondPeer.verifyEnvelope(peer); } catch (e) { ours = false; }
1402 if (ours) return HOLD;
1403 }
1404 var routed = null;
1405 try { routed = await DaimondPeer.absorb(peer, row); }
1406 catch (e) { log('a peer envelope would not apply', e); }
1407 // A non-nominee that STOOD DOWN for the account's nominated always-on
1408 // runner leaves the errand on the relay, exactly as an own-dispatch does
1409 // above: acking past it here would drop it before the nominee collects,
1410 // and a nominee that then never ran would strand the turn. HOLD keeps the
1411 // ack watermark below it, so it is re-collected -- and re-decided against
1412 // live presence -- until the nominee runs it, or its beat ages out and
1413 // this device claims. The stand-down is money-safe by the lease either way.
1414 if (routed && routed.result && routed.result.why === 'nominee') return HOLD;
1415 return NOTHING; // routed, and never a message on the list
1416 }
1417 }
1418 try {
1419 var got1 = await openEnvelope(row.envelope, row.addr);
1420 // A ROSTER IS NOT A MESSAGE, and this is the same safety
1421 // field the `kind` check above is: an artefact that says
1422 // who is in a group is machine text, so it is applied and
1423 // never stored where the list can draw it. A reader shown
1424 // JSON in a message bubble has been shown a failure as
1425 // content. `openEnvelope` has already checked that the id
1426 // recomputes from the artefact's OWN author and salt, so
1427 // nothing but the creator can reach this line.
1428 if (got1.gop) {
1429 return await absorbRoster(got1) ? ROSTER : NOTHING;
1430 }
1431 st.msgs[got1.address] = {
1432 addr: got1.address, dir: 'in',
1433 from: b64url(b64enc(unhex(got1.author))),
1434 fp: String(got1.fingerprint || ''),
1435 body: String(got1.post.body || ''),
1436 replyTo: got1.post.replyTo ? String(got1.post.replyTo) : '',
1437 refs: got1.post.refs || [],
1438 ts: ms(got1.time), seq: row.seq | 0,
1439 // THE GROUP, AND WHOSE FLAG DECIDES THE TRAY. The relay
1440 // sets `tray` per PAIR, so every message from every
1441 // member of a group somebody has just joined would
1442 // arrive as a stranger's request. The roster is the
1443 // consent -- joining a group IS accepting the people in
1444 // it -- so a message to a group this device has JOINED
1445 // goes straight to the list, and one to a group only
1446 // INVITED waits in the tray with the invitation. The
1447 // relay's flag is not being overruled about a person;
1448 // it never knew there was a group.
1449 gid: got1.gid || '',
1450 tray: got1.gid ? (groupJoined(st, got1.gid) ? 0 : 1)
1451 : (row.tray ? 1 : 0),
1452 read: 0,
1453 // THE EVIDENCE, kept because the relay will not keep it. The
1454 // ack tells the relay it may let go, and after that this
1455 // device holds the only copy of the sealed form there is. A
1456 // build that stored only the decoded words could show a
1457 // message and never report it: report.js would have the
1458 // words and nothing to prove who signed them, and an
1459 // unverifiable report is an accusation rather than evidence.
1460 //
1461 // It roughly doubles what a message costs at rest and in the
1462 // parcel -- the body cap is 8 KiB, so an envelope and an
1463 // artefact in base64 come to roughly 30 KiB a message against
1464 // the gateway's 32 MiB parcel ceiling
1465 // (gateway/src/handlers/sync.rs:71). Said out loud rather
1466 // than trimmed: a report that cannot be filed because the
1467 // evidence was cut is the worst of both.
1468 art: b64enc(got1.art),
1469 env: String(row.envelope || ''),
1470 ck: b64enc(got1.ck),
1471 };
1472 // It opened this time. The trace left by the attempt that did
1473 // not goes, or the panel says twice that one message arrived.
1474 delete st.msgs['bad:' + row.addr];
1475 return MESSAGE;
1476 } catch (e) {
1477 // KEPT, NOT DROPPED. A row that will not open is still a row the
1478 // ack would tell the relay to let go of, so it has to leave a
1479 // trace somebody can be shown rather than vanishing between two
1480 // sequence numbers.
1481 st.msgs['bad:' + row.addr] = {
1482 addr: String(row.addr), dir: 'in', bad: String(e && e.message || e),
1483 from: String(row.from_pub || ''), ts: row.ts | 0,
1484 seq: row.seq | 0, tray: row.tray ? 1 : 0, read: 0,
1485 };
1486 return UNREADABLE;
1487 }
1488 }
1489
1490 /// What one row came to. Named, because three integers in a row are three
1491 /// chances to add the wrong one.
1492 var MESSAGE = { got: 1, notes: 0, unreadable: 0 };
1493 var ROSTER = { got: 0, notes: 1, unreadable: 0 };
1494 var UNREADABLE = { got: 0, notes: 0, unreadable: 1 };
1495 var NOTHING = { got: 0, notes: 0, unreadable: 0 };
1496 // Our own un-run errand: collected but deliberately LEFT on the relay for the
1497 // peer. `hold` tells collect() to keep the ack watermark below this row's seq, so
1498 // ackThrough never drops it -- only the peer that runs it may ack it away.
1499 var HOLD = { got: 0, notes: 0, unreadable: 0, hold: true };
1500
1501 /// Collect everything above what this device has folded, and fold it.
1502 ///
1503 /// NOTHING IS ACKED HERE. The relay drops nothing on a read; it drops only on
1504 /// an ack, and the ack is `ackThrough` below, after a commit.
1505 async function collect() {
1506 var st = await read();
1507 if (!st) return { ok: false, why: 'locked' };
1508 var got = 0, notes = 0, unread = 0, more = false;
1509
1510 var holdSeq = 0; // our own un-run errand's seq; the ack watermark stays below it
1511 for (var round = 0; round < 8; round++) {
1512 var r = await call('GET', undefined, '?since=' + st.through);
1513 if (r.status !== 200 || !r.json || !r.json.ok) {
1514 return { ok: false, why: 'status_' + r.status, got: got };
1515 }
1516 var rows = r.json.rows || [];
1517 for (var i = 0; i < rows.length; i++) {
1518 var row = rows[i];
1519 var took = await takeRow(st, row);
1520 got += took.got;
1521 notes += took.notes;
1522 unread += took.unreadable;
1523 // Our own errand (takeRow -> HOLD) pins the ack watermark just below it:
1524 // every row still folds, but st.through -- what ackThrough acks through --
1525 // never passes the errand, so the relay keeps it for the peer to collect.
1526 if (took.hold && !holdSeq) holdSeq = row.seq | 0;
1527 if ((row.seq | 0) > st.through && (!holdSeq || (row.seq | 0) < holdSeq)) {
1528 st.through = row.seq | 0;
1529 }
1530 }
1531 parkAgain(); // a request that was served proves the session is back
1532 more = !!r.json.more;
1533 if (!more || holdSeq) break; // once holding, stop fetching further batches this pass
1534 }
1535 await save();
1536 render();
1537 return { ok: true, got: got, notes: notes, unreadable: unread, more: more };
1538 }
1539
1540 // ── The ordering, which is the whole safety property ───────
1541 //
1542 // COLLECTED = one device has fetched the envelope, folded it into the
1543 // account's sync parcel, and THAT PARCEL PUSH HAS COMMITTED. The device then
1544 // acks. Nothing else counts, and the ack is sent in that order and no other.
1545 //
1546 // Both halves are checked here rather than assumed:
1547 //
1548 // - the parcel that is about to be pushed is READ BACK and must actually
1549 // carry the sequence about to be acked. Without this the ack would rest on
1550 // the belief that sync.js hangs this module's record on the parcel, and a
1551 // build where that line is missing would ack messages that travel nowhere.
1552 // - the push must MOVE THE SERVER VERSION. A push that 409'd, 402'd, was
1553 // refused for size or never reached the gateway leaves the version where it
1554 // was, and none of those is a commit.
1555 //
1556 // `tries` is bumped before the parcel is read so the record is never
1557 // byte-identical to the one last pushed. sync.js returns early from a push
1558 // whose parcel has not changed, which would otherwise leave a fold that can
1559 // never be acked because the push that would prove it has nothing to send.
1560
1561 /// Whether a parcel push is available to commit through.
1562 function syncReady() {
1563 return !!(window.DaimondSync && DaimondSync.entitled && DaimondSync.entitled()
1564 && DaimondSync.parcel && DaimondSync.push && DaimondSync.version);
1565 }
1566
1567 /// Tell the relay it may let go, once the parcel carrying it has committed.
1568 ///
1569 /// Answers `{ acked, why }`. Every `why` is a refusal to ack, and every one of
1570 /// them costs a re-collect and nothing else: the relay still holds the
1571 /// envelope, and collecting it again is idempotent by address.
1572 async function ackThrough() {
1573 var st = await read();
1574 if (!st) return { acked: 0, why: 'locked' };
1575 if (st.through <= st.acked) return { acked: 0, why: 'nothing' };
1576 var want = st.through;
1577
1578 if (!syncReady()) return await soloAck(want);
1579
1580 st.tries = (st.tries | 0) + 1;
1581 await save();
1582
1583 // What a push would send, read back. `DaimondSync.parcel()` is exactly what
1584 // leaves, not an approximation of it.
1585 var parcel = null;
1586 try { parcel = await DaimondSync.parcel(); }
1587 catch (e) { return { acked: 0, why: 'no_parcel' }; }
1588 if (!parcel || !parcel.post || (parcel.post.through | 0) < want) {
1589 // The record is not on the parcel. Said out loud, because the ordinary
1590 // cause is one missing line in sync.js and the symptom -- mail that is
1591 // collected and never released -- looks like a relay fault.
1592 log('the parcel does not carry the message record; not acking');
1593 return { acked: 0, why: 'not_in_parcel' };
1594 }
1595
1596 var before = DaimondSync.version();
1597 try { await DaimondSync.push(); }
1598 catch (e) { return { acked: 0, why: 'push_failed' }; }
1599 if (DaimondSync.version() <= before) return { acked: 0, why: 'not_committed' };
1600
1601 return await tellRelay(want);
1602 }
1603
1604 /// The ack for an account with no parcel to commit to.
1605 ///
1606 /// Collection degrades honestly to this one device's own ack, and that account
1607 /// then has one copy of its mail in one place -- which is true of everything
1608 /// else it owns. It is a degrade and is reported as one by `state()`, never a
1609 /// silent equivalent of the real thing.
1610 async function soloAck(want) {
1611 await save(); // the local record IS the commit here
1612 var r = await tellRelay(want);
1613 r.solo = true;
1614 return r;
1615 }
1616
1617 /// The ack request itself, and the only place it is made.
1618 async function tellRelay(want) {
1619 var r;
1620 try { r = await call('POST', { through: want }, '?op=ack'); }
1621 catch (e) { return { acked: 0, why: 'offline' }; }
1622 if (r.status !== 200 || !r.json || !r.json.ok) {
1623 return { acked: 0, why: 'status_' + r.status };
1624 }
1625 var st = await read();
1626 if (st) { st.acked = want; await save(); }
1627 return { acked: want, dropped: (r.json.dropped | 0) };
1628 }
1629
1630 /// Collect, fold and ack, in that order. The one routine anything else calls.
1631 async function round() {
1632 var c = await collect();
1633 if (!c.ok) return c;
1634 var a = await ackThrough();
1635 return { ok: true, got: c.got, notes: c.notes, unreadable: c.unreadable,
1636 acked: a.acked | 0, why: a.why || '' };
1637 }
1638
1639 // ── The tray's buttons ─────────────────────────────────────
1640
1641 /// Accept, block or unblock somebody.
1642 ///
1643 /// IGNORE IS NOT HERE, and that is deliberate: it writes nothing and calls
1644 /// nothing. A sender who could tell an ignore from a silence has been handed a
1645 /// presence oracle. Ignoring is `hide` below, which is local and tells nobody.
1646 async function connect(peerPub, action) {
1647 if (action !== 'accept' && action !== 'block' && action !== 'unblock') {
1648 return { ok: false, why: 'unknown_action' };
1649 }
1650 var r;
1651 try { r = await call('POST', { peer: String(peerPub), action: action }, '?op=connect'); }
1652 catch (e) { return { ok: false, why: 'offline' }; }
1653 if (r.status !== 200 || !r.json || !r.json.ok) return { ok: false, why: 'status_' + r.status };
1654 if (action === 'accept') {
1655 var st = await read();
1656 if (st) {
1657 Object.keys(st.msgs).forEach(function (a) {
1658 if (st.msgs[a].from === String(peerPub)) st.msgs[a].tray = 0;
1659 });
1660 await save();
1661 render();
1662 }
1663 }
1664 return { ok: true };
1665 }
1666
1667 /// Stop drawing a tray row. Writes nothing to the relay and tells nobody --
1668 /// which is the whole of what Ignore is.
1669 async function hide(addr) {
1670 var st = await read();
1671 if (!st || !st.msgs[addr]) return false;
1672 st.msgs[addr].tray = 0;
1673 st.msgs[addr].hidden = 1;
1674 await save();
1675 render();
1676 return true;
1677 }
1678
1679 // ── Parking ────────────────────────────────────────────────
1680 //
1681 // A parked GET is answered the moment something lands, and every real park
1682 // answer carries `waited: true`. A reply WITHOUT it is a front door that
1683 // dropped the query string and served an ordinary pull -- so this stops
1684 // parking the first time it sees one, and does not start again on its own.
1685 // Without that check a stripped query turns the park into an unthrottled loop
1686 // against the server.
1687
1688 var PARK_MS = 45000; // what the gateway will hold a request for
1689 /// However fast a park answered, the next one is not immediate. The same
1690 /// floor sync.js's own poll keeps, and for the same reason: a gateway that
1691 /// answers at once -- because it has news, or because it is behaving oddly --
1692 /// must not turn this into a spin. Without it a fast answer is a loop bounded
1693 /// only by the network.
1694 var PARK_FLOOR_MS = 1000;
1695 var _parking = false; // is a park in flight or scheduled?
1696 var _parkOff = ''; // why parking stopped, or ''
1697 var _parkGen = 0; // torn down and restarted, so a stale park is ignored
1698 var _parks = 0; // parks made, for a verifier
1699
1700 /// Start parking. Idempotent, and refuses where parking has been turned off.
1701 function parkStart() {
1702 if (_parking || _parkOff) return false;
1703 _parking = true;
1704 _parkGen++;
1705 parkOnce(_parkGen);
1706 return true;
1707 }
1708
1709 /// Stop parking, with the reason. `''` for an ordinary stop.
1710 ///
1711 /// `no_park` is the one reason that STICKS. It is a property of the front door
1712 /// -- the query string is being dropped -- so nothing this client does will
1713 /// change it, and asking again is the hammering the check exists to prevent. A
1714 /// lapsed session is not like that, and `parkAgain` below lifts it.
1715 function parkStop(why) {
1716 _parking = false;
1717 _parkGen++;
1718 if (why) _parkOff = why;
1719 }
1720
1721 /// Lift a stop that a working request has disproved. Never lifts `no_park`.
1722 function parkAgain() {
1723 if (_parkOff && _parkOff !== 'no_park') _parkOff = '';
1724 }
1725
1726 async function parkOnce(gen) {
1727 while (_parking && gen === _parkGen) {
1728 var st = await read();
1729 if (!st) { parkStop(''); return; }
1730 _parks++;
1731 var began = Date.now();
1732 var r;
1733 try {
1734 r = await call('GET', undefined, '?above=' + st.through
1735 + '&ms=' + PARK_MS + '&w=' + encodeURIComponent(WAKE_ID));
1736 } catch (e) {
1737 // The network went. Not a reason to give up on the transport, so this
1738 // waits and tries again rather than turning parking off for good.
1739 await sleep(5000);
1740 continue;
1741 }
1742 if (gen !== _parkGen) return;
1743 if (r.status === 401 || r.status === 426) { parkStop('session'); return; }
1744 if (r.status !== 200 || !r.json) { await sleep(5000); continue; }
1745 // THE CHECK THIS WHOLE BLOCK EXISTS FOR.
1746 if (r.json.waited !== true) {
1747 parkStop('no_park');
1748 log('the park answered without `waited`: the query string is being dropped, '
1749 + 'so this is an ordinary pull. Parking is off.');
1750 return;
1751 }
1752 if (r.json.changed) await round();
1753 var spent = Date.now() - began;
1754 if (spent < PARK_FLOOR_MS) await sleep(PARK_FLOOR_MS - spent);
1755 }
1756 }
1757
1758 function sleep(ms) {
1759 return new Promise(function (r) { setTimeout(r, ms); });
1760 }
1761
1762 // ── The panel ──────────────────────────────────────────────
1763 //
1764 // Everything is drawn inside the one region the Social panel gives this
1765 // module, so the panel's own layout reaches none of this. Built with
1766 // `createElement` and `textContent`, never `innerHTML`: a format whose whole
1767 // claim is that a message cannot carry code must not have its own reader
1768 // building markup by string concatenation.
1769 //
1770 // References are drawn by `DaimondRefs`, which improve.js owns. The nine
1771 // refusal wordings for a reference that will not resolve exist once, there,
1772 // and a second copy of them here would be a second copy to get wrong.
1773
1774 function host() { return document.querySelector(HOST); }
1775
1776 function elt(tag, cls, text) {
1777 var e = document.createElement(tag);
1778 if (cls) e.className = cls;
1779 if (text != null) e.textContent = String(text);
1780 return e;
1781 }
1782
1783 /// The messages this account holds, newest first, tray rows excluded.
1784 function list() {
1785 if (!_st) return [];
1786 return Object.keys(_st.msgs).map(function (k) { return _st.msgs[k]; })
1787 .filter(function (m) { return !m.tray && !m.del && !m.hidden; })
1788 .sort(function (a, b) { return (b.ts | 0) - (a.ts | 0); });
1789 }
1790
1791 /// The rows waiting to be accepted, ignored or blocked.
1792 function tray() {
1793 if (!_st) return [];
1794 return Object.keys(_st.msgs).map(function (k) { return _st.msgs[k]; })
1795 .filter(function (m) { return m.tray && !m.del && !m.hidden; })
1796 .sort(function (a, b) { return (b.ts | 0) - (a.ts | 0); });
1797 }
1798
1799 /// The relay's own rows. Never a message from a person.
1800 ///
1801 /// FOLDED BY ADDRESS, which matters only for a group and costs nothing for
1802 /// anything else. One group message is one envelope delivered once per
1803 /// member, so a group of twelve that nobody collects expires twelve times and
1804 /// the relay writes the sender twelve notices -- one per box, all naming the
1805 /// same address (gateway/src/schema.rs, `Store::expire_post`). Twelve
1806 /// identical rows saying a message was never collected reads as twelve
1807 /// messages having been lost. One row, with the count on it, is what
1808 /// happened.
1809 ///
1810 /// A one-to-one message has exactly one copy, so this folds nothing and the
1811 /// count is never drawn.
1812 function notices() {
1813 if (!_st) return [];
1814 var byAddr = {}, out = [];
1815 Object.keys(_st.notes).forEach(function (k) {
1816 var n = _st.notes[k];
1817 if (!n) return;
1818 var key = n.kind === 'expired' && n.addr ? 'a:' + n.addr : 'k:' + k;
1819 var held = byAddr[key];
1820 if (!held) {
1821 byAddr[key] = { seq: n.seq | 0, kind: n.kind, addr: n.addr,
1822 ts: n.ts | 0, copies: 1 };
1823 out.push(byAddr[key]);
1824 return;
1825 }
1826 held.copies++;
1827 // The newest sighting names the fold, so a returning device sorts it
1828 // where the last copy arrived rather than where the first did.
1829 if ((n.seq | 0) > held.seq) { held.seq = n.seq | 0; held.ts = n.ts | 0; }
1830 });
1831 return out.sort(function (a, b) { return (b.seq | 0) - (a.seq | 0); });
1832 }
1833
1834 /// How many messages have not been read, for the dock's count badge.
1835 function unread() {
1836 if (!_st) return 0;
1837 var n = 0;
1838 Object.keys(_st.msgs).forEach(function (k) {
1839 var m = _st.msgs[k];
1840 if (m.dir === 'in' && !m.read && !m.del && !m.hidden) n++;
1841 });
1842 return n;
1843 }
1844
1845 /// Take the panel's own empty line down, because this view has drawn.
1846 ///
1847 /// UNLIKE People's, this line says "Messages are not switched on in this
1848 /// build" -- it is about the BUILD and not about the list being empty. So it
1849 /// goes the moment this module draws anything at all, and the empty case is
1850 /// said by `post.none` below, in this view's own words. Passing the row count
1851 /// here would leave a person with an empty list being told the feature does
1852 /// not exist.
1853 function filled(drew) {
1854 try {
1855 if (window.DaimondSocial && DaimondSocial.filled) {
1856 DaimondSocial.filled(VIEW, drew ? 1 : 0);
1857 }
1858 } catch (e) { /* the panel is not up */ }
1859 }
1860
1861 function render() {
1862 var h = host();
1863 if (!h) return;
1864 h.textContent = '';
1865
1866 if (!_st) {
1867 h.appendChild(elt('p', 'post-empty', tOr('post.locked',
1868 'Unlock Daimond to read your messages: they are kept encrypted on this device.')));
1869 filled(true); // locked is a state this view drew, not an absent feature
1870 return;
1871 }
1872
1873 // The request tray, above the list, because it is the thing waiting on a
1874 // person and the list is not.
1875 var pending = tray();
1876 if (pending.length) {
1877 var tsec = elt('section', 'post-tray');
1878 tsec.id = 'post-tray';
1879 tsec.appendChild(elt('h3', null, tOr('post.tray_head', 'Waiting for your answer')));
1880 pending.forEach(function (m) { tsec.appendChild(drawTrayRow(m)); });
1881 h.appendChild(tsec);
1882 }
1883
1884 var lsec = elt('section', 'post-list');
1885 lsec.id = 'post-list';
1886 var msgs = list();
1887 if (!msgs.length) {
1888 lsec.appendChild(elt('p', 'post-empty', tOr('post.none',
1889 'No messages yet.')));
1890 } else {
1891 msgs.forEach(function (m) { lsec.appendChild(drawRow(m)); });
1892 }
1893 h.appendChild(lsec);
1894
1895 var nots = notices();
1896 if (nots.length) {
1897 var nsec = elt('section', 'post-notices');
1898 nsec.id = 'post-notices';
1899 nots.forEach(function (n) { nsec.appendChild(drawNotice(n)); });
1900 h.appendChild(nsec);
1901 }
1902
1903 h.appendChild(drawWrite());
1904
1905 // GROUPS, inside this module's own region and drawn by group.js.
1906 //
1907 // The Social panel's views belong to improve.js, so a third view would be
1908 // an edit to a file this lane does not own; this is one container and one
1909 // call. group.js clears and fills only what is inside it, which is the
1910 // same contract improve.js gives this file for `#social-messages-list`.
1911 // It is also why nothing here has to re-register an i18n surface: a
1912 // language change redraws this, and this redraws that.
1913 var gsec = elt('div', 'post-groups');
1914 gsec.id = 'post-groups';
1915 h.appendChild(gsec);
1916 try {
1917 if (window.DaimondGroup && DaimondGroup.mount) DaimondGroup.mount(gsec);
1918 } catch (e) { log('the group section did not draw', e); }
1919
1920 filled(true);
1921 }
1922
1923 /// One message. A handle and a fingerprint and no app chrome whatever: the
1924 /// official shape is granted only by a verified signature, and this file
1925 /// draws no official shape at all.
1926 function drawRow(m) {
1927 var row = elt('article', 'post-msg');
1928 row.dataset.addr = m.addr;
1929 if (m.dir === 'out') row.classList.add('post-out');
1930 var who = elt('div', 'post-who');
1931 who.appendChild(elt('span', 'post-name', m.dir === 'out'
1932 ? tOr('post.you', 'You')
1933 : (nameFor(m.from) || tOr('post.someone', 'Someone new'))));
1934 if (m.fp) who.appendChild(elt('span', 'post-fp', m.fp));
1935 // Which group it went to, where it went to one. Beside the author and in
1936 // the quiet colour, because a message to a group is a message from a
1937 // person and the person is what the row is about.
1938 if (m.gid) {
1939 var g = groupRec(m.gid);
1940 who.appendChild(elt('span', 'post-fp',
1941 (g && g.name ? g.name : tOr('group.unnamed', 'A group'))
1942 + ' · ' + String(m.gid).slice(0, 8)));
1943 }
1944 row.appendChild(who);
1945 if (m.dir === 'in') drawKeyLine(row, m.from);
1946 if (m.bad) {
1947 // It arrived and it will not open. Said, rather than left as a gap.
1948 row.appendChild(elt('p', 'post-bad', tOr('post.unreadable',
1949 'A message arrived that this device could not open.')));
1950 row.appendChild(elt('p', 'post-bad-why', m.bad));
1951 } else {
1952 row.appendChild(elt('p', 'post-body', m.body || ''));
1953 }
1954 drawRefs(row, m.refs);
1955 drawReport(row, m);
1956 return row;
1957 }
1958
1959 /// The Report control, where there is something to report WITH.
1960 ///
1961 /// ONE ATTRIBUTE, and that is the whole of the coupling: report.js listens
1962 /// for a delegated click on `[data-report-addr]` and touches nothing in this
1963 /// panel's DOM. It also answers `canReport`, and it is asked rather than
1964 /// guessed at -- a control that exists only to produce an error explains less
1965 /// than its absence does, and a message collected by an older build has no
1966 /// artefact to prove anything with.
1967 function drawReport(row, m) {
1968 try {
1969 if (!window.DaimondReport || !DaimondReport.canReport) return;
1970 if (!DaimondReport.canReport(m)) return;
1971 var b = elt('button', 'post-btn post-report', tOr('post.report', 'Report'));
1972 b.type = 'button';
1973 b.setAttribute('data-report-addr', String(m.addr));
1974 row.appendChild(b);
1975 } catch (e) { /* no reporting in this build */ }
1976 }
1977
1978 /// Hang a message's references on a row, through the one module that owns
1979 /// them. Nothing is drawn where there are none, and never an empty container.
1980 function drawRefs(row, refs) {
1981 if (!refs || !refs.length) return 0;
1982 try {
1983 if (!window.DaimondRefs || !DaimondRefs.draw) return 0;
1984 var host = elt('div', 'post-refs');
1985 var n = DaimondRefs.draw(host, refs);
1986 if (n) row.appendChild(host);
1987 return n;
1988 } catch (e) { return 0; }
1989 }
1990
1991 /// The line under a name that says what is known about the KEY.
1992 ///
1993 /// trust.js draws it, because §12.8.5's two-axis wording lives there and a
1994 /// second rendering of a key state is the exact thing that rule forbids.
1995 /// Nothing is drawn where trust.js is absent: showing a key's standing from a
1996 /// module that does not replay the log would be a claim with nothing behind it.
1997 function drawKeyLine(row, pub) {
1998 var it = dirFor(pub);
1999 if (!it) return;
2000 try {
2001 if (window.DaimondTrust && DaimondTrust.drawKeyLine) {
2002 row.appendChild(DaimondTrust.drawKeyLine({ state: it.state }));
2003 }
2004 } catch (e) { /* trust module not up */ }
2005 }
2006
2007 /// One tray row, with the three buttons. Ignore writes nothing.
2008 function drawTrayRow(m) {
2009 var row = elt('article', 'post-req');
2010 row.dataset.addr = m.addr;
2011 row.dataset.peer = m.from || '';
2012 var who = elt('div', 'post-who');
2013 who.appendChild(elt('span', 'post-name', nameFor(m.from) || tOr('post.someone', 'Someone new')));
2014 if (m.fp) who.appendChild(elt('span', 'post-fp', m.fp));
2015 row.appendChild(who);
2016 drawKeyLine(row, m.from);
2017 row.appendChild(elt('p', 'post-body', m.bad ? '' : (m.body || '')));
2018 var acts = elt('div', 'post-acts');
2019 [['post-accept', tOr('post.accept', 'Accept')],
2020 ['post-ignore', tOr('post.ignore', 'Ignore')],
2021 ['post-block', tOr('post.block', 'Block')]].forEach(function (p) {
2022 var b = elt('button', 'post-btn', p[1]);
2023 b.type = 'button';
2024 b.dataset.act = p[0];
2025 acts.appendChild(b);
2026 });
2027 row.appendChild(acts);
2028 return row;
2029 }
2030
2031 /// A row the relay wrote. No author, no reply control, and its own section --
2032 /// never in the message stream.
2033 function drawNotice(n) {
2034 var row = elt('article', 'post-notice');
2035 var expiry = n.kind === 'expired' || n.kind === 'expiry';
2036 row.appendChild(elt('p', null, !expiry
2037 ? tOr('post.notice', 'The relay left a notice here.')
2038 : ((n.copies | 0) > 1
2039 // A group message, uncollected by several of the people it went to.
2040 // The number is the sender's own and says how many copies expired;
2041 // it is not a read receipt and cannot become one, because it is a
2042 // fact about the relay letting go and never about anybody opening
2043 // anything.
2044 ? tOr('post.expired_group',
2045 'A message you sent to a group was never collected by {n} of the '
2046 + 'people it went to, and the relay has let those copies go.',
2047 { n: n.copies })
2048 : tOr('post.expired',
2049 'A message you sent was never collected and the relay has let it go.'))));
2050 return row;
2051 }
2052
2053 /// The box, with its audience named above the button and again on it.
2054 ///
2055 /// A control labelled plain "Send" in two places that do opposite things is
2056 /// the defect the wording exists to prevent, so the button says which channel
2057 /// it is and the line above it says who can read what is typed.
2058 function drawWrite() {
2059 var box = elt('form', 'post-write');
2060 box.id = 'post-write';
2061
2062 // Who it goes to. Nobody to write to is not an error, it is a stage a new
2063 // account is in, and it says what to do next rather than disabling a
2064 // control with no explanation.
2065 var who = people();
2066 // The groups this device has JOINED. An invitation is not a destination:
2067 // offering to write to a group somebody has not answered yet would seal
2068 // their words to a roster they have not accepted.
2069 var mine = joinedGroups();
2070 if (!who.length && !mine.length) {
2071 box.appendChild(elt('p', 'post-nobody', tOr('post.nobody',
2072 'There is nobody to write to yet. Exchange codes with somebody in '
2073 + 'People, and they will be here.')));
2074 return box;
2075 }
2076 var pick = elt('select', 'post-to');
2077 pick.id = 'post-to';
2078 pick.setAttribute('aria-label', tOr('post.to_label', 'Who this goes to'));
2079 who.forEach(function (p) {
2080 var o = elt('option', null, p.label || tOr('post.someone', 'Someone new'));
2081 o.value = p.pub;
2082 if (p.pub === _to) o.selected = true;
2083 pick.appendChild(o);
2084 });
2085 // A group's option value is prefixed, because a group id and a signing key
2086 // are both thirty-two bytes and a picker that could not tell them apart
2087 // would be a picker that seals to the wrong thing on a collision of
2088 // spelling rather than of key.
2089 mine.forEach(function (g) {
2090 var o = elt('option', null, (g.name || tOr('group.unnamed', 'A group'))
2091 + ' · ' + String(g.gid).slice(0, 8)
2092 + ' (' + tOr('post.group_count', '{n} people', { n: g.members.length }) + ')');
2093 o.value = 'g:' + g.gid;
2094 if (o.value === _to) o.selected = true;
2095 pick.appendChild(o);
2096 });
2097 box.appendChild(pick);
2098
2099 // WHO CAN READ THIS, and for a group it is a different sentence with a
2100 // different set of people behind it. Drawn from what is picked, and
2101 // redrawn when the pick changes, because a line that said "only you and
2102 // the person you are writing to" over a group of twelve would be false.
2103 var aud = elt('p', 'post-audience');
2104 aud.id = 'post-audience';
2105 box.appendChild(aud);
2106 var sayAudience = function () {
2107 var v = pick.value || '';
2108 if (v.slice(0, 2) === 'g:') {
2109 var g = groupRec(v.slice(2));
2110 aud.textContent = tOr('post.audience_group',
2111 'Sealed once for each of the {n} people in this group. There is no '
2112 + 'shared key: anybody who joins later cannot read this, and anybody '
2113 + 'taken out afterwards keeps it.', { n: g ? g.members.length : 0 });
2114 } else {
2115 aud.textContent = tOr('post.audience',
2116 'Private. Only you and the person you are writing to can read this.');
2117 }
2118 };
2119 sayAudience();
2120 pick.addEventListener('change', sayAudience);
2121 var ta = elt('textarea', 'post-text');
2122 ta.id = 'post-text';
2123 ta.setAttribute('aria-label', tOr('post.box_label', 'Write a private message'));
2124 ta.placeholder = tOr('post.box_ph', 'What you want to say, and to whom.');
2125 ta.maxLength = BODY_MAX;
2126 box.appendChild(ta);
2127 var send = elt('button', 'post-btn post-send', tOr('post.send', 'Send privately'));
2128 send.type = 'submit';
2129 send.dataset.act = 'post-send';
2130 box.appendChild(send);
2131 var note = elt('p', 'post-note');
2132 note.id = 'post-note';
2133 box.appendChild(note);
2134 return box;
2135 }
2136
2137 /// Who the box is addressed to, as a base64url signing key. Remembered across
2138 /// a redraw so a collect arriving mid-sentence does not change the recipient
2139 /// under the person typing.
2140 var _to = '';
2141
2142 /// Point the box at somebody. What a People row's "Message" press would call.
2143 function to(pub) {
2144 _to = String(pub || '');
2145 var pick = document.getElementById('post-to');
2146 if (pick) pick.value = _to;
2147 return _to;
2148 }
2149
2150 /// Who the box is addressed to right now: the picker if it is up, else what
2151 /// was last chosen.
2152 function toNow() {
2153 var pick = document.getElementById('post-to');
2154 return (pick && pick.value) || _to;
2155 }
2156
2157 /// EVERYTHING A SEND DID NOT DO, in one sentence, on the screen it happened on.
2158 ///
2159 /// THE WHOLE ANSWER GOES IN, not two fields picked out of it, and that is the
2160 /// shape rather than a convenience. This was `skipWords(r.skipped)`, so
2161 /// `r.refused` -- built by `fanout`, documented AT `fanout` as "every entry is
2162 /// drawn rather than counted" -- was dropped on the floor by every caller there
2163 /// was. A group of ten where nine deliveries were refused said "Sent to 1
2164 /// people." and the sender never learnt about the other nine; a roster that
2165 /// reached one of five said five people had been told. Taking the answer rather
2166 /// than a field means the next thing added to it is reported here or nowhere,
2167 /// and nowhere is the shorter search.
2168 ///
2169 /// The principle is `skipped`'s own and is only being finished: a member left
2170 /// out of a message the sender believes went to the whole group can be put
2171 /// right in one place, and that place is the sender's own screen at the moment
2172 /// they press.
2173 ///
2174 /// TWO SENTENCES AND NOT ONE, because the two lists are fixable by different
2175 /// people. A key this device would not seal to is a refusal HERE, and the
2176 /// person reading it is the person who can lift it -- match the new key, or
2177 /// unblock. A delivery the relay would not take is a refusal ELSEWHERE, and
2178 /// what they can do about it is wait, or hand the words over another way.
2179 /// Folding both into one list is true and leaves the reader to work out which
2180 /// of those two is theirs, which is the part of a message worth paying eight
2181 /// translations for.
2182 function shortfall(r) {
2183 var mine = [], theirs = [];
2184 ((r && r.skipped) || []).forEach(function (s) {
2185 mine.push(String(s && s.label || '?') + ' (' + String(s && s.why || '') + ')');
2186 });
2187 ((r && r.refused) || []).forEach(function (x) {
2188 var to = String(x && x.to || '');
2189 var who = nameFor(to) || to.slice(0, 8);
2190 theirs.push(who + ' (' + whyRefused(x && x.status, true) + ')');
2191 });
2192 var said = '';
2193 if (mine.length) {
2194 said += ' ' + tOr('group.refused', 'Not sealed to: {who}.',
2195 { who: mine.join(', ') });
2196 }
2197 if (theirs.length) {
2198 said += ' ' + tOr('post.group_refused',
2199 'The relay would not take it for: {who}.', { who: theirs.join(', ') });
2200 }
2201 return said;
2202 }
2203
2204 /// Say something in the panel's own status line.
2205 function say(text) {
2206 var n = document.getElementById('post-note');
2207 if (n) n.textContent = String(text || '');
2208 }
2209
2210 /// The advisory label held for a key. ADVISORY: equality is always the full
2211 /// key, and a label is a thing its holder chose. On a key nobody has matched
2212 /// it is drawn as the claim it is -- trust.js's own wording, through
2213 /// `drawKeyLine`, so there is one place that says what a key state means.
2214 function nameFor(pub) {
2215 if (!pub) return '';
2216 var it = dirFor(pub);
2217 return (it && it.label) || '';
2218 }
2219
2220 // ── Wiring ─────────────────────────────────────────────────
2221
2222 /// The panel was opened. Read the store, draw it, and go and look: there is no
2223 /// change feed on the relay's ordinary path and looking IS how somebody finds
2224 /// out.
2225 function onOpen() {
2226 return read().then(function () {
2227 return refreshDir();
2228 }).then(function () {
2229 render();
2230 parkStart();
2231 return round();
2232 }).then(render, function (e) { log('open failed', e); render(); });
2233 }
2234
2235 document.addEventListener('click', function (e) {
2236 var h = e.target && e.target.closest ? e.target.closest(HOST) : null;
2237 if (!h) return;
2238 var b = e.target.closest('[data-act]');
2239 if (!b) return;
2240 var act = b.dataset.act;
2241 var row = b.closest('.post-req');
2242 if (act === 'post-send') {
2243 e.preventDefault();
2244 var ta = document.getElementById('post-text');
2245 var whom = toNow();
2246 if (!whom) { say(tOr('post.err_no_to', 'Choose who this is going to first.')); return; }
2247 _to = whom;
2248 say(tOr('post.sending', 'Sending…'));
2249 var isGroup = whom.slice(0, 2) === 'g:';
2250 var args = isGroup
2251 ? { body: ta ? ta.value : '', group: whom.slice(2) }
2252 : { body: ta ? ta.value : '', to: whom };
2253 send(args).then(function (r) {
2254 if (!r.ok) { say(r.why + shortfall(r)); return; }
2255 if (ta) ta.value = '';
2256 // SENT TO, never DELIVERED TO. The relay answers a blocked
2257 // delivery exactly as it answers an accepted one, so the number
2258 // this device holds is the number it wrote to and nothing more.
2259 //
2260 // AND THE SHORTFALL BESIDE IT. "Sent to 1 people." is a true
2261 // sentence about a group of ten and a false impression of one, so
2262 // the nine the relay would not take are named next to it.
2263 say(isGroup
2264 ? tOr('post.sent_group', 'Sent to {n} people.', { n: r.sent | 0 })
2265 + shortfall(r)
2266 : tOr('post.sent', 'Sent.'));
2267 });
2268 return;
2269 }
2270 if (!row) return;
2271 var peer = row.dataset.peer;
2272 if (act === 'post-accept') { e.preventDefault(); connect(peer, 'accept'); return; }
2273 if (act === 'post-block') { e.preventDefault(); connect(peer, 'block'); return; }
2274 if (act === 'post-ignore') { e.preventDefault(); hide(row.dataset.addr); return; }
2275 });
2276
2277 // Another tab wrote, or an account switch emptied the store.
2278 window.addEventListener('storage', function (e) {
2279 if (!e.key || e.key.indexOf(LS) === -1) return;
2280 _st = null;
2281 read().then(render, function () { render(); });
2282 });
2283
2284 // Say the panel's own words again in a new language. Every string on a row is
2285 // built here rather than marked up, so a language change reaches none of them
2286 // unless this surface is registered.
2287 try {
2288 DaimondI18n.surface(function () { return document.querySelector(HOST); },
2289 function () { render(); });
2290 } catch (e) { /* no i18n in this build */ }
2291
2292 /// Take the Messages view of the Social panel and keep in step with it.
2293 ///
2294 /// Read LAZILY, on the open, because that is when somebody is looking: a
2295 /// collect is a request and a park holds one open for the best part of a
2296 /// minute, and neither has any business happening on a boot nobody asked it
2297 /// of. The same arrangement trust.js uses for People.
2298 function attachPanel() {
2299 if (!host()) return false;
2300 try {
2301 if (window.DaimondSocial && DaimondSocial.watch) {
2302 DaimondSocial.watch(function (view) { if (view === VIEW) onOpen(); });
2303 }
2304 } catch (e) { /* no panel to watch */ }
2305 // Drawn once at rest, so a person switching to Messages sees the store
2306 // rather than a blank while the first collect is in flight.
2307 read().then(function () { return refreshDir(); }).then(render, function () { render(); });
2308 return true;
2309 }
2310
2311 function start() {
2312 if (!attachPanel()) {
2313 // The panel is built by another module; if this ran first, wait for the
2314 // document rather than deciding there is no panel.
2315 document.addEventListener('DOMContentLoaded', attachPanel);
2316 }
2317 }
2318 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
2319 else start();
2320
2321 // ── Public surface ─────────────────────────────────────────
2322 window.DaimondPost = {
2323 /// The panel.
2324 onOpen: onOpen,
2325 render: render,
2326 /// Whether this build can compose at all, and why not. A caller drawing a
2327 /// disabled control needs the sentence, not the boolean.
2328 ready: cryptoReady,
2329 why: cryptoWhy,
2330 /// The seal, and the two identities it runs between. No server is involved
2331 /// in either, which is also how they are tested.
2332 seal: seal,
2333 unseal: unseal,
2334 compose: compose,
2335 open: openEnvelope,
2336 /// The five verbs.
2337 send: send,
2338 /// The raw put: an already-sealed `{ to, addr, envelope }` in the box, for
2339 /// the peer's errand and report. Not a message; composes and stores nothing.
2340 post: post,
2341 collect: collect,
2342 ack: ackThrough,
2343 round: round,
2344 connect: connect,
2345 /// The doorbell: whether one email a day may say something is waiting.
2346 /// The read carries the REACH as well as the switch -- see above.
2347 doorbell: doorbell,
2348 setDoorbell: setDoorbell,
2349 /// Parking, and whether it is still on. `off` names the reason it stopped;
2350 /// `no_park` means the front door dropped the query string.
2351 parkStart: parkStart,
2352 parkStop: function () { parkStop(''); },
2353 parking: function () { return { on: _parking, off: _parkOff, parks: _parks }; },
2354 /// The parcel's two halves, for sync.js. `snapshot` answers null while the
2355 /// identity is locked, and the caller must leave the section OFF when it
2356 /// does -- an empty record reads to the other device as a deletion.
2357 snapshot: snapshot,
2358 adopt: adopt,
2359 /// Read the store out from under the passphrase. Idempotent, and answers
2360 /// null while the identity is locked. Fired for you at `daimond:unlock`;
2361 /// published so a caller that needs the record NOW -- the badge, a
2362 /// verifier -- can ask rather than wait for somebody to open the panel.
2363 read: read,
2364 wake: wake,
2365 /// People, so a message can be sealed to somebody. trust.js's projection is
2366 /// the only authority; this reads it and holds nothing of its own.
2367 refreshPeople: refreshDir,
2368 people: people,
2369 /// The groups half of the record, for group.js, which holds no storage of
2370 /// its own. `groups` answers a COPY and null while the identity is locked.
2371 groups: groups,
2372 putGroup: putGroup,
2373 untrayGroup: untrayGroup,
2374 joined: joinedGroups,
2375 /// One already-sealed envelope, delivered once per member. Published so
2376 /// group.js sends a roster through the same door a message takes.
2377 fanout: fanout,
2378 /// Everything a send did not do, in one sentence. Published because
2379 /// group.js draws the answer to a fan-out of its own -- a roster -- and a
2380 /// second wording for "these people have not got it" is a second wording
2381 /// to forget to draw. Takes the WHOLE answer, never a field of it.
2382 shortfall: shortfall,
2383 /// What a delivery status means, in words. One table, read by the
2384 /// one-to-one send and by the fan-out.
2385 whyRefused: whyRefused,
2386 /// The roster branch of `collect`, published so a verifier drives the
2387 /// door a collect drives rather than a second one of its own.
2388 absorbRoster: absorbRoster,
2389 /// ONE ROW, taken exactly as `collect` takes it: opened, applied if it is
2390 /// a roster, recorded if it is a message, and kept as a trace if it will
2391 /// not open. Published so that a suite carrying bytes between devices with
2392 /// no relay in the path drives the SAME function a real collect does.
2393 take: async function (row) {
2394 var st = await read();
2395 if (!st) return { got: 0, notes: 0, unreadable: 0, why: 'locked' };
2396 var r = await takeRow(st, row);
2397 await save();
2398 render();
2399 return r;
2400 },
2401 /// The half of a group send that involves no relay. Published for the
2402 /// same reason `seal` and `unseal` are: it is where the cryptography is,
2403 /// and it must be provable between devices with no server in the path.
2404 sealGroup: sealGroup,
2405 /// The format's own reader, so group.js reads back a roster it has just
2406 /// composed through the SAME code an arriving one takes. A second reader
2407 /// would be a second place for a roster to mean something different.
2408 bridgeRead: function (bytes) {
2409 var b = bridge();
2410 if (!b || typeof b.read !== 'function') throw new Error(cryptoWhy());
2411 return b.read(bytes);
2412 },
2413 /// Point the box at somebody, and read who it is pointed at.
2414 to: to,
2415 toNow: toNow,
2416 /// What is held, for a panel and for a verifier.
2417 list: list,
2418 tray: tray,
2419 notices: notices,
2420 unread: unread,
2421 hide: hide,
2422 /// Everything this module would say if asked.
2423 state: function () {
2424 return {
2425 read: !!_st,
2426 through: _st ? _st.through : 0,
2427 acked: _st ? _st.acked : 0,
2428 solo: !syncReady(),
2429 park: { on: _parking, off: _parkOff, parks: _parks },
2430 unread: unread(),
2431 };
2432 },
2433 /// Drop what is in memory, for an account switch, a lock, or a verifier
2434 /// that wants the store read again from disk.
2435 forget: forget,
2436 };
2437})();