@orangecheck/chat-core 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/chat-core",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Verify OC Chat's public artifacts: channel descriptors and posts, directory listings and did:oc session devices. See https://github.com/orangecheck/oc-chat-protocol.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -283,6 +283,65 @@ describe('resolveDirectoryHandle', () => {
283
283
  });
284
284
  });
285
285
 
286
+ it('a full page of junk the reader drops is still a full page: the read goes on past it', async () => {
287
+ const { w, deps, anchorAt } = world();
288
+ const a = claim('alice', ALICE, TIP - 100);
289
+ anchorAt(a, ALICE);
290
+ w.bound.set(ALICE, [pk(1)]);
291
+ const newer = listing(2, 'alice', BOB, claim('alice', BOB, TIP - 50, 'bob'), {}, 1_790_000_900);
292
+ w.relays.set('wss://relay.ochk.io', [newer, listing(1, 'alice', ALICE, a, {}, 1_790_000_000)]);
293
+ const real = deps.page;
294
+ // The relay sends a full page: one listing that verifies, and 199 the reader
295
+ // drops for their signatures. Only the next page reaches alice's claim.
296
+ deps.page = async (relay, filter) => {
297
+ const got = await real(relay, filter);
298
+ if (relay !== 'wss://relay.ochk.io' || filter.until !== undefined) return got;
299
+ return { events: got.events.slice(0, 1), complete: true, sent: filter.limit };
300
+ };
301
+ expect(await resolveDirectoryHandle('alice', 'people', deps)).toMatchObject({
302
+ result: 'holds',
303
+ holder: ALICE,
304
+ });
305
+ });
306
+
307
+ it('a full page that is all junk has not been read completely, never "not listed"', async () => {
308
+ const { w, deps } = world();
309
+ w.relays.set('wss://relay.ochk.io', []);
310
+ const real = deps.page;
311
+ deps.page = async (relay, filter) =>
312
+ relay === 'wss://relay.ochk.io'
313
+ ? { events: [], complete: true, sent: filter.limit }
314
+ : real(relay, filter);
315
+ expect(await resolveDirectoryHandle('alice', 'people', deps)).toEqual({
316
+ result: 'E_DIR_UNRESOLVED',
317
+ });
318
+ });
319
+
320
+ it('an audit relay that pads a page with junk is read past it, so the earlier claim it holds counts', async () => {
321
+ const { w, deps, anchorAt, publish } = world();
322
+ const a = claim('alice', ALICE, TIP - 100);
323
+ const b = claim('alice', BOB, TIP - 50, 'bob');
324
+ anchorAt(a, ALICE);
325
+ anchorAt(b, BOB);
326
+ w.bound.set(ALICE, [pk(1)]);
327
+ w.bound.set(BOB, [pk(2)]);
328
+ publish(listing(2, 'alice', BOB, b));
329
+ w.relays.set('wss://nos.lol', [
330
+ listing(2, 'alice', BOB, b, {}, 1_790_000_900),
331
+ listing(1, 'alice', ALICE, a, {}, 1_790_000_000),
332
+ ]);
333
+ const real = deps.page;
334
+ deps.page = async (relay, filter) => {
335
+ const got = await real(relay, filter);
336
+ if (relay !== 'wss://nos.lol' || filter.until !== undefined) return got;
337
+ return { events: got.events.slice(0, 1), complete: true, sent: filter.limit };
338
+ };
339
+ expect(await resolveDirectoryHandle('alice', 'people', deps)).toMatchObject({
340
+ result: 'holds',
341
+ holder: ALICE,
342
+ });
343
+ });
344
+
286
345
  it('a directory relay that stalls on a full page has not been read completely', async () => {
287
346
  const { w, deps } = world();
288
347
  const same = Array.from({ length: 200 }, (_, i) =>
package/src/directory.ts CHANGED
@@ -49,11 +49,21 @@ const AUDIT_PAGES = 2;
49
49
  /** Distinct header reads one resolution may make; past this a header is unread. */
50
50
  const HEADER_BUDGET = 128;
51
51
 
52
- /** One page from one relay: its events, and whether the relay finished answering (EOSE). */
52
+ /**
53
+ * One page from one relay: its events, whether the relay finished answering
54
+ * (EOSE), and `sent`, how many events the relay sent, the ones the reader dropped
55
+ * (a bad signature) included. A page is full by what the relay sent, not by what
56
+ * was kept: a relay that pads a page with junk has more behind it. Without
57
+ * `sent`, the events kept are the count.
58
+ */
53
59
  export type PageRead = (
54
60
  relay: string,
55
61
  filter: { kinds: number[]; '#d': string[]; limit: number; until?: number }
56
- ) => Promise<{ events: NostrEvent[]; complete: boolean }>;
62
+ ) => Promise<{ events: NostrEvent[]; complete: boolean; sent?: number }>;
63
+
64
+ /** How many events a page's relay sent: `sent`, or the events kept when it is not given. */
65
+ const sentOn = (got: { events: NostrEvent[]; sent?: number }) =>
66
+ Math.max(got.sent ?? 0, got.events.length);
57
67
 
58
68
  export interface WalkDeps {
59
69
  page: PageRead;
@@ -148,7 +158,7 @@ async function readAll(
148
158
  until = until === undefined ? ev.created_at : Math.min(until, ev.created_at);
149
159
  }
150
160
  // A short page is the end; a full page that adds nothing is a stalled read.
151
- if (got.events.length < PAGE) return [...seen.values()];
161
+ if (sentOn(got) < PAGE) return [...seen.values()];
152
162
  if (added === 0) return null;
153
163
  }
154
164
  return null;
@@ -164,7 +174,7 @@ async function readSome(
164
174
  const seen = new Map<string, NostrEvent>();
165
175
  let until: number | undefined;
166
176
  for (let i = 0; i < pages; i++) {
167
- let got: { events: NostrEvent[]; complete: boolean };
177
+ let got: Awaited<ReturnType<PageRead>>;
168
178
  try {
169
179
  got = await page(relay, {
170
180
  kinds: [30114],
@@ -179,7 +189,7 @@ async function readSome(
179
189
  seen.set(ev.id, ev);
180
190
  until = until === undefined ? ev.created_at : Math.min(until, ev.created_at);
181
191
  }
182
- if (!got.complete || got.events.length < PAGE) break;
192
+ if (!got.complete || sentOn(got) < PAGE) break;
183
193
  }
184
194
  return [...seen.values()];
185
195
  }