Skip to content
Open
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
10 changes: 9 additions & 1 deletion app/src/services/transport/CloudHttpTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import debug from 'debug';

import { redactRpcUrlForLog } from '../../utils/redactRpcUrlForLog';
import type { CoreTransport } from './CoreTransport';

const log = debug('transport:cloud');
Expand Down Expand Up @@ -35,7 +36,14 @@ export class CloudHttpTransport implements CoreTransport {
private readonly bearerToken: string | null = null,
private readonly timeoutMs: number = 30_000
) {
log('[transport:cloud] created rpcUrl=%s token=%s', rpcUrl, bearerToken ? 'set' : 'none');
// The bearer token is already reported by presence only; the URL needs the same
// care, because a profile rpcUrl may carry `user:pass@` or `?token=` (see the
// redactRpcUrlForLog test) and this line ran before any of it was stripped.
log(
'[transport:cloud] created rpcUrl=%s token=%s',
redactRpcUrlForLog(rpcUrl),
bearerToken ? 'set' : 'none'
);
}

async call<T>(method: string, params: unknown, opts?: { signal?: AbortSignal }): Promise<T> {
Expand Down
3 changes: 2 additions & 1 deletion app/src/services/transport/LanHttpTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import debug from 'debug';

import { redactRpcUrlForLog } from '../../utils/redactRpcUrlForLog';
import type { CoreTransport } from './CoreTransport';

const log = debug('transport:lan');
Expand Down Expand Up @@ -34,7 +35,7 @@ export class LanHttpTransport implements CoreTransport {
private readonly rpcUrl: string,
private readonly timeoutMs: number = 10_000
) {
log('[transport:lan] created rpcUrl=%s', rpcUrl);
log('[transport:lan] created rpcUrl=%s', redactRpcUrlForLog(rpcUrl));
}

async call<T>(method: string, params: unknown, opts?: { signal?: AbortSignal }): Promise<T> {
Expand Down
5 changes: 3 additions & 2 deletions app/src/services/transport/TransportManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
import debug from 'debug';

import { redactRpcUrlForLog } from '../../utils/redactRpcUrlForLog';
import { CloudHttpTransport } from './CloudHttpTransport';
import type { CoreTransport } from './CoreTransport';
import { LanHttpTransport } from './LanHttpTransport';
Expand Down Expand Up @@ -80,7 +81,7 @@ export class TransportManager {
throw new Error('[transport:manager] cloud profile missing rpcUrl');
}
const t = new CloudHttpTransport(rpcUrl, sessionToken ?? null);
log('[transport:manager] → CloudHttpTransport rpcUrl=%s', rpcUrl);
log('[transport:manager] → CloudHttpTransport rpcUrl=%s', redactRpcUrlForLog(rpcUrl));
return t;
}

Expand All @@ -90,7 +91,7 @@ export class TransportManager {
throw new Error('[transport:manager] lan profile missing rpcUrl');
}
const t = new LanHttpTransport(rpcUrl);
log('[transport:manager] → LanHttpTransport rpcUrl=%s', rpcUrl);
log('[transport:manager] → LanHttpTransport rpcUrl=%s', redactRpcUrlForLog(rpcUrl));
return t;
}

Expand Down
93 changes: 93 additions & 0 deletions app/src/services/transport/logRedaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import debug from 'debug';
import { describe, expect, it } from 'vitest';

import { CloudHttpTransport } from './CloudHttpTransport';
import { LanHttpTransport } from './LanHttpTransport';
import type { ConnectionProfile } from './profileStore';
import { createTransportManager } from './TransportManager';

/**
* A connection profile's `rpcUrl` is stored verbatim — `normalizeRpcUrl` keeps
* userinfo, query and hash — so it can carry `user:pass@` or `?token=`. That is
* exactly what `redactRpcUrlForLog` exists for, and what its own test pins.
*
* These transports logged the raw URL at construction time. `transport:cloud`
* was already careful with the bearer token (it reports presence, not value),
* which is what makes the URL beside it the odd one out.
*/

const SECRETS = ['HUNTER2', 'SUPERSECRET'] as const;
const SECRET_URL = 'https://svc:HUNTER2@core.example.com/rpc?token=SUPERSECRET#/tok';
const SAFE_PART = 'https://core.example.com/rpc';

/** Collect everything the `debug` namespaces emit while `fn` runs. */
function captureDebug(fn: () => void): string {
const lines: string[] = [];
const previous = debug.disable();
const previousLog = debug.log;
debug.enable('transport:*');
debug.log = (...args: unknown[]) => {
lines.push(args.map(String).join(' '));
};
try {
fn();
} finally {
debug.log = previousLog;
debug.disable();
if (previous) debug.enable(previous);
}
return lines.join('\n');
}

function expectNoSecrets(output: string) {
for (const secret of SECRETS) {
expect(output).not.toContain(secret);
}
}

function profile(kind: 'cloud' | 'lan'): ConnectionProfile {
return {
id: `p-${kind}`,
kind,
rpcUrl: SECRET_URL,
sessionToken: 'session-value',
} as unknown as ConnectionProfile;
}

describe('transport construction logging', () => {
it('CloudHttpTransport does not log rpcUrl credentials', () => {
const output = captureDebug(() => {
new CloudHttpTransport(SECRET_URL, 'bearer-value');
});
expectNoSecrets(output);
expect(output).toContain(SAFE_PART);
// The bearer value was never logged and must stay that way.
expect(output).not.toContain('bearer-value');
});

it('LanHttpTransport does not log rpcUrl credentials', () => {
const output = captureDebug(() => {
new LanHttpTransport(SECRET_URL);
});
expectNoSecrets(output);
expect(output).toContain(SAFE_PART);
});

it('TransportManager does not log rpcUrl credentials when selecting cloud', async () => {
let selected: Promise<unknown> | undefined;
const output = captureDebug(() => {
selected = createTransportManager(profile('cloud')).getTransport();
});
await selected;
expectNoSecrets(output);
});

it('TransportManager does not log rpcUrl credentials when selecting lan', async () => {
let selected: Promise<unknown> | undefined;
const output = captureDebug(() => {
selected = createTransportManager(profile('lan')).getTransport();
});
await selected;
expectNoSecrets(output);
});
});
Loading