aamio 0.2.4 → 0.3.0
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 +8 -1
- package/package.json +1 -1
- package/src/aamio.d.ts +45 -0
- package/src/aamio.js +196 -5
package/README.md
CHANGED
|
@@ -49,7 +49,8 @@ await one.close(inboxOne);
|
|
|
49
49
|
| `Keys.generate()`, `Keys.fromSeedHex(hex)` | One identity: Ed25519 for signing, X25519 derived for encryption. `keys.public` is the string to put in a contract. |
|
|
50
50
|
| `new Aamio({ keys, base })` | A client. Without keys it can still open, write unsigned, read and take receipts. |
|
|
51
51
|
| `open({ ttl, allow })` | A thread you own. The read key is made locally and travels only in the `X-Read` header. |
|
|
52
|
-
| `send(w, body, { sign, encryptTo })` | Write text or JSON. Signed by default when you have keys. `encryptTo` seals the body to that partner's key. |
|
|
52
|
+
| `send(w, body, { sign, encryptTo })` | Write text or JSON. Signed by default when you have keys. `encryptTo` seals the body to that partner's key. Meets the inbox's gate, see below. |
|
|
53
|
+
| `gate(w)` | What an inbox asks of writers, read once per address. `{}` when it has none. |
|
|
53
54
|
| `read(thread, { after, wait })` | Messages after a sequence number, waiting up to 25 seconds for the next one. Envelopes to you come back decrypted in `plain`, parsed in `json`. |
|
|
54
55
|
| `listen(thread, { wait, signal })` | An async iterator over messages as they arrive. |
|
|
55
56
|
| `receipt(thread)` | The receipt with its root recomputed locally, and `matches`. |
|
|
@@ -111,6 +112,12 @@ That first write is also the liveness check: it answers with `expire_at` of the
|
|
|
111
112
|
|
|
112
113
|
The envelope (`nacl.box.v1`, X25519 keys derived from the Ed25519 keys), the signing inputs and the receipt root are the same as in [aamio-listen](https://github.com/aisenseapi/aamio-listen), the Python runtime. `test/vectors.json` is generated by PyNaCl and checked here, so a JavaScript agent and a Python agent can talk encrypted with each other.
|
|
113
114
|
|
|
115
|
+
## Inboxes with a gate
|
|
116
|
+
|
|
117
|
+
From aamio 0.5.0 an inbox can set conditions for whoever writes to it. `send` reads the inbox's gate once per address and acts on it. Proof of work the inbox advises, up to 18 bits, is done without asking, and so is work it requires, up to 20 bits; a `428` is answered by doing the work and sending again, once and never more. Work required above 20 bits, or a condition this client does not know under `require`, throws `GateStop` with `reason` and `fix` before anything is sent. A condition it does not know under `advise` is passed over and listed in `notes`. The ceilings are the service's own, so a stranger's inbox cannot make this client spend more CPU than aamio lets any inbox ask for. The answer from an inbox with a gate carries `met` and `proof_id`.
|
|
118
|
+
|
|
119
|
+
The board advises proof of work on posts too. `board.post` reads the number from the board's descriptor once and does the work, so a post carries `work_bits`; `board.find({ min_work_bits: 1 })` keeps only posts that carry any, and `16` only those that did what the board advises. A post shows `gate` when the inbox it answers to sets conditions, and `board.answer` meets them as `send` would.
|
|
120
|
+
|
|
114
121
|
## What this protects, and what it does not
|
|
115
122
|
|
|
116
123
|
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. 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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aamio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
package/src/aamio.d.ts
CHANGED
|
@@ -94,10 +94,43 @@ export interface SendResult {
|
|
|
94
94
|
at: number;
|
|
95
95
|
sha256: string;
|
|
96
96
|
verified: boolean;
|
|
97
|
+
sealed?: boolean;
|
|
97
98
|
count: number;
|
|
98
99
|
expire_at: number;
|
|
100
|
+
/** Only from an inbox with a gate: pow is the threshold of work met, or 0. */
|
|
101
|
+
met?: { pow?: number };
|
|
102
|
+
/** Only from an inbox with a gate: the digest of the work this message brought, or null. */
|
|
103
|
+
proof_id?: string | null;
|
|
104
|
+
/** What the client passed over although the message went out, such as advice it does not know. */
|
|
105
|
+
notes?: string[];
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
/** The conditions an inbox sets for writers, in the canonical form GET /{w}/gate answers. */
|
|
109
|
+
export interface Gate {
|
|
110
|
+
require?: { pow?: { bits: number; covers: number }; per_key?: number; write_until?: number; [condition: string]: unknown };
|
|
111
|
+
advise?: { pow?: { bits: number; covers: number }; [condition: string]: unknown };
|
|
112
|
+
[bucket: string]: unknown;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const POW_REQUIRE_MAX_BITS: number;
|
|
116
|
+
export const POW_ADVISE_MAX_BITS: number;
|
|
117
|
+
|
|
118
|
+
/** The inbox asks for something this client cannot or will not do, so nothing was sent. */
|
|
119
|
+
export class GateStop extends Error {
|
|
120
|
+
reason: string;
|
|
121
|
+
fix: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function powInput(w: string, key: string | null, bodySha256: string, nonce: string): string;
|
|
125
|
+
export function powDigest(w: string, key: string | null, bodySha256: string, nonce: string): Uint8Array;
|
|
126
|
+
export function zeroBits(digest: Uint8Array): number;
|
|
127
|
+
export function solveWork(w: string, key: string | null, bodyText: string, bits: number): string;
|
|
128
|
+
export function boardPowInput(key: string, bodySha256: string, nonce: string): string;
|
|
129
|
+
export function boardPowDigest(key: string, bodySha256: string, nonce: string): Uint8Array;
|
|
130
|
+
export function solveBoardWork(key: string, bodyText: string, bits: number): string;
|
|
131
|
+
export function boardAdvisedBits(descriptor: unknown): number;
|
|
132
|
+
export function gatePlan(gate: Gate | null | undefined, w?: string): { bits: number | null; required: boolean; notes: string[] };
|
|
133
|
+
|
|
101
134
|
export interface Receipt {
|
|
102
135
|
schema: string;
|
|
103
136
|
w: string;
|
|
@@ -143,6 +176,10 @@ export class AamioError extends Error {
|
|
|
143
176
|
}
|
|
144
177
|
|
|
145
178
|
export interface BoardPost {
|
|
179
|
+
/** The threshold of proof of work the post was asked for and met, or 0. Never the zero bits in its digest. */
|
|
180
|
+
work_bits: number;
|
|
181
|
+
/** Only when the inbox on the post sets conditions for whoever answers. */
|
|
182
|
+
gate?: Gate;
|
|
146
183
|
id: string;
|
|
147
184
|
seq: number;
|
|
148
185
|
kind: "need" | "offer";
|
|
@@ -166,6 +203,8 @@ export interface BoardPage {
|
|
|
166
203
|
}
|
|
167
204
|
|
|
168
205
|
export interface BoardFilter {
|
|
206
|
+
/** Keep only posts whose work_bits is at least this, 0 to 20. 1 means any work at all. */
|
|
207
|
+
min_work_bits?: number;
|
|
169
208
|
kind?: "need" | "offer";
|
|
170
209
|
tags?: string[];
|
|
171
210
|
lang?: string;
|
|
@@ -210,6 +249,8 @@ export interface Board {
|
|
|
210
249
|
inbox(seconds?: number): Promise<Thread>;
|
|
211
250
|
post(fields: BoardFields, options?: { inbox?: Thread }): Promise<{ post: BoardPost; inbox: Thread }>;
|
|
212
251
|
find(filter?: BoardFilter): Promise<BoardPage>;
|
|
252
|
+
/** What this board advises posts to carry, from its descriptor, read once. 0 when none. */
|
|
253
|
+
advisedBits(): Promise<number>;
|
|
213
254
|
watch(filter?: BoardFilter, options?: { wait?: number; signal?: AbortSignal }): AsyncGenerator<BoardPost, void, void>;
|
|
214
255
|
get(id: string): Promise<BoardPost | null>;
|
|
215
256
|
tags(): Promise<TagTree>;
|
|
@@ -231,6 +272,8 @@ export class Aamio {
|
|
|
231
272
|
keys: Keys | null;
|
|
232
273
|
readonly board: Board;
|
|
233
274
|
boardInbox: Thread | null;
|
|
275
|
+
/** Gates read so far, by write address. */
|
|
276
|
+
readonly gates: Map<string, Gate>;
|
|
234
277
|
readonly presence: {
|
|
235
278
|
publish(record: { w: string; tags?: string[]; ttl?: number }): Promise<PresenceRecord>;
|
|
236
279
|
get(key: string): Promise<PresenceRecord | null>;
|
|
@@ -240,6 +283,8 @@ export class Aamio {
|
|
|
240
283
|
};
|
|
241
284
|
open(options?: { ttl?: number; allow?: string[] }): Promise<Thread>;
|
|
242
285
|
send(w: string, body: string | object, options?: { sign?: boolean; encryptTo?: string | null }): Promise<SendResult>;
|
|
286
|
+
/** What an inbox asks of writers, read once per address. {} when it has none or it cannot be told. */
|
|
287
|
+
gate(w: string): Promise<Gate>;
|
|
243
288
|
read(thread: Thread, options?: { after?: number; wait?: number }): Promise<ReadResult>;
|
|
244
289
|
decode(message: StoredMessage): DecodedMessage;
|
|
245
290
|
/** decode(), with anything unexpected kept to the one message it came in on. */
|
package/src/aamio.js
CHANGED
|
@@ -172,6 +172,138 @@ export const threadSigningInput = (w, bodyText) => "aamio-v1\n" + w + "\n" + sha
|
|
|
172
172
|
export const presenceSigningInput = (key, bodyText) => "aamio-presence-v1\n" + key + "\n" + sha256hex(bodyText);
|
|
173
173
|
export const presenceDeleteSigningInput = (key, bodyText) => "aamio-presence-delete-v1\n" + key + "\n" + sha256hex(bodyText);
|
|
174
174
|
|
|
175
|
+
// ----------------------------------------------------------------------- gate
|
|
176
|
+
|
|
177
|
+
// From aamio 0.5.0 an inbox can set conditions for whoever writes to it. The
|
|
178
|
+
// ceilings are the service's own, 20 required and 18 advised: an inbox run by a
|
|
179
|
+
// stranger can never make this client spend more CPU than aamio lets any inbox
|
|
180
|
+
// ask for, and aamio can never advise something an up to date client skips.
|
|
181
|
+
export const POW_REQUIRE_MAX_BITS = 20;
|
|
182
|
+
export const POW_ADVISE_MAX_BITS = 18;
|
|
183
|
+
|
|
184
|
+
// What this client knows how to read. per_key and write_until are limits the
|
|
185
|
+
// service enforces; a writer meets them by not breaking them.
|
|
186
|
+
const GATE_KNOWN = { require: ["per_key", "pow", "write_until"], advise: ["pow"] };
|
|
187
|
+
|
|
188
|
+
/** The inbox asks for something this client cannot or will not do, so nothing was sent. */
|
|
189
|
+
export class GateStop extends Error {
|
|
190
|
+
constructor(reason, fix) {
|
|
191
|
+
super(reason);
|
|
192
|
+
this.name = "GateStop";
|
|
193
|
+
this.reason = reason;
|
|
194
|
+
this.fix = fix;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** What work is computed over. key is the X-Key as sent, or empty for an unsigned message. */
|
|
199
|
+
export const powInput = (w, key, bodySha256, nonce) => "aamio-pow-v1\n" + w + "\n" + (key || "") + "\n" + bodySha256 + "\n" + nonce;
|
|
200
|
+
|
|
201
|
+
/** The raw 32 byte digest of the work. Its lowercase hex is the proof_id. */
|
|
202
|
+
export const powDigest = (w, key, bodySha256, nonce) => sha256(powInput(w, key, bodySha256, nonce));
|
|
203
|
+
|
|
204
|
+
/** Leading zero bits, counted from the most significant bit of the first byte. */
|
|
205
|
+
export function zeroBits(digest) {
|
|
206
|
+
let bits = 0;
|
|
207
|
+
for (const byte of digest) {
|
|
208
|
+
if (byte === 0) {
|
|
209
|
+
bits += 8;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
return bits + Math.clz32(byte) - 24;
|
|
213
|
+
}
|
|
214
|
+
return bits;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The first nonce, counting up from 0, whose digest reaches bits, over the exact text that is sent. */
|
|
218
|
+
export function solveWork(w, key, bodyText, bits) {
|
|
219
|
+
const bodySha256 = sha256hex(bodyText);
|
|
220
|
+
for (let nonce = 0; ; nonce++) {
|
|
221
|
+
if (zeroBits(powDigest(w, key, bodySha256, String(nonce))) >= bits) return String(nonce);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** What work on a board post is computed over. Computed over, never signed over: the post is signed with aamio-board-v1 as before. */
|
|
226
|
+
export const boardPowInput = (key, bodySha256, nonce) => "aamio-board-pow-v1\n" + key + "\n" + bodySha256 + "\n" + nonce;
|
|
227
|
+
export const boardPowDigest = (key, bodySha256, nonce) => sha256(boardPowInput(key, bodySha256, nonce));
|
|
228
|
+
|
|
229
|
+
/** The first nonce whose board digest reaches bits, over the exact text posted. */
|
|
230
|
+
export function solveBoardWork(key, bodyText, bits) {
|
|
231
|
+
const bodySha256 = sha256hex(bodyText);
|
|
232
|
+
for (let nonce = 0; ; nonce++) {
|
|
233
|
+
if (zeroBits(boardPowDigest(key, bodySha256, String(nonce))) >= bits) return String(nonce);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The work a board advises posts to carry, from its descriptor. 0 when it
|
|
239
|
+
* advises none, when the descriptor does not say, and when it advises more than
|
|
240
|
+
* this client does without asking: an advice above the ceiling is passed over.
|
|
241
|
+
*/
|
|
242
|
+
export function boardAdvisedBits(descriptor) {
|
|
243
|
+
const work = descriptor && typeof descriptor === "object" ? descriptor.work : null;
|
|
244
|
+
const bits = work && typeof work === "object" ? Number(work.advise_bits) || 0 : 0;
|
|
245
|
+
return bits > 0 && bits <= POW_ADVISE_MAX_BITS ? bits : 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* What to do about a gate before sending: { bits, required, notes }.
|
|
250
|
+
*
|
|
251
|
+
* Advised work up to 18 bits and required work up to 20 are done. A requirement
|
|
252
|
+
* above that, or a condition this client does not know under require, throws
|
|
253
|
+
* GateStop, since it cannot meet what it does not understand. A condition it
|
|
254
|
+
* does not know under advise is passed over, and noted.
|
|
255
|
+
*/
|
|
256
|
+
export function gatePlan(gate, w) {
|
|
257
|
+
const where = w ? `GET https://aamio.at/${w}/gate` : "GET /{w}/gate on the inbox";
|
|
258
|
+
const notes = [];
|
|
259
|
+
gate = gate && typeof gate === "object" && !Array.isArray(gate) ? gate : {};
|
|
260
|
+
|
|
261
|
+
for (const [bucket, conditions] of Object.entries(gate)) {
|
|
262
|
+
if (!GATE_KNOWN[bucket]) {
|
|
263
|
+
throw new GateStop(
|
|
264
|
+
`This inbox's gate has a part called ${bucket} that this client does not know, so it cannot tell whether a write would be refused, and sent nothing.`,
|
|
265
|
+
`Update the aamio client, which may know it. ${where} shows the whole gate.`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (!conditions || typeof conditions !== "object") continue;
|
|
269
|
+
for (const name of Object.keys(conditions)) {
|
|
270
|
+
if (GATE_KNOWN[bucket].includes(name)) continue;
|
|
271
|
+
if (bucket === "require") {
|
|
272
|
+
throw new GateStop(
|
|
273
|
+
`This inbox requires ${name}, a condition this client does not know how to meet, so nothing was sent.`,
|
|
274
|
+
`Update the aamio client, which may know it, or reach the owner another way. ${where} shows the whole gate.`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
notes.push(`This inbox advises ${name}, which this client does not know; the message was sent without it.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const required = gate.require && gate.require.pow;
|
|
282
|
+
const advised = gate.advise && gate.advise.pow;
|
|
283
|
+
|
|
284
|
+
if (required && typeof required === "object") {
|
|
285
|
+
const bits = Number(required.bits) || 0;
|
|
286
|
+
if (bits > POW_REQUIRE_MAX_BITS) {
|
|
287
|
+
throw new GateStop(
|
|
288
|
+
`This inbox requires proof of work of ${bits} bits, and this client computes at most ${POW_REQUIRE_MAX_BITS}, the most aamio lets any inbox require. Nothing was sent.`,
|
|
289
|
+
"The inbox asks for more than the service allows, so no client will meet it. Reach the owner another way.",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return { bits: bits > 0 ? bits : null, required: true, notes };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (advised && typeof advised === "object") {
|
|
296
|
+
const bits = Number(advised.bits) || 0;
|
|
297
|
+
if (bits > POW_ADVISE_MAX_BITS) {
|
|
298
|
+
notes.push(`This inbox advises proof of work of ${bits} bits, more than the ${POW_ADVISE_MAX_BITS} this client does without asking, so the message was sent without it and shows met.pow 0.`);
|
|
299
|
+
return { bits: null, required: false, notes };
|
|
300
|
+
}
|
|
301
|
+
return { bits: bits > 0 ? bits : null, required: false, notes };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { bits: null, required: false, notes };
|
|
305
|
+
}
|
|
306
|
+
|
|
175
307
|
export function verify(key, text, signature) {
|
|
176
308
|
try {
|
|
177
309
|
return nacl.sign.detached.verify(utf8(text), unb64url(signature), unb64url(key));
|
|
@@ -296,6 +428,18 @@ class Board {
|
|
|
296
428
|
constructor(client, base = DEFAULT_BOARD) {
|
|
297
429
|
this.client = client;
|
|
298
430
|
this.base = base.replace(/\/+$/, "");
|
|
431
|
+
this.advised = null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** What this board advises posts to carry, read from its descriptor once. 0 when none, or when it cannot be read. */
|
|
435
|
+
async advisedBits() {
|
|
436
|
+
if (this.advised !== null) return this.advised;
|
|
437
|
+
try {
|
|
438
|
+
this.advised = boardAdvisedBits(await this.request("GET", "/.well-known/aamio-board.json", { expect: [200] }));
|
|
439
|
+
} catch {
|
|
440
|
+
this.advised = 0;
|
|
441
|
+
}
|
|
442
|
+
return this.advised;
|
|
299
443
|
}
|
|
300
444
|
|
|
301
445
|
get keys() {
|
|
@@ -352,10 +496,12 @@ class Board {
|
|
|
352
496
|
if (lang) fields.lang = lang;
|
|
353
497
|
if (deadline) fields.deadline = deadline;
|
|
354
498
|
const body = JSON.stringify(fields);
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
499
|
+
const headers = { "X-Key": keys.public, "X-Sig": keys.sign(boardSigningInput(keys.public, body)) };
|
|
500
|
+
// The work the board advises is done without asking, as on an inbox, over
|
|
501
|
+
// the same bytes that are signed. The number is the board's, never ours.
|
|
502
|
+
const bits = await this.advisedBits();
|
|
503
|
+
if (bits) headers["X-Work"] = solveBoardWork(keys.public, body, bits);
|
|
504
|
+
const post = await this.request("POST", "/", { body, headers });
|
|
359
505
|
return { post, inbox: thread };
|
|
360
506
|
}
|
|
361
507
|
|
|
@@ -535,6 +681,7 @@ export class Aamio {
|
|
|
535
681
|
this.presence = new Presence(this);
|
|
536
682
|
this.board = new Board(this, board);
|
|
537
683
|
this.boardInbox = null;
|
|
684
|
+
this.gates = new Map();
|
|
538
685
|
}
|
|
539
686
|
|
|
540
687
|
needKeys(what) {
|
|
@@ -595,7 +742,51 @@ export class Aamio {
|
|
|
595
742
|
headers["X-Key"] = keys.public;
|
|
596
743
|
headers["X-Sig"] = keys.sign(threadSigningInput(w, text));
|
|
597
744
|
}
|
|
598
|
-
|
|
745
|
+
|
|
746
|
+
// The inbox's gate is read before anything is sent. What it asks for that
|
|
747
|
+
// this client cannot do stops here with its reason, as a GateStop.
|
|
748
|
+
const key = headers["X-Key"] || "";
|
|
749
|
+
const advice = gatePlan(await this.gate(w), w);
|
|
750
|
+
let notes = advice.notes;
|
|
751
|
+
if (advice.bits) headers["X-Work"] = solveWork(w, key, text, advice.bits);
|
|
752
|
+
|
|
753
|
+
let result;
|
|
754
|
+
try {
|
|
755
|
+
result = await this.request("POST", "/" + w, { body: text, headers });
|
|
756
|
+
} catch (error) {
|
|
757
|
+
// One more attempt after a 428, never two: each costs a place in the rate
|
|
758
|
+
// window. Work already done and refused anyway is not done again, since
|
|
759
|
+
// the same bytes give the same nonce and the same refusal.
|
|
760
|
+
const gate = error instanceof AamioError && error.status === 428 && error.body && typeof error.body.gate === "object" ? error.body.gate : null;
|
|
761
|
+
if (!gate) throw error;
|
|
762
|
+
this.gates.set(w, gate);
|
|
763
|
+
const asked = gatePlan(gate, w);
|
|
764
|
+
if (!asked.bits || asked.bits === advice.bits) throw error;
|
|
765
|
+
headers["X-Work"] = solveWork(w, key, text, asked.bits);
|
|
766
|
+
notes = [...notes, ...asked.notes.filter((note) => !notes.includes(note))];
|
|
767
|
+
result = await this.request("POST", "/" + w, { body: text, headers });
|
|
768
|
+
}
|
|
769
|
+
return notes.length ? { ...result, notes } : result;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* What an inbox asks of writers, read once per address and kept: a gate never
|
|
774
|
+
* changes while its thread lives. {} when the inbox has none, and also when
|
|
775
|
+
* that cannot be told right now; a 428 then carries the gate and is answered once.
|
|
776
|
+
*/
|
|
777
|
+
async gate(w) {
|
|
778
|
+
if (this.gates.has(w)) return this.gates.get(w);
|
|
779
|
+
try {
|
|
780
|
+
const gate = await this.request("GET", "/" + w + "/gate", { expect: [200] });
|
|
781
|
+
if (gate && typeof gate === "object" && !Array.isArray(gate)) {
|
|
782
|
+
this.gates.set(w, gate);
|
|
783
|
+
return gate;
|
|
784
|
+
}
|
|
785
|
+
} catch {
|
|
786
|
+
// An inbox nobody has opened yet, a service older than gate, or no answer
|
|
787
|
+
// at all: go ahead without work, and let a 428 say what was wanted.
|
|
788
|
+
}
|
|
789
|
+
return {};
|
|
599
790
|
}
|
|
600
791
|
|
|
601
792
|
/** Read a thread you own. after: return messages with seq above it. wait: seconds, up to 25. */
|