From dd86d6675d5ff6f8e3e2675f36347469f7c7b927 Mon Sep 17 00:00:00 2001 From: Timmy Date: Sat, 22 Aug 2026 21:53:08 +0000 Subject: [PATCH] fix: close hostile-review blockers in ledger portability - provenance origin set is own-safe exact membership (Set.has); inherited toString/constructor/__proto__ names can never become origins - import/export symmetry restored with an explicit bounded policy: MAX_IMPORT_BYTES raised 2 MiB -> 16 MiB UTF-8 bytes, above any export this app can produce (photos capped at 4 MiB binary), so valid exports always re-import without silent data loss while hostile files stay bounded - byte limit is byte-exact now: utf8ByteLength() measures real UTF-8 bytes (multibyte boundaries tested), and the browser rejects oversized files by File.size BEFORE File.text() reads user data - collision-safe deterministic mergeLedgers(): existing user-owned rows win, incoming rows only ever added for new ids, intra-file duplicates collapse deterministically, every collision reported explicitly in the import toast (no duplicate/overwrite/shadow of user records) - base-path Delete Everything is namespace-scoped: root still cleans/ migrates the legacy store to prevent resurrection, /timmy-staging no longer erases another namespace's global legacy ledger (browser regression covers deletion with root legacy data present) - strict current-schema values: Bristol 1-7 / urgency 0-4 / discomfort 0-4 must be true integers (out-of-range falls back instead of silent clamping), photos restricted to JPEG/PNG/WebP base64 raster data URLs (SVG/GIF/non-base64 dropped), invalid dates never throw or persist Invalid Date values Verification: npm test 91/91, test:ui/test:photo/test:sleek/test:portability PASS, staging-deploy 20/20 OK, check:syntax clean, npm audit 0 high, check_diff clean, adversarial probe battery (exact-byte boundary at cap, prototype pollution via JSON, lone surrogates, data-URL strictness) green. --- .gitignore | 1 + app.js | 6 +- artifacts/portability-import-mobile.png | Bin 0 -> 108815 bytes service-worker.js | 2 +- src/domain.js | 81 +++++++++-- tests/domain.test.js | 186 +++++++++++++++++++++++- tests/ledger-portability.acceptance.mjs | 75 +++++++++- 7 files changed, 335 insertions(+), 16 deletions(-) create mode 100644 artifacts/portability-import-mobile.png diff --git a/.gitignore b/.gitignore index a8758b0..8625199 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ *.pyc .env .env.* +.worktrees/ diff --git a/app.js b/app.js index c8eee79..663f81e 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,4 @@ -import { bucketForBristolType, buildTimmySummary, detectUrgentFlags, detectUrgentText, exportLedger, hasUrgentLedgerContext, importLedger, photoQualityMessage, sanitizeEntry, urgentChatMessage } from './src/domain.js'; +import { bucketForBristolType, buildTimmySummary, detectUrgentFlags, detectUrgentText, exportLedger, hasUrgentLedgerContext, importLedger, MAX_IMPORT_BYTES, mergeLedgers, photoQualityMessage, sanitizeEntry, urgentChatMessage } from './src/domain.js'; import { mergeVisualSuggestion } from './src/analysis.js'; const runtimeConfig = { @@ -119,8 +119,8 @@ function privacy(){ document.querySelector('#export').onclick=exportData;document.querySelector('#import').onchange=importData;document.querySelector('#delete-all').onclick=deleteData; } function exportData(){const blob=new Blob([exportLedger(entries)],{type:'application/json'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='timmy-ledger.json';a.click();URL.revokeObjectURL(a.href);toast('Export created');} -async function importData(e){try{const text=await e.target.files[0].text();entries=[...entries,...importLedger(text)];saveEntries();render();toast('Ledger imported')}catch(err){toast(err.message)}} -function deleteData(){if(confirm('Delete every local Timmy entry and photo? This cannot be undone.')){entries=[];localStorage.removeItem(STORE);localStorage.removeItem(LEGACY_STORE);render();toast('Local ledger deleted')}} +async function importData(e){try{const file=e.target.files[0];if(!file)return;if(file.size>MAX_IMPORT_BYTES)throw new RangeError('That file is too large to be a Timmy export.');const text=await file.text();const result=mergeLedgers(entries,importLedger(text));entries=result.merged;saveEntries();render();toast(result.added.length?`Ledger imported: ${result.added.length} new log${result.added.length===1?'':'s'}`:`Already in your ledger: ${result.skippedIds.length} log${result.skippedIds.length===1?'':'s'} skipped (kept your saved version)`)}catch(err){toast(err.message)}} +function deleteData(){if(confirm('Delete every local Timmy entry and photo? This cannot be undone.')){entries=[];localStorage.removeItem(STORE);if(BASE_PATH==='/')localStorage.removeItem(LEGACY_STORE);render();toast('Local ledger deleted')}} function openPhotoFirst(){form=draft();photoDataUrl='';photoHint='';aiSuggestion=null;visionStatus=null;showPhotoFirst('pick');loadVisionStatus()} async function loadVisionStatus(){ diff --git a/artifacts/portability-import-mobile.png b/artifacts/portability-import-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..8313356a4c17cd30f8101a073874fa7efce5737c GIT binary patch literal 108815 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