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: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ jobs:
- run: pnpm lint

- run: pnpm test

- run: pnpm test:e2e
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,19 @@ pnpm build
pnpm test
```

### Test/debug endpoint overrides

The following env vars are intended for local testing/debugging (for example, mock E2E servers). They are **not** normal production configuration:

- `ALCHEMY_RPC_BASE_URL`
- `ALCHEMY_ADMIN_API_BASE_URL`

Safety constraints:

- Only localhost targets are accepted (`localhost`, `127.0.0.1`, `::1`)
- Non-HTTPS transport is only allowed for localhost targets
- Default production behavior is unchanged when these vars are unset

### Type check

```bash
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:e2e": "pnpm build && vitest run --config vitest.e2e.config.ts",
"test:watch": "vitest",
"lint": "tsc --noEmit"
},
Expand Down Expand Up @@ -41,6 +43,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@vitest/coverage-v8": "^4.0.18",
"@types/node": "^25.3.0",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
Expand Down
144 changes: 144 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 46 additions & 3 deletions src/lib/admin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ interface ListAppsResponse {

export class AdminClient {
private static readonly ADMIN_API_HOST = "admin-api.alchemy.com";
// Test/debug only: used by mock E2E to route admin requests locally.
private static readonly ADMIN_API_BASE_URL_ENV = "ALCHEMY_ADMIN_API_BASE_URL";
private accessKey: string;

constructor(accessKey: string) {
Expand All @@ -66,15 +68,56 @@ export class AdminClient {
}

protected baseURL(): string {
const override = this.baseURLOverride();
if (override) return override.toString().replace(/\/$/, "");
return "https://admin-api.alchemy.com";
}

protected allowedHosts(): Set<string> {
return new Set([AdminClient.ADMIN_API_HOST]);
const hosts = new Set([AdminClient.ADMIN_API_HOST]);
const override = this.baseURLOverride();
if (override) hosts.add(override.hostname);
return hosts;
}

protected allowInsecureTransport(_hostname: string): boolean {
return false;
protected allowInsecureTransport(hostname: string): boolean {
return this.isLocalhost(hostname);
}

private isLocalhost(hostname: string): boolean {
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
}

private baseURLOverride(): URL | null {
const raw = process.env[AdminClient.ADMIN_API_BASE_URL_ENV];
if (!raw) return null;

let parsed: URL;
try {
parsed = new URL(raw);
} catch {
throw errInvalidArgs(`Invalid ${AdminClient.ADMIN_API_BASE_URL_ENV} value.`);
}

if (!this.isLocalhost(parsed.hostname)) {
throw errInvalidArgs(
`${AdminClient.ADMIN_API_BASE_URL_ENV} must target localhost or 127.0.0.1.`,
);
}

if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
throw errInvalidArgs(
`${AdminClient.ADMIN_API_BASE_URL_ENV} must use http:// or https://.`,
);
}

if (parsed.protocol === "http:" && !this.isLocalhost(parsed.hostname)) {
throw errInvalidArgs(
`${AdminClient.ADMIN_API_BASE_URL_ENV} can only use non-HTTPS for localhost targets.`,
);
}

return parsed;
}

private validateAccessKey(accessKey: string): void {
Expand Down
Loading