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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,17 @@ Reply.io login (`reply auth login`) to actually do anything.
| `REPLY_TEAM_ID` | Team/workspace id sent as `X-TEAM-ID` (same as `--team-id`) |
| `REPLY_CONFIG_DIR` | Config directory (default `~/.config/reply`; `%APPDATA%\reply` on Windows) |
| `REPLY_NO_UPDATE_CHECK` | Set to `1` to never check whether a newer release exists |
| `REPLY_CLIENT` | The program running the CLI, as `<name>/<version>`, put before `reply-cli/<version>` in the User-Agent |

`REPLY_CLIENT` lets a program that runs the CLI for you say so on every request
to Reply: with `REPLY_CLIENT=jason-harness/0.22.0` the User-Agent is
`jason-harness/0.22.0 reply-cli/0.17.2` instead of `reply-cli/0.17.2`, so calls
made through that program can be told apart from your own. The Jason harness
sets it for each command it runs. The value must be one lower-case name of up to
32 letters, digits or dashes, a `/`, and a version of up to 32 letters, digits,
dots, `+` or `-`; anything else is left out and the User-Agent stays as it is.
It is a label, not an input: it never refuses a command and never prints
anything, and it is read only from the environment of the process.

## Contributing

Expand Down
39 changes: 37 additions & 2 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,43 @@ describe('config', ()=>{
});

it('user_agent is <app>-cli/<version> and identifies the CLI', ()=>{
expect(user_agent()).toBe(`${APP_NAME}-cli/${cli_version()}`);
expect(user_agent().startsWith('reply-cli/')).toBe(true);
expect(user_agent({})).toBe(`${APP_NAME}-cli/${cli_version()}`);
expect(user_agent({}).startsWith('reply-cli/')).toBe(true);
});

it('a well-formed REPLY_CLIENT names the program running the CLI before it, the CLI\'s own token kept last', ()=>{
const own = `reply-cli/${cli_version()}`;
for (const client of ['jason-harness/0.22.0', 'a/1', 'x9/1.0.0-rc.1+build.7', `${'a'.repeat(32)}/${'9'.repeat(32)}`, 'my-tool/V2'])
{
expect(user_agent({REPLY_CLIENT: client}), client).toBe(`${client} ${own}`);
}
});

it('leaves the User-Agent exactly as it is when REPLY_CLIENT is unset or not one product token and its version', ()=>{
const own = `reply-cli/${cli_version()}`;
const malformed = [
'', ' ', 'jason harness/0.22.0', 'jason-harness/0.22 0', 'jason-harness', 'jason-harness/', '/0.22.0',
`${'a'.repeat(33)}/1`, `a/${'9'.repeat(33)}`, 'Jason-harness/0.22.0', 'JASON/1', '9jason/1', '-jason/1',
'jason_harness/1', 'jason/harness/1', 'jason/1 (Windows)', ' jason/1', 'jason/1 ', 'jason/1\n', 'jason/1\r\nX-Evil: 1',
'jason/1;', 'jasön/1', 'jason/1é',
];
expect(user_agent({}), 'unset').toBe(own);
for (const client of malformed)
{
expect(user_agent({REPLY_CLIENT: client}), JSON.stringify(client)).toBe(own);
}
});

it('reads REPLY_CLIENT from the process environment by default', ()=>{
const before = process.env.REPLY_CLIENT;
try {
process.env.REPLY_CLIENT = 'jason-harness/0.22.0';
expect(user_agent()).toBe(`jason-harness/0.22.0 reply-cli/${cli_version()}`);
delete process.env.REPLY_CLIENT;
expect(user_agent()).toBe(`reply-cli/${cli_version()}`);
} finally {
if (before === undefined) { delete process.env.REPLY_CLIENT; } else { process.env.REPLY_CLIENT = before; }
}
});
});
});
15 changes: 15 additions & 0 deletions src/__tests__/utils/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,21 @@ describe('utils/client', ()=>{
expect(init.headers['User-Agent']).toMatch(/^reply-cli\/\d+\.\d+\.\d+/);
});

it('puts the program named by REPLY_CLIENT before the CLI in the User-Agent, and leaves a malformed one out', async()=>{
mock_fetch.mockImplementation(async()=>json_res({ok: true}));
try {
vi.stubEnv('REPLY_CLIENT', 'jason-harness/0.22.0');
await get(BASE, 'tok', '/x');
vi.stubEnv('REPLY_CLIENT', 'jason harness');
await get(BASE, 'tok', '/x');
} finally {
vi.unstubAllEnvs();
}
const agents = mock_fetch.mock.calls.map(([, init])=>init.headers['User-Agent']);
expect(agents[0]).toMatch(/^jason-harness\/0\.22\.0 reply-cli\/\d+\.\d+\.\d+/);
expect(agents[1]).toMatch(/^reply-cli\/\d+\.\d+\.\d+/);
});

describe('retry policy: reads vs writes', ()=>{
const net_error = (code?: string)=>Object.assign(new TypeError('fetch failed'),
code ? {cause: Object.assign(new Error(code), {code})} : {});
Expand Down
12 changes: 11 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,19 @@ const cli_version = (): string=>{
return version;
};

// One product token and its version, as a program running the CLI names itself.
const CLIENT = /^[a-z][a-z0-9-]{0,31}\/[0-9A-Za-z.+-]{1,32}$/;

// Identifies CLI requests for telemetry (not security). Product token derives
// from APP_NAME to keep the single build identity, e.g. 'reply-cli/0.1.0'.
const user_agent = (): string=>`${APP_NAME}-cli/${cli_version()}`;
// <PREFIX>_CLIENT puts the program running the CLI before it, e.g.
// 'jason-harness/0.22.0 reply-cli/0.17.2'. It is a label, not an input: a value
// of any other shape is left out, and nothing is refused or printed.
const user_agent = (env: Env = process.env): string=>{
const own = `${APP_NAME}-cli/${cli_version()}`;
const client = get_env('CLIENT', env);
return client !== undefined && CLIENT.test(client) ? `${client} ${own}` : own;
};

// Default per-user config dir, mirroring gh/aws/az conventions.
// linux/mac: $XDG_CONFIG_HOME/<app> (fallback ~/.config/<app>)
Expand Down
Loading