{"record":{"id":"4eee81f7decffd89","repo":"schollz/croc","slug":"stored-transfer-expiration-must-be-a-whole-number","errorCode":null,"errorMessage":"Stored-transfer expiration must be a whole number of seconds of at least one minute","messagePattern":"Stored-transfer expiration must be a whole number of seconds of at least one minute","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/src/protocol/stored.ts","lineNumber":588,"sourceCode":"    downloads = 1,\n    expiresSeconds = 24 * 60 * 60,\n    callbacks = {},\n    signal,\n  } = options;\n  if (!Number.isSafeInteger(downloads) || downloads < 1) {\n    throw new Error(\"Stored-transfer downloads must be a positive integer\");\n  }\n  if (downloads > settings.maxDownloads) {\n    throw new Error(\n      `Stored transfers can allow at most ${settings.maxDownloads} downloads`,\n    );\n  }\n  if (\n    !Number.isSafeInteger(expiresSeconds) ||\n    expiresSeconds < 60 ||\n    expiresSeconds > 9_223_372_036\n  ) {\n    throw new Error(\n      \"Stored-transfer expiration must be a whole number of seconds of at least one minute\",\n    );\n  }\n  if (\n    settings.maxExpiresSeconds > 0 &&\n    expiresSeconds > settings.maxExpiresSeconds\n  ) {\n    throw new Error(\n      `Stored transfers can expire after at most ${settings.maxExpiresSeconds} seconds`,\n    );\n  }\n  const key = await wasm().storeGenerateKey();\n  const plan = planStoredUpload(files);\n  const created = await createStoredUpload(\n    key,\n    plan,\n    downloads,\n    expiresSeconds,","sourceCodeStart":570,"sourceCodeEnd":606,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/stored.ts#L570-L606","documentation":"Thrown by uploadStoredFiles() (stored.ts:583) when `expiresSeconds` is not a safe integer, is under 60 (one minute), or exceeds 9,223,372,036 seconds (~292 years, the int64 ceiling the backend accepts). The service stores expiry as whole seconds in an int64, so fractional, sub-minute, or astronomically large values are rejected before the upload starts.","triggerScenarios":"Calling uploadStoredFiles({ expiresSeconds: 30 }), with a fractional value like 86400.5, NaN, Infinity, or anything above 9_223_372_036. Also when expiry is computed from a float (fractional minutes * 60) or passed as a string ('86400' fails Number.isSafeInteger).","commonSituations":"UI permits '0 minutes'; expiry derived from a millisecond Date.now() delta without rounding; values deserialized from JSON config left as strings.","solutions":["Round and floor at 60: expiresSeconds = Math.max(60, Math.floor(v)), then re-check Number.isSafeInteger","Compute expiry from integer units (integer hours * 3600)","Coerce possible strings with Number() before validating"],"exampleFix":"// before\nawait uploadStoredFiles({ files, settings, expiresSeconds: minutes * 60 });\n\n// after\nconst expiresSeconds = Math.max(60, Math.round(minutes) * 60);\nawait uploadStoredFiles({ files, settings, expiresSeconds });","handlingStrategy":"validation","validationCode":"const expiresSeconds = Math.round(Number(raw));\nif (!Number.isSafeInteger(expiresSeconds) || expiresSeconds < 60 || expiresSeconds > 9_223_372_036) {\n  throw new RangeError('expiration must be whole seconds in [60, 9223372036]');\n}","typeGuard":"function isValidExpiration(v: unknown): v is number {\n  return typeof v === 'number' && Number.isSafeInteger(v) && v >= 60 && v <= 9_223_372_036;\n}","tryCatchPattern":"try { await uploadStoredFiles(opts); } catch (e) { if (e instanceof Error && e.message.includes('whole number of seconds')) { flagField('expiration'); return; } throw e; }","preventionTips":["Compute expiry from integer minutes/hours only","Enforce a 60-second minimum in the UI"],"tags":["validation","stored-transfer","typescript","upload"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}