Merge branch 'master' into chore/generate-types-from-open-api

This commit is contained in:
Arnaud 2025-04-02 16:55:52 +02:00
commit 0a0fed1955
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
27 changed files with 309 additions and 40 deletions

View File

@ -70,6 +70,20 @@ To use a module, you need to use the await syntax. If the module is not loaded y
const marketplace = await codex.marketplace();
```
### Authentication
You can use basic authentication when creating a new Codex object:
```js
const codex = new Codex("http://localhost:3000", {
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
The SDK provides a type called `SafeValue` for error handling instead of throwing errors. It is inspired by Go's "error as value" concept.

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();

View File

@ -1,24 +1,48 @@
{
"name": "@codex-storage/sdk-js-download-example",
"name": "@codex-storage/sdk-js-basic-auth-example",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@codex-storage/sdk-js-download-example",
"name": "@codex-storage/sdk-js-basic-auth-example",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@codex-storage/sdk-js": ".."
"@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": "..",
"resolved": "../..",
"link": true
},
"node_modules/@esbuild/aix-ppc64": {

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"
}
}

View File

@ -11,7 +11,7 @@ npm install
## Build the javascript asset
```bash
CODEX_CID=REPLACE_BY_YOUR_CIDE npm run build
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.

View File

@ -14,6 +14,9 @@ const options = {
outfile: "./index.bundle.js",
bundle: true,
define,
logOverride: {
"ignored-bare-import": "silent",
},
};
build(options).catch(() => process.exit(1));

View File

@ -9,7 +9,7 @@
"license": "ISC",
"description": "",
"dependencies": {
"@codex-storage/sdk-js": ".."
"@codex-storage/sdk-js": "../.."
},
"devDependencies": {
"esbuild": "^0.25.1",

View File

@ -11,7 +11,7 @@ npm install
## Build the javascript asset
```bash
CODEX_CID=REPLACE_BY_YOUR_CIDE npm run build
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.

View File

@ -39,6 +39,9 @@
},
"engines": {
"node": ">=20.18.1"
},
"peerDependencies": {
"undici": "^7.6.0"
}
},
"../dist": {
@ -74,4 +77,4 @@
}
}
}
}
}

View File

@ -13,4 +13,4 @@
"devDependencies": {
"prettier": "^3.5.3"
}
}
}

6
package-lock.json generated
View File

@ -24,6 +24,9 @@
},
"engines": {
"node": ">=20.18.1"
},
"peerDependencies": {
"undici": "^7.0.0"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
@ -3030,6 +3033,7 @@
"resolved": "https://registry.npmjs.org/undici/-/undici-7.7.0.tgz",
"integrity": "sha512-tZ6+5NBq4KH35rr46XJ2JPFKxfcBlYNaqLF/wyWIO9RMHqqU/gx/CLB1Y2qMcgB8lWw/bKHa7qzspqCN7mUHvA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.18.1"
}
@ -3473,4 +3477,4 @@
}
}
}
}
}

View File

@ -1,6 +1,6 @@
import { CodexError } from "../errors/errors";
import type { SafeValue } from "../values/values";
import type { UploadStategy } from "./types";
import type { UploadStategy, UploadStategyOptions } from "./types";
export class BrowserUploadStategy implements UploadStategy {
private readonly file: Document | XMLHttpRequestBodyInit;
@ -22,7 +22,10 @@ export class BrowserUploadStategy implements UploadStategy {
this.metadata = metadata;
}
download(url: string): Promise<SafeValue<string>> {
upload(
url: string,
{ auth }: UploadStategyOptions
): Promise<SafeValue<string>> {
const xhr = new XMLHttpRequest();
this.xhr = xhr;
@ -42,6 +45,10 @@ export class BrowserUploadStategy implements UploadStategy {
);
}
if (auth?.basic) {
xhr.setRequestHeader("Authorization", "Basic " + auth.basic);
}
if (this.metadata?.mimetype) {
xhr.setRequestHeader("Content-Type", this.metadata.mimetype);
}

View File

@ -1,5 +1,9 @@
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 {
CodexDataResponse,
@ -13,11 +17,20 @@ import type {
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;
}
}
/**
@ -27,7 +40,16 @@ export class CodexData {
cids(): Promise<SafeValue<CodexDataItems>> {
const url = this.url + Api.config.prefix + "/data";
return Fetch.safeJson<CodexDataResponse>(url, { method: "GET" });
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 } };
});
}
/**
@ -36,7 +58,10 @@ export class CodexData {
space(): Promise<SafeValue<CodexNodeSpace>> {
const url = this.url + Api.config.prefix + "/space";
return Fetch.safeJson<CodexSpaceResponse>(url, { method: "GET" });
return Fetch.safeJson<CodexSpaceResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
@ -49,7 +74,7 @@ export class CodexData {
const url = this.url + Api.config.prefix + "/data";
return {
result: stategy.download(url),
result: stategy.upload(url, { auth: this.auth }),
abort: () => {
stategy.abort();
},
@ -63,7 +88,10 @@ export class CodexData {
async localDownload(cid: string): Promise<SafeValue<Response>> {
const url = this.url + Api.config.prefix + "/data/" + cid;
return Fetch.safe(url, { method: "GET" });
return Fetch.safe(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
@ -73,7 +101,10 @@ export class CodexData {
async networkDownload(cid: string): Promise<SafeValue<CodexNetworkDownload>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network`;
return Fetch.safeJson<CodexDataNetworkResponse>(url, { method: "POST" });
return Fetch.safeJson<CodexDataNetworkResponse>(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
@ -83,7 +114,10 @@ export class CodexData {
async networkDownloadStream(cid: string): Promise<SafeValue<Response>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network/stream`;
return Fetch.safe(url, { method: "GET" });
return Fetch.safe(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
@ -93,6 +127,9 @@ export class CodexData {
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" });
return Fetch.safeJson<CodexManifest>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
}

View File

@ -3,7 +3,8 @@ import { CodexError } from "../errors/errors";
import type { SafeValue } from "../values/values";
import Undici from "undici";
import { type FormData } from "undici";
import type { UploadStategy } from "./types";
import type { UploadStategy, UploadStategyOptions } from "./types";
import { FetchAuthBuilder } from "../fetch-safe/fetch-safe";
export class NodeUploadStategy implements UploadStategy {
private readonly body:
@ -26,8 +27,11 @@ export class NodeUploadStategy implements UploadStategy {
this.metadata = metadata;
}
async download(url: string): Promise<SafeValue<string>> {
const headers: Record<string, string> = {};
async upload(
url: string,
{ auth }: UploadStategyOptions
): Promise<SafeValue<string>> {
const headers: Record<string, string> = FetchAuthBuilder.build(auth);
if (this.metadata?.filename) {
headers["Content-Disposition"] =

View File

@ -1,4 +1,5 @@
import type { components, paths } from "../openapi";
import type { FetchAuth } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values";
export type CodexDataResponse =
@ -28,7 +29,14 @@ export type CodexFetchManifestResponse =
export type CodexManifest = CodexFetchManifestResponse;
export type UploadStategyOptions = {
auth?: FetchAuth;
};
export interface UploadStategy {
download(url: string): Promise<SafeValue<string>>;
upload(
url: string,
options?: UploadStategyOptions
): Promise<SafeValue<string>>;
abort(): void;
}

View File

@ -1,6 +1,10 @@
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 {
CodexLogLevelInput,
@ -10,11 +14,20 @@ import {
} 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;
}
}
/**
@ -40,6 +53,7 @@ export class CodexDebug {
return Fetch.safeText(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: "",
});
}
@ -52,6 +66,7 @@ export class CodexDebug {
return Fetch.safeJson<CodexInfoResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
}

View File

@ -1,6 +1,6 @@
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", () => {
afterEach(() => {

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));

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,7 +1,11 @@
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,
@ -27,11 +31,20 @@ import {
CodexCreateStorageRequestInput,
} 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;
}
}
/**
@ -42,6 +55,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexSlotResponse[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -53,6 +67,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexSlotAgentResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -84,6 +99,7 @@ export class CodexMarketplace {
const res = await Fetch.safeJson<CodexAvailabilityResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
if (res.error) {
@ -133,6 +149,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexAvailabilityCreateResponse>(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify(body),
}).then((result) => {
if (result.error) {
@ -182,6 +199,7 @@ export class CodexMarketplace {
const res = await Fetch.safe(url, {
method: "PATCH",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify(body),
});
@ -205,6 +223,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexReservationsResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -216,6 +235,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexPurchaseIdsResponse>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -288,6 +308,7 @@ export class CodexMarketplace {
this.url + Api.config.prefix + `/storage/purchases/` + purchaseId;
return Fetch.safeJson<CodexPurchaseResponse>(url, {
headers: FetchAuthBuilder.build(this.auth),
method: "GET",
}).then((res) => {
if (res.error) {
@ -329,6 +350,7 @@ export class CodexMarketplace {
return Fetch.safeText(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({
duration: duration,
pricePerBytePerSecond: pricePerBytePerSecond.toString(),

View File

@ -1,5 +1,9 @@
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 {
CodexPeerId,
@ -10,11 +14,20 @@ import type {
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;
}
}
/**
@ -32,6 +45,7 @@ export class CodexNode {
return Fetch.safeText(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -56,6 +70,7 @@ export class CodexNode {
method: "GET",
headers: {
"Content-Type": "text/plain",
...FetchAuthBuilder.build(this.auth),
},
});
}
@ -81,6 +96,7 @@ export class CodexNode {
method: "GET",
headers: {
"Content-Type": "text/plain",
...FetchAuthBuilder.build(this.auth),
},
});
}

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 = {