@ar.io/sdk 3.12.0-beta.5 → 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,8 @@
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';
17
+ import { base32 } from 'rfc4648';
18
18
  import { ARIO } from '../io.js';
19
19
  import { Logger } from '../logger.js';
20
20
  import { NetworkGatewaysProvider } from './gateways/network.js';
@@ -50,7 +50,8 @@ export const resolveWayfinderUrl = ({ originalUrl, selectedGateway, logger, }) =
50
50
  const [firstPart, ...rest] = path.split('/');
51
51
  // TODO: this breaks 43 character named arns names - we should check a a local name cache list before resolving raw transaction ids
52
52
  if (txIdRegex.test(firstPart)) {
53
- return new URL(`${firstPart}${rest.length > 0 ? '/' + rest.join('/') : ''}`, selectedGateway);
53
+ const sandbox = sandboxFromId(firstPart);
54
+ return new URL(`${firstPart}${rest.length > 0 ? '/' + rest.join('/') : ''}`, `${selectedGateway.protocol}//${sandbox}.${selectedGateway.hostname}`);
54
55
  }
55
56
  if (arnsRegex.test(firstPart)) {
56
57
  // TODO: tests to ensure arns names support query params and paths
@@ -62,21 +63,19 @@ export const resolveWayfinderUrl = ({ originalUrl, selectedGateway, logger, }) =
62
63
  return new URL(rest.length > 0 ? rest.join('/') : '', arnsUrl);
63
64
  }
64
65
  // TODO: support .eth addresses
65
- // 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
66
67
  }
67
68
  logger?.debug('No wayfinder routing protocol applied', {
68
69
  originalUrl,
69
70
  });
70
- // 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
71
72
  return new URL(originalUrl);
72
73
  };
73
74
  export class WayfinderEmitter extends EventEmitter {
74
- constructor({ onVerificationPassed, onVerificationFailed, onVerificationProgress,
75
- // TODO: continue this pattern for all events
76
- } = {}) {
75
+ constructor({ onVerificationSucceeded, onVerificationFailed, onVerificationProgress, } = {}) {
77
76
  super();
78
- if (onVerificationPassed) {
79
- this.on('verification-succeeded', onVerificationPassed);
77
+ if (onVerificationSucceeded) {
78
+ this.on('verification-succeeded', onVerificationSucceeded);
80
79
  }
81
80
  if (onVerificationFailed) {
82
81
  this.on('verification-failed', onVerificationFailed);
@@ -85,83 +84,17 @@ export class WayfinderEmitter extends EventEmitter {
85
84
  this.on('verification-progress', onVerificationProgress);
86
85
  }
87
86
  }
88
- emit(event, payload) {
89
- return super.emit(event, payload);
90
- }
91
- on(event, listener) {
92
- return super.on(event, listener);
93
- }
94
87
  }
95
- export function tapAndVerifyStream({ originalStream, contentLength, verifyData, txId, emitter, strict = false, }) {
96
- // taps node streams
97
- if (originalStream instanceof Readable &&
98
- typeof originalStream.pipe === 'function') {
99
- const tappedClientStream = new PassThrough();
100
- const streamToVerify = new PassThrough();
101
- // kick off the verification promise, this will be awaited when the original stream ends
102
- const verificationPromise = verifyData({
103
- data: streamToVerify,
104
- txId,
105
- });
106
- let bytesProcessed = 0;
107
- // pipe the original stream to the verifier and the client stream
108
- originalStream.on('data', (chunk) => {
109
- streamToVerify.write(chunk);
110
- tappedClientStream.write(chunk);
111
- bytesProcessed += chunk.length;
112
- // only emit if contentLength is not 0
113
- if (contentLength !== 0) {
114
- emitter?.emit('verification-progress', {
115
- txId,
116
- totalBytes: contentLength,
117
- processedBytes: bytesProcessed,
118
- });
119
- }
120
- });
121
- originalStream.on('end', async () => {
122
- streamToVerify.end(); // triggers verifier completion and completes the verification promise
123
- if (strict) {
124
- // in strict mode, we wait for verification to complete before ending the client stream
125
- try {
126
- await verificationPromise;
127
- emitter?.emit('verification-succeeded', { txId });
128
- tappedClientStream.end();
129
- }
130
- catch (error) {
131
- emitter?.emit('verification-failed', { error, txId });
132
- // In strict mode, destroy the client stream with the error
133
- tappedClientStream.destroy(new Error('Verification failed', { cause: error }));
134
- }
135
- }
136
- else {
137
- // in non-strict mode, we end the client stream immediately and handle verification asynchronously
138
- tappedClientStream.end();
139
- // trigger the verification promise and emit events for the result
140
- verificationPromise
141
- .then(() => {
142
- emitter?.emit('verification-succeeded', { txId });
143
- })
144
- .catch((error) => {
145
- emitter?.emit('verification-failed', { error, txId });
146
- });
147
- }
148
- });
149
- originalStream.on('error', (err) => {
150
- // emit the verification failed event
151
- emitter?.emit('verification-failed', {
152
- error: err,
153
- txId,
154
- });
155
- // destroy both streams and propagate the original stream error
156
- streamToVerify.destroy(err);
157
- tappedClientStream.destroy(err);
158
- });
159
- // send the stream to the verify function and if it errors end the client stream
160
- return tappedClientStream;
161
- }
162
- // taps web readable streams
88
+ export function tapAndVerifyReadableStream({ originalStream, contentLength, verifyData, txId, emitter, strict = false, }) {
163
89
  if (originalStream instanceof ReadableStream &&
164
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
+ */
165
98
  const [verifyBranch, clientBranch] = originalStream.tee();
166
99
  // setup our promise to verify the data
167
100
  const verificationPromise = verifyData({
@@ -182,10 +115,8 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
182
115
  controller.close();
183
116
  }
184
117
  catch (err) {
185
- emitter?.emit('verification-failed', {
186
- txId,
187
- error: err,
188
- });
118
+ // emit the verification failed event
119
+ emitter?.emit('verification-failed', err);
189
120
  // In strict mode, we report the error to the client stream
190
121
  controller.error(new Error('Verification failed', { cause: err }));
191
122
  }
@@ -197,10 +128,7 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
197
128
  emitter?.emit('verification-succeeded', { txId });
198
129
  })
199
130
  .catch((error) => {
200
- emitter?.emit('verification-failed', {
201
- txId,
202
- error,
203
- });
131
+ emitter?.emit('verification-failed', error);
204
132
  });
205
133
  // in non-strict mode, we close the controller immediately and handle verification asynchronously
206
134
  controller.close();
@@ -228,14 +156,20 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
228
156
  },
229
157
  }),
230
158
  });
231
- // note: we don't block or throw errors here even in strict mode
232
- // since the stream is already being cancelled by the client
233
159
  },
234
160
  });
235
161
  return clientStreamWithVerification;
236
162
  }
237
163
  throw new Error('Unsupported body type for cloning');
238
164
  }
165
+ /**
166
+ * Gets the sandbox hash for a given transaction id
167
+ */
168
+ export function sandboxFromId(id) {
169
+ return base32
170
+ .stringify(Buffer.from(id, 'base64'), { pad: false })
171
+ .toLowerCase();
172
+ }
239
173
  /**
240
174
  * Creates a wrapped fetch function that supports ar:// protocol
241
175
  *
@@ -248,146 +182,128 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
248
182
  * @returns a wrapped fetch function that supports ar:// protocol and always returns Response
249
183
  */
250
184
  export const createWayfinderClient = ({ resolveUrl, verifyData, selectGateway, emitter = new WayfinderEmitter(), logger, strict = false, }) => {
251
- // Create a function that will handle the redirection logic for ar:// URLs
252
- const wayfinderRedirect = async (url, init) => {
253
- // If the url is not a string or URL (e.g., it's a Request object), extract the URL from it
254
- const originalUrl = url instanceof Request ? url.url : url.toString();
255
- // If it's not an ar:// URL, pass it directly to fetch
256
- if (!originalUrl.startsWith('ar://')) {
257
- logger?.debug('Not a wayfinder URL, passing to fetch directly', {
258
- originalUrl,
185
+ return async (url, init) => {
186
+ if (typeof url !== 'string') {
187
+ logger?.debug('URL is not a string, skipping routing', {
188
+ url,
259
189
  });
260
190
  emitter?.emit('routing-skipped', {
261
- originalUrl,
191
+ originalUrl: JSON.stringify(url),
262
192
  });
263
- // we don't do anything special with non-ar:// urls, just pass them through
264
193
  return fetch(url, init);
265
194
  }
266
- // Start the routing process
267
195
  emitter?.emit('routing-started', {
268
- originalUrl,
196
+ originalUrl: url.toString(),
269
197
  });
270
- // Retry logic for gateway selection
271
198
  const maxRetries = 3;
272
199
  const retryDelay = 1000;
273
200
  for (let i = 0; i < maxRetries; i++) {
274
201
  try {
275
202
  // select the target gateway
276
- // 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
277
203
  const selectedGateway = await selectGateway();
278
204
  logger?.debug('Selected gateway', {
279
- originalUrl,
205
+ originalUrl: url,
280
206
  selectedGateway: selectedGateway.toString(),
281
207
  });
282
208
  // route the request to the target gateway
283
209
  const redirectUrl = resolveUrl({
284
- originalUrl,
210
+ originalUrl: url,
285
211
  selectedGateway,
286
212
  logger,
287
213
  });
288
214
  emitter?.emit('routing-succeeded', {
289
- originalUrl,
215
+ originalUrl: url,
290
216
  selectedGateway: selectedGateway.toString(),
291
217
  redirectUrl: redirectUrl.toString(),
292
218
  });
293
219
  logger?.debug(`Redirecting request`, {
294
- originalUrl,
220
+ originalUrl: url,
295
221
  redirectUrl: redirectUrl.toString(),
296
222
  });
297
- // Make the request to the target gateway
223
+ // make the request to the target gateway using the redirect url
298
224
  const response = await fetch(redirectUrl.toString(), {
299
- // 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
300
226
  redirect: 'follow',
301
227
  mode: 'cors',
302
- // allow requestor to override and any additional request configuration
303
228
  ...init,
304
229
  });
305
- // TODO: update any caching we use for the request and gateway response
306
230
  logger?.debug(`Successfully routed request to gateway`, {
307
231
  redirectUrl: redirectUrl.toString(),
308
- originalUrl,
232
+ originalUrl: url.toString(),
309
233
  });
310
- // return the response right away if no redirect was made
311
- if (redirectUrl.toString() === originalUrl) {
312
- return response;
313
- }
314
- // return the response right away if no verification is needed or if there is no body
315
- if (!verifyData) {
316
- return response;
317
- }
318
- // the txId is either in the response headers or the path of the request as the first parameter
319
- const txId = response.headers.get('x-arns-resolved-id') ??
320
- redirectUrl.pathname.split('/')[1];
321
- const contentLength = +(response.headers.get('content-length') ?? 0);
322
- if (!txIdRegex.test(txId)) {
323
- // No transaction ID found, skip verification
324
- logger?.debug('No transaction ID found, skipping verification', {
325
- redirectUrl: redirectUrl.toString(),
326
- originalUrl,
327
- });
328
- emitter?.emit('verification-skipped', {
329
- originalUrl,
330
- });
331
- return response;
332
- }
333
- emitter?.emit('identified-transaction-id', {
334
- originalUrl,
335
- selectedGateway: redirectUrl.toString(),
336
- txId,
337
- });
338
- if (!response.body) {
339
- logger?.debug('No body, skipping verification', {
340
- redirectUrl: redirectUrl.toString(),
341
- originalUrl,
342
- });
343
- emitter?.emit('verification-skipped', {
344
- originalUrl,
345
- });
346
- 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
+ }
347
284
  }
348
- const verifiedStream = tapAndVerifyStream({
349
- originalStream: response.body,
350
- contentLength,
351
- verifyData,
352
- txId,
353
- emitter,
354
- strict,
355
- });
356
- // wrap the response with the verified stream
357
- return new Response(verifiedStream, {
358
- status: response.status,
359
- statusText: response.statusText,
360
- // TODO: we could add identified transaction id to the headers here, but it would be changing information from the original response
361
- headers: response.headers,
362
- });
285
+ return response;
363
286
  }
364
287
  catch (error) {
365
288
  logger?.debug('Failed to route request', {
366
289
  error: error.message,
367
290
  stack: error.stack,
368
- originalUrl,
291
+ originalUrl: url,
369
292
  attempt: i + 1,
370
293
  maxRetries,
371
294
  });
372
295
  if (i < maxRetries - 1) {
373
296
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
374
297
  }
375
- else {
376
- emitter?.emit('routing-failed', {
377
- originalUrl,
378
- error,
379
- });
380
- }
381
298
  }
382
299
  }
383
300
  throw new Error('Failed to route request after max retries', {
384
301
  cause: {
385
- originalUrl,
302
+ originalUrl: url,
386
303
  maxRetries,
387
304
  },
388
305
  });
389
306
  };
390
- return wayfinderRedirect;
391
307
  };
392
308
  /**
393
309
  * The main class for the wayfinder
@@ -538,7 +454,7 @@ export class Wayfinder {
538
454
  }),
539
455
  }),
540
456
  }), events = {
541
- onVerificationPassed: (event) => {
457
+ onVerificationSucceeded: (event) => {
542
458
  logger.debug('Verification passed!', event);
543
459
  },
544
460
  onVerificationFailed: (event) => {
@@ -547,7 +463,7 @@ export class Wayfinder {
547
463
  onVerificationProgress: (event) => {
548
464
  logger.debug('Verification progress!', event);
549
465
  },
550
- }, strict = false, }) {
466
+ }, strict = false, } = {}) {
551
467
  this.routingStrategy = routingStrategy;
552
468
  this.gatewaysProvider = gatewaysProvider;
553
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.5';
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
  }