Skip to content
Closed
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
20 changes: 17 additions & 3 deletions apps/daemon/src/integrations/vela-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ interface VelaWalletBalanceResponse {
updatedAt?: unknown;
}

function isValidUsdBalance(value: unknown): value is string {
return typeof value === 'string' && /^\d+(?:\.\d+)?$/.test(value);
}

function publicUser(user: VelaUser | null): AmrWalletSnapshot['user'] {
if (!user) return null;
return {
Expand Down Expand Up @@ -164,16 +168,26 @@ export function createVelaWalletSnapshotReader(options: VelaWalletReaderOptions
});
}
const body = (await response.json()) as VelaWalletBalanceResponse;
const balanceUsd = typeof body.balanceUsd === 'string' ? body.balanceUsd : null;
if (balanceUsd === null) {
if (!isValidUsdBalance(body.balanceUsd)) {
const cached = cache.get(key);
if (cached) {
return {
...withCacheSource(cached.snapshot, true),
error: {
code: 'upstream',
message: 'AMR wallet balance response contained an invalid balanceUsd.',
},
};
}
return unavailableSnapshot({
code: 'upstream',
fetchedAt,
message: 'AMR wallet balance response was missing balanceUsd.',
message: 'AMR wallet balance response contained an invalid balanceUsd.',
profile: input.profile,
user: input.user,
});
}
const balanceUsd = body.balanceUsd;
const snapshot: AmrWalletSnapshot = {
status: 'available',
profile: input.profile,
Expand Down
35 changes: 32 additions & 3 deletions apps/daemon/src/routes/vela.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ const AMR_API_PROXY_PREFIX = '/api/integrations/vela/api-proxy';
const VELA_MESSAGE_CENTER_PREFIX = '/api/integrations/vela/message-center';
const VELA_PUBLIC_MESSAGE_CENTER_PREFIX = '/api/integrations/vela/message-center-public';
const AMR_API_UPSTREAM_ORIGIN = 'https://amr-api.open-design.ai';
const PROXY_HOP_BY_HOP_HEADERS = new Set([
'connection',
'keep-alive',
'proxy-authenticate',
'proxy-authorization',
'te',
'trailer',
'transfer-encoding',
'upgrade',
]);
const VELA_WORKSPACE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;

type ReadAppConfig = (dataDir: string) => Promise<AppConfigPrefs>;
type PublicBaseUrlResolver = (req: Request) => string;
Expand Down Expand Up @@ -167,15 +178,26 @@ function proxyAmrApiRequest(req: Request, res: Response): void {
return;
}
const target = new URL(suffix, AMR_API_UPSTREAM_ORIGIN);
if (!target.pathname.startsWith('/api/v1/')) {
res.status(404).json({ error: 'unknown_amr_api_proxy_path' });
return;
}
const workspaceId = req.headers['x-vela-workspace-id'];
if (
workspaceId !== undefined
&& (Array.isArray(workspaceId) || !VELA_WORKSPACE_ID_PATTERN.test(workspaceId))
) {
res.status(400).json({ error: 'invalid_workspace_id' });
return;
}
const body = velaProxyRequestBody(req);
const streamBody = shouldStreamVelaProxyRequest(req, body);
const headers: Record<string, string | string[]> = {};
for (const [key, value] of Object.entries(req.headers)) {
const lower = key.toLowerCase();
if (
lower === 'host' ||
lower === 'connection' ||
lower === 'transfer-encoding'
PROXY_HOP_BY_HOP_HEADERS.has(lower)
) {
continue;
}
Expand All @@ -196,7 +218,9 @@ function proxyAmrApiRequest(req: Request, res: Response): void {
(upstreamRes) => {
res.status(upstreamRes.statusCode ?? 502);
for (const [key, value] of Object.entries(upstreamRes.headers)) {
if (value !== undefined) res.setHeader(key, value);
if (value !== undefined && !PROXY_HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {
res.setHeader(key, value);
}
}
pipeProxyStreamWithGuard(upstreamRes, res, (err) => {
if (!res.headersSent) {
Expand All @@ -215,6 +239,11 @@ function proxyAmrApiRequest(req: Request, res: Response): void {
res.end();
}
});
const abortUpstream = () => {
if (!res.writableEnded && !upstream.destroyed) upstream.destroy();
};
req.once('aborted', abortUpstream);
res.once('close', abortUpstream);
if (body) upstream.write(body);
if (streamBody) {
pipeProxyStreamWithGuard(req, upstream, () => upstream.destroy());
Expand Down
145 changes: 145 additions & 0 deletions apps/daemon/tests/integrations/vela-wallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { createVelaWalletSnapshotReader } from '../../src/integrations/vela-wallet.js';

let originalHome: string | undefined;
let testHome: string;

function seedWalletLogin(): void {
const configFile = path.join(testHome, '.amr', 'config.json');
mkdirSync(path.dirname(configFile), { recursive: true });
writeFileSync(
configFile,
JSON.stringify({
profiles: {
local: {
apiUrl: 'https://wallet.example.test',
controlKey: 'ck-wallet-unit',
runtimeKey: 'rt-wallet-unit',
user: {
id: 'wallet-unit-user',
email: 'wallet-unit@example.com',
plan: 'plus',
},
},
},
}),
'utf8',
);
}

beforeEach(() => {
originalHome = process.env.HOME;
testHome = mkdtempSync(path.join(tmpdir(), 'od-vela-wallet-'));
process.env.HOME = testHome;
process.env.OPEN_DESIGN_AMR_PROFILE = 'local';
seedWalletLogin();
});

afterEach(() => {
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
delete process.env.OPEN_DESIGN_AMR_PROFILE;
rmSync(testHome, { recursive: true, force: true });
});

describe('createVelaWalletSnapshotReader', () => {
it('honors its TTL and lets an explicit refresh bypass a fresh cache entry', async () => {
let nowMs = Date.parse('2026-07-31T02:00:00.000Z');
let balanceUsd = '20.00';
const fetchMock = vi.fn(async () =>
new Response(JSON.stringify({ balanceUsd }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const reader = createVelaWalletSnapshotReader({
fetch: fetchMock as typeof fetch,
now: () => new Date(nowMs),
ttlMs: 100,
});

const initial = await reader.read();
balanceUsd = '30.00';
nowMs += 99;
const cached = await reader.read();
nowMs += 2;
const expired = await reader.read();
balanceUsd = '40.00';
const forced = await reader.read({ refresh: true });

expect(initial).toMatchObject({ balanceUsd: '20.00', source: 'vela_api' });
expect(cached).toMatchObject({ balanceUsd: '20.00', source: 'daemon_cache' });
expect(expired).toMatchObject({ balanceUsd: '30.00', source: 'vela_api' });
expect(forced).toMatchObject({ balanceUsd: '40.00', source: 'vela_api' });
expect(fetchMock).toHaveBeenCalledTimes(3);
});

it.each([
{ label: 'missing', body: {} },
{ label: 'numeric', body: { balanceUsd: 20 } },
{ label: 'negative', body: { balanceUsd: '-1.00' } },
{ label: 'NaN', body: { balanceUsd: 'NaN' } },
{ label: 'infinite', body: { balanceUsd: 'Infinity' } },
])('rejects a $label balance without overwriting the last valid snapshot', async ({ body }) => {
let responseBody: unknown = { balanceUsd: '20.00' };
const fetchMock = vi.fn(async () =>
new Response(JSON.stringify(responseBody), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const reader = createVelaWalletSnapshotReader({
fetch: fetchMock as typeof fetch,
ttlMs: 60_000,
});

const initial = await reader.read();
responseBody = body;
const rejected = await reader.read({ refresh: true });
const cached = await reader.read();

expect(initial).toMatchObject({ balanceUsd: '20.00', source: 'vela_api' });
expect(rejected).toMatchObject({
balanceUsd: '20.00',
source: 'daemon_cache',
stale: true,
error: { code: 'upstream' },
});
expect(cached).toMatchObject({
balanceUsd: '20.00',
source: 'daemon_cache',
stale: false,
});
expect(cached.error).toBeUndefined();
});

it('keeps the last valid snapshot when the upstream returns malformed JSON', async () => {
let malformed = false;
const fetchMock = vi.fn(async () =>
new Response(malformed ? '{"balanceUsd":' : JSON.stringify({ balanceUsd: '20.00' }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const reader = createVelaWalletSnapshotReader({
fetch: fetchMock as typeof fetch,
ttlMs: 60_000,
});

await reader.read();
malformed = true;
const degraded = await reader.read({ refresh: true });

expect(degraded).toMatchObject({
balanceUsd: '20.00',
source: 'daemon_cache',
stale: true,
error: { code: 'network' },
});
});
});
Loading
Loading