mirror of
https://github.com/logos-storage/logos-storage-js.git
synced 2026-01-02 13:33:07 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a12da5cca3 | ||
|
|
26a3caacc8 | ||
|
|
59837ece13 | ||
|
|
6b04d115c1 | ||
|
|
a437e593f0 | ||
|
|
a8970d2377 | ||
|
|
df64af08e5 | ||
|
|
b50c0b7107 | ||
|
|
9cc497feca | ||
|
|
496aae84e7 | ||
|
|
3bf517d1bb | ||
|
|
921bb617ef | ||
|
|
f6d0852e86 | ||
|
|
54dca841f1 | ||
|
|
9e89c30562 | ||
|
|
746f96279b | ||
|
|
5dd17df66f | ||
|
|
5d2f3f6603 | ||
|
|
f77978cd63 | ||
|
|
6a890262cc | ||
|
|
c94d7a1257 |
55
.github/workflows/ci.yaml
vendored
Normal file
55
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
node_version: 22.12.0
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
URL: "http://localhost:8081/api/codex/v1/debug/info"
|
||||
TIMEOUT_SECONDS: 300
|
||||
SLEEP_INTERVAL: 2
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.node_version }}
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- run: npm ci
|
||||
|
||||
# - name: Start codex-factory
|
||||
# run: npx codex-factory start latest &
|
||||
|
||||
# - name: Wait for first SP to be started
|
||||
# run: |
|
||||
# MAX_RETRIES=$((TIMEOUT_SECONDS / SLEEP_INTERVAL))
|
||||
# echo "Waiting for $URL (timeout: ${TIMEOUT_SECONDS}s)..."
|
||||
|
||||
# for i in $(seq 1 $MAX_RETRIES); do
|
||||
# STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || echo "000")
|
||||
# if [ "$STATUS" = "200" ]; then
|
||||
# echo "Codex is ready"
|
||||
# exit 0
|
||||
# fi
|
||||
|
||||
# sleep $SLEEP_INTERVAL
|
||||
# done
|
||||
|
||||
# echo "Timed out after ${TIMEOUT_SECONDS}s waiting for $URL"
|
||||
# exit 1
|
||||
|
||||
# - run: npm test
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
*.tgz
|
||||
*.tgz
|
||||
index.bundle.js
|
||||
211
README.md
211
README.md
@ -6,6 +6,23 @@ The SDK has a small bundle size and support tree shaking.
|
||||
|
||||
The SDK is currently under early development and the API can change at any time.
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](#stability)
|
||||
[](https://github.com/codex-storage/codex-js/actions/workflows/ci.yml?query=branch%3Amaster)
|
||||
|
||||
## Breaking changes
|
||||
|
||||
- Version 0.1.0 introduces [upload strategy](#upload) to support browser and Node JS.
|
||||
|
||||
## Types generation
|
||||
|
||||
The types are generated from the openapi.yaml using the commande:
|
||||
|
||||
```bash
|
||||
npx openapi-typescript ./openapi.yaml -o src/openapi.ts --default-non-nullable false
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
||||
### Sync api
|
||||
@ -25,7 +42,7 @@ const { Codex } = require("@codex-storage/sdk-js");
|
||||
To create a Codex instance, provide the REST API url to interact with the Codex client:
|
||||
|
||||
```js
|
||||
const codex = new Codex("http://localhost:3000");
|
||||
const codex = new Codex("http://localhost:8080");
|
||||
```
|
||||
|
||||
Then you can access any module like this:
|
||||
@ -49,13 +66,27 @@ const { Codex } = require("@codex-storage/sdk-js/async");
|
||||
To create a Codex instance, provide the REST API url to interact with the Codex client:
|
||||
|
||||
```js
|
||||
const codex = new Codex("http://localhost:3000");
|
||||
const codex = new Codex("http://localhost:8080");
|
||||
```
|
||||
|
||||
To use a module, you need to use the await syntax. If the module is not loaded yet, it will be imported first and then cached in memory.
|
||||
|
||||
```js
|
||||
const marketplace = await codex.marketplace;
|
||||
const marketplace = await codex.marketplace();
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
You can use basic authentication when creating a new Codex object:
|
||||
|
||||
```js
|
||||
const codex = new Codex("http://localhost:8080", {
|
||||
auth: {
|
||||
basic: "MY BASIC AUTH SECRET"
|
||||
}
|
||||
});
|
||||
|
||||
You can obtain your secret using the `btoa` method in the browser or `Buffer.from(string).toString('base64')` in Node.js. The secret is stored in memory only.
|
||||
```
|
||||
|
||||
### Error handling
|
||||
@ -96,15 +127,19 @@ if (slots.error) {
|
||||
The following API assume that you have already a marketplace module loaded, example:
|
||||
|
||||
```js
|
||||
const codex = new Codex("http://localhost:3000");
|
||||
const codex = new Codex("http://localhost:8080");
|
||||
// When using the async api
|
||||
const marketplace = await codex.marketplace();
|
||||
|
||||
// When using the sync api
|
||||
const marketplace = codex.marketplace;
|
||||
```
|
||||
|
||||
#### activeSlots()
|
||||
|
||||
Returns active slots.
|
||||
|
||||
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]>
|
||||
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L7)[]>
|
||||
|
||||
Example:
|
||||
|
||||
@ -117,7 +152,7 @@ const slots = await marketplace.activeSlots();
|
||||
Returns active slot with id {slotId} for the host.
|
||||
|
||||
- slotId (string, required)
|
||||
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]>
|
||||
- returns Promise<[CodexSlotAgent](./src/marketplace/types.ts#L12)[]>
|
||||
|
||||
Example:
|
||||
|
||||
@ -130,7 +165,7 @@ const slot = await marketplace.activeSlot(slotId);
|
||||
|
||||
Returns storage that is for sale.
|
||||
|
||||
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L99)>
|
||||
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)>
|
||||
|
||||
Example:
|
||||
|
||||
@ -142,16 +177,16 @@ const availabilities = await marketplace.availabilities();
|
||||
|
||||
Offers storage for sale.
|
||||
|
||||
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L175), required)
|
||||
- returns Promise<[CodexAvailabilityCreateResponse](./src/marketplace/types.ts#L186)[]>
|
||||
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L45), required)
|
||||
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)[]>
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const response = await marketplace.createAvailability({
|
||||
maxCollateral: 1,
|
||||
totalCollateral: 1,
|
||||
totalSize: 3000,
|
||||
minPrice: 100,
|
||||
minPricePerBytePerSecond: 100,
|
||||
duration: 100,
|
||||
});
|
||||
```
|
||||
@ -160,7 +195,7 @@ const response = await marketplace.createAvailability({
|
||||
|
||||
Updates availability.
|
||||
|
||||
- input ([CodexUpdateAvailabilityInput](./src/marketplace/types.ts#L186), required)
|
||||
- input ([CodexAvailabilityPatchInput](./src/marketplace/types.ts#L66), required)
|
||||
- returns Promise<"">
|
||||
|
||||
Example:
|
||||
@ -168,9 +203,9 @@ Example:
|
||||
```js
|
||||
const response = await marketplace.updateAvailability({
|
||||
id: "0x.....................",
|
||||
maxCollateral: 1,
|
||||
totalCollateral: 1,
|
||||
totalSize: 3000,
|
||||
minPrice: 100,
|
||||
minPricePerBytePerSecond: 100,
|
||||
duration: 100,
|
||||
});
|
||||
```
|
||||
@ -180,7 +215,7 @@ const response = await marketplace.updateAvailability({
|
||||
Return list of reservations for ongoing Storage Requests that the node hosts.
|
||||
|
||||
- availabilityId (string, required)
|
||||
- returns Promise<[CodexReservation](./src/marketplace/types.ts#L198)[]>
|
||||
- returns Promise<[CodexReservation](./src/marketplace/types.ts#L83)[]>
|
||||
|
||||
Example:
|
||||
|
||||
@ -192,7 +227,7 @@ const reservations = await marketplace.reservations("Ox...");
|
||||
|
||||
Creates a new Request for storage
|
||||
|
||||
- input ([CodexCreateStorageRequestInput](./src/marketplace/types.ts#L230), required)
|
||||
- input ([CodexCreateStorageRequestInput](./src/marketplace/types.ts#L120), required)
|
||||
- returns Promise<string>
|
||||
|
||||
Example:
|
||||
@ -226,7 +261,7 @@ const ids = await marketplace.purchaseIds();
|
||||
Returns purchase details
|
||||
|
||||
- purchaseId (string, required)
|
||||
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L214)[]>
|
||||
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L103)[]>
|
||||
|
||||
Example:
|
||||
|
||||
@ -240,15 +275,19 @@ const purchase = await marketplace.purchaseDetail(purchaseId);
|
||||
The following API assume that you have already a data module loaded, example:
|
||||
|
||||
```js
|
||||
const codex = new Codex("http://localhost:3000");
|
||||
const data = await codex.data;
|
||||
const codex = new Codex("http://localhost:8080");
|
||||
// When using the async api
|
||||
const data = await codex.data();
|
||||
|
||||
// When using the sync api
|
||||
const data = codex.data;
|
||||
```
|
||||
|
||||
#### cids
|
||||
|
||||
Returns the manifest stored locally in node.
|
||||
|
||||
- returns Promise<[CodexDataResponse](./src/data/types.ts#L54)[]>
|
||||
- returns Promise<[CodexDataItem](./src/data/types.ts#L8)[]>
|
||||
|
||||
Example:
|
||||
|
||||
@ -260,7 +299,7 @@ const cids = await data.cids();
|
||||
|
||||
Returns a summary of the storage space allocation of the node
|
||||
|
||||
- returns Promise<[CodexNodeSpace](./src/data/types.ts#L58)[]>
|
||||
- returns Promise<[CodexNodeSpace](./src/data/types.ts#L15)[]>
|
||||
|
||||
Example:
|
||||
|
||||
@ -272,24 +311,55 @@ const space = await data.space();
|
||||
|
||||
Upload a file in a streaming manner
|
||||
|
||||
- file (File, required)
|
||||
- onProgress (onProgress: (loaded: number, total: number) => void, optional)
|
||||
- metadata ({ filename?: string, mimetype?: string }, optional)
|
||||
- returns [UploadResponse](./src/data/types.ts#L80)
|
||||
#### Browser
|
||||
|
||||
- strategy [BrowserUploadStrategy](./src/data/browser-upload.ts#L5)
|
||||
- returns [UploadResponse](./src/data/types.ts#L17)
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
// Get file from previous event
|
||||
const [file] = e.target.files
|
||||
const metadata = {
|
||||
filename: file.name,
|
||||
mimetype: file.type,
|
||||
const file = new File(["foo"], "foo.txt", { type: "text/plain" });
|
||||
|
||||
const onProgress = (loaded, total) => {
|
||||
console.info("Loaded", loaded, "total", total);
|
||||
};
|
||||
|
||||
const metadata = { filename: "foo.xt", mimetype: "text/plain" };
|
||||
|
||||
const strategy = new BrowserUploadStrategy(file, onProgress, metadata);
|
||||
|
||||
const uploadResponse = data.upload(strategy);
|
||||
|
||||
const res = await uploadResponse.result;
|
||||
|
||||
if (res.error) {
|
||||
console.error(res.data);
|
||||
return;
|
||||
}
|
||||
const upload = data.upload(file, (loaded: number, total: number) => {
|
||||
// Use loaded and total so update a progress bar for example
|
||||
}, metadata);
|
||||
await upload.result();
|
||||
|
||||
console.info("CID is", res.data);
|
||||
```
|
||||
|
||||
#### Node
|
||||
|
||||
- strategy [NodeUploadStrategy](./src/data/node-upload.ts#L9)
|
||||
- returns [UploadResponse](./src/data/types.ts#L17)
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const strategy = new NodeUploadStrategy("Hello World !");
|
||||
const uploadResponse = data.upload(strategy);
|
||||
|
||||
const res = await uploadResponse.result;
|
||||
|
||||
if (res.error) {
|
||||
console.error(res.data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("CID is", res.data);
|
||||
```
|
||||
|
||||
#### manifest
|
||||
@ -297,7 +367,7 @@ await upload.result();
|
||||
Download only the dataset manifest from the network to the local node if it's not available locally.
|
||||
|
||||
- cid (string, required)
|
||||
- returns [CodexManifest](./src/data/types.ts#L3)
|
||||
- returns [CodexManifest](./src/data/types.ts#L30)
|
||||
|
||||
Example:
|
||||
|
||||
@ -336,20 +406,39 @@ const cid = "QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N";
|
||||
const result = await data.localDownload(cid);
|
||||
```
|
||||
|
||||
#### delete
|
||||
|
||||
Deletes either a single block or an entire dataset from the local node.
|
||||
Does nothing if the dataset is not locally available.
|
||||
|
||||
- cid (string, required)
|
||||
- returns ""
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const cid = "QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N";
|
||||
const result = await data.delete(cid);
|
||||
```
|
||||
|
||||
### Debug
|
||||
|
||||
The following API assume that you have already a node module loaded, example:
|
||||
|
||||
```js
|
||||
const codex = new Codex("http://localhost:3000");
|
||||
const data = await codex.debug;
|
||||
const codex = new Codex("http://localhost:8080");
|
||||
// When using the async api
|
||||
const data = await codex.debug();
|
||||
|
||||
// When using the sync api
|
||||
const data = codex.debug;
|
||||
```
|
||||
|
||||
#### setLogLevel
|
||||
|
||||
Set log level at run time.
|
||||
|
||||
- level ([CodexLogLevel](./src/debug/types.ts#L3), required)
|
||||
- level ([CodexLogLevel](./src/debug/types.ts#L7), required)
|
||||
- returns Promise<"">
|
||||
|
||||
Example:
|
||||
@ -362,7 +451,7 @@ await debug.setLogLevel("DEBUG");
|
||||
|
||||
Gets node information
|
||||
|
||||
- returns Promise<[CodexDebugInfo](./src/debug/types.ts#L15)>
|
||||
- returns Promise<[CodexDebugInfo](./src/debug/types.ts#L23)>
|
||||
|
||||
Example:
|
||||
|
||||
@ -375,18 +464,56 @@ const info = await debug.info();
|
||||
The following API assume that you have already a node module loaded, example:
|
||||
|
||||
```js
|
||||
const codex = new Codex("http://localhost:3000");
|
||||
const node = await codex.node;
|
||||
const codex = new Codex("http://localhost:8080");
|
||||
// When using the async api
|
||||
const node = await codex.node();
|
||||
|
||||
// When using the sync api
|
||||
const node = codex.node;
|
||||
```
|
||||
|
||||
#### spr
|
||||
|
||||
Get Node's SPR
|
||||
|
||||
- returns Promise<[CodexSpr](./src/node/types.ts#L1)>
|
||||
- returns Promise<[CodexSpr](./src/node/types.ts#L11)>
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const spr = await node.spr();
|
||||
```
|
||||
|
||||
By default, the response will be a json. You can use `text` option to get the string:
|
||||
|
||||
#### peeriD
|
||||
|
||||
Get Node's peer id
|
||||
|
||||
- returns Promise<[CodexPeerId](./src/node/types.ts#L25)>
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const peerId = await node.peerId();
|
||||
```
|
||||
|
||||
By default, the response will be a json. You can use `text` option to get the string:
|
||||
|
||||
```js
|
||||
const peerId = await node.peerId("text");
|
||||
```
|
||||
|
||||
#### connect
|
||||
|
||||
Connect to a peer
|
||||
|
||||
- returns Promise<string>
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const peerId = "..."
|
||||
const addrs = [...]
|
||||
const spr = await node.connect(peerId, addrs);
|
||||
```
|
||||
|
||||
1
examples/basic-auth/.gitignore
vendored
Normal file
1
examples/basic-auth/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
index.bundle.js
|
||||
23
examples/basic-auth/README.md
Normal file
23
examples/basic-auth/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Download example
|
||||
|
||||
Small example to show how to download a file in the browser with Codex.
|
||||
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Build the javascript asset
|
||||
|
||||
```bash
|
||||
CODEX_CID=REPLACE_BY_YOUR_CID npm run build
|
||||
```
|
||||
|
||||
The response will be displayed as text, so it is better to test with .txt files.
|
||||
|
||||
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".
|
||||
|
||||
## Check the results
|
||||
|
||||
Open the index.html and open the web console.
|
||||
22
examples/basic-auth/esbuild.js
Normal file
22
examples/basic-auth/esbuild.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { build } = require("esbuild");
|
||||
const define = {};
|
||||
|
||||
for (const k in process.env) {
|
||||
define[`process.env.${k}`] = JSON.stringify(process.env[k]);
|
||||
}
|
||||
|
||||
if (!process.env["CODEX_NODE_URL"]) {
|
||||
define[`process.env.CODEX_NODE_URL`] = '"http://localhost:8080"';
|
||||
}
|
||||
|
||||
const options = {
|
||||
entryPoints: ["./index.js"],
|
||||
outfile: "./index.bundle.js",
|
||||
bundle: true,
|
||||
define,
|
||||
logOverride: {
|
||||
"ignored-bare-import": "silent",
|
||||
},
|
||||
};
|
||||
|
||||
build(options).catch(() => process.exit(1));
|
||||
4
examples/basic-auth/index.html
Normal file
4
examples/basic-auth/index.html
Normal file
@ -0,0 +1,4 @@
|
||||
<html>
|
||||
<script src="./index.bundle.js">
|
||||
</script>
|
||||
</html>
|
||||
19
examples/basic-auth/index.js
Normal file
19
examples/basic-auth/index.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { Codex } from "@codex-storage/sdk-js";
|
||||
|
||||
async function main() {
|
||||
const codex = new Codex(process.env.CODEX_NODE_URL, {
|
||||
auth: {
|
||||
basic: btoa("admin:SuperSecret123"),
|
||||
},
|
||||
});
|
||||
|
||||
const data = codex.data;
|
||||
|
||||
const cid = process.env.CODEX_CID;
|
||||
|
||||
const result = await data.networkDownloadStream(cid);
|
||||
|
||||
console.info(await result.data.text());
|
||||
}
|
||||
|
||||
main();
|
||||
531
examples/basic-auth/package-lock.json
generated
Normal file
531
examples/basic-auth/package-lock.json
generated
Normal file
@ -0,0 +1,531 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js-basic-auth-example",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@codex-storage/sdk-js-basic-auth-example",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "../.."
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.1",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
},
|
||||
"..": {
|
||||
"extraneous": true
|
||||
},
|
||||
"../..": {
|
||||
"name": "@codex-storage/sdk-js",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici": "^7.5.0",
|
||||
"valibot": "^0.32.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"@types/node": "^22.13.13",
|
||||
"oas-normalize": "^13.1.2",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"prettier": "^3.5.3",
|
||||
"tsup": "^8.3.6",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@codex-storage/sdk-js": {
|
||||
"resolved": "../..",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
|
||||
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
|
||||
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
|
||||
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
|
||||
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
|
||||
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
|
||||
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
|
||||
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
|
||||
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
|
||||
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
|
||||
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
|
||||
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.1",
|
||||
"@esbuild/android-arm": "0.25.1",
|
||||
"@esbuild/android-arm64": "0.25.1",
|
||||
"@esbuild/android-x64": "0.25.1",
|
||||
"@esbuild/darwin-arm64": "0.25.1",
|
||||
"@esbuild/darwin-x64": "0.25.1",
|
||||
"@esbuild/freebsd-arm64": "0.25.1",
|
||||
"@esbuild/freebsd-x64": "0.25.1",
|
||||
"@esbuild/linux-arm": "0.25.1",
|
||||
"@esbuild/linux-arm64": "0.25.1",
|
||||
"@esbuild/linux-ia32": "0.25.1",
|
||||
"@esbuild/linux-loong64": "0.25.1",
|
||||
"@esbuild/linux-mips64el": "0.25.1",
|
||||
"@esbuild/linux-ppc64": "0.25.1",
|
||||
"@esbuild/linux-riscv64": "0.25.1",
|
||||
"@esbuild/linux-s390x": "0.25.1",
|
||||
"@esbuild/linux-x64": "0.25.1",
|
||||
"@esbuild/netbsd-arm64": "0.25.1",
|
||||
"@esbuild/netbsd-x64": "0.25.1",
|
||||
"@esbuild/openbsd-arm64": "0.25.1",
|
||||
"@esbuild/openbsd-x64": "0.25.1",
|
||||
"@esbuild/sunos-x64": "0.25.1",
|
||||
"@esbuild/win32-arm64": "0.25.1",
|
||||
"@esbuild/win32-ia32": "0.25.1",
|
||||
"@esbuild/win32-x64": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
examples/basic-auth/package.json
Normal file
18
examples/basic-auth/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js-basic-auth-example",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "node esbuild.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "../.."
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.1",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
||||
1
examples/download-browser/.gitignore
vendored
Normal file
1
examples/download-browser/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
index.bundle.js
|
||||
23
examples/download-browser/README.md
Normal file
23
examples/download-browser/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Download example
|
||||
|
||||
Small example to show how to download a file in the browser with Codex.
|
||||
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Build the javascript asset
|
||||
|
||||
```bash
|
||||
CODEX_CID=REPLACE_BY_YOUR_CID npm run build
|
||||
```
|
||||
|
||||
The response will be displayed as text, so it is better to test with .txt files.
|
||||
|
||||
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".
|
||||
|
||||
## Check the results
|
||||
|
||||
Open the index.html and open the web console.
|
||||
22
examples/download-browser/esbuild.js
Normal file
22
examples/download-browser/esbuild.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { build } = require("esbuild");
|
||||
const define = {};
|
||||
|
||||
for (const k in process.env) {
|
||||
define[`process.env.${k}`] = JSON.stringify(process.env[k]);
|
||||
}
|
||||
|
||||
if (!process.env["CODEX_NODE_URL"]) {
|
||||
define[`process.env.CODEX_NODE_URL`] = '"http://localhost:8080"';
|
||||
}
|
||||
|
||||
const options = {
|
||||
entryPoints: ["./index.js"],
|
||||
outfile: "./index.bundle.js",
|
||||
bundle: true,
|
||||
define,
|
||||
logOverride: {
|
||||
"ignored-bare-import": "silent",
|
||||
},
|
||||
};
|
||||
|
||||
build(options).catch(() => process.exit(1));
|
||||
4
examples/download-browser/index.html
Normal file
4
examples/download-browser/index.html
Normal file
@ -0,0 +1,4 @@
|
||||
<html>
|
||||
<script src="./index.bundle.js">
|
||||
</script>
|
||||
</html>
|
||||
15
examples/download-browser/index.js
Normal file
15
examples/download-browser/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { Codex } from "@codex-storage/sdk-js";
|
||||
|
||||
async function main() {
|
||||
const codex = new Codex(process.env.CODEX_NODE_URL);
|
||||
|
||||
const data = codex.data;
|
||||
|
||||
const cid = process.env.CODEX_CID;
|
||||
|
||||
const result = await data.networkDownloadStream(cid);
|
||||
|
||||
console.info(await result.data.text());
|
||||
}
|
||||
|
||||
main();
|
||||
18
examples/download-browser/package.json
Normal file
18
examples/download-browser/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js-download-browser-example",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "node esbuild.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "../.."
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.1",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
||||
1
examples/download-node/.gitignore
vendored
Normal file
1
examples/download-node/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
index.bundle.js
|
||||
19
examples/download-node/README.md
Normal file
19
examples/download-node/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Download example
|
||||
|
||||
Small example to show how to download a file in node with Codex.
|
||||
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Run node
|
||||
|
||||
```bash
|
||||
CODEX_CID=REPLACE_BY_YOUR_CID node index.js
|
||||
```
|
||||
|
||||
The response will be displayed as text, so it is better to test with .txt files.
|
||||
|
||||
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".
|
||||
17
examples/download-node/index.js
Normal file
17
examples/download-node/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { Codex } from "@codex-storage/sdk-js";
|
||||
|
||||
async function main() {
|
||||
const codex = new Codex(
|
||||
process.env.CODEX_NODE_URL || "http://localhost:8080"
|
||||
);
|
||||
|
||||
const data = codex.data;
|
||||
|
||||
const cid = process.env.CODEX_CID;
|
||||
|
||||
const result = await data.networkDownloadStream(cid);
|
||||
|
||||
console.info(await result.data.text());
|
||||
}
|
||||
|
||||
main();
|
||||
74
examples/download-node/package-lock.json
generated
Normal file
74
examples/download-node/package-lock.json
generated
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js-download-node-example",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@codex-storage/sdk-js-download-node-example",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "../..",
|
||||
"undici": "^7.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
},
|
||||
"..": {
|
||||
"extraneous": true
|
||||
},
|
||||
"../..": {
|
||||
"name": "@codex-storage/sdk-js",
|
||||
"version": "0.1.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"valibot": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"@types/node": "^22.13.17",
|
||||
"oas-normalize": "^13.1.2",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"prettier": "^3.5.3",
|
||||
"tsup": "^8.3.6",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"undici": "^7.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codex-storage/sdk-js": {
|
||||
"resolved": "../..",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz",
|
||||
"integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
examples/download-node/package.json
Normal file
18
examples/download-node/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js-download-node-example",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "node esbuild.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "../..",
|
||||
"undici": "^7.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
||||
1
examples/upload-browser/.gitignore
vendored
Normal file
1
examples/upload-browser/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
index.bundle.js
|
||||
23
examples/upload-browser/README.md
Normal file
23
examples/upload-browser/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Download example
|
||||
|
||||
Small example to show how to download a file in the browser with Codex.
|
||||
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Build the javascript asset
|
||||
|
||||
```bash
|
||||
CODEX_CID=REPLACE_BY_YOUR_CID npm run build
|
||||
```
|
||||
|
||||
The response will be displayed as text, so it is better to test with .txt files.
|
||||
|
||||
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".
|
||||
|
||||
## Check the results
|
||||
|
||||
Open the index.html and open the web console.
|
||||
22
examples/upload-browser/esbuild.js
Normal file
22
examples/upload-browser/esbuild.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { build } = require("esbuild");
|
||||
const define = {};
|
||||
|
||||
for (const k in process.env) {
|
||||
define[`process.env.${k}`] = JSON.stringify(process.env[k]);
|
||||
}
|
||||
|
||||
if (!process.env["CODEX_NODE_URL"]) {
|
||||
define[`process.env.CODEX_NODE_URL`] = '"http://localhost:8080"';
|
||||
}
|
||||
|
||||
const options = {
|
||||
entryPoints: ["./index.js"],
|
||||
outfile: "./index.bundle.js",
|
||||
bundle: true,
|
||||
define,
|
||||
logOverride: {
|
||||
"ignored-bare-import": "silent",
|
||||
},
|
||||
};
|
||||
|
||||
build(options).catch(() => process.exit(1));
|
||||
4
examples/upload-browser/index.html
Normal file
4
examples/upload-browser/index.html
Normal file
@ -0,0 +1,4 @@
|
||||
<html>
|
||||
<script src="./index.bundle.js">
|
||||
</script>
|
||||
</html>
|
||||
36
examples/upload-browser/index.js
Normal file
36
examples/upload-browser/index.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { Codex } from "@codex-storage/sdk-js";
|
||||
import { BrowserUploadStrategy } from "@codex-storage/sdk-js/browser";
|
||||
|
||||
async function main() {
|
||||
const codex = new Codex(process.env.CODEX_NODE_URL);
|
||||
|
||||
const data = codex.data;
|
||||
|
||||
const file = new File(["foo"], "foo.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
const onProgress = (loaded, total) => {
|
||||
console.info("Loaded", loaded, "total", total);
|
||||
};
|
||||
|
||||
const metadata = {
|
||||
filename: "foo.xt",
|
||||
mimetype: "text/plain",
|
||||
};
|
||||
|
||||
const strategy = new BrowserUploadStrategy(file, onProgress, metadata);
|
||||
|
||||
const uploadResponse = data.upload(strategy);
|
||||
|
||||
const res = await uploadResponse.result;
|
||||
|
||||
if (res.error) {
|
||||
console.error(res.data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("CID is", res.data);
|
||||
}
|
||||
|
||||
main();
|
||||
531
examples/upload-browser/package-lock.json
generated
Normal file
531
examples/upload-browser/package-lock.json
generated
Normal file
@ -0,0 +1,531 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js-update-browser-example",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@codex-storage/sdk-js-update-browser-example",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "../.."
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.1",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
},
|
||||
"..": {
|
||||
"extraneous": true
|
||||
},
|
||||
"../..": {
|
||||
"name": "@codex-storage/sdk-js",
|
||||
"version": "0.0.23",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici": "^7.5.0",
|
||||
"valibot": "^0.32.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"prettier": "^3.5.3",
|
||||
"tsup": "^8.3.6",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"../dist": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@codex-storage/sdk-js": {
|
||||
"resolved": "../..",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
|
||||
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
|
||||
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
|
||||
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
|
||||
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
|
||||
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
|
||||
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
|
||||
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
|
||||
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
|
||||
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
|
||||
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
|
||||
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.1",
|
||||
"@esbuild/android-arm": "0.25.1",
|
||||
"@esbuild/android-arm64": "0.25.1",
|
||||
"@esbuild/android-x64": "0.25.1",
|
||||
"@esbuild/darwin-arm64": "0.25.1",
|
||||
"@esbuild/darwin-x64": "0.25.1",
|
||||
"@esbuild/freebsd-arm64": "0.25.1",
|
||||
"@esbuild/freebsd-x64": "0.25.1",
|
||||
"@esbuild/linux-arm": "0.25.1",
|
||||
"@esbuild/linux-arm64": "0.25.1",
|
||||
"@esbuild/linux-ia32": "0.25.1",
|
||||
"@esbuild/linux-loong64": "0.25.1",
|
||||
"@esbuild/linux-mips64el": "0.25.1",
|
||||
"@esbuild/linux-ppc64": "0.25.1",
|
||||
"@esbuild/linux-riscv64": "0.25.1",
|
||||
"@esbuild/linux-s390x": "0.25.1",
|
||||
"@esbuild/linux-x64": "0.25.1",
|
||||
"@esbuild/netbsd-arm64": "0.25.1",
|
||||
"@esbuild/netbsd-x64": "0.25.1",
|
||||
"@esbuild/openbsd-arm64": "0.25.1",
|
||||
"@esbuild/openbsd-x64": "0.25.1",
|
||||
"@esbuild/sunos-x64": "0.25.1",
|
||||
"@esbuild/win32-arm64": "0.25.1",
|
||||
"@esbuild/win32-ia32": "0.25.1",
|
||||
"@esbuild/win32-x64": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
examples/upload-browser/package.json
Normal file
18
examples/upload-browser/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js-update-browser-example",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "node esbuild.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "../.."
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.1",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
||||
17
examples/upload-node/README.md
Normal file
17
examples/upload-node/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Download example
|
||||
|
||||
Small example to show how to download a file in the browser with Codex.
|
||||
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Run node
|
||||
|
||||
```bash
|
||||
node index.js
|
||||
```
|
||||
|
||||
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".
|
||||
23
examples/upload-node/index.js
Normal file
23
examples/upload-node/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
const { Codex } = require("@codex-storage/sdk-js");
|
||||
const { NodeUploadStrategy } = require("@codex-storage/sdk-js/node");
|
||||
|
||||
async function main() {
|
||||
const codex = new Codex(
|
||||
process.env.CODEX_NODE_URL || "http://localhost:8080"
|
||||
);
|
||||
const data = codex.data;
|
||||
|
||||
const strategy = new NodeUploadStrategy("Hello World !");
|
||||
const uploadResponse = data.upload(strategy);
|
||||
|
||||
const res = await uploadResponse.result;
|
||||
|
||||
if (res.error) {
|
||||
console.error(res.data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("CID is", res.data);
|
||||
}
|
||||
|
||||
main();
|
||||
80
examples/upload-node/package-lock.json
generated
Normal file
80
examples/upload-node/package-lock.json
generated
Normal file
@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js-update-node-example",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@codex-storage/sdk-js-update-node-example",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "../..",
|
||||
"undici": "^7.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
},
|
||||
"..": {
|
||||
"extraneous": true
|
||||
},
|
||||
"../..": {
|
||||
"name": "@codex-storage/sdk-js",
|
||||
"version": "0.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici": "^7.7.0",
|
||||
"valibot": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"@types/node": "^22.13.17",
|
||||
"oas-normalize": "^13.1.2",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"prettier": "^3.5.3",
|
||||
"tsup": "^8.3.6",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"undici": "^7.6.0"
|
||||
}
|
||||
},
|
||||
"../dist": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@codex-storage/sdk-js": {
|
||||
"resolved": "../..",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.7.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.7.0.tgz",
|
||||
"integrity": "sha512-tZ6+5NBq4KH35rr46XJ2JPFKxfcBlYNaqLF/wyWIO9RMHqqU/gx/CLB1Y2qMcgB8lWw/bKHa7qzspqCN7mUHvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
examples/upload-node/package.json
Normal file
16
examples/upload-node/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js-update-node-example",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "../..",
|
||||
"undici": "^7.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
||||
1043
openapi.yaml
Normal file
1043
openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
2260
package-lock.json
generated
2260
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@codex-storage/sdk-js",
|
||||
"version": "0.0.22",
|
||||
"version": "0.1.3",
|
||||
"description": "Codex SDK to interact with the Codex decentralized storage network.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"prepack": "npm run build",
|
||||
"prebuild": "npm run compile && rm -Rf dist/*",
|
||||
"build": "tsup src/index.ts src/async.ts --format esm,cjs --dts --sourcemap --treeshake",
|
||||
"build": "tsup src/index.ts src/async.ts src/browser.ts src/node.ts --format esm,cjs --dts --sourcemap --treeshake",
|
||||
"compile": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
@ -35,6 +35,26 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./browser": {
|
||||
"import": {
|
||||
"types": "./dist/browser.d.ts",
|
||||
"default": "./dist/browser.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/browser.d.cts",
|
||||
"default": "./dist/browser.js"
|
||||
}
|
||||
},
|
||||
"./node": {
|
||||
"import": {
|
||||
"types": "./dist/node.d.ts",
|
||||
"default": "./dist/node.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/node.d.cts",
|
||||
"default": "./dist/node.js"
|
||||
}
|
||||
},
|
||||
"./async": {
|
||||
"import": {
|
||||
"types": "./dist/async.d.ts",
|
||||
@ -46,7 +66,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
@ -54,16 +73,22 @@
|
||||
"readme": "README.md",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=20.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"prettier": "^3.4.2",
|
||||
"@types/node": "^22.13.17",
|
||||
"oas-normalize": "^14.0.0",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"prettier": "^3.5.3",
|
||||
"tsup": "^8.3.6",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.5"
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"valibot": "^0.32.0"
|
||||
"valibot": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"undici": "^7.7.0"
|
||||
}
|
||||
}
|
||||
1
src/browser.ts
Normal file
1
src/browser.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./data/browser-upload";
|
||||
81
src/data/browser-upload.ts
Normal file
81
src/data/browser-upload.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { CodexError } from "../errors/errors";
|
||||
import type { SafeValue } from "../values/values";
|
||||
import type { UploadStrategy, UploadStrategyOptions } from "./types";
|
||||
|
||||
export class BrowserUploadStrategy implements UploadStrategy {
|
||||
private readonly file: Document | XMLHttpRequestBodyInit;
|
||||
private readonly onProgress:
|
||||
| ((loaded: number, total: number) => void)
|
||||
| undefined;
|
||||
private readonly metadata:
|
||||
| { filename?: string; mimetype?: string }
|
||||
| undefined;
|
||||
private xhr: XMLHttpRequest | undefined;
|
||||
|
||||
constructor(
|
||||
file: Document | XMLHttpRequestBodyInit,
|
||||
onProgress?: (loaded: number, total: number) => void,
|
||||
metadata?: { filename?: string; mimetype?: string }
|
||||
) {
|
||||
this.file = file;
|
||||
this.onProgress = onProgress;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
upload(
|
||||
url: string,
|
||||
{ auth }: UploadStrategyOptions
|
||||
): Promise<SafeValue<string>> {
|
||||
const xhr = new XMLHttpRequest();
|
||||
this.xhr = xhr;
|
||||
|
||||
return new Promise<SafeValue<string>>((resolve) => {
|
||||
xhr.upload.onprogress = (evt) => {
|
||||
if (evt.lengthComputable) {
|
||||
this.onProgress?.(evt.loaded, evt.total);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open("POST", url, true);
|
||||
|
||||
if (this.metadata?.filename) {
|
||||
xhr.setRequestHeader(
|
||||
"Content-Disposition",
|
||||
'attachment; filename="' + this.metadata.filename + '"'
|
||||
);
|
||||
}
|
||||
|
||||
if (auth?.basic) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + auth.basic);
|
||||
}
|
||||
|
||||
if (this.metadata?.mimetype) {
|
||||
xhr.setRequestHeader("Content-Type", this.metadata.mimetype);
|
||||
}
|
||||
|
||||
xhr.send(this.file);
|
||||
|
||||
xhr.onload = function () {
|
||||
if (xhr.status != 200) {
|
||||
resolve({
|
||||
error: true,
|
||||
data: new CodexError(xhr.responseText, { code: xhr.status }),
|
||||
});
|
||||
} else {
|
||||
resolve({ error: false, data: xhr.response });
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
resolve({
|
||||
error: true,
|
||||
data: new CodexError("Something went wrong during the file upload."),
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
abort(): void {
|
||||
this.xhr?.abort();
|
||||
}
|
||||
}
|
||||
201
src/data/data.spec.ts
Normal file
201
src/data/data.spec.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
import { CodexData } from "./data";
|
||||
import { NodeUploadStrategy } from "./node-upload";
|
||||
import crypto from "crypto";
|
||||
|
||||
describe("data", () => {
|
||||
const data = new CodexData(
|
||||
process.env["CLIENT_URL"] || "http://localhost:8080"
|
||||
);
|
||||
const spData = new CodexData(
|
||||
process.env["SP_URL"] || "http://localhost:8081"
|
||||
);
|
||||
|
||||
it("uploads a file a download it locally", async () => {
|
||||
const content = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const strategy = new NodeUploadStrategy(content);
|
||||
const res = data.upload(strategy);
|
||||
const cid = await res.result;
|
||||
assert.ok(cid.error == false);
|
||||
assert.ok(cid.data);
|
||||
|
||||
const cids = await data.cids();
|
||||
assert.ok(cids.error == false);
|
||||
assert.ok(cids.data.content.find((c) => c.cid == cid.data));
|
||||
|
||||
const localDownload = await data.localDownload(cid.data);
|
||||
assert.ok(localDownload.error == false);
|
||||
assert.strictEqual(await localDownload.data.text(), content);
|
||||
|
||||
const manifest = await data.fetchManifest(cid.data);
|
||||
assert.ok(manifest.error == false);
|
||||
assert.strictEqual(manifest.data.cid, cid.data);
|
||||
|
||||
const { blockSize, datasetSize, treeCid } = manifest.data.manifest;
|
||||
assert.ok(blockSize);
|
||||
assert.ok(datasetSize);
|
||||
assert.ok(treeCid);
|
||||
});
|
||||
|
||||
it("saves the metadata uploads provided during the upload", async () => {
|
||||
const content = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const strategy = new NodeUploadStrategy(content, {
|
||||
filename: "hello.txt",
|
||||
mimetype: "text/plain",
|
||||
});
|
||||
const res = data.upload(strategy);
|
||||
const cid = await res.result;
|
||||
|
||||
assert.ok(cid.error == false);
|
||||
assert.ok(cid.data);
|
||||
|
||||
const manifest = await data.fetchManifest(cid.data);
|
||||
assert.ok(manifest.error == false);
|
||||
assert.strictEqual(manifest.data.cid, cid.data);
|
||||
|
||||
const { filename, mimetype } = manifest.data.manifest;
|
||||
assert.strictEqual(filename, "hello.txt");
|
||||
assert.ok(mimetype, "text/plain");
|
||||
});
|
||||
|
||||
it("fails when providing wrong metadata", async () => {
|
||||
const content = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const strategy = new NodeUploadStrategy(content, {
|
||||
filename: "hello.txt",
|
||||
mimetype: "plain/text",
|
||||
});
|
||||
const res = data.upload(strategy);
|
||||
const cid = await res.result;
|
||||
|
||||
assert.ok(cid.error == true);
|
||||
assert.ok(
|
||||
cid.data.message.includes(" The MIME type 'plain/text' is not valid")
|
||||
);
|
||||
assert.equal(cid.data.code, 422);
|
||||
});
|
||||
|
||||
it("delete a file a locally", async () => {
|
||||
const content = "b".repeat(131072);
|
||||
const strategy = new NodeUploadStrategy(content);
|
||||
const res = data.upload(strategy);
|
||||
const cid = await res.result;
|
||||
assert.ok(cid.error == false);
|
||||
assert.ok(cid.data);
|
||||
|
||||
let cids = await data.cids();
|
||||
assert.ok(cids.error == false);
|
||||
assert.ok(cids.data.content.find((c) => c.cid == cid.data));
|
||||
|
||||
const del = await data.delete(cid.data);
|
||||
assert.ok(del.error == false);
|
||||
|
||||
cids = await data.cids();
|
||||
assert.ok(cids.error == false);
|
||||
assert.notOk(cids.data.content.find((c) => c.cid == cid.data));
|
||||
});
|
||||
|
||||
it("doesn't do anything when trying to delete a non existing cid", async () => {
|
||||
const content = crypto.randomBytes(16).toString("hex");
|
||||
const strategy = new NodeUploadStrategy(content);
|
||||
const res = spData.upload(strategy);
|
||||
const cid = await res.result;
|
||||
assert.ok(cid.error == false);
|
||||
assert.ok(cid.data);
|
||||
|
||||
const del = await data.delete(cid.data);
|
||||
assert.ok(del.error == false);
|
||||
});
|
||||
|
||||
it("returns an error when providing an invalid cid", async () => {
|
||||
const del = await data.delete("hello");
|
||||
assert.ok(del.error);
|
||||
assert.ok(del.data.message.includes("Incorrect Cid"));
|
||||
});
|
||||
|
||||
it("updates the space available when storing data", async () => {
|
||||
const content = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
let space = await data.space();
|
||||
assert.ok(space.error == false);
|
||||
assert.ok(space.data.quotaMaxBytes);
|
||||
|
||||
const usedBytes = space.data.quotaUsedBytes;
|
||||
|
||||
const strategy = new NodeUploadStrategy(content);
|
||||
const res = data.upload(strategy);
|
||||
const cid = await res.result;
|
||||
assert.ok(cid.error == false);
|
||||
assert.ok(cid.data);
|
||||
|
||||
space = await data.space();
|
||||
assert.ok(space.error == false);
|
||||
assert.ok(space.data.quotaMaxBytes);
|
||||
assert.ok(space.data.quotaUsedBytes > usedBytes);
|
||||
});
|
||||
|
||||
it("stream downloads a file on the network", async () => {
|
||||
const content = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const strategy = new NodeUploadStrategy(content);
|
||||
const res = spData.upload(strategy);
|
||||
const cid = await res.result;
|
||||
assert.ok(cid.error == false);
|
||||
assert.ok(cid.data);
|
||||
|
||||
const networkDownload = await data.networkDownloadStream(cid.data);
|
||||
assert.ok(networkDownload.error == false);
|
||||
assert.strictEqual(await networkDownload.data.text(), content);
|
||||
});
|
||||
|
||||
it("downloads a file on the network", async () => {
|
||||
const content = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const strategy = new NodeUploadStrategy(content);
|
||||
const res = spData.upload(strategy);
|
||||
const cid = await res.result;
|
||||
assert.ok(cid.error == false);
|
||||
assert.ok(cid.data);
|
||||
|
||||
const networkDownload = await data.networkDownload(cid.data);
|
||||
assert.ok(networkDownload.error == false);
|
||||
|
||||
const cids = await data.cids();
|
||||
assert.ok(cids.error == false);
|
||||
assert.ok(cids.data.content.find((c) => c.cid == cid.data));
|
||||
});
|
||||
|
||||
it("returns an error when trying to stream download a not existing file on the network", async () => {
|
||||
const cid = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const networkDownload = await data.networkDownloadStream(cid);
|
||||
assert.ok(networkDownload.error);
|
||||
assert.strictEqual(networkDownload.data.message, "Incorrect Cid");
|
||||
});
|
||||
|
||||
it("returns an error when trying to download a not existing file on the network", async () => {
|
||||
const cid = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const networkDownload = await data.networkDownload(cid);
|
||||
assert.ok(networkDownload.error);
|
||||
assert.strictEqual(networkDownload.data.message, "Incorrect Cid");
|
||||
});
|
||||
|
||||
it("returns an error when trying to download a not existing file locally", async () => {
|
||||
const cid = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const networkDownload = await data.localDownload(cid);
|
||||
assert.ok(networkDownload.error);
|
||||
assert.strictEqual(networkDownload.data.message, "Incorrect Cid");
|
||||
});
|
||||
|
||||
it("returns an error when trying to fetch a not existing manifest", async () => {
|
||||
const cid = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
const fetchManifest = await data.fetchManifest(cid);
|
||||
assert.ok(fetchManifest.error);
|
||||
assert.strictEqual(fetchManifest.data.message, "Incorrect Cid");
|
||||
});
|
||||
});
|
||||
145
src/data/data.ts
145
src/data/data.ts
@ -1,53 +1,66 @@
|
||||
import { Api } from "../api/config";
|
||||
import { CodexError } from "../errors/errors";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import {
|
||||
Fetch,
|
||||
FetchAuthBuilder,
|
||||
type FetchAuth,
|
||||
} from "../fetch-safe/fetch-safe";
|
||||
import type { SafeValue } from "../values/values";
|
||||
import type {
|
||||
CodexDataResponse,
|
||||
CodexManifest,
|
||||
CodexNodeSpace,
|
||||
NetworkDownloadResponse,
|
||||
UploadStrategy,
|
||||
UploadResponse,
|
||||
CodexSpaceResponse,
|
||||
CodexNodeSpace,
|
||||
CodexDataNetworkResponse,
|
||||
CodexNetworkDownload,
|
||||
CodexManifest,
|
||||
CodexDataItems,
|
||||
} from "./types";
|
||||
|
||||
type CodexDataOptions = {
|
||||
auth?: FetchAuth;
|
||||
};
|
||||
|
||||
export class CodexData {
|
||||
readonly url: string;
|
||||
readonly auth: FetchAuth = {};
|
||||
|
||||
constructor(url: string) {
|
||||
constructor(url: string, options?: CodexDataOptions) {
|
||||
this.url = url;
|
||||
|
||||
if (options?.auth) {
|
||||
this.auth = options.auth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists manifest CIDs stored locally in node.
|
||||
* TODO: remove the faker data part when the api is ready
|
||||
*/
|
||||
cids(): Promise<SafeValue<CodexDataResponse>> {
|
||||
cids(): Promise<SafeValue<CodexDataItems>> {
|
||||
const url = this.url + Api.config.prefix + "/data";
|
||||
|
||||
return Fetch.safeJson<CodexDataResponse>(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
}).then((data) => {
|
||||
if (data.error) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return {
|
||||
error: false,
|
||||
data: {
|
||||
content: data.data.content,
|
||||
},
|
||||
};
|
||||
return { error: false, data: { content: data.data.content } };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a summary of the storage space allocation of the node.
|
||||
*/
|
||||
space() {
|
||||
space(): Promise<SafeValue<CodexNodeSpace>> {
|
||||
const url = this.url + Api.config.prefix + "/space";
|
||||
|
||||
return Fetch.safeJson<CodexNodeSpace>(url, {
|
||||
return Fetch.safeJson<CodexSpaceResponse>(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
@ -57,59 +70,13 @@ export class CodexData {
|
||||
* XMLHttpRequest is used instead of fetch for this case, to obtain progress information.
|
||||
* A callback onProgress can be passed to receive upload progress data information.
|
||||
*/
|
||||
upload(
|
||||
file: Document | XMLHttpRequestBodyInit,
|
||||
onProgress?: (loaded: number, total: number) => void,
|
||||
metadata: { filename?: string, mimetype?: string } = {},
|
||||
): UploadResponse {
|
||||
upload(strategy: UploadStrategy): UploadResponse {
|
||||
const url = this.url + Api.config.prefix + "/data";
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const promise = new Promise<SafeValue<string>>((resolve) => {
|
||||
xhr.upload.onprogress = (evt) => {
|
||||
if (evt.lengthComputable) {
|
||||
onProgress?.(evt.loaded, evt.total);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open("POST", url, true);
|
||||
|
||||
if (metadata.filename) {
|
||||
xhr.setRequestHeader("Content-Disposition", "attachment; filename=\"" + metadata.filename + "\"")
|
||||
}
|
||||
|
||||
if (metadata.mimetype) {
|
||||
xhr.setRequestHeader("Content-Type", metadata.mimetype)
|
||||
}
|
||||
|
||||
xhr.send(file);
|
||||
|
||||
xhr.onload = function () {
|
||||
if (xhr.status != 200) {
|
||||
resolve({
|
||||
error: true,
|
||||
data: new CodexError(xhr.responseText, {
|
||||
code: xhr.status,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
resolve({ error: false, data: xhr.response });
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
resolve({
|
||||
error: true,
|
||||
data: new CodexError("Something went wrong during the file upload."),
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
result: promise,
|
||||
result: strategy.upload(url, { auth: this.auth }),
|
||||
abort: () => {
|
||||
xhr.abort();
|
||||
strategy.abort();
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -123,6 +90,20 @@ export class CodexData {
|
||||
|
||||
return Fetch.safe(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from the network to the local node if it's not available locally.
|
||||
* Note: Download is performed async. Call can return before download is completed.
|
||||
*/
|
||||
async networkDownload(cid: string): Promise<SafeValue<CodexNetworkDownload>> {
|
||||
const url = this.url + Api.config.prefix + `/data/${cid}/network`;
|
||||
|
||||
return Fetch.safeJson<CodexDataNetworkResponse>(url, {
|
||||
method: "POST",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
@ -130,35 +111,39 @@ export class CodexData {
|
||||
* Download a file from the network in a streaming manner.
|
||||
* If the file is not available locally, it will be retrieved from other nodes in the network if able.
|
||||
*/
|
||||
async networkDownload(cid: string): Promise<SafeValue<NetworkDownloadResponse>> {
|
||||
const url = this.url + Api.config.prefix + `/data/${cid}/network`;
|
||||
|
||||
return Fetch.safeJson(url, {
|
||||
method: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from the network in a streaming manner.
|
||||
* If the file is not available locally, it will be retrieved from other nodes in the network if able.
|
||||
*/
|
||||
async networkDownloadStream(cid: string): Promise<SafeValue<Response>> {
|
||||
const url = this.url + Api.config.prefix + `/data/${cid}/network/stream`;
|
||||
|
||||
return Fetch.safe(url, {
|
||||
method: "GET"
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download only the dataset manifest from the network to the local node
|
||||
* if it's not available locally.
|
||||
* Download only the dataset manifest from the network to the local node
|
||||
* if it's not available locally.
|
||||
*/
|
||||
async fetchManifest(cid: string) {
|
||||
async fetchManifest(cid: string): Promise<SafeValue<CodexManifest>> {
|
||||
const url = this.url + Api.config.prefix + `/data/${cid}/network/manifest`;
|
||||
|
||||
return Fetch.safeJson<CodexManifest>(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes either a single block or an entire dataset
|
||||
* from the local node. Does nothing
|
||||
* if the dataset is not locally available.
|
||||
*/
|
||||
async delete(cid: string): Promise<SafeValue<string>> {
|
||||
const url = this.url + Api.config.prefix + `/data/${cid}`;
|
||||
|
||||
return Fetch.safeText(url, {
|
||||
method: "DELETE",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
73
src/data/node-upload.ts
Normal file
73
src/data/node-upload.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import type { Readable } from "node:stream";
|
||||
import { CodexError } from "../errors/errors";
|
||||
import type { SafeValue } from "../values/values";
|
||||
import Undici from "undici";
|
||||
import { type FormData } from "undici";
|
||||
import type { UploadStrategy, UploadStrategyOptions } from "./types";
|
||||
import { FetchAuthBuilder } from "../fetch-safe/fetch-safe";
|
||||
|
||||
export class NodeUploadStrategy implements UploadStrategy {
|
||||
private readonly body:
|
||||
| string
|
||||
| Buffer
|
||||
| Uint8Array
|
||||
| null
|
||||
| Readable
|
||||
| FormData;
|
||||
private readonly metadata:
|
||||
| { filename?: string; mimetype?: string }
|
||||
| undefined;
|
||||
private abortController: AbortController | undefined;
|
||||
|
||||
constructor(
|
||||
body: string | Buffer | Uint8Array | null | Readable | FormData,
|
||||
metadata?: { filename?: string; mimetype?: string }
|
||||
) {
|
||||
this.body = body;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
async upload(
|
||||
url: string,
|
||||
{ auth }: UploadStrategyOptions
|
||||
): Promise<SafeValue<string>> {
|
||||
const headers: Record<string, string> = FetchAuthBuilder.build(auth);
|
||||
|
||||
if (this.metadata?.filename) {
|
||||
headers["Content-Disposition"] =
|
||||
'attachment; filename="' + this.metadata?.filename + '"';
|
||||
}
|
||||
|
||||
if (this.metadata?.mimetype) {
|
||||
headers["Content-Type"] = this.metadata?.mimetype;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
this.abortController = controller;
|
||||
|
||||
const res = await Undici.request(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: this.body,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
const msg = `The status code is invalid got ${res.statusCode} - ${await res.body.text()} `;
|
||||
return {
|
||||
error: true,
|
||||
data: new CodexError(msg, { code: res.statusCode }),
|
||||
};
|
||||
}
|
||||
|
||||
return { error: false, data: await res.body.text() };
|
||||
}
|
||||
|
||||
abort(): void {
|
||||
try {
|
||||
this.abortController?.abort();
|
||||
} catch (_) {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,89 +1,47 @@
|
||||
import type { components, paths } from "../openapi";
|
||||
import type { FetchAuth } from "../fetch-safe/fetch-safe";
|
||||
import type { SafeValue } from "../values/values";
|
||||
|
||||
export type CodexManifest = {
|
||||
/**
|
||||
* "Root hash of the content"
|
||||
*/
|
||||
// rootHash: string;
|
||||
export type CodexDataResponse =
|
||||
paths["/data"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
/**
|
||||
* Length of original content in bytes
|
||||
*/
|
||||
// originalBytes: number;
|
||||
export type CodexDataItem = components["schemas"]["DataItem"];
|
||||
|
||||
/**
|
||||
* Total size of all blocks
|
||||
*/
|
||||
datasetSize: number;
|
||||
export type CodexDataItems = CodexDataResponse;
|
||||
|
||||
/**
|
||||
* "Size of blocks"
|
||||
*/
|
||||
blockSize: number;
|
||||
export type CodexSpaceResponse =
|
||||
paths["/space"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
/**
|
||||
* Indicates if content is protected by erasure-coding
|
||||
*/
|
||||
protected: boolean;
|
||||
|
||||
/**
|
||||
* Root of the merkle tree
|
||||
*/
|
||||
treeCid: string;
|
||||
|
||||
/**
|
||||
* Name of the name
|
||||
*/
|
||||
filename: string | null;
|
||||
|
||||
/**
|
||||
* Mimetype
|
||||
*/
|
||||
mimetype: string | null;
|
||||
};
|
||||
|
||||
export type CodexDataContent = {
|
||||
/**
|
||||
* Content Identifier as specified at https://github.com/multiformats/cid
|
||||
*/
|
||||
cid: string;
|
||||
|
||||
manifest: CodexManifest;
|
||||
};
|
||||
|
||||
export type CodexDataResponse = {
|
||||
content: CodexDataContent[];
|
||||
};
|
||||
|
||||
export type CodexNodeSpace = {
|
||||
/**
|
||||
* Number of blocks stored by the node
|
||||
*/
|
||||
totalBlocks: number;
|
||||
|
||||
/**
|
||||
* Maximum storage space used by the node
|
||||
*/
|
||||
quotaMaxBytes: number;
|
||||
|
||||
/**
|
||||
* Amount of storage space currently in use
|
||||
*/
|
||||
quotaUsedBytes: number;
|
||||
|
||||
/**
|
||||
* Amount of storage space reserved
|
||||
*/
|
||||
quotaReservedBytes: number;
|
||||
};
|
||||
export type CodexNodeSpace = CodexSpaceResponse;
|
||||
|
||||
export type UploadResponse = {
|
||||
result: Promise<SafeValue<string>>;
|
||||
abort: () => void;
|
||||
};
|
||||
|
||||
export type CodexDataNetworkResponse =
|
||||
paths["/data/{cid}/network"]["post"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
export type NetworkDownloadResponse = {
|
||||
cid: string
|
||||
manifest: CodexManifest
|
||||
}
|
||||
export type CodexNetworkDownload = components["schemas"]["DataItem"];
|
||||
|
||||
export type CodexFetchManifestResponse =
|
||||
paths["/data/{cid}/network/manifest"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
export type CodexManifest = CodexFetchManifestResponse;
|
||||
|
||||
export type UploadStrategyOptions = {
|
||||
auth?: FetchAuth;
|
||||
};
|
||||
|
||||
export interface UploadStrategy {
|
||||
upload(
|
||||
url: string,
|
||||
options?: UploadStrategyOptions
|
||||
): Promise<SafeValue<string>>;
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
// paths["/data/{cid}"]["delete"]["responses"][204]["content"];
|
||||
export type CodexDeleteResponse = "";
|
||||
|
||||
export type CodexDelete = CodexDeleteResponse;
|
||||
|
||||
@ -1,45 +1,26 @@
|
||||
import { afterEach, assert, describe, it, vi } from "vitest";
|
||||
import { assert, describe, it } from "vitest";
|
||||
import { CodexDebug } from "./debug";
|
||||
import type { CodexLogLevel } from "./types";
|
||||
import { CodexError } from "../errors/errors";
|
||||
|
||||
describe("debug", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
const debug = new CodexDebug(
|
||||
process.env["CLIENT_URL"] || "http://localhost:8080"
|
||||
);
|
||||
|
||||
it("changes the log level", async () => {
|
||||
const logLevel = await debug.setLogLevel("NOTICE");
|
||||
assert.ok(logLevel.error == false);
|
||||
});
|
||||
|
||||
const debug = new CodexDebug("http://localhost:3000");
|
||||
|
||||
it("returns an error when trying to setup the log level with a bad value", async () => {
|
||||
const response = await debug.setLogLevel("TEST" as CodexLogLevel);
|
||||
|
||||
assert.deepStrictEqual(response, {
|
||||
error: true,
|
||||
data: new CodexError("Cannot validate the input", {
|
||||
errors: [
|
||||
{
|
||||
expected:
|
||||
'"TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL"',
|
||||
message:
|
||||
'Invalid type: Expected "TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL" but received "TEST"',
|
||||
path: undefined,
|
||||
received: '"TEST"',
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
it("gets the debug info", async () => {
|
||||
const info = await debug.info();
|
||||
assert.ok(info.error == false);
|
||||
assert.ok(info.data.spr);
|
||||
assert.ok(info.data.announceAddresses.length > 0);
|
||||
});
|
||||
|
||||
it("returns a success when trying to setup the log level with a correct value", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => "",
|
||||
} as Response;
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(mockResponse);
|
||||
|
||||
const response = await debug.setLogLevel("ERROR");
|
||||
|
||||
assert.deepStrictEqual(response, { error: false, data: "" });
|
||||
it("returns error when changing the log level with wrong value", async () => {
|
||||
const logLevel = await debug.setLogLevel("HELLO");
|
||||
assert.ok(logLevel.error);
|
||||
assert.strictEqual(logLevel.data.message, "Cannot validate the input");
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,22 +1,40 @@
|
||||
import { Api } from "../api/config";
|
||||
import { CodexError, CodexValibotIssuesMap } from "../errors/errors";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import {
|
||||
Fetch,
|
||||
FetchAuthBuilder,
|
||||
type FetchAuth,
|
||||
} from "../fetch-safe/fetch-safe";
|
||||
import type { SafeValue } from "../values/values";
|
||||
import { CodexLogLevel, type CodexDebugInfo } from "./types";
|
||||
import {
|
||||
CodexLogLevelInput,
|
||||
type CodexDebugInfo,
|
||||
type CodexInfoResponse,
|
||||
type CodexLogLevel,
|
||||
} from "./types";
|
||||
import * as v from "valibot";
|
||||
|
||||
type CodexDebugOptions = {
|
||||
auth?: FetchAuth;
|
||||
};
|
||||
|
||||
export class CodexDebug {
|
||||
readonly url: string;
|
||||
readonly auth: FetchAuth = {};
|
||||
|
||||
constructor(url: string) {
|
||||
constructor(url: string, options?: CodexDebugOptions) {
|
||||
this.url = url;
|
||||
|
||||
if (options?.auth) {
|
||||
this.auth = options.auth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set log level at run time
|
||||
*/
|
||||
async setLogLevel(level: CodexLogLevel): Promise<SafeValue<"">> {
|
||||
const result = v.safeParse(CodexLogLevel, level);
|
||||
async setLogLevel(level: CodexLogLevel): Promise<SafeValue<string>> {
|
||||
const result = v.safeParse(CodexLogLevelInput, level);
|
||||
|
||||
if (!result.success) {
|
||||
return Promise.resolve({
|
||||
@ -33,26 +51,22 @@ export class CodexDebug {
|
||||
"/debug/chronicles/loglevel?level=" +
|
||||
level;
|
||||
|
||||
const res = await Fetch.safe(url, {
|
||||
return Fetch.safeText(url, {
|
||||
method: "POST",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
body: "",
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return { error: false, data: "" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets node information
|
||||
*/
|
||||
info() {
|
||||
info(): Promise<SafeValue<CodexDebugInfo>> {
|
||||
const url = this.url + Api.config.prefix + `/debug/info`;
|
||||
|
||||
return Fetch.safeJson<CodexDebugInfo>(url, {
|
||||
return Fetch.safeJson<CodexInfoResponse>(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
import * as v from "valibot";
|
||||
import type { paths } from "../openapi";
|
||||
|
||||
export const CodexLogLevel = v.picklist([
|
||||
export type CodexLogLevelResponse =
|
||||
paths["/debug/chronicles/loglevel"]["post"]["responses"][200]["content"];
|
||||
|
||||
export type CodexLogLevel =
|
||||
paths["/debug/chronicles/loglevel"]["post"]["parameters"]["query"]["level"];
|
||||
|
||||
export const CodexLogLevelInput = v.picklist([
|
||||
"TRACE",
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
@ -10,49 +17,7 @@ export const CodexLogLevel = v.picklist([
|
||||
"FATAL",
|
||||
]);
|
||||
|
||||
export type CodexLogLevel = v.InferOutput<typeof CodexLogLevel>;
|
||||
export type CodexInfoResponse =
|
||||
paths["/debug/info"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
export type CodexDebugInfo = {
|
||||
/**
|
||||
* Peer Identity reference as specified at https://docs.libp2p.io/concepts/fundamentals/peers/
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Address of node as specified by the multi-address specification https://multiformats.io/multiaddr/
|
||||
*/
|
||||
addrs: string[];
|
||||
|
||||
announceAddresses: string[]
|
||||
|
||||
/**
|
||||
* Path of the data repository where all nodes data are stored
|
||||
*/
|
||||
repo: string;
|
||||
|
||||
// Signed Peer Record (libp2p)
|
||||
spr: string;
|
||||
|
||||
table: {
|
||||
localNode: {
|
||||
nodeId: string
|
||||
peerId: string
|
||||
record: string
|
||||
address: string
|
||||
seen: boolean
|
||||
}
|
||||
|
||||
nodes: {
|
||||
nodeId: string
|
||||
peerId: string
|
||||
record: string
|
||||
address: string
|
||||
seen: boolean
|
||||
}[]
|
||||
}
|
||||
|
||||
codex: {
|
||||
version: string
|
||||
revision: string
|
||||
}
|
||||
};
|
||||
export type CodexDebugInfo = CodexInfoResponse;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { afterEach, assert, describe, it, vi } from "vitest";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import { CodexError } from "../async";
|
||||
import { CodexError } from "../errors/errors";
|
||||
|
||||
describe.only("fetch", () => {
|
||||
describe("fetch", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
@ -26,7 +26,7 @@ describe.only("fetch", () => {
|
||||
assert.deepStrictEqual(result, { error: true, data: error });
|
||||
});
|
||||
|
||||
it.only("returns an error when the json parsing failed", async () => {
|
||||
it("returns an error when the json parsing failed", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
|
||||
@ -2,6 +2,21 @@ import { CodexError } from "../errors/errors";
|
||||
import { Promises } from "../promise-safe/promise-safe";
|
||||
import { type SafeValue } from "../values/values";
|
||||
|
||||
export type FetchAuth = {
|
||||
basic?: string;
|
||||
};
|
||||
|
||||
export const FetchAuthBuilder = {
|
||||
build(auth: FetchAuth | undefined) {
|
||||
if (auth?.basic) {
|
||||
return {
|
||||
Authorization: "Basic " + auth.basic,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
export const Fetch = {
|
||||
async safe(url: string, init: RequestInit): Promise<SafeValue<Response>> {
|
||||
const res = await Promises.safe(() => fetch(url, init));
|
||||
@ -45,4 +60,14 @@ export const Fetch = {
|
||||
|
||||
return Promises.safe(() => res.data.json());
|
||||
},
|
||||
|
||||
async safeText(url: string, init: RequestInit): Promise<SafeValue<string>> {
|
||||
const res = await this.safe(url, init);
|
||||
|
||||
if (res.error) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return Promises.safe(() => res.data.text());
|
||||
},
|
||||
};
|
||||
|
||||
20
src/index.ts
20
src/index.ts
@ -2,6 +2,7 @@ import { CodexData } from "./data/data";
|
||||
import { CodexNode } from "./node/node";
|
||||
import { CodexMarketplace } from "./marketplace/marketplace";
|
||||
import { CodexDebug } from "./debug/debug";
|
||||
import type { FetchAuth } from "./fetch-safe/fetch-safe";
|
||||
|
||||
export * from "./fetch-safe/fetch-safe";
|
||||
export * from "./marketplace/types";
|
||||
@ -15,19 +16,28 @@ export { CodexData } from "./data/data";
|
||||
export { CodexNode } from "./node/node";
|
||||
export { CodexMarketplace } from "./marketplace/marketplace";
|
||||
|
||||
type CodexProps = {
|
||||
auth?: FetchAuth;
|
||||
};
|
||||
|
||||
export class Codex {
|
||||
readonly url: string;
|
||||
private _marketplace: CodexMarketplace | null;
|
||||
private _data: CodexData | null;
|
||||
private _node: CodexNode | null;
|
||||
private _debug: CodexDebug | null;
|
||||
private readonly auth: FetchAuth = {};
|
||||
|
||||
constructor(url: string) {
|
||||
constructor(url: string, options?: CodexProps) {
|
||||
this.url = url;
|
||||
this._marketplace = null;
|
||||
this._data = null;
|
||||
this._node = null;
|
||||
this._debug = null;
|
||||
|
||||
if (options?.auth) {
|
||||
this.auth = options?.auth;
|
||||
}
|
||||
}
|
||||
|
||||
get marketplace() {
|
||||
@ -35,7 +45,7 @@ export class Codex {
|
||||
return this._marketplace;
|
||||
}
|
||||
|
||||
this._marketplace = new CodexMarketplace(this.url);
|
||||
this._marketplace = new CodexMarketplace(this.url, { auth: this.auth });
|
||||
|
||||
return this._marketplace;
|
||||
}
|
||||
@ -45,7 +55,7 @@ export class Codex {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
this._data = new CodexData(this.url);
|
||||
this._data = new CodexData(this.url, { auth: this.auth });
|
||||
|
||||
return this._data;
|
||||
}
|
||||
@ -55,7 +65,7 @@ export class Codex {
|
||||
return this._node;
|
||||
}
|
||||
|
||||
this._node = new CodexNode(this.url);
|
||||
this._node = new CodexNode(this.url, { auth: this.auth });
|
||||
|
||||
return this._node;
|
||||
}
|
||||
@ -65,7 +75,7 @@ export class Codex {
|
||||
return this._debug;
|
||||
}
|
||||
|
||||
this._debug = new CodexDebug(this.url);
|
||||
this._debug = new CodexDebug(this.url, { auth: this.auth });
|
||||
|
||||
return this._debug;
|
||||
}
|
||||
|
||||
@ -1,379 +1,276 @@
|
||||
import { afterEach, assert, describe, it, vi } from "vitest";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import { assert, describe, it } from "vitest";
|
||||
import { CodexMarketplace } from "./marketplace";
|
||||
import {
|
||||
randomEthereumAddress,
|
||||
randomInt,
|
||||
randomString,
|
||||
} from "../tests/tests.util";
|
||||
import { CodexError } from "../errors/errors";
|
||||
import { CodexData } from "../data/data";
|
||||
import { NodeUploadStrategy } from "../data/node-upload";
|
||||
import type {
|
||||
CodexAvailabilityPatchInput,
|
||||
CodexCreateAvailabilityInput,
|
||||
CodexCreateStorageRequestInput,
|
||||
} from "./types";
|
||||
|
||||
function createStorageRequest() {
|
||||
return {
|
||||
cid: randomString(64),
|
||||
duration: randomInt(1, 64000),
|
||||
pricePerBytePerSecond: randomInt(1, 100),
|
||||
proofProbability: randomInt(1, 100),
|
||||
nodes: randomInt(1, 5),
|
||||
tolerance: randomInt(1, 100),
|
||||
expiry: randomInt(1, 100),
|
||||
collateralPerByte: randomInt(1, 100),
|
||||
};
|
||||
}
|
||||
|
||||
function missingNumberValidationError(field: string) {
|
||||
return {
|
||||
error: true as any,
|
||||
data: new CodexError("Cannot validate the input", {
|
||||
errors: [
|
||||
{
|
||||
path: field,
|
||||
expected: "number",
|
||||
message: "Invalid type: Expected number but received undefined",
|
||||
received: "undefined",
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function extraValidationError(field: string, value: unknown) {
|
||||
return {
|
||||
error: true as any,
|
||||
data: new CodexError("Cannot validate the input", {
|
||||
errors: [
|
||||
{
|
||||
path: field,
|
||||
expected: "never",
|
||||
message: `Invalid type: Expected never but received "${value}"`,
|
||||
received: `"${value}"`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function missingStringValidationError(field: string) {
|
||||
return {
|
||||
error: true as any,
|
||||
data: new CodexError("Cannot validate the input", {
|
||||
errors: [
|
||||
{
|
||||
path: field,
|
||||
expected: "string",
|
||||
message: "Invalid type: Expected string but received undefined",
|
||||
received: "undefined",
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function mistypeNumberValidationError(field: string, value: string) {
|
||||
return {
|
||||
error: true as any,
|
||||
data: new CodexError("Cannot validate the input", {
|
||||
errors: [
|
||||
{
|
||||
path: field,
|
||||
expected: "number",
|
||||
message: `Invalid type: Expected number but received "${value}"`,
|
||||
received: `"${value}"`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function minNumberValidationError(field: string, min: number) {
|
||||
return {
|
||||
error: true as any,
|
||||
data: new CodexError("Cannot validate the input", {
|
||||
errors: [
|
||||
{
|
||||
path: field,
|
||||
expected: ">=" + min,
|
||||
message: "Invalid value: Expected >=1 but received 0",
|
||||
received: "0",
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createAvailability() {
|
||||
return {
|
||||
id: randomEthereumAddress(),
|
||||
totalSize: randomInt(0, 9).toString(),
|
||||
duration: randomInt(0, 9).toString(),
|
||||
minPrice: randomInt(0, 9).toString(),
|
||||
maxCollateral: randomInt(0, 9).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("marketplace", () => {
|
||||
const marketplace = new CodexMarketplace("http://localhost:3000");
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns an error when trying to create an availability without total size", async () => {
|
||||
const response = await marketplace.createAvailability({
|
||||
duration: 3000,
|
||||
maxCollateral: 1,
|
||||
minPrice: 100,
|
||||
} as any);
|
||||
|
||||
assert.deepStrictEqual(response, missingNumberValidationError("totalSize"));
|
||||
});
|
||||
|
||||
it("returns an error when trying to create an availability with an invalid number valid", async () => {
|
||||
const response = await marketplace.createAvailability({
|
||||
duration: 3000,
|
||||
maxCollateral: 1,
|
||||
minPrice: 100,
|
||||
totalSize: "abc",
|
||||
} as any);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
response,
|
||||
mistypeNumberValidationError("totalSize", "abc")
|
||||
describe("marketplace", async () => {
|
||||
describe("availability", async () => {
|
||||
const spMarketplace = new CodexMarketplace(
|
||||
process.env["SP_URL"] || "http://localhost:8081"
|
||||
);
|
||||
});
|
||||
const totalSize = 1_000_000;
|
||||
const duration = 3000;
|
||||
const minPricePerBytePerSecond = 1000;
|
||||
const totalCollateral = 1_000_000_000;
|
||||
|
||||
it("returns an error when trying to create an availability with zero total size", async () => {
|
||||
const response = await marketplace.createAvailability({
|
||||
duration: 3000,
|
||||
totalCollateral: 1,
|
||||
minPricePerBytePerSecond: 100,
|
||||
totalSize: 0,
|
||||
const body = {
|
||||
duration,
|
||||
totalCollateral,
|
||||
minPricePerBytePerSecond,
|
||||
totalSize,
|
||||
};
|
||||
|
||||
const result = await spMarketplace.createAvailability(body);
|
||||
assert.ok(result.error == false);
|
||||
|
||||
const availability = result.data;
|
||||
|
||||
describe("create", async () => {
|
||||
it("verifies that the availability was created successfully", async () => {
|
||||
assert.ok(availability.id);
|
||||
assert.equal(availability.duration, duration);
|
||||
assert.equal(availability.freeSize, totalSize);
|
||||
assert.equal(
|
||||
availability.minPricePerBytePerSecond,
|
||||
BigInt(minPricePerBytePerSecond)
|
||||
);
|
||||
assert.equal(availability.totalCollateral, BigInt(totalCollateral));
|
||||
assert.equal(
|
||||
availability.totalRemainingCollateral,
|
||||
BigInt(totalCollateral)
|
||||
);
|
||||
assert.strictEqual(availability.totalSize, totalSize);
|
||||
assert.strictEqual(availability.until, 0);
|
||||
assert.ok(availability.enabled);
|
||||
});
|
||||
|
||||
const errors: Partial<CodexCreateAvailabilityInput>[] = [
|
||||
{ duration: 0 },
|
||||
{ totalSize: 0 },
|
||||
{ totalCollateral: -1 },
|
||||
{ minPricePerBytePerSecond: -1 },
|
||||
];
|
||||
|
||||
for (const err of errors) {
|
||||
const field = Object.keys(err)[0] as keyof typeof err;
|
||||
assert.ok(field);
|
||||
|
||||
it(`fails to create availability with wrong ${field} = ${err[field]}`, async () => {
|
||||
const response = await spMarketplace.createAvailability({
|
||||
...body,
|
||||
[field]: err[field],
|
||||
});
|
||||
|
||||
assert.ok(response.error);
|
||||
assert.ok(response.data.errors?.length);
|
||||
assert.equal(response.data.errors[0]?.path, field);
|
||||
assert.equal(
|
||||
response.data.errors[0]?.received,
|
||||
err[field]?.toString()
|
||||
);
|
||||
assert.ok(response.data.errors[0]?.message.startsWith("Invalid"));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(response, minNumberValidationError("totalSize", 1));
|
||||
describe("update", async () => {
|
||||
async function getUpdatedAvailability() {
|
||||
const availabilities = await spMarketplace.availabilities();
|
||||
assert.ok(availabilities.error == false);
|
||||
return availabilities.data.find((a) => a.id == availability.id);
|
||||
}
|
||||
|
||||
const updates: Omit<CodexAvailabilityPatchInput, "id">[] = [
|
||||
{ enabled: false },
|
||||
{ duration: 3000 },
|
||||
{ minPricePerBytePerSecond: BigInt(1) },
|
||||
{ totalSize: 3000 },
|
||||
{ totalCollateral: BigInt(3000) },
|
||||
{ until: 5000 },
|
||||
];
|
||||
|
||||
for (const usecase of updates) {
|
||||
const field = Object.keys(usecase)[0] as keyof typeof usecase;
|
||||
assert.ok(field);
|
||||
|
||||
it(`updates availability's ${field}`, async () => {
|
||||
const response = await spMarketplace.updateAvailability({
|
||||
id: availability.id,
|
||||
...usecase,
|
||||
});
|
||||
assert.ok(response.error == false);
|
||||
|
||||
const updated = await getUpdatedAvailability();
|
||||
assert.ok(updated?.[field] == usecase[field]);
|
||||
});
|
||||
}
|
||||
|
||||
const errors: Omit<CodexAvailabilityPatchInput, "id">[] = [
|
||||
{ duration: 0 },
|
||||
{ totalSize: 0 },
|
||||
{ totalCollateral: -1 },
|
||||
{ minPricePerBytePerSecond: -1 },
|
||||
{ until: -1 },
|
||||
];
|
||||
|
||||
for (const err of errors) {
|
||||
const field = Object.keys(err)[0] as keyof typeof err;
|
||||
assert.ok(field);
|
||||
|
||||
it(`fails to update availability with wrong ${field}`, async () => {
|
||||
const response = await spMarketplace.updateAvailability({
|
||||
id: availability.id,
|
||||
...err,
|
||||
});
|
||||
|
||||
assert.ok(response.error);
|
||||
assert.ok(response.data.errors?.length);
|
||||
assert.equal(response.data.errors[0]?.path, field);
|
||||
assert.equal(
|
||||
response.data.errors[0]?.received,
|
||||
err[field]?.toString()
|
||||
);
|
||||
assert.ok(response.data.errors[0]?.message.startsWith("Invalid"));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("returns an error when trying to create an availability without duration", async () => {
|
||||
const response = await marketplace.createAvailability({
|
||||
totalSize: 3000,
|
||||
maxCollateral: 1,
|
||||
minPrice: 100,
|
||||
} as any);
|
||||
const data = new CodexData(
|
||||
process.env["CLIENT_URL"] || "http://localhost:8080"
|
||||
);
|
||||
const marketplace = new CodexMarketplace(
|
||||
process.env["CLIENT_URL"] || "http://localhost:8080"
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(response, missingNumberValidationError("duration"));
|
||||
});
|
||||
async function uploadContent(sizeInBytes: number) {
|
||||
const content = "a".repeat(sizeInBytes);
|
||||
const strategy = new NodeUploadStrategy(content);
|
||||
const res = data.upload(strategy);
|
||||
const cid = await res.result;
|
||||
assert.ok(cid.error == false);
|
||||
assert.ok(cid.data);
|
||||
return cid.data;
|
||||
}
|
||||
|
||||
it("returns an error when trying to create an availability with zero duration", async () => {
|
||||
const response = await marketplace.createAvailability({
|
||||
duration: 0,
|
||||
totalCollateral: 1,
|
||||
minPricePerBytePerSecond: 100,
|
||||
totalSize: 3000,
|
||||
async function createStorageRequestBody(targetSizeInBytes = 131072) {
|
||||
return {
|
||||
cid: await uploadContent(targetSizeInBytes),
|
||||
duration: 1000,
|
||||
pricePerBytePerSecond: 1,
|
||||
proofProbability: 1,
|
||||
expiry: 900,
|
||||
collateralPerByte: 1,
|
||||
nodes: 3,
|
||||
tolerance: 1,
|
||||
};
|
||||
}
|
||||
|
||||
describe("storage request", async () => {
|
||||
const body = await createStorageRequestBody();
|
||||
|
||||
it("creates successfully", async () => {
|
||||
const request = await marketplace.createStorageRequest(body);
|
||||
assert.ok(request.error == false);
|
||||
assert.ok(request.data);
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(response, minNumberValidationError("duration", 1));
|
||||
const errors: {
|
||||
request: Partial<CodexCreateStorageRequestInput>;
|
||||
message: string;
|
||||
}[] = [
|
||||
{ request: { cid: "" }, message: "Incorrect Cid" },
|
||||
{
|
||||
request: { duration: 0 },
|
||||
message: "Cannot validate the input",
|
||||
},
|
||||
{
|
||||
request: { pricePerBytePerSecond: 0 },
|
||||
message: "Cannot validate the input",
|
||||
},
|
||||
{
|
||||
request: { proofProbability: 0 },
|
||||
message: "Cannot validate the input",
|
||||
},
|
||||
{
|
||||
request: { expiry: 0 },
|
||||
message: "Cannot validate the input",
|
||||
},
|
||||
{
|
||||
request: { collateralPerByte: 0 },
|
||||
message: "Cannot validate the input",
|
||||
},
|
||||
{
|
||||
request: { tolerance: 0 },
|
||||
message: "Cannot validate the input",
|
||||
},
|
||||
{
|
||||
request: { cid: await uploadContent(1) },
|
||||
message:
|
||||
"Dataset too small for erasure parameters, need at least 131072 bytes",
|
||||
},
|
||||
{
|
||||
request: { duration: 3000, expiry: 4000 },
|
||||
message:
|
||||
"Expiry must be greater than zero and less than the request's duration",
|
||||
},
|
||||
{
|
||||
request: { nodes: 2, tolerance: 1 },
|
||||
message:
|
||||
"Invalid parameters: parameters must satify `1 < (nodes - tolerance) ≥ tolerance`",
|
||||
},
|
||||
];
|
||||
|
||||
for (const err of errors) {
|
||||
it(`fails to create storage request with wrong ${JSON.stringify(err.request)}`, async () => {
|
||||
const request = await marketplace.createStorageRequest({
|
||||
...body,
|
||||
...err.request,
|
||||
});
|
||||
|
||||
assert.ok(request.error);
|
||||
assert.ok(request.data.message.includes(err.message));
|
||||
|
||||
if (request.data.errors?.length) {
|
||||
const keys = Object.keys(err.request);
|
||||
for (const e of request.data.errors) {
|
||||
assert.ok(e.path);
|
||||
assert.ok(keys.includes(e.path));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("returns an error when trying to create an availability without min price", async () => {
|
||||
const response = await marketplace.createAvailability({
|
||||
totalSize: 3000,
|
||||
maxCollateral: 1,
|
||||
duration: 100,
|
||||
} as any);
|
||||
describe("purchases", async () => {
|
||||
const body = await createStorageRequestBody();
|
||||
|
||||
assert.deepStrictEqual(response, missingNumberValidationError("minPrice"));
|
||||
});
|
||||
const request = await marketplace.createStorageRequest(body);
|
||||
assert.ok(request.error == false);
|
||||
assert.ok(request.data);
|
||||
|
||||
it("returns an error when trying to create an availability without max collateral", async () => {
|
||||
const response = await marketplace.createAvailability({
|
||||
totalSize: 3000,
|
||||
minPrice: 100,
|
||||
duration: 100,
|
||||
} as any);
|
||||
it("lists successfully", async () => {
|
||||
const ids = await marketplace.purchaseIds();
|
||||
|
||||
assert.deepStrictEqual(
|
||||
response,
|
||||
missingNumberValidationError("maxCollateral")
|
||||
);
|
||||
});
|
||||
assert.ok(ids.error == false);
|
||||
assert.ok(ids.data.length);
|
||||
assert.ok(ids.data[0]);
|
||||
|
||||
it("returns an error when trying to create an availability with an extra field", async () => {
|
||||
const response = await marketplace.createAvailability({
|
||||
maxCollateral: 1,
|
||||
totalSize: 3000,
|
||||
minPrice: 100,
|
||||
duration: 100,
|
||||
hello: "world",
|
||||
} as any);
|
||||
const purchase = await marketplace.purchaseDetail(ids.data[0]);
|
||||
assert.ok(purchase.error == false);
|
||||
assert.ok(purchase.data.requestId);
|
||||
assert.ok(purchase.data.state);
|
||||
|
||||
assert.deepStrictEqual(response, extraValidationError("hello", "world"));
|
||||
});
|
||||
|
||||
it("returns a response when the request succeed", async () => {
|
||||
const data = { ...createAvailability(), freeSize: "1000" };
|
||||
|
||||
const spy = vi.spyOn(Fetch, "safeJson");
|
||||
spy.mockImplementationOnce(() => Promise.resolve({ error: false, data }));
|
||||
|
||||
const response = await marketplace.createAvailability({
|
||||
totalCollateral: 1,
|
||||
totalSize: 3000,
|
||||
minPricePerBytePerSecond: 100,
|
||||
duration: 100,
|
||||
const purchases = await marketplace.purchases();
|
||||
assert.ok(purchases.error == false);
|
||||
assert.ok(purchases.data.length);
|
||||
assert.ok(purchases.data[0]?.requestId);
|
||||
assert.ok(purchases.data[0]?.state);
|
||||
});
|
||||
|
||||
assert.ok(!response.error);
|
||||
// @ts-ignore
|
||||
assert.deepEqual(response.data, data);
|
||||
});
|
||||
|
||||
it("returns a response when the create availability succeed", async () => {
|
||||
const data = { ...createAvailability(), freeSize: "1000" };
|
||||
|
||||
const spy = vi.spyOn(Fetch, "safeJson");
|
||||
spy.mockImplementationOnce(() => Promise.resolve({ error: false, data }));
|
||||
|
||||
const response = await marketplace.createAvailability({
|
||||
totalCollateral: 1,
|
||||
totalSize: 3000,
|
||||
minPricePerBytePerSecond: 100,
|
||||
duration: 100,
|
||||
});
|
||||
|
||||
assert.ok(!response.error);
|
||||
// @ts-ignore
|
||||
assert.deepEqual(response.data, data);
|
||||
});
|
||||
|
||||
it("returns an error when trying to update an availability without id", async () => {
|
||||
const response = await marketplace.updateAvailability({
|
||||
totalCollateral: 1,
|
||||
totalSize: 3000,
|
||||
minPricePerBytePerSecond: 100,
|
||||
duration: 100,
|
||||
} as any);
|
||||
|
||||
assert.deepStrictEqual(response, missingStringValidationError("id"));
|
||||
});
|
||||
|
||||
it("returns an error when trying to update an availability with zero total size", async () => {
|
||||
const response = await marketplace.updateAvailability({
|
||||
id: randomString(64),
|
||||
totalSize: 0,
|
||||
minPricePerBytePerSecond: 100,
|
||||
duration: 100,
|
||||
totalCollateral: 100,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(response, minNumberValidationError("totalSize", 1));
|
||||
});
|
||||
|
||||
it("returns an error when trying to update an availability with zero duration", async () => {
|
||||
const response = await marketplace.updateAvailability({
|
||||
id: randomString(64),
|
||||
totalSize: 100,
|
||||
duration: 0,
|
||||
minPricePerBytePerSecond: 100,
|
||||
totalCollateral: 100,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(response, minNumberValidationError("duration", 1));
|
||||
});
|
||||
|
||||
it("returns a response when the update availability succeed", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as any;
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(mockResponse);
|
||||
|
||||
const response = await marketplace.updateAvailability({
|
||||
id: randomString(64),
|
||||
totalSize: 3000,
|
||||
duration: 10,
|
||||
minPricePerBytePerSecond: 100,
|
||||
totalCollateral: 100,
|
||||
});
|
||||
|
||||
assert.ok(!response.error);
|
||||
});
|
||||
|
||||
it("returns an error when trying to create a storage request without cid", async () => {
|
||||
const { cid, ...rest } = createStorageRequest();
|
||||
|
||||
const response = await marketplace.createStorageRequest(rest as any);
|
||||
|
||||
assert.deepStrictEqual(response, missingStringValidationError("cid"));
|
||||
});
|
||||
|
||||
it("returns an error when trying to create a storage request without duration", async () => {
|
||||
const { duration, ...rest } = createStorageRequest();
|
||||
|
||||
const response = await marketplace.createStorageRequest(rest as any);
|
||||
|
||||
assert.deepStrictEqual(response, missingNumberValidationError("duration"));
|
||||
});
|
||||
|
||||
it("returns an error when trying to create a storage request with zero duration", async () => {
|
||||
const { duration, ...rest } = createStorageRequest();
|
||||
|
||||
const response = await marketplace.createStorageRequest({
|
||||
...rest,
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(response, minNumberValidationError("duration", 1));
|
||||
});
|
||||
|
||||
it("returns an error when trying to create a storage request without pricePerBytePerSecond", async () => {
|
||||
const { pricePerBytePerSecond, ...rest } = createStorageRequest();
|
||||
|
||||
const response = await marketplace.createStorageRequest(rest as any);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
response,
|
||||
missingNumberValidationError("pricePerBytePerSecond")
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an error when trying to create a storage request without proof probability", async () => {
|
||||
const { proofProbability, ...rest } = createStorageRequest();
|
||||
|
||||
const response = await marketplace.createStorageRequest(rest as any);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
response,
|
||||
missingNumberValidationError("proofProbability")
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an error when trying to create a storage request without expiry", async () => {
|
||||
const { expiry, ...rest } = createStorageRequest();
|
||||
|
||||
const response = await marketplace.createStorageRequest(rest as any);
|
||||
|
||||
assert.deepStrictEqual(response, missingNumberValidationError("expiry"));
|
||||
});
|
||||
|
||||
it("returns an error when trying to create a storage request with zero expiry", async () => {
|
||||
const { expiry, ...rest } = createStorageRequest();
|
||||
|
||||
const response = await marketplace.createStorageRequest({
|
||||
...rest,
|
||||
expiry: 0,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(response, minNumberValidationError("expiry", 1));
|
||||
});
|
||||
|
||||
it("returns an error when trying to create a storage request without collateralPerByte", async () => {
|
||||
const { collateralPerByte, ...rest } = createStorageRequest();
|
||||
|
||||
const response = await marketplace.createStorageRequest(rest as any);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
response,
|
||||
missingNumberValidationError("collateralPerByte")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,26 +1,51 @@
|
||||
import * as v from "valibot";
|
||||
import { Api } from "../api/config";
|
||||
import { CodexError, CodexValibotIssuesMap } from "../errors/errors";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import {
|
||||
Fetch,
|
||||
FetchAuthBuilder,
|
||||
type FetchAuth,
|
||||
} from "../fetch-safe/fetch-safe";
|
||||
import type { SafeValue } from "../values/values";
|
||||
import {
|
||||
type CodexAvailabilityResponse,
|
||||
type CodexAvailability,
|
||||
type CodexSlot,
|
||||
type CodexSlotAgent,
|
||||
type CodexSlotResponse,
|
||||
type CodexSlotAgentResponse,
|
||||
type CodexAvailabilityWithoutTypes,
|
||||
type CodexAvailabilityCreateResponse,
|
||||
type CodexAvailabilityDto,
|
||||
type CodexAvailabilityCreateBody,
|
||||
CodexAvailabilityPatchInput,
|
||||
type CodexReservationsResponse,
|
||||
type CodexPurchaseIdsResponse,
|
||||
type CodexPurchaseResponse,
|
||||
type CodexPurchase,
|
||||
type CodexStorageRequestCreateBody,
|
||||
type CodexReservation,
|
||||
type CodexPurchaseWithoutTypes,
|
||||
type CodexAvailabilityPatchBody,
|
||||
} from "./types";
|
||||
import {
|
||||
CodexCreateAvailabilityInput,
|
||||
CodexCreateStorageRequestInput,
|
||||
type CodexPurchase,
|
||||
type CodexReservation,
|
||||
type CodexSlot,
|
||||
type CodexStorageRequest,
|
||||
CodexUpdateAvailabilityInput,
|
||||
} from "./types";
|
||||
|
||||
type CodexMarketplaceOptions = {
|
||||
auth?: FetchAuth;
|
||||
};
|
||||
|
||||
export class CodexMarketplace {
|
||||
readonly url: string;
|
||||
readonly auth: FetchAuth = {};
|
||||
|
||||
constructor(url: string) {
|
||||
constructor(url: string, options?: CodexMarketplaceOptions) {
|
||||
this.url = url;
|
||||
|
||||
if (options?.auth) {
|
||||
this.auth = options.auth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -29,30 +54,51 @@ export class CodexMarketplace {
|
||||
async activeSlots(): Promise<SafeValue<CodexSlot[]>> {
|
||||
const url = this.url + Api.config.prefix + "/sales/slots";
|
||||
|
||||
return Fetch.safeJson<CodexSlot[]>(url, {
|
||||
return Fetch.safeJson<CodexSlotResponse[]>(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns active slot with id {slotId} for the host
|
||||
*/
|
||||
async activeSlot(slotId: string): Promise<SafeValue<CodexSlot>> {
|
||||
async activeSlot(slotId: string): Promise<SafeValue<CodexSlotAgent>> {
|
||||
const url = this.url + Api.config.prefix + "/sales/slots/" + slotId;
|
||||
|
||||
return Fetch.safeJson<CodexSlot>(url, {
|
||||
return Fetch.safeJson<CodexSlotAgentResponse>(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
private transformAvailability({
|
||||
freeSize,
|
||||
...a
|
||||
}: CodexAvailabilityWithoutTypes) {
|
||||
const availability: CodexAvailability = {
|
||||
...a,
|
||||
minPricePerBytePerSecond: BigInt(a.minPricePerBytePerSecond),
|
||||
totalCollateral: BigInt(a.totalCollateral),
|
||||
totalRemainingCollateral: BigInt(a.totalRemainingCollateral),
|
||||
};
|
||||
|
||||
if (freeSize) {
|
||||
availability.freeSize = freeSize;
|
||||
}
|
||||
|
||||
return availability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns storage that is for sale
|
||||
*/
|
||||
async availabilities(): Promise<SafeValue<CodexAvailability[]>> {
|
||||
const url = this.url + Api.config.prefix + "/sales/availability";
|
||||
|
||||
const res = await Fetch.safeJson<CodexAvailabilityDto[]>(url, {
|
||||
const res = await Fetch.safeJson<CodexAvailabilityResponse>(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
@ -61,15 +107,7 @@ export class CodexMarketplace {
|
||||
|
||||
return {
|
||||
error: false,
|
||||
data: res.data.map((a) => ({
|
||||
id: a.id,
|
||||
totalSize: parseInt(a.totalSize, 10),
|
||||
freeSize: parseInt(a.freeSize, 10),
|
||||
duration: parseInt(a.duration, 10),
|
||||
minPricePerBytePerSecond: parseInt(a.minPricePerBytePerSecond, 10),
|
||||
totalCollateral: parseInt(a.totalCollateral, 10),
|
||||
totalRemainingCollateral: parseInt(a.totalRemainingCollateral, 10),
|
||||
})),
|
||||
data: res.data.map(this.transformAvailability),
|
||||
};
|
||||
}
|
||||
|
||||
@ -78,7 +116,7 @@ export class CodexMarketplace {
|
||||
*/
|
||||
async createAvailability(
|
||||
input: CodexCreateAvailabilityInput
|
||||
): Promise<SafeValue<CodexAvailabilityCreateResponse>> {
|
||||
): Promise<SafeValue<CodexAvailability>> {
|
||||
const result = v.safeParse(CodexCreateAvailabilityInput, input);
|
||||
|
||||
if (!result.success) {
|
||||
@ -92,16 +130,32 @@ export class CodexMarketplace {
|
||||
|
||||
const url = this.url + Api.config.prefix + "/sales/availability";
|
||||
|
||||
const body = result.output;
|
||||
const body: CodexAvailabilityCreateBody = {
|
||||
totalSize: result.output.totalSize,
|
||||
duration: result.output.duration,
|
||||
minPricePerBytePerSecond:
|
||||
result.output.minPricePerBytePerSecond.toString(),
|
||||
totalCollateral: result.output.totalCollateral.toString(),
|
||||
};
|
||||
|
||||
if (result.output.enabled) {
|
||||
body.enabled = result.output.enabled;
|
||||
}
|
||||
|
||||
if (result.output.until) {
|
||||
body.until = result.output.until;
|
||||
}
|
||||
|
||||
return Fetch.safeJson<CodexAvailabilityCreateResponse>(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
totalSize: body.totalSize.toString(),
|
||||
duration: body.duration.toString(),
|
||||
minPricePerBytePerSecond: body.minPricePerBytePerSecond.toString(),
|
||||
totalCollateral: body.totalCollateral.toString(),
|
||||
}),
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
body: JSON.stringify(body),
|
||||
}).then((result) => {
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return { error: false, data: this.transformAvailability(result.data) };
|
||||
});
|
||||
}
|
||||
|
||||
@ -110,9 +164,9 @@ export class CodexMarketplace {
|
||||
* Existing Requests linked to this Availability will continue as is.
|
||||
*/
|
||||
async updateAvailability(
|
||||
input: CodexUpdateAvailabilityInput
|
||||
input: CodexAvailabilityPatchInput
|
||||
): Promise<SafeValue<"">> {
|
||||
const result = v.safeParse(CodexUpdateAvailabilityInput, input);
|
||||
const result = v.safeParse(CodexAvailabilityPatchInput, input);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
@ -126,16 +180,38 @@ export class CodexMarketplace {
|
||||
const url =
|
||||
this.url + Api.config.prefix + "/sales/availability/" + result.output.id;
|
||||
|
||||
const body = result.output;
|
||||
const { totalSize, duration, minPricePerBytePerSecond, totalCollateral } =
|
||||
result.output;
|
||||
let body: CodexAvailabilityPatchBody = {};
|
||||
|
||||
if (totalSize) {
|
||||
body.totalSize = totalSize;
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
body.duration = duration;
|
||||
}
|
||||
|
||||
if (minPricePerBytePerSecond) {
|
||||
body.minPricePerBytePerSecond = minPricePerBytePerSecond.toString();
|
||||
}
|
||||
|
||||
if (totalCollateral) {
|
||||
body.totalCollateral = totalCollateral.toString();
|
||||
}
|
||||
|
||||
if (result.output.enabled != undefined) {
|
||||
body.enabled = result.output.enabled;
|
||||
}
|
||||
|
||||
if (result.output.until) {
|
||||
body.until = result.output.until;
|
||||
}
|
||||
|
||||
const res = await Fetch.safe(url, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
totalSize: body.totalSize.toString(),
|
||||
duration: body.duration.toString(),
|
||||
minPricePerBytePerSecond: body.minPricePerBytePerSecond.toString(),
|
||||
totalCollateral: body.totalCollateral.toString(),
|
||||
}),
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
@ -156,28 +232,56 @@ export class CodexMarketplace {
|
||||
Api.config.prefix +
|
||||
`/sales/availability/${availabilityId}/reservations`;
|
||||
|
||||
return Fetch.safeJson<CodexReservation[]>(url, {
|
||||
return Fetch.safeJson<CodexReservationsResponse>(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of purchase IDs
|
||||
*/
|
||||
async purchaseIds(): Promise<SafeValue<string[]>> {
|
||||
async purchaseIds(): Promise<SafeValue<CodexPurchaseIdsResponse>> {
|
||||
const url = this.url + Api.config.prefix + `/storage/purchases`;
|
||||
|
||||
return Fetch.safeJson<string[]>(url, {
|
||||
return Fetch.safeJson<CodexPurchaseIdsResponse>(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
async purchases(): Promise<SafeValue<CodexPurchase[]>> {
|
||||
const url = this.url + Api.config.prefix + `/storage/purchases`;
|
||||
private transformPurchase(p: CodexPurchaseWithoutTypes): CodexPurchase {
|
||||
const purchase: CodexPurchase = {
|
||||
requestId: p.requestId,
|
||||
state: p.state,
|
||||
};
|
||||
|
||||
const res = await Fetch.safeJson<string[]>(url, {
|
||||
method: "GET",
|
||||
});
|
||||
if (p.error) {
|
||||
purchase.error = p.error;
|
||||
}
|
||||
|
||||
if (!p.request) {
|
||||
return purchase;
|
||||
}
|
||||
|
||||
return {
|
||||
...purchase,
|
||||
request: {
|
||||
...p.request,
|
||||
ask: {
|
||||
...p.request.ask,
|
||||
proofProbability: parseInt(p.request.ask.proofProbability, 10),
|
||||
pricePerBytePerSecond: parseInt(
|
||||
p.request.ask.pricePerBytePerSecond,
|
||||
10
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async purchases(): Promise<SafeValue<CodexPurchase[]>> {
|
||||
const res = await this.purchaseIds();
|
||||
|
||||
if (res.error) {
|
||||
return res;
|
||||
@ -196,10 +300,9 @@ export class CodexMarketplace {
|
||||
data: purchases.map((p) =>
|
||||
p.error
|
||||
? ({
|
||||
state: "error",
|
||||
state: "errored",
|
||||
error: p.data.message,
|
||||
requestId: "",
|
||||
request: {} as CodexStorageRequest,
|
||||
} satisfies CodexPurchase)
|
||||
: p.data
|
||||
),
|
||||
@ -213,8 +316,15 @@ export class CodexMarketplace {
|
||||
const url =
|
||||
this.url + Api.config.prefix + `/storage/purchases/` + purchaseId;
|
||||
|
||||
return Fetch.safeJson<CodexPurchase>(url, {
|
||||
return Fetch.safeJson<CodexPurchaseResponse>(url, {
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
method: "GET",
|
||||
}).then((res) => {
|
||||
if (res.error) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return { error: false, data: this.transformPurchase(res.data) };
|
||||
});
|
||||
}
|
||||
|
||||
@ -247,23 +357,18 @@ export class CodexMarketplace {
|
||||
} = result.output;
|
||||
const url = this.url + Api.config.prefix + "/storage/request/" + cid;
|
||||
|
||||
const res = await Fetch.safe(url, {
|
||||
return Fetch.safeText(url, {
|
||||
method: "POST",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
body: JSON.stringify({
|
||||
duration: duration.toString(),
|
||||
duration,
|
||||
pricePerBytePerSecond: pricePerBytePerSecond.toString(),
|
||||
proofProbability: proofProbability.toString(),
|
||||
nodes,
|
||||
collateral: collateralPerByte.toString(),
|
||||
expiry: expiry.toString(),
|
||||
collateralPerByte: collateralPerByte.toString(),
|
||||
expiry,
|
||||
tolerance,
|
||||
}),
|
||||
} satisfies CodexStorageRequestCreateBody),
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return { error: false, data: await res.data.text() };
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,241 +1,169 @@
|
||||
import type { components, paths } from "../openapi";
|
||||
import * as v from "valibot";
|
||||
|
||||
export type CodexStorageRequest = {
|
||||
id: string;
|
||||
export type CodexSlotResponse =
|
||||
paths["/sales/slots"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
/**
|
||||
* Address of Ethereum address
|
||||
*/
|
||||
client: string;
|
||||
export type CodexSlot = CodexSlotResponse;
|
||||
|
||||
ask: {
|
||||
/**
|
||||
* Number of slots that the tequest want to have the content spread over.
|
||||
*/
|
||||
slots: number;
|
||||
export type CodexSlotAgentResponse =
|
||||
paths["/sales/slots/{slotId}"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
/**
|
||||
* Amount of storage per slot (in bytes) as decimal string.
|
||||
*/
|
||||
slotSize: string;
|
||||
export type CodexSlotAgent = CodexSlotAgentResponse;
|
||||
|
||||
/**
|
||||
* The duration of the storage request in seconds.
|
||||
*/
|
||||
duration: string;
|
||||
export type CodexAvailabilityResponse =
|
||||
paths["/sales/availability"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
/**
|
||||
* How often storage proofs are required as decimal string (in periods).
|
||||
*/
|
||||
proofProbability: string;
|
||||
export type CodexAvailabilityWithoutTypes =
|
||||
components["schemas"]["SalesAvailabilityREAD"];
|
||||
|
||||
/**
|
||||
* The amount of tokens paid per byte per second per slot to hosts the client is willing to pay
|
||||
*/
|
||||
pricePerBytePerSecond: string;
|
||||
|
||||
/**
|
||||
* Max slots that can be lost without data considered to be lost.
|
||||
*/
|
||||
maxSlotLoss: number;
|
||||
};
|
||||
|
||||
content: {
|
||||
/**
|
||||
* Unique Content ID
|
||||
*/
|
||||
cid: string;
|
||||
|
||||
/**
|
||||
* Erasure code parameters
|
||||
*/
|
||||
// erasure: {
|
||||
/**
|
||||
* Total number of chunks generated by the erasure code process.
|
||||
*/
|
||||
// totalChunks: number;
|
||||
// };
|
||||
|
||||
/**
|
||||
* Parameters for Proof of Retrievability
|
||||
*/
|
||||
// por: {
|
||||
// u: string;
|
||||
// publicKey: string;
|
||||
// name: string;
|
||||
// };
|
||||
};
|
||||
|
||||
/* Number as decimal string that represents expiry threshold in seconds from when the Request is submitted.
|
||||
* When the threshold is reached and the Request does not find requested amount of nodes to host the data, the Request is voided.
|
||||
* The number of seconds can not be higher then the Request's duration itself.
|
||||
*/
|
||||
expiry: string;
|
||||
|
||||
/**
|
||||
* Random data
|
||||
*/
|
||||
nonce: string;
|
||||
export type CodexAvailability = Omit<
|
||||
CodexAvailabilityWithoutTypes,
|
||||
| "freeSize"
|
||||
| "minPricePerBytePerSecond"
|
||||
| "totalCollateral"
|
||||
| "totalRemainingCollateral"
|
||||
> & {
|
||||
freeSize?: number;
|
||||
minPricePerBytePerSecond: BigInt;
|
||||
totalCollateral: BigInt;
|
||||
totalRemainingCollateral: BigInt;
|
||||
};
|
||||
|
||||
/**
|
||||
* A storage slot is a portion of a storage contract that needs to be fulfilled
|
||||
* by a host. To initiate a contract, all the slots must be filled.
|
||||
*/
|
||||
export type CodexSlot = {
|
||||
id: string;
|
||||
export type CodexAvailabilityCreateResponse =
|
||||
paths["/sales/availability"]["post"]["responses"][201]["content"]["application/json"];
|
||||
|
||||
request: CodexStorageRequest;
|
||||
|
||||
/**
|
||||
* The slot index as hexadecimal string
|
||||
*/
|
||||
slotIndex: "string";
|
||||
};
|
||||
|
||||
/**
|
||||
* Storage availability for sell.
|
||||
*/
|
||||
export type CodexAvailability = {
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Size of available storage in bytes
|
||||
*/
|
||||
totalSize: number;
|
||||
|
||||
/**
|
||||
* Unused size of availability's storage in bytes as decimal string
|
||||
*/
|
||||
freeSize: number;
|
||||
|
||||
/**
|
||||
* Maximum time the storage should be sold for (in seconds)
|
||||
*/
|
||||
duration: number;
|
||||
|
||||
/**
|
||||
* Minimal price per byte per second paid (in amount of tokens) for the
|
||||
* hosted request's slot for the request's duration as decimal string
|
||||
*/
|
||||
minPricePerBytePerSecond: number;
|
||||
|
||||
/**
|
||||
* Total collateral (in amount of tokens) that can be used for matching requests
|
||||
*/
|
||||
totalCollateral: number;
|
||||
|
||||
totalRemainingCollateral: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Storage availability received from the api.
|
||||
*/
|
||||
export type CodexAvailabilityDto = {
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Size of available storage in bytes
|
||||
*/
|
||||
totalSize: string;
|
||||
|
||||
/**
|
||||
* Unused size of availability's storage in bytes as decimal string
|
||||
*/
|
||||
freeSize: string;
|
||||
|
||||
/**
|
||||
* Maximum time the storage should be sold for (in seconds)
|
||||
*/
|
||||
duration: string;
|
||||
|
||||
/**
|
||||
* Minimal price per byte per second paid (in amount of tokens) for the
|
||||
* hosted request's slot for the request's duration as decimal string
|
||||
*/
|
||||
minPricePerBytePerSecond: string;
|
||||
|
||||
/**
|
||||
* Total collateral (in amount of tokens) that can be used for matching requests
|
||||
*/
|
||||
totalCollateral: string;
|
||||
|
||||
totalRemainingCollateral: string;
|
||||
};
|
||||
|
||||
export type CodexAvailabilityCreateResponse = CodexAvailability & {
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Unused size of availability's storage in bytes as decimal string
|
||||
*/
|
||||
freeSize: string;
|
||||
};
|
||||
export type CodexAvailabilityCreateBody = Exclude<
|
||||
paths["/sales/availability"]["post"]["requestBody"],
|
||||
undefined
|
||||
>["content"]["application/json"];
|
||||
|
||||
export const CodexCreateAvailabilityInput = v.strictObject({
|
||||
totalSize: v.pipe(v.number(), v.minValue(1)),
|
||||
duration: v.pipe(v.number(), v.minValue(1)),
|
||||
minPricePerBytePerSecond: v.number(),
|
||||
totalCollateral: v.number(),
|
||||
minPricePerBytePerSecond: v.union([
|
||||
v.pipe(v.bigint(), v.minValue(BigInt(0))),
|
||||
v.pipe(
|
||||
v.number(),
|
||||
v.minValue(0),
|
||||
v.transform((input) => BigInt(input))
|
||||
),
|
||||
]),
|
||||
totalCollateral: v.union([
|
||||
v.pipe(v.bigint(), v.minValue(BigInt(0))),
|
||||
v.pipe(
|
||||
v.number(),
|
||||
v.minValue(0),
|
||||
v.transform((input) => BigInt(input))
|
||||
),
|
||||
]),
|
||||
enabled: v.optional(v.boolean()),
|
||||
until: v.optional(v.pipe(v.number(), v.minValue(0))),
|
||||
});
|
||||
|
||||
export type CodexCreateAvailabilityInput = v.InferOutput<
|
||||
typeof CodexCreateAvailabilityInput
|
||||
export type CodexAvailabilityPatchResponse =
|
||||
paths["/sales/availability/{id}"]["patch"]["responses"][204]["content"];
|
||||
|
||||
export type CodexAvailabilityPatchBody = Partial<
|
||||
Exclude<
|
||||
paths["/sales/availability"]["post"]["requestBody"],
|
||||
undefined
|
||||
>["content"]["application/json"]
|
||||
>;
|
||||
|
||||
export const CodexUpdateAvailabilityInput = v.strictObject({
|
||||
export type CodexCreateAvailabilityInput = Omit<
|
||||
v.InferOutput<typeof CodexCreateAvailabilityInput>,
|
||||
"minPricePerBytePerSecond" | "totalCollateral"
|
||||
> & {
|
||||
minPricePerBytePerSecond?: number | BigInt;
|
||||
totalCollateral?: number | BigInt;
|
||||
};
|
||||
|
||||
export const CodexAvailabilityPatchInput = v.strictObject({
|
||||
id: v.string(),
|
||||
totalSize: v.pipe(v.number(), v.minValue(1)),
|
||||
duration: v.pipe(v.number(), v.minValue(1)),
|
||||
minPricePerBytePerSecond: v.number(),
|
||||
totalCollateral: v.number(),
|
||||
totalSize: v.optional(v.pipe(v.number(), v.minValue(1))),
|
||||
duration: v.optional(v.pipe(v.number(), v.minValue(1))),
|
||||
minPricePerBytePerSecond: v.optional(
|
||||
v.union([
|
||||
v.pipe(v.bigint(), v.minValue(BigInt(0))),
|
||||
v.pipe(
|
||||
v.number(),
|
||||
v.minValue(0),
|
||||
v.transform((input) => BigInt(input))
|
||||
),
|
||||
])
|
||||
),
|
||||
totalCollateral: v.optional(
|
||||
v.union([
|
||||
v.pipe(v.bigint(), v.minValue(BigInt(0))),
|
||||
v.pipe(
|
||||
v.number(),
|
||||
v.minValue(0),
|
||||
v.transform((input) => BigInt(input))
|
||||
),
|
||||
])
|
||||
),
|
||||
enabled: v.optional(v.boolean()),
|
||||
until: v.optional(v.pipe(v.number(), v.minValue(0))),
|
||||
});
|
||||
|
||||
export type CodexUpdateAvailabilityInput = v.InferOutput<
|
||||
typeof CodexUpdateAvailabilityInput
|
||||
>;
|
||||
|
||||
export type CodexReservation = {
|
||||
id: string;
|
||||
availabilityId: string;
|
||||
requestId: string;
|
||||
|
||||
/**
|
||||
* Size in bytes
|
||||
*/
|
||||
size: string;
|
||||
|
||||
/**
|
||||
* Slot Index as hexadecimal string
|
||||
*/
|
||||
slotIndex: string;
|
||||
export type CodexAvailabilityPatchInput = Omit<
|
||||
v.InferOutput<typeof CodexAvailabilityPatchInput>,
|
||||
"minPricePerBytePerSecond" | "totalCollateral"
|
||||
> & {
|
||||
minPricePerBytePerSecond?: number | BigInt;
|
||||
totalCollateral?: number | BigInt;
|
||||
};
|
||||
|
||||
export type CodexPurchase = {
|
||||
/**
|
||||
* Description of the request's state
|
||||
*/
|
||||
state: string;
|
||||
export type CodexReservationsResponse =
|
||||
paths["/sales/availability/{id}/reservations"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
/**
|
||||
* If request failed, then here is presented the error message
|
||||
*/
|
||||
error: string;
|
||||
export type CodexReservation = components["schemas"]["Reservation"];
|
||||
|
||||
request: CodexStorageRequest;
|
||||
export type CodexPurchaseIdsResponse =
|
||||
paths["/storage/purchases"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
requestId: string;
|
||||
export type CodexPurchaseResponse =
|
||||
paths["/storage/purchases/{id}"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
export type CodexStorageAsk = Omit<
|
||||
components["schemas"]["StorageAsk"],
|
||||
"slotSize" | "duration" | "proofProbability" | "pricePerBytePerSecond"
|
||||
> & {
|
||||
slotSize: number;
|
||||
duration: number;
|
||||
proofProbability: number;
|
||||
pricePerBytePerSecond: number;
|
||||
};
|
||||
|
||||
export type CodexPurchaseWithoutTypes = components["schemas"]["Purchase"];
|
||||
|
||||
export type CodexPurchase = Omit<
|
||||
components["schemas"]["Purchase"],
|
||||
"request"
|
||||
> & {
|
||||
request?: Omit<components["schemas"]["StorageRequest"], "ask"> & {
|
||||
ask: CodexStorageAsk;
|
||||
};
|
||||
};
|
||||
|
||||
export type CodexStorageRequestResponse =
|
||||
paths["/storage/request/{cid}"]["post"]["responses"][200]["content"]["text/plain"];
|
||||
|
||||
export type CodexStorageRequestCreateBody = Exclude<
|
||||
paths["/storage/request/{cid}"]["post"]["requestBody"],
|
||||
undefined
|
||||
>["content"]["application/json"];
|
||||
|
||||
export const CodexCreateStorageRequestInput = v.strictObject({
|
||||
cid: v.string(),
|
||||
duration: v.pipe(v.number(), v.minValue(1)),
|
||||
pricePerBytePerSecond: v.number(),
|
||||
proofProbability: v.number(),
|
||||
pricePerBytePerSecond: v.pipe(v.number(), v.minValue(1)),
|
||||
proofProbability: v.pipe(v.number(), v.minValue(1)),
|
||||
nodes: v.optional(v.number(), 1),
|
||||
tolerance: v.optional(v.number(), 0),
|
||||
tolerance: v.optional(v.pipe(v.number(), v.minValue(1)), 1),
|
||||
expiry: v.pipe(v.number(), v.minValue(1)),
|
||||
collateralPerByte: v.number(),
|
||||
collateralPerByte: v.pipe(v.number(), v.minValue(1)),
|
||||
});
|
||||
|
||||
export type CodexCreateStorageRequestInput = v.InferOutput<
|
||||
|
||||
1
src/node.ts
Normal file
1
src/node.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./data/node-upload";
|
||||
36
src/node/node.spec.ts
Normal file
36
src/node/node.spec.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { assert, describe, expect, it, vi } from "vitest";
|
||||
import { CodexNode } from "./node";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
|
||||
describe("node", () => {
|
||||
const clientUrl = process.env["CLIENT_URL"] || "http://localhost:8080";
|
||||
const node = new CodexNode(clientUrl);
|
||||
|
||||
it("gets the json spr", async () => {
|
||||
const spr = await node.spr("json");
|
||||
assert.ok(spr.error == false);
|
||||
assert.ok(spr.data);
|
||||
});
|
||||
|
||||
it("gets the text spr", async () => {
|
||||
const spr = await node.spr("text");
|
||||
assert.ok(spr.error == false);
|
||||
assert.ok(spr.data);
|
||||
});
|
||||
|
||||
it("connects to a peer", async () => {
|
||||
const spy = vi.spyOn(Fetch, "safeText");
|
||||
spy.mockImplementationOnce(() =>
|
||||
Promise.resolve({ error: false, data: "" })
|
||||
);
|
||||
|
||||
await node.connect("1234", ["5678"]);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
clientUrl + "/api/codex/v1/connect/1234?addrs=5678",
|
||||
{
|
||||
headers: {},
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,20 +1,39 @@
|
||||
import { Api } from "../api/config";
|
||||
import { Fetch } from "../fetch-safe/fetch-safe";
|
||||
import {
|
||||
Fetch,
|
||||
FetchAuthBuilder,
|
||||
type FetchAuth,
|
||||
} from "../fetch-safe/fetch-safe";
|
||||
import type { SafeValue } from "../values/values";
|
||||
import type { CodexSpr } from "./types";
|
||||
import type {
|
||||
CodexPeerId,
|
||||
CodexPeerIdContentType,
|
||||
CodexPeerIdJsonResponse,
|
||||
CodexSpr,
|
||||
CodexSprContentType,
|
||||
CodexSprJsonResponse,
|
||||
} from "./types";
|
||||
|
||||
type CodexNodeOptions = {
|
||||
auth?: FetchAuth;
|
||||
};
|
||||
|
||||
export class CodexNode {
|
||||
readonly url: string;
|
||||
readonly auth: FetchAuth = {};
|
||||
|
||||
constructor(url: string) {
|
||||
constructor(url: string, options?: CodexNodeOptions) {
|
||||
this.url = url;
|
||||
|
||||
if (options?.auth) {
|
||||
this.auth = options.auth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a peer
|
||||
* TODO check result
|
||||
*/
|
||||
connect(peerId: string, addrs: string[] = []) {
|
||||
connect(peerId: string, addrs: string[] = []): Promise<SafeValue<string>> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
for (const addr of addrs) {
|
||||
@ -24,31 +43,63 @@ export class CodexNode {
|
||||
const url =
|
||||
this.url + Api.config.prefix + `/connect/${peerId}?` + params.toString();
|
||||
|
||||
return Fetch.safe(url, {
|
||||
return Fetch.safeText(url, {
|
||||
method: "GET",
|
||||
headers: FetchAuthBuilder.build(this.auth),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Node's SPR
|
||||
*/
|
||||
async spr(): Promise<SafeValue<CodexSpr>> {
|
||||
async spr(
|
||||
type: CodexSprContentType = "json"
|
||||
): Promise<SafeValue<CodexSpr<CodexSprContentType>>> {
|
||||
const url = this.url + Api.config.prefix + "/spr";
|
||||
|
||||
return Fetch.safeJson(url, {
|
||||
if (type == "json") {
|
||||
return Fetch.safeJson<CodexSprJsonResponse>(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...FetchAuthBuilder.build(this.auth),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Fetch.safeText(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...FetchAuthBuilder.build(this.auth),
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Node's PeerID
|
||||
* TODO check result
|
||||
*/
|
||||
peerId() {
|
||||
peerId(
|
||||
type: CodexPeerIdContentType = "json"
|
||||
): Promise<SafeValue<CodexPeerId<CodexPeerIdContentType>>> {
|
||||
const url = this.url + Api.config.prefix + "/node/peerid";
|
||||
|
||||
return Fetch.safe(url, {
|
||||
if (type == "json") {
|
||||
return Fetch.safeJson<CodexPeerIdJsonResponse>(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...FetchAuthBuilder.build(this.auth),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Fetch.safeText(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...FetchAuthBuilder.build(this.auth),
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,29 @@
|
||||
export type CodexSpr = {
|
||||
spr: string;
|
||||
};
|
||||
import type { paths } from "../openapi";
|
||||
|
||||
export type CodexSprTextResponse =
|
||||
paths["/spr"]["get"]["responses"][200]["content"]["text/plain"];
|
||||
|
||||
export type CodexSprJsonResponse =
|
||||
paths["/spr"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
export type CodexSprContentType = "json" | "text";
|
||||
|
||||
export type CodexSpr<T extends CodexSprContentType> = T extends "json"
|
||||
? CodexSprJsonResponse
|
||||
: T extends "text"
|
||||
? CodexSprTextResponse
|
||||
: never;
|
||||
|
||||
export type CodexPeerIdTextResponse =
|
||||
paths["/peerid"]["get"]["responses"][200]["content"]["text/plain"];
|
||||
|
||||
export type CodexPeerIdJsonResponse =
|
||||
paths["/peerid"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
export type CodexPeerIdContentType = "json" | "text";
|
||||
|
||||
export type CodexPeerId<T extends CodexPeerIdContentType> = T extends "json"
|
||||
? CodexPeerIdJsonResponse
|
||||
: T extends "text"
|
||||
? CodexPeerIdTextResponse
|
||||
: never;
|
||||
|
||||
1496
src/openapi.ts
Normal file
1496
src/openapi.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
import { Promises } from "./promise-safe";
|
||||
import { CodexError } from "../async";
|
||||
import { CodexError } from "../errors/errors";
|
||||
|
||||
describe("promise safe", () => {
|
||||
it("returns an error when the promise failed", async () => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { CodexError } from "../async";
|
||||
import { CodexError } from "../errors/errors";
|
||||
import type { SafeValue } from "../values/values";
|
||||
|
||||
export const Promises = {
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"verbatimModuleSyntax": true,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
}
|
||||
}
|
||||
9
vitest.config.js
Normal file
9
vitest.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// run tests sequentially, not in parallel
|
||||
// number of workers set to 1 disables parallelism
|
||||
maxThreads: 1,
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user