{"record":{"id":"9b4dd0ee8b6963e5","repo":"paperclipai/paperclip","slug":"durable-prp-bootstrap-ttl-is-invalid","errorCode":null,"errorMessage":"Durable PRP bootstrap TTL is invalid.","messagePattern":"Durable PRP bootstrap TTL is invalid\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts","lineNumber":1736,"sourceCode":"      existing !== null &&\n      canonicalJson(existing) !== canonicalJson(runAttachTemplate)\n    ) {\n      throw new Error(\"Durable PRP run attachment template conflicts.\");\n    }\n    if (existing !== undefined && existing !== null) return;\n    this.#store.state.runAttachTemplate = structuredClone(runAttachTemplate);\n    this.#store.save();\n  }\n\n  issueBootstrapTicket(ttlMs = 5_000): string {\n    this.#store.assertWritable();\n    if (this.#store.state.warmTransition) {\n      throw new Error(\n        \"Warm transition recovery requires its explicit one-use bootstrap capability.\",\n      );\n    }\n    if (!Number.isInteger(ttlMs) || ttlMs < 1_000 || ttlMs > 60_000) {\n      throw new Error(\"Durable PRP bootstrap TTL is invalid.\");\n    }\n    this.#pruneCredentials();\n    const ticket = `bootstrap_${randomUUID()}`;\n    const material = credentialMaterial(ticket);\n    const expiresAtUnixMs = Date.now() + ttlMs;\n    this.#store.state.tickets[material.credentialId] = {\n      recordId: `bootstrap_ticket_${randomUUID()}`,\n      credentialId: material.credentialId,\n      authKeyDigest: `sha256:${material.authKey.toString(\"hex\")}`,\n      identity: structuredClone(this.#identity),\n      runnerVersion: this.#expectedRunnerVersion,\n      runnerDigest: this.#expectedRunnerDigest,\n      expiresAt: new Date(expiresAtUnixMs).toISOString(),\n      expiresAtUnixMs,\n      usedAt: null,\n    };\n    this.#store.state.freshBootstraps += 1;\n    this.#store.save();","sourceCodeStart":1718,"sourceCodeEnd":1754,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts#L1718-L1754","documentation":"issueBootstrapTicket(ttlMs) validates that the TTL is an integer between 1,000 and 60,000 milliseconds inclusive. A non-integer, NaN, zero/negative, or over-60-second TTL fails this range check and the ticket is not created.","triggerScenarios":"Calling issueBootstrapTicket(0), issueBootstrapTicket(500), issueBootstrapTicket(120000), issueBootstrapTicket(2.5), or passing a value parsed from config as a string/NaN (e.g. issueBootstrapTicket(Number(process.env.TTL)) with TTL unset).","commonSituations":"TTL read from env/config in the wrong unit (seconds vs milliseconds); unset env var becoming NaN; a default of 0 used to mean \"use default\" instead of omitting the argument; fractional seconds passed through unconverted.","solutions":["Pass an integer TTL in milliseconds within [1000, 60000], or omit the argument to use the 5000 ms default.","Convert seconds to milliseconds explicitly (seconds * 1000) when the config is expressed in seconds.","Clamp or validate the configured value before calling: Number.isInteger(ttl) && ttl >= 1000 && ttl <= 60000.","Handle unset config by falling back to the default instead of passing Number(undefined) (NaN)."],"exampleFix":"// before\ncontrolPlane.issueBootstrapTicket(Number(process.env.BOOTSTRAP_TTL_SECONDS)); // NaN / seconds unit\n// after\nconst ttlMs = process.env.BOOTSTRAP_TTL_SECONDS\n  ? Math.min(60000, Math.max(1000, Number(process.env.BOOTSTRAP_TTL_SECONDS) * 1000))\n  : undefined;\ncontrolPlane.issueBootstrapTicket(ttlMs);","handlingStrategy":"validation","validationCode":"function safeTtlMs(raw: string | undefined, fallback = 5000): number {\n  if (raw === undefined) return fallback;\n  const n = Number(raw) * (raw.includes(\"ms\") ? 1 : 1000); // beware unit mix\n  return Number.isInteger(n) && n >= 1000 && n <= 60000 ? n : fallback;\n}\ncontrolPlane.issueBootstrapTicket(safeTtlMs(process.env.BOOTSTRAP_TTL));","typeGuard":"function isValidTtl(v: unknown): v is number {\n  return typeof v === \"number\" && Number.isInteger(v) && v >= 1000 && v <= 60000;\n}","tryCatchPattern":"try {\n  ticket = controlPlane.issueBootstrapTicket(ttlMs);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"bootstrap TTL is invalid\")) {\n    ticket = controlPlane.issueBootstrapTicket(); // default 5s\n  } else throw err;\n}","preventionTips":["Standardize TTL config in milliseconds and convert at the config boundary.","Clamp configured TTLs into [1000, 60000] before calling.","Treat unset/NaN config as \"use default\" rather than passing Number(undefined)."],"tags":["validation","argument","ttl","range-check"],"backgroundTag":"value-out-of-range","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"}