--- phase: 08-stack-upgrade-backend-decomposition plan: 07 type: execute wave: 2 depends_on: [08-03] files_modified: - frontend/src/api/utils.js - frontend/src/api/documents.js - frontend/src/api/auth.js - frontend/src/api/admin.js - frontend/src/api/folders.js - frontend/src/api/shares.js - frontend/src/api/cloud.js - frontend/src/api/topics.js - frontend/src/api/client.js autonomous: true requirements: [CODE-04, CODE-08] tags: [frontend-decomposition, api-client, barrel-reexport] must_haves: truths: - "frontend/src/api/utils.js exists, exports request(), exports fetchWithRetry() — the consolidated 401-retry helper" - "Each of documents.js / auth.js / admin.js / folders.js / shares.js / cloud.js / topics.js exists and imports `request` from './utils.js'" - "fetchWithRetry() consolidates the 3 blob-download patterns (adminExportAuditLogCsv, adminDownloadDailyExport, fetchDocumentContent) into one helper" - "client.js is reduced to a barrel re-export: `export * from './documents.js'` etc., plus `export { fetchWithRetry, request } from './utils.js'`" - "Every function name from the old client.js is exported by exactly one domain module (no duplication, no missing names)" - "Zero consumer files (35+) under frontend/src/stores/ frontend/src/components/ frontend/src/views/ are modified — all `import * as api from '../api/client.js'` and `import { name } from '../api/client.js'` continue to resolve" - "Frontend test suite passes" artifacts: - path: "frontend/src/api/utils.js" provides: "HTTP transport: request() with bearer injection + 401-refresh-retry, fetchWithRetry() for raw Response endpoints" contains: "export async function request" - path: "frontend/src/api/documents.js" provides: "Document domain functions including fetchDocumentContent (now using fetchWithRetry)" contains: "export function listDocuments" - path: "frontend/src/api/auth.js" provides: "Auth domain functions: login, register, refreshToken, logout, totp*, password*, preferences, quota" contains: "export function login" - path: "frontend/src/api/admin.js" provides: "Admin domain functions including blob-download admin endpoints" contains: "export function adminListUsers" - path: "frontend/src/api/folders.js" provides: "Folder domain functions" contains: "export function listFolders" - path: "frontend/src/api/shares.js" provides: "Share domain functions" contains: "export function createShare" - path: "frontend/src/api/cloud.js" provides: "Cloud connection domain functions including initiateOAuth" contains: "export function listCloudConnections" - path: "frontend/src/api/topics.js" provides: "Topic domain functions" contains: "export function listTopics" - path: "frontend/src/api/client.js" provides: "Barrel re-export — preserves zero-change consumer contract" contains: "export * from './documents.js'" key_links: - from: "frontend/src/api/client.js" to: "all 7 domain modules + utils.js" via: "export * from" pattern: "export \\* from './(documents|auth|admin|folders|shares|cloud|topics)\\.js'" - from: "each domain module" to: "frontend/src/api/utils.js" via: "import { request } from './utils.js'" pattern: "import \\{ request" - from: "blob-download functions (admin.js, documents.js)" to: "frontend/src/api/utils.js fetchWithRetry" via: "import { fetchWithRetry } from './utils.js'" pattern: "import \\{.*fetchWithRetry" --- Decompose the 635-line `frontend/src/api/client.js` monolith into 7 domain modules + `utils.js` (HTTP transport + 401-retry consolidation) per locked D-12/D-13/D-14. `client.js` is reduced to a barrel re-export so the 35+ consumer files do not require any edits. The 3 blob-download 401-retry copy-paste patterns are consolidated into a single `fetchWithRetry()` helper in `utils.js`. Purpose: CODE-04 (decomposition) + CODE-08 (single definition of the auth+retry pattern instead of 3 copies). Output: 1 new `utils.js`, 7 new domain modules, `client.js` reduced to ~12 lines. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md @frontend/src/api/client.js @CLAUDE.md Function to domain map (locked per RESEARCH.md §"Frontend API Client Decomposition" and PATTERNS.md §"frontend/src/api/*"): utils.js: request(path, options) — moved from client.js lines 11-57 to break circular dep fetchWithRetry(url, options) — new consolidator for blob-download 401-retry documents.js (consumers: stores/documents.js, DocumentCard.vue, DocumentView.vue, DocumentPreviewModal.vue, etc): listDocuments, getDocument, deleteDocument, deleteDocumentRemoveOnly, classifyDocument, getUploadUrl, confirmUpload, uploadToCloud, fetchDocumentContent (refactored to use fetchWithRetry), getDocumentContentUrl auth.js (consumers: stores/auth.js, SettingsAccountTab.vue, LoginView.vue, etc): login, register, refreshToken, logout, logoutAll, getMe, changePassword, totpSetup, totpEnable, totpDisable, passwordResetRequest, passwordResetConfirm, getMyPreferences, updateMyPreferences, getMyQuota admin.js (consumers: AdminUsersTab.vue, AdminQuotasTab.vue, AdminAiConfigTab.vue, AuditLogTab.vue, etc): adminListUsers, adminCreateUser, adminDeactivateUser, adminReactivateUser, adminResetUserPassword, adminGetUserQuota, adminUpdateQuota, adminUpdateAiConfig, adminDeleteUser, getAiConfig, saveAiConfig, testAiConnection, getAiModels, adminListAuditLog, adminExportAuditLogCsv (refactored to use fetchWithRetry), adminListDailyExports, adminDownloadDailyExport (refactored to use fetchWithRetry) folders.js (consumers: stores/folders.js, FolderTreeItem.vue, AppSidebar.vue): listFolders, createFolder, getFolder, renameFolder, deleteFolder, moveDocument shares.js (consumers: SharedView.vue, ShareModal.vue): createShare, updateSharePermission, listShares, deleteShare, getSharedWithMe cloud.js (consumers: stores/cloudConnections.js, SettingsCloudTab.vue, CloudCredentialModal.vue, CloudProviderTreeItem.vue, CloudFolderTreeItem.vue): listCloudConnections, disconnectCloud, connectWebDav, updateDefaultStorage, getCloudFolders, initiateOAuth, getConnectionConfig topics.js (consumers: stores/topics.js, SearchableModelSelect.vue is unrelated): listTopics, createTopic, updateTopic, deleteTopic, suggestTopics client.js (after refactor — barrel only): export * from './documents.js' export * from './auth.js' export * from './admin.js' export * from './folders.js' export * from './shares.js' export * from './cloud.js' export * from './topics.js' export { fetchWithRetry, request } from './utils.js' Critical: All function names across these 8 files are unique — no `export *` collisions (RESEARCH.md §Pitfall 5 confirms). Task 1: Create utils.js with request() and fetchWithRetry() frontend/src/api/utils.js - frontend/src/api/client.js lines 11-57 (current `request` function — copy verbatim) - frontend/src/api/client.js lines 428-471 (adminExportAuditLogCsv blob pattern) - frontend/src/api/client.js lines 492-529 (adminDownloadDailyExport blob pattern) - frontend/src/api/client.js lines 552-581 (fetchDocumentContent blob pattern) - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/api/utils.js" (full code template for both functions) Create `frontend/src/api/utils.js`. Add a top-of-file JSDoc comment explaining: "HTTP transport + 401-retry consolidator. `request()` moved from client.js to break circular dependency (domain modules import request from here; client.js re-exports from domain modules). `fetchWithRetry()` consolidates 3 blob-download patterns. Security: Bearer from authStore memory only (CLAUDE.md)." (A) Export `async function request(path, options = {})` — copy the body verbatim from `client.js` lines 11-57. Keep the lazy `await import('../stores/auth.js')` to avoid the Pinia bootstrap cycle. Keep the `noRefreshPaths` array as a local const inside the function (or as a module-level const above the function — either works, but match the original pattern from client.js). (B) Export `async function fetchWithRetry(url, options = {}, _retry = false)` per PATTERNS.md §"frontend/src/api/utils.js" template. The function: lazy-imports `useAuthStore`, copies `headers`, injects `Authorization: Bearer ${authStore.accessToken}` if present, calls `fetch(url, { ...options, headers, credentials: 'include' })`, on 401-and-not-already-retried calls `authStore.refresh()` and recurses with `_retry=true`, on refresh failure clears `authStore.accessToken` + `authStore.user` and throws `'Session expired'`, otherwise returns the raw `Response` (caller decides how to consume it — text(), blob(), etc.). Do NOT modify `client.js` in this task — task 9 owns the barrel rewrite. cd frontend && node -e "import('./src/api/utils.js').then(m => { if (typeof m.request !== 'function') throw new Error('request missing'); if (typeof m.fetchWithRetry !== 'function') throw new Error('fetchWithRetry missing'); console.log('ok'); }).catch(e => { console.error(e); process.exit(1); })" - File `frontend/src/api/utils.js` exists - `grep -c "export async function request" frontend/src/api/utils.js` returns 1 - `grep -c "export async function fetchWithRetry" frontend/src/api/utils.js` returns 1 - `grep -c "await import('../stores/auth.js')" frontend/src/api/utils.js` returns at least 1 (lazy import preserved) - `grep -c "noRefreshPaths" frontend/src/api/utils.js` returns at least 1 - `grep -c "_retry" frontend/src/api/utils.js` returns at least 2 (request uses options._retry, fetchWithRetry uses positional _retry) utils.js exists with both helpers, identifiable by export grep, module loads without error. Task 2: Create domain modules documents.js, auth.js, topics.js frontend/src/api/documents.js, frontend/src/api/auth.js, frontend/src/api/topics.js - frontend/src/api/client.js (full file — confirm exact function bodies and signatures for each function listed in the function-to-domain map in <interfaces>) - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/api/documents.js" and §"frontend/src/api/auth.js" For each file, write a small header docstring naming the domain (e.g., "Document API — listing, fetching, uploading, content streaming"). Add `import { request, fetchWithRetry } from './utils.js'` for documents.js (because fetchDocumentContent uses fetchWithRetry); for auth.js and topics.js use only `import { request } from './utils.js'`. (A) `documents.js`: move these functions VERBATIM from client.js (preserve every parameter, every default value, every URL path, every body shape): `listDocuments`, `getDocument`, `deleteDocument`, `deleteDocumentRemoveOnly`, `classifyDocument`, `getUploadUrl`, `confirmUpload`, `uploadToCloud`, `getDocumentContentUrl`. Then refactor `fetchDocumentContent` to use `fetchWithRetry`: the new body becomes `export async function fetchDocumentContent(docId, options = {}) { const res = await fetchWithRetry('/api/documents/' + docId + '/content', options); if (!res.ok) throw new Error('Failed to fetch document content: ' + res.status); return res; }` — preserving the public contract (returns raw Response, throws on non-ok). (B) `auth.js`: move these functions VERBATIM: `login`, `register`, `refreshToken`, `logout`, `logoutAll`, `getMe`, `changePassword`, `totpSetup`, `totpEnable`, `totpDisable`, `passwordResetRequest`, `passwordResetConfirm`, `getMyPreferences`, `updateMyPreferences`, `getMyQuota`. (C) `topics.js`: move these functions VERBATIM: `listTopics`, `createTopic`, `updateTopic`, `deleteTopic`, `suggestTopics`. Do NOT remove the functions from `client.js` yet — task 9 owns the barrel rewrite and deletion of the original bodies. This task only adds new files. cd frontend && node -e "Promise.all([import('./src/api/documents.js'), import('./src/api/auth.js'), import('./src/api/topics.js')]).then(([d, a, t]) => { ['listDocuments','getDocument','deleteDocument','classifyDocument','getUploadUrl','fetchDocumentContent'].forEach(n => { if (typeof d[n] !== 'function') throw new Error('documents.' + n + ' missing'); }); ['login','register','refreshToken','logout','getMe','changePassword','totpSetup','passwordResetRequest','getMyQuota'].forEach(n => { if (typeof a[n] !== 'function') throw new Error('auth.' + n + ' missing'); }); ['listTopics','createTopic','suggestTopics'].forEach(n => { if (typeof t[n] !== 'function') throw new Error('topics.' + n + ' missing'); }); console.log('ok'); }).catch(e => { console.error(e); process.exit(1); })" - Files exist: `frontend/src/api/documents.js`, `frontend/src/api/auth.js`, `frontend/src/api/topics.js` - `grep -c "^export function listDocuments\\|^export async function fetchDocumentContent" frontend/src/api/documents.js` returns at least 2 - `grep -c "import { request, fetchWithRetry } from './utils.js'" frontend/src/api/documents.js` returns 1 - `grep -c "import { request } from './utils.js'" frontend/src/api/auth.js` returns 1 - `grep -c "import { request } from './utils.js'" frontend/src/api/topics.js` returns 1 - `grep -c "^export function login\\|^export function register" frontend/src/api/auth.js` returns at least 2 - The node -e import check above prints `ok` (all named exports resolvable) Three domain files exist with all expected exports; verified by node import. Task 3: Create domain modules admin.js, folders.js, shares.js, cloud.js frontend/src/api/admin.js, frontend/src/api/folders.js, frontend/src/api/shares.js, frontend/src/api/cloud.js - frontend/src/api/client.js (full file — confirm exact function bodies and signatures) - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/api/admin.js" (blob refactor pattern using fetchWithRetry) For each file, add a header docstring naming the domain. Import statements: - admin.js: `import { request, fetchWithRetry } from './utils.js'` (audit-log CSV + daily-export use fetchWithRetry) - folders.js, shares.js, cloud.js: `import { request } from './utils.js'` (A) `admin.js`: move these functions VERBATIM from client.js: `adminListUsers`, `adminCreateUser`, `adminDeactivateUser`, `adminReactivateUser`, `adminResetUserPassword`, `adminGetUserQuota`, `adminUpdateQuota`, `adminUpdateAiConfig`, `adminDeleteUser`, `getAiConfig`, `saveAiConfig`, `testAiConnection`, `getAiModels`, `adminListAuditLog`, `adminListDailyExports`. Then refactor `adminExportAuditLogCsv` and `adminDownloadDailyExport` to use `fetchWithRetry` (per PATTERNS.md §"frontend/src/api/admin.js"). The new `adminExportAuditLogCsv(params = {})` body: build the URLSearchParams exactly as before, call `const res = await fetchWithRetry('/api/admin/audit-log/export?' + searchParams)`, on `!res.ok` throw `'Export failed: ' + res.status`, then call `res.text()`, create the Blob, trigger the download via temporary anchor click, revokeObjectURL on a setTimeout. The new `adminDownloadDailyExport(date)` body: `const res = await fetchWithRetry('/api/admin/audit-log/daily-exports/' + date)`, on `!res.ok` throw, then download the blob the same way. Drop the `_retry` boilerplate — fetchWithRetry handles it. (B) `folders.js`: move these functions VERBATIM: `listFolders`, `createFolder`, `getFolder`, `renameFolder`, `deleteFolder`, `moveDocument`. (C) `shares.js`: move these functions VERBATIM: `createShare`, `updateSharePermission`, `listShares`, `deleteShare`, `getSharedWithMe`. (D) `cloud.js`: move these functions VERBATIM: `listCloudConnections`, `disconnectCloud`, `connectWebDav`, `updateDefaultStorage`, `getCloudFolders`, `initiateOAuth`, `getConnectionConfig`. Do NOT remove the functions from `client.js` yet — task 4 owns the barrel rewrite. cd frontend && node -e "Promise.all([import('./src/api/admin.js'), import('./src/api/folders.js'), import('./src/api/shares.js'), import('./src/api/cloud.js')]).then(([ad, fo, sh, cl]) => { ['adminListUsers','adminCreateUser','adminUpdateQuota','getAiConfig','saveAiConfig','testAiConnection','adminListAuditLog','adminExportAuditLogCsv','adminDownloadDailyExport'].forEach(n => { if (typeof ad[n] !== 'function') throw new Error('admin.' + n + ' missing'); }); ['listFolders','createFolder','moveDocument'].forEach(n => { if (typeof fo[n] !== 'function') throw new Error('folders.' + n + ' missing'); }); ['createShare','updateSharePermission','getSharedWithMe'].forEach(n => { if (typeof sh[n] !== 'function') throw new Error('shares.' + n + ' missing'); }); ['listCloudConnections','initiateOAuth','getConnectionConfig'].forEach(n => { if (typeof cl[n] !== 'function') throw new Error('cloud.' + n + ' missing'); }); console.log('ok'); }).catch(e => { console.error(e); process.exit(1); })" - Files exist: `frontend/src/api/admin.js`, `frontend/src/api/folders.js`, `frontend/src/api/shares.js`, `frontend/src/api/cloud.js` - `grep -c "import { request, fetchWithRetry } from './utils.js'" frontend/src/api/admin.js` returns 1 - `grep -c "import { request } from './utils.js'" frontend/src/api/folders.js` returns 1 - `grep -c "import { request } from './utils.js'" frontend/src/api/shares.js` returns 1 - `grep -c "import { request } from './utils.js'" frontend/src/api/cloud.js` returns 1 - `grep -c "fetchWithRetry" frontend/src/api/admin.js` returns at least 2 (one each for adminExportAuditLogCsv and adminDownloadDailyExport) - `grep -c "_retry" frontend/src/api/admin.js` returns 0 (boilerplate dropped — fetchWithRetry owns retry) - `grep -c "^export function initiateOAuth" frontend/src/api/cloud.js` returns 1 (named import is used by SettingsCloudTab.vue) - The node -e import check above prints `ok` Four domain files exist with all expected exports; blob-download functions use fetchWithRetry; node import succeeds. Task 4: Rewrite client.js as barrel re-export and run frontend tests frontend/src/api/client.js - frontend/src/api/client.js (the current 635-line file — you are about to replace its contents) - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/api/client.js (transport + barrel, request-response) — MODIFIED" (the full final-state template) Replace the ENTIRE contents of `frontend/src/api/client.js` with this final form (preserves all previous exports via `export *`): /** * API client — barrel re-export. * * The HTTP transport (request) and 401-retry consolidator (fetchWithRetry) live in utils.js * to avoid the circular import that would arise if domain modules imported request from here * while this file re-exported from those same domain modules. * * All 35+ consumer files continue using one of: * import * as api from '...api/client.js' — namespace pattern * import { funcName } from '...api/client.js' — named import pattern * without any changes. */ export * from './documents.js' export * from './auth.js' export * from './admin.js' export * from './folders.js' export * from './shares.js' export * from './cloud.js' export * from './topics.js' export { fetchWithRetry, request } from './utils.js' After writing the new file contents, run `cd frontend && npm test` to confirm the test suite passes. The frontend tests use the `import { ... } from '../api/client.js'` and `import * as api from '../api/client.js'` patterns — both must resolve every previously-exported name through the barrel. If any test fails with `TypeError: ... is not a function` or similar, the symptom is a missing or misspelled export in one of the domain modules — diagnose by re-checking the function-to-domain map. After tests pass, smoke the dev build: `cd frontend && npm run build` (Vite production build). Vite will fail loudly if `export * from` produces ambiguous names or unresolved modules. If the build fails, fix and rerun. cd frontend && wc -l src/api/client.js && npm test 2>&1 | tail -20 && npm run build 2>&1 | tail -10 - `wc -l < frontend/src/api/client.js` returns less than 25 (was 635; barrel is ~15-20 lines) - `grep -c "^export \\* from " frontend/src/api/client.js` returns 7 - `grep -c "export { fetchWithRetry, request } from './utils.js'" frontend/src/api/client.js` returns 1 - `grep -c "async function request" frontend/src/api/client.js` returns 0 (request moved to utils.js) - `grep -c "noRefreshPaths" frontend/src/api/client.js` returns 0 (lived inside request, now in utils.js) - `cd frontend && npm test` exits 0 - `cd frontend && npm run build` exits 0 - `grep -rn "from '../api/client.js'\\|from '../../api/client.js'\\|from '../../../api/client.js'" frontend/src/stores/ frontend/src/components/ frontend/src/views/ 2>/dev/null | wc -l` returns same count as before this plan (35+ files; none modified) - No file under `frontend/src/stores/`, `frontend/src/components/`, or `frontend/src/views/` is listed in git diff for this plan client.js is a thin barrel; frontend tests pass; production build succeeds; zero consumer files modified. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Browser → backend API | All requests carry Bearer token from authStore memory (never localStorage per CLAUDE.md) | | Domain module → utils.js request | In-process import; no untrusted data crosses | | fetchWithRetry → authStore.refresh on 401 | Refresh flow uses httpOnly cookie; on failure the in-memory access token is cleared | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-08-07-01 | Spoofing | Bearer token injection | mitigate | `request` body copied verbatim from client.js — preserves `if (authStore.accessToken) headers['Authorization'] = ...` and lazy import of authStore | | T-08-07-02 | Tampering | Circular import producing undefined exports | mitigate | `request` lives in utils.js; client.js never re-exports request from itself; domain modules import from utils.js (PATTERNS.md §"Frontend domain module import line") | | T-08-07-03 | Repudiation | 401-refresh-retry loop | mitigate | `_retry` flag is preserved in both `request` and `fetchWithRetry`; recursion is bounded | | T-08-07-04 | Information Disclosure | Token stored in JS storage | mitigate | CLAUDE.md rule preserved: token from `authStore.accessToken` (memory only), never read from localStorage/sessionStorage | | T-08-07-05 | Tampering | `export *` name collision | mitigate | RESEARCH.md §Pitfall 5 verified all current client.js function names are unique; acceptance criterion runs frontend tests + production build which would fail on collision | | T-08-07-06 | Denial of Service | Consumer files break | mitigate | Barrel re-export preserves every named export; acceptance criterion verifies zero consumer files in stores/components/views were modified | | T-08-07-SC | Supply Chain | No new npm packages | accept | This plan installs zero new packages | - `cd frontend && npm test` — zero failures - `cd frontend && npm run build` — succeeds - `wc -l frontend/src/api/client.js` — under 25 lines - `grep -rn "import.*from '../api/client.js'\\|import.*from '../../api/client.js'\\|import.*from '../../../api/client.js'" frontend/src/stores/ frontend/src/components/ frontend/src/views/` returns the same line count as before this plan - `git diff --stat HEAD frontend/src/stores/ frontend/src/components/ frontend/src/views/` returns no entries - 8 new files in `frontend/src/api/` (utils.js + 7 domain modules) - client.js reduced to ~15 lines - Frontend tests + production build pass - Zero consumer files modified (the entire point of the barrel pattern) - fetchWithRetry consolidates 3 blob-download patterns (CODE-08) Create `.planning/phases/08-stack-upgrade-backend-decomposition/08-07-SUMMARY.md` when done. Include: (a) line counts before/after for client.js and the 8 new files, (b) list of all consumer files (stores + components + views) confirmed untouched, (c) confirmation that the 3 old blob-download _retry boilerplate blocks were consolidated into the single `fetchWithRetry` helper.