@opendatalabs/vana-sdk
Version:
A TypeScript library for interacting with Vana Network smart contracts.
376 lines • 11.9 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var r2_exports = {};
__export(r2_exports, {
R2Storage: () => R2Storage
});
module.exports = __toCommonJS(r2_exports);
var import_hmac = require("@noble/hashes/hmac");
var import_sha2 = require("@noble/hashes/sha2");
var import__ = require("../index");
const SERVICE = "s3";
const DEFAULT_REGION = "auto";
class R2Storage {
constructor(config) {
this.config = config;
if (!config.accessKeyId) {
throw new import__.StorageError(
"R2 accessKeyId is required",
"MISSING_ACCESS_KEY",
"r2"
);
}
if (!config.secretAccessKey) {
throw new import__.StorageError(
"R2 secretAccessKey is required",
"MISSING_SECRET_KEY",
"r2"
);
}
if (!config.bucket) {
throw new import__.StorageError("R2 bucket is required", "MISSING_BUCKET", "r2");
}
if (!config.endpoint && !config.accountId) {
throw new import__.StorageError(
"R2 endpoint or accountId is required",
"MISSING_ENDPOINT",
"r2"
);
}
if (!config.publicUrl && !config.accountId) {
throw new import__.StorageError(
"R2 publicUrl is required when accountId is not provided",
"MISSING_PUBLIC_URL",
"r2"
);
}
this.bucket = config.bucket;
this.region = config.region ?? DEFAULT_REGION;
this.endpoint = (config.endpoint ?? `https://${config.accountId}.r2.cloudflarestorage.com`).replace(/\/$/, "");
this.publicUrl = (config.publicUrl ?? `https://${config.bucket}.${config.accountId}.r2.dev`).replace(/\/$/, "");
}
config;
endpoint;
publicUrl;
region;
bucket;
async upload(file, filename) {
const key = filename ?? `vana-${Date.now()}-${randomSuffix()}.dat`;
const body = new Uint8Array(await file.arrayBuffer());
const contentType = file.type !== "" ? file.type : "application/octet-stream";
try {
const signed = await this.signRequest({
method: "PUT",
path: `/${this.bucket}/${encodePath(key)}`,
body,
extraHeaders: { "content-type": contentType }
});
const response = await fetch(signed.url, {
method: "PUT",
headers: signed.headers,
body
});
if (!response.ok) {
throw new import__.StorageError(
`R2 upload failed: ${response.status} ${response.statusText} - ${await safeText(response)}`,
"UPLOAD_FAILED",
"r2"
);
}
return {
url: `${this.publicUrl}/${encodePath(key)}`,
size: file.size,
contentType
};
} catch (error) {
throw wrapError(error, "UPLOAD_ERROR", "r2");
}
}
async download(url) {
const key = this.extractKey(url);
if (!key) {
throw new import__.StorageError(
`Could not extract object key from URL: ${url}`,
"INVALID_URL",
"r2"
);
}
try {
const signed = await this.signRequest({
method: "GET",
path: `/${this.bucket}/${encodePath(key)}`
});
const response = await fetch(signed.url, {
method: "GET",
headers: signed.headers
});
if (!response.ok) {
throw new import__.StorageError(
`R2 download failed: ${response.status} ${response.statusText}`,
"DOWNLOAD_FAILED",
"r2"
);
}
return await response.blob();
} catch (error) {
throw wrapError(error, "DOWNLOAD_ERROR", "r2");
}
}
async list(options) {
try {
const params = new URLSearchParams({ "list-type": "2" });
if (options?.namePattern) {
params.set("prefix", options.namePattern);
}
if (options?.limit !== void 0) {
params.set("max-keys", String(options.limit));
}
if (options?.offset !== void 0) {
params.set("continuation-token", String(options.offset));
}
const signed = await this.signRequest({
method: "GET",
path: `/${this.bucket}`,
query: params
});
const response = await fetch(signed.url, {
method: "GET",
headers: signed.headers
});
if (!response.ok) {
throw new import__.StorageError(
`R2 list failed: ${response.status} ${response.statusText}`,
"LIST_FAILED",
"r2"
);
}
const xml = await response.text();
return parseListObjects(xml).map((obj) => ({
id: obj.key,
name: obj.key,
url: `${this.publicUrl}/${encodePath(obj.key)}`,
size: obj.size,
contentType: "application/octet-stream",
createdAt: obj.lastModified
}));
} catch (error) {
throw wrapError(error, "LIST_ERROR", "r2");
}
}
async delete(url) {
const key = this.extractKey(url);
if (!key) {
throw new import__.StorageError(
`Could not extract object key from URL: ${url}`,
"INVALID_URL",
"r2"
);
}
try {
const signed = await this.signRequest({
method: "DELETE",
path: `/${this.bucket}/${encodePath(key)}`
});
const response = await fetch(signed.url, {
method: "DELETE",
headers: signed.headers
});
if (!response.ok && response.status !== 204) {
throw new import__.StorageError(
`R2 delete failed: ${response.status} ${response.statusText}`,
"DELETE_FAILED",
"r2"
);
}
return true;
} catch (error) {
throw wrapError(error, "DELETE_ERROR", "r2");
}
}
getConfig() {
return {
name: "Cloudflare R2",
type: "r2",
requiresAuth: true,
features: {
upload: true,
download: true,
list: true,
delete: true
}
};
}
/**
* Extract the object key from a URL. Accepts public URLs minted by `upload()`
* as well as raw keys for callers that already track them out-of-band.
*
* @internal
*/
extractKey(urlOrKey) {
if (urlOrKey.startsWith(this.publicUrl + "/")) {
return decodeURIComponent(urlOrKey.slice(this.publicUrl.length + 1));
}
if (urlOrKey.startsWith(this.endpoint + "/")) {
const rest = urlOrKey.slice(this.endpoint.length + 1);
const slash = rest.indexOf("/");
return slash === -1 ? null : decodeURIComponent(rest.slice(slash + 1));
}
if (!urlOrKey.includes("://")) {
return urlOrKey;
}
return null;
}
async signRequest(req) {
const { host } = new URL(this.endpoint);
const now = /* @__PURE__ */ new Date();
const amzDate = formatAmzDate(now);
const dateStamp = amzDate.slice(0, 8);
const payloadHash = req.body ? toHex((0, import_sha2.sha256)(req.body)) : EMPTY_PAYLOAD_HASH;
const canonicalQuery = req.query ? canonicalizeQuery(req.query) : "";
const headers = {
host,
"x-amz-content-sha256": payloadHash,
"x-amz-date": amzDate,
...req.extraHeaders ?? {}
};
const sortedHeaderNames = Object.keys(headers).map((h) => h.toLowerCase()).sort();
const canonicalHeaders = sortedHeaderNames.map((h) => `${h}:${headers[h].trim().replace(/\s+/g, " ")}`).join("\n") + "\n";
const signedHeaders = sortedHeaderNames.join(";");
const canonicalRequest = [
req.method,
req.path,
canonicalQuery,
canonicalHeaders,
signedHeaders,
payloadHash
].join("\n");
const credentialScope = `${dateStamp}/${this.region}/${SERVICE}/aws4_request`;
const stringToSign = [
"AWS4-HMAC-SHA256",
amzDate,
credentialScope,
toHex((0, import_sha2.sha256)(new TextEncoder().encode(canonicalRequest)))
].join("\n");
const signingKey = deriveSigningKey(
this.config.secretAccessKey,
dateStamp,
this.region,
SERVICE
);
const signature = toHex(
(0, import_hmac.hmac)(import_sha2.sha256, signingKey, new TextEncoder().encode(stringToSign))
);
headers.authorization = `AWS4-HMAC-SHA256 Credential=${this.config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
const url = this.endpoint + req.path + (canonicalQuery !== "" ? `?${canonicalQuery}` : "");
return { url, headers };
}
}
const EMPTY_PAYLOAD_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
function deriveSigningKey(secret, dateStamp, region, service) {
const enc = new TextEncoder();
const kDate = (0, import_hmac.hmac)(
import_sha2.sha256,
enc.encode(`AWS4${secret}`),
enc.encode(dateStamp)
);
const kRegion = (0, import_hmac.hmac)(import_sha2.sha256, kDate, enc.encode(region));
const kService = (0, import_hmac.hmac)(import_sha2.sha256, kRegion, enc.encode(service));
return (0, import_hmac.hmac)(import_sha2.sha256, kService, enc.encode("aws4_request"));
}
function formatAmzDate(date) {
const iso = date.toISOString();
return iso.replace(/[-:]/g, "").replace(/\.\d+/, "");
}
function toHex(bytes) {
let s = "";
for (let i = 0; i < bytes.length; i++) {
s += bytes[i].toString(16).padStart(2, "0");
}
return s;
}
function rfc3986Encode(value) {
return encodeURIComponent(value).replace(
/[!'()*]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
);
}
function encodePath(key) {
return key.split("/").map(rfc3986Encode).join("/");
}
function canonicalizeQuery(params) {
const entries = [];
for (const [k, v] of params.entries()) {
entries.push([k, v]);
}
entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
return entries.map(([k, v]) => `${rfc3986Encode(k)}=${rfc3986Encode(v)}`).join("&");
}
function parseListObjects(xml) {
const out = [];
const contentsRe = /<Contents>([\s\S]*?)<\/Contents>/g;
let match;
while ((match = contentsRe.exec(xml)) !== null) {
const block = match[1];
const key = matchTag(block, "Key");
const size = matchTag(block, "Size");
const lastModified = matchTag(block, "LastModified");
if (key === null) {
continue;
}
out.push({
key: decodeXmlEntities(key),
size: size !== null ? parseInt(size, 10) || 0 : 0,
lastModified: lastModified !== null ? new Date(lastModified) : /* @__PURE__ */ new Date(0)
});
}
return out;
}
function matchTag(block, tag) {
const re = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`);
const m = re.exec(block);
return m === null ? null : m[1];
}
function decodeXmlEntities(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
}
function randomSuffix() {
return Math.random().toString(36).slice(2, 10);
}
async function safeText(response) {
try {
return await response.text();
} catch {
return "";
}
}
function wrapError(error, code, provider) {
if (error instanceof import__.StorageError) {
return error;
}
return new import__.StorageError(
`R2 error: ${error instanceof Error ? error.message : "Unknown error"}`,
code,
provider
);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
R2Storage
});
//# sourceMappingURL=r2.cjs.map