@fedify/vocab 2.4.0-pr.936.41 → 2.4.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.
Files changed (36) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +31328 -8723
  3. package/dist/mod.d.cts +4599 -6
  4. package/dist/mod.d.ts +4599 -6
  5. package/dist/mod.js +31221 -8623
  6. package/dist-tests/actor-D7_iX4I4.mjs +276 -0
  7. package/dist-tests/actor.test.mjs +55 -49
  8. package/dist-tests/{actor-MBpzF3-D.mjs → esm-DP2Spt0u.mjs} +15 -211
  9. package/dist-tests/gateway.test.mjs +1906 -0
  10. package/dist-tests/lookup-IiNFWuXL.mjs +480 -0
  11. package/dist-tests/lookup.test.mjs +10 -268
  12. package/dist-tests/object.yaml +15 -0
  13. package/dist-tests/portable-lookup.test.mjs +653 -0
  14. package/dist-tests/signed-representation.test.mjs +189 -0
  15. package/dist-tests/translation.test.mjs +404 -0
  16. package/dist-tests/translation.yaml +91 -0
  17. package/dist-tests/type.test.mjs +2 -2
  18. package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-COZcxf00.mjs} +30831 -8515
  19. package/dist-tests/vocab.test.mjs +677 -10
  20. package/package.json +5 -5
  21. package/src/__snapshots__/vocab.test.ts.snap +291 -15
  22. package/src/actor.test.ts +41 -40
  23. package/src/actor.ts +120 -1
  24. package/src/gateway.test.ts +2637 -0
  25. package/src/lookup.test.ts +6 -5
  26. package/src/lookup.ts +460 -33
  27. package/src/mod.ts +9 -0
  28. package/src/object.yaml +15 -0
  29. package/src/portable-lookup.test.ts +889 -0
  30. package/src/preprocessors.ts +1 -1
  31. package/src/signed-representation.test.ts +277 -0
  32. package/src/translation.test.ts +518 -0
  33. package/src/translation.yaml +91 -0
  34. package/src/vocab.test.ts +914 -5
  35. /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
  36. /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
@@ -0,0 +1,480 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ globalThis.addEventListener = () => {};
3
+ import { A as Object$1 } from "./vocab-COZcxf00.mjs";
4
+ import { i as isActor, o as name, s as version } from "./actor-D7_iX4I4.mjs";
5
+ import { t as getTypeId } from "./type-BMdjm4Eg.mjs";
6
+ import { canonicalizePortableUri, formatIri, getDocumentLoader, haveSameFe34Origin, haveSameIriOrigin, parseIri } from "@fedify/vocab-runtime";
7
+ import { lookupWebFinger } from "@fedify/webfinger";
8
+ import { SpanStatusCode, trace } from "@opentelemetry/api";
9
+ import { getLogger } from "@logtape/logtape";
10
+ import { PortableObjectRejectedError, createSnapshotContextLoader, dereferencePortableIri, getPortableGatewayCandidates, getPortableResponseClaim, isPortableIri, parseCompatibleEf61Reference } from "@fedify/vocab-runtime/internal/portable-dereference";
11
+ import { delay } from "es-toolkit";
12
+ //#region src/handle.ts
13
+ /**
14
+ * Regular expression to match a fediverse handle in the format `@user@server`
15
+ * or `user@server`. The `user` part can contain alphanumeric characters and
16
+ * some special characters except `@`. The `server` part is all characters
17
+ * after the `@` symbol in the middle.
18
+ */
19
+ const handleRegexp = /^@?((?:[-A-Za-z0-9._~!$&'()*+,;=]|%[A-Fa-f0-9]{2})+)@([^@]+)$/;
20
+ /**
21
+ * Parses a fediverse handle in the format `@user@server` or `user@server`.
22
+ * The `user` part can contain alphanumeric characters and some special
23
+ * characters except `@`. The `server` part is all characters after the `@`
24
+ * symbol in the middle.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const handle = parseFediverseHandle("@username@example.com");
29
+ * console.log(handle?.username); // "username"
30
+ * console.log(handle?.host); // "example.com"
31
+ * ```
32
+ *
33
+ * @param handle - The fediverse handle string to parse.
34
+ * @returns A {@link FediverseHandle} object with `username` and `host`
35
+ * if the input is valid; otherwise `null`.
36
+ * @since 1.8.0
37
+ */
38
+ function parseFediverseHandle(handle) {
39
+ const match = handleRegexp.exec(handle);
40
+ if (match) return {
41
+ username: match[1],
42
+ host: match[2]
43
+ };
44
+ return null;
45
+ }
46
+ /**
47
+ * Converts a fediverse handle in the format `@user@server` or `user@server`
48
+ * to an `acct:` URI, which is a URL-like identifier for ActivityPub actors.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const identifier = toAcctUrl("@username@example.com");
53
+ * console.log(identifier?.href); // "acct:username@example.com"
54
+ * ```
55
+ *
56
+ * @param handle - The fediverse handle string to convert.
57
+ * @returns A `URL` object representing the `acct:` URI if conversion succeeds;
58
+ * otherwise `null`.
59
+ * @since 1.8.0
60
+ */
61
+ function toAcctUrl(handle) {
62
+ const parsed = parseFediverseHandle(handle);
63
+ if (!parsed) return null;
64
+ return new URL(`acct:${parsed.username}@${parsed.host}`);
65
+ }
66
+ //#endregion
67
+ //#region src/lookup.ts
68
+ const objectLookupCounters = /* @__PURE__ */ new WeakMap();
69
+ function getObjectLookupCounter(meterProvider) {
70
+ let counter = objectLookupCounters.get(meterProvider);
71
+ if (counter == null) {
72
+ counter = meterProvider.getMeter(name, version).createCounter("activitypub.object.lookup", {
73
+ description: "ActivityStreams object lookups via lookupObject(), classified by whether the resolved value is an Actor.",
74
+ unit: "{lookup}"
75
+ });
76
+ objectLookupCounters.set(meterProvider, counter);
77
+ }
78
+ return counter;
79
+ }
80
+ function getLookupRemoteHost(identifier) {
81
+ let url;
82
+ if (identifier instanceof URL) url = identifier;
83
+ else if (PORTABLE_IRI_PATTERN.test(identifier)) return;
84
+ else try {
85
+ url = new URL(identifier);
86
+ } catch {
87
+ return extractHandleHost(identifier.startsWith("@") ? identifier.slice(1) : identifier);
88
+ }
89
+ if (isPortableIri(url)) return void 0;
90
+ if (url.host !== "") return url.host;
91
+ if (url.protocol === "acct:") return extractHandleHost(url.pathname);
92
+ }
93
+ function extractHandleHost(handle) {
94
+ const at = handle.lastIndexOf("@");
95
+ if (at < 0 || at >= handle.length - 1) return void 0;
96
+ const candidate = handle.slice(at + 1);
97
+ if (/[/?#\s]/.test(candidate)) return void 0;
98
+ try {
99
+ return new URL(`https://${candidate}`).host || void 0;
100
+ } catch {
101
+ return;
102
+ }
103
+ }
104
+ const logger = getLogger([
105
+ "fedify",
106
+ "vocab",
107
+ "lookup"
108
+ ]);
109
+ /**
110
+ * The maximum number of requests to FEP-ef61 gateways that a single
111
+ * {@link lookupObject} call makes for portable objects. Gateways come from
112
+ * possibly untrusted WebFinger responses and location hints, so they are
113
+ * bounded to keep a single lookup from fanning out to many servers.
114
+ */
115
+ const MAX_PORTABLE_ATTEMPTS = 5;
116
+ /**
117
+ * Looks up an ActivityStreams object by its URI (including `acct:` URIs)
118
+ * or a fediverse handle (e.g., `@user@server` or `user@server`).
119
+ *
120
+ * @example
121
+ * ``` typescript
122
+ * // Look up an actor by its fediverse handle:
123
+ * await lookupObject("@hongminhee@fosstodon.org");
124
+ * // returning a `Person` object.
125
+ *
126
+ * // A fediverse handle can omit the leading '@':
127
+ * await lookupObject("hongminhee@fosstodon.org");
128
+ * // returning a `Person` object.
129
+ *
130
+ * // A `acct:` URI can be used as well:
131
+ * await lookupObject("acct:hongminhee@fosstodon.org");
132
+ * // returning a `Person` object.
133
+ *
134
+ * // Look up an object by its URI:
135
+ * await lookupObject("https://todon.eu/@hongminhee/112060633798771581");
136
+ * // returning a `Note` object.
137
+ *
138
+ * // It can be a `URL` object as well:
139
+ * await lookupObject(new URL("https://todon.eu/@hongminhee/112060633798771581"));
140
+ * // returning a `Note` object.
141
+ * ```
142
+ *
143
+ * [FEP-ef61] portable objects, including portable actors found through
144
+ * WebFinger, are looked up only if the `verifyPortableObject` option is
145
+ * given; see {@link LookupObjectOptions.verifyPortableObject}. Pass
146
+ * {@link LookupObjectOptions.gateways} to look up a portable ID without
147
+ * `@gateway` location hints.
148
+ *
149
+ * [FEP-ef61]: https://w3id.org/fep/ef61
150
+ *
151
+ * @param identifier The URI or fediverse handle to look up.
152
+ * @param options Lookup options.
153
+ * @returns The object, or `null` if not found.
154
+ * @since 0.2.0
155
+ */
156
+ async function lookupObject(identifier, options = {}) {
157
+ return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.lookup_object", async (span) => {
158
+ let kind = "other";
159
+ try {
160
+ const result = await lookupObjectInternal(identifier, options);
161
+ if (result != null) kind = isActor(result) ? "actor" : "object";
162
+ if (result == null) span.setStatus({ code: SpanStatusCode.ERROR });
163
+ else {
164
+ if (result.id != null) span.setAttribute("activitypub.object.id", result.id.href);
165
+ span.setAttribute("activitypub.object.type", getTypeId(result).href);
166
+ if (result.replyTargetIds.length > 0) span.setAttribute("activitypub.object.in_reply_to", result.replyTargetIds.map((id) => id.href));
167
+ span.addEvent("activitypub.object.fetched", {
168
+ "activitypub.object.type": getTypeId(result).href,
169
+ "activitypub.object.json": JSON.stringify(await result.toJsonLd(options))
170
+ });
171
+ }
172
+ return result;
173
+ } catch (error) {
174
+ span.recordException(error);
175
+ span.setStatus({
176
+ code: SpanStatusCode.ERROR,
177
+ message: String(error)
178
+ });
179
+ throw error;
180
+ } finally {
181
+ if (options.meterProvider != null) {
182
+ const attributes = { "activitypub.lookup.kind": kind };
183
+ const host = getLookupRemoteHost(identifier);
184
+ if (host != null) attributes["activitypub.remote.host"] = host;
185
+ getObjectLookupCounter(options.meterProvider).add(1, attributes);
186
+ }
187
+ span.end();
188
+ }
189
+ });
190
+ }
191
+ async function lookupObjectInternal(identifier, options = {}) {
192
+ const documentLoader = options.documentLoader ?? getDocumentLoader({ userAgent: options.userAgent });
193
+ const portable = {
194
+ options,
195
+ documentLoader,
196
+ attempted: /* @__PURE__ */ new Set(),
197
+ remaining: MAX_PORTABLE_ATTEMPTS
198
+ };
199
+ if (typeof identifier === "string") {
200
+ if (PORTABLE_IRI_PATTERN.test(identifier)) {
201
+ const candidate = parsePortableCandidate(identifier);
202
+ if (candidate == null) return null;
203
+ return await lookupPortableObject(portable, candidate, void 0);
204
+ }
205
+ if (options.verifyPortableObject != null && parseCompatibleEf61Reference(identifier) === null) return null;
206
+ identifier = toAcctUrl(identifier) ?? parseIri(identifier);
207
+ }
208
+ if (isPortableIri(identifier)) {
209
+ let candidate;
210
+ try {
211
+ candidate = parseIri(identifier);
212
+ } catch (error) {
213
+ if (error instanceof TypeError) return null;
214
+ throw error;
215
+ }
216
+ return await lookupPortableObject(portable, candidate, void 0);
217
+ }
218
+ let remoteDoc = null;
219
+ if (identifier.protocol === "http:" || identifier.protocol === "https:") {
220
+ const compatible = getCompatibleCandidate(identifier.href, options);
221
+ if (compatible !== void 0) {
222
+ if (compatible == null) return null;
223
+ return await lookupPortableObject(portable, compatible.id, [compatible.gateway]);
224
+ }
225
+ try {
226
+ remoteDoc = await documentLoader(identifier.href, { signal: options.signal });
227
+ } catch (error) {
228
+ logger.debug("Failed to fetch remote document:\n{error}", { error });
229
+ }
230
+ }
231
+ if (remoteDoc == null) {
232
+ const jrd = await lookupWebFinger(identifier, {
233
+ userAgent: options.userAgent,
234
+ tracerProvider: options.tracerProvider,
235
+ meterProvider: options.meterProvider,
236
+ allowPrivateAddress: "allowPrivateAddress" in options && options.allowPrivateAddress === true,
237
+ signal: options.signal
238
+ });
239
+ if (jrd?.links == null) return null;
240
+ const webFingerGateway = getWebFingerGateway(identifier);
241
+ for (const l of jrd.links) {
242
+ if (l.type !== "application/activity+json" && !l.type?.match(/application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/) || l.rel !== "self" || l.href == null) continue;
243
+ if (PORTABLE_IRI_PATTERN.test(l.href)) {
244
+ if (options.verifyPortableObject == null) {
245
+ logger.debug("Skipping the portable self link {href}, as the verifyPortableObject option is not given.", { href: l.href });
246
+ continue;
247
+ }
248
+ const candidate = parsePortableCandidate(l.href);
249
+ if (candidate == null) continue;
250
+ const gateways = webFingerGateway == null ? [] : [webFingerGateway];
251
+ if (options.gateways == null) gateways.push(...getPortableGatewayCandidates(candidate));
252
+ const object = await lookupPortableObject(portable, candidate, gateways);
253
+ if (object != null) return object;
254
+ if (options.signal?.aborted) return null;
255
+ continue;
256
+ }
257
+ const compatible = getCompatibleCandidate(l.href, options);
258
+ if (compatible !== void 0) {
259
+ if (compatible == null) continue;
260
+ const object = await lookupPortableObject(portable, compatible.id, [compatible.gateway]);
261
+ if (object != null) return object;
262
+ if (options.signal?.aborted) return null;
263
+ continue;
264
+ }
265
+ try {
266
+ remoteDoc = await documentLoader(l.href, { signal: options.signal });
267
+ break;
268
+ } catch (error) {
269
+ logger.debug("Failed to fetch remote document:\n{error}", { error });
270
+ continue;
271
+ }
272
+ }
273
+ }
274
+ if (remoteDoc == null) return null;
275
+ const snapshot = options.verifyPortableObject == null ? null : createSnapshotContextLoader(options.contextLoader ?? getDocumentLoader({ userAgent: options.userAgent }));
276
+ try {
277
+ let object;
278
+ let documentUrl;
279
+ try {
280
+ documentUrl = parseIri(remoteDoc.documentUrl);
281
+ object = await Object$1.fromJsonLd(remoteDoc.document, {
282
+ documentLoader,
283
+ contextLoader: snapshot?.loader ?? options.contextLoader,
284
+ tracerProvider: options.tracerProvider,
285
+ verifyPortableObject: options.verifyPortableObject,
286
+ baseUrl: documentUrl
287
+ });
288
+ } catch (error) {
289
+ if (error instanceof TypeError) {
290
+ logger.debug("Failed to parse JSON-LD document: {error}\n{document}", {
291
+ ...remoteDoc,
292
+ error
293
+ });
294
+ return null;
295
+ }
296
+ throw error;
297
+ }
298
+ if (snapshot != null) {
299
+ const claim = getPortableResponseClaim(remoteDoc.documentUrl, object.id);
300
+ if (claim === null) {
301
+ logger.debug("Refusing the document {documentUrl}, as it claims to be a portable object with a malformed compatible identifier.", { documentUrl: remoteDoc.documentUrl });
302
+ return null;
303
+ } else if (claim != null) {
304
+ const explicit = getExplicitGateways(portable, claim.id);
305
+ return await dereference(portable, claim.id, claim.inferredGateways, explicit, {
306
+ response: remoteDoc,
307
+ contextLoader: snapshot.loader
308
+ });
309
+ }
310
+ }
311
+ if (object.id != null && (options.crossOrigin !== "trust" || isPortableIri(object.id)) && !haveSameIriOrigin(object.id, documentUrl) && !haveSameFe34Origin(object.id, documentUrl)) {
312
+ if (options.crossOrigin === "throw") throw new Error(`The object's @id (${object.id.href}) has a different origin than the document URL (${remoteDoc.documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to "trust".`);
313
+ logger.warn("The object's @id ({objectId}) has a different origin than the document URL ({documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to \"trust\".", {
314
+ ...remoteDoc,
315
+ objectId: object.id.href
316
+ });
317
+ return null;
318
+ }
319
+ return object;
320
+ } finally {
321
+ snapshot?.release();
322
+ }
323
+ }
324
+ const PORTABLE_IRI_PATTERN = /^ap(?:\+ef61)?:/i;
325
+ function getExplicitGateways(lookup, id) {
326
+ if (lookup.options.gateways == null) return void 0;
327
+ return lookup.explicitGateways ??= getPortableGatewayCandidates(id, lookup.options.gateways);
328
+ }
329
+ /**
330
+ * Parses a raw portable IRI. URL parsing normalizes dot segments in the
331
+ * opaque path, which would make the parsed IRI identify another portable
332
+ * object, so such IRIs are refused.
333
+ */
334
+ function parsePortableCandidate(iri) {
335
+ try {
336
+ const parsed = parseIri(iri);
337
+ if (canonicalizePortableUri(iri) !== canonicalizePortableUri(formatIri(parsed))) {
338
+ logger.debug("Refusing to look up the portable IRI {iri}, as its path cannot be represented without changing the identified object.", { iri });
339
+ return null;
340
+ }
341
+ return parsed;
342
+ } catch (error) {
343
+ if (error instanceof TypeError) {
344
+ logger.debug("Invalid portable IRI {iri}: {error}", {
345
+ iri,
346
+ error
347
+ });
348
+ return null;
349
+ }
350
+ throw error;
351
+ }
352
+ }
353
+ /**
354
+ * Recognizes an FEP-ef61 compatible identifier to look up as a portable
355
+ * object.
356
+ * @returns `undefined` if the URL should be fetched as an ordinary HTTP(S)
357
+ * URL, i.e., it is not a compatible identifier, or the
358
+ * `verifyPortableObject` option is not given; `null` if it is
359
+ * a malformed compatible identifier; otherwise, the portable ID and
360
+ * the gateway to ask.
361
+ */
362
+ function getCompatibleCandidate(href, options) {
363
+ if (options.verifyPortableObject == null) return void 0;
364
+ return parseCompatibleEf61Reference(href);
365
+ }
366
+ /**
367
+ * Gets the origin of the WebFinger server that {@link lookupWebFinger} asks
368
+ * for the identifier, which is also the first gateway of a portable actor.
369
+ */
370
+ function getWebFingerGateway(identifier) {
371
+ let url;
372
+ if (identifier.protocol === "acct:") {
373
+ const host = extractHandleHost(identifier.pathname);
374
+ if (host == null) return void 0;
375
+ url = new URL(`https://${host}/`);
376
+ } else if (identifier.protocol === "http:" || identifier.protocol === "https:") url = new URL(identifier.origin);
377
+ else return;
378
+ return url.username === "" && url.password === "" ? url : void 0;
379
+ }
380
+ /**
381
+ * Looks up a portable object through FEP-ef61 gateways.
382
+ * @param inferredGateways Gateways inferred from a compatible identifier or
383
+ * WebFinger. If omitted, location hints in the IRI
384
+ * are used unless explicit gateways were given.
385
+ */
386
+ async function lookupPortableObject(lookup, id, inferredGateways) {
387
+ const { options } = lookup;
388
+ const iri = formatIri(id);
389
+ const explicitGateways = getExplicitGateways(lookup, id);
390
+ if (options.verifyPortableObject == null) {
391
+ logger.debug("Cannot look up the portable object {iri}, as the verifyPortableObject option is not given.", { iri });
392
+ return null;
393
+ }
394
+ const canonicalId = canonicalizePortableUri(iri);
395
+ const inferred = inferredGateways ?? (explicitGateways == null ? getPortableGatewayCandidates(id) : []);
396
+ const candidates = [...inferred];
397
+ for (const gateway of explicitGateways ?? []) if (!candidates.some((candidate) => candidate.href === gateway.href)) candidates.push(gateway);
398
+ if (candidates.length < 1 && inferredGateways == null) {
399
+ const key = `${canonicalId} `;
400
+ if (lookup.remaining < 1 || lookup.attempted.has(key)) return null;
401
+ lookup.attempted.add(key);
402
+ lookup.remaining--;
403
+ return await dereference(lookup, id, [], explicitGateways == null ? void 0 : []);
404
+ }
405
+ const selected = [];
406
+ for (const gateway of candidates) {
407
+ if (selected.length >= lookup.remaining) break;
408
+ const key = `${canonicalId} ${gateway.href}`;
409
+ if (lookup.attempted.has(key)) continue;
410
+ lookup.attempted.add(key);
411
+ selected.push(gateway);
412
+ }
413
+ if (selected.length < 1) return null;
414
+ lookup.remaining -= selected.length;
415
+ const selectedInferred = selected.filter((candidate) => inferred.some((gateway) => gateway.href === candidate.href));
416
+ const selectedExplicit = explicitGateways?.filter((gateway) => selected.some((candidate) => candidate.href === gateway.href));
417
+ return await dereference(lookup, id, selectedInferred, selectedExplicit);
418
+ }
419
+ async function dereference({ options, documentLoader }, id, inferredGateways, gateways, extra = {}) {
420
+ const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
421
+ try {
422
+ return await dereferencePortableIri(id, {
423
+ documentLoader,
424
+ contextLoader: extra.contextLoader ?? options.contextLoader ?? getDocumentLoader({ userAgent: options.userAgent }),
425
+ tracerProvider,
426
+ inferredGateways,
427
+ ...gateways == null ? {} : { gateways },
428
+ response: extra.response,
429
+ verifyPortableObject: options.verifyPortableObject,
430
+ crossOrigin: options.crossOrigin === "throw" ? "throw" : "ignore",
431
+ signal: options.signal,
432
+ parse: (document, { contextLoader, baseUrl }) => Object$1.fromJsonLd(document, {
433
+ documentLoader,
434
+ contextLoader,
435
+ tracerProvider,
436
+ verifyPortableObject: options.verifyPortableObject,
437
+ baseUrl
438
+ })
439
+ });
440
+ } catch (error) {
441
+ if (error instanceof PortableObjectRejectedError) throw error;
442
+ logger.debug("Failed to look up the portable object {iri}:\n{error}", {
443
+ iri: formatIri(id),
444
+ error
445
+ });
446
+ return null;
447
+ }
448
+ }
449
+ /**
450
+ * Traverses a collection, yielding each item in the collection.
451
+ * If the collection is paginated, it will fetch the next page
452
+ * automatically.
453
+ *
454
+ * @example
455
+ * ``` typescript
456
+ * const collection = await lookupObject(collectionUrl);
457
+ * if (collection instanceof Collection) {
458
+ * for await (const item of traverseCollection(collection)) {
459
+ * console.log(item.id?.href);
460
+ * }
461
+ * }
462
+ * ```
463
+ *
464
+ * @param collection The collection to traverse.
465
+ * @param options Options for traversing the collection.
466
+ * @returns An async iterable of each item in the collection.
467
+ * @since 1.1.0
468
+ */
469
+ async function* traverseCollection(collection, options = {}) {
470
+ const interval = Temporal.Duration.from(options.interval ?? { seconds: 0 }).total("millisecond");
471
+ let page = await collection.getFirst(options);
472
+ if (page == null) for await (const item of collection.getItems(options)) yield item;
473
+ else while (page != null) {
474
+ for await (const item of page.getItems(options)) yield item;
475
+ if (interval > 0) await delay(interval);
476
+ page = await page.getNext(options);
477
+ }
478
+ }
479
+ //#endregion
480
+ export { traverseCollection as n, lookupObject as t };