{"record":{"id":"e35b6bae51580ce7","repo":"paperclipai/paperclip","slug":"cloud-runtime-identity-previous-or-canonical-origi","errorCode":null,"errorMessage":"Cloud runtime identity previous or canonical origin is invalid","messagePattern":"Cloud runtime identity previous or canonical origin is invalid","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/cloud-runtime-identity.ts","lineNumber":331,"sourceCode":"}\n\n/** Verify that an assertion is signed for this exact, still-unclaimed instance. */\nexport function verifyCloudRuntimeIdentityAssertion(input: {\n  compactJws: string;\n  env?: NodeJS.ProcessEnv;\n  now?: Date;\n  expectedPreviousOrigin: string | null;\n}): RuntimeIdentityClaims {\n  const env = input.env ?? process.env;\n  const claims = verifyClaims({ compactJws: input.compactJws, env, now: input.now ?? new Date() });\n  const configuredStackId = nonEmpty(env.PAPERCLIP_CLOUD_STACK_ID);\n  if (!configuredStackId || claims.sub !== configuredStackId) {\n    throw new Error(\"Cloud runtime identity stack does not match this instance\");\n  }\n  const previousOrigin = exactHttpsOrigin(claims.previousOrigin);\n  const canonicalOrigin = exactHttpsOrigin(claims.canonicalOrigin);\n  if (!previousOrigin || !canonicalOrigin || previousOrigin !== input.expectedPreviousOrigin) {\n    throw new Error(\"Cloud runtime identity previous or canonical origin is invalid\");\n  }\n  if (\n    !STACK_SLUG_PATTERN.test(claims.stackSlug)\n    || new URL(canonicalOrigin).hostname.split(\".\")[0] !== claims.stackSlug\n    || claims.claimId.length > 256\n    || claims.claimId.trim() !== claims.claimId\n    || !claims.claimId\n  ) {\n    throw new Error(\"Cloud runtime identity destination is invalid\");\n  }\n  return claims;\n}\n\nfunction assertionsEqual(row: PersistedRuntimeIdentity, claims: RuntimeIdentityClaims): boolean {\n  const left = Buffer.from(JSON.stringify([\n    row.stackId,\n    row.claimId,\n    row.previousOrigin,","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/cloud-runtime-identity.ts#L313-L349","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Re-mint the assertion with exact https origins matching the instance's actual previous/canonical origins","Verify expectedPreviousOrigin passed by the caller matches the origin recorded when the claim flow started","Normalize origin strings to scheme://host only (no path, trailing slash, or non-443 port)","Check for replay of an assertion from an earlier handoff and start a fresh claim flow"],"exampleFix":"// before\npreviousOrigin: \"https://old.example.com/\"\n// after\npreviousOrigin: \"https://old.example.com\" // exact origin, no path or trailing slash","handlingStrategy":"validation","validationCode":"const prev = new URL(claims.previousOrigin);\nconst canon = new URL(claims.canonicalOrigin);\nif (prev.protocol !== 'https:' || canon.protocol !== 'https:' || prev.pathname !== '/' || prev.origin !== expectedPreviousOrigin) {\n  throw new Error('origin claims invalid before apply');\n}","typeGuard":"function isExactHttpsOrigin(v: unknown): v is string {\n  if (typeof v !== 'string') return false;\n  try { const u = new URL(v); return u.protocol === 'https:' && u.pathname === '/' && u.search === '' && u.hash === '' && v === u.origin; } catch { return false; }\n}","tryCatchPattern":"try {\n  await verifyAssertion({ compactJws: jws, expectedPreviousOrigin });\n} catch (e) {\n  if (e.message.includes('origin is invalid')) throw new Error(`origin mismatch: expected ${expectedPreviousOrigin}; restart claim flow`);\n  throw e;\n}","preventionTips":["Always mint exact origins (URL.origin, no path/trailing slash/port surprises)","Store and reuse the same expectedPreviousOrigin throughout a claim flow","Reject http:// origins at mint time","Watch for handoff migrations that change the previous origin mid-flow"],"tags":["auth","url-validation","cloud"],"backgroundTag":"invalid-url-format","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}