{"record":{"id":"2c85c76785c8f105","repo":"paperclipai/paperclip","slug":"cloud-runtime-identity-claims-are-incomplete","errorCode":null,"errorMessage":"Cloud runtime identity claims are incomplete","messagePattern":"Cloud runtime identity claims are incomplete","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/cloud-runtime-identity.ts","lineNumber":302,"sourceCode":"  }\n\n  const payload = decodeJsonPart(encodedPayload, \"payload\");\n  const nowSeconds = Math.floor(input.now.getTime() / 1000);\n  if (\n    payload.v !== 1\n    || payload.iss !== CLOUD_RUNTIME_IDENTITY_ISSUER\n    || payload.aud !== CLOUD_RUNTIME_IDENTITY_AUDIENCE\n    || typeof payload.sub !== \"string\"\n    || typeof payload.claimId !== \"string\"\n    || typeof payload.previousOrigin !== \"string\"\n    || typeof payload.canonicalOrigin !== \"string\"\n    || typeof payload.stackSlug !== \"string\"\n    || typeof payload.iat !== \"number\"\n    || !Number.isInteger(payload.iat)\n    || typeof payload.exp !== \"number\"\n    || !Number.isInteger(payload.exp)\n  ) {\n    throw new Error(\"Cloud runtime identity claims are incomplete\");\n  }\n  if (\n    payload.exp <= nowSeconds\n    || payload.iat > nowSeconds + MAX_CLOCK_SKEW_SECONDS\n    || payload.exp <= payload.iat\n    || payload.exp - payload.iat > MAX_ASSERTION_LIFETIME_SECONDS\n  ) {\n    throw new Error(\"Cloud runtime identity assertion is expired or has an invalid lifetime\");\n  }\n  return payload as RuntimeIdentityClaims;\n}\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;","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/cloud-runtime-identity.ts#L284-L320","documentation":"Thrown by verifyClaims when the decoded payload is valid JSON but lacks the required claims or has wrong types: v must be 1, iss/aud must match the expected issuer/audience constants, and sub, claimId, previousOrigin, canonicalOrigin, stackSlug must be strings, iat/exp must be integers. This guards against assertions missing fields this identity protocol depends on.","triggerScenarios":"Payload parses as an object but any of v, iss, aud, sub, claimId, previousOrigin, canonicalOrigin, stackSlug is missing/wrong-typed, iss/aud do not equal CLOUD_RUNTIME_IDENTITY_ISSUER/AUDIENCE, v !== 1, or iat/exp are non-integer numbers (e.g. seconds with fractional part or milliseconds).","commonSituations":"Assertions minted by an older protocol version (v:0) after a server upgrade; a generic JWT library producing numeric-string iat/exp or millisecond timestamps; tokens from a different product/issuer reusing the same endpoint; custom claim names used instead of the protocol's exact claim keys.","solutions":["Re-mint the assertion with the current protocol version (v: 1) and the exact claim set expected here","Ensure iat/exp are integer Unix seconds, not milliseconds or strings","Set iss and aud to the values of CLOUD_RUNTIME_IDENTITY_ISSUER and CLOUD_RUNTIME_IDENTITY_AUDIENCE constants","Base64url-decode the payload segment and diff its keys against the required claims list in verifyClaims"],"exampleFix":"// before: ms timestamps\npayload.iat = Date.now(); // 1725879600000\n// after: integer unix seconds\npayload.iat = Math.floor(Date.now() / 1000);","handlingStrategy":"validation","validationCode":"interface ExpectedClaims { v: number; iss: string; aud: string; sub: string; claimId: string; previousOrigin: string; canonicalOrigin: string; stackSlug: string; iat: number; exp: number; }\nfunction hasRequiredClaims(p: Record<string, unknown>): p is ExpectedClaims {\n  return p.v === 1 && typeof p.iss === \"string\" && typeof p.aud === \"string\"\n    && [\"sub\",\"claimId\",\"previousOrigin\",\"canonicalOrigin\",\"stackSlug\"].every(k => typeof p[k] === \"string\")\n    && Number.isInteger(p.iat) && Number.isInteger(p.exp);\n}","typeGuard":null,"tryCatchPattern":"try {\n  verifyCloudRuntimeIdentityAssertion({ compactJws: assertion, expectedPreviousOrigin: prev });\n} catch (e) {\n  if (String((e as Error).message).includes(\"claims are incomplete\")) {\n    logger.error(\"Assertion payload missing required identity claims — check minter protocol version\", { error: e });\n  } else throw e;\n}","preventionTips":["Keep minter and verifier on the same protocol version (v: 1); bump both together","Use integer Unix seconds for iat/exp — Math.floor(Date.now()/1000)","Copy claim names exactly from the RuntimeIdentityClaims type; no aliases","Add a contract test that mints a token and runs it through verifyClaims"],"tags":["jwt","claims-validation","schema","security"],"backgroundTag":"schema-validation-failed","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}