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