{"record":{"id":"f4862ec0fe049de3","repo":"schollz/croc","slug":"expired-or-invalid-stored-upload-receipt","errorCode":null,"errorMessage":"expired or invalid stored-upload receipt","messagePattern":"expired or invalid stored-upload receipt","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"web/src/App.tsx","lineNumber":286,"sourceCode":"      const storageKey = sessionStorage.key(index);\n      if (!storageKey?.startsWith(\"croc-store-upload:\")) continue;\n      try {\n        const raw = sessionStorage.getItem(storageKey);\n        if (!raw) continue;\n        const receipt = JSON.parse(raw) as {\n          browserURL?: string;\n          uploadToken?: string;\n          expiresAt?: string;\n          downloads?: number;\n        };\n        const expiresAt = new Date(receipt.expiresAt ?? \"\");\n        if (\n          !receipt.browserURL ||\n          !receipt.uploadToken ||\n          !Number.isFinite(expiresAt.getTime()) ||\n          expiresAt.getTime() <= Date.now()\n        ) {\n          throw new Error(\"expired or invalid stored-upload receipt\");\n        }\n        const share = parseStoredShare(receipt.browserURL);\n        const candidate: StoredUploadResult = {\n          share,\n          uploadToken: receipt.uploadToken,\n          expiresAt: expiresAt.toISOString(),\n          browserURL: receipt.browserURL,\n          cliToken: formatStoredCLIToken(share),\n          downloads:\n            Number.isSafeInteger(receipt.downloads) &&\n            (receipt.downloads ?? 0) > 0\n              ? receipt.downloads!\n              : 1,\n        };\n        if (\n          !restored ||\n          new Date(candidate.expiresAt).getTime() >\n            new Date(restored.expiresAt).getTime()","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/App.tsx#L268-L304","documentation":"Thrown in App.tsx when restoring a persisted stored-upload receipt: it must contain browserURL and uploadToken, and a parseable expiresAt strictly in the future (expiresAt.getTime() > Date.now()). Any missing field, unparseable date (including the empty-string default), or elapsed expiry throws.","triggerScenarios":"Re-opening the app after a stored upload's expiry window passed (default 24h); a receipt written by an older app version lacking uploadToken/browserURL; a corrupted or hand-edited receipt string in localStorage.","commonSituations":"User returns the next day after default expiry; schema-migration leftovers in storage; device clock skew making valid receipts look expired; private-mode storage writing partial JSON.","solutions":["Catch this error during restore and prune the receipt from storage instead of surfacing a crash","Write receipts as one atomic setItem of fully-built JSON so partial writes cannot persist","On suspected clock skew, compare against server time before declaring expiry"],"exampleFix":"// before\nconst receipt = JSON.parse(raw);\nvalidateReceipt(receipt); // throws on stale receipt, breaks app load\n\n// after\nlet receipt;\ntry {\n  receipt = parseStoredReceipt(raw);\n} catch {\n  localStorage.removeItem(key); // stale/corrupt receipt: prune silently\n}","handlingStrategy":"try-catch","validationCode":"function isViableReceipt(r: unknown): boolean {\n  const x = r as Record<string, unknown>;\n  return typeof x?.browserURL === 'string' && x.browserURL !== '' &&\n    typeof x?.uploadToken === 'string' && x.uploadToken !== '' &&\n    typeof x?.expiresAt === 'string' && Number.isFinite(Date.parse(x.expiresAt)) &&\n    Date.parse(x.expiresAt) > Date.now();\n}","typeGuard":"function isRestorableReceipt(r: unknown): r is { browserURL: string; uploadToken: string; expiresAt: string } {\n  return isViableReceipt(r);\n}","tryCatchPattern":"try { receipt = parseStoredReceipt(raw); } catch (e) { if (e instanceof Error && e.message === 'expired or invalid stored-upload receipt') { localStorage.removeItem(key); receipt = null; } else throw e; }","preventionTips":["Prune expired receipts on a schedule, not only at restore time","Write receipts as one atomic setItem of complete JSON after upload finishes"],"tags":["stored-transfer","localstorage","persistence","receipt"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}