fix(telemetry): withhold package.json when cordis.yml is absent

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.
This commit is contained in:
imccyu
2026-07-18 21:53:47 +08:00
parent 013db6e8f0
commit e674f4c20e
3 changed files with 22 additions and 5 deletions
+1 -1
View File
@@ -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. | | `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. | | `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). | | `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. | | `TelemetryReporter` | Fire-and-forget send: `report()` never blocks or throws; delivery resolves on every path; `flush()` optionally drains in-flight sends within a cap. |
+10 -3
View File
@@ -5,7 +5,9 @@
* the project `cordis.yml` and `package.json`. It NEVER reads or includes `.env` * 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 * — 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 * 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 * @module @deepseek-ai/dsh-telemetry/payload
*/ */
@@ -27,7 +29,7 @@ export interface TelemetryPayload {
success: boolean success: boolean
/** Redacted full text of the project `cordis.yml`, absent when the file does not exist. */ /** Redacted full text of the project `cordis.yml`, absent when the file does not exist. */
cordisYmlContent?: string 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 packageJsonContent?: string
} }
@@ -70,6 +72,11 @@ export async function buildTelemetryPayload(input: BuildTelemetryPayloadInput):
durationMs: input.durationMs, durationMs: input.durationMs,
success: input.success, success: input.success,
...cordisYml !== undefined ? { cordisYmlContent: redactor.redactText(cordisYml) } : {}, ...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) }
: {},
} }
} }
+11 -1
View File
@@ -47,8 +47,18 @@ describe('buildTelemetryPayload', () => {
expect('packageJsonContent' in payload).toBe(false) 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 () => { 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: '<<hidden>>' }) const redactor = new SecretRedactor({ placeholder: '<<hidden>>' })
const payload = await buildTelemetryPayload({ const payload = await buildTelemetryPayload({
command: 'config', durationMs: 5, success: true, projectDir: dir, redactor, command: 'config', durationMs: 5, success: true, projectDir: dir, redactor,