paperclipai/paperclip · error

Cloud runtime identity previous or canonical origin is inval

Error message

Cloud runtime identity previous or canonical origin is invalid

What it means

verifyCloudRuntimeIdentityAssertion validates that both previousOrigin and canonicalOrigin are exact HTTPS origins and that previousOrigin equals the caller-supplied expectedPreviousOrigin. It throws when either origin is missing/malformed/not https, or when the previous origin on the assertion does not match the origin expected for this claim, blocking origin-rewrite or downgrade attacks during a runtime identity handoff.

Source

Thrown at server/src/services/cloud-runtime-identity.ts:331

}

/** Verify that an assertion is signed for this exact, still-unclaimed instance. */
export function verifyCloudRuntimeIdentityAssertion(input: {
  compactJws: string;
  env?: NodeJS.ProcessEnv;
  now?: Date;
  expectedPreviousOrigin: string | null;
}): RuntimeIdentityClaims {
  const env = input.env ?? process.env;
  const claims = verifyClaims({ compactJws: input.compactJws, env, now: input.now ?? new Date() });
  const configuredStackId = nonEmpty(env.PAPERCLIP_CLOUD_STACK_ID);
  if (!configuredStackId || claims.sub !== configuredStackId) {
    throw new Error("Cloud runtime identity stack does not match this instance");
  }
  const previousOrigin = exactHttpsOrigin(claims.previousOrigin);
  const canonicalOrigin = exactHttpsOrigin(claims.canonicalOrigin);
  if (!previousOrigin || !canonicalOrigin || previousOrigin !== input.expectedPreviousOrigin) {
    throw new Error("Cloud runtime identity previous or canonical origin is invalid");
  }
  if (
    !STACK_SLUG_PATTERN.test(claims.stackSlug)
    || new URL(canonicalOrigin).hostname.split(".")[0] !== claims.stackSlug
    || claims.claimId.length > 256
    || claims.claimId.trim() !== claims.claimId
    || !claims.claimId
  ) {
    throw new Error("Cloud runtime identity destination is invalid");
  }
  return claims;
}

function assertionsEqual(row: PersistedRuntimeIdentity, claims: RuntimeIdentityClaims): boolean {
  const left = Buffer.from(JSON.stringify([
    row.stackId,
    row.claimId,
    row.previousOrigin,

View on GitHub (pinned to 01ad858492)

Solutions

  1. Re-mint the assertion with exact https origins matching the instance's actual previous/canonical origins
  2. Verify expectedPreviousOrigin passed by the caller matches the origin recorded when the claim flow started
  3. Normalize origin strings to scheme://host only (no path, trailing slash, or non-443 port)
  4. Check for replay of an assertion from an earlier handoff and start a fresh claim flow

Example fix

// before
previousOrigin: "https://old.example.com/"
// after
previousOrigin: "https://old.example.com" // exact origin, no path or trailing slash
Defensive patterns

Strategy: validation

Validate before calling

const prev = new URL(claims.previousOrigin);
const canon = new URL(claims.canonicalOrigin);
if (prev.protocol !== 'https:' || canon.protocol !== 'https:' || prev.pathname !== '/' || prev.origin !== expectedPreviousOrigin) {
  throw new Error('origin claims invalid before apply');
}

Type guard

function isExactHttpsOrigin(v: unknown): v is string {
  if (typeof v !== 'string') return false;
  try { const u = new URL(v); return u.protocol === 'https:' && u.pathname === '/' && u.search === '' && u.hash === '' && v === u.origin; } catch { return false; }
}

Try / catch

try {
  await verifyAssertion({ compactJws: jws, expectedPreviousOrigin });
} catch (e) {
  if (e.message.includes('origin is invalid')) throw new Error(`origin mismatch: expected ${expectedPreviousOrigin}; restart claim flow`);
  throw e;
}

Prevention

When it happens

Trigger: Assertion claims.previousOrigin or claims.canonicalOrigin is absent, not an exact https origin (has path/query/port surprises), or previousOrigin !== input.expectedPreviousOrigin passed by verifyAssertion (e.g. claim retried against a different previous origin than originally expected).

Common situations: Assertion minted with http:// origin; trailing-slash or path-bearing origin strings; an assertion from a prior migration/handoff replayed after the expected previous origin changed; hostile assertion attempting to rewrite canonical origin.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/e35b6bae51580ce7. Report an issue: GitHub.