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

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,70 @@ 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
+ ...init,
309
+ // follow redirects as gateways use sandboxing on /txId requests
310
+ redirect: 'follow',
311
+ });
312
+ // TODO: update any caching we use for the request and gateway response
322
313
  logger?.debug(`Successfully routed request to gateway`, {
323
314
  redirectUrl: redirectUrl.toString(),
324
- originalUrl: originalUrl.toString(),
315
+ originalUrl,
325
316
  });
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
- }
317
+ // return the response right away if no redirect was made
318
+ if (redirectUrl.toString() === originalUrl) {
319
+ return response;
320
+ }
321
+ // return the response right away if no verification is needed or if there is no body
322
+ if (!verifyData) {
323
+ return response;
324
+ }
325
+ // the txId is either in the response headers or the path of the request as the first parameter
326
+ const txId = response.headers.get('x-arns-resolved-id') ??
327
+ redirectUrl.pathname.split('/')[1];
328
+ const contentLength = +(response.headers.get('content-length') ?? 0);
329
+ if (!exports.txIdRegex.test(txId)) {
330
+ // No transaction ID found, skip verification
331
+ logger?.debug('No transaction ID found, skipping verification', {
332
+ redirectUrl: redirectUrl.toString(),
333
+ originalUrl,
334
+ });
335
+ emitter?.emit('verification-skipped', {
336
+ originalUrl,
337
+ });
338
+ return response;
339
+ }
340
+ emitter?.emit('identified-transaction-id', {
341
+ originalUrl,
342
+ selectedGateway: redirectUrl.toString(),
343
+ txId,
344
+ });
345
+ if (!response.body) {
346
+ logger?.debug('No body, skipping verification', {
347
+ redirectUrl: redirectUrl.toString(),
348
+ originalUrl,
349
+ });
350
+ emitter?.emit('verification-skipped', {
351
+ originalUrl,
352
+ });
353
+ return response;
482
354
  }
483
- // TODO: if strict - wait for verification to finish and succeed before returning the response
484
- return response;
355
+ const verifiedStream = tapAndVerifyStream({
356
+ originalStream: response.body,
357
+ contentLength,
358
+ verifyData,
359
+ txId,
360
+ emitter,
361
+ strict,
362
+ });
363
+ // wrap the response with the verified stream
364
+ return new Response(verifiedStream, {
365
+ status: response.status,
366
+ statusText: response.statusText,
367
+ // TODO: we could add identified transaction id to the headers here, but it would be changing information from the original response
368
+ headers: response.headers,
369
+ });
485
370
  }
486
371
  catch (error) {
487
372
  logger?.debug('Failed to route request', {
@@ -494,6 +379,12 @@ const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, selectGatew
494
379
  if (i < maxRetries - 1) {
495
380
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
496
381
  }
382
+ else {
383
+ emitter?.emit('routing-failed', {
384
+ originalUrl,
385
+ error,
386
+ });
387
+ }
497
388
  }
498
389
  }
499
390
  throw new Error('Failed to route request after max retries', {
@@ -503,38 +394,13 @@ const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, selectGatew
503
394
  },
504
395
  });
505
396
  };
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
- });
397
+ return wayfinderRedirect;
519
398
  };
520
399
  exports.createWayfinderClient = createWayfinderClient;
521
400
  /**
522
401
  * 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
402
  */
527
403
  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
404
  /**
539
405
  * The gateways provider is responsible for providing the list of gateways to use for routing requests.
540
406
  *
@@ -575,8 +441,7 @@ class Wayfinder {
575
441
  */
576
442
  resolveUrl;
577
443
  /**
578
- *
579
- * A wrapped http client that supports ar:// protocol. If a verification strategy is provided,
444
+ * A wrapped fetch function that supports ar:// protocol. If a verification strategy is provided,
580
445
  * the request will be verified and events will be emitted as the request is processed.
581
446
  *
582
447
  * @example
@@ -596,9 +461,6 @@ class Wayfinder {
596
461
  * // request a transaction id
597
462
  * const response = await wayfinder.request('ar://1234567890')
598
463
  *
599
- * // request a transaction id with a custom http client
600
- * const response = await wayfinder.request('ar://1234567890')
601
- *
602
464
  * // Set strict mode to true to make verification blocking
603
465
  * const wayfinder = new Wayfinder({
604
466
  * strict: true,
@@ -664,14 +526,13 @@ class Wayfinder {
664
526
  emitter;
665
527
  /**
666
528
  * The constructor for the wayfinder
667
- * @param httpClient - the http client to use for requests
668
529
  * @param routingStrategy - the routing strategy to use for requests
669
530
  * @param verificationStrategy - the verification strategy to use for requests
670
531
  * @param gatewaysProvider - the gateways provider to use for routing requests
671
532
  * @param logger - the logger to use for logging
672
533
  * @param strict - if true, verification will be blocking and will fail requests if verification fails; if false, verification will be non-blocking
673
534
  */
674
- constructor({ httpClient = fetch, logger = logger_js_1.Logger.default, gatewaysProvider = new simple_cache_js_1.SimpleCacheGatewaysProvider({
535
+ constructor({ logger = logger_js_1.Logger.default, gatewaysProvider = new simple_cache_js_1.SimpleCacheGatewaysProvider({
675
536
  gatewaysProvider: new network_js_1.NetworkGatewaysProvider({
676
537
  ario: io_js_1.ARIO.mainnet(),
677
538
  }),
@@ -694,12 +555,9 @@ class Wayfinder {
694
555
  onVerificationProgress: (event) => {
695
556
  logger.debug('Verification progress!', event);
696
557
  },
697
- }, strict = false,
698
- // TODO: stats provider
699
- }) {
558
+ }, strict = false, }) {
700
559
  this.routingStrategy = routingStrategy;
701
560
  this.gatewaysProvider = gatewaysProvider;
702
- this.httpClient = httpClient;
703
561
  this.emitter = new WayfinderEmitter(events);
704
562
  this.verifyData =
705
563
  verificationStrategy.verifyData.bind(verificationStrategy);
@@ -717,7 +575,6 @@ class Wayfinder {
717
575
  };
718
576
  // create a wayfinder client with the routing strategy and gateways provider
719
577
  this.request = (0, exports.createWayfinderClient)({
720
- httpClient,
721
578
  selectGateway: async () => {
722
579
  return this.routingStrategy.selectGateway({
723
580
  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.4';