Compare commits
159 Commits
Author | SHA1 | Date |
---|---|---|
Derrick Hammer | 37789394f4 | |
Derrick Hammer | 6ba0e4af07 | |
Derrick Hammer | 17c3addeb2 | |
Derrick Hammer | a91b58924b | |
Derrick Hammer | f85bd2b728 | |
Derrick Hammer | f77af25b84 | |
Derrick Hammer | 645df0c8d0 | |
Derrick Hammer | fb8a1b3c7d | |
Derrick Hammer | afb3d7fa3d | |
Derrick Hammer | b6f335bc09 | |
Derrick Hammer | 6bdc43dd85 | |
Derrick Hammer | c1b55c5d98 | |
Derrick Hammer | 300f6876c6 | |
Derrick Hammer | a2bdba0599 | |
Derrick Hammer | 6a02b9f256 | |
Derrick Hammer | a53e54049d | |
Derrick Hammer | c32edef621 | |
Derrick Hammer | f31b343aa0 | |
Derrick Hammer | 5bee4f9e8c | |
Derrick Hammer | 86ce3874ec | |
Derrick Hammer | f2d4a146c5 | |
Derrick Hammer | 0f6495afa8 | |
Derrick Hammer | dabaaf3ced | |
Derrick Hammer | 4561e06b61 | |
Derrick Hammer | dfe79e2fc2 | |
Derrick Hammer | da7c405905 | |
Derrick Hammer | 560c7c374e | |
Derrick Hammer | 268df87b63 | |
Derrick Hammer | a9cd7c54a9 | |
Derrick Hammer | 2b2b846c45 | |
Derrick Hammer | 8846a23b8a | |
Derrick Hammer | 2ee6124637 | |
Derrick Hammer | e86187ac77 | |
Derrick Hammer | c157999fd3 | |
Derrick Hammer | b33c856654 | |
Derrick Hammer | 8e368e4e0c | |
Derrick Hammer | 61c9ef880a | |
Derrick Hammer | faf7492ff8 | |
Derrick Hammer | 12dd703bdb | |
Derrick Hammer | 457c8081a3 | |
Derrick Hammer | 07048c526b | |
Derrick Hammer | c34a6ef094 | |
Derrick Hammer | eb242bd3c7 | |
Derrick Hammer | 1eb9ebbdf8 | |
Derrick Hammer | 5bc2915655 | |
Derrick Hammer | 7bc48484a2 | |
Derrick Hammer | ba370250a2 | |
Derrick Hammer | b4d1e0ec67 | |
Derrick Hammer | 0dd72358da | |
Derrick Hammer | b1179a868f | |
Derrick Hammer | 64a453923a | |
Derrick Hammer | f22a7ba511 | |
Derrick Hammer | 54ca99d8d1 | |
Derrick Hammer | 83b7d78af1 | |
Derrick Hammer | 7cdcfe3b3e | |
Derrick Hammer | e9f4b03979 | |
Derrick Hammer | 9510735005 | |
Derrick Hammer | 853db73634 | |
Derrick Hammer | 3c9022d8c7 | |
Derrick Hammer | bc6d545501 | |
Derrick Hammer | 39ccac179d | |
Derrick Hammer | 8e172ebef9 | |
Derrick Hammer | 10e41875e6 | |
Derrick Hammer | dd98f51f4b | |
Derrick Hammer | 109278021d | |
semantic-release-bot | 89cf5b398c | |
Derrick Hammer | 8be4e311fd | |
Derrick Hammer | d1ce4a1ae0 | |
semantic-release-bot | 42cbb29692 | |
Derrick Hammer | 11a033b3f7 | |
Derrick Hammer | c4c0a947a9 | |
semantic-release-bot | b97a212f57 | |
Derrick Hammer | 7d466b17c3 | |
Derrick Hammer | 115caeeaee | |
semantic-release-bot | a697be1865 | |
Derrick Hammer | 27d739a816 | |
Derrick Hammer | d78ad5eb29 | |
semantic-release-bot | 4125ff524a | |
Derrick Hammer | f66969c698 | |
Derrick Hammer | ffad64a787 | |
semantic-release-bot | f3365838d9 | |
Derrick Hammer | c3377135b6 | |
Derrick Hammer | bb7c7dcece | |
semantic-release-bot | ef072d01b4 | |
Derrick Hammer | 36529bfe06 | |
Derrick Hammer | fcdd42f2a3 | |
semantic-release-bot | 3a5d96f3be | |
Derrick Hammer | 9f59a2389d | |
Derrick Hammer | bf230478b7 | |
semantic-release-bot | e3b3f319a8 | |
Derrick Hammer | f330c673f4 | |
Derrick Hammer | 389a15eef4 | |
Derrick Hammer | c61f2f6127 | |
semantic-release-bot | a6e544bebb | |
Derrick Hammer | edda00090a | |
Derrick Hammer | e4dc098547 | |
semantic-release-bot | c6be47605d | |
Derrick Hammer | 55e07bb689 | |
Derrick Hammer | 6388669ca8 | |
semantic-release-bot | 12d54f454f | |
Derrick Hammer | 9ecea29244 | |
Derrick Hammer | 820cc5bd89 | |
semantic-release-bot | 881ad76d76 | |
Derrick Hammer | 17d594265c | |
Derrick Hammer | e24d352d4e | |
semantic-release-bot | 236bae5a90 | |
Derrick Hammer | 68f9c91c62 | |
Derrick Hammer | aea236067e | |
semantic-release-bot | 0239aacef6 | |
Derrick Hammer | 6a3eea92b7 | |
Derrick Hammer | eb3f2ee5c9 | |
semantic-release-bot | 03cea1aec4 | |
Derrick Hammer | a86fdff26b | |
Derrick Hammer | 502c8fb079 | |
semantic-release-bot | e0038fc095 | |
Derrick Hammer | c358fa3acc | |
Derrick Hammer | 74407cf9a1 | |
semantic-release-bot | 8ecc0d93be | |
Derrick Hammer | a577ee694d | |
Derrick Hammer | ce47696d90 | |
semantic-release-bot | 08aeb6bb64 | |
Derrick Hammer | 354506bbb1 | |
Derrick Hammer | fd959c91df | |
semantic-release-bot | 254cba9349 | |
Derrick Hammer | e5559c75b3 | |
Derrick Hammer | 27c4e94072 | |
semantic-release-bot | 838fb98abc | |
Derrick Hammer | be0f6298c3 | |
Derrick Hammer | 83143dc960 | |
Derrick Hammer | 02eefe442c | |
Derrick Hammer | 5c19245b4b | |
semantic-release-bot | 4ed50d4ee7 | |
Derrick Hammer | fbffadab8c | |
Derrick Hammer | 28d7c30225 | |
semantic-release-bot | 24632570e5 | |
Derrick Hammer | 4da2719c26 | |
Derrick Hammer | 300eb99bfc | |
semantic-release-bot | e6951fc9fe | |
Derrick Hammer | 31bf33c9a7 | |
Derrick Hammer | 50ed08ac6a | |
semantic-release-bot | 19b546c8de | |
Derrick Hammer | 8b4d189c80 | |
Derrick Hammer | bfce51d1cc | |
semantic-release-bot | 99a661c46e | |
Derrick Hammer | 6296c1ef10 | |
Derrick Hammer | 6b17e32cca | |
Derrick Hammer | 0dd0197c89 | |
Derrick Hammer | 672b462bbe | |
Derrick Hammer | 41274582b9 | |
Derrick Hammer | 04143ec463 | |
Derrick Hammer | 561b4a0685 | |
Derrick Hammer | 395b8a6ca7 | |
Derrick Hammer | 80d5d2d027 | |
Derrick Hammer | b39b852498 | |
Derrick Hammer | 618806c45a | |
Derrick Hammer | 2752499444 | |
Derrick Hammer | 2a57fb2a7e | |
Derrick Hammer | bf8d216f49 | |
Derrick Hammer | b3671237b9 |
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "master",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"preset": [
|
||||
"@lumeweb/node-library-preset"
|
||||
],
|
||||
"config": {
|
||||
"tsconfig": {
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
# [0.1.0-develop.29](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.28...v0.1.0-develop.29) (2024-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* set binaryType ([d1ce4a1](https://git.lumeweb.com/LumeWeb/s5-js/commit/d1ce4a1ae039dba8ae5497d6106730c44801b83e))
|
||||
|
||||
# [0.1.0-develop.28](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.27...v0.1.0-develop.28) (2024-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use addEventListener, on is not available in all environments ([c4c0a94](https://git.lumeweb.com/LumeWeb/s5-js/commit/c4c0a947a93c60ea2364b7de9b975d6dccc188cc))
|
||||
|
||||
# [0.1.0-develop.27](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.26...v0.1.0-develop.27) (2023-12-28)
|
||||
|
||||
# [0.1.0-develop.26](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.25...v0.1.0-develop.26) (2023-12-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use on, once is not available in all environments ([d78ad5e](https://git.lumeweb.com/LumeWeb/s5-js/commit/d78ad5eb2996dc11cc67cf2e4ff1e1b3b62e1519))
|
||||
|
||||
# [0.1.0-develop.25](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.24...v0.1.0-develop.25) (2023-12-28)
|
||||
|
||||
# [0.1.0-develop.24](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.23...v0.1.0-develop.24) (2023-12-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensure we are using a ws protocol ([bb7c7dc](https://git.lumeweb.com/LumeWeb/s5-js/commit/bb7c7dcece26e4ff7de338ca2bf7bc12046edebe))
|
||||
|
||||
# [0.1.0-develop.23](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.22...v0.1.0-develop.23) (2023-12-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return GetMetadataResponse ([fcdd42f](https://git.lumeweb.com/LumeWeb/s5-js/commit/fcdd42f2a32281ba4dabf4c548657b40b9b4a4ef))
|
||||
|
||||
# [0.1.0-develop.22](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.21...v0.1.0-develop.22) (2023-12-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* need to pass endpointPath ([bf23047](https://git.lumeweb.com/LumeWeb/s5-js/commit/bf230478b792860ce16b68b976853d9a7d828d0f))
|
||||
|
||||
# [0.1.0-develop.21](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.20...v0.1.0-develop.21) (2023-12-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* on existing entry, need to update the entry data ([389a15e](https://git.lumeweb.com/LumeWeb/s5-js/commit/389a15eef41c941e36d2b08ca47c775f13bbba55))
|
||||
|
||||
# [0.1.0-develop.20](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.19...v0.1.0-develop.20) (2023-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensure we prefix the CID ed22519 type to the registry pubkey ([e4dc098](https://git.lumeweb.com/LumeWeb/s5-js/commit/e4dc0985471747404d82b434f9b8edc97bf0ee1b))
|
||||
|
||||
# [0.1.0-develop.19](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.18...v0.1.0-develop.19) (2023-12-12)
|
||||
|
||||
# [0.1.0-develop.18](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.17...v0.1.0-develop.18) (2023-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* need to pass as query, not data ([820cc5b](https://git.lumeweb.com/LumeWeb/s5-js/commit/820cc5bd8924391305ab37e20df53fb9f68924f5))
|
||||
|
||||
# [0.1.0-develop.17](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.16...v0.1.0-develop.17) (2023-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use get not post ([e24d352](https://git.lumeweb.com/LumeWeb/s5-js/commit/e24d352d4eb6fd5baf16698f11c876e79a3040fd))
|
||||
|
||||
# [0.1.0-develop.16](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.15...v0.1.0-develop.16) (2023-12-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add getEntry and update createEntry to use it ([aea2360](https://git.lumeweb.com/LumeWeb/s5-js/commit/aea236067e6011502f6df2ec9856597b9fe5b1a9))
|
||||
|
||||
# [0.1.0-develop.15](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.14...v0.1.0-develop.15) (2023-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* need to use toRegistryEntry ([eb3f2ee](https://git.lumeweb.com/LumeWeb/s5-js/commit/eb3f2ee5c9da7b3b46a5d3d61c5dfed8ecfc167c))
|
||||
|
||||
# [0.1.0-develop.14](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.13...v0.1.0-develop.14) (2023-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* need to strip off multiformats prefix ([502c8fb](https://git.lumeweb.com/LumeWeb/s5-js/commit/502c8fb0795986b0a488ad3684c1c23f0b807a40))
|
||||
|
||||
# [0.1.0-develop.13](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.12...v0.1.0-develop.13) (2023-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Revert "fix: use base64urlpad" ([74407cf](https://git.lumeweb.com/LumeWeb/s5-js/commit/74407cf9a15c5c846592f3929646cdcbba98f032))
|
||||
|
||||
# [0.1.0-develop.12](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.11...v0.1.0-develop.12) (2023-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use base64urlpad ([ce47696](https://git.lumeweb.com/LumeWeb/s5-js/commit/ce47696d907513e6b5a78f3c71407e12b90fb952))
|
||||
|
||||
# [0.1.0-develop.11](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.10...v0.1.0-develop.11) (2023-12-12)
|
||||
|
||||
# [0.1.0-develop.10](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.9...v0.1.0-develop.10) (2023-12-12)
|
||||
|
||||
# [0.1.0-develop.9](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.8...v0.1.0-develop.9) (2023-12-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add createEntry ([02eefe4](https://git.lumeweb.com/LumeWeb/s5-js/commit/02eefe442cc8694898b0989dd8f0b6d15fb32b0e))
|
||||
* add publishEntry ([5c19245](https://git.lumeweb.com/LumeWeb/s5-js/commit/5c19245b4bfb75ece2c7ffa3153fbf49bc70e602))
|
||||
|
||||
# [0.1.0-develop.8](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.7...v0.1.0-develop.8) (2023-12-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add uploadWebapp ([28d7c30](https://git.lumeweb.com/LumeWeb/s5-js/commit/28d7c30225c400cf87fc6e6e2d7eda6061acb025))
|
||||
|
||||
# [0.1.0-develop.7](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.6...v0.1.0-develop.7) (2023-12-12)
|
||||
|
||||
# [0.1.0-develop.6](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.5...v0.1.0-develop.6) (2023-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add downloadData method ([50ed08a](https://git.lumeweb.com/LumeWeb/s5-js/commit/50ed08ac6a23f9fa7e43450c0f8942124fa175cd))
|
||||
|
||||
# [0.1.0-develop.5](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.4...v0.1.0-develop.5) (2023-12-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* missing mime dep ([bfce51d](https://git.lumeweb.com/LumeWeb/s5-js/commit/bfce51d1cc704fb4dc6752fc7fc878db97266d6b))
|
||||
|
||||
# [0.1.0-develop.4](https://git.lumeweb.com/LumeWeb/s5-js/compare/v0.1.0-develop.3...v0.1.0-develop.4) (2023-12-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add main to package.json ([6b17e32](https://git.lumeweb.com/LumeWeb/s5-js/commit/6b17e32cca10733fab1f04aecb06615c19a3e694))
|
4
LICENSE
4
LICENSE
|
@ -1,6 +1,8 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 LumeWeb
|
||||
Copyright (c) 2023 Hammer Technologies LLC
|
||||
Copyright (c) 2022 parajbs
|
||||
Copyright (c) 2020 Nebulous
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
|||
import { defineConfig } from "orval";
|
||||
|
||||
export default defineConfig({
|
||||
s5: {
|
||||
input: "./swagger.yaml",
|
||||
output: {
|
||||
mode: "split",
|
||||
workspace: "./src/generated",
|
||||
target: "openapi.ts",
|
||||
override: {
|
||||
mutator: {
|
||||
path: "../axios.ts",
|
||||
name: "customInstance",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "@lumeweb/s5-js",
|
||||
"version": "0.1.0-develop.29",
|
||||
"type": "module",
|
||||
"module": "lib/index.js",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "gitea@git.lumeweb.com:LumeWeb/s5-js.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"@lumeweb/node-library-preset": "^0.2.7",
|
||||
"@types/ws": "^8.5.10",
|
||||
"orval": "^6.24.0",
|
||||
"presetter": "*"
|
||||
},
|
||||
"readme": "ERROR: No README data found!",
|
||||
"scripts": {
|
||||
"prepare": "presetter bootstrap",
|
||||
"build": "run build",
|
||||
"patch-package": "patch-package",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lumeweb/libs5": "0.0.0-20240321165322",
|
||||
"@noble/hashes": "^1.3.2",
|
||||
"axios": "^1.6.2",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"mime": "^4.0.1",
|
||||
"multiformats": "^13.0.0",
|
||||
"p-defer": "^4.0.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"tus-js-client": "^4.0.0",
|
||||
"url-join": "^5.0.0",
|
||||
"url-parse": "^1.5.10"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
diff --git a/node_modules/tsc-alias/dist/utils/import-path-resolver.js b/node_modules/tsc-alias/dist/utils/import-path-resolver.js
|
||||
index ebaf620..227d4c7 100644
|
||||
--- a/node_modules/tsc-alias/dist/utils/import-path-resolver.js
|
||||
+++ b/node_modules/tsc-alias/dist/utils/import-path-resolver.js
|
||||
@@ -5,8 +5,9 @@ const normalizePath = require("normalize-path");
|
||||
const fs_1 = require("fs");
|
||||
const path_1 = require("path");
|
||||
const anyQuote = `["']`;
|
||||
+const excludeLocalImportSyntax = `[(?!\.)]`;
|
||||
const pathStringContent = `[^"'\r\n]+`;
|
||||
-const importString = `(?:${anyQuote}${pathStringContent}${anyQuote})`;
|
||||
+const importString = `(?:${anyQuote}${excludeLocalImportSyntax}${pathStringContent}${anyQuote})`;
|
||||
const funcStyle = `(?:\\b(?:import|require)\\s*\\(\\s*(\\/\\*.*\\*\\/\\s*)?${importString}\\s*\\))`;
|
||||
const globalStyle = `(?:\\bimport\\s+${importString})`;
|
||||
const fromStyle = `(?:\\bfrom\\s+${importString})`;
|
|
@ -0,0 +1,46 @@
|
|||
import Axios, { AxiosError, AxiosRequestConfig } from "axios";
|
||||
import { S5Error } from "./client.js";
|
||||
|
||||
export interface CancelablePromise<T> extends Promise<T> {
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
export const customInstance = <T>(
|
||||
config: AxiosRequestConfig,
|
||||
options?: AxiosRequestConfig,
|
||||
): CancelablePromise<T> => {
|
||||
const abort = new AbortController();
|
||||
|
||||
/*
|
||||
Hack to ensure that the data is passed to the request as an option.
|
||||
*/
|
||||
if (options?.data) {
|
||||
config = config || {};
|
||||
config.data = options.data;
|
||||
delete config.data;
|
||||
}
|
||||
|
||||
const instance = Axios.create({ baseURL: options?.baseURL });
|
||||
const promise = instance({
|
||||
signal: abort.signal,
|
||||
...config,
|
||||
...options,
|
||||
})
|
||||
.then(({ data }) => data)
|
||||
.catch((error) => {
|
||||
if (Axios.isCancel(error)) {
|
||||
return;
|
||||
}
|
||||
throw new S5Error(
|
||||
(error as AxiosError).message,
|
||||
(error as AxiosError).response?.status as number,
|
||||
);
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
promise.cancel = () => {
|
||||
abort.abort("Query was cancelled");
|
||||
};
|
||||
|
||||
return promise as CancelablePromise<T>;
|
||||
};
|
|
@ -0,0 +1,759 @@
|
|||
import {
|
||||
CustomDownloadOptions,
|
||||
CustomGetMetadataOptions,
|
||||
DEFAULT_DOWNLOAD_OPTIONS,
|
||||
DEFAULT_GET_METADATA_OPTIONS,
|
||||
MetadataResult,
|
||||
} from "./options/download.js";
|
||||
|
||||
import {addUrlQuery, ensureUrl} from "./utils/url.js";
|
||||
|
||||
import {
|
||||
CustomRegistryOptions,
|
||||
DEFAULT_GET_ENTRY_OPTIONS,
|
||||
DEFAULT_PUBLISH_ENTRY_OPTIONS,
|
||||
DEFAULT_SUBSCRIBE_ENTRY_OPTIONS,
|
||||
} from "./options/registry.js";
|
||||
import {CustomClientOptions, optionsToConfig} from "./utils/options.js";
|
||||
import {throwValidationError} from "./utils/validation.js";
|
||||
import {
|
||||
AccountPinsResponse,
|
||||
BasicUploadResponse, deleteS5DeleteCid,
|
||||
getS5AccountPins,
|
||||
getS5BlobCid,
|
||||
getS5DownloadCid,
|
||||
getS5MetadataCid, getS5PinCidStatus,
|
||||
getS5Registry, postS5PinCid,
|
||||
postS5Registry,
|
||||
postS5Upload,
|
||||
postS5UploadDirectory,
|
||||
PostS5UploadDirectoryParams,
|
||||
PostS5UploadResult,
|
||||
} from "./generated/index.js";
|
||||
import path from "path";
|
||||
import {customInstance} from "./axios.js";
|
||||
import {ensureBytes, equalBytes} from "@noble/curves/abstract/utils";
|
||||
import {concatBytes} from "@noble/hashes/utils";
|
||||
import {CID_HASH_TYPES} from "@lumeweb/libs5";
|
||||
import {buildRequestUrl} from "./request.js";
|
||||
import WS from "isomorphic-ws";
|
||||
import {
|
||||
CID,
|
||||
CID_TYPES,
|
||||
createKeyPair,
|
||||
KeyPairEd25519,
|
||||
Packer,
|
||||
SignedRegistryEntry,
|
||||
} from "@lumeweb/libs5";
|
||||
import {
|
||||
deserializeRegistryEntry,
|
||||
signRegistryEntry,
|
||||
verifyRegistryEntry,
|
||||
} from "@lumeweb/libs5/lib/service/registry.js";
|
||||
import {Buffer} from "buffer";
|
||||
import {AxiosError} from "axios";
|
||||
import {
|
||||
CustomUploadOptions,
|
||||
DEFAULT_UPLOAD_OPTIONS,
|
||||
TUS_ENDPOINT,
|
||||
UploadResult,
|
||||
} from "./options/upload.js";
|
||||
import {
|
||||
DetailedError,
|
||||
HttpRequest,
|
||||
Upload,
|
||||
UploadOptions,
|
||||
} from "tus-js-client";
|
||||
import {ensureFileObjectConsistency} from "./utils/file.js";
|
||||
import defer from "p-defer";
|
||||
import {Multihash} from "@lumeweb/libs5/lib/multihash.js";
|
||||
import {blake3} from "@noble/hashes/blake3";
|
||||
import {base64urlDecode, base64urlEncode} from "./utils/encoding.js";
|
||||
import {CustomPinOptions, DEFAULT_PIN_OPTIONS} from "./options/pin.js";
|
||||
|
||||
export class S5Error extends Error {
|
||||
public statusCode: number;
|
||||
|
||||
constructor(message: string, statusCode: number) {
|
||||
super(message);
|
||||
this.name = "S5Error";
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The S5 Client which can be used to access S5-net.
|
||||
*/
|
||||
export class S5Client {
|
||||
/**
|
||||
* The S5 Client which can be used to access S5-net.
|
||||
*
|
||||
* @class
|
||||
* @param [portalUrl] The initial portal URL to use to access S5, if specified. A request will be made to this URL to get the actual portal URL. To use the default portal while passing custom options, pass "".
|
||||
* @param [customOptions] Configuration for the client.
|
||||
*/
|
||||
constructor(portalUrl: string, customOptions: CustomClientOptions = {}) {
|
||||
if (!portalUrl) {
|
||||
throwValidationError("portalUrl", portalUrl, "parameter", "string");
|
||||
}
|
||||
this._portalUrl = ensureUrl(portalUrl);
|
||||
this._clientOptions = customOptions;
|
||||
}
|
||||
|
||||
private _clientOptions: CustomClientOptions;
|
||||
|
||||
get clientOptions(): CustomClientOptions {
|
||||
return this._clientOptions;
|
||||
}
|
||||
|
||||
set clientOptions(value: CustomClientOptions) {
|
||||
this._clientOptions = value;
|
||||
}
|
||||
|
||||
private _portalUrl: string;
|
||||
|
||||
get portalUrl(): string {
|
||||
return this._portalUrl;
|
||||
}
|
||||
|
||||
public static create(
|
||||
portalUrl: string,
|
||||
customOptions: CustomClientOptions = {},
|
||||
) {
|
||||
return new S5Client(portalUrl, customOptions);
|
||||
}
|
||||
|
||||
public async accountPins(
|
||||
customOptions: CustomClientOptions = {},
|
||||
): Promise<AccountPinsResponse> {
|
||||
const opts = {
|
||||
...this.clientOptions,
|
||||
...customOptions,
|
||||
...{
|
||||
endpointPath: "/s5/account/pins",
|
||||
baseUrl: await this.portalUrl,
|
||||
},
|
||||
};
|
||||
|
||||
const config = optionsToConfig(this, opts);
|
||||
|
||||
return await getS5AccountPins(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a download of the content of the cid within the browser.
|
||||
*
|
||||
* @param cid - 46-character cid, or a valid cid URL. Can be followed by a path. Note that the cid will not be encoded, so if your path might contain special characters, consider using `clientOptions.path`.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @param [customOptions.endpointDownload="/"] - The relative URL path of the portal endpoint to contact.
|
||||
* @returns - The full URL that was used.
|
||||
* @throws - Will throw if the cid does not contain a cid or if the path option is not a string.
|
||||
*/
|
||||
public async downloadFile(
|
||||
cid: string,
|
||||
customOptions?: CustomDownloadOptions,
|
||||
): Promise<string> {
|
||||
const url = await this.getCidUrl(cid, customOptions);
|
||||
|
||||
// Download the url.
|
||||
window.location.assign(url);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the full URL for the given cid.
|
||||
*
|
||||
* @param cid - Base64 cid, or a valid URL that contains a cid. See `downloadFile`.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @param [customOptions.endpointDownload="/"] - The relative URL path of the portal endpoint to contact.
|
||||
* @returns - The full URL for the cid.
|
||||
* @throws - Will throw if the cid does not contain a cid or if the path option is not a string.
|
||||
*/
|
||||
public async getCidUrl(
|
||||
cid: string,
|
||||
customOptions: CustomDownloadOptions = {},
|
||||
): Promise<string> {
|
||||
const opt = {...this.clientOptions, customOptions};
|
||||
return addUrlQuery(path.join(this.portalUrl, cid), {
|
||||
auth_token: opt.apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets only the metadata for the given cid without the contents.
|
||||
*
|
||||
* @param cid - Base64 cid.
|
||||
* @param [customOptions] - Additional settings that can optionally be set. See `downloadFile` for the full list.
|
||||
* @param [customOptions.endpointGetMetadata="/"] - The relative URL path of the portal endpoint to contact.
|
||||
* @returns - The metadata in JSON format. Empty if no metadata was found.
|
||||
* @throws - Will throw if the cid does not contain a cid .
|
||||
*/
|
||||
public async getMetadata(
|
||||
cid: string,
|
||||
customOptions: CustomGetMetadataOptions = {},
|
||||
): Promise<MetadataResult> {
|
||||
const config = optionsToConfig(
|
||||
this,
|
||||
DEFAULT_GET_METADATA_OPTIONS,
|
||||
customOptions,
|
||||
);
|
||||
|
||||
const response = await getS5MetadataCid(cid, config);
|
||||
|
||||
return {metadata: response};
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads in-memory data from a S5 cid.
|
||||
* @param cid - 46-character cid, or a valid cid URL.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @returns - The data
|
||||
*/
|
||||
public async downloadData(
|
||||
cid: string,
|
||||
customOptions: CustomDownloadOptions = {},
|
||||
): Promise<ArrayBuffer> {
|
||||
const config = optionsToConfig(
|
||||
this,
|
||||
DEFAULT_DOWNLOAD_OPTIONS,
|
||||
customOptions,
|
||||
);
|
||||
|
||||
return await (await getS5DownloadCid(cid, config)).arrayBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a proof for the given cid.
|
||||
* @param cid - 46-character cid, or a valid cid URL.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @returns - The data
|
||||
*/
|
||||
public async downloadProof(
|
||||
cid: string,
|
||||
customOptions: CustomDownloadOptions = {},
|
||||
): Promise<ArrayBuffer> {
|
||||
return this.downloadData(`${cid}.obao`, customOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a blob from the given cid. This will capture a 301 redirect to the actual blob location, then download the blob.
|
||||
* @param cid - 46-character cid, or a valid cid URL.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @returns - The data
|
||||
*/
|
||||
|
||||
async downloadBlob(
|
||||
cid: string,
|
||||
customOptions: CustomDownloadOptions = {},
|
||||
): Promise<ArrayBuffer> {
|
||||
const config = optionsToConfig(
|
||||
this,
|
||||
DEFAULT_DOWNLOAD_OPTIONS,
|
||||
customOptions,
|
||||
);
|
||||
|
||||
let location: string | null = null;
|
||||
|
||||
await getS5BlobCid(cid, {
|
||||
...config,
|
||||
responseType: "arraybuffer",
|
||||
beforeRedirect: (config, responseDetails) => {
|
||||
location = responseDetails.headers["location"];
|
||||
},
|
||||
});
|
||||
|
||||
if (!location) {
|
||||
throw new Error("Failed to download blob");
|
||||
}
|
||||
|
||||
return await customInstance<ArrayBuffer>(
|
||||
{
|
||||
url: `/s5/blob/${cid}`,
|
||||
method: "GET",
|
||||
responseType: "arraybuffer",
|
||||
},
|
||||
config,
|
||||
);
|
||||
}
|
||||
|
||||
public async subscribeToEntry(
|
||||
publicKey: Uint8Array,
|
||||
customOptions: CustomRegistryOptions = {},
|
||||
) {
|
||||
const opts = {
|
||||
...DEFAULT_SUBSCRIBE_ENTRY_OPTIONS,
|
||||
...this.clientOptions,
|
||||
...customOptions,
|
||||
} satisfies CustomRegistryOptions;
|
||||
|
||||
publicKey = ensureBytes("public key", publicKey, 32);
|
||||
publicKey = concatBytes(
|
||||
Uint8Array.from([CID_HASH_TYPES.ED25519]),
|
||||
publicKey,
|
||||
);
|
||||
|
||||
const url = await buildRequestUrl(this, {
|
||||
baseUrl: await this.portalUrl,
|
||||
endpointPath: opts.endpointSubscribeEntry,
|
||||
});
|
||||
|
||||
const wsUrl = url.replace(/^http/, "ws");
|
||||
|
||||
const socket = new WS(wsUrl);
|
||||
socket.binaryType = "arraybuffer";
|
||||
|
||||
socket.addEventListener("open", () => {
|
||||
const packer = new Packer();
|
||||
packer.pack(2);
|
||||
packer.pack(publicKey);
|
||||
|
||||
socket.send(packer.takeBytes());
|
||||
});
|
||||
|
||||
return {
|
||||
listen(cb: (entry: SignedRegistryEntry) => void) {
|
||||
socket.addEventListener("message", (data) => {
|
||||
cb(deserializeRegistryEntry(new Uint8Array(data.data as Buffer)));
|
||||
});
|
||||
},
|
||||
end() {
|
||||
if (
|
||||
[socket.CLOSING, socket.CLOSED].includes(socket.readyState as any)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
socket.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async publishEntry(
|
||||
signedEntry: SignedRegistryEntry,
|
||||
customOptions: CustomRegistryOptions = {},
|
||||
) {
|
||||
const config = optionsToConfig(
|
||||
this,
|
||||
DEFAULT_PUBLISH_ENTRY_OPTIONS,
|
||||
customOptions,
|
||||
);
|
||||
|
||||
if (!verifyRegistryEntry(signedEntry)) {
|
||||
throwValidationError(
|
||||
"signedEntry", // name of the variable
|
||||
signedEntry, // actual value
|
||||
"parameter", // valueKind (assuming it's a function parameter)
|
||||
"a valid signed registry entry", // expected description
|
||||
);
|
||||
}
|
||||
|
||||
return postS5Registry(
|
||||
{
|
||||
pk: base64urlEncode(signedEntry.pk),
|
||||
revision: signedEntry.revision,
|
||||
data: base64urlEncode(signedEntry.data),
|
||||
signature: base64urlEncode(signedEntry.signature),
|
||||
},
|
||||
config,
|
||||
);
|
||||
}
|
||||
|
||||
public async createEntry(
|
||||
sk: Uint8Array | KeyPairEd25519,
|
||||
cid: CID,
|
||||
revision = 0,
|
||||
) {
|
||||
if (sk instanceof Uint8Array) {
|
||||
sk = createKeyPair(sk);
|
||||
}
|
||||
|
||||
let existing = true;
|
||||
let entry = await this.getEntry(sk.publicKey);
|
||||
|
||||
if (!entry) {
|
||||
existing = false;
|
||||
entry = {
|
||||
pk: sk.publicKey,
|
||||
data: cid.toRegistryEntry(),
|
||||
revision,
|
||||
} as unknown as SignedRegistryEntry;
|
||||
}
|
||||
|
||||
if (!equalBytes(sk.publicKey, entry.pk)) {
|
||||
throwValidationError(
|
||||
"entry.pk", // name of the variable
|
||||
Buffer.from(entry.pk).toString("hex"), // actual value
|
||||
"result", // valueKind (assuming it's a function parameter)
|
||||
Buffer.from(sk.publicKey).toString("hex"), // expected description
|
||||
);
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
const newEntry = cid.toRegistryEntry();
|
||||
if (equalBytes(entry.data, newEntry)) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
entry.revision++;
|
||||
entry.data = newEntry;
|
||||
}
|
||||
const signedEntry = signRegistryEntry({
|
||||
kp: sk,
|
||||
data: entry.data,
|
||||
revision: entry.revision,
|
||||
});
|
||||
|
||||
await this.publishEntry(signedEntry);
|
||||
|
||||
return signedEntry;
|
||||
}
|
||||
|
||||
public async getEntry(
|
||||
publicKey: Uint8Array,
|
||||
customOptions: CustomRegistryOptions = {},
|
||||
) {
|
||||
const config = optionsToConfig(
|
||||
this,
|
||||
DEFAULT_GET_ENTRY_OPTIONS,
|
||||
customOptions,
|
||||
);
|
||||
|
||||
try {
|
||||
const ret = await getS5Registry(
|
||||
{
|
||||
pk: base64urlEncode(publicKey),
|
||||
},
|
||||
config,
|
||||
);
|
||||
|
||||
const signedEntry = {
|
||||
pk: base64urlDecode(<string>ret.pk),
|
||||
revision: ret.revision,
|
||||
data: base64urlDecode(<string>ret.data),
|
||||
signature: base64urlDecode(<string>ret.signature),
|
||||
} as SignedRegistryEntry;
|
||||
|
||||
if (!verifyRegistryEntry(signedEntry)) {
|
||||
throwValidationError(
|
||||
"signedEntry", // name of the variable
|
||||
signedEntry, // actual value
|
||||
"result", // valueKind (assuming it's a function parameter)
|
||||
"a valid signed registry entry", // expected description
|
||||
);
|
||||
}
|
||||
|
||||
return signedEntry;
|
||||
} catch (e) {
|
||||
if ((e as AxiosError).response?.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to S5-net.
|
||||
*
|
||||
* @param file - The file to upload.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @returns - The returned cid.
|
||||
* @throws - Will throw if the request is successful but the upload response does not contain a complete response.
|
||||
*/
|
||||
public async uploadFile(
|
||||
file: File,
|
||||
customOptions: CustomUploadOptions = {},
|
||||
): Promise<any> {
|
||||
const opts = {
|
||||
...DEFAULT_UPLOAD_OPTIONS,
|
||||
...this.clientOptions,
|
||||
...customOptions,
|
||||
} as CustomUploadOptions;
|
||||
|
||||
if (file.size < <number>opts?.largeFileSize) {
|
||||
return this.uploadSmallFile(file, opts);
|
||||
} else {
|
||||
return this.uploadLargeFile(file, opts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a small file to S5-net.
|
||||
*
|
||||
* @param file - The file to upload.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
|
||||
* @returns UploadResult - The returned cid.
|
||||
* @throws - Will throw if the request is successful but the upload response does not contain a complete response.
|
||||
*/
|
||||
public async uploadSmallFile(
|
||||
file: File,
|
||||
customOptions: CustomUploadOptions,
|
||||
): Promise<UploadResult> {
|
||||
const response = await this.uploadSmallFileRequest(file, customOptions);
|
||||
|
||||
return {cid: CID.decode(<string>response.cid)};
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
/**
|
||||
* Uploads a large file to S5-net using tus.
|
||||
*
|
||||
* @param file - The file to upload.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @param [customOptions.endpointLargeUpload="/s5/upload/tus"] - The relative URL path of the portal endpoint to contact.
|
||||
* @returns - The returned cid.
|
||||
* @throws - Will throw if the request is successful but the upload response does not contain a complete response.
|
||||
*/
|
||||
public async uploadLargeFile(
|
||||
file: File,
|
||||
customOptions: CustomUploadOptions = {},
|
||||
): Promise<UploadResult> {
|
||||
return await this.uploadLargeFileRequest(file, customOptions);
|
||||
}
|
||||
|
||||
public async getTusOptions(
|
||||
file: File,
|
||||
tusOptions: Partial<UploadOptions> = {},
|
||||
customOptions: CustomUploadOptions = {},
|
||||
): Promise<UploadOptions> {
|
||||
const config = optionsToConfig(this, DEFAULT_UPLOAD_OPTIONS, customOptions);
|
||||
|
||||
// Validation.
|
||||
const url = await buildRequestUrl(this, {
|
||||
endpointPath: TUS_ENDPOINT,
|
||||
});
|
||||
|
||||
file = ensureFileObjectConsistency(file);
|
||||
|
||||
const hasher = blake3.create({});
|
||||
|
||||
const chunkSize = 1024 * 1024;
|
||||
|
||||
let position = 0;
|
||||
|
||||
while (position <= file.size) {
|
||||
const chunk = file.slice(position, position + chunkSize);
|
||||
``;
|
||||
hasher.update(new Uint8Array(await chunk.arrayBuffer()));
|
||||
position += chunkSize;
|
||||
customOptions.onHashProgress?.({
|
||||
bytes: position,
|
||||
total: file.size,
|
||||
});
|
||||
}
|
||||
|
||||
const b3hash = hasher.digest();
|
||||
|
||||
const filename = new Multihash(
|
||||
Buffer.concat([
|
||||
Buffer.alloc(1, CID_HASH_TYPES.BLAKE3),
|
||||
Buffer.from(b3hash),
|
||||
]),
|
||||
).toBase64Url();
|
||||
|
||||
return {
|
||||
endpoint: url,
|
||||
metadata: {
|
||||
hash: filename,
|
||||
filename: filename,
|
||||
filetype: file.type,
|
||||
},
|
||||
headers: config.headers as any,
|
||||
onBeforeRequest: function (req: HttpRequest) {
|
||||
const xhr = req.getUnderlyingObject();
|
||||
xhr.withCredentials = true;
|
||||
},
|
||||
...tusOptions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a directory to S5-net.
|
||||
*
|
||||
* @param directory - File objects to upload, indexed by their path strings.
|
||||
* @param filename - The name of the directory.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @param [customOptions.endpointPath="/s5/upload/directory"] - The relative URL path of the portal endpoint to contact.
|
||||
* @returns - The returned cid.
|
||||
* @throws - Will throw if the request is successful but the upload response does not contain a complete response.
|
||||
*/
|
||||
public async uploadDirectory(
|
||||
directory: Record<string, File>,
|
||||
filename: string,
|
||||
customOptions: CustomUploadOptions = {},
|
||||
): Promise<UploadResult> {
|
||||
const response = await this.uploadDirectoryRequest(
|
||||
directory,
|
||||
filename,
|
||||
customOptions,
|
||||
);
|
||||
|
||||
return {cid: CID.decode(<string>response.cid)};
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to upload a directory to S5-net.
|
||||
*
|
||||
* @param directory - File objects to upload, indexed by their path strings.
|
||||
* @param filename - The name of the directory.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @returns - The upload response.
|
||||
* @throws - Will throw if the input filename is not a string.
|
||||
*/
|
||||
public async uploadDirectoryRequest(
|
||||
directory: Record<string, File>,
|
||||
filename: string,
|
||||
customOptions: CustomUploadOptions = {},
|
||||
): Promise<BasicUploadResponse> {
|
||||
const config = optionsToConfig(this, DEFAULT_UPLOAD_OPTIONS, customOptions);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
for (const entry in directory) {
|
||||
const file = ensureFileObjectConsistency(directory[entry]);
|
||||
formData.append(entry, file, entry);
|
||||
}
|
||||
|
||||
const params = {} as PostS5UploadDirectoryParams;
|
||||
|
||||
if (customOptions.tryFiles) {
|
||||
params.tryFiles = customOptions.tryFiles;
|
||||
}
|
||||
if (customOptions.errorPages) {
|
||||
params.errorPages = customOptions.errorPages;
|
||||
}
|
||||
|
||||
params.name = filename;
|
||||
|
||||
/*
|
||||
Hack to pass the data right since OpenAPI doesn't support variable file inputs without knowing the names ahead of time.
|
||||
*/
|
||||
config.data = formData;
|
||||
|
||||
return postS5UploadDirectory({}, params, config);
|
||||
}
|
||||
|
||||
public async uploadWebapp(
|
||||
directory: Record<string, File>,
|
||||
customOptions: CustomUploadOptions = {},
|
||||
): Promise<UploadResult> {
|
||||
const response = await this.uploadWebappRequest(directory, customOptions);
|
||||
|
||||
return {cid: CID.decode(<string>response.cid)};
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to upload a directory to S5-net.
|
||||
* @param directory - File objects to upload, indexed by their path strings.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @param [customOptions.endpointPath] - The relative URL path of the portal endpoint to contact.
|
||||
* @returns - The upload response.
|
||||
* @throws - Will throw if the input filename is not a string.
|
||||
*/
|
||||
public async uploadWebappRequest(
|
||||
directory: Record<string, File>,
|
||||
customOptions: CustomUploadOptions = {},
|
||||
): Promise<BasicUploadResponse> {
|
||||
return this.uploadDirectoryRequest(directory, "webapp", customOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to upload a small file to S5-net.
|
||||
*
|
||||
* @param file - The file to upload.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
|
||||
* @returns PostS5UploadResult - The upload response.
|
||||
*/
|
||||
private async uploadSmallFileRequest(
|
||||
file: File,
|
||||
customOptions: CustomUploadOptions = {},
|
||||
): Promise<PostS5UploadResult> {
|
||||
const config = optionsToConfig(this, DEFAULT_UPLOAD_OPTIONS, customOptions);
|
||||
|
||||
file = ensureFileObjectConsistency(file);
|
||||
|
||||
return postS5Upload(
|
||||
file,
|
||||
config,
|
||||
);
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
/**
|
||||
* Makes a request to upload a file to S5-net.
|
||||
*
|
||||
* @param file - The file to upload.
|
||||
* @param [customOptions] - Additional settings that can optionally be set.
|
||||
* @returns - The upload response.
|
||||
*/
|
||||
private async uploadLargeFileRequest(
|
||||
file: File,
|
||||
customOptions: CustomUploadOptions = {},
|
||||
): Promise<UploadResult> {
|
||||
const p = defer<UploadResult>();
|
||||
|
||||
const options = await this.getTusOptions(
|
||||
file,
|
||||
{
|
||||
onSuccess: async () => {
|
||||
if (!upload.url) {
|
||||
p.reject(new Error("'upload.url' was not set"));
|
||||
return;
|
||||
}
|
||||
|
||||
p.resolve({cid});
|
||||
},
|
||||
onError: (error: Error | DetailedError) => {
|
||||
// Return error body rather than entire error.
|
||||
const res = (error as DetailedError).originalResponse;
|
||||
const newError = res
|
||||
? new Error(res.getBody().trim()) || error
|
||||
: error;
|
||||
p.reject(newError);
|
||||
},
|
||||
},
|
||||
customOptions,
|
||||
);
|
||||
const cid = CID.fromHash(
|
||||
Multihash.fromBase64Url(<string>options.metadata?.hash).fullBytes,
|
||||
file.size,
|
||||
CID_TYPES.RAW,
|
||||
);
|
||||
|
||||
const upload = new Upload(file, options);
|
||||
|
||||
return p.promise;
|
||||
}
|
||||
|
||||
public async pin(cid: string, customOptions: CustomPinOptions = {}) {
|
||||
const config = optionsToConfig(
|
||||
this,
|
||||
DEFAULT_PIN_OPTIONS,
|
||||
customOptions,
|
||||
);
|
||||
|
||||
await postS5PinCid(cid, config);
|
||||
}
|
||||
|
||||
public async unpin(cid: string, customOptions: CustomPinOptions = {}) {
|
||||
const config = optionsToConfig(
|
||||
this,
|
||||
DEFAULT_PIN_OPTIONS,
|
||||
customOptions,
|
||||
);
|
||||
|
||||
await deleteS5DeleteCid(cid, config);
|
||||
}
|
||||
|
||||
public async pinStatus(cid: string, customOptions: CustomPinOptions = {}) {
|
||||
const config = optionsToConfig(
|
||||
this,
|
||||
DEFAULT_PIN_OPTIONS,
|
||||
customOptions,
|
||||
);
|
||||
|
||||
return await getS5PinCidStatus(cid, config);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { S5Client } from "./client.js";
|
||||
export type { CustomClientOptions } from "./utils/options.js";
|
||||
export type { HashProgressEvent } from "./options/upload.js";
|
|
@ -0,0 +1,21 @@
|
|||
import { Buffer } from "buffer";
|
||||
|
||||
/**
|
||||
* convert a number to Buffer.
|
||||
*
|
||||
* @param value - File objects to upload, indexed by their path strings.
|
||||
* @returns - The returned cid.
|
||||
* @throws - Will throw if the request is successful but the upload response does not contain a complete response.
|
||||
*/
|
||||
function numberToBuffer(value: number) {
|
||||
const view = Buffer.alloc(16);
|
||||
let lastIndex = 15;
|
||||
for (let index = 0; index <= 15; ++index) {
|
||||
if (value % 256 !== 0) {
|
||||
lastIndex = index;
|
||||
}
|
||||
view[index] = value % 256;
|
||||
value = value >> 8;
|
||||
}
|
||||
return view.subarray(0, lastIndex + 1);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { ResponseType } from "axios";
|
||||
import { CustomClientOptions } from "../utils/options.js";
|
||||
|
||||
export type CustomDownloadOptions = CustomClientOptions & {
|
||||
path?: string;
|
||||
range?: string;
|
||||
responseType?: ResponseType;
|
||||
};
|
||||
|
||||
export type CustomGetMetadataOptions = CustomClientOptions & {};
|
||||
|
||||
/**
|
||||
* The response for a get metadata request.
|
||||
*
|
||||
* @property metadata - The metadata in JSON format.
|
||||
* @property portalUrl - The URL of the portal.
|
||||
* @property cid - 46-character cid.
|
||||
*/
|
||||
export type MetadataResult = {
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const DEFAULT_DOWNLOAD_OPTIONS = {
|
||||
range: undefined,
|
||||
responseType: undefined,
|
||||
} as CustomDownloadOptions;
|
||||
|
||||
export const DEFAULT_GET_METADATA_OPTIONS = {};
|
|
@ -0,0 +1,8 @@
|
|||
import {CustomClientOptions} from "#utils/options.js";
|
||||
import {ResponseType} from "axios";
|
||||
|
||||
export type CustomPinOptions = CustomClientOptions & {
|
||||
|
||||
};
|
||||
|
||||
export const DEFAULT_PIN_OPTIONS = {};
|
|
@ -0,0 +1,22 @@
|
|||
import { CustomClientOptions } from "../utils/options.js";
|
||||
|
||||
export const DEFAULT_GET_ENTRY_OPTIONS = {};
|
||||
|
||||
export const DEFAULT_SET_ENTRY_OPTIONS = {
|
||||
endpointSetEntry: "/s5/registry",
|
||||
};
|
||||
|
||||
export const DEFAULT_SUBSCRIBE_ENTRY_OPTIONS = {
|
||||
endpointSubscribeEntry: "/s5/registry/subscription",
|
||||
} satisfies CustomRegistryOptions;
|
||||
|
||||
export const DEFAULT_PUBLISH_ENTRY_OPTIONS = {
|
||||
endpointPublishEntry: "/s5/registry",
|
||||
} satisfies CustomRegistryOptions;
|
||||
|
||||
export type BaseCustomOptions = CustomClientOptions;
|
||||
|
||||
export interface CustomRegistryOptions extends BaseCustomOptions {
|
||||
endpointSubscribeEntry?: string;
|
||||
endpointPublishEntry?: string;
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import { AxiosProgressEvent } from "axios";
|
||||
import {
|
||||
DetailedError,
|
||||
HttpRequest,
|
||||
Upload,
|
||||
UploadOptions,
|
||||
} from "tus-js-client";
|
||||
|
||||
import { blake3 } from "@noble/hashes/blake3";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
import { getFileMimeType } from "../utils/file.js";
|
||||
import { S5Client } from "../client.js";
|
||||
import { CID, CID_HASH_TYPES, CID_TYPES } from "@lumeweb/libs5";
|
||||
import {
|
||||
type BasicUploadResponse,
|
||||
postS5Upload,
|
||||
postS5UploadDirectory,
|
||||
PostS5UploadDirectoryParams,
|
||||
PostS5UploadResult,
|
||||
} from "../generated/index.js";
|
||||
import { BaseCustomOptions } from "./registry.js";
|
||||
import { optionsToConfig } from "../utils/options.js";
|
||||
import { buildRequestUrl } from "../request.js";
|
||||
import defer from "p-defer";
|
||||
import { Multihash } from "@lumeweb/libs5/lib/multihash.js";
|
||||
|
||||
/**
|
||||
* The tus chunk size is (4MiB - encryptionOverhead) * dataPieces, set as default.
|
||||
*/
|
||||
export const TUS_CHUNK_SIZE = (1 << 22) * 8;
|
||||
|
||||
/**
|
||||
* The retry delays, in ms. Data is stored in for up to 20 minutes, so the
|
||||
* total delays should not exceed that length of time.
|
||||
*/
|
||||
const DEFAULT_TUS_RETRY_DELAYS = [0, 5_000, 15_000, 60_000, 300_000, 600_000];
|
||||
|
||||
/**
|
||||
* The portal file field name.
|
||||
*/
|
||||
const PORTAL_FILE_FIELD_NAME = "file";
|
||||
|
||||
export const TUS_ENDPOINT = "/s5/upload/tus";
|
||||
|
||||
export interface HashProgressEvent {
|
||||
bytes: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom upload options.
|
||||
*
|
||||
* @property [largeFileSize=32943040] - The size at which files are considered "large" and will be uploaded using the tus resumable upload protocol. This is the size of one chunk by default (32 mib). Note that this does not affect the actual size of chunks used by the protocol.
|
||||
* @property [errorPages] - Defines a mapping of error codes and subfiles which are to be served in case we are serving the respective error code. All subfiles referred like this must be defined with absolute paths and must exist.
|
||||
* @property [retryDelays=[0, 5_000, 15_000, 60_000, 300_000, 600_000]] - An array or undefined, indicating how many milliseconds should pass before the next attempt to uploading will be started after the transfer has been interrupted. The array's length indicates the maximum number of attempts.
|
||||
* @property [tryFiles] - Allows us to set a list of potential subfiles to return in case the requested one does not exist or is a directory. Those subfiles might be listed with relative or absolute paths. If the path is absolute the file must exist.
|
||||
*/
|
||||
export type CustomUploadOptions = BaseCustomOptions & {
|
||||
errorPages?: Record<string, string>;
|
||||
tryFiles?: string[];
|
||||
|
||||
// Large files.
|
||||
largeFileSize?: number;
|
||||
retryDelays?: number[];
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||
onHashProgress?: (progressEvent: HashProgressEvent) => void;
|
||||
};
|
||||
|
||||
export const DEFAULT_UPLOAD_OPTIONS = {
|
||||
errorPages: { 404: "/404.html" },
|
||||
tryFiles: ["index.html"],
|
||||
|
||||
// Large files.
|
||||
largeFileSize: TUS_CHUNK_SIZE,
|
||||
retryDelays: DEFAULT_TUS_RETRY_DELAYS,
|
||||
} as CustomUploadOptions;
|
||||
|
||||
export interface UploadResult {
|
||||
cid: CID;
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
import { S5Client } from "./client.js";
|
||||
import {
|
||||
addUrlQuery,
|
||||
addUrlSubdomain,
|
||||
ensureUrlPrefix,
|
||||
makeUrl,
|
||||
} from "./utils/url.js";
|
||||
|
||||
export type Headers = { [key: string]: string };
|
||||
|
||||
/**
|
||||
* Helper function that builds the request URL. Ensures that the final URL
|
||||
* always has a protocol prefix for consistency.
|
||||
*
|
||||
* @param client - The S5 client.
|
||||
* @param parts - The URL parts to use when constructing the URL.
|
||||
* @param [parts.baseUrl] - The base URL to use, instead of the portal URL.
|
||||
* @param [parts.endpointPath] - The endpoint to contact.
|
||||
* @param [parts.subdomain] - An optional subdomain to add to the URL.
|
||||
* @param [parts.extraPath] - An optional path to append to the URL.
|
||||
* @param [parts.query] - Optional query parameters to append to the URL.
|
||||
* @returns - The built URL.
|
||||
*/
|
||||
export async function buildRequestUrl(
|
||||
client: S5Client,
|
||||
parts: {
|
||||
baseUrl?: string;
|
||||
endpointPath?: string;
|
||||
subdomain?: string;
|
||||
extraPath?: string;
|
||||
query?: { [key: string]: string | undefined };
|
||||
},
|
||||
): Promise<string> {
|
||||
let url;
|
||||
|
||||
// Get the base URL, if not passed in.
|
||||
if (!parts.baseUrl) {
|
||||
url = await client.portalUrl;
|
||||
} else {
|
||||
url = parts.baseUrl;
|
||||
}
|
||||
|
||||
// Make sure the URL has a protocol.
|
||||
url = ensureUrlPrefix(url);
|
||||
|
||||
if (parts.endpointPath) {
|
||||
url = makeUrl(url, parts.endpointPath);
|
||||
}
|
||||
if (parts.extraPath) {
|
||||
url = makeUrl(url, parts.extraPath);
|
||||
}
|
||||
if (parts.subdomain) {
|
||||
url = addUrlSubdomain(url, parts.subdomain);
|
||||
}
|
||||
if (parts.query) {
|
||||
url = addUrlQuery(url, parts.query);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { base64url } from "multiformats/bases/base64";
|
||||
|
||||
export const base64urlEncode = (d: Uint8Array) =>
|
||||
base64url.encode(d).substring(1);
|
||||
export const base64urlDecode = (d: string) => base64url.decode(`u${d}`);
|
|
@ -0,0 +1,38 @@
|
|||
import mime from "mime";
|
||||
import path from "path";
|
||||
|
||||
import { trimPrefix } from "./string.js";
|
||||
|
||||
/**
|
||||
* Get the file mime type. In case the type is not provided, try to guess the
|
||||
* file type based on the extension.
|
||||
*
|
||||
* @param file - The file.
|
||||
* @returns - The mime type.
|
||||
*/
|
||||
export function getFileMimeType(file: File): string {
|
||||
if (file.type) return file.type;
|
||||
let ext = path.extname(file.name);
|
||||
ext = trimPrefix(ext, ".");
|
||||
if (ext !== "") {
|
||||
const mimeType = mime.getType(ext);
|
||||
if (mimeType) {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sometimes file object might have had the type property defined manually with
|
||||
* Object.defineProperty and some browsers (namely firefox) can have problems
|
||||
* reading it after the file has been appended to form data. To overcome this,
|
||||
* we recreate the file object using native File constructor with a type defined
|
||||
* as a constructor argument.
|
||||
*
|
||||
* @param file - The input file.
|
||||
* @returns - The processed file.
|
||||
*/
|
||||
export function ensureFileObjectConsistency(file: File): File {
|
||||
return new File([file], file.name, { type: getFileMimeType(file) });
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import { AxiosHeaders, AxiosProgressEvent, AxiosRequestConfig } from "axios";
|
||||
import { S5Client } from "../client.js";
|
||||
import { BaseCustomOptions, CustomRegistryOptions } from "#options/registry.js";
|
||||
|
||||
/**
|
||||
* Custom client options.
|
||||
*
|
||||
* @property [apiKey] - Authentication password to use for a single S5 node/portal.
|
||||
* @property [customUserAgent] - Custom user agent header to set.
|
||||
* @property [customCookie] - Custom cookie header to set. WARNING: the Cookie header cannot be set in browsers. This is meant for usage in server contexts.
|
||||
* @property [onDownloadProgress] - Optional callback to track download progress.
|
||||
* @property [onUploadProgress] - Optional callback to track upload progress.
|
||||
*/
|
||||
|
||||
export type CustomClientOptions = {
|
||||
apiKey?: string;
|
||||
customUserAgent?: string;
|
||||
customCookie?: string;
|
||||
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||
httpConfig?: AxiosRequestConfig;
|
||||
};
|
||||
|
||||
export function optionsToConfig(
|
||||
client: S5Client,
|
||||
def: CustomClientOptions | BaseCustomOptions | CustomRegistryOptions,
|
||||
...options: (
|
||||
| CustomClientOptions
|
||||
| BaseCustomOptions
|
||||
| CustomRegistryOptions
|
||||
)[]
|
||||
): AxiosRequestConfig {
|
||||
let config: AxiosRequestConfig = {};
|
||||
|
||||
config.baseURL = client.portalUrl;
|
||||
|
||||
const extraOptions= Object.values(options.reduce((acc, val) => {
|
||||
return {
|
||||
...acc,
|
||||
...val,
|
||||
};
|
||||
}, options)).reverse().pop();
|
||||
|
||||
const finalOptions = {
|
||||
...def,
|
||||
...client.clientOptions,
|
||||
...extraOptions,
|
||||
} as CustomClientOptions;
|
||||
|
||||
if (finalOptions?.onDownloadProgress) {
|
||||
config.onDownloadProgress = finalOptions?.onDownloadProgress;
|
||||
}
|
||||
|
||||
if (finalOptions?.onUploadProgress) {
|
||||
config.onUploadProgress = finalOptions?.onUploadProgress;
|
||||
}
|
||||
|
||||
const headers = new AxiosHeaders(config.headers as AxiosHeaders);
|
||||
|
||||
if (finalOptions?.customCookie) {
|
||||
headers.set("Cookie", finalOptions.customCookie);
|
||||
}
|
||||
if (finalOptions?.customUserAgent) {
|
||||
headers.set("User-Agent", finalOptions.customUserAgent);
|
||||
}
|
||||
|
||||
if (finalOptions?.apiKey) {
|
||||
headers.set("Authorization", `Bearer ${finalOptions.apiKey}`);
|
||||
config.withCredentials = true;
|
||||
|
||||
config.params = {
|
||||
...config.params,
|
||||
auth_token: finalOptions?.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
config.headers = headers;
|
||||
|
||||
if (finalOptions?.httpConfig) {
|
||||
config = {
|
||||
...config,
|
||||
...finalOptions.httpConfig,
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Prepends the prefix to the given string only if the string does not already start with the prefix.
|
||||
*
|
||||
* @param str - The string.
|
||||
* @param prefix - The prefix.
|
||||
* @returns - The prefixed string.
|
||||
*/
|
||||
export function ensurePrefix(str: string, prefix: string): string {
|
||||
if (!str.startsWith(prefix)) {
|
||||
str = `${prefix}${str}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a prefix from the beginning of the string.
|
||||
*
|
||||
* @param str - The string to process.
|
||||
* @param prefix - The prefix to remove.
|
||||
* @param [limit] - Maximum amount of times to trim. No limit by default.
|
||||
* @returns - The processed string.
|
||||
*/
|
||||
export function trimPrefix(
|
||||
str: string,
|
||||
prefix: string,
|
||||
limit?: number,
|
||||
): string {
|
||||
while (str.startsWith(prefix)) {
|
||||
if (limit !== undefined && limit <= 0) {
|
||||
break;
|
||||
}
|
||||
str = str.slice(prefix.length);
|
||||
if (limit) {
|
||||
limit -= 1;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a suffix from the end of the string.
|
||||
*
|
||||
* @param str - The string to process.
|
||||
* @param suffix - The suffix to remove.
|
||||
* @param [limit] - Maximum amount of times to trim. No limit by default.
|
||||
* @returns - The processed string.
|
||||
*/
|
||||
export function trimSuffix(
|
||||
str: string,
|
||||
suffix: string,
|
||||
limit?: number,
|
||||
): string {
|
||||
while (str.endsWith(suffix)) {
|
||||
if (limit !== undefined && limit <= 0) {
|
||||
break;
|
||||
}
|
||||
str = str.substring(0, str.length - suffix.length);
|
||||
if (limit) {
|
||||
limit -= 1;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import urljoin from "url-join";
|
||||
import parse from "url-parse";
|
||||
|
||||
import { trimSuffix } from "./string.js";
|
||||
import { throwValidationError } from "./validation.js";
|
||||
|
||||
export const URI_S5_PREFIX = "s5://";
|
||||
|
||||
/**
|
||||
* Adds a subdomain to the given URL.
|
||||
*
|
||||
* @param url - The URL.
|
||||
* @param subdomain - The subdomain to add.
|
||||
* @returns - The final URL.
|
||||
*/
|
||||
export function addUrlSubdomain(url: string, subdomain: string): string {
|
||||
const urlObj = new URL(url);
|
||||
urlObj.hostname = `${subdomain}.${urlObj.hostname}`;
|
||||
const str = urlObj.toString();
|
||||
return trimSuffix(str, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a query to the given URL.
|
||||
*
|
||||
* @param url - The URL.
|
||||
* @param query - The query parameters.
|
||||
* @returns - The final URL.
|
||||
*/
|
||||
export function addUrlQuery(
|
||||
url: string,
|
||||
query: { [key: string]: string | undefined },
|
||||
): string {
|
||||
const parsed = parse(url, true);
|
||||
// Combine the desired query params with the already existing ones.
|
||||
query = { ...parsed.query, ...query };
|
||||
parsed.set("query", query);
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends the prefix to the given string only if the string does not already start with the prefix.
|
||||
*
|
||||
* @param str - The string.
|
||||
* @param prefix - The prefix.
|
||||
* @returns - The prefixed string.
|
||||
*/
|
||||
export function ensurePrefix(str: string, prefix: string): string {
|
||||
if (!str.startsWith(prefix)) {
|
||||
str = `${prefix}${str}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the given string is a URL.
|
||||
*
|
||||
* @param url - The given string.
|
||||
* @returns - The URL.
|
||||
*/
|
||||
export function ensureUrl(url: string): string {
|
||||
if (url.startsWith("http://")) {
|
||||
return url;
|
||||
}
|
||||
return ensurePrefix(url, "https://");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the given string is a URL with a protocol prefix.
|
||||
*
|
||||
* @param url - The given string.
|
||||
* @returns - The URL.
|
||||
*/
|
||||
export function ensureUrlPrefix(url: string): string {
|
||||
if (url === "localhost") {
|
||||
return "http://localhost/";
|
||||
}
|
||||
|
||||
if (!/^https?:(\/\/)?/i.test(url)) {
|
||||
return `https://${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properly joins paths together to create a URL. Takes a variable number of
|
||||
* arguments.
|
||||
*
|
||||
* @param args - Array of URL parts to join.
|
||||
* @returns - Final URL constructed from the input parts.
|
||||
*/
|
||||
export function makeUrl(...args: string[]): string {
|
||||
if (args.length === 0) {
|
||||
throwValidationError("args", args, "parameter", "non-empty");
|
||||
}
|
||||
return ensureUrl(args.reduce((acc, cur) => urljoin(acc, cur)));
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Throws an error for the given value
|
||||
*
|
||||
* @param name - The name of the value.
|
||||
* @param value - The actual value.
|
||||
* @param valueKind - The kind of value that is being checked (e.g. "parameter", "response field", etc.)
|
||||
* @param expected - The expected aspect of the value that could not be validated (e.g. "type 'string'" or "non-null").
|
||||
* @throws - Will always throw.
|
||||
*/
|
||||
export function throwValidationError(
|
||||
name: string,
|
||||
value: unknown,
|
||||
valueKind: string,
|
||||
expected: string,
|
||||
): void {
|
||||
throw validationError(name, value, valueKind, expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an error for the given value
|
||||
*
|
||||
* @param name - The name of the value.
|
||||
* @param value - The actual value.
|
||||
* @param valueKind - The kind of value that is being checked (e.g. "parameter", "response field", etc.)
|
||||
* @param expected - The expected aspect of the value that could not be validated (e.g. "type 'string'" or "non-null").
|
||||
* @returns - The validation error.
|
||||
*/
|
||||
export function validationError(
|
||||
name: string,
|
||||
value: unknown,
|
||||
valueKind: string,
|
||||
expected: string,
|
||||
): Error {
|
||||
let actualValue: string;
|
||||
if (value === undefined) {
|
||||
actualValue = "type 'undefined'";
|
||||
} else if (value === null) {
|
||||
actualValue = "type 'null'";
|
||||
} else {
|
||||
actualValue = `type '${typeof value}', value '${value}'`;
|
||||
}
|
||||
return new Error(
|
||||
`Expected ${valueKind} '${name}' to be ${expected}, was ${actualValue}`,
|
||||
);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue