@mediaruntime/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/cli.d.ts +79 -0
- package/dist/cli.js +895 -0
- package/dist/cli.js.map +1 -0
- package/docs/CLI_SDD.md +466 -0
- package/docs/TRIGGER.md +41 -0
- package/package.json +68 -0
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/errors.ts","../src/commands/jobs.ts","../src/commands/run.ts","../src/commands/trigger.ts","../src/trigger/destination.ts","../src/trigger/event.ts","../src/trigger/signature.ts"],"sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { constants, realpathSync } from \"node:fs\";\nimport {\n access,\n link,\n rename,\n rm,\n} from \"node:fs/promises\";\nimport { createWriteStream } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { Readable, Transform } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { ReadableStream as NodeReadableStream } from \"node:stream/web\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n JobWaitTimeoutError,\n MediaRuntime,\n MediaRuntimeApiError,\n MediaRuntimeConnectionError,\n} from \"@mediaruntime/node\";\nimport {\n runJobsCommand,\n type JobsReadClient,\n} from \"./commands/jobs.js\";\nimport {\n runCommand,\n type RunJobsClient,\n} from \"./commands/run.js\";\nimport { runTriggerCommand } from \"./commands/trigger.js\";\nimport { BundleDownloadError, CliError, UsageError } from \"./errors.js\";\nimport { isLoopbackDestination } from \"./trigger/destination.js\";\n\nexport { parseTriggerArguments, runTriggerCommand } from \"./commands/trigger.js\";\nexport { createSyntheticTerminalEvent } from \"./trigger/event.js\";\nexport { signSyntheticWebhook } from \"./trigger/signature.js\";\n\nconst VERSION = \"0.1.0\";\nconst HELP = `MediaRuntime CLI\n\nUsage:\n mediaruntime run <source> --output <alias> [--output <alias>] [--wait]\n mediaruntime jobs list [--status <status>] [--limit <n>] [--cursor <cursor>]\n mediaruntime jobs get <job_id> [--download <bundle.zip>] [--force]\n mediaruntime trigger <job.completed|job.failed|job.rejected> --to <local-url>\n\nGlobal options:\n --base-url <url> Developer override; normally leave unset\n --help Show help\n --version Show version\n\nRun/jobs options:\n --json Machine-readable, URL-redacted output\n\nAuthentication uses MEDIARUNTIME_API_KEY.\n`;\n\ntype CliJobsClient = RunJobsClient & JobsReadClient;\n\nexport interface CliClient {\n jobs: CliJobsClient;\n}\n\nexport interface CliDependencies {\n createClient?(options: { baseUrl?: string }): CliClient;\n downloadBundle?(\n url: string,\n destination: string,\n options: {\n force: boolean;\n expectedSizeBytes?: number | null;\n expectedSha256?: string | null;\n },\n ): Promise<void>;\n writeStdout?(text: string): void;\n writeStderr?(text: string): void;\n}\n\nfunction isFileExistsError(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"EEXIST\";\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function downloadBundleAtomically(\n url: string,\n destination: string,\n options: {\n force: boolean;\n expectedSizeBytes?: number | null;\n expectedSha256?: string | null;\n },\n): Promise<void> {\n const target = resolve(destination);\n if (!options.force && await exists(target)) {\n throw new BundleDownloadError(`Refusing to overwrite existing file: ${destination}`);\n }\n const temporary = resolve(\n dirname(target),\n `.${basename(target)}.${randomUUID()}.part`,\n );\n\n try {\n let response: Response;\n try {\n response = await fetch(url);\n } catch (error) {\n throw new BundleDownloadError(\"Bundle download failed before receiving a response\", { cause: error });\n }\n if (!response.ok) {\n await response.body?.cancel();\n throw new BundleDownloadError(`Bundle download failed with HTTP ${response.status}`);\n }\n if (!response.body) throw new BundleDownloadError(\"Bundle download returned an empty response body\");\n const hash = createHash(\"sha256\");\n let sizeBytes = 0;\n const integrity = new Transform({\n transform(chunk: Buffer, _encoding, callback) {\n sizeBytes += chunk.length;\n hash.update(chunk);\n callback(null, chunk);\n },\n });\n await pipeline(\n Readable.fromWeb(response.body as unknown as NodeReadableStream<Uint8Array>),\n integrity,\n createWriteStream(temporary, { flags: \"wx\", mode: 0o600 }),\n );\n const sha256 = hash.digest(\"hex\");\n if (options.expectedSizeBytes !== undefined && options.expectedSizeBytes !== null &&\n sizeBytes !== options.expectedSizeBytes) {\n throw new BundleDownloadError(\n `Bundle size mismatch: expected ${options.expectedSizeBytes} bytes, received ${sizeBytes}`,\n );\n }\n if (options.expectedSha256 && sha256 !== options.expectedSha256.toLowerCase()) {\n throw new BundleDownloadError(\"Bundle SHA-256 mismatch\");\n }\n\n if (options.force) {\n await rename(temporary, target);\n } else {\n try {\n // A hard link publishes the completed same-filesystem temporary file atomically\n // and fails rather than replacing a destination created during the download.\n await link(temporary, target);\n } catch (error) {\n if (isFileExistsError(error)) {\n throw new BundleDownloadError(`Refusing to overwrite existing file: ${destination}`);\n }\n throw error;\n }\n await rm(temporary);\n }\n } catch (error) {\n await rm(temporary, { force: true });\n if (error instanceof BundleDownloadError) throw error;\n throw new BundleDownloadError(`Could not write bundle to ${destination}`, { cause: error });\n }\n}\n\nfunction normalizeBaseUrl(raw: string): string {\n let value: URL;\n try {\n value = new URL(raw);\n } catch {\n throw new UsageError(\"--base-url must be an absolute HTTP(S) origin\");\n }\n if (value.username || value.password || value.search || value.hash || value.pathname !== \"/\") {\n throw new UsageError(\"--base-url must be an origin without credentials, path, query, or fragment\");\n }\n if (value.protocol !== \"https:\" && !(value.protocol === \"http:\" && isLoopbackDestination(value))) {\n throw new UsageError(\"--base-url must use HTTPS unless it points to loopback\");\n }\n return value.origin;\n}\n\nfunction extractGlobalOptions(argv: string[]): { baseUrl?: string; args: string[] } {\n const args = [...argv];\n let baseUrl: string | undefined;\n while (args[0] === \"--base-url\") {\n const value = args[1];\n if (!value || value.startsWith(\"--\")) throw new UsageError(\"--base-url requires a value\");\n baseUrl = normalizeBaseUrl(value);\n args.splice(0, 2);\n }\n return { ...(baseUrl === undefined ? {} : { baseUrl }), args };\n}\n\nfunction safeApiError(error: MediaRuntimeApiError): Record<string, unknown> {\n return {\n name: error.name,\n message: error.message,\n code: error.code,\n status: error.status,\n retryable: error.retryable,\n requestId: error.requestId ?? null,\n field: error.field ?? null,\n };\n}\n\nfunction exitCodeFor(error: unknown): number {\n if (error instanceof CliError) return error.exitCode;\n if (error instanceof JobWaitTimeoutError) return 7;\n if (error instanceof MediaRuntimeConnectionError) return 5;\n if (error instanceof MediaRuntimeApiError) {\n if ([401, 402, 403].includes(error.status)) return 3;\n if (error.status >= 500) return 5;\n return 4;\n }\n if (error instanceof DOMException && error.name === \"AbortError\") return 130;\n if (error instanceof TypeError && /fetch|network|socket|connect/i.test(error.message)) return 5;\n return 1;\n}\n\nfunction safeError(error: unknown, exitCode: number): Record<string, unknown> {\n if (error instanceof MediaRuntimeApiError) return { ...safeApiError(error), exitCode };\n return {\n code: error instanceof CliError ? error.code : \"cli_error\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n exitCode,\n };\n}\n\nexport async function executeCli(\n argv: string[],\n dependencies: CliDependencies = {},\n): Promise<number> {\n const writeStdout = dependencies.writeStdout ?? ((text: string) => process.stdout.write(text));\n const writeStderr = dependencies.writeStderr ?? ((text: string) => process.stderr.write(text));\n const json = argv.includes(\"--json\");\n try {\n const global = extractGlobalOptions(argv);\n const command = global.args[0];\n if (!command || command === \"--help\" || command === \"-h\") {\n writeStdout(HELP);\n return 0;\n }\n if (command === \"--version\" || command === \"-v\") {\n writeStdout(`${VERSION}\\n`);\n return 0;\n }\n if (command === \"trigger\") {\n return await runTriggerCommand(global.args.slice(1), { writeStdout });\n }\n\n const configuredBaseUrl = global.baseUrl ?? process.env.MEDIARUNTIME_API_URL;\n const baseUrl = configuredBaseUrl ? normalizeBaseUrl(configuredBaseUrl) : undefined;\n if (!dependencies.createClient && !process.env.MEDIARUNTIME_API_KEY?.trim()) {\n throw new UsageError(\"Set MEDIARUNTIME_API_KEY before using run or jobs\");\n }\n const client = dependencies.createClient?.({\n ...(baseUrl === undefined ? {} : { baseUrl }),\n }) ?? new MediaRuntime({ ...(baseUrl === undefined ? {} : { baseUrl }) });\n const downloadBundle = dependencies.downloadBundle ?? downloadBundleAtomically;\n const commandDependencies = { jobs: client.jobs, writeStdout, downloadBundle };\n if (command === \"run\") return await runCommand(global.args.slice(1), commandDependencies);\n if (command === \"jobs\") {\n return await runJobsCommand(global.args.slice(1), commandDependencies);\n }\n throw new UsageError(`Unknown command: ${command}`);\n } catch (error) {\n const exitCode = exitCodeFor(error);\n if (json) {\n writeStderr(`${JSON.stringify({ error: safeError(error, exitCode) })}\\n`);\n } else {\n const requestId = error instanceof MediaRuntimeApiError && error.requestId\n ? ` (request ${error.requestId})`\n : \"\";\n writeStderr(`Error: ${error instanceof Error ? error.message : \"Unknown error\"}${requestId}\\n`);\n }\n return exitCode;\n }\n}\n\nlet isEntrypoint = false;\nif (process.argv[1] !== undefined) {\n try {\n // npm exposes binaries through a symlink. Resolve both sides so the installed command\n // executes while importing dist/cli.js in tests remains side-effect free.\n isEntrypoint = realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));\n } catch {\n isEntrypoint = false;\n }\n}\nif (isEntrypoint) process.exitCode = await executeCli(process.argv.slice(2));\n","export class CliError extends Error {\n readonly code: string;\n readonly exitCode: number;\n\n constructor(code: string, message: string, exitCode: number, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"CliError\";\n this.code = code;\n this.exitCode = exitCode;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\nexport class UsageError extends CliError {\n constructor(message: string, options?: { cause?: unknown }) {\n super(\"usage_error\", message, 2, options);\n this.name = \"UsageError\";\n }\n}\n\nexport class TriggerDeliveryError extends CliError {\n constructor(message: string, options?: { cause?: unknown }) {\n super(\"trigger_delivery_error\", message, 8, options);\n this.name = \"TriggerDeliveryError\";\n }\n}\n\nexport class BundleDownloadError extends CliError {\n constructor(message: string, options?: { cause?: unknown }) {\n super(\"bundle_download_error\", message, 9, options);\n this.name = \"BundleDownloadError\";\n }\n}\n","import type { JobDetails, JobPage, JobStatus, ListJobsParams } from \"@mediaruntime/node\";\nimport { BundleDownloadError, UsageError } from \"../errors.js\";\n\nconst UNSUCCESSFUL_TERMINAL_STATUSES = new Set([\"FAILED\", \"REJECTED\", \"PARTIAL\"]);\n\nexport interface JobsReadClient {\n list(params?: ListJobsParams): Promise<JobPage>;\n get(jobId: string): Promise<JobDetails>;\n}\n\nexport interface JobsCommandDependencies {\n jobs: JobsReadClient;\n writeStdout(text: string): void;\n downloadBundle(\n url: string,\n destination: string,\n options: { force: boolean; expectedSizeBytes?: number | null; expectedSha256?: string | null },\n ): Promise<void>;\n}\n\nfunction optionValue(args: string[], index: number, option: string): string {\n const value = args[index + 1];\n if (!value || value.startsWith(\"--\")) throw new UsageError(`${option} requires a value`);\n return value;\n}\n\nfunction detailsProjection(job: JobDetails): Record<string, unknown> {\n return {\n id: job.id,\n status: job.status,\n tier: job.tier,\n usage: job.usage,\n billing: job.billing,\n bundle: {\n available: job.bundle.available,\n expiresAt: job.bundle.expiresAt,\n sizeBytes: job.bundle.sizeBytes,\n sha256: job.bundle.sha256,\n retentionDays: job.bundle.retentionDays,\n },\n media: job.media,\n metadata: job.metadata,\n error: job.error,\n createdAt: job.createdAt,\n updatedAt: job.updatedAt,\n startedAt: job.startedAt,\n completedAt: job.completedAt,\n };\n}\n\nfunction humanDetails(job: JobDetails): string {\n const lines = [\n `Job: ${job.id}`,\n `Status: ${job.status}`,\n `Tier: ${job.tier.billed ?? job.tier.effective ?? job.tier.requested ?? \"-\"}`,\n `Bundle: ${job.bundle.available ? \"available\" : \"unavailable\"}`,\n ];\n if (job.bundle.sizeBytes !== null) lines.push(`Bundle size: ${job.bundle.sizeBytes} bytes`);\n if (job.error) lines.push(`Error: ${job.error}`);\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction cell(value: unknown): string {\n if (value === null || value === undefined || value === \"\") return \"-\";\n return String(value);\n}\n\nfunction humanList(page: JobPage): string {\n const rows = page.jobs.map((job) => [\n job.id,\n job.status,\n cell(job.tierBilled),\n cell(job.unitsTotal),\n job.bundleAvailable ? \"yes\" : \"no\",\n cell(job.updatedAt),\n ]);\n const headings = [\"ID\", \"STATUS\", \"TIER\", \"UNITS\", \"BUNDLE\", \"UPDATED\"];\n const widths = headings.map((heading, index) =>\n Math.max(heading.length, ...rows.map((row) => row[index]?.length ?? 0)),\n );\n const render = (row: string[]): string =>\n row.map((value, index) => value.padEnd(widths[index] ?? value.length)).join(\" \").trimEnd();\n const lines = [render(headings), render(widths.map((width) => \"-\".repeat(width))), ...rows.map(render)];\n if (page.nextCursor) lines.push(`Next cursor: ${page.nextCursor}`);\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nasync function listCommand(args: string[], dependencies: JobsCommandDependencies): Promise<number> {\n let status: string | undefined;\n let limit: number | undefined;\n let cursor: string | undefined;\n let json = false;\n for (let index = 0; index < args.length; index += 1) {\n const argument = args[index];\n if (argument === \"--status\") {\n status = optionValue(args, index, argument).toUpperCase();\n index += 1;\n } else if (argument === \"--limit\") {\n const value = Number(optionValue(args, index, argument));\n if (!Number.isSafeInteger(value) || value < 1 || value > 100) {\n throw new UsageError(\"--limit must be an integer between 1 and 100\");\n }\n limit = value;\n index += 1;\n } else if (argument === \"--cursor\") {\n cursor = optionValue(args, index, argument);\n index += 1;\n } else if (argument === \"--json\") {\n json = true;\n } else {\n throw new UsageError(`Unknown jobs list option: ${argument}`);\n }\n }\n const params: ListJobsParams = {\n ...(status === undefined ? {} : { status: status as JobStatus }),\n ...(limit === undefined ? {} : { limit }),\n ...(cursor === undefined ? {} : { cursor }),\n };\n const page = await dependencies.jobs.list(params);\n dependencies.writeStdout(json ? `${JSON.stringify(page)}\\n` : humanList(page));\n return 0;\n}\n\nasync function getCommand(args: string[], dependencies: JobsCommandDependencies): Promise<number> {\n let jobId: string | undefined;\n let download: string | undefined;\n let force = false;\n let json = false;\n for (let index = 0; index < args.length; index += 1) {\n const argument = args[index];\n if (argument === \"--download\") {\n download = optionValue(args, index, argument);\n index += 1;\n } else if (argument === \"--force\") {\n force = true;\n } else if (argument === \"--json\") {\n json = true;\n } else if (argument?.startsWith(\"-\")) {\n throw new UsageError(`Unknown jobs get option: ${argument}`);\n } else if (!jobId) {\n jobId = argument;\n } else {\n throw new UsageError(\"jobs get accepts exactly one job ID\");\n }\n }\n if (!jobId) throw new UsageError(\"Usage: mediaruntime jobs get <job_id>\");\n if (force && !download) throw new UsageError(\"--force requires --download\");\n const job = await dependencies.jobs.get(jobId);\n if (!json) dependencies.writeStdout(humanDetails(job));\n\n if (UNSUCCESSFUL_TERMINAL_STATUSES.has(String(job.status).toUpperCase())) {\n if (json) dependencies.writeStdout(`${JSON.stringify(detailsProjection(job))}\\n`);\n return 6;\n }\n if (download) {\n if (String(job.status).toUpperCase() !== \"COMPLETED\" ||\n !job.bundle.available || !job.bundle.downloadUrl) {\n throw new BundleDownloadError(\"Bundle download requires a COMPLETED job with an available canonical bundle\");\n }\n try {\n await dependencies.downloadBundle(job.bundle.downloadUrl, download, {\n force,\n expectedSizeBytes: job.bundle.sizeBytes,\n expectedSha256: job.bundle.sha256,\n });\n } catch (error) {\n if (error instanceof BundleDownloadError) throw error;\n throw new BundleDownloadError(`Could not download bundle to ${download}`, { cause: error });\n }\n if (!json) dependencies.writeStdout(`Downloaded bundle to ${download}\\n`);\n }\n if (json) dependencies.writeStdout(`${JSON.stringify(detailsProjection(job))}\\n`);\n return 0;\n}\n\nexport async function runJobsCommand(\n args: string[],\n dependencies: JobsCommandDependencies,\n): Promise<number> {\n const subcommand = args[0];\n if (subcommand === \"list\") return listCommand(args.slice(1), dependencies);\n if (subcommand === \"get\") return getCommand(args.slice(1), dependencies);\n throw new UsageError(\"Usage: mediaruntime jobs <list|get>\");\n}\n","import type {\n CreateJobParams,\n JobDetails,\n JobOutput,\n JobReceiptData,\n Metadata,\n OutputAlias,\n WaitForJobOptions,\n} from \"@mediaruntime/node\";\nimport { BundleDownloadError, UsageError } from \"../errors.js\";\n\nconst UNSUCCESSFUL_TERMINAL_STATUSES = new Set([\"FAILED\", \"REJECTED\", \"PARTIAL\"]);\nconst OUTPUT_ALIASES = new Set([\n \"video.web\",\n \"video.streaming\",\n \"video.social\",\n \"audio.web\",\n \"audio.transcription\",\n \"image.web\",\n]);\n\ninterface SubmittedJob extends JobReceiptData {\n wait(options?: WaitForJobOptions): Promise<JobDetails>;\n}\n\nexport interface RunJobsClient {\n create(params: CreateJobParams): Promise<SubmittedJob>;\n}\n\nexport interface RunCommandDependencies {\n jobs: RunJobsClient;\n writeStdout(text: string): void;\n downloadBundle(\n url: string,\n destination: string,\n options: { force: boolean; expectedSizeBytes?: number | null; expectedSha256?: string | null },\n ): Promise<void>;\n}\n\ninterface RunOptions {\n source: string;\n outputs: string[];\n metadata?: Metadata;\n idempotencyKey?: string;\n wait: boolean;\n timeoutMs?: number;\n download?: string;\n force: boolean;\n json: boolean;\n}\n\nfunction optionValue(args: string[], index: number, option: string): string {\n const value = args[index + 1];\n if (!value || value.startsWith(\"--\")) throw new UsageError(`${option} requires a value`);\n return value;\n}\n\nfunction parseMetadata(value: string): Metadata {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value) as unknown;\n } catch {\n throw new UsageError(\"--metadata must be a valid JSON object\");\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new UsageError(\"--metadata must be a JSON object\");\n }\n return parsed as Metadata;\n}\n\nfunction parseRunOptions(args: string[]): RunOptions {\n let source: string | undefined;\n const outputs: string[] = [];\n let metadata: Metadata | undefined;\n let idempotencyKey: string | undefined;\n let wait = false;\n let timeoutMs: number | undefined;\n let download: string | undefined;\n let force = false;\n let json = false;\n\n for (let index = 0; index < args.length; index += 1) {\n const argument = args[index];\n if (argument === \"--output\" || argument === \"-o\") {\n outputs.push(optionValue(args, index, argument));\n index += 1;\n } else if (argument === \"--metadata\") {\n metadata = parseMetadata(optionValue(args, index, argument));\n index += 1;\n } else if (argument === \"--idempotency-key\") {\n idempotencyKey = optionValue(args, index, argument);\n index += 1;\n } else if (argument === \"--wait\") {\n wait = true;\n } else if (argument === \"--timeout-ms\") {\n const value = Number(optionValue(args, index, argument));\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new UsageError(\"--timeout-ms must be a positive integer\");\n }\n timeoutMs = value;\n index += 1;\n } else if (argument === \"--download\") {\n download = optionValue(args, index, argument);\n wait = true;\n index += 1;\n } else if (argument === \"--force\") {\n force = true;\n } else if (argument === \"--json\") {\n json = true;\n } else if (argument?.startsWith(\"-\")) {\n throw new UsageError(`Unknown run option: ${argument}`);\n } else if (source === undefined) {\n source = argument;\n } else {\n throw new UsageError(\"run accepts exactly one source\");\n }\n }\n\n if (!source) throw new UsageError(\"Usage: mediaruntime run <source> --output <alias> [--wait]\");\n if (outputs.length === 0) throw new UsageError(\"run requires at least one --output\");\n const unsupported = outputs.find((output) => !OUTPUT_ALIASES.has(output));\n if (unsupported) throw new UsageError(`Unsupported output alias: ${unsupported}`);\n if (force && !download) throw new UsageError(\"--force requires --download\");\n if (timeoutMs !== undefined && !wait) throw new UsageError(\"--timeout-ms requires --wait or --download\");\n return {\n source,\n outputs,\n ...(metadata === undefined ? {} : { metadata }),\n ...(idempotencyKey === undefined ? {} : { idempotencyKey }),\n wait,\n ...(timeoutMs === undefined ? {} : { timeoutMs }),\n ...(download === undefined ? {} : { download }),\n force,\n json,\n };\n}\n\nfunction receiptProjection(job: SubmittedJob): JobReceiptData {\n return {\n id: job.id,\n status: job.status,\n tier: job.tier,\n requiredTier: job.requiredTier,\n outputs: job.outputs,\n message: job.message,\n };\n}\n\nfunction detailsProjection(job: JobDetails): Record<string, unknown> {\n return {\n id: job.id,\n status: job.status,\n tier: job.tier,\n usage: job.usage,\n billing: job.billing,\n bundle: {\n available: job.bundle.available,\n expiresAt: job.bundle.expiresAt,\n sizeBytes: job.bundle.sizeBytes,\n sha256: job.bundle.sha256,\n retentionDays: job.bundle.retentionDays,\n },\n media: job.media,\n metadata: job.metadata,\n error: job.error,\n createdAt: job.createdAt,\n updatedAt: job.updatedAt,\n startedAt: job.startedAt,\n completedAt: job.completedAt,\n };\n}\n\nfunction writeHumanReceipt(job: SubmittedJob, write: (text: string) => void): void {\n write(`Job: ${job.id}\\nStatus: ${job.status}\\nTier: ${job.tier || \"-\"}\\n`);\n}\n\nfunction writeHumanDetails(job: JobDetails, write: (text: string) => void): void {\n const error = job.error ? `\\nError: ${job.error}` : \"\";\n write(\n `Job: ${job.id}\\nStatus: ${job.status}\\n` +\n `Bundle: ${job.bundle.available ? \"available\" : \"unavailable\"}${error}\\n`,\n );\n}\n\nexport async function runCommand(\n args: string[],\n dependencies: RunCommandDependencies,\n): Promise<number> {\n const options = parseRunOptions(args);\n const params: CreateJobParams = {\n source: options.source,\n outputs: options.outputs as Array<OutputAlias | JobOutput>,\n ...(options.metadata === undefined ? {} : { metadata: options.metadata }),\n ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),\n };\n const submitted = await dependencies.jobs.create(params);\n\n if (!options.wait) {\n if (options.json) dependencies.writeStdout(`${JSON.stringify(receiptProjection(submitted))}\\n`);\n else writeHumanReceipt(submitted, dependencies.writeStdout);\n return UNSUCCESSFUL_TERMINAL_STATUSES.has(String(submitted.status).toUpperCase()) ? 6 : 0;\n }\n\n const details = await submitted.wait(\n options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs },\n );\n if (!options.json) writeHumanDetails(details, dependencies.writeStdout);\n if (String(details.status).toUpperCase() !== \"COMPLETED\") {\n if (options.json) dependencies.writeStdout(`${JSON.stringify(detailsProjection(details))}\\n`);\n return 6;\n }\n if (options.download) {\n const url = details.bundle.downloadUrl;\n if (!details.bundle.available || !url) {\n throw new BundleDownloadError(\"Completed job does not have an available canonical bundle\");\n }\n try {\n await dependencies.downloadBundle(url, options.download, {\n force: options.force,\n expectedSizeBytes: details.bundle.sizeBytes,\n expectedSha256: details.bundle.sha256,\n });\n } catch (error) {\n if (error instanceof BundleDownloadError) throw error;\n throw new BundleDownloadError(`Could not download bundle to ${options.download}`, { cause: error });\n }\n if (!options.json) dependencies.writeStdout(`Downloaded bundle to ${options.download}\\n`);\n }\n if (options.json) dependencies.writeStdout(`${JSON.stringify(detailsProjection(details))}\\n`);\n return 0;\n}\n","import { randomBytes, randomUUID } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\n\nimport { parseLocalDestination } from \"../trigger/destination.js\";\nimport {\n SUPPORTED_TRIGGER_EVENTS,\n createSyntheticTerminalEvent,\n isTriggerEventType,\n type TriggerEventType,\n} from \"../trigger/event.js\";\nimport { signSyntheticWebhook } from \"../trigger/signature.js\";\nimport { TriggerDeliveryError, UsageError } from \"../errors.js\";\n\nexport interface TriggerCommandDependencies {\n env?: Readonly<Record<string, string | undefined>>;\n fetch?: typeof globalThis.fetch;\n now?: () => number;\n randomUUID?: () => string;\n randomBytes?: (size: number) => Uint8Array;\n readFile?: (path: string, encoding: \"utf8\") => Promise<string>;\n writeStdout?: (text: string) => void;\n}\n\ninterface TriggerArguments {\n type: TriggerEventType;\n destination: URL;\n secretFile?: string;\n generateSecret: boolean;\n jobId?: string;\n accountId?: string;\n json: boolean;\n}\n\nfunction requireOptionValue(args: string[], index: number, option: string): string {\n const value = args[index + 1];\n if (!value || value.startsWith(\"--\")) throw new UsageError(`${option} requires a value`);\n return value;\n}\n\nexport function parseTriggerArguments(args: string[]): TriggerArguments {\n const typeValue = args[0];\n if (!typeValue || typeValue.startsWith(\"--\")) {\n throw new UsageError(\n `trigger requires an event type: ${SUPPORTED_TRIGGER_EVENTS.join(\", \")}`,\n );\n }\n if (!isTriggerEventType(typeValue)) {\n throw new UsageError(\n `Unsupported trigger event ${JSON.stringify(typeValue)}; expected ${SUPPORTED_TRIGGER_EVENTS.join(\", \")}`,\n );\n }\n\n let to: string | undefined;\n let secretFile: string | undefined;\n let generateSecret = false;\n let jobId: string | undefined;\n let accountId: string | undefined;\n let json = false;\n for (let index = 1; index < args.length; index += 1) {\n const option = args[index];\n if (!option) continue;\n if (option === \"--generate-secret\") {\n generateSecret = true;\n continue;\n }\n if (option === \"--json\") {\n json = true;\n continue;\n }\n if ([\"--to\", \"--secret-file\", \"--job-id\", \"--account-id\"].includes(option)) {\n const value = requireOptionValue(args, index, option);\n index += 1;\n if (option === \"--to\") to = value;\n else if (option === \"--secret-file\") secretFile = value;\n else if (option === \"--job-id\") jobId = value;\n else accountId = value;\n continue;\n }\n throw new UsageError(`Unknown trigger option: ${option}`);\n }\n\n if (!to) throw new UsageError(\"trigger requires an explicit --to local URL\");\n if (generateSecret && secretFile) {\n throw new UsageError(\"--generate-secret and --secret-file are mutually exclusive\");\n }\n if (generateSecret && json) {\n throw new UsageError(\"--generate-secret and --json are mutually exclusive\");\n }\n if (jobId !== undefined && !jobId.trim()) throw new UsageError(\"--job-id must not be empty\");\n if (accountId !== undefined && !accountId.trim()) {\n throw new UsageError(\"--account-id must not be empty\");\n }\n return {\n type: typeValue,\n destination: parseLocalDestination(to),\n ...(secretFile === undefined ? {} : { secretFile }),\n generateSecret,\n ...(jobId === undefined ? {} : { jobId: jobId.trim() }),\n ...(accountId === undefined ? {} : { accountId: accountId.trim() }),\n json,\n };\n}\n\nasync function resolveSecret(\n args: TriggerArguments,\n dependencies: Required<\n Pick<TriggerCommandDependencies, \"env\" | \"randomBytes\" | \"readFile\" | \"writeStdout\">\n >,\n): Promise<string> {\n if (args.generateSecret) {\n const generated = Buffer.from(dependencies.randomBytes(32)).toString(\"base64url\");\n dependencies.writeStdout(\n `Generated webhook secret (configure the local receiver with this value): ${generated}\\n`,\n );\n return generated;\n }\n let secret: string | undefined;\n if (args.secretFile) {\n try {\n secret = await dependencies.readFile(args.secretFile, \"utf8\");\n } catch (error) {\n throw new UsageError(`Could not read webhook secret file: ${args.secretFile}`, { cause: error });\n }\n } else {\n secret = dependencies.env.MEDIARUNTIME_WEBHOOK_SECRET;\n }\n const normalized = secret?.trim();\n if (!normalized) {\n throw new UsageError(\n \"Set MEDIARUNTIME_WEBHOOK_SECRET, pass --secret-file, or explicitly use --generate-secret\",\n );\n }\n return normalized;\n}\n\nexport async function runTriggerCommand(\n args: string[],\n dependencies: TriggerCommandDependencies = {},\n): Promise<number> {\n const parsed = parseTriggerArguments(args);\n const deps = {\n env: dependencies.env ?? process.env,\n fetch: dependencies.fetch ?? globalThis.fetch,\n now: dependencies.now ?? Date.now,\n randomUUID: dependencies.randomUUID ?? randomUUID,\n randomBytes: dependencies.randomBytes ?? randomBytes,\n readFile: dependencies.readFile ?? readFile,\n writeStdout: dependencies.writeStdout ?? ((text: string) => process.stdout.write(text)),\n };\n const secret = await resolveSecret(parsed, deps);\n const timestamp = Math.floor(deps.now() / 1000);\n const unique = deps.randomUUID();\n const eventId = `evt_cli_${unique}`;\n const jobId = parsed.jobId ?? `job_cli_${unique}`;\n const accountId = parsed.accountId ?? \"acc_cli_local\";\n const payload = createSyntheticTerminalEvent(parsed.type, {\n eventId,\n jobId,\n accountId,\n timestamp,\n });\n const signed = signSyntheticWebhook(payload, { eventId, timestamp, secret });\n\n const response = await deps.fetch(parsed.destination, {\n method: \"POST\",\n headers: signed.headers,\n body: signed.body,\n redirect: \"manual\",\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n throw new TriggerDeliveryError(\n `Local webhook endpoint returned HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : \"\"}`,\n );\n }\n if (parsed.json) {\n deps.writeStdout(`${JSON.stringify({\n eventId,\n type: parsed.type,\n jobId,\n status: payload.status,\n destination: parsed.destination.href,\n httpStatus: response.status,\n })}\\n`);\n } else {\n deps.writeStdout(\n `Sent synthetic ${parsed.type} event ${eventId} to ${parsed.destination.href} (HTTP ${response.status})\\n`,\n );\n }\n return 0;\n}\n","import { UsageError } from \"../errors.js\";\n\nexport function isLoopbackDestination(value: URL): boolean {\n let hostname = value.hostname.toLowerCase();\n if (hostname.endsWith(\".\")) hostname = hostname.slice(0, -1);\n if (hostname.startsWith(\"[\") && hostname.endsWith(\"]\")) {\n hostname = hostname.slice(1, -1);\n }\n if (hostname === \"localhost\" || hostname === \"::1\" || hostname === \"0:0:0:0:0:0:0:1\") {\n return true;\n }\n\n const octets = hostname.split(\".\");\n if (octets.length !== 4 || octets.some((part) => !/^\\d{1,3}$/.test(part))) return false;\n const numbers = octets.map(Number);\n return numbers[0] === 127 && numbers.every((part) => part >= 0 && part <= 255);\n}\n\nexport function parseLocalDestination(raw: string): URL {\n let destination: URL;\n try {\n destination = new URL(raw);\n } catch {\n throw new UsageError(\"--to must be an absolute local HTTP(S) URL\");\n }\n if (destination.protocol !== \"http:\" && destination.protocol !== \"https:\") {\n throw new UsageError(\"--to must use http:// or https://\");\n }\n if (destination.username || destination.password) {\n throw new UsageError(\"--to must not contain URL credentials\");\n }\n if (!isLoopbackDestination(destination)) {\n throw new UsageError(\n \"trigger only sends to loopback destinations (localhost, 127.0.0.0/8, or ::1)\",\n );\n }\n return destination;\n}\n","export const SUPPORTED_TRIGGER_EVENTS = [\n \"job.completed\",\n \"job.failed\",\n \"job.rejected\",\n] as const;\n\nexport type TriggerEventType = (typeof SUPPORTED_TRIGGER_EVENTS)[number];\n\nexport function isTriggerEventType(value: string): value is TriggerEventType {\n return (SUPPORTED_TRIGGER_EVENTS as readonly string[]).includes(value);\n}\n\nexport interface SyntheticEventOptions {\n eventId: string;\n jobId: string;\n accountId: string;\n timestamp: number;\n}\n\nexport function createSyntheticTerminalEvent(\n type: TriggerEventType,\n options: SyntheticEventOptions,\n): Record<string, unknown> {\n const status = type.slice(\"job.\".length).toUpperCase();\n const occurredAt = new Date(options.timestamp * 1000).toISOString();\n const base = {\n event_id: options.eventId,\n job_id: options.jobId,\n account_id: options.accountId,\n status,\n };\n\n if (type === \"job.completed\") {\n const expiresAt = new Date((options.timestamp + 7 * 24 * 60 * 60) * 1000).toISOString();\n return {\n ...base,\n completedAt: occurredAt,\n delivery: {\n mode: \"PULL\",\n retentionDays: 7,\n expiresAt,\n bundle: {\n type: \"zip\",\n filename: `${options.jobId}_outputs.zip`,\n size_bytes: 0,\n sha256: \"0\".repeat(64),\n download: {\n url: `http://127.0.0.1/__mediaruntime_synthetic__/${encodeURIComponent(options.jobId)}.zip`,\n expiresAt,\n },\n },\n layout: \"bundle_relative_v1\",\n deliverables: [],\n manifests: {},\n },\n billing: { status: \"PAID\" },\n usage: { units_total: 0 },\n meta: { synthetic: true, request_metadata: {} },\n };\n }\n\n const failed = type === \"job.failed\";\n return {\n ...base,\n [failed ? \"failedAt\" : \"rejectedAt\"]: occurredAt,\n delivery: { mode: \"NONE\" },\n billing: { status: \"RELEASED\" },\n error: {\n code: failed ? \"SYNTHETIC_ENGINE_FAILED\" : \"SYNTHETIC_REJECTED\",\n message: `Synthetic ${type} event from the MediaRuntime CLI`,\n },\n meta: { synthetic: true, request_metadata: {} },\n };\n}\n","import { createHmac } from \"node:crypto\";\n\nexport const WEBHOOK_HEADERS = {\n id: \"X-Transcoder-Id\",\n timestamp: \"X-Transcoder-Timestamp\",\n signature: \"X-Transcoder-Signature\",\n} as const;\n\nexport interface SignedWebhook {\n body: string;\n headers: Record<string, string>;\n}\n\nexport function signSyntheticWebhook(\n payload: Readonly<Record<string, unknown>>,\n options: { eventId: string; timestamp: number; secret: string },\n): SignedWebhook {\n if (!options.eventId.trim()) throw new Error(\"Webhook event ID must not be empty\");\n if (!Number.isInteger(options.timestamp) || options.timestamp < 0) {\n throw new Error(\"Webhook timestamp must be a non-negative integer\");\n }\n if (!options.secret) throw new Error(\"Webhook secret must not be empty\");\n\n // JSON is serialized once. These exact bytes are both signed and sent.\n const body = JSON.stringify(payload);\n const bodyBytes = Buffer.from(body, \"utf8\");\n const prefix = Buffer.from(`${options.timestamp}.${options.eventId}.`, \"utf8\");\n const digest = createHmac(\"sha256\", options.secret)\n .update(prefix)\n .update(bodyBytes)\n .digest(\"hex\");\n\n return {\n body,\n headers: {\n \"Content-Type\": \"application/json\",\n [WEBHOOK_HEADERS.id]: options.eventId,\n [WEBHOOK_HEADERS.timestamp]: String(options.timestamp),\n [WEBHOOK_HEADERS.signature]: `t=${options.timestamp},v1=${digest}`,\n },\n };\n}\n"],"mappings":"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","names":["randomUUID","UNSUCCESSFUL_TERMINAL_STATUSES","optionValue","detailsProjection","randomUUID"]}
|
package/docs/CLI_SDD.md
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# MediaRuntime CLI software design
|
|
2
|
+
|
|
3
|
+
Status: implementation contract for the first public release
|
|
4
|
+
|
|
5
|
+
Package: `@mediaruntime/cli`
|
|
6
|
+
|
|
7
|
+
Executable: `mediaruntime`
|
|
8
|
+
|
|
9
|
+
Runtime: Node.js 20 or newer
|
|
10
|
+
|
|
11
|
+
## 1. Decision summary
|
|
12
|
+
|
|
13
|
+
The first CLI release is a thin, script-safe layer over the published
|
|
14
|
+
`@mediaruntime/node` SDK. It ships four independently useful surfaces:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
mediaruntime run <source> --output <alias>
|
|
18
|
+
mediaruntime jobs list
|
|
19
|
+
mediaruntime jobs get <job_id>
|
|
20
|
+
mediaruntime trigger job.completed --to http://127.0.0.1:3000/webhooks/mediaruntime
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`run` and `jobs` use the same production API, authentication, local-file upload,
|
|
24
|
+
idempotency, retry, error, and polling behavior as the Node SDK. `trigger` is entirely
|
|
25
|
+
local: it creates and signs a synthetic terminal webhook without contacting the
|
|
26
|
+
MediaRuntime API.
|
|
27
|
+
|
|
28
|
+
The canonical result remains one expiring ZIP bundle containing the complete output tree.
|
|
29
|
+
The CLI downloads that ZIP as-is; it does not expose individual artifacts or infer an
|
|
30
|
+
engine directory layout.
|
|
31
|
+
|
|
32
|
+
## 2. Goals and non-goals
|
|
33
|
+
|
|
34
|
+
### Goals
|
|
35
|
+
|
|
36
|
+
- Make the first successful local-file transcode a single command.
|
|
37
|
+
- Give shell scripts stable job inspection, waiting, pagination, and exit behavior.
|
|
38
|
+
- Download the canonical ZIP safely without leaking credentials to the storage host.
|
|
39
|
+
- Let a developer exercise their local webhook receiver with the production signing
|
|
40
|
+
protocol and realistic terminal payloads.
|
|
41
|
+
- Preserve the SDK and gateway as the authorities for request serialization, uploads,
|
|
42
|
+
retries, aliases, status parsing, and API errors.
|
|
43
|
+
|
|
44
|
+
### Explicitly deferred
|
|
45
|
+
|
|
46
|
+
- `mediaruntime listen` is not a v1 command. It requires a multi-tenant relay,
|
|
47
|
+
authenticated connections, event ownership, expiry, rate limits, and an abuse model.
|
|
48
|
+
- `mediaruntime login`, browser authentication, credential persistence, and profiles are
|
|
49
|
+
not v1 features. Authentication is non-interactive and environment-based.
|
|
50
|
+
- Batch submission, explicit output recipe editing, moderation, watermark management,
|
|
51
|
+
webhook registration, media-report retrieval, and moderation-result retrieval remain
|
|
52
|
+
available through the SDK/API but are not CLI v1 commands.
|
|
53
|
+
- The CLI does not accept a per-job webhook URL. Terminal events go to the account-level
|
|
54
|
+
destination configured in the MediaRuntime profile.
|
|
55
|
+
- ZIP extraction, artifact selectors, HLS serving, and indefinite bundle retention are
|
|
56
|
+
out of scope.
|
|
57
|
+
|
|
58
|
+
## 3. Sources of truth
|
|
59
|
+
|
|
60
|
+
The implementation depends on the released Node SDK and must not copy its HTTP client.
|
|
61
|
+
The following published contracts govern the CLI:
|
|
62
|
+
|
|
63
|
+
- Node SDK `MediaRuntime`, `jobs.create`, `jobs.list`, `jobs.get`, and `Job.wait`.
|
|
64
|
+
- Gateway `contracts/v1/openapi.json` and `contracts/v1/conformance.json`.
|
|
65
|
+
- Frozen output aliases: `video.web`, `video.streaming`, `video.social`, `audio.web`,
|
|
66
|
+
`audio.transcription`, and `image.web`.
|
|
67
|
+
- Single-job terminal states: `COMPLETED`, `FAILED`, and `REJECTED`.
|
|
68
|
+
- Batch jobs visible through `jobs` may also end in `PARTIAL`.
|
|
69
|
+
- Webhook signing base: `{timestamp}.{event_id}.{exact_raw_body_bytes}` with HMAC-SHA256.
|
|
70
|
+
- Bundle access is owner/job scoped and bounded by `retentionDays` / `expiresAt`; expired
|
|
71
|
+
redemption returns HTTP `410`.
|
|
72
|
+
|
|
73
|
+
Contract drift is handled by upgrading the SDK and its vendored gateway snapshot, not by
|
|
74
|
+
adding a second hand-maintained API model to the CLI.
|
|
75
|
+
|
|
76
|
+
## 4. Configuration and precedence
|
|
77
|
+
|
|
78
|
+
Authenticated commands resolve configuration once before doing file or network work.
|
|
79
|
+
|
|
80
|
+
| Setting | Highest precedence | Environment | Default |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| API key | none | `MEDIARUNTIME_API_KEY` | required for `run` and `jobs` |
|
|
83
|
+
| API base URL | `--base-url <url>` | `MEDIARUNTIME_API_URL` | `https://mediaruntime.com` |
|
|
84
|
+
| Webhook signing secret | `--secret-file <path>` | `MEDIARUNTIME_WEBHOOK_SECRET` | required by `trigger`, unless explicitly generated |
|
|
85
|
+
|
|
86
|
+
There is deliberately no `--api-key` or `--secret` option: command-line arguments are
|
|
87
|
+
visible in shell history and process listings. The CLI does not load `.env` files, write
|
|
88
|
+
credentials, use an OS keychain, or search parent directories for configuration.
|
|
89
|
+
|
|
90
|
+
`--base-url` exists for a local gateway, staging, and deterministic tests; ordinary users
|
|
91
|
+
must not need it. It must be an absolute HTTP(S) origin with no credentials, query, or
|
|
92
|
+
fragment. HTTPS is required except for an explicitly loopback host. Trailing slashes are
|
|
93
|
+
normalized. The flag is accepted before the command:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
mediaruntime --base-url http://127.0.0.1:8001 jobs list
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`trigger` neither reads the API key nor uses the base URL.
|
|
100
|
+
|
|
101
|
+
## 5. Command contract
|
|
102
|
+
|
|
103
|
+
### 5.1 `run`
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
mediaruntime run <source> --output <alias> [--output <alias> ...]
|
|
107
|
+
[--metadata <json-object>]
|
|
108
|
+
[--idempotency-key <key>]
|
|
109
|
+
[--wait]
|
|
110
|
+
[--timeout-ms <positive-integer>]
|
|
111
|
+
[--download <zip-path>] [--force]
|
|
112
|
+
[--json]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`-o` is the short spelling of `--output`. Exactly one source and at least one output are
|
|
116
|
+
required. `<source>` may be an HTTP(S) URL, `gs://` URI, local path, or `file://` URL. The
|
|
117
|
+
CLI passes it to the SDK so local inputs use the SDK's signed-upload flow.
|
|
118
|
+
|
|
119
|
+
Each `--output` must be one of the six frozen aliases. Repeating the option preserves the
|
|
120
|
+
caller order. Explicit output objects and batch inputs are intentionally deferred from
|
|
121
|
+
the quick CLI; use an SDK when that control is required.
|
|
122
|
+
|
|
123
|
+
`--metadata` is one JSON object, for example:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
--metadata '{"asset_id":"asset_42","producer":"local-script"}'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
It is parsed locally and rejected before submission if malformed, an array, or a scalar.
|
|
130
|
+
`--idempotency-key` is the caller's durable business key. If it is omitted, the SDK
|
|
131
|
+
creates one invocation-scoped key and reuses it only for that invocation's transport
|
|
132
|
+
retries.
|
|
133
|
+
|
|
134
|
+
Without `--wait`, the command prints the accepted receipt and exits. `--wait` uses the SDK
|
|
135
|
+
poller until a terminal state. `--timeout-ms` applies to that wait, defaults to `300000`,
|
|
136
|
+
and is invalid without `--wait` or `--download`. `--download` implies `--wait`.
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
mediaruntime run ./launch.mp4 -o video.web --wait
|
|
142
|
+
|
|
143
|
+
mediaruntime run ./launch.mp4 \
|
|
144
|
+
-o video.streaming -o audio.transcription \
|
|
145
|
+
--metadata '{"asset_id":"launch-01"}' \
|
|
146
|
+
--idempotency-key 'asset:launch-01:v1' \
|
|
147
|
+
--download ./launch-01.zip
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 5.2 `jobs list`
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
mediaruntime jobs list
|
|
154
|
+
[--status <status>]
|
|
155
|
+
[--limit <1..100>]
|
|
156
|
+
[--cursor <opaque-cursor>]
|
|
157
|
+
[--json]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The command retrieves exactly one account-scoped page, newest first. Status is normalized
|
|
161
|
+
to uppercase. `--cursor` is opaque and passed back unchanged; the CLI never decodes it.
|
|
162
|
+
It does not automatically walk all pages. Human output ends with the next cursor when one
|
|
163
|
+
exists. JSON output retains `nextCursor`.
|
|
164
|
+
|
|
165
|
+
The human table columns are `ID`, `STATUS`, `TIER`, `UNITS`, `BUNDLE`, and `UPDATED`.
|
|
166
|
+
List rows intentionally have no bundle download URL; `jobs get` is the download-aware
|
|
167
|
+
surface.
|
|
168
|
+
|
|
169
|
+
### 5.3 `jobs get`
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
mediaruntime jobs get <job_id>
|
|
173
|
+
[--download <zip-path>] [--force]
|
|
174
|
+
[--json]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Exactly one non-empty job ID is required. The gateway returns `404` for both an unknown
|
|
178
|
+
job and a job owned by another account; the CLI must not try to distinguish them.
|
|
179
|
+
|
|
180
|
+
Human output includes ID, status, effective/billed tier, bundle availability and size,
|
|
181
|
+
and a terminal error when present. Machine output contains the curated SDK job projection.
|
|
182
|
+
Neither format prints the signed bundle URL. `--download` redeems it internally when the
|
|
183
|
+
job is `COMPLETED` and the bundle is available.
|
|
184
|
+
|
|
185
|
+
`jobs get` is a snapshot operation and does not poll. A nonterminal job is reported as it
|
|
186
|
+
currently exists. Waiting is provided by `run --wait` in v1.
|
|
187
|
+
|
|
188
|
+
### 5.4 `trigger`
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
mediaruntime trigger <event-type> --to <loopback-url>
|
|
192
|
+
[--job-id <job_id>]
|
|
193
|
+
[--account-id <account_id>]
|
|
194
|
+
[--secret-file <path> | --generate-secret]
|
|
195
|
+
[--json]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Supported event types match the production Firebase terminal-webhook sender exactly:
|
|
199
|
+
|
|
200
|
+
| Event argument | Payload status | Timestamp field |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `job.completed` | `COMPLETED` | `completedAt` |
|
|
203
|
+
| `job.failed` | `FAILED` | `failedAt` |
|
|
204
|
+
| `job.rejected` | `REJECTED` | `rejectedAt` |
|
|
205
|
+
|
|
206
|
+
`PARTIAL` is a batch polling state but is not currently emitted by the production webhook
|
|
207
|
+
sender, so `job.partial` is not a v1 trigger type.
|
|
208
|
+
|
|
209
|
+
`--to` is mandatory and must be an absolute HTTP(S) URL whose parsed hostname is
|
|
210
|
+
`localhost`, an address in `127.0.0.0/8`, or IPv6 loopback `::1`. URL credentials are
|
|
211
|
+
rejected. Redirects are not followed, including a redirect to another loopback URL.
|
|
212
|
+
|
|
213
|
+
The usual secret source is `MEDIARUNTIME_WEBHOOK_SECRET`. `--secret-file` reads a UTF-8
|
|
214
|
+
file, trims surrounding whitespace, and overrides the environment. It is mutually
|
|
215
|
+
exclusive with `--generate-secret`. A blank value is invalid.
|
|
216
|
+
|
|
217
|
+
`--generate-secret` is deliberately explicit and development-only. It creates 32 random
|
|
218
|
+
bytes with the operating-system CSPRNG and prints their base64url form before sending.
|
|
219
|
+
Because the receiver must verify with that exact value, this mode is only useful when a
|
|
220
|
+
local test harness can capture the emitted secret and coordinate receiver configuration.
|
|
221
|
+
Normal interactive development should configure one secret in both the receiver and
|
|
222
|
+
`MEDIARUNTIME_WEBHOOK_SECRET` before running `trigger`. An environment- or file-sourced
|
|
223
|
+
secret is never printed. `--generate-secret` and `--json` are mutually exclusive: the
|
|
224
|
+
generated value is a human preflight line rather than an ordinary result field.
|
|
225
|
+
|
|
226
|
+
When IDs are omitted, the command generates a UUID-backed job/event pair and uses
|
|
227
|
+
`acc_cli_local`. A generated completion payload contains realistic `delivery`, bounded
|
|
228
|
+
retention, billing, usage, and metadata shapes; its bundle URL is loopback-only synthetic
|
|
229
|
+
data and is not a real downloadable artifact. Failure and rejection payloads contain a
|
|
230
|
+
synthetic `{code,message}` error and no deliverable bundle.
|
|
231
|
+
|
|
232
|
+
## 6. Webhook signing and delivery
|
|
233
|
+
|
|
234
|
+
The trigger serializer creates the JSON body once. Those exact bytes are signed and sent;
|
|
235
|
+
the body must never be parsed and serialized again between the two operations.
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
timestamp = floor(current Unix milliseconds / 1000)
|
|
239
|
+
signing_base = utf8("{timestamp}.{event_id}.") || raw_body_bytes
|
|
240
|
+
digest = lowercase_hex(HMAC_SHA256(secret, signing_base))
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The POST contains:
|
|
244
|
+
|
|
245
|
+
```text
|
|
246
|
+
Content-Type: application/json
|
|
247
|
+
X-Transcoder-Id: <event_id>
|
|
248
|
+
X-Transcoder-Timestamp: <timestamp>
|
|
249
|
+
X-Transcoder-Signature: t=<timestamp>,v1=<digest>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The payload `event_id` and the header ID are identical. The request has a 10-second
|
|
253
|
+
timeout, uses `redirect: manual`, and succeeds only for HTTP `2xx`. Response bodies are not
|
|
254
|
+
echoed because a local receiver may return application secrets or arbitrary HTML.
|
|
255
|
+
|
|
256
|
+
## 7. Bundle download contract
|
|
257
|
+
|
|
258
|
+
Both `run --download` and `jobs get --download` call one shared downloader.
|
|
259
|
+
|
|
260
|
+
1. Obtain the job's current `bundle.downloadUrl` from the authenticated SDK response.
|
|
261
|
+
2. Make a fresh `GET` to that exact URL and follow the gateway's expected redirect to
|
|
262
|
+
object storage.
|
|
263
|
+
3. Do **not** attach `X-API-Key`, `Authorization`, cookies, webhook headers, or the API base
|
|
264
|
+
URL to this request. The URL's scoped token is the only redemption credential.
|
|
265
|
+
4. Stream to a uniquely named temporary file beside the requested destination; never
|
|
266
|
+
buffer the ZIP in memory.
|
|
267
|
+
5. If the job supplies `sizeBytes` or `sha256`, validate them while streaming.
|
|
268
|
+
6. Close and atomically rename the temporary file only after the response and integrity
|
|
269
|
+
checks succeed.
|
|
270
|
+
7. On any failure, close and remove the temporary file. Preserve an existing destination.
|
|
271
|
+
|
|
272
|
+
The destination is always a ZIP file path, not a directory. Its parent directory must
|
|
273
|
+
already exist. The command refuses to overwrite an existing file unless `--force` is
|
|
274
|
+
present; even with `--force`, the old file remains intact until the replacement has been
|
|
275
|
+
fully downloaded and verified.
|
|
276
|
+
|
|
277
|
+
An unavailable bundle, missing download URL, expired redemption (`410`), non-`2xx` final
|
|
278
|
+
response, truncated stream, size mismatch, SHA-256 mismatch, or filesystem failure is a
|
|
279
|
+
download error. The CLI does not silently resubmit the job, extend retention, or fall back
|
|
280
|
+
to internal storage URIs. It never extracts the archive.
|
|
281
|
+
|
|
282
|
+
## 8. Standard streams and output formats
|
|
283
|
+
|
|
284
|
+
The process contract is designed for composition:
|
|
285
|
+
|
|
286
|
+
- `stdout` contains successful command data only, except that the explicitly requested
|
|
287
|
+
`--generate-secret` preflight line is emitted before its local POST so a coordinated
|
|
288
|
+
test harness can configure verification.
|
|
289
|
+
- `stderr` contains usage errors, configuration errors, API/network errors, request IDs,
|
|
290
|
+
and any human waiting diagnostic.
|
|
291
|
+
- The CLI never prompts. It reads no stdin in v1.
|
|
292
|
+
- Every emitted record ends with one newline.
|
|
293
|
+
- ANSI color is used only when stderr/stdout is a TTY and never in JSON mode.
|
|
294
|
+
- Secrets are never written except the explicitly requested generated trigger secret.
|
|
295
|
+
|
|
296
|
+
### Human format
|
|
297
|
+
|
|
298
|
+
Human output is concise and may evolve cosmetically. It is not a parsing contract. Job
|
|
299
|
+
IDs, statuses, cursors, paths, and HTTP status values remain literal. Waiting output must
|
|
300
|
+
not invent percentages; the gateway supplies state, not continuous progress.
|
|
301
|
+
|
|
302
|
+
### JSON format
|
|
303
|
+
|
|
304
|
+
`--json` emits exactly one compact JSON object plus a newline on success, with no spinner,
|
|
305
|
+
table, ANSI sequence, or explanatory text. Field names use the SDK's camelCase public
|
|
306
|
+
surface.
|
|
307
|
+
|
|
308
|
+
- `run` without waiting: the job receipt (`id`, `status`, `tier`, `requiredTier`,
|
|
309
|
+
`outputs`, `message`).
|
|
310
|
+
- `run --wait` and `jobs get`: curated job details, with bundle availability,
|
|
311
|
+
`expiresAt`, `sizeBytes`, `sha256`, and `retentionDays`, but no `downloadUrl`.
|
|
312
|
+
- `jobs list`: `{jobs, nextCursor}` from the SDK.
|
|
313
|
+
- `trigger`: `{eventId, type, jobId, status, destination, httpStatus}`.
|
|
314
|
+
|
|
315
|
+
If `--download` is used, a successful atomic download is required before exit `0`. The
|
|
316
|
+
JSON job projection remains the sole stdout object; the destination is already known from
|
|
317
|
+
the command argument. Implementations should delay writing it until download success so a
|
|
318
|
+
failed download cannot leave success data on stdout.
|
|
319
|
+
|
|
320
|
+
In JSON mode, errors are one compact object on `stderr`:
|
|
321
|
+
|
|
322
|
+
```json
|
|
323
|
+
{"error":{"code":"authentication_error","message":"Unauthorized","exitCode":3,"requestId":"req_...","retryable":false}}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Optional `requestId`, `retryable`, and sanitized `details` come from
|
|
327
|
+
`MediaRuntimeApiError`. Raw response bodies, request bodies, API keys, webhook secrets,
|
|
328
|
+
signed URLs, and filesystem temporary names are never included.
|
|
329
|
+
|
|
330
|
+
## 9. Exit codes
|
|
331
|
+
|
|
332
|
+
The central entrypoint owns error-to-exit translation; command modules return semantic
|
|
333
|
+
results and must not invent conflicting mappings.
|
|
334
|
+
|
|
335
|
+
| Code | Meaning |
|
|
336
|
+
|---:|---|
|
|
337
|
+
| `0` | Command succeeded; a waited/inspected job is `COMPLETED` or nonterminal |
|
|
338
|
+
| `1` | Unexpected internal CLI error |
|
|
339
|
+
| `2` | Command syntax, local validation, or missing configuration |
|
|
340
|
+
| `3` | Authentication, billing-required, or permission failure (`401`/`402`/`403`) |
|
|
341
|
+
| `4` | Non-retryable API outcome such as `400`, `404`, `409`, `410`, `413`, or `422`, or exhausted `429` |
|
|
342
|
+
| `5` | Connection/HTTP timeout or exhausted retryable `5xx` response |
|
|
343
|
+
| `6` | A submitted or inspected job is terminal `FAILED`, `REJECTED`, or `PARTIAL` |
|
|
344
|
+
| `7` | `run --wait` exceeded its wait timeout |
|
|
345
|
+
| `8` | Local trigger endpoint returned a redirect or non-`2xx` response |
|
|
346
|
+
| `9` | Bundle availability, redemption, integrity, or filesystem failure |
|
|
347
|
+
| `130` | Interrupted by `SIGINT`; in-flight fetch/poll/download is aborted and temporary files are removed |
|
|
348
|
+
|
|
349
|
+
`jobs list` returns `0` for an empty page. `jobs get` returns `0` for queued/processing or
|
|
350
|
+
completed jobs and `6` for unsuccessful terminal states after still printing the job.
|
|
351
|
+
`run` without waiting returns `0` for an accepted nonterminal/completed receipt and `6`
|
|
352
|
+
for an immediate unsuccessful terminal receipt. `run --wait` returns `6` after printing
|
|
353
|
+
the final failed/rejected/partial job. Trigger connection failure is `5`; a reached local
|
|
354
|
+
endpoint that refuses the event is `8`.
|
|
355
|
+
|
|
356
|
+
## 10. Security rules
|
|
357
|
+
|
|
358
|
+
- Never accept API keys or webhook secrets as ordinary command arguments.
|
|
359
|
+
- Never persist credentials or generated secrets.
|
|
360
|
+
- Redact credentials and signed query strings from errors and debug output.
|
|
361
|
+
- Treat JSON output containing metadata as caller-sensitive even though signed bundle URLs
|
|
362
|
+
are deliberately omitted.
|
|
363
|
+
- Delegate local upload URL creation and API retries to the SDK.
|
|
364
|
+
- Validate local source existence/readability before requesting an upload where practical;
|
|
365
|
+
do not follow local symlinks for any purpose other than normal file opening.
|
|
366
|
+
- Trigger only literal loopback destinations, forbid URL credentials, and do not follow
|
|
367
|
+
redirects. Its API key environment is irrelevant.
|
|
368
|
+
- Sign the exact bytes sent, compare/verify only through SDK helpers in receiver examples,
|
|
369
|
+
and never describe a parsed-and-reserialized JSON body as safe to verify.
|
|
370
|
+
- Bundle redirects are the one intentional cross-origin redirect flow. Never forward API
|
|
371
|
+
authentication headers to the redirected host.
|
|
372
|
+
- Use bounded memory for uploads/downloads and cap locally generated webhook payloads.
|
|
373
|
+
- Preserve the account-scoped `404` ambiguity; do not reveal whether another account owns
|
|
374
|
+
a requested job.
|
|
375
|
+
|
|
376
|
+
## 11. Architecture boundaries
|
|
377
|
+
|
|
378
|
+
Recommended modules are deliberately narrow:
|
|
379
|
+
|
|
380
|
+
```text
|
|
381
|
+
src/cli.ts argv dispatch, configuration, signals, exit/error rendering
|
|
382
|
+
src/commands/run.ts run parsing and SDK orchestration
|
|
383
|
+
src/commands/jobs.ts list/get parsing and projections
|
|
384
|
+
src/commands/trigger.ts local event orchestration
|
|
385
|
+
src/download.ts streamed atomic bundle redemption
|
|
386
|
+
src/trigger/event.ts synthetic public payloads
|
|
387
|
+
src/trigger/signature.ts byte signing and headers
|
|
388
|
+
src/trigger/destination.ts loopback-only URL validation
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
All command functions accept injected SDK clients, fetch, clocks, UUID/random sources,
|
|
392
|
+
filesystem operations, and output writers as appropriate. Unit tests must not require
|
|
393
|
+
production credentials, billable jobs, real storage, or listening ports.
|
|
394
|
+
|
|
395
|
+
## 12. Acceptance tests
|
|
396
|
+
|
|
397
|
+
### Parsing and configuration
|
|
398
|
+
|
|
399
|
+
- Every documented command/flag combination parses; unknown flags and extra positionals
|
|
400
|
+
exit `2` before network access.
|
|
401
|
+
- Missing/blank API key fails `run` and `jobs`; `trigger` never reads it.
|
|
402
|
+
- `--base-url` overrides the environment, which overrides production; invalid or insecure
|
|
403
|
+
non-loopback HTTP URLs fail closed.
|
|
404
|
+
- No command accepts `--api-key`, `--secret`, a per-job webhook URL, `listen`, or `login`.
|
|
405
|
+
|
|
406
|
+
### Run
|
|
407
|
+
|
|
408
|
+
- Each of the six aliases is accepted, forwarded unchanged, and covered by a fixture;
|
|
409
|
+
unknown aliases are rejected locally.
|
|
410
|
+
- HTTP(S), `gs://`, local path, and `file://` sources use the SDK path, including signed
|
|
411
|
+
upload for local files.
|
|
412
|
+
- Metadata must be an object; output order and a supplied idempotency key are preserved.
|
|
413
|
+
- No-wait prints a receipt once. Wait recognizes all SDK terminal states, maps unsuccessful
|
|
414
|
+
status to `6`, and maps `JobWaitTimeoutError` to `7`.
|
|
415
|
+
- `--download` implies wait and does not print success before the atomic download finishes.
|
|
416
|
+
|
|
417
|
+
### Jobs
|
|
418
|
+
|
|
419
|
+
- List forwards uppercase status, validates limit `1..100`, preserves the opaque cursor,
|
|
420
|
+
and displays/serializes the next cursor.
|
|
421
|
+
- Get percent-encodes the ID through the SDK, does not expose a signed URL, preserves the
|
|
422
|
+
account-scoped `404`, and maps unsuccessful terminal state to `6`.
|
|
423
|
+
|
|
424
|
+
### Bundle download
|
|
425
|
+
|
|
426
|
+
- Gateway `307` followed by storage `200` succeeds without forwarding API credentials.
|
|
427
|
+
- Existing destination refusal, `--force` atomic replacement, expired `410`, interrupted
|
|
428
|
+
stream, HTTP failure, SHA mismatch, size mismatch, write failure, and `SIGINT` cleanup
|
|
429
|
+
are covered.
|
|
430
|
+
- Tests assert the ZIP bytes are not extracted or interpreted.
|
|
431
|
+
|
|
432
|
+
### Trigger
|
|
433
|
+
|
|
434
|
+
- All three production event types create the correct status/timestamp/error-or-delivery
|
|
435
|
+
shape; unsupported and `job.partial` fail locally.
|
|
436
|
+
- Fixed clock, UUID, payload, and secret fixtures reproduce the exact expected HMAC.
|
|
437
|
+
- The body bytes used for HMAC are byte-identical to the POST body.
|
|
438
|
+
- Environment, trimmed secret-file, missing secret, blank file, mutually exclusive secret
|
|
439
|
+
modes, and CSPRNG-generated secret paths are covered without logging non-generated
|
|
440
|
+
secrets.
|
|
441
|
+
- `localhost`, `127.0.0.0/8`, and IPv6 loopback pass; public/private non-loopback hosts,
|
|
442
|
+
URL credentials, unsupported schemes, and every redirect fail closed.
|
|
443
|
+
- A `2xx` response exits `0`; local non-`2xx` exits `8`; connection/timeout exits `5`.
|
|
444
|
+
|
|
445
|
+
### Process I/O
|
|
446
|
+
|
|
447
|
+
- Every JSON success/error path parses as exactly one object on the documented stream and
|
|
448
|
+
contains no ANSI bytes.
|
|
449
|
+
- Human errors include a gateway request ID when available.
|
|
450
|
+
- stdout stays empty on pre-execution and download errors; stderr never contains an API
|
|
451
|
+
key, webhook secret, or signed bundle URL.
|
|
452
|
+
- `SIGINT` aborts active work and exits `130` without a stack trace.
|
|
453
|
+
|
|
454
|
+
## 13. Release gates
|
|
455
|
+
|
|
456
|
+
The v1 package is ready only when:
|
|
457
|
+
|
|
458
|
+
1. Type checking, unit tests, package smoke tests, and Node 20/22 CI pass.
|
|
459
|
+
2. A packed tarball installs and exposes the `mediaruntime` executable with a valid
|
|
460
|
+
shebang and no development fixtures.
|
|
461
|
+
3. Contract tests use the released Node SDK's gateway fixtures for aliases and statuses.
|
|
462
|
+
4. A local-file `run --wait --download` smoke test succeeds against a non-production test
|
|
463
|
+
account and produces a valid ZIP.
|
|
464
|
+
5. A local receiver using `@mediaruntime/node` verification accepts every trigger type and
|
|
465
|
+
rejects a changed body, stale timestamp, and wrong secret.
|
|
466
|
+
6. Documentation labels `listen` and `login` as deferred rather than advertising stubs.
|
package/docs/TRIGGER.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Local webhook trigger
|
|
2
|
+
|
|
3
|
+
`mediaruntime trigger` sends one synthetic, correctly signed terminal webhook directly
|
|
4
|
+
from the CLI process to a local HTTP endpoint. It does not call the MediaRuntime API, open
|
|
5
|
+
a tunnel, register a webhook, or use a relay.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export MEDIARUNTIME_WEBHOOK_SECRET='whsec_...'
|
|
9
|
+
|
|
10
|
+
mediaruntime trigger job.completed \
|
|
11
|
+
--to http://127.0.0.1:3000/webhooks/mediaruntime
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Supported event types are `job.completed`, `job.failed`, and `job.rejected`. The body uses
|
|
15
|
+
the corresponding uppercase terminal status. `--job-id` and `--account-id` replace the
|
|
16
|
+
synthetic identifiers when application routing needs known values.
|
|
17
|
+
|
|
18
|
+
The CLI serializes the JSON body once, signs those exact bytes with HMAC-SHA256, and sends:
|
|
19
|
+
|
|
20
|
+
- `X-Transcoder-Id`
|
|
21
|
+
- `X-Transcoder-Timestamp`
|
|
22
|
+
- `X-Transcoder-Signature: t=<timestamp>,v1=<hex digest>`
|
|
23
|
+
|
|
24
|
+
The normal secret source is `MEDIARUNTIME_WEBHOOK_SECRET`, matching the official SDK
|
|
25
|
+
verification helpers. `--secret-file /path/to/secret` avoids exposing a secret in shell
|
|
26
|
+
history or the process list; restrict that file to the current user. There is deliberately
|
|
27
|
+
no `--secret` argument.
|
|
28
|
+
|
|
29
|
+
`--generate-secret` is an explicit development-only mode. It securely generates and
|
|
30
|
+
prints a one-time secret before sending; the local receiver must be configured with that
|
|
31
|
+
exact value to verify the request. The CLI never prints an environment- or file-sourced
|
|
32
|
+
secret.
|
|
33
|
+
|
|
34
|
+
For safety, `--to` is required and accepts only `http://` or `https://` loopback URLs:
|
|
35
|
+
`localhost`, `127.0.0.0/8`, or `::1`. URL credentials and redirects are rejected/not
|
|
36
|
+
followed, preventing the signed body from being forwarded to another host. Use a real
|
|
37
|
+
gateway webhook for any non-local integration test.
|
|
38
|
+
|
|
39
|
+
The command exits `0` only after a `2xx` response. Invalid arguments, missing secrets,
|
|
40
|
+
connection/time-out failures, redirects, and non-`2xx` responses are errors handled by the
|
|
41
|
+
CLI entrypoint as a nonzero exit. No API credential is required or read by this command.
|