diff --git a/dist/neox/cli.js b/dist/neox/cli.js index fa41440..baa5d6a 100644 --- a/dist/neox/cli.js +++ b/dist/neox/cli.js @@ -87,7 +87,12 @@ export async function runNeoxRegistrationCli(config, argv = process.argv.slice(2 registry, projectDir, config, - storage: needsPublication ? createMetadataStorage(config) : undefined, + storage: needsPublication + ? createMetadataStorage(config, fetch, { + signerAddress: account.address, + signMessage: (message) => walletClient.signMessage({ account, message }), + }) + : undefined, }, state); } if (command === "verify" && !hasMinted(state)) { diff --git a/dist/neox/storage/index.d.ts b/dist/neox/storage/index.d.ts index 15388c3..540074b 100644 --- a/dist/neox/storage/index.d.ts +++ b/dist/neox/storage/index.d.ts @@ -4,7 +4,13 @@ export * from "./types.js"; export * from "./inline.js"; export * from "./neofs.js"; export * from "./managed.js"; +export * from "./managed-upload-auth.js"; export * from "./user-uri.js"; export declare function metadataBackend(config: AgentProjectConfig): MetadataStorageBackend; -export declare function createMetadataStorage(config: AgentProjectConfig, fetchImpl?: FetchLike): MetadataStorage; +export interface CreateMetadataStorageOptions { + fetchImpl?: FetchLike; + signerAddress?: string; + signMessage?: (message: string) => Promise; +} +export declare function createMetadataStorage(config: AgentProjectConfig, fetchImpl?: FetchLike, options?: CreateMetadataStorageOptions): MetadataStorage; export declare function uriForStoragePreflight(config: AgentProjectConfig): string | undefined; diff --git a/dist/neox/storage/index.js b/dist/neox/storage/index.js index dad1f8a..7212244 100644 --- a/dist/neox/storage/index.js +++ b/dist/neox/storage/index.js @@ -1,6 +1,7 @@ import { assertRegistrationMetadataUri } from "../metadata.js"; import { MANAGED_URI_GAS_ESTIMATE } from "./neofs-uri.js"; import { InlineMetadataStorage } from "./inline.js"; +import { createWalletManagedMetadataProtection, managedUploadAuthDisabled, } from "./managed-upload-auth.js"; import { ManagedMetadataStorage, resolveAgentoryApiBaseUrl } from "./managed.js"; import { NeofsMetadataStorage, neofsPublicUri, validateNeofsStorageConfig } from "./neofs.js"; import { UserUriMetadataStorage } from "./user-uri.js"; @@ -8,6 +9,7 @@ export * from "./types.js"; export * from "./inline.js"; export * from "./neofs.js"; export * from "./managed.js"; +export * from "./managed-upload-auth.js"; export * from "./user-uri.js"; const BACKENDS = ["managed", "uri", "inline", "neofs"]; export function metadataBackend(config) { @@ -17,12 +19,29 @@ export function metadataBackend(config) { } return backend; } -export function createMetadataStorage(config, fetchImpl = fetch) { +export function createMetadataStorage(config, fetchImpl = fetch, options = {}) { const backend = metadataBackend(config); if (backend === "inline") return new InlineMetadataStorage(); if (backend === "managed") { - return new ManagedMetadataStorage({ apiBaseUrl: process.env.AGENTORY_API_BASE_URL, fetchImpl }, fetchImpl); + const apiBaseUrl = resolveAgentoryApiBaseUrl(); + let protection; + if (!managedUploadAuthDisabled()) { + if (!options.signerAddress || !options.signMessage) { + throw new Error("Managed Agentory uploads require a connected wallet signer when authorization is enabled."); + } + protection = createWalletManagedMetadataProtection({ + apiBaseUrl, + fetchImpl: options.fetchImpl ?? fetchImpl, + signerAddress: options.signerAddress, + signMessage: options.signMessage, + }); + } + return new ManagedMetadataStorage({ + apiBaseUrl, + fetchImpl: options.fetchImpl ?? fetchImpl, + protection, + }, options.fetchImpl ?? fetchImpl); } if (backend === "uri") { return new UserUriMetadataStorage(config.metadataUri ?? ""); diff --git a/dist/neox/storage/managed-upload-auth.d.ts b/dist/neox/storage/managed-upload-auth.d.ts new file mode 100644 index 0000000..5704675 --- /dev/null +++ b/dist/neox/storage/managed-upload-auth.d.ts @@ -0,0 +1,15 @@ +import type { FetchLike } from "./types.js"; +import { type ManagedMetadataRequestProtection } from "./managed.js"; +export declare const MANAGED_UPLOAD_AUTHORIZATION_SCHEME = "AgentoryUpload"; +export declare const MANAGED_UPLOAD_AUTHORIZATION_VERSION = "v1"; +export declare function formatManagedUploadAuthorizationHeader(args: { + challengeId: string; + signature: string; +}): string; +export declare function managedUploadAuthDisabled(): boolean; +export declare function createWalletManagedMetadataProtection(args: { + apiBaseUrl?: string; + fetchImpl?: FetchLike; + signerAddress: string; + signMessage(message: string): Promise; +}): ManagedMetadataRequestProtection; diff --git a/dist/neox/storage/managed-upload-auth.js b/dist/neox/storage/managed-upload-auth.js new file mode 100644 index 0000000..6212c2a --- /dev/null +++ b/dist/neox/storage/managed-upload-auth.js @@ -0,0 +1,94 @@ +import { MANAGED_METADATA_PATH } from "../constants.js"; +import { ManagedMetadataError, resolveAgentoryApiBaseUrl, } from "./managed.js"; +export const MANAGED_UPLOAD_AUTHORIZATION_SCHEME = "AgentoryUpload"; +export const MANAGED_UPLOAD_AUTHORIZATION_VERSION = "v1"; +export function formatManagedUploadAuthorizationHeader(args) { + return `${MANAGED_UPLOAD_AUTHORIZATION_SCHEME} ${MANAGED_UPLOAD_AUTHORIZATION_VERSION} challenge="${args.challengeId}" signature="${args.signature}"`; +} +export function managedUploadAuthDisabled() { + const raw = process.env.MANAGED_UPLOAD_AUTH_DISABLED ?? process.env.AGENTORY_MANAGED_UPLOAD_AUTH_DISABLED; + return raw === "true" || raw === "1"; +} +export function createWalletManagedMetadataProtection(args) { + const apiBaseUrl = resolveAgentoryApiBaseUrl(args.apiBaseUrl); + const fetchImpl = args.fetchImpl ?? fetch; + return { + async apply(headers, context) { + const challengeUrl = `${apiBaseUrl}${MANAGED_METADATA_PATH}/challenge`; + const challengeBody = { + signerAddress: args.signerAddress, + chainId: String(context.chainId), + agentRegistry: context.registry, + agentId: context.agentId.toString(10), + contentHash: context.contentHash, + }; + let response; + try { + response = await fetchImpl(challengeUrl, { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + }, + body: JSON.stringify(challengeBody), + }); + } + catch (error) { + const detail = error instanceof Error ? error.message : "network request failed"; + throw new ManagedMetadataError({ + code: "storage_unavailable", + message: `Agentory managed upload challenge could not be reached (${detail}).`, + failureClass: "transport", + retryable: true, + }); + } + if (!response.ok) { + throw new ManagedMetadataError({ + code: "challenge_rejected", + message: `Agentory refused the managed upload challenge (HTTP ${response.status}).`, + failureClass: "security", + retryable: false, + }); + } + let challenge; + try { + challenge = (await response.json()); + } + catch { + throw new ManagedMetadataError({ + code: "challenge_malformed", + message: "Agentory returned a malformed managed upload challenge response.", + failureClass: "security", + retryable: false, + }); + } + if (typeof challenge.challengeId !== "string" || + !challenge.challengeId.trim() || + typeof challenge.message !== "string" || + !challenge.message.trim()) { + throw new ManagedMetadataError({ + code: "challenge_malformed", + message: "Agentory returned a malformed managed upload challenge response.", + failureClass: "security", + retryable: false, + }); + } + let signature; + try { + signature = await args.signMessage(challenge.message); + } + catch { + throw new ManagedMetadataError({ + code: "signing_failed", + message: "Could not sign the managed upload challenge with the connected wallet.", + failureClass: "security", + retryable: false, + }); + } + headers.set("authorization", formatManagedUploadAuthorizationHeader({ + challengeId: challenge.challengeId, + signature, + })); + }, + }; +} diff --git a/dist/neox/storage/managed.d.ts b/dist/neox/storage/managed.d.ts index 88fd656..df4443f 100644 --- a/dist/neox/storage/managed.d.ts +++ b/dist/neox/storage/managed.d.ts @@ -6,8 +6,14 @@ export type ManagedMetadataFailureClass = "validation" | "storage" | "security" * Development and staging currently accept the registration document with no client credential. * Implementations must not attach a NeoFS write secret or an EVM signing key. */ +export interface ManagedUploadProtectionContext { + contentHash: string; + chainId: number; + registry: string; + agentId: bigint; +} export interface ManagedMetadataRequestProtection { - apply(headers: Headers): void | Promise; + apply(headers: Headers, context: ManagedUploadProtectionContext): void | Promise; } export declare const environmentManagedMetadataProtection: ManagedMetadataRequestProtection; export declare class ManagedMetadataError extends Error { diff --git a/dist/neox/storage/managed.js b/dist/neox/storage/managed.js index b05f9d4..365cc55 100644 --- a/dist/neox/storage/managed.js +++ b/dist/neox/storage/managed.js @@ -3,7 +3,7 @@ import { metadataContentHash } from "../metadata.js"; import { parseCanonicalNeofsAgentUri } from "./neofs-uri.js"; export const environmentManagedMetadataProtection = { apply() { - // No client credential until the API's CLI protection mechanism is selected. + // Used only when the Agentory API has MANAGED_UPLOAD_AUTH_DISABLED (non-production). }, }; export class ManagedMetadataError extends Error { @@ -92,7 +92,12 @@ export class ManagedMetadataStorage { accept: "application/json", "content-type": "application/json", }); - await this.protection.apply(headers); + await this.protection.apply(headers, { + contentHash: metadataContentHash(input.metadata), + chainId: input.chainId, + registry: input.registry, + agentId: input.agentId, + }); const secrets = headerSecrets(headers, this.redactedValues); const url = managedUploadUrl(this.apiBaseUrl); let response; diff --git a/src/neox/cli.ts b/src/neox/cli.ts index 35aec4f..26802a1 100644 --- a/src/neox/cli.ts +++ b/src/neox/cli.ts @@ -130,7 +130,13 @@ export async function runNeoxRegistrationCli( registry, projectDir, config, - storage: needsPublication ? createMetadataStorage(config) : undefined, + storage: needsPublication + ? createMetadataStorage(config, fetch, { + signerAddress: account.address, + signMessage: (message) => + walletClient.signMessage({ account, message }), + }) + : undefined, }, state ); diff --git a/src/neox/storage/index.ts b/src/neox/storage/index.ts index cdac626..9ae9009 100644 --- a/src/neox/storage/index.ts +++ b/src/neox/storage/index.ts @@ -2,6 +2,10 @@ import { assertRegistrationMetadataUri } from "../metadata.js"; import type { AgentProjectConfig, MetadataStorageBackend } from "../types.js"; import { MANAGED_URI_GAS_ESTIMATE } from "./neofs-uri.js"; import { InlineMetadataStorage } from "./inline.js"; +import { + createWalletManagedMetadataProtection, + managedUploadAuthDisabled, +} from "./managed-upload-auth.js"; import { ManagedMetadataStorage, resolveAgentoryApiBaseUrl } from "./managed.js"; import { NeofsMetadataStorage, neofsPublicUri, validateNeofsStorageConfig } from "./neofs.js"; import type { FetchLike, MetadataStorage } from "./types.js"; @@ -11,6 +15,7 @@ export * from "./types.js"; export * from "./inline.js"; export * from "./neofs.js"; export * from "./managed.js"; +export * from "./managed-upload-auth.js"; export * from "./user-uri.js"; const BACKENDS: readonly MetadataStorageBackend[] = ["managed", "uri", "inline", "neofs"]; @@ -25,16 +30,42 @@ export function metadataBackend(config: AgentProjectConfig): MetadataStorageBack return backend; } +export interface CreateMetadataStorageOptions { + fetchImpl?: FetchLike; + signerAddress?: string; + signMessage?: (message: string) => Promise; +} + export function createMetadataStorage( config: AgentProjectConfig, - fetchImpl: FetchLike = fetch + fetchImpl: FetchLike = fetch, + options: CreateMetadataStorageOptions = {} ): MetadataStorage { const backend = metadataBackend(config); if (backend === "inline") return new InlineMetadataStorage(); if (backend === "managed") { + const apiBaseUrl = resolveAgentoryApiBaseUrl(); + let protection; + if (!managedUploadAuthDisabled()) { + if (!options.signerAddress || !options.signMessage) { + throw new Error( + "Managed Agentory uploads require a connected wallet signer when authorization is enabled." + ); + } + protection = createWalletManagedMetadataProtection({ + apiBaseUrl, + fetchImpl: options.fetchImpl ?? fetchImpl, + signerAddress: options.signerAddress, + signMessage: options.signMessage, + }); + } return new ManagedMetadataStorage( - { apiBaseUrl: process.env.AGENTORY_API_BASE_URL, fetchImpl }, - fetchImpl + { + apiBaseUrl, + fetchImpl: options.fetchImpl ?? fetchImpl, + protection, + }, + options.fetchImpl ?? fetchImpl ); } if (backend === "uri") { diff --git a/src/neox/storage/managed-upload-auth.ts b/src/neox/storage/managed-upload-auth.ts new file mode 100644 index 0000000..a61e1b2 --- /dev/null +++ b/src/neox/storage/managed-upload-auth.ts @@ -0,0 +1,121 @@ +import { MANAGED_METADATA_PATH } from "../constants.js"; +import type { FetchLike } from "./types.js"; +import { + type ManagedMetadataRequestProtection, + type ManagedUploadProtectionContext, + ManagedMetadataError, + resolveAgentoryApiBaseUrl, +} from "./managed.js"; + +export const MANAGED_UPLOAD_AUTHORIZATION_SCHEME = "AgentoryUpload"; +export const MANAGED_UPLOAD_AUTHORIZATION_VERSION = "v1"; + +export function formatManagedUploadAuthorizationHeader(args: { + challengeId: string; + signature: string; +}): string { + return `${MANAGED_UPLOAD_AUTHORIZATION_SCHEME} ${MANAGED_UPLOAD_AUTHORIZATION_VERSION} challenge="${args.challengeId}" signature="${args.signature}"`; +} + +interface ChallengeResponse { + challengeId: string; + message: string; + expiresAt: string; +} + +export function managedUploadAuthDisabled(): boolean { + const raw = + process.env.MANAGED_UPLOAD_AUTH_DISABLED ?? process.env.AGENTORY_MANAGED_UPLOAD_AUTH_DISABLED; + return raw === "true" || raw === "1"; +} + +export function createWalletManagedMetadataProtection(args: { + apiBaseUrl?: string; + fetchImpl?: FetchLike; + signerAddress: string; + signMessage(message: string): Promise; +}): ManagedMetadataRequestProtection { + const apiBaseUrl = resolveAgentoryApiBaseUrl(args.apiBaseUrl); + const fetchImpl = args.fetchImpl ?? fetch; + return { + async apply(headers, context: ManagedUploadProtectionContext) { + const challengeUrl = `${apiBaseUrl}${MANAGED_METADATA_PATH}/challenge`; + const challengeBody = { + signerAddress: args.signerAddress, + chainId: String(context.chainId), + agentRegistry: context.registry, + agentId: context.agentId.toString(10), + contentHash: context.contentHash, + }; + let response: Response; + try { + response = await fetchImpl(challengeUrl, { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + }, + body: JSON.stringify(challengeBody), + }); + } catch (error) { + const detail = error instanceof Error ? error.message : "network request failed"; + throw new ManagedMetadataError({ + code: "storage_unavailable", + message: `Agentory managed upload challenge could not be reached (${detail}).`, + failureClass: "transport", + retryable: true, + }); + } + if (!response.ok) { + throw new ManagedMetadataError({ + code: "challenge_rejected", + message: `Agentory refused the managed upload challenge (HTTP ${response.status}).`, + failureClass: "security", + retryable: false, + }); + } + let challenge: ChallengeResponse; + try { + challenge = (await response.json()) as ChallengeResponse; + } catch { + throw new ManagedMetadataError({ + code: "challenge_malformed", + message: "Agentory returned a malformed managed upload challenge response.", + failureClass: "security", + retryable: false, + }); + } + if ( + typeof challenge.challengeId !== "string" || + !challenge.challengeId.trim() || + typeof challenge.message !== "string" || + !challenge.message.trim() + ) { + throw new ManagedMetadataError({ + code: "challenge_malformed", + message: "Agentory returned a malformed managed upload challenge response.", + failureClass: "security", + retryable: false, + }); + } + let signature: string; + try { + signature = await args.signMessage(challenge.message); + } catch { + throw new ManagedMetadataError({ + code: "signing_failed", + message: "Could not sign the managed upload challenge with the connected wallet.", + failureClass: "security", + retryable: false, + }); + } + headers.set( + "authorization", + formatManagedUploadAuthorizationHeader({ + challengeId: challenge.challengeId, + signature, + }) + ); + }, + }; +} diff --git a/src/neox/storage/managed.ts b/src/neox/storage/managed.ts index 4f84524..c3f4ce7 100644 --- a/src/neox/storage/managed.ts +++ b/src/neox/storage/managed.ts @@ -11,13 +11,20 @@ export type ManagedMetadataFailureClass = "validation" | "storage" | "security" * Development and staging currently accept the registration document with no client credential. * Implementations must not attach a NeoFS write secret or an EVM signing key. */ +export interface ManagedUploadProtectionContext { + contentHash: string; + chainId: number; + registry: string; + agentId: bigint; +} + export interface ManagedMetadataRequestProtection { - apply(headers: Headers): void | Promise; + apply(headers: Headers, context: ManagedUploadProtectionContext): void | Promise; } export const environmentManagedMetadataProtection: ManagedMetadataRequestProtection = { apply() { - // No client credential until the API's CLI protection mechanism is selected. + // Used only when the Agentory API has MANAGED_UPLOAD_AUTH_DISABLED (non-production). }, }; @@ -143,7 +150,12 @@ export class ManagedMetadataStorage implements MetadataStorage { accept: "application/json", "content-type": "application/json", }); - await this.protection.apply(headers); + await this.protection.apply(headers, { + contentHash: metadataContentHash(input.metadata), + chainId: input.chainId, + registry: input.registry, + agentId: input.agentId, + }); const secrets = headerSecrets(headers, this.redactedValues); const url = managedUploadUrl(this.apiBaseUrl); diff --git a/tests/managed-metadata.test.ts b/tests/managed-metadata.test.ts index 6f7d8e9..5129919 100644 --- a/tests/managed-metadata.test.ts +++ b/tests/managed-metadata.test.ts @@ -707,6 +707,9 @@ describe("managed API origin configuration", () => { it("accepts an explicit staging HTTPS origin and a localhost HTTP origin", async () => { const metadata = buildRegistrationMetadata(CONFIG, 7n, REGISTRY); + const previousAuthDisabled = process.env.MANAGED_UPLOAD_AUTH_DISABLED; + process.env.MANAGED_UPLOAD_AUTH_DISABLED = "true"; + try { await withApiBaseUrl("https://staging.agentory.xyz/ignored-path", async () => { const fetchImpl = vi.fn().mockResolvedValue(successResponse()); const storage = createMetadataStorage(CONFIG, fetchImpl); @@ -742,5 +745,9 @@ describe("managed API origin configuration", () => { await withApiBaseUrl("http://localhost:4010", async () => { expect(resolveAgentoryApiBaseUrl()).toBe("http://localhost:4010"); }); + } finally { + if (previousAuthDisabled === undefined) delete process.env.MANAGED_UPLOAD_AUTH_DISABLED; + else process.env.MANAGED_UPLOAD_AUTH_DISABLED = previousAuthDisabled; + } }); }); diff --git a/tests/managed-upload-auth.test.ts b/tests/managed-upload-auth.test.ts new file mode 100644 index 0000000..71f3a99 --- /dev/null +++ b/tests/managed-upload-auth.test.ts @@ -0,0 +1,185 @@ +import { privateKeyToAccount } from "viem/accounts"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createWalletManagedMetadataProtection, + formatManagedUploadAuthorizationHeader, + managedUploadAuthDisabled, +} from "../src/neox/storage/managed-upload-auth.js"; +import { ManagedMetadataError } from "../src/neox/storage/managed.js"; + +const SIGNER = privateKeyToAccount(`0x${"ab".repeat(32)}`); +const PRIVATE_KEY_FRAGMENT = "abababab"; + +async function withEnv( + values: Record, + run: () => Promise | T +): Promise { + const previous = new Map(); + for (const [key, value] of Object.entries(values)) { + previous.set(key, process.env[key]); + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + try { + return await run(); + } finally { + for (const [key, value] of previous.entries()) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + } +} + +describe("managed upload wallet auth", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("formats AgentoryUpload authorization headers", () => { + expect( + formatManagedUploadAuthorizationHeader({ + challengeId: "challenge-1", + signature: "0x01", + }) + ).toBe('AgentoryUpload v1 challenge="challenge-1" signature="0x01"'); + }); + + it("requests a challenge with the expected payload and signs the returned message", async () => { + const fetchImpl = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + challengeId: "challenge-42", + message: "sign-this", + expiresAt: "2026-01-01T00:05:00.000Z", + }), + { status: 200, headers: { "content-type": "application/json" } } + ) + ); + const signMessage = vi.fn().mockResolvedValue("0xsig"); + const protection = createWalletManagedMetadataProtection({ + apiBaseUrl: "https://staging.agentory.xyz", + fetchImpl, + signerAddress: SIGNER.address, + signMessage, + }); + const headers = new Headers(); + await protection.apply(headers, { + contentHash: "a".repeat(64), + chainId: 12227332, + registry: "0x8004A856a396D08d31E597a867B1D8273901e641", + agentId: 7n, + }); + + expect(fetchImpl).toHaveBeenCalledWith( + "https://staging.agentory.xyz/api/registration-metadata/challenge", + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ + signerAddress: SIGNER.address, + chainId: "12227332", + agentRegistry: "0x8004A856a396D08d31E597a867B1D8273901e641", + agentId: "7", + contentHash: "a".repeat(64), + }), + }) + ); + expect(signMessage).toHaveBeenCalledWith("sign-this"); + expect(headers.get("authorization")).toBe( + 'AgentoryUpload v1 challenge="challenge-42" signature="0xsig"' + ); + }); + + it("surfaces actionable errors when the challenge is rejected", async () => { + const fetchImpl = vi.fn().mockResolvedValue(new Response("nope", { status: 403 })); + const protection = createWalletManagedMetadataProtection({ + apiBaseUrl: "https://staging.agentory.xyz", + fetchImpl, + signerAddress: SIGNER.address, + signMessage: vi.fn(), + }); + await expect( + protection.apply(new Headers(), { + contentHash: "b".repeat(64), + chainId: 12227332, + registry: "0x8004A856a396D08d31E597a867B1D8273901e641", + agentId: 1n, + }) + ).rejects.toMatchObject({ + code: "challenge_rejected", + failureClass: "security", + retryable: false, + }); + }); + + it("rejects malformed challenge responses", async () => { + const fetchImpl = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ challengeId: "", message: "" }), { + status: 200, + headers: { "content-type": "application/json" }, + }) + ); + const protection = createWalletManagedMetadataProtection({ + apiBaseUrl: "https://staging.agentory.xyz", + fetchImpl, + signerAddress: SIGNER.address, + signMessage: vi.fn(), + }); + await expect( + protection.apply(new Headers(), { + contentHash: "c".repeat(64), + chainId: 12227332, + registry: "0x8004A856a396D08d31E597a867B1D8273901e641", + agentId: 2n, + }) + ).rejects.toMatchObject({ code: "challenge_malformed" }); + }); + + it("honors MANAGED_UPLOAD_AUTH_DISABLED only when explicitly enabled", async () => { + await withEnv({ MANAGED_UPLOAD_AUTH_DISABLED: undefined }, () => { + expect(managedUploadAuthDisabled()).toBe(false); + }); + await withEnv({ MANAGED_UPLOAD_AUTH_DISABLED: "true" }, () => { + expect(managedUploadAuthDisabled()).toBe(true); + }); + await withEnv({ MANAGED_UPLOAD_AUTH_DISABLED: "0" }, () => { + expect(managedUploadAuthDisabled()).toBe(false); + }); + }); + + it("does not leak signatures or private key material in thrown errors", async () => { + const signature = "0xdeadbeef"; + const signMessage = vi.fn().mockRejectedValue(new Error(`wallet refused ${PRIVATE_KEY_FRAGMENT}`)); + const fetchImpl = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + challengeId: "challenge-99", + message: "sign-this", + expiresAt: "2026-01-01T00:05:00.000Z", + }), + { status: 200, headers: { "content-type": "application/json" } } + ) + ); + const protection = createWalletManagedMetadataProtection({ + apiBaseUrl: "https://staging.agentory.xyz", + fetchImpl, + signerAddress: SIGNER.address, + signMessage, + }); + let caught: unknown; + try { + await protection.apply(new Headers(), { + contentHash: "d".repeat(64), + chainId: 12227332, + registry: "0x8004A856a396D08d31E597a867B1D8273901e641", + agentId: 3n, + }); + } catch (error) { + caught = error; + } + expect(caught).toBeInstanceOf(ManagedMetadataError); + const message = (caught as Error).message; + expect(message).not.toContain(signature); + expect(message).not.toContain(SIGNER.address); + expect(message).not.toContain(PRIVATE_KEY_FRAGMENT); + }); +});