Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/vana-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,63 @@ const decrypted = await ecies.decrypt(recipientPrivateKey, encrypted);

The browser entry exposes the same surface as `BrowserECIESProvider`.

### Sealed job result format v2

Job result objects use a hybrid envelope so the enclave can encrypt large bodies
without retaining a plaintext-sized ECIES intermediate. The Node and browser
entries export `sealJobResultStream` and `openJobResultStream`; the existing
buffered `sealJobResult` and `openJobResult` APIs drive the same format for
callers that already hold the complete body.

All integers below are unsigned big-endian. The stored object is:

```text
uint8 formatVersion (= 2)
uint32 wrappedKeyLength
wrappedKey[wrappedKeyLength]
frame[0] || frame[1] || ... || finalFrame
```

`wrappedKey` uses the existing ECIES wire encoding
`iv(16) || ephemeralPublicKey(65) || ciphertext || mac(32)`. Its authenticated
plaintext is:

```text
contentKey(32) || noncePrefix(12) || uint32 metadataLength || metadata JSON
```

The metadata JSON is canonical (recursively key-sorted), capped at 4 KiB, and
contains exactly `v`, `jobId`, `scope`, `version`, and `contentType`. Each body
frame is:

```text
uint32 encryptedLength || uint8 flags || AES-GCM ciphertext || tag(16)
```

The only defined flag is bit value `0x01`, which marks the final frame.
Non-final plaintext chunks are exactly 1 MiB; the final chunk is 0 through
1 MiB, so an empty result still has one authenticated final frame. AES-256-GCM
uses the wrapped content key. A chunk nonce is the 12-byte nonce prefix with
its final big-endian uint32 XORed with the zero-based chunk index.

Every frame authenticates this AAD:

```text
uint8 formatVersion
|| uint32 metadataLength
|| metadata JSON
|| uint32 chunkIndex
|| uint8 flags
|| uint32 plaintextLength
```

The metadata, index, final marker, and length therefore cannot be changed, and
frames cannot be reordered, omitted, or appended without authentication
failing. The streaming sealer retains only fixed-size chunk buffers regardless
of total payload size; a local 50 MiB probe measured about 5 MiB of peak
`arrayBuffers` growth, versus the payload-sized plaintext, ciphertext, and
ECIES intermediates required by the previous buffered format.

### Upload a file via the storage manager

```typescript
Expand Down
31 changes: 31 additions & 0 deletions packages/vana-sdk/scripts/bundle-entry-points.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
import { build } from "esbuild";
import { dirname, relative, resolve, sep } from "node:path";
import type { Plugin } from "esbuild";

const sourceRoot = resolve("src");

function sharedErrorsPlugin(extension: ".js" | ".cjs"): Plugin {
return {
name: "shared-errors",
setup(build) {
build.onResolve(
{ filter: /^(?:\.\.?\/)+(?:[^/]+\/)*errors$/ },
(args) => {
const sourcePath = resolve(dirname(args.importer), args.path);
const outputSubpath = relative(sourceRoot, sourcePath)
.split(sep)
.join("/");
if (outputSubpath.startsWith("../")) {
throw new Error(`Error module is outside src: ${sourcePath}`);
}
return {
path: `./${outputSubpath}${extension}`,
external: true,
};
},
);
},
};
}

await build({
entryPoints: ["src/index.node.ts"],
Expand All @@ -9,6 +37,7 @@ await build({
format: "esm",
sourcemap: true,
packages: "external",
plugins: [sharedErrorsPlugin(".js")],
});

await build({
Expand All @@ -20,6 +49,7 @@ await build({
format: "cjs",
sourcemap: true,
packages: "external",
plugins: [sharedErrorsPlugin(".cjs")],
});

await build({
Expand All @@ -32,6 +62,7 @@ await build({
sourcemap: true,
packages: "external",
external: ["crypto", "secp256k1"],
plugins: [sharedErrorsPlugin(".js")],
define: {
"process.browser": "true",
"process.env.NODE_ENV": JSON.stringify("production"),
Expand Down
48 changes: 31 additions & 17 deletions packages/vana-sdk/scripts/fix-esm-import-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,26 @@ import { dirname, extname, join, relative, resolve } from "node:path";
const distDir = resolve(process.cwd(), "dist");
const specifierPattern =
/(\bfrom\s*["']|import\s*\(\s*["'])(\.{1,2}\/[^"']+)(["'])/g;
const cjsErrorsSpecifierPattern =
/(\brequire\(\s*["'])(\.{1,2}\/(?:\.{1,2}\/)*errors)(["']\s*\))/g;

function collectEsmFiles(dir: string): string[] {
function collectModuleFiles(dir: string): string[] {
const files: string[] = [];

for (const entry of readdirSync(dir)) {
const path = join(dir, entry);
const stat = statSync(path);

if (stat.isDirectory()) {
files.push(...collectEsmFiles(path));
files.push(...collectModuleFiles(path));
continue;
}

if (path.endsWith(".js") || path.endsWith(".d.ts")) {
if (
path.endsWith(".js") ||
path.endsWith(".cjs") ||
path.endsWith(".d.ts")
) {
files.push(path);
}
}
Expand Down Expand Up @@ -63,22 +69,30 @@ if (!existsSync(distDir)) {
let filesChanged = 0;
let importsChanged = 0;

for (const file of collectEsmFiles(distDir)) {
for (const file of collectModuleFiles(distDir)) {
const original = readFileSync(file, "utf8");
let fileImportsChanged = 0;

const updated = original.replace(
specifierPattern,
(match, prefix: string, specifier: string, suffix: string) => {
const target = resolveEsmTarget(file, specifier);
if (!target) {
return match;
}

fileImportsChanged += 1;
return `${prefix}${target}${suffix}`;
},
);
const updated = file.endsWith(".cjs")
? original.replace(
cjsErrorsSpecifierPattern,
(_match, prefix: string, specifier: string, suffix: string) => {
fileImportsChanged += 1;
return `${prefix}${specifier}.cjs${suffix}`;
},
)
: original.replace(
specifierPattern,
(match, prefix: string, specifier: string, suffix: string) => {
const target = resolveEsmTarget(file, specifier);
if (!target) {
return match;
}

fileImportsChanged += 1;
return `${prefix}${target}${suffix}`;
},
);

if (updated !== original) {
writeFileSync(file, updated);
Expand All @@ -88,5 +102,5 @@ for (const file of collectEsmFiles(distDir)) {
}

console.log(
`Fixed ${importsChanged} ESM import specifier${importsChanged === 1 ? "" : "s"} in ${filesChanged} file${filesChanged === 1 ? "" : "s"} under ${relative(process.cwd(), distDir)}`,
`Fixed ${importsChanged} module specifier${importsChanged === 1 ? "" : "s"} in ${filesChanged} file${filesChanged === 1 ? "" : "s"} under ${relative(process.cwd(), distDir)}`,
);
30 changes: 29 additions & 1 deletion packages/vana-sdk/scripts/validate-package-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ function validateTypeScriptConsumer(consumerDir: string): void {
join(consumerDir, "index.ts"),
[
'import { createSessionRelayBuilderClient, SessionRelayError, type SessionRelayInitResult } from "@opendatalabs/vana-sdk/session-relay";',
'import { buildEscrowPaymentHeader, type EscrowPaymentConfig, type EscrowPaymentHeaderConfig, type SignTypedDataFn } from "@opendatalabs/vana-sdk/server";',
'import { buildEscrowPaymentHeader, type EscrowPaymentConfig, type EscrowPaymentHeaderConfig, type PersonalServerPaymentOperation, type SignTypedDataFn } from "@opendatalabs/vana-sdk/server";',
'import { buildWithdrawAuthorizationTypedData, createEscrowGatewayClient, EscrowWithdrawalLifecycleError, EscrowWithdrawalRejectionError, type EscrowWithdrawalResult } from "@opendatalabs/vana-sdk/node";',
'import { buildEscrowPaymentHeader as buildDirectEscrowPaymentHeader } from "@opendatalabs/vana-sdk/direct/escrow-payment";',
'import { readPersonalServerData } from "@opendatalabs/vana-sdk/direct/personal-server-read";',
"",
Expand Down Expand Up @@ -123,6 +124,14 @@ function validateTypeScriptConsumer(consumerDir: string): void {
" legacyConfig;",
"void headerOnlyInput;",
"void legacyInput;",
"const paymentOperation = {} as PersonalServerPaymentOperation;",
"const withdrawalResult = {} as EscrowWithdrawalResult;",
"void paymentOperation;",
"void withdrawalResult;",
"void buildWithdrawAuthorizationTypedData;",
"void createEscrowGatewayClient;",
"void EscrowWithdrawalLifecycleError;",
"void EscrowWithdrawalRejectionError;",
"void buildDirectEscrowPaymentHeader;",
"void readPersonalServerData;",
"",
Expand Down Expand Up @@ -173,6 +182,25 @@ try {
console.log(`✓ ${specifier}`);
}

run(
"node",
[
"--input-type=module",
"-e",
'const sdk = await import("@opendatalabs/vana-sdk/server"); if (typeof sdk.buildEscrowPaymentHeader !== "function") throw new Error("Server entry point is missing buildEscrowPaymentHeader");',
],
consumerDir,
);
run(
"node",
[
"--input-type=module",
"-e",
'const sdk = await import("@opendatalabs/vana-sdk/node"); for (const name of ["buildWithdrawAuthorizationTypedData", "createEscrowGatewayClient", "EscrowWithdrawalLifecycleError", "EscrowWithdrawalRejectionError"]) if (!(name in sdk)) throw new Error(`Node entry point is missing ${name}`);',
],
consumerDir,
);

for (const specifier of browserBlockedImports) {
validateBrowserBlockedImport(specifier, consumerDir);
}
Expand Down
Loading
Loading