Support basic authentication

This commit is contained in:
Arnaud 2025-03-28 09:13:01 +01:00
parent 746f96279b
commit 9e89c30562
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
24 changed files with 303 additions and 48 deletions

View File

@ -62,6 +62,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.
@ -105,7 +119,7 @@ const codex = new Codex("http://localhost:3000");
const marketplace = await codex.marketplace();
// When using the sync api
const marketplace = codex.marketplace
const marketplace = codex.marketplace;
```
#### activeSlots()
@ -253,7 +267,7 @@ const codex = new Codex("http://localhost:3000");
const data = await codex.data();
// When using the sync api
const data = codex.data
const data = codex.data;
```
#### cids
@ -389,7 +403,7 @@ const codex = new Codex("http://localhost:3000");
const data = await codex.debug();
// When using the sync api
const data = codex.debug
const data = codex.debug;
```
#### setLogLevel
@ -427,7 +441,7 @@ const codex = new Codex("http://localhost:3000");
const node = await codex.node();
// When using the sync api
const node = codex.node
const node = codex.node;
```
#### spr

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

@ -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,
@ -10,11 +14,20 @@ import type {
UploadResponse,
} 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;
}
}
/**
@ -24,15 +37,16 @@ export class CodexData {
cids(): Promise<SafeValue<CodexDataResponse>> {
const url = this.url + Api.config.prefix + "/data";
return Fetch.safeJson<CodexDataResponse>(url, { method: "GET" }).then(
(data) => {
if (data.error) {
return data;
}
return { error: false, data: { content: data.data.content } };
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 } };
});
}
/**
@ -41,7 +55,10 @@ export class CodexData {
space() {
const url = this.url + Api.config.prefix + "/space";
return Fetch.safeJson<CodexNodeSpace>(url, { method: "GET" });
return Fetch.safeJson<CodexNodeSpace>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
@ -54,7 +71,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();
},
@ -68,7 +85,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),
});
}
/**
@ -80,7 +100,10 @@ export class CodexData {
): Promise<SafeValue<NetworkDownloadResponse>> {
const url = this.url + Api.config.prefix + `/data/${cid}/network`;
return Fetch.safeJson(url, { method: "POST" });
return Fetch.safeJson(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
});
}
/**
@ -90,7 +113,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),
});
}
/**
@ -100,6 +126,9 @@ export class CodexData {
async fetchManifest(cid: string) {
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,3 +1,4 @@
import type { FetchAuth } from "../fetch-safe/fetch-safe";
import type { SafeValue } from "../values/values";
export type CodexManifest = {
@ -82,7 +83,14 @@ export type UploadResponse = {
export type NetworkDownloadResponse = { cid: string; manifest: CodexManifest };
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,15 +1,28 @@
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 * 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;
}
}
/**
@ -35,6 +48,7 @@ export class CodexDebug {
const res = await Fetch.safe(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: "",
});
@ -53,6 +67,7 @@ export class CodexDebug {
return Fetch.safeJson<CodexDebugInfo>(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, { auth }: CodexProps) {
this.url = url;
this._marketplace = null;
this._data = null;
this._node = null;
this._debug = null;
if (auth) {
this.auth = 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 CodexAvailability,
@ -16,11 +20,20 @@ import {
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;
}
}
/**
@ -31,6 +44,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexSlot[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -42,6 +56,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexSlot>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -53,6 +68,7 @@ export class CodexMarketplace {
const res = await Fetch.safeJson<CodexAvailabilityDto[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
if (res.error) {
@ -96,6 +112,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexAvailabilityCreateResponse>(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({
totalSize: body.totalSize.toString(),
duration: body.duration.toString(),
@ -130,6 +147,7 @@ export class CodexMarketplace {
const res = await Fetch.safe(url, {
method: "PATCH",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({
totalSize: body.totalSize.toString(),
duration: body.duration.toString(),
@ -158,6 +176,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexReservation[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -169,6 +188,7 @@ export class CodexMarketplace {
return Fetch.safeJson<string[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -177,6 +197,7 @@ export class CodexMarketplace {
const res = await Fetch.safeJson<string[]>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
if (res.error) {
@ -215,6 +236,7 @@ export class CodexMarketplace {
return Fetch.safeJson<CodexPurchase>(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -249,6 +271,7 @@ export class CodexMarketplace {
const res = await Fetch.safe(url, {
method: "POST",
headers: FetchAuthBuilder.build(this.auth),
body: JSON.stringify({
duration: duration.toString(),
pricePerBytePerSecond: pricePerBytePerSecond.toString(),

View File

@ -1,13 +1,26 @@
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";
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;
}
}
/**
@ -26,6 +39,7 @@ export class CodexNode {
return Fetch.safe(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -37,6 +51,7 @@ export class CodexNode {
return Fetch.safeJson(url, {
method: "GET",
headers: FetchAuthBuilder.build(this.auth),
});
}
@ -49,6 +64,7 @@ export class CodexNode {
return Fetch.safe(url, {
method: "GET",
headers: 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 = {