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
2 changes: 1 addition & 1 deletion connect/src/app/admin/_lib/register-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const BUILDER_REGISTRATION_TYPES = {
],
} as const;

const GATEWAY_URL = "https://data-gateway.vana.org";
const GATEWAY_URL = process.env.DATA_GATEWAY_URL ?? "https://dp-rpc.vana.org";

export type RegisterBuilderErrorCode =
| "ALREADY_REGISTERED"
Expand Down
2 changes: 1 addition & 1 deletion connect/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type DownloadAsset = {
comingSoon?: boolean;
};

const DOWNLOAD_VERSION = "0.7.35";
const DOWNLOAD_VERSION = "0.7.52";
const DATA_CONNECT_GITHUB_RELEASES_URL =
"https://github.com/vana-com/data-connect/releases";

Expand Down
3 changes: 1 addition & 2 deletions connect/src/lib/server-provider/register-on-chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ const SERVER_REGISTRATION_TYPES = {
],
} as const;

const GATEWAY_URL =
process.env.DATA_GATEWAY_URL ?? "https://data-gateway.vana.org";
const GATEWAY_URL = process.env.DATA_GATEWAY_URL ?? "https://dp-rpc.vana.org";

export type RegisterServerErrorCode =
| "ALREADY_REGISTERED"
Expand Down
8 changes: 8 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,7 @@ async function runConnect(
let ingestFailureMessage: string | null = null;
let resultPath = getSourceResultPath(source);
let collectedResult = false;
let blockedByRequiredInput = false;
let ingestScopeResults:
| Array<{
scope: string;
Expand Down Expand Up @@ -1348,6 +1349,9 @@ async function runConnect(
}

if (event.type === "needs-input") {
if (options.noInput && !options.ipc) {
blockedByRequiredInput = true;
}
await updateSourceState(resolution.source, {
lastRunAt: new Date().toISOString(),
lastRunOutcome: CliOutcomeStatus.NEEDS_INPUT,
Expand Down Expand Up @@ -1448,6 +1452,10 @@ async function runConnect(
}

if (event.type === "collection-complete" && event.resultPath) {
if (blockedByRequiredInput) {
continue;
}

// Check if the result is actually an error object
try {
const raw = await fsp.readFile(event.resultPath, "utf8");
Expand Down
4 changes: 2 additions & 2 deletions src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ export type VanaEnvironment = "dev" | "prod";
export const ENV_CONFIG = {
dev: {
sessionRelayUrl: "https://dev.session-relay.vana.org",
gatewayUrl: "https://dev.data-gateway.vana.org",
gatewayUrl: "https://dp-rpc-dev.vana.org",
accountUrl: "https://account-dev.vana.org",
},
prod: {
sessionRelayUrl: "https://session-relay.vana.org",
gatewayUrl: "https://data-gateway.vana.org",
gatewayUrl: "https://dp-rpc.vana.org",
accountUrl: "https://account.vana.org",
},
} as const;
Expand Down
6 changes: 6 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ export interface DataFetchParams {
export interface RequestSignerConfig {
/** Builder private key in hex format. */
privateKey: `0x${string}`;
/**
* Body hash wire format.
* - `"legacy"`: bare sha256 hex, with `""` for empty bodies. Required by the current Session Relay.
* - `"prefixed"`: `sha256:<hex>`, including the canonical empty-body hash.
*/
bodyHashFormat?: "legacy" | "prefixed";
}

/** Configuration for {@link createSessionRelay}. */
Expand Down
5 changes: 4 additions & 1 deletion src/server/data-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export interface DataClient {
*/
export function createDataClient(config: DataClientConfig): DataClient {
const gatewayBase = config.gatewayUrl.replace(/\/+$/, "");
const signer = createRequestSigner({ privateKey: config.privateKey });
const signer = createRequestSigner({
privateKey: config.privateKey,
bodyHashFormat: "prefixed",
});

return {
async resolveServerUrl(userAddress: string): Promise<string> {
Expand Down
17 changes: 13 additions & 4 deletions src/server/request-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { createHash } from "node:crypto";
import { privateKeyToAccount } from "viem/accounts";
import type { RequestSignerConfig } from "../core/types.js";

const EMPTY_BODY_HASH =
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
type BodyHashFormat = NonNullable<RequestSignerConfig["bodyHashFormat"]>;

function base64urlEncode(input: string): string {
return Buffer.from(input, "utf-8")
.toString("base64")
Expand All @@ -21,14 +25,18 @@ function canonicalizeJson(obj: unknown): unknown {
return sorted;
}

function computeBodyHash(body?: string): string {
function computeBodyHash(
body: string | undefined,
format: BodyHashFormat,
): string {
if (!body || body.length === 0) {
return "";
return format === "prefixed" ? EMPTY_BODY_HASH : "";
}
const parsed = JSON.parse(body);
const canonical = canonicalizeJson(parsed);
const canonicalStr = JSON.stringify(canonical);
return createHash("sha256").update(canonicalStr).digest("hex");
const hash = createHash("sha256").update(canonicalStr).digest("hex");
return format === "prefixed" ? `sha256:${hash}` : hash;
}

/**
Expand Down Expand Up @@ -64,6 +72,7 @@ export function createRequestSigner(
config: RequestSignerConfig,
): RequestSigner {
const account = privateKeyToAccount(config.privateKey);
const bodyHashFormat = config.bodyHashFormat ?? "legacy";

return {
address: account.address,
Expand All @@ -73,7 +82,7 @@ export function createRequestSigner(

const payload: Record<string, unknown> = {
aud: params.aud,
bodyHash: computeBodyHash(params.body),
bodyHash: computeBodyHash(params.body, bodyHashFormat),
exp: now + 300,
iat: now,
method: params.method,
Expand Down
23 changes: 21 additions & 2 deletions test/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,12 +1070,12 @@ describe("runCli", () => {
account: {
address: "0x2ab394e4be7c43ac360d226a31e1c90bc01aafa1",
session_token: "vana_account_session",
expires_at: "2026-04-22T19:25:14.420Z",
expires_at: "2027-04-22T19:25:14.420Z",
},
personal_server: {
url: "https://0x2ab394e4be7c43ac360d226a31e1c90bc01aafa1.myvana.app",
session_token: "vana_ps_session",
expires_at: "2026-04-22T19:25:14.420Z",
expires_at: "2027-04-22T19:25:14.420Z",
},
});
}
Expand Down Expand Up @@ -1137,6 +1137,12 @@ describe("runCli", () => {
message: "Steam needs credentials",
fields: ["username", "password"],
},
{
type: "collection-complete",
source: "steam",
resultPath: "/tmp/.vana/steam-result.json",
logPath: "/tmp/logs/run.log",
},
];

const { runCli } = await import("../../src/cli/index.js");
Expand Down Expand Up @@ -1168,6 +1174,19 @@ describe("runCli", () => {
source: "steam",
}),
);
expect(lines).not.toContainEqual(
expect.objectContaining({
type: "outcome",
status: "connected_local_only",
source: "steam",
}),
);
expect(mockUpdateSourceState).not.toHaveBeenCalledWith(
"steam",
expect.objectContaining({
lastRunOutcome: "connected_local_only",
}),
);
});

it("shows a clear human message when input is required in no-input mode", async () => {
Expand Down
3 changes: 2 additions & 1 deletion test/core/constants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ describe("getEnvConfig", () => {
const config = getEnvConfig("dev");
expect(config).toBe(ENV_CONFIG.dev);
expect(config.sessionRelayUrl).toContain("session-relay");
expect(config.gatewayUrl).toContain("data-gateway");
expect(config.gatewayUrl).toBe("https://dp-rpc-dev.vana.org");
expect(config.accountUrl).toBe("https://account-dev.vana.org");
});

it("returns prod config for 'prod'", () => {
const config = getEnvConfig("prod");
expect(config).toBe(ENV_CONFIG.prod);
expect(config.gatewayUrl).toBe("https://dp-rpc.vana.org");
expect(config.accountUrl).toBe("https://account.vana.org");
});

Expand Down
4 changes: 4 additions & 0 deletions test/server/data-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe("createDataClient", () => {
Buffer.from(payloadBase64, "base64").toString("utf-8"),
);
expect(payload.grantId).toBe("grant-123");
expect(payload.bodyHash).toMatch(/^sha256:[0-9a-f]{64}$/);
});

it("includes query params for fileId and at", async () => {
Expand Down Expand Up @@ -174,6 +175,9 @@ describe("createDataClient", () => {
);
expect(payload.grantId).toBeUndefined();
expect(payload.uri).toBe("/v1/data");
expect(payload.bodyHash).toBe(
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
);
});
});

Expand Down
40 changes: 38 additions & 2 deletions test/server/request-signer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { privateKeyToAccount } from "viem/accounts";
const TEST_PRIVATE_KEY =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as const;
const TEST_ADDRESS = privateKeyToAccount(TEST_PRIVATE_KEY).address;
const PREFIXED_EMPTY_BODY_HASH =
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

describe("createRequestSigner", () => {
it("produces a Web3Signed header with correct format", async () => {
Expand Down Expand Up @@ -99,7 +101,7 @@ describe("createRequestSigner", () => {
expect(payload.grantId).toBeUndefined();
});

it("computes body hash for non-empty body", async () => {
it("computes legacy body hash by default for non-empty body", async () => {
const signer = createRequestSigner({ privateKey: TEST_PRIVATE_KEY });
const header = await signer.signRequest({
aud: "https://example.com",
Expand All @@ -117,7 +119,7 @@ describe("createRequestSigner", () => {
expect(payload.bodyHash).not.toBe("");
});

it("uses empty-body hash when no body provided", async () => {
it("uses legacy empty body hash by default when no body provided", async () => {
const signer = createRequestSigner({ privateKey: TEST_PRIVATE_KEY });
const header = await signer.signRequest({
aud: "https://example.com",
Expand All @@ -132,6 +134,40 @@ describe("createRequestSigner", () => {
expect(payload.bodyHash).toBe("");
});

it("supports sha256-prefixed body hashes when requested", async () => {
const signer = createRequestSigner({
privateKey: TEST_PRIVATE_KEY,
bodyHashFormat: "prefixed",
});

const nonEmptyHeader = await signer.signRequest({
aud: "https://example.com",
method: "POST",
uri: "/v1/data/test",
body: JSON.stringify({ scopes: ["test"], granteeAddress: "0x123" }),
});
const nonEmptyPayloadBase64 = nonEmptyHeader
.replace("Web3Signed ", "")
.split(".")[0];
const nonEmptyPayload = JSON.parse(
Buffer.from(nonEmptyPayloadBase64, "base64").toString("utf-8"),
);
expect(nonEmptyPayload.bodyHash).toMatch(/^sha256:[0-9a-f]{64}$/);

const emptyHeader = await signer.signRequest({
aud: "https://example.com",
method: "GET",
uri: "/v1/data/test",
});
const emptyPayloadBase64 = emptyHeader
.replace("Web3Signed ", "")
.split(".")[0];
const emptyPayload = JSON.parse(
Buffer.from(emptyPayloadBase64, "base64").toString("utf-8"),
);
expect(emptyPayload.bodyHash).toBe(PREFIXED_EMPTY_BODY_HASH);
});

it("canonicalizes body before hashing (key order does not matter)", async () => {
const signer = createRequestSigner({ privateKey: TEST_PRIVATE_KEY });

Expand Down
9 changes: 9 additions & 0 deletions test/server/session-relay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ describe("createSessionRelay", () => {
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(callBody.granteeAddress).toBe(TEST_GRANTEE);
expect(callBody.scopes).toEqual(["instagram.profile"]);

const authHeader = mockFetch.mock.calls[0][1].headers
.Authorization as string;
const payloadBase64 = authHeader.replace("Web3Signed ", "").split(".")[0];
const payload = JSON.parse(
Buffer.from(payloadBase64, "base64").toString("utf-8"),
);
expect(payload.bodyHash).toMatch(/^[0-9a-f]{64}$/);
expect(payload.bodyHash).not.toMatch(/^sha256:/);
});

it("includes optional webhookUrl and appUserId in body", async () => {
Expand Down
Loading