@twin.org/web 0.0.1-next.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 (43) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +21 -0
  3. package/dist/cjs/index.cjs +1056 -0
  4. package/dist/esm/index.mjs +1046 -0
  5. package/dist/types/errors/fetchError.d.ts +22 -0
  6. package/dist/types/index.d.ts +14 -0
  7. package/dist/types/models/IFetchOptions.d.ts +30 -0
  8. package/dist/types/models/IHttpHeaders.d.ts +6 -0
  9. package/dist/types/models/IJwk.d.ts +62 -0
  10. package/dist/types/models/IJwtHeader.d.ts +22 -0
  11. package/dist/types/models/IJwtPayload.d.ts +37 -0
  12. package/dist/types/models/headerTypes.d.ts +41 -0
  13. package/dist/types/models/httpMethod.d.ts +18 -0
  14. package/dist/types/models/httpStatusCode.d.ts +257 -0
  15. package/dist/types/models/jwtAlgorithms.d.ts +17 -0
  16. package/dist/types/models/mimeTypes.d.ts +93 -0
  17. package/dist/types/utils/fetchHelper.d.ts +52 -0
  18. package/dist/types/utils/jwt.d.ts +85 -0
  19. package/dist/types/utils/mimeTypeHelper.d.ts +17 -0
  20. package/docs/changelog.md +5 -0
  21. package/docs/examples.md +1 -0
  22. package/docs/reference/classes/FetchError.md +379 -0
  23. package/docs/reference/classes/FetchHelper.md +185 -0
  24. package/docs/reference/classes/Jwt.md +313 -0
  25. package/docs/reference/classes/MimeTypeHelper.md +53 -0
  26. package/docs/reference/index.md +32 -0
  27. package/docs/reference/interfaces/IFetchOptions.md +53 -0
  28. package/docs/reference/interfaces/IHttpHeaders.md +7 -0
  29. package/docs/reference/interfaces/IJwk.md +111 -0
  30. package/docs/reference/interfaces/IJwtHeader.md +31 -0
  31. package/docs/reference/interfaces/IJwtPayload.md +63 -0
  32. package/docs/reference/type-aliases/HeaderTypes.md +5 -0
  33. package/docs/reference/type-aliases/HttpMethod.md +5 -0
  34. package/docs/reference/type-aliases/HttpStatusCode.md +5 -0
  35. package/docs/reference/type-aliases/JwtAlgorithms.md +5 -0
  36. package/docs/reference/type-aliases/MimeTypes.md +5 -0
  37. package/docs/reference/variables/HeaderTypes.md +55 -0
  38. package/docs/reference/variables/HttpMethod.md +43 -0
  39. package/docs/reference/variables/HttpStatusCode.md +379 -0
  40. package/docs/reference/variables/JwtAlgorithms.md +19 -0
  41. package/docs/reference/variables/MimeTypes.md +133 -0
  42. package/locales/en.json +18 -0
  43. package/package.json +65 -0
@@ -0,0 +1,1046 @@
1
+ import { BaseError, StringHelper, Guards, Is, AsyncCache, ObjectHelper, Converter, GeneralError, ArrayHelper } from '@twin.org/core';
2
+ import { HmacSha256, Ed25519 } from '@twin.org/crypto';
3
+
4
+ // Copyright 2024 IOTA Stiftung.
5
+ // SPDX-License-Identifier: Apache-2.0.
6
+ /**
7
+ * Class to represent errors from fetch.
8
+ */
9
+ class FetchError extends BaseError {
10
+ /**
11
+ * Runtime name for the class.
12
+ */
13
+ static CLASS_NAME = "FetchError";
14
+ /**
15
+ * Create a new instance of FetchError.
16
+ * @param source The source of the error.
17
+ * @param message The message as a code.
18
+ * @param httpStatus The http status code.
19
+ * @param properties Any additional information for the error.
20
+ * @param inner The inner error if we have wrapped another error.
21
+ */
22
+ constructor(source, message, httpStatus, properties, inner) {
23
+ super(FetchError.CLASS_NAME, source, message, {
24
+ httpStatus,
25
+ ...properties
26
+ }, inner);
27
+ }
28
+ }
29
+
30
+ // Copyright 2024 IOTA Stiftung.
31
+ // SPDX-License-Identifier: Apache-2.0.
32
+ /**
33
+ * Common http header types.
34
+ */
35
+ // eslint-disable-next-line @typescript-eslint/naming-convention
36
+ const HeaderTypes = {
37
+ /**
38
+ * Content Type.
39
+ */
40
+ ContentType: "Content-Type",
41
+ /**
42
+ * Content Length.
43
+ */
44
+ ContentLength: "Content-Length",
45
+ /**
46
+ * Content Disposition.
47
+ */
48
+ ContentDisposition: "Content-Disposition",
49
+ /**
50
+ * Accept.
51
+ */
52
+ Accept: "Accept",
53
+ /**
54
+ * Authorization.
55
+ */
56
+ Authorization: "Authorization",
57
+ /**
58
+ * Cookie.
59
+ */
60
+ Cookie: "Cookie",
61
+ /**
62
+ * Set Cookie.
63
+ */
64
+ SetCookie: "Set-Cookie",
65
+ /**
66
+ * Location
67
+ */
68
+ Location: "Location"
69
+ };
70
+
71
+ // Copyright 2024 IOTA Stiftung.
72
+ // SPDX-License-Identifier: Apache-2.0.
73
+ /**
74
+ * The names of the HTTP Methods.
75
+ */
76
+ // eslint-disable-next-line @typescript-eslint/naming-convention
77
+ const HttpMethod = {
78
+ GET: "GET",
79
+ POST: "POST",
80
+ PUT: "PUT",
81
+ PATCH: "PATCH",
82
+ DELETE: "DELETE",
83
+ OPTIONS: "OPTIONS",
84
+ HEAD: "HEAD",
85
+ CONNECT: "CONNECT",
86
+ TRACE: "TRACE"
87
+ };
88
+
89
+ // Copyright 2024 IOTA Stiftung.
90
+ // SPDX-License-Identifier: Apache-2.0.
91
+ /**
92
+ * Standard HTTP status codes.
93
+ */
94
+ // eslint-disable-next-line @typescript-eslint/naming-convention
95
+ const HttpStatusCode = {
96
+ /**
97
+ * Continue status code.
98
+ */
99
+ continue: 100,
100
+ /**
101
+ * Switching Protocols status code.
102
+ */
103
+ switchingProtocols: 101,
104
+ /**
105
+ * Processing status code.
106
+ */
107
+ processing: 102,
108
+ /**
109
+ * Early Hints status code.
110
+ */
111
+ earlyHints: 103,
112
+ /**
113
+ * OK status code.
114
+ */
115
+ ok: 200,
116
+ /**
117
+ * Created status code.
118
+ */
119
+ created: 201,
120
+ /**
121
+ * Accepted status code.
122
+ */
123
+ accepted: 202,
124
+ /**
125
+ * Non-Authoritative Information status code.
126
+ */
127
+ nonAuthoritativeInformation: 203,
128
+ /**
129
+ * No Content status code.
130
+ */
131
+ noContent: 204,
132
+ /**
133
+ * Reset Content status code.
134
+ */
135
+ resetContent: 205,
136
+ /**
137
+ * Partial Content status code.
138
+ */
139
+ partialContent: 206,
140
+ /**
141
+ * Multi-Status status code.
142
+ */
143
+ multiStatus: 207,
144
+ /**
145
+ * Already Reported status code.
146
+ */
147
+ alreadyReported: 208,
148
+ /**
149
+ * IM Used status code.
150
+ */
151
+ imUsed: 226,
152
+ /**
153
+ * Multiple Choices status code.
154
+ */
155
+ multipleChoices: 300,
156
+ /**
157
+ * Moved Permanently status code.
158
+ */
159
+ movedPermanently: 301,
160
+ /**
161
+ * Found status code.
162
+ */
163
+ found: 302,
164
+ /**
165
+ * See Other status code.
166
+ */
167
+ seeOther: 303,
168
+ /**
169
+ * Not Modified status code.
170
+ */
171
+ notModified: 304,
172
+ /**
173
+ * Use Proxy status code.
174
+ */
175
+ useProxy: 305,
176
+ /**
177
+ * Temporary Redirect status code.
178
+ */
179
+ temporaryRedirect: 307,
180
+ /**
181
+ * Permanent Redirect status code.
182
+ */
183
+ permanentRedirect: 308,
184
+ /**
185
+ * Bad Request status code.
186
+ */
187
+ badRequest: 400,
188
+ /**
189
+ * Unauthorized status code.
190
+ */
191
+ unauthorized: 401,
192
+ /**
193
+ * Payment Required status code.
194
+ */
195
+ paymentRequired: 402,
196
+ /**
197
+ * Forbidden status code.
198
+ */
199
+ forbidden: 403,
200
+ /**
201
+ * Not Found status code.
202
+ */
203
+ notFound: 404,
204
+ /**
205
+ * Method Not Allowed status code.
206
+ */
207
+ methodNotAllowed: 405,
208
+ /**
209
+ * Not Acceptable status code.
210
+ */
211
+ notAcceptable: 406,
212
+ /**
213
+ * Proxy Authentication Required status code.
214
+ */
215
+ proxyAuthenticationRequired: 407,
216
+ /**
217
+ * Request Timeout status code.
218
+ */
219
+ requestTimeout: 408,
220
+ /**
221
+ * Conflict status code.
222
+ */
223
+ conflict: 409,
224
+ /**
225
+ * Gone status code.
226
+ */
227
+ gone: 410,
228
+ /**
229
+ * Length Required status code.
230
+ */
231
+ lengthRequired: 411,
232
+ /**
233
+ * Precondition Failed status code.
234
+ */
235
+ preconditionFailed: 412,
236
+ /**
237
+ * Payload Too Large status code.
238
+ */
239
+ payloadTooLarge: 413,
240
+ /**
241
+ * URI Too Long status code.
242
+ */
243
+ uriTooLong: 414,
244
+ /**
245
+ * Unsupported Media Type status code.
246
+ */
247
+ unsupportedMediaType: 415,
248
+ /**
249
+ * Range Not Satisfiable status code.
250
+ */
251
+ rangeNotSatisfiable: 416,
252
+ /**
253
+ * Expectation Failed status code.
254
+ */
255
+ expectationFailed: 417,
256
+ /**
257
+ * I'm a Teapot status code.
258
+ */
259
+ imATeapot: 418,
260
+ /**
261
+ * Misdirected Request status code.
262
+ */
263
+ misdirectedRequest: 421,
264
+ /**
265
+ * Unprocessable Entity status code.
266
+ */
267
+ unprocessableEntity: 422,
268
+ /**
269
+ * Locked status code.
270
+ */
271
+ locked: 423,
272
+ /**
273
+ * Failed Dependency status code.
274
+ */
275
+ failedDependency: 424,
276
+ /**
277
+ * Too Early status code.
278
+ */
279
+ tooEarly: 425,
280
+ /**
281
+ * Upgrade Required status code.
282
+ */
283
+ upgradeRequired: 426,
284
+ /**
285
+ * Precondition Required status code.
286
+ */
287
+ preconditionRequired: 428,
288
+ /**
289
+ * Too Many Requests status code.
290
+ */
291
+ tooManyRequests: 429,
292
+ /**
293
+ * Request Header Fields Too Large status code.
294
+ */
295
+ requestHeaderFieldsTooLarge: 431,
296
+ /**
297
+ * Unavailable For Legal Reasons status code.
298
+ */
299
+ unavailableForLegalReasons: 451,
300
+ /**
301
+ * Internal Server Error status code.
302
+ */
303
+ internalServerError: 500,
304
+ /**
305
+ * Not Implemented status code.
306
+ */
307
+ notImplemented: 501,
308
+ /**
309
+ * Bad Gateway status code.
310
+ */
311
+ badGateway: 502,
312
+ /**
313
+ * Service Unavailable status code.
314
+ */
315
+ serviceUnavailable: 503,
316
+ /**
317
+ * Gateway Timeout status code.
318
+ */
319
+ gatewayTimeout: 504,
320
+ /**
321
+ * HTTP Version Not Supported status code.
322
+ */
323
+ httpVersionNotSupported: 505,
324
+ /**
325
+ * Variant Also Negotiates status code.
326
+ */
327
+ variantAlsoNegotiates: 506,
328
+ /**
329
+ * Insufficient Storage status code.
330
+ */
331
+ insufficientStorage: 507,
332
+ /**
333
+ * Loop Detected status code.
334
+ */
335
+ loopDetected: 508,
336
+ /**
337
+ * Not Extended status code.
338
+ */
339
+ notExtended: 510,
340
+ /**
341
+ * Network Authentication Required status code.
342
+ */
343
+ networkAuthenticationRequired: 511
344
+ };
345
+
346
+ // Copyright 2024 IOTA Stiftung.
347
+ // SPDX-License-Identifier: Apache-2.0.
348
+ /**
349
+ * The cryptographic algorithms supported for JSON Web Tokens and JSON Web Keys.
350
+ */
351
+ // eslint-disable-next-line @typescript-eslint/naming-convention
352
+ const JwtAlgorithms = {
353
+ /**
354
+ * HMAC using SHA-256.
355
+ */
356
+ HS256: "HS256",
357
+ /**
358
+ * EdDSA using Ed25519.
359
+ */
360
+ EdDSA: "EdDSA"
361
+ };
362
+
363
+ // Copyright 2024 IOTA Stiftung.
364
+ // SPDX-License-Identifier: Apache-2.0.
365
+ /**
366
+ * Common mime types.
367
+ */
368
+ // eslint-disable-next-line @typescript-eslint/naming-convention
369
+ const MimeTypes = {
370
+ /**
371
+ * Plaint Text - text/plain
372
+ */
373
+ PlainText: "text/plain",
374
+ /**
375
+ * HTML - text/html
376
+ */
377
+ Html: "text/html",
378
+ /**
379
+ * Javascript - text/javascript
380
+ */
381
+ Javascript: "text/javascript",
382
+ /**
383
+ * JSON - application/json
384
+ */
385
+ Json: "application/json",
386
+ /**
387
+ * JSON-LD - application/ld+json
388
+ */
389
+ JsonLd: "application/ld+json",
390
+ /**
391
+ * XML - application/xml
392
+ */
393
+ Xml: "application/xml",
394
+ /**
395
+ * Application Octet Stream, arbitrary binary - application/octet-stream
396
+ */
397
+ OctetStream: "application/octet-stream",
398
+ /**
399
+ * Application GZIP - application/gzip
400
+ */
401
+ Gzip: "application/gzip",
402
+ /**
403
+ * Application BZIP2 - application/x-bzip2
404
+ */
405
+ Bzip2: "application/x-bzip2",
406
+ /**
407
+ * Application ZIP - application/zip
408
+ */
409
+ Zip: "application/zip",
410
+ /**
411
+ * Application PDF - application/pdf
412
+ */
413
+ Pdf: "application/pdf",
414
+ /**
415
+ * Image GIF - image/gif
416
+ */
417
+ Gif: "image/gif",
418
+ /**
419
+ * Image BMP - image/bmp
420
+ */
421
+ Bmp: "image/bmp",
422
+ /**
423
+ * Image JPEG - image/jpeg
424
+ */
425
+ Jpeg: "image/jpeg",
426
+ /**
427
+ * Image PNG - image/png
428
+ */
429
+ Png: "image/png",
430
+ /**
431
+ * Image Tiff - image/tiff
432
+ */
433
+ Tiff: "image/tiff",
434
+ /**
435
+ * Image SVG - image/svg+xml
436
+ */
437
+ Svg: "image/svg+xml",
438
+ /**
439
+ * Image WEBP - image/webp
440
+ */
441
+ WebP: "image/webp",
442
+ /**
443
+ * Video MP4 - video/mp4
444
+ */
445
+ Mp4: "video/mp4",
446
+ /**
447
+ * Audio/Video MPEG - video/mpeg
448
+ */
449
+ Mpeg: "video/mpeg",
450
+ /**
451
+ * Video WEBM - video/webm
452
+ */
453
+ Webm: "video/webm"
454
+ };
455
+
456
+ // Copyright 2024 IOTA Stiftung.
457
+ // SPDX-License-Identifier: Apache-2.0.
458
+ /**
459
+ * Class to helper with fetch operations.
460
+ */
461
+ class FetchHelper {
462
+ /**
463
+ * Runtime name for the class.
464
+ * @internal
465
+ */
466
+ static _CLASS_NAME = "FetchHelper";
467
+ /**
468
+ * Prefix to use for cache entries.
469
+ * @internal
470
+ */
471
+ static _CACHE_PREFIX = "fetch_";
472
+ /**
473
+ * Runtime name for the class.
474
+ * @internal
475
+ */
476
+ static _CLASS_NAME_CAMEL_CASE = StringHelper.camelCase("FetchHelper");
477
+ /**
478
+ * Perform a fetch request.
479
+ * @param source The source for the request.
480
+ * @param url The url for the request.
481
+ * @param method The http method.
482
+ * @param body Request to send to the endpoint.
483
+ * @param options Options for sending the requests.
484
+ * @returns The response.
485
+ */
486
+ static async fetch(source, url, method, body, options) {
487
+ Guards.string(FetchHelper._CLASS_NAME, "source", source);
488
+ Guards.string(FetchHelper._CLASS_NAME, "url", url);
489
+ Guards.arrayOneOf(FetchHelper._CLASS_NAME, "method", method, Object.values(HttpMethod));
490
+ if (!Is.undefined(body) && !Is.uint8Array(body)) {
491
+ Guards.string(FetchHelper._CLASS_NAME, "body", body);
492
+ }
493
+ if (!Is.undefined(options)) {
494
+ Guards.object(FetchHelper._CLASS_NAME, "options", options);
495
+ if (!Is.undefined(options.headers)) {
496
+ Guards.object(FetchHelper._CLASS_NAME, "options.headers", options.headers);
497
+ }
498
+ if (!Is.undefined(options.timeoutMs)) {
499
+ Guards.integer(FetchHelper._CLASS_NAME, "options.timeoutMs", options.timeoutMs);
500
+ }
501
+ if (!Is.undefined(options.includeCredentials)) {
502
+ Guards.boolean(FetchHelper._CLASS_NAME, "options.includeCredentials", options.includeCredentials);
503
+ }
504
+ if (!Is.undefined(options.retryCount)) {
505
+ Guards.integer(FetchHelper._CLASS_NAME, "options.retryCount", options.retryCount);
506
+ }
507
+ if (!Is.undefined(options.retryDelayMs)) {
508
+ Guards.integer(FetchHelper._CLASS_NAME, "options.retryDelayMs", options.retryDelayMs);
509
+ }
510
+ }
511
+ let controller;
512
+ let timerId;
513
+ const retryCount = options?.retryCount ?? 1;
514
+ const baseDelayMilliseconds = options?.retryDelayMs ?? 3000;
515
+ let lastError;
516
+ let attempt;
517
+ for (attempt = 0; attempt < retryCount; attempt++) {
518
+ if (attempt > 0) {
519
+ const exponentialBackoffDelay = baseDelayMilliseconds * Math.pow(2, attempt - 1);
520
+ await new Promise(resolve => globalThis.setTimeout(resolve, exponentialBackoffDelay));
521
+ }
522
+ if (options?.timeoutMs !== undefined) {
523
+ controller = new AbortController();
524
+ timerId = globalThis.setTimeout(() => {
525
+ if (controller) {
526
+ controller.abort();
527
+ }
528
+ }, options?.timeoutMs);
529
+ }
530
+ try {
531
+ const requestOptions = {
532
+ method,
533
+ headers: options?.headers,
534
+ body: method === HttpMethod.POST || method === HttpMethod.PUT ? body : undefined,
535
+ signal: controller ? controller.signal : undefined
536
+ };
537
+ if (Is.boolean(options?.includeCredentials)) {
538
+ requestOptions.credentials = "include";
539
+ }
540
+ const response = await fetch(url, requestOptions);
541
+ if (!response.ok && retryCount > 1) {
542
+ lastError = new FetchError(source, `${FetchHelper._CLASS_NAME_CAMEL_CASE}.general`, response.status ?? HttpStatusCode.internalServerError, {
543
+ url,
544
+ statusText: response.statusText
545
+ });
546
+ }
547
+ else {
548
+ return response;
549
+ }
550
+ }
551
+ catch (err) {
552
+ const isErr = Is.object(err);
553
+ if (isErr && Is.stringValue(err.message) && err.message.includes("Failed to fetch")) {
554
+ lastError = new FetchError(source, `${FetchHelper._CLASS_NAME_CAMEL_CASE}.connectivity`, HttpStatusCode.serviceUnavailable, {
555
+ url
556
+ });
557
+ }
558
+ else {
559
+ const isAbort = isErr && err.name === "AbortError";
560
+ const props = { url };
561
+ let httpStatus = HttpStatusCode.internalServerError;
562
+ if (isAbort) {
563
+ httpStatus = HttpStatusCode.requestTimeout;
564
+ }
565
+ else if (isErr && "httpStatus" in err) {
566
+ httpStatus = err.httpStatus;
567
+ }
568
+ if (isErr && "statusText" in err) {
569
+ props.statusText = err.statusText;
570
+ }
571
+ lastError = new FetchError(source, `${FetchHelper._CLASS_NAME_CAMEL_CASE}.${isAbort ? "timeout" : "general"}`, httpStatus, props);
572
+ }
573
+ }
574
+ finally {
575
+ if (timerId) {
576
+ globalThis.clearTimeout(timerId);
577
+ }
578
+ }
579
+ }
580
+ if (retryCount > 1 && attempt === retryCount) {
581
+ // False positive as FetchError is derived from Error
582
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
583
+ throw new FetchError(source, `${FetchHelper._CLASS_NAME_CAMEL_CASE}.retryLimitExceeded`, HttpStatusCode.internalServerError, { url }, lastError);
584
+ }
585
+ throw lastError;
586
+ }
587
+ /**
588
+ * Perform a request in json format.
589
+ * @param source The source for the request.
590
+ * @param url The url for the request.
591
+ * @param method The http method.
592
+ * @param requestData Request to send to the endpoint.
593
+ * @param options Options for sending the requests.
594
+ * @returns The response.
595
+ */
596
+ static async fetchJson(source, url, method, requestData, options) {
597
+ if (Is.integer(options?.cacheTtlMs) && options.cacheTtlMs >= 0) {
598
+ // The cache option is set, so call the same method again but without
599
+ // the cache option to get the result and cache it.
600
+ const cacheResponse = AsyncCache.exec(`${FetchHelper._CACHE_PREFIX}${url}`, options.cacheTtlMs, async () => FetchHelper.fetchJson(source, url, method, requestData, ObjectHelper.omit(options, ["cacheTtlMs"])));
601
+ // If the return value is a promise return it, otherwise continue
602
+ // with the regular processing.
603
+ if (Is.promise(cacheResponse)) {
604
+ return cacheResponse;
605
+ }
606
+ }
607
+ options ??= {};
608
+ options.headers ??= {};
609
+ if (Is.undefined(options.headers[HeaderTypes.ContentType]) &&
610
+ (method === HttpMethod.POST || method === HttpMethod.PUT || method === HttpMethod.PATCH)) {
611
+ options.headers[HeaderTypes.ContentType] = MimeTypes.Json;
612
+ }
613
+ const response = await FetchHelper.fetch(source, url, method, requestData ? JSON.stringify(requestData) : undefined, options);
614
+ if (response.ok) {
615
+ if (response.status === HttpStatusCode.noContent) {
616
+ return {};
617
+ }
618
+ try {
619
+ return (await response.json());
620
+ }
621
+ catch (err) {
622
+ // False positive as FetchError is derived from Error
623
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
624
+ throw new FetchError(source, `${FetchHelper._CLASS_NAME_CAMEL_CASE}.decodingJSON`, HttpStatusCode.badRequest, { url }, err);
625
+ }
626
+ }
627
+ const errorResponseData = await response.json();
628
+ // False positive as FetchError is derived from Error
629
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
630
+ throw new FetchError(source, `${FetchHelper._CLASS_NAME_CAMEL_CASE}.failureStatusText`, response.status, {
631
+ statusText: response.statusText,
632
+ url
633
+ }, errorResponseData);
634
+ }
635
+ /**
636
+ * Perform a request for binary data.
637
+ * @param source The source for the request.
638
+ * @param url The url for the request.
639
+ * @param method The http method.
640
+ * @param requestData Request to send to the endpoint.
641
+ * @param options Options for sending the requests.
642
+ * @returns The response.
643
+ */
644
+ static async fetchBinary(source, url, method, requestData, options) {
645
+ if (Is.integer(options?.cacheTtlMs) && options.cacheTtlMs >= 0) {
646
+ // The cache option is set, so call the same method again but without
647
+ // the cache option to get the result and cache it.
648
+ const cacheResponse = AsyncCache.exec(`${FetchHelper._CACHE_PREFIX}${url}`, options.cacheTtlMs * 1000, async () => FetchHelper.fetchBinary(source, url, method, requestData, ObjectHelper.omit(options, ["cacheTtlMs"])));
649
+ // If the return value is a promise return it, otherwise continue
650
+ // with the regular processing.
651
+ if (Is.promise(cacheResponse)) {
652
+ return cacheResponse;
653
+ }
654
+ }
655
+ options ??= {};
656
+ options.headers ??= {};
657
+ if (Is.undefined(options.headers[HeaderTypes.ContentType])) {
658
+ options.headers[HeaderTypes.ContentType] = MimeTypes.OctetStream;
659
+ }
660
+ const response = await this.fetch(source, url, method, requestData, options);
661
+ if (response.ok) {
662
+ if (method === HttpMethod.GET) {
663
+ if (response.status === HttpStatusCode.noContent) {
664
+ return new Uint8Array();
665
+ }
666
+ return new Uint8Array(await response.arrayBuffer());
667
+ }
668
+ try {
669
+ return (await response.json());
670
+ }
671
+ catch (err) {
672
+ // False positive as FetchError is derived from Error
673
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
674
+ throw new FetchError(source, `${FetchHelper._CLASS_NAME_CAMEL_CASE}.decodingJSON`, HttpStatusCode.badRequest, { url }, err);
675
+ }
676
+ }
677
+ const errorResponseData = await response.json();
678
+ // False positive as FetchError is derived from Error
679
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
680
+ throw new FetchError(source, `${FetchHelper._CLASS_NAME_CAMEL_CASE}.failureStatusText`, response.status, {
681
+ statusText: response.statusText,
682
+ url
683
+ }, errorResponseData);
684
+ }
685
+ /**
686
+ * Clears the cache.
687
+ */
688
+ static clearCache() {
689
+ AsyncCache.clearCache(FetchHelper._CACHE_PREFIX);
690
+ }
691
+ /**
692
+ * Get a cache entry.
693
+ * @param url The url for the request.
694
+ * @returns The cache entry if it exists.
695
+ */
696
+ static async getCacheEntry(url) {
697
+ return AsyncCache.get(`${FetchHelper._CACHE_PREFIX}${url}`);
698
+ }
699
+ /**
700
+ * Remove a cache entry.
701
+ * @param url The url for the request.
702
+ */
703
+ static removeCacheEntry(url) {
704
+ AsyncCache.remove(`${FetchHelper._CACHE_PREFIX}${url}`);
705
+ }
706
+ }
707
+
708
+ // Copyright 2024 IOTA Stiftung.
709
+ // SPDX-License-Identifier: Apache-2.0.
710
+ /**
711
+ * Class to encode and decode JSON Web Tokens.
712
+ */
713
+ class Jwt {
714
+ /**
715
+ * Runtime name for the class.
716
+ * @internal
717
+ */
718
+ static _CLASS_NAME = "Jwt";
719
+ /**
720
+ * Encode a token.
721
+ * @param header The header to encode.
722
+ * @param payload The payload to encode.
723
+ * @param key The key for signing the token, can be omitted if a signer is provided.
724
+ * @returns The encoded token.
725
+ */
726
+ static async encode(header, payload, key) {
727
+ Guards.object(Jwt._CLASS_NAME, "header", header);
728
+ Guards.arrayOneOf(Jwt._CLASS_NAME, "header.alg", header.alg, Object.values(JwtAlgorithms));
729
+ Guards.object(Jwt._CLASS_NAME, "payload", payload);
730
+ Guards.uint8Array(Jwt._CLASS_NAME, "key", key);
731
+ return Jwt.internalEncode(header, payload, key);
732
+ }
733
+ /**
734
+ * Encode a token.
735
+ * @param header The header to encode.
736
+ * @param payload The payload to encode.
737
+ * @param signer Custom signer method.
738
+ * @returns The encoded token.
739
+ */
740
+ static async encodeWithSigner(header, payload, signer) {
741
+ Guards.object(Jwt._CLASS_NAME, "header", header);
742
+ Guards.arrayOneOf(Jwt._CLASS_NAME, "header.alg", header.alg, Object.values(JwtAlgorithms));
743
+ Guards.object(Jwt._CLASS_NAME, "payload", payload);
744
+ Guards.function(Jwt._CLASS_NAME, "signer", signer);
745
+ return Jwt.internalEncode(header, payload, undefined, signer);
746
+ }
747
+ /**
748
+ * Decode a token.
749
+ * @param token The token to decode.
750
+ * @returns The decoded payload.
751
+ */
752
+ static async decode(token) {
753
+ Guards.stringValue(Jwt._CLASS_NAME, "token", token);
754
+ let header;
755
+ let payload;
756
+ let signature;
757
+ const segments = token.split(".");
758
+ if (segments.length > 0) {
759
+ try {
760
+ const bytesHeader = Converter.base64UrlToBytes(segments[0]);
761
+ header = JSON.parse(Converter.bytesToUtf8(bytesHeader));
762
+ }
763
+ catch { }
764
+ }
765
+ if (segments.length > 1) {
766
+ try {
767
+ const bytesPayload = Converter.base64UrlToBytes(segments[1]);
768
+ payload = JSON.parse(Converter.bytesToUtf8(bytesPayload));
769
+ }
770
+ catch { }
771
+ }
772
+ if (segments.length > 2) {
773
+ signature = Converter.base64UrlToBytes(segments[2]);
774
+ }
775
+ return {
776
+ header,
777
+ payload,
778
+ signature
779
+ };
780
+ }
781
+ /**
782
+ * Verify a token.
783
+ * @param token The token to verify.
784
+ * @param key The key for verifying the token
785
+ * @returns The decoded payload.
786
+ */
787
+ static async verify(token, key) {
788
+ Guards.stringValue(Jwt._CLASS_NAME, "token", token);
789
+ Guards.uint8Array(Jwt._CLASS_NAME, "key", key);
790
+ const decoded = await Jwt.decode(token);
791
+ const verified = await Jwt.verifySignature(decoded.header, decoded.payload, decoded.signature, key);
792
+ return {
793
+ verified,
794
+ ...decoded
795
+ };
796
+ }
797
+ /**
798
+ * Verify a token.
799
+ * @param token The token to verify.
800
+ * @param verifier Custom verification method.
801
+ * @returns The decoded payload.
802
+ */
803
+ static async verifyWithVerifier(token, verifier) {
804
+ Guards.stringValue(Jwt._CLASS_NAME, "token", token);
805
+ Guards.function(Jwt._CLASS_NAME, "verifier", verifier);
806
+ Guards.stringValue(Jwt._CLASS_NAME, "token", token);
807
+ const decoded = await Jwt.decode(token);
808
+ const verified = await Jwt.verifySignature(decoded.header, decoded.payload, decoded.signature, undefined, verifier);
809
+ return {
810
+ verified,
811
+ ...decoded
812
+ };
813
+ }
814
+ /**
815
+ * Verify a token by parts.
816
+ * @param header The header to verify.
817
+ * @param payload The payload to verify.
818
+ * @param signature The signature to verify.
819
+ * @param key The key for verifying the token, if not provided no verification occurs.
820
+ * @param verifier Custom verification method.
821
+ * @returns True if the parts are verified.
822
+ */
823
+ static async verifySignature(header, payload, signature, key, verifier) {
824
+ const hasKey = Is.notEmpty(key);
825
+ const hasVerifier = Is.notEmpty(verifier);
826
+ if (!hasKey && !hasVerifier) {
827
+ throw new GeneralError(Jwt._CLASS_NAME, "noKeyOrVerifier");
828
+ }
829
+ let verified = false;
830
+ if (Is.object(header) &&
831
+ Is.object(payload) &&
832
+ Is.uint8Array(signature) &&
833
+ Is.arrayOneOf(header.alg, Object.values(JwtAlgorithms))) {
834
+ const segments = [];
835
+ const headerBytes = Converter.utf8ToBytes(JSON.stringify(header));
836
+ segments.push(Converter.bytesToBase64Url(headerBytes));
837
+ const payloadBytes = Converter.utf8ToBytes(JSON.stringify(payload));
838
+ segments.push(Converter.bytesToBase64Url(payloadBytes));
839
+ const jwtHeaderAndPayload = Converter.utf8ToBytes(segments.join("."));
840
+ verifier ??= async (alg, k, p, s) => Jwt.defaultVerifier(alg, k, p, s);
841
+ verified = await verifier(header.alg, key, jwtHeaderAndPayload, signature);
842
+ }
843
+ return verified;
844
+ }
845
+ /**
846
+ * The default signer for the JWT.
847
+ * @param alg The algorithm to use.
848
+ * @param key The key to sign with.
849
+ * @param payload The payload to sign.
850
+ * @returns The signature.
851
+ */
852
+ static async defaultSigner(alg, key, payload) {
853
+ Guards.uint8Array(Jwt._CLASS_NAME, "key", key);
854
+ Guards.uint8Array(Jwt._CLASS_NAME, "payload", payload);
855
+ if (alg === "HS256") {
856
+ const algo = new HmacSha256(key);
857
+ return algo.update(payload).digest();
858
+ }
859
+ return Ed25519.sign(key, payload);
860
+ }
861
+ /**
862
+ * The default verifier for the JWT.
863
+ * @param alg The algorithm to use.
864
+ * @param key The key to verify with.
865
+ * @param payload The payload to verify.
866
+ * @param signature The signature to verify.
867
+ * @returns True if the signature was verified.
868
+ */
869
+ static async defaultVerifier(alg, key, payload, signature) {
870
+ Guards.uint8Array(Jwt._CLASS_NAME, "key", key);
871
+ Guards.uint8Array(Jwt._CLASS_NAME, "payload", payload);
872
+ Guards.uint8Array(Jwt._CLASS_NAME, "signature", signature);
873
+ if (alg === "HS256") {
874
+ const algo = new HmacSha256(key);
875
+ const sigBytes = algo.update(payload).digest();
876
+ return ArrayHelper.matches(sigBytes, signature);
877
+ }
878
+ return Ed25519.verify(key, payload, signature);
879
+ }
880
+ /**
881
+ * Encode a token.
882
+ * @param header The header to encode.
883
+ * @param payload The payload to encode.
884
+ * @param key The key for signing the token, can be omitted if a signer is provided.
885
+ * @param signer Custom signer method.
886
+ * @returns The encoded token.
887
+ * @internal
888
+ */
889
+ static async internalEncode(header, payload, key, signer) {
890
+ const hasKey = Is.notEmpty(key);
891
+ const hasSigner = Is.notEmpty(signer);
892
+ if (!hasKey && !hasSigner) {
893
+ throw new GeneralError(Jwt._CLASS_NAME, "noKeyOrSigner");
894
+ }
895
+ signer ??= async (alg, k, p) => Jwt.defaultSigner(alg, k, p);
896
+ if (Is.undefined(header.typ)) {
897
+ header.typ = "JWT";
898
+ }
899
+ const segments = [];
900
+ const headerBytes = Converter.utf8ToBytes(JSON.stringify(header));
901
+ segments.push(Converter.bytesToBase64Url(headerBytes));
902
+ const payloadBytes = Converter.utf8ToBytes(JSON.stringify(payload));
903
+ segments.push(Converter.bytesToBase64Url(payloadBytes));
904
+ const jwtHeaderAndPayload = Converter.utf8ToBytes(segments.join("."));
905
+ const sigBytes = await signer(header.alg, key, jwtHeaderAndPayload);
906
+ segments.push(Converter.bytesToBase64Url(sigBytes));
907
+ return segments.join(".");
908
+ }
909
+ }
910
+
911
+ // Copyright 2024 IOTA Stiftung.
912
+ // SPDX-License-Identifier: Apache-2.0.
913
+ /**
914
+ * Class to help with mime types.
915
+ */
916
+ class MimeTypeHelper {
917
+ /**
918
+ * Detect the mime type from a byte array.
919
+ * @param data The data to test.
920
+ * @returns The mime type if detected.
921
+ */
922
+ static async detect(data) {
923
+ if (!Is.uint8Array(data)) {
924
+ return undefined;
925
+ }
926
+ // Image
927
+ if (MimeTypeHelper.checkBytes(data, [0x47, 0x49, 0x46])) {
928
+ return MimeTypes.Gif;
929
+ }
930
+ if (MimeTypeHelper.checkBytes(data, [0x42, 0x4d])) {
931
+ return MimeTypes.Bmp;
932
+ }
933
+ if (MimeTypeHelper.checkBytes(data, [0xff, 0xd8, 0xff])) {
934
+ return MimeTypes.Jpeg;
935
+ }
936
+ if (MimeTypeHelper.checkBytes(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
937
+ return MimeTypes.Png;
938
+ }
939
+ if (MimeTypeHelper.checkBytes(data, [0x49, 0x49, 0x2a, 0x00]) ||
940
+ MimeTypeHelper.checkBytes(data, [0x4d, 0x4d, 0x00, 0x2a])) {
941
+ return MimeTypes.Tiff;
942
+ }
943
+ // Compression
944
+ if (MimeTypeHelper.checkBytes(data, [0x1f, 0x8b, 0x8])) {
945
+ return MimeTypes.Gzip;
946
+ }
947
+ if (MimeTypeHelper.checkBytes(data, [0x42, 0x5a, 0x68])) {
948
+ return MimeTypes.Bzip2;
949
+ }
950
+ if (MimeTypeHelper.checkBytes(data, [0x50, 0x4b, 0x3, 0x4])) {
951
+ return MimeTypes.Zip;
952
+ }
953
+ // Documents
954
+ if (MimeTypeHelper.checkText(data, ["%PDF"])) {
955
+ return MimeTypes.Pdf;
956
+ }
957
+ // Lookup svg before xml, as svg are xml files as well
958
+ const asText = Converter.bytesToUtf8(data);
959
+ if (asText.includes("<svg")) {
960
+ return MimeTypes.Svg;
961
+ }
962
+ if (MimeTypeHelper.checkText(data, ["<?xml ", "<message"])) {
963
+ return MimeTypes.Xml;
964
+ }
965
+ if (MimeTypeHelper.checkBytes(data, [0xef, 0xbb, 0xbf]) &&
966
+ MimeTypeHelper.checkText(data, ["<?xml "], 3)) {
967
+ // UTF-8-BOM
968
+ return MimeTypes.Xml;
969
+ }
970
+ if (StringHelper.isUtf8(data)) {
971
+ try {
972
+ JSON.parse(new TextDecoder().decode(data));
973
+ return MimeTypes.Json;
974
+ }
975
+ catch {
976
+ return MimeTypes.PlainText;
977
+ }
978
+ }
979
+ }
980
+ /**
981
+ * Return the default extension for a mime type.
982
+ * @param mimeType The mimetype to get the extension for.
983
+ * @returns The extension for the mime type.
984
+ */
985
+ static defaultExtension(mimeType) {
986
+ if (!Is.stringValue(mimeType)) {
987
+ return undefined;
988
+ }
989
+ const lookup = {
990
+ [MimeTypes.PlainText]: "txt",
991
+ [MimeTypes.Html]: "html",
992
+ [MimeTypes.Javascript]: "js",
993
+ [MimeTypes.Json]: "json",
994
+ [MimeTypes.JsonLd]: "jsonld",
995
+ [MimeTypes.Xml]: "xml",
996
+ [MimeTypes.OctetStream]: "bin",
997
+ [MimeTypes.Gzip]: "gzip",
998
+ [MimeTypes.Bzip2]: "bz2",
999
+ [MimeTypes.Zip]: "zip",
1000
+ [MimeTypes.Pdf]: "pfd",
1001
+ [MimeTypes.Gif]: "gif",
1002
+ [MimeTypes.Bmp]: "bmp",
1003
+ [MimeTypes.Jpeg]: "jpeg",
1004
+ [MimeTypes.Png]: "png",
1005
+ [MimeTypes.Tiff]: "tif",
1006
+ [MimeTypes.Svg]: "svg",
1007
+ [MimeTypes.WebP]: "webp",
1008
+ [MimeTypes.Mp4]: "mp4",
1009
+ [MimeTypes.Mpeg]: "mpg",
1010
+ [MimeTypes.Webm]: "webm"
1011
+ };
1012
+ return lookup[mimeType];
1013
+ }
1014
+ /**
1015
+ * Check if the bytes match.
1016
+ * @param data The data to look at.
1017
+ * @param bytes The bytes to try and match.
1018
+ * @param startOffset Start offset in the data.
1019
+ * @returns True if the bytes match.
1020
+ * @internal
1021
+ */
1022
+ static checkBytes(data, bytes, startOffset = 0) {
1023
+ if (data.length - startOffset < bytes.length) {
1024
+ return false;
1025
+ }
1026
+ for (let i = 0; i < bytes.length; i++) {
1027
+ if (data[i + startOffset] !== bytes[i]) {
1028
+ return false;
1029
+ }
1030
+ }
1031
+ return true;
1032
+ }
1033
+ /**
1034
+ * Check if the text matches.
1035
+ * @param data The data to look at.
1036
+ * @param texts The text to try and match.
1037
+ * @param startOffset Start offset in the data.
1038
+ * @returns True if the bytes match.
1039
+ * @internal
1040
+ */
1041
+ static checkText(data, texts, startOffset = 0) {
1042
+ return texts.some(text => MimeTypeHelper.checkBytes(data, Array.from(Converter.utf8ToBytes(text)), startOffset));
1043
+ }
1044
+ }
1045
+
1046
+ export { FetchError, FetchHelper, HeaderTypes, HttpMethod, HttpStatusCode, Jwt, JwtAlgorithms, MimeTypeHelper, MimeTypes };