@ar.io/sdk 3.11.0-alpha.9 → 3.11.0-beta.1

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 (77) hide show
  1. package/bundles/web.bundle.min.js +106 -106
  2. package/lib/cjs/cli/utils.js +4 -1
  3. package/lib/cjs/common/contracts/ao-process.js +2 -1
  4. package/lib/cjs/common/wayfinder/{gateways.js → gateways/network.js} +3 -41
  5. package/lib/cjs/common/wayfinder/gateways/simple-cache.js +35 -0
  6. package/lib/cjs/common/wayfinder/gateways/static.js +13 -0
  7. package/lib/cjs/common/wayfinder/index.js +11 -8
  8. package/lib/cjs/common/wayfinder/routing/strategies/ping.js +72 -0
  9. package/lib/cjs/common/wayfinder/routing/strategies/ping.test.js +156 -0
  10. package/lib/cjs/common/wayfinder/routing/strategies/random.js +13 -0
  11. package/lib/cjs/common/wayfinder/routing/strategies/random.test.js +68 -0
  12. package/lib/cjs/common/wayfinder/routing/strategies/round-robin.js +42 -0
  13. package/lib/cjs/common/wayfinder/routing/strategies/round-robin.test.js +78 -0
  14. package/lib/cjs/common/wayfinder/routing/strategies/static.js +29 -0
  15. package/lib/cjs/common/wayfinder/routing/strategies/static.test.js +40 -0
  16. package/lib/cjs/common/wayfinder/verification/{data-root-verifier.js → strategies/data-root-verifier.js} +4 -4
  17. package/lib/cjs/common/wayfinder/verification/{hash-verifier.js → strategies/hash-verifier.js} +4 -4
  18. package/lib/cjs/common/wayfinder/{gateways/trusted-gateways.js → verification/trusted.js} +1 -1
  19. package/lib/cjs/common/wayfinder/wayfinder.js +397 -257
  20. package/lib/cjs/common/wayfinder/wayfinder.test.js +227 -208
  21. package/lib/cjs/version.js +1 -1
  22. package/lib/esm/cli/utils.js +4 -1
  23. package/lib/esm/common/contracts/ao-process.js +2 -1
  24. package/lib/esm/common/wayfinder/{gateways.js → gateways/network.js} +2 -38
  25. package/lib/esm/common/wayfinder/gateways/simple-cache.js +31 -0
  26. package/lib/esm/common/wayfinder/gateways/static.js +9 -0
  27. package/lib/esm/common/wayfinder/index.js +11 -8
  28. package/lib/esm/common/wayfinder/routing/strategies/ping.js +68 -0
  29. package/lib/esm/common/wayfinder/routing/strategies/ping.test.js +151 -0
  30. package/lib/esm/common/wayfinder/routing/strategies/random.js +9 -0
  31. package/lib/esm/common/wayfinder/routing/strategies/random.test.js +63 -0
  32. package/lib/esm/common/wayfinder/routing/strategies/round-robin.js +38 -0
  33. package/lib/esm/common/wayfinder/routing/strategies/round-robin.test.js +73 -0
  34. package/lib/esm/common/wayfinder/routing/strategies/static.js +25 -0
  35. package/lib/esm/common/wayfinder/routing/strategies/static.test.js +35 -0
  36. package/lib/esm/common/wayfinder/verification/{data-root-verifier.js → strategies/data-root-verifier.js} +2 -2
  37. package/lib/esm/common/wayfinder/verification/{hash-verifier.js → strategies/hash-verifier.js} +2 -2
  38. package/lib/esm/common/wayfinder/{gateways/trusted-gateways.js → verification/trusted.js} +1 -1
  39. package/lib/esm/common/wayfinder/wayfinder.js +395 -255
  40. package/lib/esm/common/wayfinder/wayfinder.test.js +227 -208
  41. package/lib/esm/version.js +1 -1
  42. package/lib/types/common/wayfinder/{gateways.d.ts → gateways/network.d.ts} +3 -23
  43. package/lib/types/common/wayfinder/{routers/random.d.ts → gateways/simple-cache.d.ts} +12 -8
  44. package/lib/types/common/wayfinder/{routers → gateways}/static.d.ts +6 -7
  45. package/lib/types/common/wayfinder/index.d.ts +10 -7
  46. package/lib/types/common/wayfinder/{routers/simple-cache.d.ts → routing/strategies/ping.d.ts} +10 -11
  47. package/lib/types/common/wayfinder/routing/strategies/random.d.ts +21 -0
  48. package/lib/types/common/wayfinder/routing/strategies/round-robin.d.ts +29 -0
  49. package/lib/types/common/wayfinder/routing/strategies/static.d.ts +29 -0
  50. package/lib/types/common/wayfinder/verification/{data-root-verifier.d.ts → strategies/data-root-verifier.d.ts} +2 -2
  51. package/lib/types/common/wayfinder/verification/{hash-verifier.d.ts → strategies/hash-verifier.d.ts} +2 -2
  52. package/lib/types/common/wayfinder/{gateways/trusted-gateways.d.ts → verification/trusted.d.ts} +1 -1
  53. package/lib/types/common/wayfinder/wayfinder.d.ts +111 -77
  54. package/lib/types/types/wayfinder.d.ts +8 -4
  55. package/lib/types/version.d.ts +1 -1
  56. package/package.json +1 -1
  57. package/lib/cjs/common/wayfinder/routers/priority.js +0 -29
  58. package/lib/cjs/common/wayfinder/routers/priority.test.js +0 -155
  59. package/lib/cjs/common/wayfinder/routers/random.js +0 -23
  60. package/lib/cjs/common/wayfinder/routers/random.test.js +0 -25
  61. package/lib/cjs/common/wayfinder/routers/simple-cache.js +0 -25
  62. package/lib/cjs/common/wayfinder/routers/simple-cache.test.js +0 -41
  63. package/lib/cjs/common/wayfinder/routers/static.js +0 -14
  64. package/lib/cjs/common/wayfinder/routers/static.test.js +0 -14
  65. package/lib/esm/common/wayfinder/routers/priority.js +0 -25
  66. package/lib/esm/common/wayfinder/routers/priority.test.js +0 -153
  67. package/lib/esm/common/wayfinder/routers/random.js +0 -19
  68. package/lib/esm/common/wayfinder/routers/random.test.js +0 -23
  69. package/lib/esm/common/wayfinder/routers/simple-cache.js +0 -21
  70. package/lib/esm/common/wayfinder/routers/simple-cache.test.js +0 -39
  71. package/lib/esm/common/wayfinder/routers/static.js +0 -10
  72. package/lib/esm/common/wayfinder/routers/static.test.js +0 -12
  73. package/lib/types/common/wayfinder/routers/priority.d.ts +0 -29
  74. /package/lib/types/common/wayfinder/{routers/priority.test.d.ts → routing/strategies/ping.test.d.ts} +0 -0
  75. /package/lib/types/common/wayfinder/{routers → routing/strategies}/random.test.d.ts +0 -0
  76. /package/lib/types/common/wayfinder/{routers/simple-cache.test.d.ts → routing/strategies/round-robin.test.d.ts} +0 -0
  77. /package/lib/types/common/wayfinder/{routers → routing/strategies}/static.test.d.ts +0 -0
@@ -17,50 +17,47 @@ import EventEmitter from 'node:events';
17
17
  import { PassThrough, Readable } from 'node:stream';
18
18
  import { ARIO } from '../io.js';
19
19
  import { Logger } from '../logger.js';
20
- import { NetworkGatewaysProvider, SimpleCacheGatewaysProvider, StaticGatewaysProvider, } from './gateways.js';
21
- import { TrustedGatewaysHashProvider } from './gateways/trusted-gateways.js';
22
- import { RandomGatewayRouter } from './routers/random.js';
23
- import { HashVerifier } from './verification/hash-verifier.js';
20
+ import { NetworkGatewaysProvider } from './gateways/network.js';
21
+ import { SimpleCacheGatewaysProvider } from './gateways/simple-cache.js';
22
+ import { StaticGatewaysProvider } from './gateways/static.js';
23
+ import { FastestPingRoutingStrategy } from './routing/strategies/ping.js';
24
+ import { HashVerificationStrategy } from './verification/strategies/hash-verifier.js';
25
+ import { TrustedGatewaysHashProvider } from './verification/trusted.js';
24
26
  // known regexes for wayfinder urls
25
27
  export const arnsRegex = /^[a-z0-9_-]{1,51}$/;
26
28
  export const txIdRegex = /^[A-Za-z0-9_-]{43}$/;
27
29
  /**
28
- * Core function to resolve a wayfinder url against a target gateway
30
+ * Core function that converts a wayfinder url to the proper ar-io gateway URL
29
31
  * @param originalUrl - the wayfinder url to resolve
30
- * @param targetGateway - the target gateway to resolve the url against
32
+ * @param selectedGateway - the target gateway to resolve the url against
31
33
  * @returns the resolved url that can be used to make a request
32
34
  */
33
- export const resolveWayfinderUrl = async ({ originalUrl, targetGateway, logger, }) => {
35
+ export const resolveWayfinderUrl = ({ originalUrl, selectedGateway, logger, }) => {
34
36
  if (originalUrl.toString().startsWith('ar://')) {
35
37
  logger?.debug(`Applying wayfinder routing protocol to ${originalUrl}`, {
36
38
  originalUrl,
37
39
  });
38
- const targetGatewayUrl = new URL(await targetGateway());
39
- logger?.debug(`Selected target gateway: ${targetGatewayUrl}`, {
40
- originalUrl,
41
- targetGateway: targetGatewayUrl,
42
- });
43
40
  const [, path] = originalUrl.toString().split('ar://');
44
41
  // e.g. ar:///info should route to the info endpoint of the target gateway
45
42
  if (path.startsWith('/')) {
46
- logger?.debug(`Routing to ${path.slice(1)} on ${targetGatewayUrl}`, {
43
+ logger?.debug(`Routing to ${path.slice(1)} on ${selectedGateway}`, {
47
44
  originalUrl,
48
- targetGateway: targetGatewayUrl,
45
+ selectedGateway,
49
46
  });
50
- return new URL(path.slice(1), targetGatewayUrl);
47
+ return new URL(path.slice(1), selectedGateway);
51
48
  }
52
49
  // TODO: this breaks 43 character named arns names - we should check a a local name cache list before resolving raw transaction ids
53
50
  if (txIdRegex.test(path)) {
54
51
  const [txId, ...rest] = path.split('/');
55
- return new URL(`${txId}${rest.join('/')}`, targetGatewayUrl);
52
+ return new URL(`${txId}${rest.join('/')}`, selectedGateway);
56
53
  }
57
54
  if (arnsRegex.test(path)) {
58
55
  // TODO: tests to ensure arns names support query params and paths
59
56
  const [name, ...rest] = path.split('/');
60
- const arnsUrl = `${targetGatewayUrl.protocol}//${name}.${targetGatewayUrl.hostname}${targetGatewayUrl.port ? `:${targetGatewayUrl.port}` : ''}`;
57
+ const arnsUrl = `${selectedGateway.protocol}//${name}.${selectedGateway.hostname}${selectedGateway.port ? `:${selectedGateway.port}` : ''}`;
61
58
  logger?.debug(`Routing to ${path} on ${arnsUrl}`, {
62
59
  originalUrl,
63
- targetGateway: targetGatewayUrl,
60
+ selectedGateway,
64
61
  });
65
62
  return new URL(rest.join('/'), arnsUrl);
66
63
  }
@@ -79,7 +76,7 @@ export class WayfinderEmitter extends EventEmitter {
79
76
  } = {}) {
80
77
  super();
81
78
  if (onVerificationPassed) {
82
- this.on('verification-passed', onVerificationPassed);
79
+ this.on('verification-succeeded', onVerificationPassed);
83
80
  }
84
81
  if (onVerificationFailed) {
85
82
  this.on('verification-failed', onVerificationFailed);
@@ -95,7 +92,7 @@ export class WayfinderEmitter extends EventEmitter {
95
92
  return super.on(event, listener);
96
93
  }
97
94
  }
98
- export function tapAndVerifyStream({ originalStream, contentLength, verifyData, txId, emitter, }) {
95
+ export function tapAndVerifyStream({ originalStream, contentLength, verifyData, txId, emitter, strict = false, }) {
99
96
  // taps node streams
100
97
  if (originalStream instanceof Readable &&
101
98
  typeof originalStream.pipe === 'function') {
@@ -123,26 +120,39 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
123
120
  });
124
121
  originalStream.on('end', async () => {
125
122
  streamToVerify.end(); // triggers verifier completion and completes the verification promise
126
- try {
127
- await verificationPromise;
128
- emitter?.emit('verification-passed', {
129
- txId,
130
- });
131
- tappedClientStream.end();
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
+ }
132
135
  }
133
- catch (error) {
134
- emitter?.emit('verification-failed', {
135
- error,
136
- txId,
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 });
137
146
  });
138
- tappedClientStream.destroy(error);
139
147
  }
140
148
  });
141
149
  originalStream.on('error', (err) => {
150
+ // emit the verification failed event
142
151
  emitter?.emit('verification-failed', {
143
152
  error: err,
144
153
  txId,
145
154
  });
155
+ // destroy both streams and propagate the original stream error
146
156
  streamToVerify.destroy(err);
147
157
  tappedClientStream.destroy(err);
148
158
  });
@@ -164,20 +174,37 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
164
174
  async pull(controller) {
165
175
  const { done, value } = await reader.read();
166
176
  if (done) {
167
- try {
168
- // due to backpressure, if the client does not consume the stream, the verification will not complete (particularly important for fetch, where the response body needs to be awaited for verification to complete)
169
- await verificationPromise;
170
- emitter?.emit('verification-passed', {
171
- txId,
172
- });
173
- controller.close();
177
+ if (strict) {
178
+ // in strict mode, we wait for verification to complete before closing the controller
179
+ try {
180
+ await verificationPromise;
181
+ emitter?.emit('verification-succeeded', { txId });
182
+ controller.close();
183
+ }
184
+ catch (err) {
185
+ emitter?.emit('verification-failed', {
186
+ txId,
187
+ error: err,
188
+ });
189
+ // In strict mode, we report the error to the client stream
190
+ controller.error(new Error('Verification failed', { cause: err }));
191
+ }
174
192
  }
175
- catch (err) {
176
- emitter?.emit('verification-failed', {
177
- txId,
178
- error: err,
193
+ else {
194
+ // in non-strict mode, we close the controller immediately and handle verification asynchronously
195
+ controller.close();
196
+ // trigger the verification promise and emit events for the result
197
+ verificationPromise
198
+ .then(() => {
199
+ emitter?.emit('verification-succeeded', { txId });
200
+ })
201
+ .catch((err) => {
202
+ emitter?.emit('verification-failed', {
203
+ txId,
204
+ error: err,
205
+ });
206
+ // we don't call controller.error() to avoid breaking the client stream
179
207
  });
180
- controller.error(err);
181
208
  }
182
209
  }
183
210
  else {
@@ -191,7 +218,9 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
191
218
  }
192
219
  },
193
220
  cancel(reason) {
221
+ // cancel the reader regardless of verification status
194
222
  reader.cancel(reason);
223
+ // emit the verification cancellation event
195
224
  emitter?.emit('verification-failed', {
196
225
  txId,
197
226
  error: new Error('Verification cancelled', {
@@ -200,6 +229,8 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
200
229
  },
201
230
  }),
202
231
  });
232
+ // note: we don't block or throw errors here even in strict mode
233
+ // since the stream is already being cancelled by the client
203
234
  },
204
235
  });
205
236
  return clientStreamWithVerification;
@@ -234,7 +265,7 @@ export function wrapVerifiedResponse(original, newBody, txId) {
234
265
  * @param resolveUrl - the function to construct the redirect url for ar:// requests
235
266
  * @returns a wrapped http client that supports ar:// protocol
236
267
  */
237
- export const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, emitter = new WayfinderEmitter(), logger, }) => {
268
+ export const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, selectGateway, emitter = new WayfinderEmitter(), logger, strict = false, }) => {
238
269
  const wayfinderRedirect = async (fn, rawArgs) => {
239
270
  // TODO: handle if first arg is not a string (i.e. just return the result of the function call)
240
271
  const [originalUrl, ...rest] = rawArgs;
@@ -242,157 +273,226 @@ export const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, emit
242
273
  logger?.debug('Original URL is not a string, skipping routing', {
243
274
  originalUrl,
244
275
  });
276
+ emitter?.emit('routing-skipped', {
277
+ originalUrl: JSON.stringify(originalUrl),
278
+ });
245
279
  return fn(...rawArgs);
246
280
  }
247
281
  emitter?.emit('routing-started', {
248
282
  originalUrl: originalUrl.toString(),
249
283
  });
250
- // route the request to the target gateway
251
- const redirectUrl = await resolveUrl({
252
- originalUrl,
253
- logger,
254
- });
255
- emitter?.emit('routing-succeeded', {
256
- originalUrl,
257
- targetGateway: redirectUrl.toString(),
258
- });
259
- logger?.debug(`Redirecting request to ${redirectUrl}`, {
260
- originalUrl,
261
- redirectUrl,
262
- });
263
- // make the request to the target gateway using the redirect url and http client
264
- const response = await fn(redirectUrl.toString(), ...rest);
265
- // TODO: trigger a routing event with the raw response object?
266
- logger?.debug(`Successfully routed request to ${redirectUrl}`, {
267
- redirectUrl,
268
- originalUrl,
269
- });
270
- // only verify data if the redirect url is different from the original url
271
- if (response && redirectUrl.toString() !== originalUrl.toString()) {
272
- if (verifyData) {
273
- // if the headers do not have .get on them, we need to parse the headers manually
274
- const headers = new Headers();
275
- let headersObject = response.headers ?? {};
276
- if (typeof headersObject.get !== 'function') {
277
- headersObject = Object.fromEntries(headersObject);
278
- for (const [key, value] of Object.entries(headersObject)) {
279
- headers.set(key, value);
280
- }
281
- }
282
- else {
283
- for (const [key, value] of headersObject.entries()) {
284
- headers.set(key, value);
285
- }
286
- }
287
- // transaction id is either in the response headers or the path of the request as the first parameter
288
- // TODO: we may want to move this parsing to be returned by the resolveUrl function depending on the redirect URL we've constructed
289
- const txId = headers.get('x-arns-resolved-id') ??
290
- redirectUrl.pathname.split('/')[1];
291
- // TODO: validate nodes return content length for all responses
292
- const contentLength = +(headers.get('content-length') ?? 0);
293
- if (!txIdRegex.test(txId)) {
294
- // no transaction id found, skip verification
295
- logger?.debug('No transaction id found, skipping verification', {
296
- redirectUrl,
297
- originalUrl,
298
- });
299
- emitter?.emit('verification-skipped', {
300
- originalUrl,
301
- });
302
- return response;
303
- }
304
- emitter?.emit('identified-transaction-id', {
284
+ // TODO: by default we will retry 3 times but this should be configurable and moved to a routing strategy
285
+ const maxRetries = 3;
286
+ const retryDelay = 1000;
287
+ for (let i = 0; i < maxRetries; i++) {
288
+ try {
289
+ // select the target gateway
290
+ const selectedGateway = await selectGateway();
291
+ logger?.debug('Selected gateway', {
305
292
  originalUrl,
306
- targetGateway: redirectUrl.toString(),
307
- txId,
293
+ selectedGateway: selectedGateway.toString(),
308
294
  });
309
- // parse out the key that contains the response body, we'll use it later when updating the response object
310
- const responseDataKey = response.body
311
- ? 'body'
312
- : response.data
313
- ? 'data'
314
- : undefined;
315
- if (responseDataKey === undefined) {
316
- throw new Error('No data body or data provided, skipping verification', {
317
- cause: {
318
- redirectUrl: redirectUrl.toString(),
319
- originalUrl: originalUrl.toString(),
320
- },
321
- });
322
- }
323
- const responseBody = response[responseDataKey];
324
- // TODO: determine if it is data item or L1 transaction, and tell the verifier accordingly, just drop in hit to graphql now
325
- if (txId === undefined) {
326
- throw new Error('Failed to parse data hash from response headers', {
327
- cause: {
328
- redirectUrl: redirectUrl.toString(),
329
- originalUrl: originalUrl.toString(),
330
- txId,
331
- },
332
- });
333
- }
334
- else if (responseBody === undefined) {
335
- throw new Error('No data body provided, skipping verification', {
336
- cause: {
337
- redirectUrl: redirectUrl.toString(),
338
- originalUrl: originalUrl.toString(),
295
+ // route the request to the target gateway
296
+ const redirectUrl = resolveUrl({
297
+ originalUrl,
298
+ selectedGateway,
299
+ logger,
300
+ });
301
+ emitter?.emit('routing-succeeded', {
302
+ originalUrl,
303
+ selectedGateway: selectedGateway.toString(),
304
+ redirectUrl: redirectUrl.toString(),
305
+ });
306
+ logger?.debug(`Redirecting request`, {
307
+ originalUrl,
308
+ redirectUrl: redirectUrl.toString(),
309
+ });
310
+ // make the request to the target gateway using the redirect url and http client
311
+ const response = await fn(redirectUrl.toString(), ...rest);
312
+ // TODO: trigger a routing event with the raw response object?
313
+ logger?.debug(`Successfully routed request to gateway`, {
314
+ redirectUrl: redirectUrl.toString(),
315
+ originalUrl: originalUrl.toString(),
316
+ });
317
+ // only verify data if the redirect url is different from the original url
318
+ if (response && redirectUrl.toString() !== originalUrl.toString()) {
319
+ if (verifyData) {
320
+ // if the headers do not have .get on them, we need to parse the headers manually
321
+ const headers = new Headers();
322
+ const headersObject = response.headers ?? {};
323
+ if (headersObject instanceof Map) {
324
+ for (const [key, value] of headersObject.entries()) {
325
+ headers.set(key, value);
326
+ }
327
+ }
328
+ else if (headersObject instanceof Headers) {
329
+ for (const [key, value] of headersObject.entries()) {
330
+ headers.set(key, value);
331
+ }
332
+ }
333
+ else if (headersObject !== undefined &&
334
+ typeof headersObject === 'object') {
335
+ for (const [key, value] of Object.entries(headersObject)) {
336
+ headers.set(key, value);
337
+ }
338
+ }
339
+ else {
340
+ throw new Error('Gateway did not return headers needed for verification', {
341
+ cause: {
342
+ redirectUrl: redirectUrl.toString(),
343
+ originalUrl: originalUrl.toString(),
344
+ },
345
+ });
346
+ }
347
+ // transaction id is either in the response headers or the path of the request as the first parameter
348
+ // TODO: we may want to move this parsing to be returned by the resolveUrl function depending on the redirect URL we've constructed
349
+ const txId = headers.get('x-arns-resolved-id') ??
350
+ redirectUrl.pathname.split('/')[1];
351
+ // TODO: validate nodes return content length for all responses
352
+ const contentLength = +(headers.get('content-length') ?? 0);
353
+ if (!txIdRegex.test(txId)) {
354
+ // no transaction id found, skip verification
355
+ logger?.debug('No transaction id found, skipping verification', {
356
+ redirectUrl: redirectUrl.toString(),
357
+ originalUrl,
358
+ });
359
+ emitter?.emit('verification-skipped', {
360
+ originalUrl,
361
+ });
362
+ return response;
363
+ }
364
+ emitter?.emit('identified-transaction-id', {
365
+ originalUrl,
366
+ selectedGateway: redirectUrl.toString(),
339
367
  txId,
340
- },
341
- });
342
- }
343
- else {
344
- logger?.debug('Verifying data hash for txId', {
345
- redirectUrl: redirectUrl.toString(),
346
- originalUrl: originalUrl.toString(),
347
- txId,
348
- });
349
- const newClientStream = tapAndVerifyStream({
350
- originalStream: responseBody,
351
- contentLength,
352
- verifyData,
353
- txId,
354
- emitter,
355
- });
356
- if (responseBody instanceof ReadableStream) {
357
- // specific to fetch
358
- return wrapVerifiedResponse(response, newClientStream, txId);
359
- }
360
- else if (responseBody instanceof Readable) {
361
- // overwrite the response body with the new client stream
362
- response.txId = txId;
363
- response.body = newClientStream;
364
- return response;
365
- }
366
- else {
367
- // TODO: content-application/json and it's smaller than 10mb
368
- // TODO: add tests and verify this works for all non-Readable/streamed responses
369
- try {
370
- // if strict set to true
371
- await verifyData({
372
- data: responseBody,
373
- txId,
368
+ });
369
+ // parse out the key that contains the response body, we'll use it later when updating the response object
370
+ const responseDataKey = response.body
371
+ ? 'body'
372
+ : response.data
373
+ ? 'data'
374
+ : undefined;
375
+ if (responseDataKey === undefined) {
376
+ throw new Error('No data body or data provided, skipping verification', {
377
+ cause: {
378
+ redirectUrl: redirectUrl.toString(),
379
+ originalUrl: originalUrl.toString(),
380
+ },
374
381
  });
375
- emitter?.emit('verification-passed', {
376
- txId,
382
+ }
383
+ const responseBody = response[responseDataKey];
384
+ // TODO: determine if it is data item or L1 transaction, and tell the verifier accordingly, just drop in hit to graphql now
385
+ if (txId === undefined) {
386
+ throw new Error('Failed to parse data hash from response headers', {
387
+ cause: {
388
+ redirectUrl: redirectUrl.toString(),
389
+ originalUrl: originalUrl.toString(),
390
+ txId,
391
+ },
377
392
  });
378
393
  }
379
- catch (error) {
380
- logger?.debug('Failed to verify data hash', {
381
- error,
382
- txId,
394
+ else if (responseBody === undefined) {
395
+ throw new Error('No data body provided, skipping verification', {
396
+ cause: {
397
+ redirectUrl: redirectUrl.toString(),
398
+ originalUrl: originalUrl.toString(),
399
+ txId,
400
+ },
383
401
  });
384
- emitter?.emit('verification-failed', {
402
+ }
403
+ else {
404
+ logger?.debug('Verifying data hash for txId', {
405
+ redirectUrl: redirectUrl.toString(),
406
+ originalUrl: originalUrl.toString(),
385
407
  txId,
386
- error,
387
408
  });
409
+ if (responseBody instanceof ReadableStream ||
410
+ responseBody instanceof Readable) {
411
+ const newClientStream = tapAndVerifyStream({
412
+ originalStream: responseBody,
413
+ contentLength,
414
+ verifyData,
415
+ txId,
416
+ emitter,
417
+ strict,
418
+ });
419
+ if (response instanceof Response) {
420
+ // specific to fetch
421
+ return wrapVerifiedResponse(response, newClientStream, txId);
422
+ }
423
+ else {
424
+ // overwrite the response body with the new client stream
425
+ response.txId = txId;
426
+ response.body = newClientStream;
427
+ return response;
428
+ }
429
+ }
430
+ else {
431
+ // TODO: content-application/json and it's smaller than 10mb
432
+ // TODO: add tests and verify this works for all non-Readable/streamed responses
433
+ if (strict) {
434
+ // In strict mode, wait for verification before returning response
435
+ try {
436
+ await verifyData({
437
+ data: responseBody,
438
+ txId,
439
+ });
440
+ emitter?.emit('verification-succeeded', { txId });
441
+ return response;
442
+ }
443
+ catch (error) {
444
+ logger?.debug('Failed to verify data hash', {
445
+ error,
446
+ txId,
447
+ });
448
+ emitter?.emit('verification-failed', { txId, error });
449
+ throw new Error('Verification failed', { cause: error });
450
+ }
451
+ }
452
+ else {
453
+ // In non-strict mode, perform verification in the background
454
+ verifyData({
455
+ data: responseBody,
456
+ txId,
457
+ })
458
+ .then(() => {
459
+ emitter?.emit('verification-succeeded', { txId });
460
+ })
461
+ .catch((error) => {
462
+ logger?.debug('Failed to verify data hash', {
463
+ error,
464
+ txId,
465
+ });
466
+ emitter?.emit('verification-failed', { txId, error });
467
+ });
468
+ return response;
469
+ }
470
+ }
388
471
  }
389
- return response;
390
472
  }
391
473
  }
474
+ // TODO: if strict - wait for verification to finish and succeed before returning the response
475
+ return response;
476
+ }
477
+ catch (error) {
478
+ logger?.debug('Failed to route request', {
479
+ error: error.message,
480
+ stack: error.stack,
481
+ originalUrl,
482
+ attempt: i + 1,
483
+ maxRetries,
484
+ });
485
+ if (i < maxRetries - 1) {
486
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
487
+ }
392
488
  }
393
489
  }
394
- // TODO: if strict - wait for verification to finish and succeed before returning the response
395
- return response;
490
+ throw new Error('Failed to route request after max retries', {
491
+ cause: {
492
+ originalUrl,
493
+ maxRetries,
494
+ },
495
+ });
396
496
  };
397
497
  return new Proxy(httpClient, {
398
498
  // support direct calls: fetch('ar://…', options)
@@ -416,87 +516,116 @@ export const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, emit
416
516
  */
417
517
  export class Wayfinder {
418
518
  /**
419
- * The router to use for requests
519
+ * The native http client used by wayfinder. By default, the native fetch api is used.
420
520
  *
421
521
  * @example
422
522
  * const wayfinder = new Wayfinder({
423
- * router: new RandomGatewayRouter({
424
- * gatewaysProvider: new SimpleCacheGatewaysProvider({
425
- * gatewaysProvider: new NetworkGatewaysProvider({ ario: ARIO.mainnet() }),
426
- * ttlSeconds: 60 * 60 * 24, // 1 day
427
- * }),
428
- * }),
523
+ * httpClient: axios,
429
524
  * });
430
525
  *
431
- * // Returns a target gateway based on the routing strategy
432
- * const targetGateway = await wayfinder.router.getTargetGateway();
433
526
  */
434
- router;
527
+ httpClient;
435
528
  /**
436
- * The native http client used by wayfinder
529
+ * The gateways provider is responsible for providing the list of gateways to use for routing requests.
437
530
  *
438
531
  * @example
439
532
  * const wayfinder = new Wayfinder({
440
- * router: new RandomGatewayRouter({
441
- * gatewaysProvider: new SimpleCacheGatewaysProvider({
442
- * gatewaysProvider: new NetworkGatewaysProvider({ ario: ARIO.mainnet() }),
443
- * ttlSeconds: 60 * 60 * 24, // 1 day
444
- * }),
533
+ * gatewaysProvider: new SimpleCacheGatewaysProvider({
534
+ * gatewaysProvider: new NetworkGatewaysProvider({ ario: ARIO.mainnet() }),
535
+ * ttlSeconds: 60 * 60 * 24, // 1 day
445
536
  * }),
446
- * httpClient: axios,
447
537
  * });
448
- *
449
538
  */
450
- httpClient;
539
+ gatewaysProvider;
451
540
  /**
452
- * The function that resolves the redirect url for ar:// requests to a target gateway
541
+ * The routing strategy to use when routing requests.
453
542
  *
454
543
  * @example
455
544
  * const wayfinder = new Wayfinder({
456
- * router: new RandomGatewayRouter({
457
- * gatewaysProvider: new SimpleCacheGatewaysProvider({
458
- * gatewaysProvider: new NetworkGatewaysProvider({ ario: ARIO.mainnet() }),
459
- * ttlSeconds: 60 * 60 * 24, // 1 day
460
- * }),
545
+ * strategy: new FastestPingStrategy({
546
+ * timeoutMs: 1000,
461
547
  * }),
462
- * httpClient: axios,
463
548
  * });
549
+ */
550
+ routingStrategy;
551
+ /**
552
+ * A helper function that resolves the redirect url for ar:// requests to a target gateway.
553
+ *
554
+ * Note: no verification is done when resolving an ar://<path> url to a wayfinder route.
555
+ * In order to verify the data, you must use the `request` function or request the data and
556
+ * verify it yourself via the `verifyData` function.
557
+ *
558
+ * @example
559
+ * const { resolveUrl } = new Wayfinder();
464
560
  *
465
561
  * // returns the redirected URL based on the routing strategy and the original url
466
- * const redirectUrl = await wayfinder.resolveUrl({ originalUrl: 'ar://example' });
562
+ * const redirectUrl = await resolveUrl({ originalUrl: 'ar://example' });
563
+ *
564
+ * window.open(redirectUrl.toString(), '_blank');
467
565
  */
468
566
  resolveUrl;
469
567
  /**
470
- * A wrapped http client that supports ar:// protocol
568
+ *
569
+ * A wrapped http client that supports ar:// protocol. If a verification strategy is provided,
570
+ * the request will be verified and events will be emitted as the request is processed.
471
571
  *
472
572
  * @example
473
- * const { request: wayfind } = new Wayfinder({
474
- * router: new RandomGatewayRouter({
475
- * gatewaysProvider: new SimpleCacheGatewaysProvider({
476
- * gatewaysProvider: new NetworkGatewaysProvider({ ario: ARIO.mainnet() }),
477
- * ttlSeconds: 60 * 60 * 24, // 1 day
573
+ * const wayfinder = new Wayfinder({
574
+ * verificationStrategy: new HashVerificationStrategy({
575
+ * trustedHashProvider: new TrustedGatewaysHashProvider({
576
+ * gatewaysProvider: new StaticGatewaysProvider({
577
+ * gateways: ['https://permagate.io'],
578
+ * }),
478
579
  * }),
479
580
  * }),
480
- * httpClient: axios,
481
- * });;
482
- *
483
- * const response = await wayfind('ar://example', {
484
- * method: 'POST',
485
- * data: {
486
- * name: 'John Doe',
487
- * },
488
581
  * })
582
+ *
583
+ * // request an arns name
584
+ * const response = await wayfinder.request('ar://ardrive')
585
+ *
586
+ * // request a transaction id
587
+ * const response = await wayfinder.request('ar://1234567890')
588
+ *
589
+ * // request a transaction id with a custom http client
590
+ * const response = await wayfinder.request('ar://1234567890')
591
+ *
592
+ * // Set strict mode to true to make verification blocking
593
+ * const wayfinder = new Wayfinder({
594
+ * strict: true,
595
+ * });
596
+ *
597
+ * // This will throw an error if verification fails
598
+ * try {
599
+ * const response = await wayfinder.request('ar://1234567890');
600
+ * } catch (error) {
601
+ * console.error('Verification failed', error);
602
+ * }
489
603
  */
490
604
  request;
491
- // TODO: stats provider
492
- // TODO: metricsProvider for otel/prom support
605
+ /**
606
+ * The function that verifies the data hash for a given transaction id.
607
+ *
608
+ * @example
609
+ * const wayfinder = new Wayfinder({
610
+ * verifyData: (data, txId) => {
611
+ * // some custom verification logic
612
+ * return true;
613
+ * },
614
+ * });
615
+ */
493
616
  verifyData;
617
+ /**
618
+ * Whether verification should be strict (blocking) or not.
619
+ * If true, verification failures will cause requests to fail.
620
+ * If false, verification will be performed asynchronously and failures will only emit events.
621
+ */
622
+ strict;
494
623
  /**
495
624
  * The event emitter for wayfinder that emits verification events.
496
625
  *
497
626
  * const wayfinder = new Wayfinder()
498
627
  *
499
- * wayfinder.emitter.on('verification-passed', (event) => {
628
+ * wayfinder.emitter.on('verification-succeeded', (event) => {
500
629
  * console.log('Verification passed!', event);
501
630
  * })
502
631
  *
@@ -522,63 +651,74 @@ export class Wayfinder {
522
651
  *
523
652
  * const response = await wayfind('ar://example');
524
653
  */
525
- // TODO: consider changing this to events or event emitter
526
654
  emitter;
527
- constructor({ httpClient,
528
- // TODO: consider changing router to routingStrategy or strategy
529
- router = new RandomGatewayRouter({
530
- gatewaysProvider: new SimpleCacheGatewaysProvider({
531
- gatewaysProvider: new NetworkGatewaysProvider({ ario: ARIO.mainnet() }),
532
- ttlSeconds: 60 * 60 * 24, // 1 day
655
+ /**
656
+ * The constructor for the wayfinder
657
+ * @param httpClient - the http client to use for requests
658
+ * @param routingStrategy - the routing strategy to use for requests
659
+ * @param verificationStrategy - the verification strategy to use for requests
660
+ * @param gatewaysProvider - the gateways provider to use for routing requests
661
+ * @param logger - the logger to use for logging
662
+ * @param strict - if true, verification will be blocking and will fail requests if verification fails; if false, verification will be non-blocking
663
+ */
664
+ constructor({ httpClient = fetch, logger = Logger.default, gatewaysProvider = new SimpleCacheGatewaysProvider({
665
+ gatewaysProvider: new NetworkGatewaysProvider({
666
+ ario: ARIO.mainnet(),
533
667
  }),
534
- }), logger = Logger.default,
535
- // TODO: support disabling verification or create some PassThroughVerifier like thing
536
- verifier = new HashVerifier({
668
+ ttlSeconds: 60 * 60, // 1 hour
669
+ }), routingStrategy = new FastestPingRoutingStrategy({
670
+ timeoutMs: 1000,
671
+ }), verificationStrategy = new HashVerificationStrategy({
537
672
  trustedHashProvider: new TrustedGatewaysHashProvider({
538
673
  gatewaysProvider: new StaticGatewaysProvider({
539
674
  gateways: ['https://permagate.io'],
540
675
  }),
541
676
  }),
542
- }), events,
677
+ }), events = {
678
+ onVerificationPassed: (event) => {
679
+ logger.debug('Verification passed!', event);
680
+ },
681
+ onVerificationFailed: (event) => {
682
+ logger.error('Verification failed!', event);
683
+ },
684
+ onVerificationProgress: (event) => {
685
+ logger.debug('Verification progress!', event);
686
+ },
687
+ }, strict = false,
543
688
  // TODO: stats provider
544
689
  }) {
545
- this.router = router;
690
+ this.routingStrategy = routingStrategy;
691
+ this.gatewaysProvider = gatewaysProvider;
546
692
  this.httpClient = httpClient;
547
693
  this.emitter = new WayfinderEmitter(events);
548
- this.verifyData = verifier.verifyData.bind(verifier);
694
+ this.verifyData =
695
+ verificationStrategy.verifyData.bind(verificationStrategy);
696
+ this.strict = strict;
697
+ // top level function to easily resolve wayfinder urls using the routing strategy and gateways provider
549
698
  this.resolveUrl = async ({ originalUrl, logger }) => {
699
+ const selectedGateway = await this.routingStrategy.selectGateway({
700
+ gateways: await this.gatewaysProvider.getGateways(),
701
+ });
550
702
  return resolveWayfinderUrl({
551
703
  originalUrl,
552
- targetGateway: async () => await this.router.getTargetGateway(),
704
+ selectedGateway,
553
705
  logger,
554
706
  });
555
707
  };
708
+ // create a wayfinder client with the routing strategy and gateways provider
556
709
  this.request = createWayfinderClient({
557
710
  httpClient,
558
- resolveUrl: this.resolveUrl,
711
+ selectGateway: async () => {
712
+ return this.routingStrategy.selectGateway({
713
+ gateways: await this.gatewaysProvider.getGateways(),
714
+ });
715
+ },
716
+ resolveUrl: resolveWayfinderUrl,
559
717
  verifyData: this.verifyData,
560
718
  emitter: this.emitter,
561
719
  logger,
720
+ strict,
562
721
  });
563
- logger?.debug(`Wayfinder initialized with ${router.name} routing strategy`);
722
+ logger?.debug(`Wayfinder initialized with ${routingStrategy.constructor.name} routing strategy`);
564
723
  }
565
724
  }
566
- // TODO: add a chart for verification strategies and what they do
567
- // include complexity, performance, and security
568
- // explain use cases that each strategy is best for
569
- // e.g.
570
- /**
571
- *
572
- * type | complexity | performance | security
573
- * ---------|------------|-------------|---------
574
- * hash | low | high | low
575
- * ---------|------------|-------------|---------
576
- * data root | medium | medium | low | only L1
577
- * ---------|------------|-------------|---------
578
- * signature | medium | medium | medium
579
- * ---------|------------|-------------|---------
580
- * composite | high | low | high
581
- * ---------|------------|-------------|---------
582
- *
583
- *
584
- */