Skip to content

Commit afcedbf

Browse files
mariojgtclaude
andcommitted
Report manifests under the production environment by default
The connector never sent an `environment` field, so the backend defaulted every manifest to "sandbox". Since the connector reports from real builds (the prebuild scan / build hooks), the manifest should be "production". - Add `environment` to Config (type) and the wire payload, defaulting to `production`. - Resolve it from `PATCHSTACK_ENVIRONMENT` (env) or `environment` in `.patchstackrc.json`; reject anything other than production|sandbox. - Surface it in `scan` output and `status`; document the env var in --help. - Bump version to 0.2.7. Tests: added body + config-resolution coverage (94 passing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7b9d2e9 commit afcedbf

7 files changed

Lines changed: 95 additions & 13 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@patchstack/connect",
3-
"version": "0.2.6",
3+
"version": "0.2.7",
44
"description": "Patchstack connector for JavaScript applications. Scans your lockfile and reports installed packages to Patchstack for vulnerability monitoring.",
55
"keywords": [
66
"patchstack",

‎src/cli.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Environment:
3333
PATCHSTACK_SITE_UUID Site UUID
3434
PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
3535
PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
36+
PATCHSTACK_ENVIRONMENT Manifest environment: production | sandbox (default: production)
3637
3738
Precedence: CLI flag > environment variable > .patchstackrc.json.
3839
@@ -121,6 +122,7 @@ async function runScan(args: ParsedArgs): Promise<number> {
121122
console.log(
122123
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`,
123124
);
125+
console.log(`Reporting under the ${config.environment} environment.`);
124126
if (stats.duplicateNames.length > 0) {
125127
console.log(`${stats.duplicateNames.length} package(s) appear at multiple versions:`);
126128
if (stats.duplicateNames.length <= 10) {
@@ -184,9 +186,10 @@ async function runStatus(args: ParsedArgs): Promise<number> {
184186
cliSiteUuid: getStringFlag(args.flags, 'site-uuid'),
185187
cliEndpoint: getStringFlag(args.flags, 'endpoint'),
186188
});
187-
console.log(`Site UUID: ${config.siteUuid ?? '(none yet — the next `scan` will provision one)'}`);
188-
console.log(`Endpoint: ${config.endpoint}`);
189-
console.log(`Timeout: ${config.timeoutMs}ms`);
189+
console.log(`Site UUID: ${config.siteUuid ?? '(none yet — the next `scan` will provision one)'}`);
190+
console.log(`Endpoint: ${config.endpoint}`);
191+
console.log(`Timeout: ${config.timeoutMs}ms`);
192+
console.log(`Environment: ${config.environment}`);
190193
if (config.siteUuid !== null) {
191194
console.log(`Claim URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
192195
}

‎src/client.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export async function postManifest(
3939
Accept: 'application/json',
4040
'User-Agent': '@patchstack/connect',
4141
},
42-
body: JSON.stringify(payload),
42+
body: JSON.stringify({ ...payload, environment: config.environment }),
4343
signal: AbortSignal.timeout(timeoutMs),
4444
});
4545
} catch (cause) {

‎src/config.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { readFile, writeFile } from 'node:fs/promises';
22
import path from 'node:path';
3-
import { PatchstackError, type Config } from './types.js';
3+
import { PatchstackError, type Config, type Environment } from './types.js';
44
import { DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS } from './client.js';
55

66
const CONFIG_FILENAME = '.patchstackrc.json';
77

8+
export const DEFAULT_ENVIRONMENT: Environment = 'production';
9+
810
interface ConfigFile {
911
siteUuid?: string;
1012
endpoint?: string;
1113
timeoutMs?: number;
14+
environment?: string;
1215
}
1316

1417
export interface ResolveConfigOptions {
@@ -42,6 +45,15 @@ export async function resolveConfig(options: ResolveConfigOptions): Promise<Conf
4245

4346
const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4447

48+
const environmentRaw = fromEnv.environment ?? fromFile.environment;
49+
if (environmentRaw !== undefined && !isEnvironment(environmentRaw)) {
50+
throw new PatchstackError(
51+
`Environment must be "production" or "sandbox"; got "${environmentRaw}".`,
52+
'CONFIG_INVALID',
53+
);
54+
}
55+
const environment: Environment = environmentRaw ?? DEFAULT_ENVIRONMENT;
56+
4557
if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
4658
throw new PatchstackError(
4759
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
@@ -60,6 +72,7 @@ export async function resolveConfig(options: ResolveConfigOptions): Promise<Conf
6072
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
6173
endpoint,
6274
timeoutMs,
75+
environment,
6376
};
6477
}
6578

@@ -119,13 +132,20 @@ function readEnv(): ConfigFile {
119132
}
120133
timeoutMs = parsed;
121134
}
135+
const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
122136
return {
123137
siteUuid: process.env.PATCHSTACK_SITE_UUID ?? undefined,
124138
endpoint: process.env.PATCHSTACK_ENDPOINT ?? undefined,
125139
timeoutMs,
140+
environment:
141+
environmentRaw !== undefined && environmentRaw.length > 0 ? environmentRaw : undefined,
126142
};
127143
}
128144

129145
function isUuid(value: string): boolean {
130146
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
131147
}
148+
149+
function isEnvironment(value: string): value is Environment {
150+
return value === 'production' || value === 'sandbox';
151+
}

‎src/types.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
export type Ecosystem = 'npm' | 'composer';
22

3+
/**
4+
* Which environment a manifest was captured in. The connector reports from real
5+
* builds (prebuild scan / build hooks), so it defaults to 'production'. Override
6+
* with PATCHSTACK_ENVIRONMENT=sandbox (or "environment" in .patchstackrc.json)
7+
* for test manifests.
8+
*/
9+
export type Environment = 'production' | 'sandbox';
10+
311
export interface PackageEntry {
412
name: string;
513
version: string;
@@ -21,6 +29,8 @@ export interface Config {
2129
siteUuid: string | null;
2230
endpoint: string;
2331
timeoutMs: number;
32+
/** Environment to report the manifest under. Defaults to 'production'. */
33+
environment: Environment;
2434
}
2535

2636
export interface StoreManifestResponse {

‎tests/client.test.ts‎

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,30 @@ describe('postManifest', () => {
7777
vi.stubGlobal('fetch', fetchMock);
7878

7979
const result = await postManifest(
80-
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 30_000 },
80+
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 30_000, environment: 'production' },
8181
{ ecosystem: 'npm', packages: [{ name: 'lodash', version: '4.17.21' }] },
8282
);
8383
expect(result.stored).toBe(true);
8484
expect(result.manifest_id).toBe(1);
8585
});
8686

87+
it('sends the configured environment in the request body', async () => {
88+
const fetchMock = vi.fn().mockResolvedValue(
89+
new Response(JSON.stringify({ stored: true }), { status: 200 }),
90+
);
91+
vi.stubGlobal('fetch', fetchMock);
92+
93+
await postManifest(
94+
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 30_000, environment: 'sandbox' },
95+
{ ecosystem: 'npm', packages: [{ name: 'lodash', version: '4.17.21' }] },
96+
);
97+
98+
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
99+
const body = JSON.parse(init.body as string) as { environment: string; ecosystem: string };
100+
expect(body.environment).toBe('sandbox');
101+
expect(body.ecosystem).toBe('npm');
102+
});
103+
87104
it('throws SITE_NOT_FOUND on 404', async () => {
88105
vi.stubGlobal(
89106
'fetch',
@@ -94,7 +111,7 @@ describe('postManifest', () => {
94111

95112
await expect(
96113
postManifest(
97-
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 30_000 },
114+
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 30_000, environment: 'production' },
98115
{ ecosystem: 'npm', packages: [] },
99116
),
100117
).rejects.toMatchObject({ code: 'SITE_NOT_FOUND' });
@@ -112,7 +129,7 @@ describe('postManifest', () => {
112129

113130
await expect(
114131
postManifest(
115-
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 30_000 },
132+
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 30_000, environment: 'production' },
116133
{ ecosystem: 'npm', packages: [] },
117134
),
118135
).rejects.toMatchObject({ code: 'VALIDATION_ERROR' });
@@ -123,7 +140,7 @@ describe('postManifest', () => {
123140

124141
await expect(
125142
postManifest(
126-
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 30_000 },
143+
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 30_000, environment: 'production' },
127144
{ ecosystem: 'npm', packages: [] },
128145
),
129146
).rejects.toBeInstanceOf(PatchstackError);
@@ -136,7 +153,7 @@ describe('postManifest', () => {
136153
vi.stubGlobal('fetch', fetchMock);
137154

138155
await postManifest(
139-
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 12345 },
156+
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 12345, environment: 'production' },
140157
{ ecosystem: 'npm', packages: [] },
141158
);
142159

@@ -154,7 +171,7 @@ describe('postManifest', () => {
154171
vi.stubGlobal('fetch', fetchMock);
155172

156173
const result = await postManifest(
157-
{ siteUuid: null, endpoint: 'https://example.com/monitor/pulse/manifest', timeoutMs: 30_000 },
174+
{ siteUuid: null, endpoint: 'https://example.com/monitor/pulse/manifest', timeoutMs: 30_000, environment: 'production' },
158175
{ ecosystem: 'npm', packages: [{ name: 'lodash', version: '4.17.21' }] },
159176
);
160177

@@ -171,7 +188,7 @@ describe('postManifest', () => {
171188

172189
await expect(
173190
postManifest(
174-
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 1 },
191+
{ siteUuid: 'uuid', endpoint: 'https://example.com', timeoutMs: 1, environment: 'production' },
175192
{ ecosystem: 'npm', packages: [] },
176193
),
177194
).rejects.toMatchObject({ code: 'NETWORK_TIMEOUT' });

‎tests/config.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('resolveConfig', () => {
1818
delete process.env.PATCHSTACK_SITE_UUID;
1919
delete process.env.PATCHSTACK_ENDPOINT;
2020
delete process.env.PATCHSTACK_TIMEOUT_MS;
21+
delete process.env.PATCHSTACK_ENVIRONMENT;
2122
});
2223

2324
afterEach(async () => {
@@ -92,4 +93,35 @@ describe('resolveConfig', () => {
9293
code: 'CONFIG_INVALID',
9394
});
9495
});
96+
97+
it('defaults the environment to production', async () => {
98+
const config = await resolveConfig({ cwd, cliSiteUuid: VALID_UUID });
99+
expect(config.environment).toBe('production');
100+
});
101+
102+
it('reads PATCHSTACK_ENVIRONMENT from the environment', async () => {
103+
process.env.PATCHSTACK_ENVIRONMENT = 'sandbox';
104+
const config = await resolveConfig({ cwd, cliSiteUuid: VALID_UUID });
105+
expect(config.environment).toBe('sandbox');
106+
});
107+
108+
it('reads environment from the config file', async () => {
109+
await writeConfigFile(cwd, { siteUuid: VALID_UUID, environment: 'sandbox' });
110+
const config = await resolveConfig({ cwd });
111+
expect(config.environment).toBe('sandbox');
112+
});
113+
114+
it('lets PATCHSTACK_ENVIRONMENT override the file', async () => {
115+
await writeConfigFile(cwd, { siteUuid: VALID_UUID, environment: 'sandbox' });
116+
process.env.PATCHSTACK_ENVIRONMENT = 'production';
117+
const config = await resolveConfig({ cwd });
118+
expect(config.environment).toBe('production');
119+
});
120+
121+
it('throws CONFIG_INVALID when the environment is not production or sandbox', async () => {
122+
process.env.PATCHSTACK_ENVIRONMENT = 'staging';
123+
await expect(resolveConfig({ cwd, cliSiteUuid: VALID_UUID })).rejects.toMatchObject({
124+
code: 'CONFIG_INVALID',
125+
});
126+
});
95127
});

0 commit comments

Comments
 (0)