From e674f4c20e21710b8b9ea14706c0955a2b681706 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:53:47 +0800 Subject: [PATCH] fix(telemetry): withhold package.json when cordis.yml is absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildTelemetryPayload read the two reported files independently, so a dsh-sdk command mistakenly run in an arbitrary non-SDK directory (no cordis.yml, e.g. any unrelated repo) still uploaded that directory's package.json — dependency names and metadata of a project that never opted into the SDK toolchain. Gate the manifest on cordis.yml presence: without the config the directory is not an SDK project and its manifest is not ours to report. Consent semantics are unchanged. --- packages/sdk/telemetry/README.md | 2 +- packages/sdk/telemetry/src/payload.ts | 13 ++++++++++--- packages/sdk/telemetry/tests/payload.spec.ts | 12 +++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/sdk/telemetry/README.md b/packages/sdk/telemetry/README.md index 43d13d3dcc..8ec3da2c78 100644 --- a/packages/sdk/telemetry/README.md +++ b/packages/sdk/telemetry/README.md @@ -6,7 +6,7 @@ Launcher-side telemetry primitives for the dsh-sdk toolchain. This is a plain li |---|---| | `SecretRedactor` | Conservative safety backstop: replaces secret-shaped values (secret-like keys, known token shapes, PEM blocks, URL credentials, high-entropy opaque tokens) with a placeholder in both parsed values (`redactValue`) and raw text (`redactText`). Never drops a field or line. | | `ConsentResolver` | Parses (never boots) a project `cordis.yml` and reads the telemetry entry's enabled/disabled state as consent; `DO_NOT_TRACK`/CI env force a hard opt-out. | -| `buildTelemetryPayload` | Assembles `{command, durationMs, success, cordisYmlContent, packageJsonContent}`, running the redactor over the full `cordis.yml` and `package.json` text. Never reads `.env`. | +| `buildTelemetryPayload` | Assembles `{command, durationMs, success, cordisYmlContent, packageJsonContent}`, running the redactor over the full `cordis.yml` and `package.json` text. Never reads `.env`; `package.json` ships only alongside a `cordis.yml`, so a command run in a non-SDK directory never uploads that directory's unrelated manifest. | | `getOrCreateAnonymousId` | Random UUID persisted in a per-user GLOBAL config file (never in the project, never derived from git). | | `TelemetryReporter` | Fire-and-forget send: `report()` never blocks or throws; delivery resolves on every path; `flush()` optionally drains in-flight sends within a cap. | diff --git a/packages/sdk/telemetry/src/payload.ts b/packages/sdk/telemetry/src/payload.ts index 96a7b19d76..505cedb872 100644 --- a/packages/sdk/telemetry/src/payload.ts +++ b/packages/sdk/telemetry/src/payload.ts @@ -5,7 +5,9 @@ * the project `cordis.yml` and `package.json`. It NEVER reads or includes `.env` * — secrets live only in `.env`, and the redactor is the backstop for any that * leak into the two reported files. A file that does not exist (the first - * `create` run) simply omits its field. + * `create` run) simply omits its field, and `package.json` ships only when + * `cordis.yml` is present: without it the directory is not an SDK project, and + * its manifest belongs to whatever unrelated project the command ran in. * * @module @deepseek-ai/dsh-telemetry/payload */ @@ -27,7 +29,7 @@ export interface TelemetryPayload { success: boolean /** Redacted full text of the project `cordis.yml`, absent when the file does not exist. */ cordisYmlContent?: string - /** Redacted full text of the project `package.json`, absent when the file does not exist. */ + /** Redacted full text of the project `package.json`, absent when it or `cordis.yml` does not exist. */ packageJsonContent?: string } @@ -70,6 +72,11 @@ export async function buildTelemetryPayload(input: BuildTelemetryPayloadInput): durationMs: input.durationMs, success: input.success, ...cordisYml !== undefined ? { cordisYmlContent: redactor.redactText(cordisYml) } : {}, - ...packageJson !== undefined ? { packageJsonContent: redactor.redactText(packageJson) } : {}, + // package.json is an SDK-project manifest only alongside cordis.yml; a + // command run in an arbitrary directory must not upload that directory's + // unrelated manifest. + ...cordisYml !== undefined && packageJson !== undefined + ? { packageJsonContent: redactor.redactText(packageJson) } + : {}, } } diff --git a/packages/sdk/telemetry/tests/payload.spec.ts b/packages/sdk/telemetry/tests/payload.spec.ts index 1af5139ef5..ed3ab2f82f 100644 --- a/packages/sdk/telemetry/tests/payload.spec.ts +++ b/packages/sdk/telemetry/tests/payload.spec.ts @@ -47,8 +47,18 @@ describe('buildTelemetryPayload', () => { expect('packageJsonContent' in payload).toBe(false) }) + it('withholds package.json when cordis.yml is absent (not an SDK project)', async () => { + const dir = await projectDir({ 'package.json': '{ "name": "unrelated-repo" }' }) + const payload = await buildTelemetryPayload({ command: 'build', durationMs: 3, success: false, projectDir: dir }) + expect('cordisYmlContent' in payload).toBe(false) + expect('packageJsonContent' in payload).toBe(false) + }) + it('uses a supplied redactor', async () => { - const dir = await projectDir({ 'package.json': '{ "password": "hunter2" }' }) + const dir = await projectDir({ + 'cordis.yml': '- id: llm\n name: \'@deepseek-ai/dsh-llm-deepseek\'\n', + 'package.json': '{ "password": "hunter2" }', + }) const redactor = new SecretRedactor({ placeholder: '<>' }) const payload = await buildTelemetryPayload({ command: 'config', durationMs: 5, success: true, projectDir: dir, redactor,