Compare commits

..

No commits in common. "master" and "v0.0.22" have entirely different histories.

59 changed files with 1401 additions and 7473 deletions

View File

@ -1,55 +0,0 @@
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

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
node_modules
dist
*.tgz
index.bundle.js

211
README.md
View File

@ -6,23 +6,6 @@ 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.
[![License: Apache](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Stability: experimental](https://img.shields.io/badge/stability-experimental-orange.svg)](#stability)
[![CI](https://github.com/codex-storage/codex-js/actions/workflows/ci.yml/badge.svg?branch=master)](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
@ -42,7 +25,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:8080");
const codex = new Codex("http://localhost:3000");
```
Then you can access any module like this:
@ -66,27 +49,13 @@ 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:8080");
const codex = new Codex("http://localhost:3000");
```
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();
```
### 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.
const marketplace = await codex.marketplace;
```
### Error handling
@ -127,19 +96,15 @@ if (slots.error) {
The following API assume that you have already a marketplace module loaded, example:
```js
const codex = new Codex("http://localhost:8080");
// When using the async api
const codex = new Codex("http://localhost:3000");
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#L7)[]>
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]>
Example:
@ -152,7 +117,7 @@ const slots = await marketplace.activeSlots();
Returns active slot with id {slotId} for the host.
- slotId (string, required)
- returns Promise<[CodexSlotAgent](./src/marketplace/types.ts#L12)[]>
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]>
Example:
@ -165,7 +130,7 @@ const slot = await marketplace.activeSlot(slotId);
Returns storage that is for sale.
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)>
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L99)>
Example:
@ -177,16 +142,16 @@ const availabilities = await marketplace.availabilities();
Offers storage for sale.
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L45), required)
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)[]>
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L175), required)
- returns Promise<[CodexAvailabilityCreateResponse](./src/marketplace/types.ts#L186)[]>
Example:
```js
const response = await marketplace.createAvailability({
totalCollateral: 1,
maxCollateral: 1,
totalSize: 3000,
minPricePerBytePerSecond: 100,
minPrice: 100,
duration: 100,
});
```
@ -195,7 +160,7 @@ const response = await marketplace.createAvailability({
Updates availability.
- input ([CodexAvailabilityPatchInput](./src/marketplace/types.ts#L66), required)
- input ([CodexUpdateAvailabilityInput](./src/marketplace/types.ts#L186), required)
- returns Promise<"">
Example:
@ -203,9 +168,9 @@ Example:
```js
const response = await marketplace.updateAvailability({
id: "0x.....................",
totalCollateral: 1,
maxCollateral: 1,
totalSize: 3000,
minPricePerBytePerSecond: 100,
minPrice: 100,
duration: 100,
});
```
@ -215,7 +180,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#L83)[]>
- returns Promise<[CodexReservation](./src/marketplace/types.ts#L198)[]>
Example:
@ -227,7 +192,7 @@ const reservations = await marketplace.reservations("Ox...");
Creates a new Request for storage
- input ([CodexCreateStorageRequestInput](./src/marketplace/types.ts#L120), required)
- input ([CodexCreateStorageRequestInput](./src/marketplace/types.ts#L230), required)
- returns Promise<string>
Example:
@ -261,7 +226,7 @@ const ids = await marketplace.purchaseIds();
Returns purchase details
- purchaseId (string, required)
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L103)[]>
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L214)[]>
Example:
@ -275,19 +240,15 @@ 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:8080");
// When using the async api
const data = await codex.data();
// When using the sync api
const data = codex.data;
const codex = new Codex("http://localhost:3000");
const data = await codex.data;
```
#### cids
Returns the manifest stored locally in node.
- returns Promise<[CodexDataItem](./src/data/types.ts#L8)[]>
- returns Promise<[CodexDataResponse](./src/data/types.ts#L54)[]>
Example:
@ -299,7 +260,7 @@ const cids = await data.cids();
Returns a summary of the storage space allocation of the node
- returns Promise<[CodexNodeSpace](./src/data/types.ts#L15)[]>
- returns Promise<[CodexNodeSpace](./src/data/types.ts#L58)[]>
Example:
@ -311,55 +272,24 @@ const space = await data.space();
Upload a file in a streaming manner
#### Browser
- strategy [BrowserUploadStrategy](./src/data/browser-upload.ts#L5)
- returns [UploadResponse](./src/data/types.ts#L17)
- file (File, required)
- onProgress (onProgress: (loaded: number, total: number) => void, optional)
- metadata ({ filename?: string, mimetype?: string }, optional)
- returns [UploadResponse](./src/data/types.ts#L80)
Example:
```js
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;
// Get file from previous event
const [file] = e.target.files
const metadata = {
filename: file.name,
mimetype: file.type,
}
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);
const upload = data.upload(file, (loaded: number, total: number) => {
// Use loaded and total so update a progress bar for example
}, metadata);
await upload.result();
```
#### manifest
@ -367,7 +297,7 @@ console.info("CID is", res.data);
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#L30)
- returns [CodexManifest](./src/data/types.ts#L3)
Example:
@ -406,39 +336,20 @@ 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:8080");
// When using the async api
const data = await codex.debug();
// When using the sync api
const data = codex.debug;
const codex = new Codex("http://localhost:3000");
const data = await codex.debug;
```
#### setLogLevel
Set log level at run time.
- level ([CodexLogLevel](./src/debug/types.ts#L7), required)
- level ([CodexLogLevel](./src/debug/types.ts#L3), required)
- returns Promise<"">
Example:
@ -451,7 +362,7 @@ await debug.setLogLevel("DEBUG");
Gets node information
- returns Promise<[CodexDebugInfo](./src/debug/types.ts#L23)>
- returns Promise<[CodexDebugInfo](./src/debug/types.ts#L15)>
Example:
@ -464,56 +375,18 @@ 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:8080");
// When using the async api
const node = await codex.node();
// When using the sync api
const node = codex.node;
const codex = new Codex("http://localhost:3000");
const node = await codex.node;
```
#### spr
Get Node's SPR
- returns Promise<[CodexSpr](./src/node/types.ts#L11)>
- returns Promise<[CodexSpr](./src/node/types.ts#L1)>
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);
```

View File

@ -1 +0,0 @@
index.bundle.js

View File

@ -1,23 +0,0 @@
# 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.

View File

@ -1,22 +0,0 @@
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));

View File

@ -1,4 +0,0 @@
<html>
<script src="./index.bundle.js">
</script>
</html>

View File

@ -1,19 +0,0 @@
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();

View File

@ -1,531 +0,0 @@
{
"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"
}
}
}
}

View File

@ -1,18 +0,0 @@
{
"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"
}
}

View File

@ -1 +0,0 @@
index.bundle.js

View File

@ -1,23 +0,0 @@
# 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.

View File

@ -1,22 +0,0 @@
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));

View File

@ -1,4 +0,0 @@
<html>
<script src="./index.bundle.js">
</script>
</html>

View File

@ -1,15 +0,0 @@
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();

View File

@ -1,18 +0,0 @@
{
"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"
}
}

View File

@ -1 +0,0 @@
index.bundle.js

View File

@ -1,19 +0,0 @@
# 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".

View File

@ -1,17 +0,0 @@
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();

View File

@ -1,74 +0,0 @@
{
"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"
}
}
}
}

View File

@ -1,18 +0,0 @@
{
"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"
}
}

View File

@ -1 +0,0 @@
index.bundle.js

View File

@ -1,23 +0,0 @@
# 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.

View File

@ -1,22 +0,0 @@
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));

View File

@ -1,4 +0,0 @@
<html>
<script src="./index.bundle.js">
</script>
</html>

View File

@ -1,36 +0,0 @@
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();

View File

@ -1,531 +0,0 @@
{
"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"
}
}
}
}

View File

@ -1,18 +0,0 @@
{
"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"
}
}

View File

@ -1,17 +0,0 @@
# 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".

View File

@ -1,23 +0,0 @@
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();

View File

@ -1,80 +0,0 @@
{
"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"
}
}
}
}

View File

@ -1,16 +0,0 @@
{
"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"
}
}

File diff suppressed because it is too large Load Diff

2264
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@codex-storage/sdk-js",
"version": "0.1.3",
"version": "0.0.22",
"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 src/browser.ts src/node.ts --format esm,cjs --dts --sourcemap --treeshake",
"build": "tsup src/index.ts src/async.ts --format esm,cjs --dts --sourcemap --treeshake",
"compile": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
@ -35,26 +35,6 @@
"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",
@ -66,6 +46,7 @@
}
}
},
"sideEffects": false,
"files": [
"dist"
],
@ -73,22 +54,16 @@
"readme": "README.md",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
"node": ">=20"
},
"devDependencies": {
"@tsconfig/strictest": "^2.0.5",
"@types/node": "^22.13.17",
"oas-normalize": "^14.0.0",
"openapi-typescript": "^7.6.1",
"prettier": "^3.5.3",
"prettier": "^3.4.2",
"tsup": "^8.3.6",
"typescript": "^5.8.2",
"vitest": "^3.1.1"
"typescript": "^5.7.3",
"vitest": "^3.0.5"
},
"dependencies": {
"valibot": "^1.0.0"
},
"peerDependencies": {
"undici": "^7.7.0"
"valibot": "^0.32.0"
}
}

View File

@ -1 +0,0 @@
export * from "./data/browser-upload";

View File

@ -1,81 +0,0 @@
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();
}
}

View File

@ -1,201 +0,0 @@
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");
});
});

View File

@ -1,66 +1,53 @@
import { Api } from "../api/config";
import {
Fetch,
FetchAuthBuilder,
type FetchAuth,
} from "../fetch-safe/fetch-safe";
import { CodexError } from "../errors/errors";
import { Fetch } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values";
import type {
CodexDataResponse,
UploadStrategy,
UploadResponse,
CodexSpaceResponse,
CodexNodeSpace,
CodexDataNetworkResponse,
CodexNetworkDownload,
CodexManifest,
CodexDataItems,
CodexNodeSpace,
NetworkDownloadResponse,
UploadResponse,
} from "./types";
type CodexDataOptions = {
auth?: FetchAuth;
};
export class CodexData {
readonly url: string;
readonly auth: FetchAuth = {};
constructor(url: string, options?: CodexDataOptions) {
constructor(url: string) {
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<CodexDataItems>> {
cids(): Promise<SafeValue<CodexDataResponse>> {
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(): Promise<SafeValue<CodexNodeSpace>> {
space() {
const url = this.url + Api.config.prefix + "/space";
return Fetch.safeJson<CodexSpaceResponse>(url, {
return Fetch.safeJson<CodexNodeSpace>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -70,13 +57,59 @@ 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(strategy: UploadStrategy): UploadResponse {
upload(
file: Document | XMLHttpRequestBodyInit,
onProgress?: (loaded: number, total: number) => void,
metadata: { filename?: string, mimetype?: string } = {},
): 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: strategy.upload(url, { auth: this.auth }),
result: promise,
abort: () => {
strategy.abort();
xhr.abort();
},
};
}
@ -90,20 +123,18 @@ 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.
* 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<CodexNetworkDownload>> {
async networkDownload(cid: string): Promise<SafeValue<NetworkDownloadResponse>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network`;
return Fetch.safeJson<CodexDataNetworkResponse>(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
return Fetch.safeJson(url, {
method: "POST"
});
}
@ -115,8 +146,7 @@ export class CodexData {
const url = this.url + Api.config.prefix + `/data/${cid}/network/stream`;
return Fetch.safe(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
method: "GET"
});
}
@ -124,26 +154,11 @@ export class CodexData {
* Download only the dataset manifest from the network to the local node
* if it's not available locally.
*/
async fetchManifest(cid: string): Promise<SafeValue<CodexManifest>> {
async fetchManifest(cid: string) {
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),
});
}
}

View File

@ -1,73 +0,0 @@
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
}
}
}

View File

@ -1,47 +1,89 @@
import type { components, paths } from "../openapi";
import type { FetchAuth } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values";
export type CodexDataResponse =
paths["/data"]["get"]["responses"][200]["content"]["application/json"];
export type CodexManifest = {
/**
* "Root hash of the content"
*/
// rootHash: string;
export type CodexDataItem = components["schemas"]["DataItem"];
/**
* Length of original content in bytes
*/
// originalBytes: number;
export type CodexDataItems = CodexDataResponse;
/**
* Total size of all blocks
*/
datasetSize: number;
export type CodexSpaceResponse =
paths["/space"]["get"]["responses"][200]["content"]["application/json"];
/**
* "Size of blocks"
*/
blockSize: number;
export type CodexNodeSpace = CodexSpaceResponse;
/**
* 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 UploadResponse = {
result: Promise<SafeValue<string>>;
abort: () => void;
};
export type CodexDataNetworkResponse =
paths["/data/{cid}/network"]["post"]["responses"][200]["content"]["application/json"];
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;
export type NetworkDownloadResponse = {
cid: string
manifest: CodexManifest
}
// paths["/data/{cid}"]["delete"]["responses"][204]["content"];
export type CodexDeleteResponse = "";
export type CodexDelete = CodexDeleteResponse;

View File

@ -1,26 +1,45 @@
import { assert, describe, it } from "vitest";
import { afterEach, assert, describe, it, vi } from "vitest";
import { CodexDebug } from "./debug";
import type { CodexLogLevel } from "./types";
import { CodexError } from "../errors/errors";
describe("debug", () => {
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);
afterEach(() => {
vi.restoreAllMocks();
});
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);
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("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");
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: "" });
});
});

View File

@ -1,40 +1,22 @@
import { Api } from "../api/config";
import { CodexError, CodexValibotIssuesMap } from "../errors/errors";
import {
Fetch,
FetchAuthBuilder,
type FetchAuth,
} from "../fetch-safe/fetch-safe";
import { Fetch } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values";
import {
CodexLogLevelInput,
type CodexDebugInfo,
type CodexInfoResponse,
type CodexLogLevel,
} from "./types";
import { CodexLogLevel, type CodexDebugInfo } from "./types";
import * as v from "valibot";
type CodexDebugOptions = {
auth?: FetchAuth;
};
export class CodexDebug {
readonly url: string;
readonly auth: FetchAuth = {};
constructor(url: string, options?: CodexDebugOptions) {
constructor(url: string) {
this.url = url;
if (options?.auth) {
this.auth = options.auth;
}
}
/**
* Set log level at run time
*/
async setLogLevel(level: CodexLogLevel): Promise<SafeValue<string>> {
const result = v.safeParse(CodexLogLevelInput, level);
async setLogLevel(level: CodexLogLevel): Promise<SafeValue<"">> {
const result = v.safeParse(CodexLogLevel, level);
if (!result.success) {
return Promise.resolve({
@ -51,22 +33,26 @@ export class CodexDebug {
"/debug/chronicles/loglevel?level=" +
level;
return Fetch.safeText(url, {
const res = await Fetch.safe(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: "",
});
if (res.error) {
return res;
}
return { error: false, data: "" };
}
/**
* Gets node information
*/
info(): Promise<SafeValue<CodexDebugInfo>> {
info() {
const url = this.url + Api.config.prefix + `/debug/info`;
return Fetch.safeJson<CodexInfoResponse>(url, {
return Fetch.safeJson<CodexDebugInfo>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
}

View File

@ -1,13 +1,6 @@
import * as v from "valibot";
import type { paths } from "../openapi";
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([
export const CodexLogLevel = v.picklist([
"TRACE",
"DEBUG",
"INFO",
@ -17,7 +10,49 @@ export const CodexLogLevelInput = v.picklist([
"FATAL",
]);
export type CodexInfoResponse =
paths["/debug/info"]["get"]["responses"][200]["content"]["application/json"];
export type CodexLogLevel = v.InferOutput<typeof CodexLogLevel>;
export type CodexDebugInfo = CodexInfoResponse;
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
}
};

View File

@ -1,8 +1,8 @@
import { afterEach, assert, describe, it, vi } from "vitest";
import { Fetch } from "../fetch-safe/fetch-safe";
import { CodexError } from "../errors/errors";
import { CodexError } from "../async";
describe("fetch", () => {
describe.only("fetch", () => {
afterEach(() => {
vi.restoreAllMocks();
});
@ -26,7 +26,7 @@ describe("fetch", () => {
assert.deepStrictEqual(result, { error: true, data: error });
});
it("returns an error when the json parsing failed", async () => {
it.only("returns an error when the json parsing failed", async () => {
const mockResponse = {
ok: true,
status: 200,

View File

@ -2,21 +2,6 @@ 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));
@ -60,14 +45,4 @@ 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());
},
};

View File

@ -2,7 +2,6 @@ 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";
@ -16,28 +15,19 @@ 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, options?: CodexProps) {
constructor(url: string) {
this.url = url;
this._marketplace = null;
this._data = null;
this._node = null;
this._debug = null;
if (options?.auth) {
this.auth = options?.auth;
}
}
get marketplace() {
@ -45,7 +35,7 @@ export class Codex {
return this._marketplace;
}
this._marketplace = new CodexMarketplace(this.url, { auth: this.auth });
this._marketplace = new CodexMarketplace(this.url);
return this._marketplace;
}
@ -55,7 +45,7 @@ export class Codex {
return this._data;
}
this._data = new CodexData(this.url, { auth: this.auth });
this._data = new CodexData(this.url);
return this._data;
}
@ -65,7 +55,7 @@ export class Codex {
return this._node;
}
this._node = new CodexNode(this.url, { auth: this.auth });
this._node = new CodexNode(this.url);
return this._node;
}
@ -75,7 +65,7 @@ export class Codex {
return this._debug;
}
this._debug = new CodexDebug(this.url, { auth: this.auth });
this._debug = new CodexDebug(this.url);
return this._debug;
}

View File

@ -1,276 +1,379 @@
import { assert, describe, it } from "vitest";
import { afterEach, assert, describe, it, vi } from "vitest";
import { Fetch } from "../fetch-safe/fetch-safe";
import { CodexMarketplace } from "./marketplace";
import { CodexData } from "../data/data";
import { NodeUploadStrategy } from "../data/node-upload";
import type {
CodexAvailabilityPatchInput,
CodexCreateAvailabilityInput,
CodexCreateStorageRequestInput,
} from "./types";
import {
randomEthereumAddress,
randomInt,
randomString,
} from "../tests/tests.util";
import { CodexError } from "../errors/errors";
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;
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"));
});
}
});
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"));
});
}
});
});
const data = new CodexData(
process.env["CLIENT_URL"] || "http://localhost:8080"
);
const marketplace = new CodexMarketplace(
process.env["CLIENT_URL"] || "http://localhost:8080"
);
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;
}
async function createStorageRequestBody(targetSizeInBytes = 131072) {
function createStorageRequest() {
return {
cid: await uploadContent(targetSizeInBytes),
duration: 1000,
pricePerBytePerSecond: 1,
proofProbability: 1,
expiry: 900,
collateralPerByte: 1,
nodes: 3,
tolerance: 1,
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),
};
}
}
describe("storage request", async () => {
const body = await createStorageRequestBody();
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",
},
],
}),
};
}
it("creates successfully", async () => {
const request = await marketplace.createStorageRequest(body);
assert.ok(request.error == false);
assert.ok(request.data);
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();
});
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`",
},
];
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);
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.deepStrictEqual(response, missingNumberValidationError("totalSize"));
});
assert.ok(request.error);
assert.ok(request.data.message.includes(err.message));
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);
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));
}
}
});
}
assert.deepStrictEqual(
response,
mistypeNumberValidationError("totalSize", "abc")
);
});
describe("purchases", async () => {
const body = await createStorageRequestBody();
const request = await marketplace.createStorageRequest(body);
assert.ok(request.error == false);
assert.ok(request.data);
it("lists successfully", async () => {
const ids = await marketplace.purchaseIds();
assert.ok(ids.error == false);
assert.ok(ids.data.length);
assert.ok(ids.data[0]);
const purchase = await marketplace.purchaseDetail(ids.data[0]);
assert.ok(purchase.error == false);
assert.ok(purchase.data.requestId);
assert.ok(purchase.data.state);
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);
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,
});
assert.deepStrictEqual(response, minNumberValidationError("totalSize", 1));
});
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);
assert.deepStrictEqual(response, missingNumberValidationError("duration"));
});
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,
});
assert.deepStrictEqual(response, minNumberValidationError("duration", 1));
});
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);
assert.deepStrictEqual(response, missingNumberValidationError("minPrice"));
});
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);
assert.deepStrictEqual(
response,
missingNumberValidationError("maxCollateral")
);
});
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);
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,
});
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")
);
});
});

View File

@ -1,51 +1,26 @@
import * as v from "valibot";
import { Api } from "../api/config";
import { CodexError, CodexValibotIssuesMap } from "../errors/errors";
import {
Fetch,
FetchAuthBuilder,
type FetchAuth,
} from "../fetch-safe/fetch-safe";
import { Fetch } 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 CodexAvailabilityCreateBody,
CodexAvailabilityPatchInput,
type CodexReservationsResponse,
type CodexPurchaseIdsResponse,
type CodexPurchaseResponse,
type CodexPurchase,
type CodexStorageRequestCreateBody,
type CodexReservation,
type CodexPurchaseWithoutTypes,
type CodexAvailabilityPatchBody,
} from "./types";
import {
type CodexAvailabilityDto,
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, options?: CodexMarketplaceOptions) {
constructor(url: string) {
this.url = url;
if (options?.auth) {
this.auth = options.auth;
}
}
/**
@ -54,51 +29,30 @@ export class CodexMarketplace {
async activeSlots(): Promise<SafeValue<CodexSlot[]>> {
const url = this.url + Api.config.prefix + "/sales/slots";
return Fetch.safeJson<CodexSlotResponse[]>(url, {
return Fetch.safeJson<CodexSlot[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
* Returns active slot with id {slotId} for the host
*/
async activeSlot(slotId: string): Promise<SafeValue<CodexSlotAgent>> {
async activeSlot(slotId: string): Promise<SafeValue<CodexSlot>> {
const url = this.url + Api.config.prefix + "/sales/slots/" + slotId;
return Fetch.safeJson<CodexSlotAgentResponse>(url, {
return Fetch.safeJson<CodexSlot>(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<CodexAvailabilityResponse>(url, {
const res = await Fetch.safeJson<CodexAvailabilityDto[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
if (res.error) {
@ -107,7 +61,15 @@ export class CodexMarketplace {
return {
error: false,
data: res.data.map(this.transformAvailability),
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),
})),
};
}
@ -116,7 +78,7 @@ export class CodexMarketplace {
*/
async createAvailability(
input: CodexCreateAvailabilityInput
): Promise<SafeValue<CodexAvailability>> {
): Promise<SafeValue<CodexAvailabilityCreateResponse>> {
const result = v.safeParse(CodexCreateAvailabilityInput, input);
if (!result.success) {
@ -130,32 +92,16 @@ export class CodexMarketplace {
const url = this.url + Api.config.prefix + "/sales/availability";
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;
}
const body = result.output;
return Fetch.safeJson<CodexAvailabilityCreateResponse>(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify(body),
}).then((result) => {
if (result.error) {
return result;
}
return { error: false, data: this.transformAvailability(result.data) };
body: JSON.stringify({
totalSize: body.totalSize.toString(),
duration: body.duration.toString(),
minPricePerBytePerSecond: body.minPricePerBytePerSecond.toString(),
totalCollateral: body.totalCollateral.toString(),
}),
});
}
@ -164,9 +110,9 @@ export class CodexMarketplace {
* Existing Requests linked to this Availability will continue as is.
*/
async updateAvailability(
input: CodexAvailabilityPatchInput
input: CodexUpdateAvailabilityInput
): Promise<SafeValue<"">> {
const result = v.safeParse(CodexAvailabilityPatchInput, input);
const result = v.safeParse(CodexUpdateAvailabilityInput, input);
if (!result.success) {
return {
@ -180,38 +126,16 @@ export class CodexMarketplace {
const url =
this.url + Api.config.prefix + "/sales/availability/" + result.output.id;
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 body = result.output;
const res = await Fetch.safe(url, {
method: "PATCH",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify(body),
body: JSON.stringify({
totalSize: body.totalSize.toString(),
duration: body.duration.toString(),
minPricePerBytePerSecond: body.minPricePerBytePerSecond.toString(),
totalCollateral: body.totalCollateral.toString(),
}),
});
if (res.error) {
@ -232,56 +156,28 @@ export class CodexMarketplace {
Api.config.prefix +
`/sales/availability/${availabilityId}/reservations`;
return Fetch.safeJson<CodexReservationsResponse>(url, {
return Fetch.safeJson<CodexReservation[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
* Returns list of purchase IDs
*/
async purchaseIds(): Promise<SafeValue<CodexPurchaseIdsResponse>> {
async purchaseIds(): Promise<SafeValue<string[]>> {
const url = this.url + Api.config.prefix + `/storage/purchases`;
return Fetch.safeJson<CodexPurchaseIdsResponse>(url, {
return Fetch.safeJson<string[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
private transformPurchase(p: CodexPurchaseWithoutTypes): CodexPurchase {
const purchase: CodexPurchase = {
requestId: p.requestId,
state: p.state,
};
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();
const url = this.url + Api.config.prefix + `/storage/purchases`;
const res = await Fetch.safeJson<string[]>(url, {
method: "GET",
});
if (res.error) {
return res;
@ -300,9 +196,10 @@ export class CodexMarketplace {
data: purchases.map((p) =>
p.error
? ({
state: "errored",
state: "error",
error: p.data.message,
requestId: "",
request: {} as CodexStorageRequest,
} satisfies CodexPurchase)
: p.data
),
@ -316,15 +213,8 @@ export class CodexMarketplace {
const url =
this.url + Api.config.prefix + `/storage/purchases/` + purchaseId;
return Fetch.safeJson<CodexPurchaseResponse>(url, {
headers: FetchAuthBuilder.build(this.auth),
return Fetch.safeJson<CodexPurchase>(url, {
method: "GET",
}).then((res) => {
if (res.error) {
return res;
}
return { error: false, data: this.transformPurchase(res.data) };
});
}
@ -357,18 +247,23 @@ export class CodexMarketplace {
} = result.output;
const url = this.url + Api.config.prefix + "/storage/request/" + cid;
return Fetch.safeText(url, {
const res = await Fetch.safe(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({
duration,
duration: duration.toString(),
pricePerBytePerSecond: pricePerBytePerSecond.toString(),
proofProbability: proofProbability.toString(),
nodes,
collateralPerByte: collateralPerByte.toString(),
expiry,
collateral: collateralPerByte.toString(),
expiry: expiry.toString(),
tolerance,
} satisfies CodexStorageRequestCreateBody),
}),
});
if (res.error) {
return res;
}
return { error: false, data: await res.data.text() };
}
}

View File

@ -1,169 +1,241 @@
import type { components, paths } from "../openapi";
import * as v from "valibot";
export type CodexSlotResponse =
paths["/sales/slots"]["get"]["responses"][200]["content"]["application/json"];
export type CodexStorageRequest = {
id: string;
export type CodexSlot = CodexSlotResponse;
/**
* Address of Ethereum address
*/
client: string;
export type CodexSlotAgentResponse =
paths["/sales/slots/{slotId}"]["get"]["responses"][200]["content"]["application/json"];
ask: {
/**
* Number of slots that the tequest want to have the content spread over.
*/
slots: number;
export type CodexSlotAgent = CodexSlotAgentResponse;
/**
* Amount of storage per slot (in bytes) as decimal string.
*/
slotSize: string;
export type CodexAvailabilityResponse =
paths["/sales/availability"]["get"]["responses"][200]["content"]["application/json"];
/**
* The duration of the storage request in seconds.
*/
duration: string;
export type CodexAvailabilityWithoutTypes =
components["schemas"]["SalesAvailabilityREAD"];
/**
* How often storage proofs are required as decimal string (in periods).
*/
proofProbability: string;
export type CodexAvailability = Omit<
CodexAvailabilityWithoutTypes,
| "freeSize"
| "minPricePerBytePerSecond"
| "totalCollateral"
| "totalRemainingCollateral"
> & {
freeSize?: number;
minPricePerBytePerSecond: BigInt;
totalCollateral: BigInt;
totalRemainingCollateral: BigInt;
/**
* 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 CodexAvailabilityCreateResponse =
paths["/sales/availability"]["post"]["responses"][201]["content"]["application/json"];
/**
* 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 CodexAvailabilityCreateBody = Exclude<
paths["/sales/availability"]["post"]["requestBody"],
undefined
>["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 const CodexCreateAvailabilityInput = v.strictObject({
totalSize: v.pipe(v.number(), v.minValue(1)),
duration: v.pipe(v.number(), v.minValue(1)),
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))),
minPricePerBytePerSecond: v.number(),
totalCollateral: v.number(),
});
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 type CodexCreateAvailabilityInput = v.InferOutput<
typeof CodexCreateAvailabilityInput
>;
export type CodexCreateAvailabilityInput = Omit<
v.InferOutput<typeof CodexCreateAvailabilityInput>,
"minPricePerBytePerSecond" | "totalCollateral"
> & {
minPricePerBytePerSecond?: number | BigInt;
totalCollateral?: number | BigInt;
};
export const CodexAvailabilityPatchInput = v.strictObject({
export const CodexUpdateAvailabilityInput = v.strictObject({
id: v.string(),
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))),
totalSize: v.pipe(v.number(), v.minValue(1)),
duration: v.pipe(v.number(), v.minValue(1)),
minPricePerBytePerSecond: v.number(),
totalCollateral: v.number(),
});
export type CodexAvailabilityPatchInput = Omit<
v.InferOutput<typeof CodexAvailabilityPatchInput>,
"minPricePerBytePerSecond" | "totalCollateral"
> & {
minPricePerBytePerSecond?: number | BigInt;
totalCollateral?: number | BigInt;
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 CodexReservationsResponse =
paths["/sales/availability/{id}/reservations"]["get"]["responses"][200]["content"]["application/json"];
export type CodexPurchase = {
/**
* Description of the request's state
*/
state: string;
export type CodexReservation = components["schemas"]["Reservation"];
/**
* If request failed, then here is presented the error message
*/
error: string;
export type CodexPurchaseIdsResponse =
paths["/storage/purchases"]["get"]["responses"][200]["content"]["application/json"];
request: CodexStorageRequest;
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;
requestId: string;
};
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.pipe(v.number(), v.minValue(1)),
proofProbability: v.pipe(v.number(), v.minValue(1)),
pricePerBytePerSecond: v.number(),
proofProbability: v.number(),
nodes: v.optional(v.number(), 1),
tolerance: v.optional(v.pipe(v.number(), v.minValue(1)), 1),
tolerance: v.optional(v.number(), 0),
expiry: v.pipe(v.number(), v.minValue(1)),
collateralPerByte: v.pipe(v.number(), v.minValue(1)),
collateralPerByte: v.number(),
});
export type CodexCreateStorageRequestInput = v.InferOutput<

View File

@ -1 +0,0 @@
export * from "./data/node-upload";

View File

@ -1,36 +0,0 @@
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",
}
);
});
});

View File

@ -1,39 +1,20 @@
import { Api } from "../api/config";
import {
Fetch,
FetchAuthBuilder,
type FetchAuth,
} from "../fetch-safe/fetch-safe";
import { Fetch } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values";
import type {
CodexPeerId,
CodexPeerIdContentType,
CodexPeerIdJsonResponse,
CodexSpr,
CodexSprContentType,
CodexSprJsonResponse,
} from "./types";
type CodexNodeOptions = {
auth?: FetchAuth;
};
import type { CodexSpr } from "./types";
export class CodexNode {
readonly url: string;
readonly auth: FetchAuth = {};
constructor(url: string, options?: CodexNodeOptions) {
constructor(url: string) {
this.url = url;
if (options?.auth) {
this.auth = options.auth;
}
}
/**
* Connect to a peer
* TODO check result
*/
connect(peerId: string, addrs: string[] = []): Promise<SafeValue<string>> {
connect(peerId: string, addrs: string[] = []) {
const params = new URLSearchParams();
for (const addr of addrs) {
@ -43,63 +24,31 @@ export class CodexNode {
const url =
this.url + Api.config.prefix + `/connect/${peerId}?` + params.toString();
return Fetch.safeText(url, {
return Fetch.safe(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
* Get Node's SPR
*/
async spr(
type: CodexSprContentType = "json"
): Promise<SafeValue<CodexSpr<CodexSprContentType>>> {
async spr(): Promise<SafeValue<CodexSpr>> {
const url = this.url + Api.config.prefix + "/spr";
if (type == "json") {
return Fetch.safeJson<CodexSprJsonResponse>(url, {
return Fetch.safeJson(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(
type: CodexPeerIdContentType = "json"
): Promise<SafeValue<CodexPeerId<CodexPeerIdContentType>>> {
peerId() {
const url = this.url + Api.config.prefix + "/node/peerid";
if (type == "json") {
return Fetch.safeJson<CodexPeerIdJsonResponse>(url, {
return Fetch.safe(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",
},
});
}
}

View File

@ -1,29 +1,3 @@
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;
export type CodexSpr = {
spr: string;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { assert, describe, it } from "vitest";
import { Promises } from "./promise-safe";
import { CodexError } from "../errors/errors";
import { CodexError } from "../async";
describe("promise safe", () => {
it("returns an error when the promise failed", async () => {

View File

@ -1,4 +1,4 @@
import { CodexError } from "../errors/errors";
import { CodexError } from "../async";
import type { SafeValue } from "../values/values";
export const Promises = {

View File

@ -14,7 +14,6 @@
"module": "ESNext",
"moduleResolution": "Bundler",
"verbatimModuleSyntax": true,
"sourceMap": true,
"noUncheckedIndexedAccess": true
"sourceMap": true
}
}

View File

@ -1,9 +0,0 @@
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,
},
});