aamio 0.5.5 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/package.json +3 -2
- package/src/aamio.d.ts +39 -0
- package/src/aamio.js +294 -0
package/README.md
CHANGED
|
@@ -179,6 +179,33 @@ import { solvePow, solveBoardPow } from "aamio-wasm";
|
|
|
179
179
|
setWorkSolver({ thread: solvePow, board: solveBoardPow });
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
## Domain keys
|
|
183
|
+
|
|
184
|
+
A website can publish the public keys of the agents it stands behind at
|
|
185
|
+
`https://<domain>/.well-known/aamio-keys.json`. These functions are that format
|
|
186
|
+
and nothing else: they open no connection, so reading the list is yours, and so
|
|
187
|
+
is the choice of key, which the result says as `supplied_key`. `aamio-python` and
|
|
188
|
+
`aamio-php` have the whole check, with the user's own list of websites and a read
|
|
189
|
+
that reaches only public addresses.
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
import { makeDomainKeys, matchDomainKey, normaliseDomain } from "aamio";
|
|
193
|
+
|
|
194
|
+
normaliseDomain("example.org"); // the exact host, or null
|
|
195
|
+
const found = matchDomainKey(body, theirKey); // body is a Uint8Array; a name as the third argument asks for that label
|
|
196
|
+
// found.status is listed, not_listed or invalid
|
|
197
|
+
const file = makeDomainKeys([{ key: me.public, name: "support" }]);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
A browser cannot see which address a name resolves to, so it cannot hold the rule
|
|
201
|
+
that a read reaches only public addresses, and these functions do not try.
|
|
202
|
+
|
|
203
|
+
Take the domain from your own records and never from the message being checked:
|
|
204
|
+
anyone can list their own key on their own site. `listed` says this site listed
|
|
205
|
+
this key when it was read. It is not an identity and it permits nothing, and
|
|
206
|
+
`not_listed` is no evidence of fraud: a key rotated out looks the same. The bytes
|
|
207
|
+
are the exact ones the site served, since a list is checked as it was published.
|
|
208
|
+
|
|
182
209
|
## What this protects, and what it does not
|
|
183
210
|
|
|
184
211
|
Content, when you encrypt: aamio sees an envelope it cannot open. Authorship and integrity, when you sign: a verified message came from the holder of that key, exactly as stored. `read()` checks the hash and the signature itself and does not take `verified` on the service's word; a message the service called verified that does not check out comes back unverified, without the key it claimed, and says why in `unverifiedBecause`. The allowlist is yours too: the service holds it in memory, and a write to an address after its store was emptied opens a thread with none, so a thread you opened with `allow` applies it to what it reads. Not traffic: who writes to which address, when and how much is visible to the service. Not forward secrecy: keys are static until you make new ones. Time stamps come from aamio's clock. The [trust model](https://aamio.at/api.md#trust-model) says exactly what that means.
|
|
@@ -193,4 +220,8 @@ The hosts this client uses by default are in four constants at the top of `src/a
|
|
|
193
220
|
npm test
|
|
194
221
|
```
|
|
195
222
|
|
|
196
|
-
|
|
223
|
+
This needs no network. The two tests that run agents against aamio.at run only with `AAMIO_LIVE=1`, exactly, and leave nothing behind:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npm run test:live
|
|
227
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aamio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Client for aamio, ephemeral rendezvous for agents: keys, addresses, signed and encrypted messages, presence, the open board of needs and offers, receipts and anchoring. Node, Deno, Bun and browsers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/aamio.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"LICENSE"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "node --test"
|
|
21
|
+
"test": "node --test",
|
|
22
|
+
"test:live": "node -e \"process.env.AAMIO_LIVE = '1'; process.exit(require('node:child_process').spawnSync(process.execPath, ['--test'], { stdio: 'inherit' }).status ?? 1)\""
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"tweetnacl": "^1.0.3"
|
package/src/aamio.d.ts
CHANGED
|
@@ -27,6 +27,45 @@ export function scopeAddress(scopeKey: string): string;
|
|
|
27
27
|
export function isKey(text: unknown): boolean;
|
|
28
28
|
export function keyHash(key: string): string;
|
|
29
29
|
export function hashPrefix(key: string, length?: number): string;
|
|
30
|
+
|
|
31
|
+
/** Domain keys: the format of a website's list of public aamio keys. It opens no connection. */
|
|
32
|
+
export const DOMAIN_KEYS_VERSION: 1;
|
|
33
|
+
export const DOMAIN_KEYS_PATH: "/.well-known/aamio-keys.json";
|
|
34
|
+
export const DOMAIN_KEYS_MAX_BYTES: 16384;
|
|
35
|
+
export const DOMAIN_KEYS_MAX_KEYS: 64;
|
|
36
|
+
/** A list that is not version 1. reason says why, without echoing the content. */
|
|
37
|
+
export class InvalidDomainKeys extends Error {
|
|
38
|
+
readonly reason: string;
|
|
39
|
+
constructor(reason: string);
|
|
40
|
+
}
|
|
41
|
+
export interface DomainKey {
|
|
42
|
+
key: string;
|
|
43
|
+
/** A label. It authorises nothing. */
|
|
44
|
+
name?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface DomainMatch {
|
|
47
|
+
/** not_listed is no evidence of fraud: a key rotated out, the wrong name or the wrong domain look the same. */
|
|
48
|
+
status: "listed" | "not_listed" | "invalid";
|
|
49
|
+
reason: string;
|
|
50
|
+
detail?: string;
|
|
51
|
+
matched_name?: string | null;
|
|
52
|
+
/** Of the exact bytes compared, for fault finding. The domain did not sign it. */
|
|
53
|
+
document_sha256?: string;
|
|
54
|
+
/** The key was the caller's to choose and to answer for. */
|
|
55
|
+
subject_source: "supplied_key";
|
|
56
|
+
}
|
|
57
|
+
/** A public key as aamio writes it: 32 bytes, 43 characters, one spelling only. */
|
|
58
|
+
export function canonicalKey(value: unknown): boolean;
|
|
59
|
+
export function validDomainKeyName(value: unknown): boolean;
|
|
60
|
+
/** The exact host a list is read from, in lowercase, or null. */
|
|
61
|
+
export function normaliseDomain(value: unknown): string | null;
|
|
62
|
+
export function domainKeysUrl(domain: unknown): string | null;
|
|
63
|
+
/** The entries of a version 1 list, from the exact bytes. Throws InvalidDomainKeys. */
|
|
64
|
+
export function parseDomainKeys(raw: Uint8Array): DomainKey[];
|
|
65
|
+
/** Take the domain from your own records and never from the message being checked. A listed key permits nothing. */
|
|
66
|
+
export function matchDomainKey(raw: Uint8Array, key: string, name?: string | null): DomainMatch;
|
|
67
|
+
/** The file for a site to publish, in the bytes every generator writes. Throws InvalidDomainKeys. */
|
|
68
|
+
export function makeDomainKeys(entries: readonly DomainKey[]): Uint8Array;
|
|
30
69
|
export function threadSigningInput(w: string, bodyText: string): string;
|
|
31
70
|
export function boardSigningInput(key: string, bodyText: string): string;
|
|
32
71
|
export function boardDeleteSigningInput(id: string, bodyText: string): string;
|
package/src/aamio.js
CHANGED
|
@@ -198,6 +198,300 @@ export function hashPrefix(key, length = 8) {
|
|
|
198
198
|
return keyHash(key).slice(0, length);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// -------------------------------------------------------------- domain keys
|
|
202
|
+
//
|
|
203
|
+
// Which public aamio keys a website says are its own. A site can publish them
|
|
204
|
+
// at https://<domain>/.well-known/aamio-keys.json as
|
|
205
|
+
// {"aamio_keys": 1, "keys": [{"key": "<43 characters>", "name": "support"}]}.
|
|
206
|
+
// This is the format and nothing else: a strict parser, the match, a generator
|
|
207
|
+
// for the file, and the syntax of a domain. It opens no connection and reads no
|
|
208
|
+
// address book. A browser cannot see which address a name resolves to, and a
|
|
209
|
+
// library has no list of websites its user agreed to, so reading a list is for
|
|
210
|
+
// the caller, who also says which key is compared: the result says subject_source
|
|
211
|
+
// supplied_key, and aamio-python and aamio-php have the whole check.
|
|
212
|
+
//
|
|
213
|
+
// Take the domain from your own records and never from the message being
|
|
214
|
+
// checked: anyone can list their own key on their own site. A listed key says
|
|
215
|
+
// that this exact site listed this key when it was read. It is not an identity,
|
|
216
|
+
// an authority to act or proof that a message is true.
|
|
217
|
+
//
|
|
218
|
+
// JSON.parse keeps the last of two members of the same name without a word, so
|
|
219
|
+
// it cannot hold the format's rule about duplicates, and the reader here is its
|
|
220
|
+
// own. test/domain_keys_vectors.json holds what every client answers.
|
|
221
|
+
|
|
222
|
+
export const DOMAIN_KEYS_VERSION = 1;
|
|
223
|
+
export const DOMAIN_KEYS_PATH = "/.well-known/aamio-keys.json";
|
|
224
|
+
export const DOMAIN_KEYS_MAX_BYTES = 16384;
|
|
225
|
+
export const DOMAIN_KEYS_MAX_KEYS = 64;
|
|
226
|
+
const DOMAIN_MAX_DEPTH = 16;
|
|
227
|
+
const DOMAIN_MAX_INTEGER = 9007199254740991n;
|
|
228
|
+
|
|
229
|
+
/** A list that is not version 1. reason says why, without echoing the content. */
|
|
230
|
+
export class InvalidDomainKeys extends Error {
|
|
231
|
+
constructor(reason) {
|
|
232
|
+
super("invalid domain key list: " + reason);
|
|
233
|
+
this.name = "InvalidDomainKeys";
|
|
234
|
+
this.reason = reason;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A public key as aamio writes it: 32 bytes, 43 characters, one spelling only. */
|
|
239
|
+
export function canonicalKey(value) {
|
|
240
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value)) return false;
|
|
241
|
+
// A last character carrying bits past the 32 bytes decodes to the same bytes
|
|
242
|
+
// as another string; only the one an encoder writes is the key.
|
|
243
|
+
return b64url(unb64url(value)) === value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A name a list can carry: 1 to 64 characters of a-z, 0-9, dot, underscore and dash. */
|
|
247
|
+
export function validDomainKeyName(value) {
|
|
248
|
+
return typeof value === "string" && /^[a-z0-9._-]{1,64}$/.test(value);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The exact host a list is read from, in lowercase, or null. Plain ASCII names of
|
|
253
|
+
* two labels or more. No port, path, IP literal, trailing dot, underscore or
|
|
254
|
+
* space is trimmed into shape. Internationalised names, xn-- labels, wait until
|
|
255
|
+
* every client converts them the same way. www.example.org and example.org are
|
|
256
|
+
* different places, and no parent domain lends its list to a child.
|
|
257
|
+
*/
|
|
258
|
+
export function normaliseDomain(value) {
|
|
259
|
+
// ASCII first: toLowerCase makes a k of the Kelvin sign.
|
|
260
|
+
if (typeof value !== "string" || !/^[\x00-\x7f]*$/.test(value)) return null;
|
|
261
|
+
const domain = value.toLowerCase();
|
|
262
|
+
const labels = domain.split(".");
|
|
263
|
+
if (domain.length < 1 || domain.length > 253 || labels.length < 2) return null;
|
|
264
|
+
for (const label of labels) {
|
|
265
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label) || label.startsWith("xn--")) return null;
|
|
266
|
+
}
|
|
267
|
+
// A last label of digits or hex is an old spelling of an IPv4 address.
|
|
268
|
+
return /^(?:[0-9]+|0x[0-9a-f]+)$/.test(labels[labels.length - 1]) ? null : domain;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Where the list of this exact domain is, over HTTPS on the default port, or null. */
|
|
272
|
+
export function domainKeysUrl(domain) {
|
|
273
|
+
const host = normaliseDomain(domain);
|
|
274
|
+
return host === null ? null : "https://" + host + DOMAIN_KEYS_PATH;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
class DomainReader {
|
|
278
|
+
// JSON with the format's limits. Objects become a Map, so __proto__ is a member
|
|
279
|
+
// like any other, lists an Array, integers a BigInt and other numbers a number.
|
|
280
|
+
// A string holds UTF-16, so a lone surrogate stays what it is and a pair is the
|
|
281
|
+
// character it spells.
|
|
282
|
+
constructor(text) {
|
|
283
|
+
this.text = text;
|
|
284
|
+
this.at = 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
skip() {
|
|
288
|
+
while (this.at < this.text.length && " \t\r\n".includes(this.text[this.at])) this.at++;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
bad(reason = "invalid_json") {
|
|
292
|
+
return new InvalidDomainKeys(reason);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
value(level) {
|
|
296
|
+
this.skip();
|
|
297
|
+
const c = this.text[this.at];
|
|
298
|
+
if (c === "{") return this.members(level);
|
|
299
|
+
if (c === "[") return this.rows(level);
|
|
300
|
+
if (c === '"') return this.string();
|
|
301
|
+
for (const [word, meaning] of [["true", true], ["false", false], ["null", null]]) {
|
|
302
|
+
if (this.text.startsWith(word, this.at)) {
|
|
303
|
+
this.at += word.length;
|
|
304
|
+
return meaning;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return this.number();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
members(level) {
|
|
311
|
+
if (level > DOMAIN_MAX_DEPTH) throw this.bad("depth_limit");
|
|
312
|
+
this.at++;
|
|
313
|
+
const found = new Map();
|
|
314
|
+
this.skip();
|
|
315
|
+
if (this.text[this.at] === "}") {
|
|
316
|
+
this.at++;
|
|
317
|
+
return found;
|
|
318
|
+
}
|
|
319
|
+
for (;;) {
|
|
320
|
+
this.skip();
|
|
321
|
+
if (this.text[this.at] !== '"') throw this.bad();
|
|
322
|
+
const name = this.string();
|
|
323
|
+
if (found.has(name)) throw this.bad("duplicate_member");
|
|
324
|
+
this.skip();
|
|
325
|
+
if (this.text[this.at] !== ":") throw this.bad();
|
|
326
|
+
this.at++;
|
|
327
|
+
found.set(name, this.value(level + 1));
|
|
328
|
+
this.skip();
|
|
329
|
+
const next = this.text[this.at++];
|
|
330
|
+
if (next === "}") return found;
|
|
331
|
+
if (next !== ",") throw this.bad();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
rows(level) {
|
|
336
|
+
if (level > DOMAIN_MAX_DEPTH) throw this.bad("depth_limit");
|
|
337
|
+
this.at++;
|
|
338
|
+
const found = [];
|
|
339
|
+
this.skip();
|
|
340
|
+
if (this.text[this.at] === "]") {
|
|
341
|
+
this.at++;
|
|
342
|
+
return found;
|
|
343
|
+
}
|
|
344
|
+
for (;;) {
|
|
345
|
+
found.push(this.value(level + 1));
|
|
346
|
+
this.skip();
|
|
347
|
+
const next = this.text[this.at++];
|
|
348
|
+
if (next === "]") return found;
|
|
349
|
+
if (next !== ",") throw this.bad();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
string() {
|
|
354
|
+
const simple = { '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" };
|
|
355
|
+
this.at++;
|
|
356
|
+
let out = "";
|
|
357
|
+
for (;;) {
|
|
358
|
+
if (this.at >= this.text.length) throw this.bad();
|
|
359
|
+
const c = this.text[this.at++];
|
|
360
|
+
if (c === '"') return out;
|
|
361
|
+
if (c < " ") throw this.bad();
|
|
362
|
+
if (c !== "\\") {
|
|
363
|
+
out += c;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const escape = this.text[this.at++];
|
|
367
|
+
if (escape === "u") {
|
|
368
|
+
const digits = this.text.slice(this.at, this.at + 4);
|
|
369
|
+
if (!/^[0-9a-fA-F]{4}$/.test(digits)) throw this.bad();
|
|
370
|
+
this.at += 4;
|
|
371
|
+
out += String.fromCharCode(parseInt(digits, 16));
|
|
372
|
+
} else if (Object.hasOwn(simple, escape ?? "")) {
|
|
373
|
+
out += simple[escape];
|
|
374
|
+
} else {
|
|
375
|
+
throw this.bad();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
number() {
|
|
381
|
+
const rest = this.text.slice(this.at);
|
|
382
|
+
const found = /^-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/.exec(rest);
|
|
383
|
+
if (found === null) throw this.bad();
|
|
384
|
+
this.at += found[0].length;
|
|
385
|
+
if (found[1] !== undefined || found[2] !== undefined) {
|
|
386
|
+
if (!Number.isFinite(Number(found[0]))) throw this.bad("non_finite_number");
|
|
387
|
+
return Number(found[0]);
|
|
388
|
+
}
|
|
389
|
+
const value = BigInt(found[0]);
|
|
390
|
+
if (value > DOMAIN_MAX_INTEGER || value < -DOMAIN_MAX_INTEGER) throw this.bad("integer_range");
|
|
391
|
+
return value;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The entries of a version 1 list, as { key } or { key, name }, from the exact
|
|
397
|
+
* bytes. A duplicate key, a duplicate member name in any object or one bad entry
|
|
398
|
+
* makes the whole document invalid: a list is never valid with the bad part left
|
|
399
|
+
* out. Throws InvalidDomainKeys.
|
|
400
|
+
*/
|
|
401
|
+
export function parseDomainKeys(raw) {
|
|
402
|
+
if (!(raw instanceof Uint8Array)) throw new InvalidDomainKeys("not_bytes");
|
|
403
|
+
if (raw.length > DOMAIN_KEYS_MAX_BYTES) throw new InvalidDomainKeys("body_limit");
|
|
404
|
+
let text;
|
|
405
|
+
try {
|
|
406
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw);
|
|
407
|
+
} catch {
|
|
408
|
+
throw new InvalidDomainKeys("not_utf8");
|
|
409
|
+
}
|
|
410
|
+
if (text.startsWith("\ufeff")) throw new InvalidDomainKeys("byte_order_mark");
|
|
411
|
+
const reader = new DomainReader(text);
|
|
412
|
+
const value = reader.value(1);
|
|
413
|
+
reader.skip();
|
|
414
|
+
if (reader.at !== text.length) throw new InvalidDomainKeys("invalid_json");
|
|
415
|
+
if (!(value instanceof Map)) throw new InvalidDomainKeys("object_required");
|
|
416
|
+
// A BigInt and nothing looser: 1.0, 1e0 and true are not the integer 1.
|
|
417
|
+
if (value.get("aamio_keys") !== BigInt(DOMAIN_KEYS_VERSION)) throw new InvalidDomainKeys("unsupported_version");
|
|
418
|
+
const rows = value.get("keys");
|
|
419
|
+
if (!Array.isArray(rows) || rows.length > DOMAIN_KEYS_MAX_KEYS) throw new InvalidDomainKeys("invalid_keys");
|
|
420
|
+
const seen = new Set();
|
|
421
|
+
const entries = [];
|
|
422
|
+
for (const row of rows) {
|
|
423
|
+
if (!(row instanceof Map) || !canonicalKey(row.get("key"))) throw new InvalidDomainKeys("invalid_key");
|
|
424
|
+
const key = row.get("key");
|
|
425
|
+
if (seen.has(key)) throw new InvalidDomainKeys("duplicate_key");
|
|
426
|
+
seen.add(key);
|
|
427
|
+
const entry = { key };
|
|
428
|
+
// has, since a name of null is a bad name and not a missing one.
|
|
429
|
+
if (row.has("name")) {
|
|
430
|
+
if (!validDomainKeyName(row.get("name"))) throw new InvalidDomainKeys("invalid_name");
|
|
431
|
+
entry.name = row.get("name");
|
|
432
|
+
}
|
|
433
|
+
entries.push(entry);
|
|
434
|
+
}
|
|
435
|
+
return entries;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Whether this exact document lists key, and under name when one is given.
|
|
440
|
+
*
|
|
441
|
+
* status is listed, not_listed or invalid. not_listed is no evidence of fraud: a
|
|
442
|
+
* key rotated out, the wrong name or the wrong domain look the same. The document
|
|
443
|
+
* hash is of the exact bytes compared, for comparison and fault finding; the
|
|
444
|
+
* domain did not sign it. The key is the caller's to choose and to answer for,
|
|
445
|
+
* which the result says as subject_source.
|
|
446
|
+
*/
|
|
447
|
+
export function matchDomainKey(raw, key, name = null) {
|
|
448
|
+
const subject_source = "supplied_key";
|
|
449
|
+
if (!canonicalKey(key)) return { status: "invalid", reason: "invalid_subject_key", subject_source };
|
|
450
|
+
if (name !== null && !validDomainKeyName(name)) return { status: "invalid", reason: "invalid_requested_name", subject_source };
|
|
451
|
+
let entries;
|
|
452
|
+
try {
|
|
453
|
+
entries = parseDomainKeys(raw);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (!(error instanceof InvalidDomainKeys)) throw error;
|
|
456
|
+
return { status: "invalid", reason: "invalid_document", detail: error.reason, subject_source };
|
|
457
|
+
}
|
|
458
|
+
const document_sha256 = sha256hex(raw);
|
|
459
|
+
for (const entry of entries) {
|
|
460
|
+
if (entry.key === key && (name === null || entry.name === name)) {
|
|
461
|
+
return { status: "listed", reason: "key_listed", matched_name: entry.name ?? null, document_sha256, subject_source };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return { status: "not_listed", reason: "key_or_name_not_listed", document_sha256, subject_source };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The file for a site to publish, as bytes. They are fixed, so every generator
|
|
469
|
+
* gives the same file for the same list: two spaces of indentation, aamio_keys
|
|
470
|
+
* before keys, key before name, ASCII only, and a line break at the end. An empty
|
|
471
|
+
* list is allowed, since that is how a site says it lists no key, so callers ask
|
|
472
|
+
* for it on purpose. A list this would refuse to read is refused here as well.
|
|
473
|
+
*/
|
|
474
|
+
export function makeDomainKeys(entries) {
|
|
475
|
+
const rows = [];
|
|
476
|
+
const seen = new Set();
|
|
477
|
+
for (const entry of entries) {
|
|
478
|
+
if (!canonicalKey(entry?.key)) throw new InvalidDomainKeys("invalid_key");
|
|
479
|
+
if (seen.has(entry.key)) throw new InvalidDomainKeys("duplicate_key");
|
|
480
|
+
seen.add(entry.key);
|
|
481
|
+
let row = ' {\n "key": "' + entry.key + '"';
|
|
482
|
+
if (entry.name !== undefined && entry.name !== null) {
|
|
483
|
+
if (!validDomainKeyName(entry.name)) throw new InvalidDomainKeys("invalid_name");
|
|
484
|
+
row += ',\n "name": "' + entry.name + '"';
|
|
485
|
+
}
|
|
486
|
+
rows.push(row + "\n }");
|
|
487
|
+
}
|
|
488
|
+
if (rows.length > DOMAIN_KEYS_MAX_KEYS) throw new InvalidDomainKeys("invalid_keys");
|
|
489
|
+
const list = rows.length === 0 ? "[]" : "[\n" + rows.join(",\n") + "\n ]";
|
|
490
|
+
const raw = utf8('{\n "aamio_keys": ' + DOMAIN_KEYS_VERSION + ',\n "keys": ' + list + "\n}\n");
|
|
491
|
+
parseDomainKeys(raw);
|
|
492
|
+
return raw;
|
|
493
|
+
}
|
|
494
|
+
|
|
201
495
|
export const threadSigningInput = (w, bodyText) => "aamio-v1\n" + w + "\n" + sha256hex(bodyText);
|
|
202
496
|
export const presenceSigningInput = (key, bodyText) => "aamio-presence-v1\n" + key + "\n" + sha256hex(bodyText);
|
|
203
497
|
export const presenceDeleteSigningInput = (key, bodyText) => "aamio-presence-delete-v1\n" + key + "\n" + sha256hex(bodyText);
|