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.
83 lines
3.4 KiB
TypeScript
83 lines
3.4 KiB
TypeScript
/**
|
|
* Telemetry payload assembly.
|
|
*
|
|
* The payload carries the command lifecycle plus the FULL redacted content of
|
|
* 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, 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
|
|
*/
|
|
|
|
import { readFile } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import { SecretRedactor } from './secret-redactor.ts'
|
|
|
|
/** Project files whose full (redacted) content ships with the payload. */
|
|
const REPORTED_FILES = ['cordis.yml', 'package.json'] as const
|
|
|
|
/** One command's telemetry payload. */
|
|
export interface TelemetryPayload {
|
|
/** The dsh-sdk command that ran (`start`/`dev`/`build`/`config`/`create`). */
|
|
command: string
|
|
/** Wall-clock duration of the command in milliseconds. */
|
|
durationMs: number
|
|
/** Whether the command completed without error. */
|
|
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 it or `cordis.yml` does not exist. */
|
|
packageJsonContent?: string
|
|
}
|
|
|
|
/** Inputs for {@link buildTelemetryPayload}. */
|
|
export interface BuildTelemetryPayloadInput {
|
|
/** The dsh-sdk command that ran. */
|
|
command: string
|
|
/** Wall-clock duration of the command in milliseconds. */
|
|
durationMs: number
|
|
/** Whether the command completed without error. */
|
|
success: boolean
|
|
/** Project root whose `cordis.yml` and `package.json` are read. */
|
|
projectDir: string
|
|
/** Redactor applied to reported file content; defaults to a fresh {@link SecretRedactor}. */
|
|
redactor?: SecretRedactor
|
|
}
|
|
|
|
/** Read a project file's text, returning `undefined` when it cannot be read. */
|
|
async function readReportedFile(projectDir: string, name: string): Promise<string | undefined> {
|
|
try {
|
|
return await readFile(join(projectDir, name), 'utf8')
|
|
} catch {
|
|
// Missing/unreadable reported file: telemetry omits the field rather than fail.
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assemble a redacted telemetry payload for one command invocation.
|
|
* @param input - command lifecycle facts, project directory, and optional redactor.
|
|
* @returns the payload with redacted `cordis.yml`/`package.json` content.
|
|
*/
|
|
export async function buildTelemetryPayload(input: BuildTelemetryPayloadInput): Promise<TelemetryPayload> {
|
|
const redactor = input.redactor ?? new SecretRedactor()
|
|
const [cordisYml, packageJson] = await Promise.all(
|
|
REPORTED_FILES.map(name => readReportedFile(input.projectDir, name)),
|
|
)
|
|
return {
|
|
command: input.command,
|
|
durationMs: input.durationMs,
|
|
success: input.success,
|
|
...cordisYml !== undefined ? { cordisYmlContent: redactor.redactText(cordisYml) } : {},
|
|
// 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) }
|
|
: {},
|
|
}
|
|
}
|