Compare commits

...

21 Commits

Author SHA1 Message Date
Arnaud
a12da5cca3
Prepare release (#24)
* Prepare release

* Temporary disable ci
2025-08-27 11:54:50 +02:00
Arnaud
26a3caacc8
fix: use BigInt instead of number of token values (#17)
* Use BigInt instead of number of token values

* Remove useless triple =

* Update tests
2025-05-30 16:12:29 +02:00
Arnaud
59837ece13
chore: fix typo (#19)
* Fix typo

* Fix rebase
2025-05-30 15:37:59 +02:00
Arnaud
6b04d115c1
chore: update openapi (#22)
* Add delete endpoint

* Add more test for metadata

* Update packages
2025-05-30 15:22:39 +02:00
Arnaud
a437e593f0
chore: improve tests (#21)
* Add CI

* Rename CI

* Remove only modifier

* Define specific node version

* Improve tests

* Update CI

* Fix syntax

* Don't exit when curl is failing to connect
2025-05-30 12:04:11 +02:00
Arnaud
a8970d2377
feat: add CI (#20)
* Add CI

* Rename CI

* Remove only modifier

* Define specific node version
2025-05-28 18:32:45 +02:00
Arnaud
df64af08e5
Merge branch 'master' of github.com:codex-storage/codex-js 2025-05-26 12:55:13 +02:00
Arnaud
b50c0b7107
Add node download example 2025-05-26 12:54:34 +02:00
Adam Uhlíř
9cc497feca
docs: use correct node's api port (#15) 2025-04-09 16:42:08 +02:00
Arnaud
496aae84e7
Prepare release 2025-04-02 17:16:24 +02:00
Arnaud
3bf517d1bb
chore: generate types from open api (#14)
* Generate types from the open api file

* Update dependencies

* Update dependencies

* Update the openapi schema

* Fix merge

* Fix doc reference
2025-04-02 17:13:40 +02:00
Arnaud
921bb617ef
Update dependencies 2025-03-31 20:56:13 +02:00
Arnaud
f6d0852e86
Update version 2025-03-31 20:51:54 +02:00
Arnaud
54dca841f1
Set props as optional 2025-03-28 11:30:51 +01:00
Arnaud
9e89c30562
Support basic authentication 2025-03-28 09:13:01 +01:00
Arnaud
746f96279b
Add node types for dev 2025-03-26 18:39:31 +01:00
Arnaud
5dd17df66f
feat/data: add Node js support (#11)
* Add Node JS support

* Refactoring

* Fix documentation

* Fix doc reference
2025-03-26 18:35:24 +01:00
Vaclav Pavlin
5d2f3f6603
various small README and comment fixes (#10)
* various small README and comment fixes

* add async/sync variants with comments
2025-03-26 12:46:39 +01:00
Arnaud
f77978cd63
Add a download example in the browser 2025-03-26 10:18:17 +01:00
Arnaud
6a890262cc
Prepare release 2025-03-04 11:07:05 +01:00
Arnaud
c94d7a1257
Replace collateral by collateralPerByte 2025-03-04 11:06:13 +01:00
59 changed files with 7463 additions and 1391 deletions

55
.github/workflows/ci.yaml vendored Normal file
View File

@ -0,0 +1,55 @@
name: CI
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
env:
node_version: 22.12.0
permissions:
id-token: write
jobs:
test:
runs-on: ubuntu-latest
env:
URL: "http://localhost:8081/api/codex/v1/debug/info"
TIMEOUT_SECONDS: 300
SLEEP_INTERVAL: 2
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.node_version }}
registry-url: "https://registry.npmjs.org"
- run: npm ci
# - name: Start codex-factory
# run: npx codex-factory start latest &
# - name: Wait for first SP to be started
# run: |
# MAX_RETRIES=$((TIMEOUT_SECONDS / SLEEP_INTERVAL))
# echo "Waiting for $URL (timeout: ${TIMEOUT_SECONDS}s)..."
# for i in $(seq 1 $MAX_RETRIES); do
# STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || echo "000")
# if [ "$STATUS" = "200" ]; then
# echo "Codex is ready"
# exit 0
# fi
# sleep $SLEEP_INTERVAL
# done
# echo "Timed out after ${TIMEOUT_SECONDS}s waiting for $URL"
# exit 1
# - run: npm test

3
.gitignore vendored
View File

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

211
README.md
View File

@ -6,6 +6,23 @@ The SDK has a small bundle size and support tree shaking.
The SDK is currently under early development and the API can change at any time.
[![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
@ -25,7 +42,7 @@ const { Codex } = require("@codex-storage/sdk-js");
To create a Codex instance, provide the REST API url to interact with the Codex client:
```js
const codex = new Codex("http://localhost:3000");
const codex = new Codex("http://localhost:8080");
```
Then you can access any module like this:
@ -49,13 +66,27 @@ const { Codex } = require("@codex-storage/sdk-js/async");
To create a Codex instance, provide the REST API url to interact with the Codex client:
```js
const codex = new Codex("http://localhost:3000");
const codex = new Codex("http://localhost:8080");
```
To use a module, you need to use the await syntax. If the module is not loaded yet, it will be imported first and then cached in memory.
```js
const marketplace = await codex.marketplace;
const marketplace = await codex.marketplace();
```
### Authentication
You can use basic authentication when creating a new Codex object:
```js
const codex = new Codex("http://localhost:8080", {
auth: {
basic: "MY BASIC AUTH SECRET"
}
});
You can obtain your secret using the `btoa` method in the browser or `Buffer.from(string).toString('base64')` in Node.js. The secret is stored in memory only.
```
### Error handling
@ -96,15 +127,19 @@ if (slots.error) {
The following API assume that you have already a marketplace module loaded, example:
```js
const codex = new Codex("http://localhost:3000");
const codex = new Codex("http://localhost:8080");
// When using the async api
const marketplace = await codex.marketplace();
// When using the sync api
const marketplace = codex.marketplace;
```
#### activeSlots()
Returns active slots.
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]>
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L7)[]>
Example:
@ -117,7 +152,7 @@ const slots = await marketplace.activeSlots();
Returns active slot with id {slotId} for the host.
- slotId (string, required)
- returns Promise<[CodexSlot](./src/marketplace/types.ts#L85)[]>
- returns Promise<[CodexSlotAgent](./src/marketplace/types.ts#L12)[]>
Example:
@ -130,7 +165,7 @@ const slot = await marketplace.activeSlot(slotId);
Returns storage that is for sale.
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L99)>
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)>
Example:
@ -142,16 +177,16 @@ const availabilities = await marketplace.availabilities();
Offers storage for sale.
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L175), required)
- returns Promise<[CodexAvailabilityCreateResponse](./src/marketplace/types.ts#L186)[]>
- input ([CodexCreateAvailabilityInput](./src/marketplace/types.ts#L45), required)
- returns Promise<[CodexAvailability](./src/marketplace/types.ts#L20)[]>
Example:
```js
const response = await marketplace.createAvailability({
maxCollateral: 1,
totalCollateral: 1,
totalSize: 3000,
minPrice: 100,
minPricePerBytePerSecond: 100,
duration: 100,
});
```
@ -160,7 +195,7 @@ const response = await marketplace.createAvailability({
Updates availability.
- input ([CodexUpdateAvailabilityInput](./src/marketplace/types.ts#L186), required)
- input ([CodexAvailabilityPatchInput](./src/marketplace/types.ts#L66), required)
- returns Promise<"">
Example:
@ -168,9 +203,9 @@ Example:
```js
const response = await marketplace.updateAvailability({
id: "0x.....................",
maxCollateral: 1,
totalCollateral: 1,
totalSize: 3000,
minPrice: 100,
minPricePerBytePerSecond: 100,
duration: 100,
});
```
@ -180,7 +215,7 @@ const response = await marketplace.updateAvailability({
Return list of reservations for ongoing Storage Requests that the node hosts.
- availabilityId (string, required)
- returns Promise<[CodexReservation](./src/marketplace/types.ts#L198)[]>
- returns Promise<[CodexReservation](./src/marketplace/types.ts#L83)[]>
Example:
@ -192,7 +227,7 @@ const reservations = await marketplace.reservations("Ox...");
Creates a new Request for storage
- input ([CodexCreateStorageRequestInput](./src/marketplace/types.ts#L230), required)
- input ([CodexCreateStorageRequestInput](./src/marketplace/types.ts#L120), required)
- returns Promise<string>
Example:
@ -226,7 +261,7 @@ const ids = await marketplace.purchaseIds();
Returns purchase details
- purchaseId (string, required)
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L214)[]>
- returns Promise<[CodexPurchase](./src/marketplace/types.ts#L103)[]>
Example:
@ -240,15 +275,19 @@ const purchase = await marketplace.purchaseDetail(purchaseId);
The following API assume that you have already a data module loaded, example:
```js
const codex = new Codex("http://localhost:3000");
const data = await codex.data;
const codex = new Codex("http://localhost:8080");
// When using the async api
const data = await codex.data();
// When using the sync api
const data = codex.data;
```
#### cids
Returns the manifest stored locally in node.
- returns Promise<[CodexDataResponse](./src/data/types.ts#L54)[]>
- returns Promise<[CodexDataItem](./src/data/types.ts#L8)[]>
Example:
@ -260,7 +299,7 @@ const cids = await data.cids();
Returns a summary of the storage space allocation of the node
- returns Promise<[CodexNodeSpace](./src/data/types.ts#L58)[]>
- returns Promise<[CodexNodeSpace](./src/data/types.ts#L15)[]>
Example:
@ -272,24 +311,55 @@ const space = await data.space();
Upload a file in a streaming manner
- file (File, required)
- onProgress (onProgress: (loaded: number, total: number) => void, optional)
- metadata ({ filename?: string, mimetype?: string }, optional)
- returns [UploadResponse](./src/data/types.ts#L80)
#### Browser
- strategy [BrowserUploadStrategy](./src/data/browser-upload.ts#L5)
- returns [UploadResponse](./src/data/types.ts#L17)
Example:
```js
// Get file from previous event
const [file] = e.target.files
const metadata = {
filename: file.name,
mimetype: file.type,
const file = new File(["foo"], "foo.txt", { type: "text/plain" });
const onProgress = (loaded, total) => {
console.info("Loaded", loaded, "total", total);
};
const metadata = { filename: "foo.xt", mimetype: "text/plain" };
const strategy = new BrowserUploadStrategy(file, onProgress, metadata);
const uploadResponse = data.upload(strategy);
const res = await uploadResponse.result;
if (res.error) {
console.error(res.data);
return;
}
const upload = data.upload(file, (loaded: number, total: number) => {
// Use loaded and total so update a progress bar for example
}, metadata);
await upload.result();
console.info("CID is", res.data);
```
#### Node
- strategy [NodeUploadStrategy](./src/data/node-upload.ts#L9)
- returns [UploadResponse](./src/data/types.ts#L17)
Example:
```js
const strategy = new NodeUploadStrategy("Hello World !");
const uploadResponse = data.upload(strategy);
const res = await uploadResponse.result;
if (res.error) {
console.error(res.data);
return;
}
console.info("CID is", res.data);
```
#### manifest
@ -297,7 +367,7 @@ await upload.result();
Download only the dataset manifest from the network to the local node if it's not available locally.
- cid (string, required)
- returns [CodexManifest](./src/data/types.ts#L3)
- returns [CodexManifest](./src/data/types.ts#L30)
Example:
@ -336,20 +406,39 @@ const cid = "QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N";
const result = await data.localDownload(cid);
```
#### delete
Deletes either a single block or an entire dataset from the local node.
Does nothing if the dataset is not locally available.
- cid (string, required)
- returns ""
Example:
```js
const cid = "QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N";
const result = await data.delete(cid);
```
### Debug
The following API assume that you have already a node module loaded, example:
```js
const codex = new Codex("http://localhost:3000");
const data = await codex.debug;
const codex = new Codex("http://localhost:8080");
// When using the async api
const data = await codex.debug();
// When using the sync api
const data = codex.debug;
```
#### setLogLevel
Set log level at run time.
- level ([CodexLogLevel](./src/debug/types.ts#L3), required)
- level ([CodexLogLevel](./src/debug/types.ts#L7), required)
- returns Promise<"">
Example:
@ -362,7 +451,7 @@ await debug.setLogLevel("DEBUG");
Gets node information
- returns Promise<[CodexDebugInfo](./src/debug/types.ts#L15)>
- returns Promise<[CodexDebugInfo](./src/debug/types.ts#L23)>
Example:
@ -375,18 +464,56 @@ const info = await debug.info();
The following API assume that you have already a node module loaded, example:
```js
const codex = new Codex("http://localhost:3000");
const node = await codex.node;
const codex = new Codex("http://localhost:8080");
// When using the async api
const node = await codex.node();
// When using the sync api
const node = codex.node;
```
#### spr
Get Node's SPR
- returns Promise<[CodexSpr](./src/node/types.ts#L1)>
- returns Promise<[CodexSpr](./src/node/types.ts#L11)>
Example:
```js
const spr = await node.spr();
```
By default, the response will be a json. You can use `text` option to get the string:
#### peeriD
Get Node's peer id
- returns Promise<[CodexPeerId](./src/node/types.ts#L25)>
Example:
```js
const peerId = await node.peerId();
```
By default, the response will be a json. You can use `text` option to get the string:
```js
const peerId = await node.peerId("text");
```
#### connect
Connect to a peer
- returns Promise<string>
Example:
```js
const peerId = "..."
const addrs = [...]
const spr = await node.connect(peerId, addrs);
```

1
examples/basic-auth/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,23 @@
# Download example
Small example to show how to download a file in the browser with Codex.
## Install dependencies
```bash
npm install
```
## Build the javascript asset
```bash
CODEX_CID=REPLACE_BY_YOUR_CID npm run build
```
The response will be displayed as text, so it is better to test with .txt files.
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".
## Check the results
Open the index.html and open the web console.

View File

@ -0,0 +1,22 @@
const { build } = require("esbuild");
const define = {};
for (const k in process.env) {
define[`process.env.${k}`] = JSON.stringify(process.env[k]);
}
if (!process.env["CODEX_NODE_URL"]) {
define[`process.env.CODEX_NODE_URL`] = '"http://localhost:8080"';
}
const options = {
entryPoints: ["./index.js"],
outfile: "./index.bundle.js",
bundle: true,
define,
logOverride: {
"ignored-bare-import": "silent",
},
};
build(options).catch(() => process.exit(1));

View File

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

View File

@ -0,0 +1,19 @@
import { Codex } from "@codex-storage/sdk-js";
async function main() {
const codex = new Codex(process.env.CODEX_NODE_URL, {
auth: {
basic: btoa("admin:SuperSecret123"),
},
});
const data = codex.data;
const cid = process.env.CODEX_CID;
const result = await data.networkDownloadStream(cid);
console.info(await result.data.text());
}
main();

531
examples/basic-auth/package-lock.json generated Normal file
View File

@ -0,0 +1,531 @@
{
"name": "@codex-storage/sdk-js-basic-auth-example",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@codex-storage/sdk-js-basic-auth-example",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@codex-storage/sdk-js": "../.."
},
"devDependencies": {
"esbuild": "^0.25.1",
"prettier": "^3.5.3"
}
},
"..": {
"extraneous": true
},
"../..": {
"name": "@codex-storage/sdk-js",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"undici": "^7.5.0",
"valibot": "^0.32.0"
},
"devDependencies": {
"@tsconfig/strictest": "^2.0.5",
"@types/node": "^22.13.13",
"oas-normalize": "^13.1.2",
"openapi-typescript": "^7.6.1",
"prettier": "^3.5.3",
"tsup": "^8.3.6",
"typescript": "^5.8.2",
"vitest": "^3.0.9"
},
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/@codex-storage/sdk-js": {
"resolved": "../..",
"link": true
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.1",
"@esbuild/android-arm": "0.25.1",
"@esbuild/android-arm64": "0.25.1",
"@esbuild/android-x64": "0.25.1",
"@esbuild/darwin-arm64": "0.25.1",
"@esbuild/darwin-x64": "0.25.1",
"@esbuild/freebsd-arm64": "0.25.1",
"@esbuild/freebsd-x64": "0.25.1",
"@esbuild/linux-arm": "0.25.1",
"@esbuild/linux-arm64": "0.25.1",
"@esbuild/linux-ia32": "0.25.1",
"@esbuild/linux-loong64": "0.25.1",
"@esbuild/linux-mips64el": "0.25.1",
"@esbuild/linux-ppc64": "0.25.1",
"@esbuild/linux-riscv64": "0.25.1",
"@esbuild/linux-s390x": "0.25.1",
"@esbuild/linux-x64": "0.25.1",
"@esbuild/netbsd-arm64": "0.25.1",
"@esbuild/netbsd-x64": "0.25.1",
"@esbuild/openbsd-arm64": "0.25.1",
"@esbuild/openbsd-x64": "0.25.1",
"@esbuild/sunos-x64": "0.25.1",
"@esbuild/win32-arm64": "0.25.1",
"@esbuild/win32-ia32": "0.25.1",
"@esbuild/win32-x64": "0.25.1"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"name": "@codex-storage/sdk-js-basic-auth-example",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "node esbuild.js"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@codex-storage/sdk-js": "../.."
},
"devDependencies": {
"esbuild": "^0.25.1",
"prettier": "^3.5.3"
}
}

1
examples/download-browser/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,23 @@
# Download example
Small example to show how to download a file in the browser with Codex.
## Install dependencies
```bash
npm install
```
## Build the javascript asset
```bash
CODEX_CID=REPLACE_BY_YOUR_CID npm run build
```
The response will be displayed as text, so it is better to test with .txt files.
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".
## Check the results
Open the index.html and open the web console.

View File

@ -0,0 +1,22 @@
const { build } = require("esbuild");
const define = {};
for (const k in process.env) {
define[`process.env.${k}`] = JSON.stringify(process.env[k]);
}
if (!process.env["CODEX_NODE_URL"]) {
define[`process.env.CODEX_NODE_URL`] = '"http://localhost:8080"';
}
const options = {
entryPoints: ["./index.js"],
outfile: "./index.bundle.js",
bundle: true,
define,
logOverride: {
"ignored-bare-import": "silent",
},
};
build(options).catch(() => process.exit(1));

View File

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

View File

@ -0,0 +1,15 @@
import { Codex } from "@codex-storage/sdk-js";
async function main() {
const codex = new Codex(process.env.CODEX_NODE_URL);
const data = codex.data;
const cid = process.env.CODEX_CID;
const result = await data.networkDownloadStream(cid);
console.info(await result.data.text());
}
main();

View File

@ -0,0 +1,18 @@
{
"name": "@codex-storage/sdk-js-download-browser-example",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "node esbuild.js"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@codex-storage/sdk-js": "../.."
},
"devDependencies": {
"esbuild": "^0.25.1",
"prettier": "^3.5.3"
}
}

1
examples/download-node/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,19 @@
# Download example
Small example to show how to download a file in node with Codex.
## Install dependencies
```bash
npm install
```
## Run node
```bash
CODEX_CID=REPLACE_BY_YOUR_CID node index.js
```
The response will be displayed as text, so it is better to test with .txt files.
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".

View File

@ -0,0 +1,17 @@
import { Codex } from "@codex-storage/sdk-js";
async function main() {
const codex = new Codex(
process.env.CODEX_NODE_URL || "http://localhost:8080"
);
const data = codex.data;
const cid = process.env.CODEX_CID;
const result = await data.networkDownloadStream(cid);
console.info(await result.data.text());
}
main();

74
examples/download-node/package-lock.json generated Normal file
View File

@ -0,0 +1,74 @@
{
"name": "@codex-storage/sdk-js-download-node-example",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@codex-storage/sdk-js-download-node-example",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@codex-storage/sdk-js": "../..",
"undici": "^7.7.0"
},
"devDependencies": {
"prettier": "^3.5.3"
}
},
"..": {
"extraneous": true
},
"../..": {
"name": "@codex-storage/sdk-js",
"version": "0.1.2",
"license": "MIT",
"dependencies": {
"valibot": "^1.0.0"
},
"devDependencies": {
"@tsconfig/strictest": "^2.0.5",
"@types/node": "^22.13.17",
"oas-normalize": "^13.1.2",
"openapi-typescript": "^7.6.1",
"prettier": "^3.5.3",
"tsup": "^8.3.6",
"typescript": "^5.8.2",
"vitest": "^3.1.1"
},
"engines": {
"node": ">=20.18.1"
},
"peerDependencies": {
"undici": "^7.7.0"
}
},
"node_modules/@codex-storage/sdk-js": {
"resolved": "../..",
"link": true
},
"node_modules/prettier": {
"version": "3.5.3",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/undici": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz",
"integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"name": "@codex-storage/sdk-js-download-node-example",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "node esbuild.js"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@codex-storage/sdk-js": "../..",
"undici": "^7.7.0"
},
"devDependencies": {
"prettier": "^3.5.3"
}
}

1
examples/upload-browser/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,23 @@
# Download example
Small example to show how to download a file in the browser with Codex.
## Install dependencies
```bash
npm install
```
## Build the javascript asset
```bash
CODEX_CID=REPLACE_BY_YOUR_CID npm run build
```
The response will be displayed as text, so it is better to test with .txt files.
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".
## Check the results
Open the index.html and open the web console.

View File

@ -0,0 +1,22 @@
const { build } = require("esbuild");
const define = {};
for (const k in process.env) {
define[`process.env.${k}`] = JSON.stringify(process.env[k]);
}
if (!process.env["CODEX_NODE_URL"]) {
define[`process.env.CODEX_NODE_URL`] = '"http://localhost:8080"';
}
const options = {
entryPoints: ["./index.js"],
outfile: "./index.bundle.js",
bundle: true,
define,
logOverride: {
"ignored-bare-import": "silent",
},
};
build(options).catch(() => process.exit(1));

View File

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

View File

@ -0,0 +1,36 @@
import { Codex } from "@codex-storage/sdk-js";
import { BrowserUploadStrategy } from "@codex-storage/sdk-js/browser";
async function main() {
const codex = new Codex(process.env.CODEX_NODE_URL);
const data = codex.data;
const file = new File(["foo"], "foo.txt", {
type: "text/plain",
});
const onProgress = (loaded, total) => {
console.info("Loaded", loaded, "total", total);
};
const metadata = {
filename: "foo.xt",
mimetype: "text/plain",
};
const strategy = new BrowserUploadStrategy(file, onProgress, metadata);
const uploadResponse = data.upload(strategy);
const res = await uploadResponse.result;
if (res.error) {
console.error(res.data);
return;
}
console.info("CID is", res.data);
}
main();

531
examples/upload-browser/package-lock.json generated Normal file
View File

@ -0,0 +1,531 @@
{
"name": "@codex-storage/sdk-js-update-browser-example",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@codex-storage/sdk-js-update-browser-example",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@codex-storage/sdk-js": "../.."
},
"devDependencies": {
"esbuild": "^0.25.1",
"prettier": "^3.5.3"
}
},
"..": {
"extraneous": true
},
"../..": {
"name": "@codex-storage/sdk-js",
"version": "0.0.23",
"license": "MIT",
"dependencies": {
"undici": "^7.5.0",
"valibot": "^0.32.0"
},
"devDependencies": {
"@tsconfig/strictest": "^2.0.5",
"prettier": "^3.5.3",
"tsup": "^8.3.6",
"typescript": "^5.8.2",
"vitest": "^3.0.9"
},
"engines": {
"node": ">=20.18.1"
}
},
"../dist": {
"extraneous": true
},
"node_modules/@codex-storage/sdk-js": {
"resolved": "../..",
"link": true
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.1",
"@esbuild/android-arm": "0.25.1",
"@esbuild/android-arm64": "0.25.1",
"@esbuild/android-x64": "0.25.1",
"@esbuild/darwin-arm64": "0.25.1",
"@esbuild/darwin-x64": "0.25.1",
"@esbuild/freebsd-arm64": "0.25.1",
"@esbuild/freebsd-x64": "0.25.1",
"@esbuild/linux-arm": "0.25.1",
"@esbuild/linux-arm64": "0.25.1",
"@esbuild/linux-ia32": "0.25.1",
"@esbuild/linux-loong64": "0.25.1",
"@esbuild/linux-mips64el": "0.25.1",
"@esbuild/linux-ppc64": "0.25.1",
"@esbuild/linux-riscv64": "0.25.1",
"@esbuild/linux-s390x": "0.25.1",
"@esbuild/linux-x64": "0.25.1",
"@esbuild/netbsd-arm64": "0.25.1",
"@esbuild/netbsd-x64": "0.25.1",
"@esbuild/openbsd-arm64": "0.25.1",
"@esbuild/openbsd-x64": "0.25.1",
"@esbuild/sunos-x64": "0.25.1",
"@esbuild/win32-arm64": "0.25.1",
"@esbuild/win32-ia32": "0.25.1",
"@esbuild/win32-x64": "0.25.1"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"name": "@codex-storage/sdk-js-update-browser-example",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "node esbuild.js"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@codex-storage/sdk-js": "../.."
},
"devDependencies": {
"esbuild": "^0.25.1",
"prettier": "^3.5.3"
}
}

View File

@ -0,0 +1,17 @@
# Download example
Small example to show how to download a file in the browser with Codex.
## Install dependencies
```bash
npm install
```
## Run node
```bash
node index.js
```
Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080".

View File

@ -0,0 +1,23 @@
const { Codex } = require("@codex-storage/sdk-js");
const { NodeUploadStrategy } = require("@codex-storage/sdk-js/node");
async function main() {
const codex = new Codex(
process.env.CODEX_NODE_URL || "http://localhost:8080"
);
const data = codex.data;
const strategy = new NodeUploadStrategy("Hello World !");
const uploadResponse = data.upload(strategy);
const res = await uploadResponse.result;
if (res.error) {
console.error(res.data);
return;
}
console.info("CID is", res.data);
}
main();

80
examples/upload-node/package-lock.json generated Normal file
View File

@ -0,0 +1,80 @@
{
"name": "@codex-storage/sdk-js-update-node-example",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@codex-storage/sdk-js-update-node-example",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@codex-storage/sdk-js": "../..",
"undici": "^7.7.0"
},
"devDependencies": {
"prettier": "^3.5.3"
}
},
"..": {
"extraneous": true
},
"../..": {
"name": "@codex-storage/sdk-js",
"version": "0.1.1",
"license": "MIT",
"dependencies": {
"undici": "^7.7.0",
"valibot": "^1.0.0"
},
"devDependencies": {
"@tsconfig/strictest": "^2.0.5",
"@types/node": "^22.13.17",
"oas-normalize": "^13.1.2",
"openapi-typescript": "^7.6.1",
"prettier": "^3.5.3",
"tsup": "^8.3.6",
"typescript": "^5.8.2",
"vitest": "^3.1.1"
},
"engines": {
"node": ">=20.18.1"
},
"peerDependencies": {
"undici": "^7.6.0"
}
},
"../dist": {
"extraneous": true
},
"node_modules/@codex-storage/sdk-js": {
"resolved": "../..",
"link": true
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/undici": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.7.0.tgz",
"integrity": "sha512-tZ6+5NBq4KH35rr46XJ2JPFKxfcBlYNaqLF/wyWIO9RMHqqU/gx/CLB1Y2qMcgB8lWw/bKHa7qzspqCN7mUHvA==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
}
}
}

View File

@ -0,0 +1,16 @@
{
"name": "@codex-storage/sdk-js-update-node-example",
"version": "1.0.0",
"main": "index.js",
"scripts": {},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@codex-storage/sdk-js": "../..",
"undici": "^7.7.0"
},
"devDependencies": {
"prettier": "^3.5.3"
}
}

1043
openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

2260
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.0.22",
"version": "0.1.3",
"description": "Codex SDK to interact with the Codex decentralized storage network.",
"repository": {
"type": "git",
@ -9,7 +9,7 @@
"scripts": {
"prepack": "npm run build",
"prebuild": "npm run compile && rm -Rf dist/*",
"build": "tsup src/index.ts src/async.ts --format esm,cjs --dts --sourcemap --treeshake",
"build": "tsup src/index.ts src/async.ts src/browser.ts src/node.ts --format esm,cjs --dts --sourcemap --treeshake",
"compile": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
@ -35,6 +35,26 @@
"default": "./dist/index.js"
}
},
"./browser": {
"import": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.mjs"
},
"require": {
"types": "./dist/browser.d.cts",
"default": "./dist/browser.js"
}
},
"./node": {
"import": {
"types": "./dist/node.d.ts",
"default": "./dist/node.mjs"
},
"require": {
"types": "./dist/node.d.cts",
"default": "./dist/node.js"
}
},
"./async": {
"import": {
"types": "./dist/async.d.ts",
@ -46,7 +66,6 @@
}
}
},
"sideEffects": false,
"files": [
"dist"
],
@ -54,16 +73,22 @@
"readme": "README.md",
"license": "MIT",
"engines": {
"node": ">=20"
"node": ">=20.18.1"
},
"devDependencies": {
"@tsconfig/strictest": "^2.0.5",
"prettier": "^3.4.2",
"@types/node": "^22.13.17",
"oas-normalize": "^14.0.0",
"openapi-typescript": "^7.6.1",
"prettier": "^3.5.3",
"tsup": "^8.3.6",
"typescript": "^5.7.3",
"vitest": "^3.0.5"
"typescript": "^5.8.2",
"vitest": "^3.1.1"
},
"dependencies": {
"valibot": "^0.32.0"
"valibot": "^1.0.0"
},
"peerDependencies": {
"undici": "^7.7.0"
}
}

1
src/browser.ts Normal file
View File

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

View File

@ -0,0 +1,81 @@
import { CodexError } from "../errors/errors";
import type { SafeValue } from "../values/values";
import type { UploadStrategy, UploadStrategyOptions } from "./types";
export class BrowserUploadStrategy implements UploadStrategy {
private readonly file: Document | XMLHttpRequestBodyInit;
private readonly onProgress:
| ((loaded: number, total: number) => void)
| undefined;
private readonly metadata:
| { filename?: string; mimetype?: string }
| undefined;
private xhr: XMLHttpRequest | undefined;
constructor(
file: Document | XMLHttpRequestBodyInit,
onProgress?: (loaded: number, total: number) => void,
metadata?: { filename?: string; mimetype?: string }
) {
this.file = file;
this.onProgress = onProgress;
this.metadata = metadata;
}
upload(
url: string,
{ auth }: UploadStrategyOptions
): Promise<SafeValue<string>> {
const xhr = new XMLHttpRequest();
this.xhr = xhr;
return new Promise<SafeValue<string>>((resolve) => {
xhr.upload.onprogress = (evt) => {
if (evt.lengthComputable) {
this.onProgress?.(evt.loaded, evt.total);
}
};
xhr.open("POST", url, true);
if (this.metadata?.filename) {
xhr.setRequestHeader(
"Content-Disposition",
'attachment; filename="' + this.metadata.filename + '"'
);
}
if (auth?.basic) {
xhr.setRequestHeader("Authorization", "Basic " + auth.basic);
}
if (this.metadata?.mimetype) {
xhr.setRequestHeader("Content-Type", this.metadata.mimetype);
}
xhr.send(this.file);
xhr.onload = function () {
if (xhr.status != 200) {
resolve({
error: true,
data: new CodexError(xhr.responseText, { code: xhr.status }),
});
} else {
resolve({ error: false, data: xhr.response });
}
};
xhr.onerror = function () {
resolve({
error: true,
data: new CodexError("Something went wrong during the file upload."),
});
};
});
}
abort(): void {
this.xhr?.abort();
}
}

201
src/data/data.spec.ts Normal file
View File

@ -0,0 +1,201 @@
import { assert, describe, it } from "vitest";
import { CodexData } from "./data";
import { NodeUploadStrategy } from "./node-upload";
import crypto from "crypto";
describe("data", () => {
const data = new CodexData(
process.env["CLIENT_URL"] || "http://localhost:8080"
);
const spData = new CodexData(
process.env["SP_URL"] || "http://localhost:8081"
);
it("uploads a file a download it locally", async () => {
const content = crypto.randomBytes(16).toString("hex");
const strategy = new NodeUploadStrategy(content);
const res = data.upload(strategy);
const cid = await res.result;
assert.ok(cid.error == false);
assert.ok(cid.data);
const cids = await data.cids();
assert.ok(cids.error == false);
assert.ok(cids.data.content.find((c) => c.cid == cid.data));
const localDownload = await data.localDownload(cid.data);
assert.ok(localDownload.error == false);
assert.strictEqual(await localDownload.data.text(), content);
const manifest = await data.fetchManifest(cid.data);
assert.ok(manifest.error == false);
assert.strictEqual(manifest.data.cid, cid.data);
const { blockSize, datasetSize, treeCid } = manifest.data.manifest;
assert.ok(blockSize);
assert.ok(datasetSize);
assert.ok(treeCid);
});
it("saves the metadata uploads provided during the upload", async () => {
const content = crypto.randomBytes(16).toString("hex");
const strategy = new NodeUploadStrategy(content, {
filename: "hello.txt",
mimetype: "text/plain",
});
const res = data.upload(strategy);
const cid = await res.result;
assert.ok(cid.error == false);
assert.ok(cid.data);
const manifest = await data.fetchManifest(cid.data);
assert.ok(manifest.error == false);
assert.strictEqual(manifest.data.cid, cid.data);
const { filename, mimetype } = manifest.data.manifest;
assert.strictEqual(filename, "hello.txt");
assert.ok(mimetype, "text/plain");
});
it("fails when providing wrong metadata", async () => {
const content = crypto.randomBytes(16).toString("hex");
const strategy = new NodeUploadStrategy(content, {
filename: "hello.txt",
mimetype: "plain/text",
});
const res = data.upload(strategy);
const cid = await res.result;
assert.ok(cid.error == true);
assert.ok(
cid.data.message.includes(" The MIME type 'plain/text' is not valid")
);
assert.equal(cid.data.code, 422);
});
it("delete a file a locally", async () => {
const content = "b".repeat(131072);
const strategy = new NodeUploadStrategy(content);
const res = data.upload(strategy);
const cid = await res.result;
assert.ok(cid.error == false);
assert.ok(cid.data);
let cids = await data.cids();
assert.ok(cids.error == false);
assert.ok(cids.data.content.find((c) => c.cid == cid.data));
const del = await data.delete(cid.data);
assert.ok(del.error == false);
cids = await data.cids();
assert.ok(cids.error == false);
assert.notOk(cids.data.content.find((c) => c.cid == cid.data));
});
it("doesn't do anything when trying to delete a non existing cid", async () => {
const content = crypto.randomBytes(16).toString("hex");
const strategy = new NodeUploadStrategy(content);
const res = spData.upload(strategy);
const cid = await res.result;
assert.ok(cid.error == false);
assert.ok(cid.data);
const del = await data.delete(cid.data);
assert.ok(del.error == false);
});
it("returns an error when providing an invalid cid", async () => {
const del = await data.delete("hello");
assert.ok(del.error);
assert.ok(del.data.message.includes("Incorrect Cid"));
});
it("updates the space available when storing data", async () => {
const content = crypto.randomBytes(16).toString("hex");
let space = await data.space();
assert.ok(space.error == false);
assert.ok(space.data.quotaMaxBytes);
const usedBytes = space.data.quotaUsedBytes;
const strategy = new NodeUploadStrategy(content);
const res = data.upload(strategy);
const cid = await res.result;
assert.ok(cid.error == false);
assert.ok(cid.data);
space = await data.space();
assert.ok(space.error == false);
assert.ok(space.data.quotaMaxBytes);
assert.ok(space.data.quotaUsedBytes > usedBytes);
});
it("stream downloads a file on the network", async () => {
const content = crypto.randomBytes(16).toString("hex");
const strategy = new NodeUploadStrategy(content);
const res = spData.upload(strategy);
const cid = await res.result;
assert.ok(cid.error == false);
assert.ok(cid.data);
const networkDownload = await data.networkDownloadStream(cid.data);
assert.ok(networkDownload.error == false);
assert.strictEqual(await networkDownload.data.text(), content);
});
it("downloads a file on the network", async () => {
const content = crypto.randomBytes(16).toString("hex");
const strategy = new NodeUploadStrategy(content);
const res = spData.upload(strategy);
const cid = await res.result;
assert.ok(cid.error == false);
assert.ok(cid.data);
const networkDownload = await data.networkDownload(cid.data);
assert.ok(networkDownload.error == false);
const cids = await data.cids();
assert.ok(cids.error == false);
assert.ok(cids.data.content.find((c) => c.cid == cid.data));
});
it("returns an error when trying to stream download a not existing file on the network", async () => {
const cid = crypto.randomBytes(16).toString("hex");
const networkDownload = await data.networkDownloadStream(cid);
assert.ok(networkDownload.error);
assert.strictEqual(networkDownload.data.message, "Incorrect Cid");
});
it("returns an error when trying to download a not existing file on the network", async () => {
const cid = crypto.randomBytes(16).toString("hex");
const networkDownload = await data.networkDownload(cid);
assert.ok(networkDownload.error);
assert.strictEqual(networkDownload.data.message, "Incorrect Cid");
});
it("returns an error when trying to download a not existing file locally", async () => {
const cid = crypto.randomBytes(16).toString("hex");
const networkDownload = await data.localDownload(cid);
assert.ok(networkDownload.error);
assert.strictEqual(networkDownload.data.message, "Incorrect Cid");
});
it("returns an error when trying to fetch a not existing manifest", async () => {
const cid = crypto.randomBytes(16).toString("hex");
const fetchManifest = await data.fetchManifest(cid);
assert.ok(fetchManifest.error);
assert.strictEqual(fetchManifest.data.message, "Incorrect Cid");
});
});

View File

@ -1,53 +1,66 @@
import { Api } from "../api/config";
import { CodexError } from "../errors/errors";
import { Fetch } from "../fetch-safe/fetch-safe";
import {
Fetch,
FetchAuthBuilder,
type FetchAuth,
} from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values";
import type {
CodexDataResponse,
CodexManifest,
CodexNodeSpace,
NetworkDownloadResponse,
UploadStrategy,
UploadResponse,
CodexSpaceResponse,
CodexNodeSpace,
CodexDataNetworkResponse,
CodexNetworkDownload,
CodexManifest,
CodexDataItems,
} from "./types";
type CodexDataOptions = {
auth?: FetchAuth;
};
export class CodexData {
readonly url: string;
readonly auth: FetchAuth = {};
constructor(url: string) {
constructor(url: string, options?: CodexDataOptions) {
this.url = url;
if (options?.auth) {
this.auth = options.auth;
}
}
/**
* Lists manifest CIDs stored locally in node.
* TODO: remove the faker data part when the api is ready
*/
cids(): Promise<SafeValue<CodexDataResponse>> {
cids(): Promise<SafeValue<CodexDataItems>> {
const url = this.url + Api.config.prefix + "/data";
return Fetch.safeJson<CodexDataResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
}).then((data) => {
if (data.error) {
return data;
}
return {
error: false,
data: {
content: data.data.content,
},
};
return { error: false, data: { content: data.data.content } };
});
}
/**
* Gets a summary of the storage space allocation of the node.
*/
space() {
space(): Promise<SafeValue<CodexNodeSpace>> {
const url = this.url + Api.config.prefix + "/space";
return Fetch.safeJson<CodexNodeSpace>(url, {
return Fetch.safeJson<CodexSpaceResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -57,59 +70,13 @@ export class CodexData {
* XMLHttpRequest is used instead of fetch for this case, to obtain progress information.
* A callback onProgress can be passed to receive upload progress data information.
*/
upload(
file: Document | XMLHttpRequestBodyInit,
onProgress?: (loaded: number, total: number) => void,
metadata: { filename?: string, mimetype?: string } = {},
): UploadResponse {
upload(strategy: UploadStrategy): UploadResponse {
const url = this.url + Api.config.prefix + "/data";
const xhr = new XMLHttpRequest();
const promise = new Promise<SafeValue<string>>((resolve) => {
xhr.upload.onprogress = (evt) => {
if (evt.lengthComputable) {
onProgress?.(evt.loaded, evt.total);
}
};
xhr.open("POST", url, true);
if (metadata.filename) {
xhr.setRequestHeader("Content-Disposition", "attachment; filename=\"" + metadata.filename + "\"")
}
if (metadata.mimetype) {
xhr.setRequestHeader("Content-Type", metadata.mimetype)
}
xhr.send(file);
xhr.onload = function () {
if (xhr.status != 200) {
resolve({
error: true,
data: new CodexError(xhr.responseText, {
code: xhr.status,
}),
});
} else {
resolve({ error: false, data: xhr.response });
}
};
xhr.onerror = function () {
resolve({
error: true,
data: new CodexError("Something went wrong during the file upload."),
});
};
});
return {
result: promise,
result: strategy.upload(url, { auth: this.auth }),
abort: () => {
xhr.abort();
strategy.abort();
},
};
}
@ -123,6 +90,20 @@ export class CodexData {
return Fetch.safe(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
* Download a file from the network to the local node if it's not available locally.
* Note: Download is performed async. Call can return before download is completed.
*/
async networkDownload(cid: string): Promise<SafeValue<CodexNetworkDownload>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network`;
return Fetch.safeJson<CodexDataNetworkResponse>(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -130,35 +111,39 @@ export class CodexData {
* Download a file from the network in a streaming manner.
* If the file is not available locally, it will be retrieved from other nodes in the network if able.
*/
async networkDownload(cid: string): Promise<SafeValue<NetworkDownloadResponse>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network`;
return Fetch.safeJson(url, {
method: "POST"
});
}
/**
* Download a file from the network in a streaming manner.
* If the file is not available locally, it will be retrieved from other nodes in the network if able.
*/
async networkDownloadStream(cid: string): Promise<SafeValue<Response>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network/stream`;
return Fetch.safe(url, {
method: "GET"
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
* Download only the dataset manifest from the network to the local node
* if it's not available locally.
* Download only the dataset manifest from the network to the local node
* if it's not available locally.
*/
async fetchManifest(cid: string) {
async fetchManifest(cid: string): Promise<SafeValue<CodexManifest>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network/manifest`;
return Fetch.safeJson<CodexManifest>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
* Deletes either a single block or an entire dataset
* from the local node. Does nothing
* if the dataset is not locally available.
*/
async delete(cid: string): Promise<SafeValue<string>> {
const url = this.url + Api.config.prefix + `/data/${cid}`;
return Fetch.safeText(url, {
method: "DELETE",
headers: FetchAuthBuilder.build(this.auth),
});
}
}

73
src/data/node-upload.ts Normal file
View File

@ -0,0 +1,73 @@
import type { Readable } from "node:stream";
import { CodexError } from "../errors/errors";
import type { SafeValue } from "../values/values";
import Undici from "undici";
import { type FormData } from "undici";
import type { UploadStrategy, UploadStrategyOptions } from "./types";
import { FetchAuthBuilder } from "../fetch-safe/fetch-safe";
export class NodeUploadStrategy implements UploadStrategy {
private readonly body:
| string
| Buffer
| Uint8Array
| null
| Readable
| FormData;
private readonly metadata:
| { filename?: string; mimetype?: string }
| undefined;
private abortController: AbortController | undefined;
constructor(
body: string | Buffer | Uint8Array | null | Readable | FormData,
metadata?: { filename?: string; mimetype?: string }
) {
this.body = body;
this.metadata = metadata;
}
async upload(
url: string,
{ auth }: UploadStrategyOptions
): Promise<SafeValue<string>> {
const headers: Record<string, string> = FetchAuthBuilder.build(auth);
if (this.metadata?.filename) {
headers["Content-Disposition"] =
'attachment; filename="' + this.metadata?.filename + '"';
}
if (this.metadata?.mimetype) {
headers["Content-Type"] = this.metadata?.mimetype;
}
const controller = new AbortController();
this.abortController = controller;
const res = await Undici.request(url, {
method: "POST",
headers,
body: this.body,
signal: controller.signal,
});
if (res.statusCode < 200 || res.statusCode >= 300) {
const msg = `The status code is invalid got ${res.statusCode} - ${await res.body.text()} `;
return {
error: true,
data: new CodexError(msg, { code: res.statusCode }),
};
}
return { error: false, data: await res.body.text() };
}
abort(): void {
try {
this.abortController?.abort();
} catch (_) {
// Nothing to do
}
}
}

View File

@ -1,89 +1,47 @@
import type { components, paths } from "../openapi";
import type { FetchAuth } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values";
export type CodexManifest = {
/**
* "Root hash of the content"
*/
// rootHash: string;
export type CodexDataResponse =
paths["/data"]["get"]["responses"][200]["content"]["application/json"];
/**
* Length of original content in bytes
*/
// originalBytes: number;
export type CodexDataItem = components["schemas"]["DataItem"];
/**
* Total size of all blocks
*/
datasetSize: number;
export type CodexDataItems = CodexDataResponse;
/**
* "Size of blocks"
*/
blockSize: number;
export type CodexSpaceResponse =
paths["/space"]["get"]["responses"][200]["content"]["application/json"];
/**
* Indicates if content is protected by erasure-coding
*/
protected: boolean;
/**
* Root of the merkle tree
*/
treeCid: string;
/**
* Name of the name
*/
filename: string | null;
/**
* Mimetype
*/
mimetype: string | null;
};
export type CodexDataContent = {
/**
* Content Identifier as specified at https://github.com/multiformats/cid
*/
cid: string;
manifest: CodexManifest;
};
export type CodexDataResponse = {
content: CodexDataContent[];
};
export type CodexNodeSpace = {
/**
* Number of blocks stored by the node
*/
totalBlocks: number;
/**
* Maximum storage space used by the node
*/
quotaMaxBytes: number;
/**
* Amount of storage space currently in use
*/
quotaUsedBytes: number;
/**
* Amount of storage space reserved
*/
quotaReservedBytes: number;
};
export type CodexNodeSpace = CodexSpaceResponse;
export type UploadResponse = {
result: Promise<SafeValue<string>>;
abort: () => void;
};
export type CodexDataNetworkResponse =
paths["/data/{cid}/network"]["post"]["responses"][200]["content"]["application/json"];
export type NetworkDownloadResponse = {
cid: string
manifest: CodexManifest
}
export type CodexNetworkDownload = components["schemas"]["DataItem"];
export type CodexFetchManifestResponse =
paths["/data/{cid}/network/manifest"]["get"]["responses"][200]["content"]["application/json"];
export type CodexManifest = CodexFetchManifestResponse;
export type UploadStrategyOptions = {
auth?: FetchAuth;
};
export interface UploadStrategy {
upload(
url: string,
options?: UploadStrategyOptions
): Promise<SafeValue<string>>;
abort(): void;
}
// paths["/data/{cid}"]["delete"]["responses"][204]["content"];
export type CodexDeleteResponse = "";
export type CodexDelete = CodexDeleteResponse;

View File

@ -1,45 +1,26 @@
import { afterEach, assert, describe, it, vi } from "vitest";
import { assert, describe, it } from "vitest";
import { CodexDebug } from "./debug";
import type { CodexLogLevel } from "./types";
import { CodexError } from "../errors/errors";
describe("debug", () => {
afterEach(() => {
vi.restoreAllMocks();
const debug = new CodexDebug(
process.env["CLIENT_URL"] || "http://localhost:8080"
);
it("changes the log level", async () => {
const logLevel = await debug.setLogLevel("NOTICE");
assert.ok(logLevel.error == false);
});
const debug = new CodexDebug("http://localhost:3000");
it("returns an error when trying to setup the log level with a bad value", async () => {
const response = await debug.setLogLevel("TEST" as CodexLogLevel);
assert.deepStrictEqual(response, {
error: true,
data: new CodexError("Cannot validate the input", {
errors: [
{
expected:
'"TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL"',
message:
'Invalid type: Expected "TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL" but received "TEST"',
path: undefined,
received: '"TEST"',
},
],
}),
});
it("gets the debug info", async () => {
const info = await debug.info();
assert.ok(info.error == false);
assert.ok(info.data.spr);
assert.ok(info.data.announceAddresses.length > 0);
});
it("returns a success when trying to setup the log level with a correct value", async () => {
const mockResponse = {
ok: true,
status: 200,
text: async () => "",
} as Response;
globalThis.fetch = vi.fn().mockResolvedValue(mockResponse);
const response = await debug.setLogLevel("ERROR");
assert.deepStrictEqual(response, { error: false, data: "" });
it("returns error when changing the log level with wrong value", async () => {
const logLevel = await debug.setLogLevel("HELLO");
assert.ok(logLevel.error);
assert.strictEqual(logLevel.data.message, "Cannot validate the input");
});
});

View File

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

View File

@ -1,6 +1,13 @@
import * as v from "valibot";
import type { paths } from "../openapi";
export const CodexLogLevel = v.picklist([
export type CodexLogLevelResponse =
paths["/debug/chronicles/loglevel"]["post"]["responses"][200]["content"];
export type CodexLogLevel =
paths["/debug/chronicles/loglevel"]["post"]["parameters"]["query"]["level"];
export const CodexLogLevelInput = v.picklist([
"TRACE",
"DEBUG",
"INFO",
@ -10,49 +17,7 @@ export const CodexLogLevel = v.picklist([
"FATAL",
]);
export type CodexLogLevel = v.InferOutput<typeof CodexLogLevel>;
export type CodexInfoResponse =
paths["/debug/info"]["get"]["responses"][200]["content"]["application/json"];
export type CodexDebugInfo = {
/**
* Peer Identity reference as specified at https://docs.libp2p.io/concepts/fundamentals/peers/
*/
id: string;
/**
* Address of node as specified by the multi-address specification https://multiformats.io/multiaddr/
*/
addrs: string[];
announceAddresses: string[]
/**
* Path of the data repository where all nodes data are stored
*/
repo: string;
// Signed Peer Record (libp2p)
spr: string;
table: {
localNode: {
nodeId: string
peerId: string
record: string
address: string
seen: boolean
}
nodes: {
nodeId: string
peerId: string
record: string
address: string
seen: boolean
}[]
}
codex: {
version: string
revision: string
}
};
export type CodexDebugInfo = CodexInfoResponse;

View File

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

View File

@ -2,6 +2,21 @@ import { CodexError } from "../errors/errors";
import { Promises } from "../promise-safe/promise-safe";
import { type SafeValue } from "../values/values";
export type FetchAuth = {
basic?: string;
};
export const FetchAuthBuilder = {
build(auth: FetchAuth | undefined) {
if (auth?.basic) {
return {
Authorization: "Basic " + auth.basic,
};
}
return {};
},
};
export const Fetch = {
async safe(url: string, init: RequestInit): Promise<SafeValue<Response>> {
const res = await Promises.safe(() => fetch(url, init));
@ -45,4 +60,14 @@ export const Fetch = {
return Promises.safe(() => res.data.json());
},
async safeText(url: string, init: RequestInit): Promise<SafeValue<string>> {
const res = await this.safe(url, init);
if (res.error) {
return res;
}
return Promises.safe(() => res.data.text());
},
};

View File

@ -2,6 +2,7 @@ import { CodexData } from "./data/data";
import { CodexNode } from "./node/node";
import { CodexMarketplace } from "./marketplace/marketplace";
import { CodexDebug } from "./debug/debug";
import type { FetchAuth } from "./fetch-safe/fetch-safe";
export * from "./fetch-safe/fetch-safe";
export * from "./marketplace/types";
@ -15,19 +16,28 @@ export { CodexData } from "./data/data";
export { CodexNode } from "./node/node";
export { CodexMarketplace } from "./marketplace/marketplace";
type CodexProps = {
auth?: FetchAuth;
};
export class Codex {
readonly url: string;
private _marketplace: CodexMarketplace | null;
private _data: CodexData | null;
private _node: CodexNode | null;
private _debug: CodexDebug | null;
private readonly auth: FetchAuth = {};
constructor(url: string) {
constructor(url: string, options?: CodexProps) {
this.url = url;
this._marketplace = null;
this._data = null;
this._node = null;
this._debug = null;
if (options?.auth) {
this.auth = options?.auth;
}
}
get marketplace() {
@ -35,7 +45,7 @@ export class Codex {
return this._marketplace;
}
this._marketplace = new CodexMarketplace(this.url);
this._marketplace = new CodexMarketplace(this.url, { auth: this.auth });
return this._marketplace;
}
@ -45,7 +55,7 @@ export class Codex {
return this._data;
}
this._data = new CodexData(this.url);
this._data = new CodexData(this.url, { auth: this.auth });
return this._data;
}
@ -55,7 +65,7 @@ export class Codex {
return this._node;
}
this._node = new CodexNode(this.url);
this._node = new CodexNode(this.url, { auth: this.auth });
return this._node;
}
@ -65,7 +75,7 @@ export class Codex {
return this._debug;
}
this._debug = new CodexDebug(this.url);
this._debug = new CodexDebug(this.url, { auth: this.auth });
return this._debug;
}

View File

@ -1,379 +1,276 @@
import { afterEach, assert, describe, it, vi } from "vitest";
import { Fetch } from "../fetch-safe/fetch-safe";
import { assert, describe, it } from "vitest";
import { CodexMarketplace } from "./marketplace";
import {
randomEthereumAddress,
randomInt,
randomString,
} from "../tests/tests.util";
import { CodexError } from "../errors/errors";
import { CodexData } from "../data/data";
import { NodeUploadStrategy } from "../data/node-upload";
import type {
CodexAvailabilityPatchInput,
CodexCreateAvailabilityInput,
CodexCreateStorageRequestInput,
} from "./types";
function createStorageRequest() {
return {
cid: randomString(64),
duration: randomInt(1, 64000),
pricePerBytePerSecond: randomInt(1, 100),
proofProbability: randomInt(1, 100),
nodes: randomInt(1, 5),
tolerance: randomInt(1, 100),
expiry: randomInt(1, 100),
collateralPerByte: randomInt(1, 100),
};
}
function missingNumberValidationError(field: string) {
return {
error: true as any,
data: new CodexError("Cannot validate the input", {
errors: [
{
path: field,
expected: "number",
message: "Invalid type: Expected number but received undefined",
received: "undefined",
},
],
}),
};
}
function extraValidationError(field: string, value: unknown) {
return {
error: true as any,
data: new CodexError("Cannot validate the input", {
errors: [
{
path: field,
expected: "never",
message: `Invalid type: Expected never but received "${value}"`,
received: `"${value}"`,
},
],
}),
};
}
function missingStringValidationError(field: string) {
return {
error: true as any,
data: new CodexError("Cannot validate the input", {
errors: [
{
path: field,
expected: "string",
message: "Invalid type: Expected string but received undefined",
received: "undefined",
},
],
}),
};
}
function mistypeNumberValidationError(field: string, value: string) {
return {
error: true as any,
data: new CodexError("Cannot validate the input", {
errors: [
{
path: field,
expected: "number",
message: `Invalid type: Expected number but received "${value}"`,
received: `"${value}"`,
},
],
}),
};
}
function minNumberValidationError(field: string, min: number) {
return {
error: true as any,
data: new CodexError("Cannot validate the input", {
errors: [
{
path: field,
expected: ">=" + min,
message: "Invalid value: Expected >=1 but received 0",
received: "0",
},
],
}),
};
}
function createAvailability() {
return {
id: randomEthereumAddress(),
totalSize: randomInt(0, 9).toString(),
duration: randomInt(0, 9).toString(),
minPrice: randomInt(0, 9).toString(),
maxCollateral: randomInt(0, 9).toString(),
};
}
describe("marketplace", () => {
const marketplace = new CodexMarketplace("http://localhost:3000");
afterEach(() => {
vi.restoreAllMocks();
});
it("returns an error when trying to create an availability without total size", async () => {
const response = await marketplace.createAvailability({
duration: 3000,
maxCollateral: 1,
minPrice: 100,
} as any);
assert.deepStrictEqual(response, missingNumberValidationError("totalSize"));
});
it("returns an error when trying to create an availability with an invalid number valid", async () => {
const response = await marketplace.createAvailability({
duration: 3000,
maxCollateral: 1,
minPrice: 100,
totalSize: "abc",
} as any);
assert.deepStrictEqual(
response,
mistypeNumberValidationError("totalSize", "abc")
describe("marketplace", async () => {
describe("availability", async () => {
const spMarketplace = new CodexMarketplace(
process.env["SP_URL"] || "http://localhost:8081"
);
});
const totalSize = 1_000_000;
const duration = 3000;
const minPricePerBytePerSecond = 1000;
const totalCollateral = 1_000_000_000;
it("returns an error when trying to create an availability with zero total size", async () => {
const response = await marketplace.createAvailability({
duration: 3000,
totalCollateral: 1,
minPricePerBytePerSecond: 100,
totalSize: 0,
const body = {
duration,
totalCollateral,
minPricePerBytePerSecond,
totalSize,
};
const result = await spMarketplace.createAvailability(body);
assert.ok(result.error == false);
const availability = result.data;
describe("create", async () => {
it("verifies that the availability was created successfully", async () => {
assert.ok(availability.id);
assert.equal(availability.duration, duration);
assert.equal(availability.freeSize, totalSize);
assert.equal(
availability.minPricePerBytePerSecond,
BigInt(minPricePerBytePerSecond)
);
assert.equal(availability.totalCollateral, BigInt(totalCollateral));
assert.equal(
availability.totalRemainingCollateral,
BigInt(totalCollateral)
);
assert.strictEqual(availability.totalSize, totalSize);
assert.strictEqual(availability.until, 0);
assert.ok(availability.enabled);
});
const errors: Partial<CodexCreateAvailabilityInput>[] = [
{ duration: 0 },
{ totalSize: 0 },
{ totalCollateral: -1 },
{ minPricePerBytePerSecond: -1 },
];
for (const err of errors) {
const field = Object.keys(err)[0] as keyof typeof err;
assert.ok(field);
it(`fails to create availability with wrong ${field} = ${err[field]}`, async () => {
const response = await spMarketplace.createAvailability({
...body,
[field]: err[field],
});
assert.ok(response.error);
assert.ok(response.data.errors?.length);
assert.equal(response.data.errors[0]?.path, field);
assert.equal(
response.data.errors[0]?.received,
err[field]?.toString()
);
assert.ok(response.data.errors[0]?.message.startsWith("Invalid"));
});
}
});
assert.deepStrictEqual(response, minNumberValidationError("totalSize", 1));
describe("update", async () => {
async function getUpdatedAvailability() {
const availabilities = await spMarketplace.availabilities();
assert.ok(availabilities.error == false);
return availabilities.data.find((a) => a.id == availability.id);
}
const updates: Omit<CodexAvailabilityPatchInput, "id">[] = [
{ enabled: false },
{ duration: 3000 },
{ minPricePerBytePerSecond: BigInt(1) },
{ totalSize: 3000 },
{ totalCollateral: BigInt(3000) },
{ until: 5000 },
];
for (const usecase of updates) {
const field = Object.keys(usecase)[0] as keyof typeof usecase;
assert.ok(field);
it(`updates availability's ${field}`, async () => {
const response = await spMarketplace.updateAvailability({
id: availability.id,
...usecase,
});
assert.ok(response.error == false);
const updated = await getUpdatedAvailability();
assert.ok(updated?.[field] == usecase[field]);
});
}
const errors: Omit<CodexAvailabilityPatchInput, "id">[] = [
{ duration: 0 },
{ totalSize: 0 },
{ totalCollateral: -1 },
{ minPricePerBytePerSecond: -1 },
{ until: -1 },
];
for (const err of errors) {
const field = Object.keys(err)[0] as keyof typeof err;
assert.ok(field);
it(`fails to update availability with wrong ${field}`, async () => {
const response = await spMarketplace.updateAvailability({
id: availability.id,
...err,
});
assert.ok(response.error);
assert.ok(response.data.errors?.length);
assert.equal(response.data.errors[0]?.path, field);
assert.equal(
response.data.errors[0]?.received,
err[field]?.toString()
);
assert.ok(response.data.errors[0]?.message.startsWith("Invalid"));
});
}
});
});
it("returns an error when trying to create an availability without duration", async () => {
const response = await marketplace.createAvailability({
totalSize: 3000,
maxCollateral: 1,
minPrice: 100,
} as any);
const data = new CodexData(
process.env["CLIENT_URL"] || "http://localhost:8080"
);
const marketplace = new CodexMarketplace(
process.env["CLIENT_URL"] || "http://localhost:8080"
);
assert.deepStrictEqual(response, missingNumberValidationError("duration"));
});
async function uploadContent(sizeInBytes: number) {
const content = "a".repeat(sizeInBytes);
const strategy = new NodeUploadStrategy(content);
const res = data.upload(strategy);
const cid = await res.result;
assert.ok(cid.error == false);
assert.ok(cid.data);
return cid.data;
}
it("returns an error when trying to create an availability with zero duration", async () => {
const response = await marketplace.createAvailability({
duration: 0,
totalCollateral: 1,
minPricePerBytePerSecond: 100,
totalSize: 3000,
async function createStorageRequestBody(targetSizeInBytes = 131072) {
return {
cid: await uploadContent(targetSizeInBytes),
duration: 1000,
pricePerBytePerSecond: 1,
proofProbability: 1,
expiry: 900,
collateralPerByte: 1,
nodes: 3,
tolerance: 1,
};
}
describe("storage request", async () => {
const body = await createStorageRequestBody();
it("creates successfully", async () => {
const request = await marketplace.createStorageRequest(body);
assert.ok(request.error == false);
assert.ok(request.data);
});
assert.deepStrictEqual(response, minNumberValidationError("duration", 1));
const errors: {
request: Partial<CodexCreateStorageRequestInput>;
message: string;
}[] = [
{ request: { cid: "" }, message: "Incorrect Cid" },
{
request: { duration: 0 },
message: "Cannot validate the input",
},
{
request: { pricePerBytePerSecond: 0 },
message: "Cannot validate the input",
},
{
request: { proofProbability: 0 },
message: "Cannot validate the input",
},
{
request: { expiry: 0 },
message: "Cannot validate the input",
},
{
request: { collateralPerByte: 0 },
message: "Cannot validate the input",
},
{
request: { tolerance: 0 },
message: "Cannot validate the input",
},
{
request: { cid: await uploadContent(1) },
message:
"Dataset too small for erasure parameters, need at least 131072 bytes",
},
{
request: { duration: 3000, expiry: 4000 },
message:
"Expiry must be greater than zero and less than the request's duration",
},
{
request: { nodes: 2, tolerance: 1 },
message:
"Invalid parameters: parameters must satify `1 < (nodes - tolerance) ≥ tolerance`",
},
];
for (const err of errors) {
it(`fails to create storage request with wrong ${JSON.stringify(err.request)}`, async () => {
const request = await marketplace.createStorageRequest({
...body,
...err.request,
});
assert.ok(request.error);
assert.ok(request.data.message.includes(err.message));
if (request.data.errors?.length) {
const keys = Object.keys(err.request);
for (const e of request.data.errors) {
assert.ok(e.path);
assert.ok(keys.includes(e.path));
}
}
});
}
});
it("returns an error when trying to create an availability without min price", async () => {
const response = await marketplace.createAvailability({
totalSize: 3000,
maxCollateral: 1,
duration: 100,
} as any);
describe("purchases", async () => {
const body = await createStorageRequestBody();
assert.deepStrictEqual(response, missingNumberValidationError("minPrice"));
});
const request = await marketplace.createStorageRequest(body);
assert.ok(request.error == false);
assert.ok(request.data);
it("returns an error when trying to create an availability without max collateral", async () => {
const response = await marketplace.createAvailability({
totalSize: 3000,
minPrice: 100,
duration: 100,
} as any);
it("lists successfully", async () => {
const ids = await marketplace.purchaseIds();
assert.deepStrictEqual(
response,
missingNumberValidationError("maxCollateral")
);
});
assert.ok(ids.error == false);
assert.ok(ids.data.length);
assert.ok(ids.data[0]);
it("returns an error when trying to create an availability with an extra field", async () => {
const response = await marketplace.createAvailability({
maxCollateral: 1,
totalSize: 3000,
minPrice: 100,
duration: 100,
hello: "world",
} as any);
const purchase = await marketplace.purchaseDetail(ids.data[0]);
assert.ok(purchase.error == false);
assert.ok(purchase.data.requestId);
assert.ok(purchase.data.state);
assert.deepStrictEqual(response, extraValidationError("hello", "world"));
});
it("returns a response when the request succeed", async () => {
const data = { ...createAvailability(), freeSize: "1000" };
const spy = vi.spyOn(Fetch, "safeJson");
spy.mockImplementationOnce(() => Promise.resolve({ error: false, data }));
const response = await marketplace.createAvailability({
totalCollateral: 1,
totalSize: 3000,
minPricePerBytePerSecond: 100,
duration: 100,
const purchases = await marketplace.purchases();
assert.ok(purchases.error == false);
assert.ok(purchases.data.length);
assert.ok(purchases.data[0]?.requestId);
assert.ok(purchases.data[0]?.state);
});
assert.ok(!response.error);
// @ts-ignore
assert.deepEqual(response.data, data);
});
it("returns a response when the create availability succeed", async () => {
const data = { ...createAvailability(), freeSize: "1000" };
const spy = vi.spyOn(Fetch, "safeJson");
spy.mockImplementationOnce(() => Promise.resolve({ error: false, data }));
const response = await marketplace.createAvailability({
totalCollateral: 1,
totalSize: 3000,
minPricePerBytePerSecond: 100,
duration: 100,
});
assert.ok(!response.error);
// @ts-ignore
assert.deepEqual(response.data, data);
});
it("returns an error when trying to update an availability without id", async () => {
const response = await marketplace.updateAvailability({
totalCollateral: 1,
totalSize: 3000,
minPricePerBytePerSecond: 100,
duration: 100,
} as any);
assert.deepStrictEqual(response, missingStringValidationError("id"));
});
it("returns an error when trying to update an availability with zero total size", async () => {
const response = await marketplace.updateAvailability({
id: randomString(64),
totalSize: 0,
minPricePerBytePerSecond: 100,
duration: 100,
totalCollateral: 100,
});
assert.deepStrictEqual(response, minNumberValidationError("totalSize", 1));
});
it("returns an error when trying to update an availability with zero duration", async () => {
const response = await marketplace.updateAvailability({
id: randomString(64),
totalSize: 100,
duration: 0,
minPricePerBytePerSecond: 100,
totalCollateral: 100,
});
assert.deepStrictEqual(response, minNumberValidationError("duration", 1));
});
it("returns a response when the update availability succeed", async () => {
const mockResponse = {
ok: true,
status: 200,
} as any;
globalThis.fetch = vi.fn().mockResolvedValue(mockResponse);
const response = await marketplace.updateAvailability({
id: randomString(64),
totalSize: 3000,
duration: 10,
minPricePerBytePerSecond: 100,
totalCollateral: 100,
});
assert.ok(!response.error);
});
it("returns an error when trying to create a storage request without cid", async () => {
const { cid, ...rest } = createStorageRequest();
const response = await marketplace.createStorageRequest(rest as any);
assert.deepStrictEqual(response, missingStringValidationError("cid"));
});
it("returns an error when trying to create a storage request without duration", async () => {
const { duration, ...rest } = createStorageRequest();
const response = await marketplace.createStorageRequest(rest as any);
assert.deepStrictEqual(response, missingNumberValidationError("duration"));
});
it("returns an error when trying to create a storage request with zero duration", async () => {
const { duration, ...rest } = createStorageRequest();
const response = await marketplace.createStorageRequest({
...rest,
duration: 0,
});
assert.deepStrictEqual(response, minNumberValidationError("duration", 1));
});
it("returns an error when trying to create a storage request without pricePerBytePerSecond", async () => {
const { pricePerBytePerSecond, ...rest } = createStorageRequest();
const response = await marketplace.createStorageRequest(rest as any);
assert.deepStrictEqual(
response,
missingNumberValidationError("pricePerBytePerSecond")
);
});
it("returns an error when trying to create a storage request without proof probability", async () => {
const { proofProbability, ...rest } = createStorageRequest();
const response = await marketplace.createStorageRequest(rest as any);
assert.deepStrictEqual(
response,
missingNumberValidationError("proofProbability")
);
});
it("returns an error when trying to create a storage request without expiry", async () => {
const { expiry, ...rest } = createStorageRequest();
const response = await marketplace.createStorageRequest(rest as any);
assert.deepStrictEqual(response, missingNumberValidationError("expiry"));
});
it("returns an error when trying to create a storage request with zero expiry", async () => {
const { expiry, ...rest } = createStorageRequest();
const response = await marketplace.createStorageRequest({
...rest,
expiry: 0,
});
assert.deepStrictEqual(response, minNumberValidationError("expiry", 1));
});
it("returns an error when trying to create a storage request without collateralPerByte", async () => {
const { collateralPerByte, ...rest } = createStorageRequest();
const response = await marketplace.createStorageRequest(rest as any);
assert.deepStrictEqual(
response,
missingNumberValidationError("collateralPerByte")
);
});
});

View File

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

View File

@ -1,241 +1,169 @@
import type { components, paths } from "../openapi";
import * as v from "valibot";
export type CodexStorageRequest = {
id: string;
export type CodexSlotResponse =
paths["/sales/slots"]["get"]["responses"][200]["content"]["application/json"];
/**
* Address of Ethereum address
*/
client: string;
export type CodexSlot = CodexSlotResponse;
ask: {
/**
* Number of slots that the tequest want to have the content spread over.
*/
slots: number;
export type CodexSlotAgentResponse =
paths["/sales/slots/{slotId}"]["get"]["responses"][200]["content"]["application/json"];
/**
* Amount of storage per slot (in bytes) as decimal string.
*/
slotSize: string;
export type CodexSlotAgent = CodexSlotAgentResponse;
/**
* The duration of the storage request in seconds.
*/
duration: string;
export type CodexAvailabilityResponse =
paths["/sales/availability"]["get"]["responses"][200]["content"]["application/json"];
/**
* How often storage proofs are required as decimal string (in periods).
*/
proofProbability: string;
export type CodexAvailabilityWithoutTypes =
components["schemas"]["SalesAvailabilityREAD"];
/**
* The amount of tokens paid per byte per second per slot to hosts the client is willing to pay
*/
pricePerBytePerSecond: string;
/**
* Max slots that can be lost without data considered to be lost.
*/
maxSlotLoss: number;
};
content: {
/**
* Unique Content ID
*/
cid: string;
/**
* Erasure code parameters
*/
// erasure: {
/**
* Total number of chunks generated by the erasure code process.
*/
// totalChunks: number;
// };
/**
* Parameters for Proof of Retrievability
*/
// por: {
// u: string;
// publicKey: string;
// name: string;
// };
};
/* Number as decimal string that represents expiry threshold in seconds from when the Request is submitted.
* When the threshold is reached and the Request does not find requested amount of nodes to host the data, the Request is voided.
* The number of seconds can not be higher then the Request's duration itself.
*/
expiry: string;
/**
* Random data
*/
nonce: string;
export type CodexAvailability = Omit<
CodexAvailabilityWithoutTypes,
| "freeSize"
| "minPricePerBytePerSecond"
| "totalCollateral"
| "totalRemainingCollateral"
> & {
freeSize?: number;
minPricePerBytePerSecond: BigInt;
totalCollateral: BigInt;
totalRemainingCollateral: BigInt;
};
/**
* A storage slot is a portion of a storage contract that needs to be fulfilled
* by a host. To initiate a contract, all the slots must be filled.
*/
export type CodexSlot = {
id: string;
export type CodexAvailabilityCreateResponse =
paths["/sales/availability"]["post"]["responses"][201]["content"]["application/json"];
request: CodexStorageRequest;
/**
* The slot index as hexadecimal string
*/
slotIndex: "string";
};
/**
* Storage availability for sell.
*/
export type CodexAvailability = {
id: string;
/**
* Size of available storage in bytes
*/
totalSize: number;
/**
* Unused size of availability's storage in bytes as decimal string
*/
freeSize: number;
/**
* Maximum time the storage should be sold for (in seconds)
*/
duration: number;
/**
* Minimal price per byte per second paid (in amount of tokens) for the
* hosted request's slot for the request's duration as decimal string
*/
minPricePerBytePerSecond: number;
/**
* Total collateral (in amount of tokens) that can be used for matching requests
*/
totalCollateral: number;
totalRemainingCollateral: number;
};
/**
* Storage availability received from the api.
*/
export type CodexAvailabilityDto = {
id: string;
/**
* Size of available storage in bytes
*/
totalSize: string;
/**
* Unused size of availability's storage in bytes as decimal string
*/
freeSize: string;
/**
* Maximum time the storage should be sold for (in seconds)
*/
duration: string;
/**
* Minimal price per byte per second paid (in amount of tokens) for the
* hosted request's slot for the request's duration as decimal string
*/
minPricePerBytePerSecond: string;
/**
* Total collateral (in amount of tokens) that can be used for matching requests
*/
totalCollateral: string;
totalRemainingCollateral: string;
};
export type CodexAvailabilityCreateResponse = CodexAvailability & {
id: string;
/**
* Unused size of availability's storage in bytes as decimal string
*/
freeSize: string;
};
export type CodexAvailabilityCreateBody = Exclude<
paths["/sales/availability"]["post"]["requestBody"],
undefined
>["content"]["application/json"];
export const CodexCreateAvailabilityInput = v.strictObject({
totalSize: v.pipe(v.number(), v.minValue(1)),
duration: v.pipe(v.number(), v.minValue(1)),
minPricePerBytePerSecond: v.number(),
totalCollateral: v.number(),
minPricePerBytePerSecond: v.union([
v.pipe(v.bigint(), v.minValue(BigInt(0))),
v.pipe(
v.number(),
v.minValue(0),
v.transform((input) => BigInt(input))
),
]),
totalCollateral: v.union([
v.pipe(v.bigint(), v.minValue(BigInt(0))),
v.pipe(
v.number(),
v.minValue(0),
v.transform((input) => BigInt(input))
),
]),
enabled: v.optional(v.boolean()),
until: v.optional(v.pipe(v.number(), v.minValue(0))),
});
export type CodexCreateAvailabilityInput = v.InferOutput<
typeof CodexCreateAvailabilityInput
export type CodexAvailabilityPatchResponse =
paths["/sales/availability/{id}"]["patch"]["responses"][204]["content"];
export type CodexAvailabilityPatchBody = Partial<
Exclude<
paths["/sales/availability"]["post"]["requestBody"],
undefined
>["content"]["application/json"]
>;
export const CodexUpdateAvailabilityInput = v.strictObject({
export type CodexCreateAvailabilityInput = Omit<
v.InferOutput<typeof CodexCreateAvailabilityInput>,
"minPricePerBytePerSecond" | "totalCollateral"
> & {
minPricePerBytePerSecond?: number | BigInt;
totalCollateral?: number | BigInt;
};
export const CodexAvailabilityPatchInput = v.strictObject({
id: v.string(),
totalSize: v.pipe(v.number(), v.minValue(1)),
duration: v.pipe(v.number(), v.minValue(1)),
minPricePerBytePerSecond: v.number(),
totalCollateral: v.number(),
totalSize: v.optional(v.pipe(v.number(), v.minValue(1))),
duration: v.optional(v.pipe(v.number(), v.minValue(1))),
minPricePerBytePerSecond: v.optional(
v.union([
v.pipe(v.bigint(), v.minValue(BigInt(0))),
v.pipe(
v.number(),
v.minValue(0),
v.transform((input) => BigInt(input))
),
])
),
totalCollateral: v.optional(
v.union([
v.pipe(v.bigint(), v.minValue(BigInt(0))),
v.pipe(
v.number(),
v.minValue(0),
v.transform((input) => BigInt(input))
),
])
),
enabled: v.optional(v.boolean()),
until: v.optional(v.pipe(v.number(), v.minValue(0))),
});
export type CodexUpdateAvailabilityInput = v.InferOutput<
typeof CodexUpdateAvailabilityInput
>;
export type CodexReservation = {
id: string;
availabilityId: string;
requestId: string;
/**
* Size in bytes
*/
size: string;
/**
* Slot Index as hexadecimal string
*/
slotIndex: string;
export type CodexAvailabilityPatchInput = Omit<
v.InferOutput<typeof CodexAvailabilityPatchInput>,
"minPricePerBytePerSecond" | "totalCollateral"
> & {
minPricePerBytePerSecond?: number | BigInt;
totalCollateral?: number | BigInt;
};
export type CodexPurchase = {
/**
* Description of the request's state
*/
state: string;
export type CodexReservationsResponse =
paths["/sales/availability/{id}/reservations"]["get"]["responses"][200]["content"]["application/json"];
/**
* If request failed, then here is presented the error message
*/
error: string;
export type CodexReservation = components["schemas"]["Reservation"];
request: CodexStorageRequest;
export type CodexPurchaseIdsResponse =
paths["/storage/purchases"]["get"]["responses"][200]["content"]["application/json"];
requestId: string;
export type CodexPurchaseResponse =
paths["/storage/purchases/{id}"]["get"]["responses"][200]["content"]["application/json"];
export type CodexStorageAsk = Omit<
components["schemas"]["StorageAsk"],
"slotSize" | "duration" | "proofProbability" | "pricePerBytePerSecond"
> & {
slotSize: number;
duration: number;
proofProbability: number;
pricePerBytePerSecond: number;
};
export type CodexPurchaseWithoutTypes = components["schemas"]["Purchase"];
export type CodexPurchase = Omit<
components["schemas"]["Purchase"],
"request"
> & {
request?: Omit<components["schemas"]["StorageRequest"], "ask"> & {
ask: CodexStorageAsk;
};
};
export type CodexStorageRequestResponse =
paths["/storage/request/{cid}"]["post"]["responses"][200]["content"]["text/plain"];
export type CodexStorageRequestCreateBody = Exclude<
paths["/storage/request/{cid}"]["post"]["requestBody"],
undefined
>["content"]["application/json"];
export const CodexCreateStorageRequestInput = v.strictObject({
cid: v.string(),
duration: v.pipe(v.number(), v.minValue(1)),
pricePerBytePerSecond: v.number(),
proofProbability: v.number(),
pricePerBytePerSecond: v.pipe(v.number(), v.minValue(1)),
proofProbability: v.pipe(v.number(), v.minValue(1)),
nodes: v.optional(v.number(), 1),
tolerance: v.optional(v.number(), 0),
tolerance: v.optional(v.pipe(v.number(), v.minValue(1)), 1),
expiry: v.pipe(v.number(), v.minValue(1)),
collateralPerByte: v.number(),
collateralPerByte: v.pipe(v.number(), v.minValue(1)),
});
export type CodexCreateStorageRequestInput = v.InferOutput<

1
src/node.ts Normal file
View File

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

36
src/node/node.spec.ts Normal file
View File

@ -0,0 +1,36 @@
import { assert, describe, expect, it, vi } from "vitest";
import { CodexNode } from "./node";
import { Fetch } from "../fetch-safe/fetch-safe";
describe("node", () => {
const clientUrl = process.env["CLIENT_URL"] || "http://localhost:8080";
const node = new CodexNode(clientUrl);
it("gets the json spr", async () => {
const spr = await node.spr("json");
assert.ok(spr.error == false);
assert.ok(spr.data);
});
it("gets the text spr", async () => {
const spr = await node.spr("text");
assert.ok(spr.error == false);
assert.ok(spr.data);
});
it("connects to a peer", async () => {
const spy = vi.spyOn(Fetch, "safeText");
spy.mockImplementationOnce(() =>
Promise.resolve({ error: false, data: "" })
);
await node.connect("1234", ["5678"]);
expect(spy).toHaveBeenCalledWith(
clientUrl + "/api/codex/v1/connect/1234?addrs=5678",
{
headers: {},
method: "GET",
}
);
});
});

View File

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

View File

@ -1,3 +1,29 @@
export type CodexSpr = {
spr: string;
};
import type { paths } from "../openapi";
export type CodexSprTextResponse =
paths["/spr"]["get"]["responses"][200]["content"]["text/plain"];
export type CodexSprJsonResponse =
paths["/spr"]["get"]["responses"][200]["content"]["application/json"];
export type CodexSprContentType = "json" | "text";
export type CodexSpr<T extends CodexSprContentType> = T extends "json"
? CodexSprJsonResponse
: T extends "text"
? CodexSprTextResponse
: never;
export type CodexPeerIdTextResponse =
paths["/peerid"]["get"]["responses"][200]["content"]["text/plain"];
export type CodexPeerIdJsonResponse =
paths["/peerid"]["get"]["responses"][200]["content"]["application/json"];
export type CodexPeerIdContentType = "json" | "text";
export type CodexPeerId<T extends CodexPeerIdContentType> = T extends "json"
? CodexPeerIdJsonResponse
: T extends "text"
? CodexPeerIdTextResponse
: never;

1496
src/openapi.ts Normal file

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 "../async";
import { CodexError } from "../errors/errors";
describe("promise safe", () => {
it("returns an error when the promise failed", async () => {

View File

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

View File

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

9
vitest.config.js Normal file
View File

@ -0,0 +1,9 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// run tests sequentially, not in parallel
// number of workers set to 1 disables parallelism
maxThreads: 1,
},
});