@ar.io/sdk 3.12.0-beta.6 → 3.12.0-beta.7

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.
@@ -13,8 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import EventEmitter from 'node:events';
17
- import { PassThrough, Readable } from 'node:stream';
16
+ import { EventEmitter } from 'eventemitter3';
18
17
  import { base32 } from 'rfc4648';
19
18
  import { ARIO } from '../io.js';
20
19
  import { Logger } from '../logger.js';
@@ -64,21 +63,19 @@ export const resolveWayfinderUrl = ({ originalUrl, selectedGateway, logger, }) =
64
63
  return new URL(rest.length > 0 ? rest.join('/') : '', arnsUrl);
65
64
  }
66
65
  // TODO: support .eth addresses
67
- // TODO: "gasless" routing via DNS TXT records (e.g. ar://gatewaypie.com -> TXT record lookup for TX ID and redirect to that gateway)
66
+ // TODO: "gasless" routing via DNS TXT records
68
67
  }
69
68
  logger?.debug('No wayfinder routing protocol applied', {
70
69
  originalUrl,
71
70
  });
72
- // return the original url if it's not a wayfinder url (allows you to use the wayfinder client with non-wayfinder urls)
71
+ // return the original url if it's not a wayfinder url
73
72
  return new URL(originalUrl);
74
73
  };
75
74
  export class WayfinderEmitter extends EventEmitter {
76
- constructor({ onVerificationPassed, onVerificationFailed, onVerificationProgress,
77
- // TODO: continue this pattern for all events
78
- } = {}) {
75
+ constructor({ onVerificationSucceeded, onVerificationFailed, onVerificationProgress, } = {}) {
79
76
  super();
80
- if (onVerificationPassed) {
81
- this.on('verification-succeeded', onVerificationPassed);
77
+ if (onVerificationSucceeded) {
78
+ this.on('verification-succeeded', onVerificationSucceeded);
82
79
  }
83
80
  if (onVerificationFailed) {
84
81
  this.on('verification-failed', onVerificationFailed);
@@ -87,83 +84,17 @@ export class WayfinderEmitter extends EventEmitter {
87
84
  this.on('verification-progress', onVerificationProgress);
88
85
  }
89
86
  }
90
- emit(event, payload) {
91
- return super.emit(event, payload);
92
- }
93
- on(event, listener) {
94
- return super.on(event, listener);
95
- }
96
87
  }
97
- export function tapAndVerifyStream({ originalStream, contentLength, verifyData, txId, emitter, strict = false, }) {
98
- // taps node streams
99
- if (originalStream instanceof Readable &&
100
- typeof originalStream.pipe === 'function') {
101
- const tappedClientStream = new PassThrough();
102
- const streamToVerify = new PassThrough();
103
- // kick off the verification promise, this will be awaited when the original stream ends
104
- const verificationPromise = verifyData({
105
- data: streamToVerify,
106
- txId,
107
- });
108
- let bytesProcessed = 0;
109
- // pipe the original stream to the verifier and the client stream
110
- originalStream.on('data', (chunk) => {
111
- streamToVerify.write(chunk);
112
- tappedClientStream.write(chunk);
113
- bytesProcessed += chunk.length;
114
- // only emit if contentLength is not 0
115
- if (contentLength !== 0) {
116
- emitter?.emit('verification-progress', {
117
- txId,
118
- totalBytes: contentLength,
119
- processedBytes: bytesProcessed,
120
- });
121
- }
122
- });
123
- originalStream.on('end', async () => {
124
- streamToVerify.end(); // triggers verifier completion and completes the verification promise
125
- if (strict) {
126
- // in strict mode, we wait for verification to complete before ending the client stream
127
- try {
128
- await verificationPromise;
129
- emitter?.emit('verification-succeeded', { txId });
130
- tappedClientStream.end();
131
- }
132
- catch (error) {
133
- emitter?.emit('verification-failed', { error, txId });
134
- // In strict mode, destroy the client stream with the error
135
- tappedClientStream.destroy(new Error('Verification failed', { cause: error }));
136
- }
137
- }
138
- else {
139
- // in non-strict mode, we end the client stream immediately and handle verification asynchronously
140
- tappedClientStream.end();
141
- // trigger the verification promise and emit events for the result
142
- verificationPromise
143
- .then(() => {
144
- emitter?.emit('verification-succeeded', { txId });
145
- })
146
- .catch((error) => {
147
- emitter?.emit('verification-failed', { error, txId });
148
- });
149
- }
150
- });
151
- originalStream.on('error', (err) => {
152
- // emit the verification failed event
153
- emitter?.emit('verification-failed', {
154
- error: err,
155
- txId,
156
- });
157
- // destroy both streams and propagate the original stream error
158
- streamToVerify.destroy(err);
159
- tappedClientStream.destroy(err);
160
- });
161
- // send the stream to the verify function and if it errors end the client stream
162
- return tappedClientStream;
163
- }
164
- // taps web readable streams
88
+ export function tapAndVerifyReadableStream({ originalStream, contentLength, verifyData, txId, emitter, strict = false, }) {
165
89
  if (originalStream instanceof ReadableStream &&
166
90
  typeof originalStream.tee === 'function') {
91
+ /**
92
+ * NOTE: tee requires the streams both streams to be consumed, so we need to make sure we consume the client branch
93
+ * by the caller. This means when `request` is called, the client stream must be consumed by the caller via await request.text()
94
+ * for verification to complete.
95
+ *
96
+ * It is feasible to make the verification stream not to depend on the client branch being consumed, should the DX not be obvious.
97
+ */
167
98
  const [verifyBranch, clientBranch] = originalStream.tee();
168
99
  // setup our promise to verify the data
169
100
  const verificationPromise = verifyData({
@@ -184,10 +115,8 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
184
115
  controller.close();
185
116
  }
186
117
  catch (err) {
187
- emitter?.emit('verification-failed', {
188
- txId,
189
- error: err,
190
- });
118
+ // emit the verification failed event
119
+ emitter?.emit('verification-failed', err);
191
120
  // In strict mode, we report the error to the client stream
192
121
  controller.error(new Error('Verification failed', { cause: err }));
193
122
  }
@@ -199,10 +128,7 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
199
128
  emitter?.emit('verification-succeeded', { txId });
200
129
  })
201
130
  .catch((error) => {
202
- emitter?.emit('verification-failed', {
203
- txId,
204
- error,
205
- });
131
+ emitter?.emit('verification-failed', error);
206
132
  });
207
133
  // in non-strict mode, we close the controller immediately and handle verification asynchronously
208
134
  controller.close();
@@ -230,8 +156,6 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
230
156
  },
231
157
  }),
232
158
  });
233
- // note: we don't block or throw errors here even in strict mode
234
- // since the stream is already being cancelled by the client
235
159
  },
236
160
  });
237
161
  return clientStreamWithVerification;
@@ -258,146 +182,128 @@ export function sandboxFromId(id) {
258
182
  * @returns a wrapped fetch function that supports ar:// protocol and always returns Response
259
183
  */
260
184
  export const createWayfinderClient = ({ resolveUrl, verifyData, selectGateway, emitter = new WayfinderEmitter(), logger, strict = false, }) => {
261
- // Create a function that will handle the redirection logic for ar:// URLs
262
- const wayfinderRedirect = async (url, init) => {
263
- // If the url is not a string or URL (e.g., it's a Request object), extract the URL from it
264
- const originalUrl = url instanceof Request ? url.url : url.toString();
265
- // If it's not an ar:// URL, pass it directly to fetch
266
- if (!originalUrl.startsWith('ar://')) {
267
- logger?.debug('Not a wayfinder URL, passing to fetch directly', {
268
- originalUrl,
185
+ return async (url, init) => {
186
+ if (typeof url !== 'string') {
187
+ logger?.debug('URL is not a string, skipping routing', {
188
+ url,
269
189
  });
270
190
  emitter?.emit('routing-skipped', {
271
- originalUrl,
191
+ originalUrl: JSON.stringify(url),
272
192
  });
273
- // we don't do anything special with non-ar:// urls, just pass them through
274
193
  return fetch(url, init);
275
194
  }
276
- // Start the routing process
277
195
  emitter?.emit('routing-started', {
278
- originalUrl,
196
+ originalUrl: url.toString(),
279
197
  });
280
- // Retry logic for gateway selection
281
198
  const maxRetries = 3;
282
199
  const retryDelay = 1000;
283
200
  for (let i = 0; i < maxRetries; i++) {
284
201
  try {
285
202
  // select the target gateway
286
- // TODO: we may want to provide the `path` to select gateway so the HEAD checks in routers check the existence of the actual path/request
287
203
  const selectedGateway = await selectGateway();
288
204
  logger?.debug('Selected gateway', {
289
- originalUrl,
205
+ originalUrl: url,
290
206
  selectedGateway: selectedGateway.toString(),
291
207
  });
292
208
  // route the request to the target gateway
293
209
  const redirectUrl = resolveUrl({
294
- originalUrl,
210
+ originalUrl: url,
295
211
  selectedGateway,
296
212
  logger,
297
213
  });
298
214
  emitter?.emit('routing-succeeded', {
299
- originalUrl,
215
+ originalUrl: url,
300
216
  selectedGateway: selectedGateway.toString(),
301
217
  redirectUrl: redirectUrl.toString(),
302
218
  });
303
219
  logger?.debug(`Redirecting request`, {
304
- originalUrl,
220
+ originalUrl: url,
305
221
  redirectUrl: redirectUrl.toString(),
306
222
  });
307
- // Make the request to the target gateway
223
+ // make the request to the target gateway using the redirect url
308
224
  const response = await fetch(redirectUrl.toString(), {
309
- // follow redirects as gateways use sandboxing on /txId requests
225
+ // enforce CORS given we're likely going to a different origin, but always allow the client to override
310
226
  redirect: 'follow',
311
227
  mode: 'cors',
312
- // allow requestor to override and any additional request configuration
313
228
  ...init,
314
229
  });
315
- // TODO: update any caching we use for the request and gateway response
316
230
  logger?.debug(`Successfully routed request to gateway`, {
317
231
  redirectUrl: redirectUrl.toString(),
318
- originalUrl,
319
- });
320
- // return the response right away if no redirect was made
321
- if (redirectUrl.toString() === originalUrl) {
322
- return response;
323
- }
324
- // return the response right away if no verification is needed or if there is no body
325
- if (!verifyData) {
326
- return response;
327
- }
328
- // the txId is either in the response headers or the path of the request as the first parameter
329
- const txId = response.headers.get('x-arns-resolved-id') ??
330
- redirectUrl.pathname.split('/')[1];
331
- const contentLength = +(response.headers.get('content-length') ?? 0);
332
- if (!txIdRegex.test(txId)) {
333
- // No transaction ID found, skip verification
334
- logger?.debug('No transaction ID found, skipping verification', {
335
- redirectUrl: redirectUrl.toString(),
336
- originalUrl,
337
- });
338
- emitter?.emit('verification-skipped', {
339
- originalUrl,
340
- });
341
- return response;
342
- }
343
- emitter?.emit('identified-transaction-id', {
344
- originalUrl,
345
- selectedGateway: redirectUrl.toString(),
346
- txId,
232
+ originalUrl: url.toString(),
347
233
  });
348
- if (!response.body) {
349
- logger?.debug('No body, skipping verification', {
350
- redirectUrl: redirectUrl.toString(),
351
- originalUrl,
352
- });
353
- emitter?.emit('verification-skipped', {
354
- originalUrl,
355
- });
356
- return response;
234
+ // only verify data if the redirect url is different from the original url
235
+ if (redirectUrl.toString() !== url.toString()) {
236
+ if (verifyData) {
237
+ const headers = response.headers;
238
+ // transaction id is either in the response headers or the path of the request as the first parameter
239
+ const txId = headers.get('x-arns-resolved-id') ??
240
+ redirectUrl.pathname.split('/')[1];
241
+ const contentLength = +(headers.get('content-length') ?? 0);
242
+ if (!txIdRegex.test(txId)) {
243
+ // no transaction id found, skip verification
244
+ logger?.debug('No transaction id found, skipping verification', {
245
+ redirectUrl: redirectUrl.toString(),
246
+ originalUrl: url,
247
+ });
248
+ emitter?.emit('verification-skipped', {
249
+ originalUrl: url,
250
+ });
251
+ return response;
252
+ }
253
+ emitter?.emit('identified-transaction-id', {
254
+ originalUrl: url,
255
+ selectedGateway: redirectUrl.toString(),
256
+ txId,
257
+ });
258
+ // Check if the response has a body
259
+ if (response.body) {
260
+ const newClientStream = tapAndVerifyReadableStream({
261
+ originalStream: response.body,
262
+ contentLength,
263
+ verifyData,
264
+ txId,
265
+ emitter,
266
+ strict,
267
+ });
268
+ return new Response(newClientStream, {
269
+ status: response.status,
270
+ statusText: response.statusText,
271
+ headers: response.headers,
272
+ });
273
+ }
274
+ else {
275
+ // No response body to verify, skip verification
276
+ logger?.debug('No response body to verify', {
277
+ redirectUrl: redirectUrl.toString(),
278
+ originalUrl: url,
279
+ txId,
280
+ });
281
+ return response;
282
+ }
283
+ }
357
284
  }
358
- const verifiedStream = tapAndVerifyStream({
359
- originalStream: response.body,
360
- contentLength,
361
- verifyData,
362
- txId,
363
- emitter,
364
- strict,
365
- });
366
- // wrap the response with the verified stream
367
- return new Response(verifiedStream, {
368
- status: response.status,
369
- statusText: response.statusText,
370
- // TODO: we could add identified transaction id to the headers here, but it would be changing information from the original response
371
- headers: response.headers,
372
- });
285
+ return response;
373
286
  }
374
287
  catch (error) {
375
288
  logger?.debug('Failed to route request', {
376
289
  error: error.message,
377
290
  stack: error.stack,
378
- originalUrl,
291
+ originalUrl: url,
379
292
  attempt: i + 1,
380
293
  maxRetries,
381
294
  });
382
295
  if (i < maxRetries - 1) {
383
296
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
384
297
  }
385
- else {
386
- emitter?.emit('routing-failed', {
387
- originalUrl,
388
- error,
389
- });
390
- }
391
298
  }
392
299
  }
393
300
  throw new Error('Failed to route request after max retries', {
394
301
  cause: {
395
- originalUrl,
302
+ originalUrl: url,
396
303
  maxRetries,
397
304
  },
398
305
  });
399
306
  };
400
- return wayfinderRedirect;
401
307
  };
402
308
  /**
403
309
  * The main class for the wayfinder
@@ -548,7 +454,7 @@ export class Wayfinder {
548
454
  }),
549
455
  }),
550
456
  }), events = {
551
- onVerificationPassed: (event) => {
457
+ onVerificationSucceeded: (event) => {
552
458
  logger.debug('Verification passed!', event);
553
459
  },
554
460
  onVerificationFailed: (event) => {
@@ -557,7 +463,7 @@ export class Wayfinder {
557
463
  onVerificationProgress: (event) => {
558
464
  logger.debug('Verification progress!', event);
559
465
  },
560
- }, strict = false, }) {
466
+ }, strict = false, } = {}) {
561
467
  this.routingStrategy = routingStrategy;
562
468
  this.gatewaysProvider = gatewaysProvider;
563
469
  this.emitter = new WayfinderEmitter(events);
@@ -13,38 +13,85 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ import Arweave from 'arweave';
17
+ import { MAX_CHUNK_SIZE, MIN_CHUNK_SIZE, buildLayers, generateLeaves, } from 'arweave/node/lib/merkle.js';
16
18
  import { createHash } from 'crypto';
17
19
  import { toB64Url } from './base64.js';
18
- export const hashReadableToB64Url = (stream, algorithm = 'sha256') => {
19
- return new Promise((resolve, reject) => {
20
- const hash = createHash(algorithm);
21
- stream.on('data', (chunk) => hash.update(chunk));
22
- stream.on('end', () => resolve(toB64Url(hash.digest())));
23
- stream.on('error', (err) => reject(err));
24
- });
20
+ export const isAsyncIterable = (x) => {
21
+ return x && typeof x[Symbol.asyncIterator] === 'function';
25
22
  };
26
- export const hashReadableStreamToB64Url = (stream, algorithm = 'sha256') => {
27
- return new Promise((resolve, reject) => {
28
- const hash = createHash(algorithm);
23
+ export const isReadableStream = (x) => {
24
+ return x && typeof x.getReader === 'function';
25
+ };
26
+ // convert ReadableStream to async iterable
27
+ export const readableStreamToAsyncIterable = (stream) => ({
28
+ async *[Symbol.asyncIterator]() {
29
29
  const reader = stream.getReader();
30
- const read = async () => {
31
- try {
30
+ try {
31
+ while (true) {
32
32
  const { done, value } = await reader.read();
33
- if (done) {
34
- resolve(toB64Url(hash.digest()));
35
- }
36
- else {
37
- hash.update(value);
38
- read();
39
- }
40
- }
41
- catch (err) {
42
- reject(err);
33
+ if (done)
34
+ break;
35
+ if (value !== undefined)
36
+ yield value;
43
37
  }
44
- };
45
- read().catch(reject);
46
- });
38
+ }
39
+ finally {
40
+ reader.releaseLock();
41
+ }
42
+ },
43
+ });
44
+ export const hashDataStreamToB64Url = async (stream, algorithm = 'sha256') => {
45
+ const asyncIterable = isAsyncIterable(stream)
46
+ ? stream
47
+ : readableStreamToAsyncIterable(stream);
48
+ const hash = createHash(algorithm);
49
+ for await (const chunk of asyncIterable) {
50
+ hash.update(chunk);
51
+ }
52
+ return toB64Url(hash.digest());
47
53
  };
48
- export const hashBufferToB64Url = (buffer, algorithm = 'sha256') => {
49
- return toB64Url(createHash(algorithm).update(buffer).digest());
54
+ export const convertDataStreamToDataRoot = async ({ dataStream, }) => {
55
+ const chunks = [];
56
+ let leftover = new Uint8Array(0);
57
+ let cursor = 0;
58
+ const asyncIterable = isAsyncIterable(dataStream)
59
+ ? dataStream
60
+ : readableStreamToAsyncIterable(dataStream);
61
+ for await (const data of asyncIterable) {
62
+ const inputChunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
63
+ const combined = new Uint8Array(leftover.length + inputChunk.length);
64
+ combined.set(leftover, 0);
65
+ combined.set(inputChunk, leftover.length);
66
+ let startIndex = 0;
67
+ while (combined.length - startIndex >= MAX_CHUNK_SIZE) {
68
+ let chunkSize = MAX_CHUNK_SIZE;
69
+ const remainderAfterThis = combined.length - startIndex - MAX_CHUNK_SIZE;
70
+ if (remainderAfterThis > 0 && remainderAfterThis < MIN_CHUNK_SIZE) {
71
+ chunkSize = Math.ceil((combined.length - startIndex) / 2);
72
+ }
73
+ const chunkData = combined.slice(startIndex, startIndex + chunkSize);
74
+ const dataHash = await Arweave.crypto.hash(chunkData);
75
+ chunks.push({
76
+ dataHash,
77
+ minByteRange: cursor,
78
+ maxByteRange: cursor + chunkSize,
79
+ });
80
+ cursor += chunkSize;
81
+ startIndex += chunkSize;
82
+ }
83
+ leftover = combined.slice(startIndex);
84
+ }
85
+ if (leftover.length > 0) {
86
+ // TODO: ensure a web friendly crypto hash function is used in web
87
+ const dataHash = await Arweave.crypto.hash(leftover);
88
+ chunks.push({
89
+ dataHash,
90
+ minByteRange: cursor,
91
+ maxByteRange: cursor + leftover.length,
92
+ });
93
+ }
94
+ const leaves = await generateLeaves(chunks);
95
+ const root = await buildLayers(leaves);
96
+ return toB64Url(Buffer.from(root.id));
50
97
  };
@@ -14,4 +14,4 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  // AUTOMATICALLY GENERATED FILE - DO NOT TOUCH
17
- export const version = '3.12.0-beta.6';
17
+ export const version = '3.12.0-beta.7';
@@ -1,10 +1,6 @@
1
- import { Readable } from 'node:stream';
2
- import { DataRootProvider, DataVerificationStrategy } from '../../../../types/wayfinder.js';
3
- export declare function convertBufferToDataRoot({ buffer, }: {
4
- buffer: Buffer;
5
- }): Promise<string>;
6
- export declare const convertReadableToDataRoot: <T extends AsyncIterable<Uint8Array>>({ iterable, }: {
7
- iterable: T;
1
+ import { DataRootProvider, DataStream, DataVerificationStrategy } from '../../../../types/wayfinder.js';
2
+ export declare const convertDataStreamToDataRoot: ({ dataStream, }: {
3
+ dataStream: DataStream;
8
4
  }) => Promise<string>;
9
5
  export declare class DataRootVerificationStrategy implements DataVerificationStrategy {
10
6
  private readonly trustedDataRootProvider;
@@ -12,7 +8,7 @@ export declare class DataRootVerificationStrategy implements DataVerificationStr
12
8
  trustedDataRootProvider: DataRootProvider;
13
9
  });
14
10
  verifyData({ data, txId, }: {
15
- data: Buffer | Readable | ReadableStream;
11
+ data: DataStream;
16
12
  txId: string;
17
13
  }): Promise<void>;
18
14
  }
@@ -13,15 +13,14 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { Readable } from 'node:stream';
17
- import { DataHashProvider, DataVerificationStrategy } from '../../../../types/wayfinder.js';
16
+ import { DataHashProvider, DataStream, DataVerificationStrategy } from '../../../../types/wayfinder.js';
18
17
  export declare class HashVerificationStrategy implements DataVerificationStrategy {
19
18
  private readonly trustedHashProvider;
20
19
  constructor({ trustedHashProvider, }: {
21
20
  trustedHashProvider: DataHashProvider;
22
21
  });
23
22
  verifyData({ data, txId, }: {
24
- data: Buffer | Readable | ReadableStream;
23
+ data: DataStream;
25
24
  txId: string;
26
25
  }): Promise<void>;
27
26
  }