@ar.io/sdk 3.12.0-beta.3 → 3.12.0-beta.5

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.
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Wayfinder = exports.createWayfinderClient = exports.WayfinderEmitter = exports.resolveWayfinderUrl = exports.txIdRegex = exports.arnsRegex = void 0;
7
7
  exports.tapAndVerifyStream = tapAndVerifyStream;
8
- exports.wrapVerifiedResponse = wrapVerifiedResponse;
9
8
  /**
10
9
  * Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
11
10
  *
@@ -246,56 +245,44 @@ function tapAndVerifyStream({ originalStream, contentLength, verifyData, txId, e
246
245
  }
247
246
  throw new Error('Unsupported body type for cloning');
248
247
  }
249
- function wrapVerifiedResponse(original, newBody, txId) {
250
- // Clone headers (Header objects aren't serializable)
251
- const headers = new Headers();
252
- original.headers.forEach((value, key) => headers.set(key, value));
253
- // Create a new Response with the new body and cloned headers
254
- const wrapped = new Response(newBody, {
255
- status: original.status,
256
- statusText: original.statusText,
257
- headers,
258
- });
259
- // Attach txId for downstream tracking
260
- wrapped.txId = txId;
261
- wrapped.redirectedFrom = original.url;
262
- return wrapped;
263
- }
264
248
  /**
265
- * Creates a wrapped http client that supports ar:// protocol
249
+ * Creates a wrapped fetch function that supports ar:// protocol
266
250
  *
267
- * This function leverages a Proxy to intercept calls to the http client
268
- * and redirects them to the target gateway using the resolveUrl function url.
269
- * It also supports the http client methods like get(), post(), put(), delete(), etc.
251
+ * This function leverages a Proxy to intercept calls to fetch
252
+ * and redirects them to the target gateway using the resolveUrl function.
270
253
  *
271
- * Any URLs provided that are not wayfinder urls will be returned as is.
254
+ * Any URLs provided that are not wayfinder urls will be passed directly to fetch.
272
255
  *
273
- * @param httpClient - the http client to wrap (e.g. axios, fetch, got, etc.)
274
256
  * @param resolveUrl - the function to construct the redirect url for ar:// requests
275
- * @returns a wrapped http client that supports ar:// protocol
257
+ * @returns a wrapped fetch function that supports ar:// protocol and always returns Response
276
258
  */
277
- const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, selectGateway, emitter = new WayfinderEmitter(), logger, strict = false, }) => {
278
- const wayfinderRedirect = async (fn, rawArgs) => {
279
- // TODO: handle if first arg is not a string (i.e. just return the result of the function call)
280
- const [originalUrl, ...rest] = rawArgs;
281
- if (typeof originalUrl !== 'string') {
282
- logger?.debug('Original URL is not a string, skipping routing', {
259
+ const createWayfinderClient = ({ resolveUrl, verifyData, selectGateway, emitter = new WayfinderEmitter(), logger, strict = false, }) => {
260
+ // Create a function that will handle the redirection logic for ar:// URLs
261
+ const wayfinderRedirect = async (url, init) => {
262
+ // If the url is not a string or URL (e.g., it's a Request object), extract the URL from it
263
+ const originalUrl = url instanceof Request ? url.url : url.toString();
264
+ // If it's not an ar:// URL, pass it directly to fetch
265
+ if (!originalUrl.startsWith('ar://')) {
266
+ logger?.debug('Not a wayfinder URL, passing to fetch directly', {
283
267
  originalUrl,
284
268
  });
285
269
  emitter?.emit('routing-skipped', {
286
- originalUrl: JSON.stringify(originalUrl),
270
+ originalUrl,
287
271
  });
288
- return fn(...rawArgs);
272
+ // we don't do anything special with non-ar:// urls, just pass them through
273
+ return fetch(url, init);
289
274
  }
275
+ // Start the routing process
290
276
  emitter?.emit('routing-started', {
291
- originalUrl: originalUrl.toString(),
277
+ originalUrl,
292
278
  });
293
- // TODO: by default we will retry 3 times but this should be configurable and moved to a routing strategy
279
+ // Retry logic for gateway selection
294
280
  const maxRetries = 3;
295
281
  const retryDelay = 1000;
296
282
  for (let i = 0; i < maxRetries; i++) {
297
283
  try {
298
284
  // select the target gateway
285
+ // 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
299
286
  const selectedGateway = await selectGateway();
300
287
  logger?.debug('Selected gateway', {
301
288
  originalUrl,
@@ -316,172 +303,72 @@ const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, selectGatew
316
303
  originalUrl,
317
304
  redirectUrl: redirectUrl.toString(),
318
305
  });
319
- // make the request to the target gateway using the redirect url and http client
320
- const response = await fn(redirectUrl.toString(), ...rest);
321
- // TODO: trigger a routing event with the raw response object?
306
+ // Make the request to the target gateway
307
+ const response = await fetch(redirectUrl.toString(), {
308
+ // follow redirects as gateways use sandboxing on /txId requests
309
+ redirect: 'follow',
310
+ mode: 'cors',
311
+ // allow requestor to override and any additional request configuration
312
+ ...init,
313
+ });
314
+ // TODO: update any caching we use for the request and gateway response
322
315
  logger?.debug(`Successfully routed request to gateway`, {
323
316
  redirectUrl: redirectUrl.toString(),
324
- originalUrl: originalUrl.toString(),
317
+ originalUrl,
325
318
  });
326
- // only verify data if the redirect url is different from the original url
327
- if (response && redirectUrl.toString() !== originalUrl.toString()) {
328
- if (verifyData) {
329
- // if the headers do not have .get on them, we need to parse the headers manually
330
- const headers = new Headers();
331
- const headersObject = response.headers ?? {};
332
- if (headersObject instanceof Map) {
333
- for (const [key, value] of headersObject.entries()) {
334
- headers.set(key, value);
335
- }
336
- }
337
- else if (headersObject instanceof Headers) {
338
- for (const [key, value] of headersObject.entries()) {
339
- headers.set(key, value);
340
- }
341
- }
342
- else if (headersObject !== undefined &&
343
- typeof headersObject === 'object') {
344
- for (const [key, value] of Object.entries(headersObject)) {
345
- headers.set(key, value);
346
- }
347
- }
348
- else {
349
- throw new Error('Gateway did not return headers needed for verification', {
350
- cause: {
351
- redirectUrl: redirectUrl.toString(),
352
- originalUrl: originalUrl.toString(),
353
- },
354
- });
355
- }
356
- // transaction id is either in the response headers or the path of the request as the first parameter
357
- // TODO: we may want to move this parsing to be returned by the resolveUrl function depending on the redirect URL we've constructed
358
- const txId = headers.get('x-arns-resolved-id') ??
359
- redirectUrl.pathname.split('/')[1];
360
- // TODO: validate nodes return content length for all responses
361
- const contentLength = +(headers.get('content-length') ?? 0);
362
- if (!exports.txIdRegex.test(txId)) {
363
- // no transaction id found, skip verification
364
- logger?.debug('No transaction id found, skipping verification', {
365
- redirectUrl: redirectUrl.toString(),
366
- originalUrl,
367
- });
368
- emitter?.emit('verification-skipped', {
369
- originalUrl,
370
- });
371
- return response;
372
- }
373
- emitter?.emit('identified-transaction-id', {
374
- originalUrl,
375
- selectedGateway: redirectUrl.toString(),
376
- txId,
377
- });
378
- // parse out the key that contains the response body, we'll use it later when updating the response object
379
- const responseDataKey = response.body
380
- ? 'body'
381
- : response.data
382
- ? 'data'
383
- : undefined;
384
- if (responseDataKey === undefined) {
385
- throw new Error('No data body or data provided, skipping verification', {
386
- cause: {
387
- redirectUrl: redirectUrl.toString(),
388
- originalUrl: originalUrl.toString(),
389
- },
390
- });
391
- }
392
- const responseBody = response[responseDataKey];
393
- // TODO: determine if it is data item or L1 transaction, and tell the verifier accordingly, just drop in hit to graphql now
394
- if (txId === undefined) {
395
- throw new Error('Failed to parse data hash from response headers', {
396
- cause: {
397
- redirectUrl: redirectUrl.toString(),
398
- originalUrl: originalUrl.toString(),
399
- txId,
400
- },
401
- });
402
- }
403
- else if (responseBody === undefined) {
404
- throw new Error('No data body provided, skipping verification', {
405
- cause: {
406
- redirectUrl: redirectUrl.toString(),
407
- originalUrl: originalUrl.toString(),
408
- txId,
409
- },
410
- });
411
- }
412
- else {
413
- logger?.debug('Verifying data hash for txId', {
414
- redirectUrl: redirectUrl.toString(),
415
- originalUrl: originalUrl.toString(),
416
- txId,
417
- });
418
- if (responseBody instanceof ReadableStream ||
419
- responseBody instanceof node_stream_1.Readable) {
420
- const newClientStream = tapAndVerifyStream({
421
- originalStream: responseBody,
422
- contentLength,
423
- verifyData,
424
- txId,
425
- emitter,
426
- strict,
427
- });
428
- if (response instanceof Response) {
429
- // specific to fetch
430
- return wrapVerifiedResponse(response, newClientStream, txId);
431
- }
432
- else {
433
- // overwrite the response body with the new client stream
434
- response.txId = txId;
435
- response.body = newClientStream;
436
- return response;
437
- }
438
- }
439
- else {
440
- // TODO: content-application/json and it's smaller than 10mb
441
- // TODO: add tests and verify this works for all non-Readable/streamed responses
442
- if (strict) {
443
- // In strict mode, wait for verification before returning response
444
- try {
445
- await verifyData({
446
- data: responseBody,
447
- txId,
448
- });
449
- emitter?.emit('verification-succeeded', { txId });
450
- return response;
451
- }
452
- catch (error) {
453
- logger?.debug('Failed to verify data hash', {
454
- error,
455
- txId,
456
- });
457
- emitter?.emit('verification-failed', { txId, error });
458
- throw new Error('Verification failed', { cause: error });
459
- }
460
- }
461
- else {
462
- // In non-strict mode, perform verification in the background
463
- verifyData({
464
- data: responseBody,
465
- txId,
466
- })
467
- .then(() => {
468
- emitter?.emit('verification-succeeded', { txId });
469
- })
470
- .catch((error) => {
471
- logger?.debug('Failed to verify data hash', {
472
- error,
473
- txId,
474
- });
475
- emitter?.emit('verification-failed', { txId, error });
476
- });
477
- return response;
478
- }
479
- }
480
- }
481
- }
319
+ // return the response right away if no redirect was made
320
+ if (redirectUrl.toString() === originalUrl) {
321
+ return response;
322
+ }
323
+ // return the response right away if no verification is needed or if there is no body
324
+ if (!verifyData) {
325
+ return response;
326
+ }
327
+ // the txId is either in the response headers or the path of the request as the first parameter
328
+ const txId = response.headers.get('x-arns-resolved-id') ??
329
+ redirectUrl.pathname.split('/')[1];
330
+ const contentLength = +(response.headers.get('content-length') ?? 0);
331
+ if (!exports.txIdRegex.test(txId)) {
332
+ // No transaction ID found, skip verification
333
+ logger?.debug('No transaction ID found, skipping verification', {
334
+ redirectUrl: redirectUrl.toString(),
335
+ originalUrl,
336
+ });
337
+ emitter?.emit('verification-skipped', {
338
+ originalUrl,
339
+ });
340
+ return response;
341
+ }
342
+ emitter?.emit('identified-transaction-id', {
343
+ originalUrl,
344
+ selectedGateway: redirectUrl.toString(),
345
+ txId,
346
+ });
347
+ if (!response.body) {
348
+ logger?.debug('No body, skipping verification', {
349
+ redirectUrl: redirectUrl.toString(),
350
+ originalUrl,
351
+ });
352
+ emitter?.emit('verification-skipped', {
353
+ originalUrl,
354
+ });
355
+ return response;
482
356
  }
483
- // TODO: if strict - wait for verification to finish and succeed before returning the response
484
- return response;
357
+ const verifiedStream = tapAndVerifyStream({
358
+ originalStream: response.body,
359
+ contentLength,
360
+ verifyData,
361
+ txId,
362
+ emitter,
363
+ strict,
364
+ });
365
+ // wrap the response with the verified stream
366
+ return new Response(verifiedStream, {
367
+ status: response.status,
368
+ statusText: response.statusText,
369
+ // TODO: we could add identified transaction id to the headers here, but it would be changing information from the original response
370
+ headers: response.headers,
371
+ });
485
372
  }
486
373
  catch (error) {
487
374
  logger?.debug('Failed to route request', {
@@ -494,6 +381,12 @@ const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, selectGatew
494
381
  if (i < maxRetries - 1) {
495
382
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
496
383
  }
384
+ else {
385
+ emitter?.emit('routing-failed', {
386
+ originalUrl,
387
+ error,
388
+ });
389
+ }
497
390
  }
498
391
  }
499
392
  throw new Error('Failed to route request after max retries', {
@@ -503,38 +396,13 @@ const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, selectGatew
503
396
  },
504
397
  });
505
398
  };
506
- return new Proxy(httpClient, {
507
- // support direct calls: fetch('ar://…', options)
508
- // axios() or got()
509
- apply: (_target, _thisArg, argArray) => wayfinderRedirect(httpClient, argArray),
510
- // support http clients that use functions like `got.get`, `got.post`, `axios.get`, etc. while still using the wayfinder redirect function
511
- get: (target, prop, receiver) => {
512
- const value = Reflect.get(target, prop, receiver);
513
- if (typeof value === 'function') {
514
- return (...inner) => wayfinderRedirect(value.bind(target), inner);
515
- }
516
- return value; // numbers, objects, symbols pass through untouched
517
- },
518
- });
399
+ return wayfinderRedirect;
519
400
  };
520
401
  exports.createWayfinderClient = createWayfinderClient;
521
402
  /**
522
403
  * The main class for the wayfinder
523
- * @param router - the router to use for requests
524
- * @param httpClient - the http client to use for requests
525
- * @param blocklist - the blocklist of gateways to avoid
526
404
  */
527
405
  class Wayfinder {
528
- /**
529
- * The native http client used by wayfinder. By default, the native fetch api is used.
530
- *
531
- * @example
532
- * const wayfinder = new Wayfinder({
533
- * httpClient: axios,
534
- * });
535
- *
536
- */
537
- httpClient;
538
406
  /**
539
407
  * The gateways provider is responsible for providing the list of gateways to use for routing requests.
540
408
  *
@@ -575,8 +443,7 @@ class Wayfinder {
575
443
  */
576
444
  resolveUrl;
577
445
  /**
578
- *
579
- * A wrapped http client that supports ar:// protocol. If a verification strategy is provided,
446
+ * A wrapped fetch function that supports ar:// protocol. If a verification strategy is provided,
580
447
  * the request will be verified and events will be emitted as the request is processed.
581
448
  *
582
449
  * @example
@@ -596,9 +463,6 @@ class Wayfinder {
596
463
  * // request a transaction id
597
464
  * const response = await wayfinder.request('ar://1234567890')
598
465
  *
599
- * // request a transaction id with a custom http client
600
- * const response = await wayfinder.request('ar://1234567890')
601
- *
602
466
  * // Set strict mode to true to make verification blocking
603
467
  * const wayfinder = new Wayfinder({
604
468
  * strict: true,
@@ -664,14 +528,13 @@ class Wayfinder {
664
528
  emitter;
665
529
  /**
666
530
  * The constructor for the wayfinder
667
- * @param httpClient - the http client to use for requests
668
531
  * @param routingStrategy - the routing strategy to use for requests
669
532
  * @param verificationStrategy - the verification strategy to use for requests
670
533
  * @param gatewaysProvider - the gateways provider to use for routing requests
671
534
  * @param logger - the logger to use for logging
672
535
  * @param strict - if true, verification will be blocking and will fail requests if verification fails; if false, verification will be non-blocking
673
536
  */
674
- constructor({ httpClient = fetch, logger = logger_js_1.Logger.default, gatewaysProvider = new simple_cache_js_1.SimpleCacheGatewaysProvider({
537
+ constructor({ logger = logger_js_1.Logger.default, gatewaysProvider = new simple_cache_js_1.SimpleCacheGatewaysProvider({
675
538
  gatewaysProvider: new network_js_1.NetworkGatewaysProvider({
676
539
  ario: io_js_1.ARIO.mainnet(),
677
540
  }),
@@ -694,12 +557,9 @@ class Wayfinder {
694
557
  onVerificationProgress: (event) => {
695
558
  logger.debug('Verification progress!', event);
696
559
  },
697
- }, strict = false,
698
- // TODO: stats provider
699
- }) {
560
+ }, strict = false, }) {
700
561
  this.routingStrategy = routingStrategy;
701
562
  this.gatewaysProvider = gatewaysProvider;
702
- this.httpClient = httpClient;
703
563
  this.emitter = new WayfinderEmitter(events);
704
564
  this.verifyData =
705
565
  verificationStrategy.verifyData.bind(verificationStrategy);
@@ -717,7 +577,6 @@ class Wayfinder {
717
577
  };
718
578
  // create a wayfinder client with the routing strategy and gateways provider
719
579
  this.request = (0, exports.createWayfinderClient)({
720
- httpClient,
721
580
  selectGateway: async () => {
722
581
  return this.routingStrategy.selectGateway({
723
582
  gateways: await this.gatewaysProvider.getGateways(),
@@ -17,4 +17,4 @@
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.version = void 0;
19
19
  // AUTOMATICALLY GENERATED FILE - DO NOT TOUCH
20
- exports.version = '3.12.0-beta.3';
20
+ exports.version = '3.12.0-beta.5';