diff --git a/connect/src/app/admin/_lib/register-builder.ts b/connect/src/app/admin/_lib/register-builder.ts index 4a80239..0f60181 100644 --- a/connect/src/app/admin/_lib/register-builder.ts +++ b/connect/src/app/admin/_lib/register-builder.ts @@ -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" diff --git a/connect/src/config/config.ts b/connect/src/config/config.ts index e78541d..972375d 100644 --- a/connect/src/config/config.ts +++ b/connect/src/config/config.ts @@ -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"; diff --git a/connect/src/lib/server-provider/register-on-chain.ts b/connect/src/lib/server-provider/register-on-chain.ts index 5fdd0bb..afd0949 100644 --- a/connect/src/lib/server-provider/register-on-chain.ts +++ b/connect/src/lib/server-provider/register-on-chain.ts @@ -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" diff --git a/src/cli/index.ts b/src/cli/index.ts index efa56da..439a301 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -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; @@ -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, @@ -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"); diff --git a/src/core/constants.ts b/src/core/constants.ts index f9986ec..952aebf 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -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; diff --git a/src/core/types.ts b/src/core/types.ts index 4159076..5a36e8b 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -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:`, including the canonical empty-body hash. + */ + bodyHashFormat?: "legacy" | "prefixed"; } /** Configuration for {@link createSessionRelay}. */ diff --git a/src/server/data-client.ts b/src/server/data-client.ts index 5a08ce7..a5ec276 100644 --- a/src/server/data-client.ts +++ b/src/server/data-client.ts @@ -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 { diff --git a/src/server/request-signer.ts b/src/server/request-signer.ts index e0aa7c6..f536472 100644 --- a/src/server/request-signer.ts +++ b/src/server/request-signer.ts @@ -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; + function base64urlEncode(input: string): string { return Buffer.from(input, "utf-8") .toString("base64") @@ -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; } /** @@ -64,6 +72,7 @@ export function createRequestSigner( config: RequestSignerConfig, ): RequestSigner { const account = privateKeyToAccount(config.privateKey); + const bodyHashFormat = config.bodyHashFormat ?? "legacy"; return { address: account.address, @@ -73,7 +82,7 @@ export function createRequestSigner( const payload: Record = { aud: params.aud, - bodyHash: computeBodyHash(params.body), + bodyHash: computeBodyHash(params.body, bodyHashFormat), exp: now + 300, iat: now, method: params.method, diff --git a/test/cli/index.test.ts b/test/cli/index.test.ts index 535e8f4..61a5016 100644 --- a/test/cli/index.test.ts +++ b/test/cli/index.test.ts @@ -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", }, }); } @@ -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"); @@ -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 () => { diff --git a/test/core/constants.test.ts b/test/core/constants.test.ts index 990deda..368908c 100644 --- a/test/core/constants.test.ts +++ b/test/core/constants.test.ts @@ -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"); }); diff --git a/test/server/data-client.test.ts b/test/server/data-client.test.ts index 9ce73a3..75e4f24 100644 --- a/test/server/data-client.test.ts +++ b/test/server/data-client.test.ts @@ -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 () => { @@ -174,6 +175,9 @@ describe("createDataClient", () => { ); expect(payload.grantId).toBeUndefined(); expect(payload.uri).toBe("/v1/data"); + expect(payload.bodyHash).toBe( + "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ); }); }); diff --git a/test/server/request-signer.test.ts b/test/server/request-signer.test.ts index e72381f..37e363b 100644 --- a/test/server/request-signer.test.ts +++ b/test/server/request-signer.test.ts @@ -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 () => { @@ -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", @@ -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", @@ -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 }); diff --git a/test/server/session-relay.test.ts b/test/server/session-relay.test.ts index c66f6d8..cebaff6 100644 --- a/test/server/session-relay.test.ts +++ b/test/server/session-relay.test.ts @@ -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 () => {