heygen-com/hyperframes · critical · PlanV2IntegrityError

PLAN_V2_INTEGRITY_UNRECOVERABLE

PLAN_V2_INTEGRITY_UNRECOVERABLE

Error message

[planV2] ${label} must be a lowercase SHA-256 digest

What it means

A `PlanV2IntegrityError` (code `PLAN_V2_INTEGRITY_UNRECOVERABLE`) thrown by `assertSha256` when a value expected to be a content-addressing digest is not a 64-char lowercase hex SHA-256. The publisher keys every v2 artifact under its digest, so a malformed digest breaks the CAS addressing scheme and is treated as unrecoverable.

Source

Thrown at packages/aws-lambda/src/s3PlanV2Publisher.ts:27

  type PlanV2PublishBlob,
} from "@hyperframes/producer/distributed";
import { parseS3Uri, uploadContentAddressedFileToS3 } from "./s3Transport.js";

export interface S3PlanV2ArtifactPublisherOptions {
  readonly s3: S3Client;
  /** Validated render output prefix from which all v2 object keys are derived. */
  readonly planOutputS3Prefix: string;
  /** Planner-local scratch parent for the small manifest upload file. */
  readonly temporaryRoot?: string;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

function assertSha256(value: unknown, label: string): string {
  if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
    throw new PlanV2IntegrityError(`${label} must be a lowercase SHA-256 digest`);
  }
  return value;
}

function manifestDigests(manifestBytes: string): ReadonlySet<string> {
  let value: unknown;
  try {
    value = JSON.parse(manifestBytes);
  } catch {
    throw new PlanV2IntegrityError("S3 publisher received invalid manifest JSON");
  }
  if (!isRecord(value) || !Array.isArray(value.artifacts)) {
    throw new PlanV2IntegrityError("S3 publisher manifest requires an artifacts array");
  }
  return new Set(
    value.artifacts.map((artifact, index) => {
      if (!isRecord(artifact)) {
        throw new PlanV2IntegrityError(`S3 publisher artifacts[${index}] must be an object`);

View on GitHub (pinned to c2996c8626)

Solutions

  1. Produce digests with `crypto.createHash('sha256').update(data).digest('hex')` — always lowercase hex, 64 chars.
  2. Validate the digest at the producer boundary before publishing (the same regex `/^[a-f0-9]{64}$/`).
  3. Regenerate the manifest with correct digests and redeploy.
  4. Check for field-name drift in the manifest schema (`sha256` vs `digest` vs `hash`).

Example fix

// before: base64 + uppercase digests
const digest = crypto.createHash('sha256').update(buf).digest('hex').toUpperCase();
// after: lowercase 64-char hex
const digest = crypto.createHash('sha256').update(buf).digest('hex');
Defensive patterns

Strategy: validation

Validate before calling

const SHA256_RE = /^[a-f0-9]{64}$/;
function assertSha256(value: unknown, label: string): string {
  if (typeof value !== "string" || !SHA256_RE.test(value)) {
    throw new Error(`${label} must be a lowercase SHA-256 digest`);
  }
  return value;
}

Type guard

function isLowercaseSha256(value: unknown): value is string {
  return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
}

Prevention

When it happens

Trigger: Calling `assertSha256(value, label)` (directly or via `putBlob`/`commitManifest`) where `value` is non-string, uppercase hex, too short/long, or contains non-hex characters.

Common situations: Producer computing digests with a non-normalized hash (base64, uppercase, truncated); a test fixture with a placeholder digest; manifest JSON whose `sha256` field was renamed or is the wrong field; a copy/paste digest typo.

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/9447f951085ef25c. Report an issue: GitHub.