feat: Phase 29 Infrastructure Hardening#383
Conversation
Create lib/logger.ts with level-filtered output (debug/info/warn/error). In production, only warn and error are emitted. Replace all 124 raw console.* calls across 28 web app source files with logger equivalents. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All .catch with empty callbacks on IPFS unpin calls and AudioContext close calls now log warnings via the structured logger so failures are visible instead of silently swallowed. Also fixes the one occurrence in packages/sdk/src/client.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace all `as any` casts in production web code with typed alternatives. Polyfill globalThis augmentations use proper declare-global blocks, the debug error log uses a typed Window interface, and the Zustand debug store uses a typed window property. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove 00-Preliminary-R&D/poc/ which was superseded by the production implementation. The POC code is preserved in git history. Add ARCHIVED.md documenting what was removed and where to find it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create plans, summaries, and verification for Phase 28. All 4 success criteria verified: structured logger, visible unpin failures, no as-any casts, POC archived. Update ROADMAP.md and STATE.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exposes the existing RepublishService.unenrollIpns() via a new REST endpoint. Accepts up to 200 IPNS names per call with validation. Scoped to authenticated user via JWT guard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fires when POST /auth/test-login exceeds 100 calls/hour on staging. Verified: production guard (NODE_ENV + timingSafeEqual) and Kubo port 5001 bound to 127.0.0.1 in staging compose are already in place. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add fireAndForgetUnenroll() and collectSubtreeIpnsNames() to CipherBoxClient. deleteItem, deleteToBin, permanentDelete, and emptyBin now batch-unenroll orphaned IPNS records from the TEE republish schedule via POST /ipns/unenroll. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
IPNS unenrollment is now handled by SDK's fireAndForgetUnenroll(). Removes orphaned-record warnings and deferred-work comments from folder.service.ts and delete.service.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-existing implicit-any on line 1165 caught by DTS build. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 3 plans executed, verification passed on all 4 success criteria. ROADMAP.md, STATE.md updated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WalkthroughMarks Phases 28 and 29 complete (2026-03-28) and implements Phase 29 work: a batch IPNS unenroll REST API plus DTOs, SDK fire-and-forget batch unenroll integration into delete flows, structured logger replacements in web services, and a Grafana test-login alert. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User/Client
participant SDK as SDK Client
participant API as IPNS Controller
participant Service as IPNS Service
participant Republish as Republish Service
User->>SDK: deleteItem(item)
SDK->>SDK: collectRemovedItemIpnsNames(item)
Note over SDK: extract fileMetaIpnsName(s)
SDK->>SDK: chunk & fireAndForgetUnenroll(chunks)
SDK->>API: POST /ipns/unenroll (chunk)
activate API
API->>Service: unenrollBatch(userId, chunk)
activate Service
Service->>Republish: unenrollIpns(userId, name) per item
Republish-->>Service: success/failure (per-item)
Service-->>API: { totalUnenrolled: N }
deactivate Service
API-->>SDK: BatchUnenrollIpnsResponseDto
deactivate API
SDK->>SDK: log failures (fire-and-forget)
SDK-->>User: delete flow completes
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Phase 29 infrastructure hardening to reduce operational buildup of orphaned IPNS republish enrollments and add staging monitoring around the test-login endpoint (plus Phase 28 base commits: structured logging/type hygiene + POC archival).
Changes:
- Added
POST /ipns/unenrollbatch endpoint (max 200) and regenerated@cipherbox/api-clientbindings. - Wired SDK delete flows to fire-and-forget batch unenrollment (including recursive subtree collection for loaded folders).
- Added a Grafana alert for staging test-login rate; replaced remaining web
console.*usage with structuredloggerand tightened types for debug globals/polyfills.
Reviewed changes
Copilot reviewed 71 out of 74 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/sdk/src/client.ts | Adds fire-and-forget IPNS unenroll + subtree collection; hooks into delete flows; improves unpin error visibility |
| packages/sdk/src/bin/index.ts | Bin ops plumbing returns removed item for unenroll wiring |
| apps/api/src/ipns/dto/unenroll.dto.ts | Adds batch unenroll DTO + response DTO with validation |
| apps/api/src/ipns/dto/index.ts | Re-exports new unenroll DTOs |
| apps/api/src/ipns/ipns.controller.ts | Adds POST /ipns/unenroll route with throttling + swagger docs |
| apps/api/src/ipns/ipns.service.ts | Implements unenrollBatch() delegating to RepublishService |
| packages/api-client/openapi.json | Updates OpenAPI spec to include unenroll endpoint and formatting updates |
| packages/api-client/src/generated/ipns/ipns.ts | Regenerates client with ipnsControllerUnenrollBatch |
| packages/api-client/src/models/batchUnenrollIpnsDto.ts | Adds generated unenroll request model |
| packages/api-client/src/models/batchUnenrollIpnsResponseDto.ts | Adds generated unenroll response model |
| packages/api-client/src/models/index.ts | Exports newly generated unenroll models |
| docker/grafana/alerts/test-login-rate.json | Adds staging alert for elevated test-login usage |
| apps/web/src/lib/logger.ts | Introduces structured, level-filtered logger for the web app |
| apps/web/src/services/upload.service.ts | Switches error logging to structured logger |
| apps/web/src/services/share.service.ts | Switches warn logging to structured logger |
| apps/web/src/services/search-index.service.ts | Switches warn logging to structured logger |
| apps/web/src/services/ipns.service.ts | Switches warn logging to structured logger |
| apps/web/src/services/folder.service.ts | Removes legacy TODOs; points TEE unenroll responsibility to SDK path |
| apps/web/src/services/device-registry.service.ts | Switches error logging to structured logger |
| apps/web/src/services/delete.service.ts | Removes legacy TODO; switches error logging to structured logger |
| apps/web/src/services/bin.service.ts | Switches logging to structured logger; makes unpin failures visible |
| apps/web/src/hooks/useSharedNavigation.ts | Switches error logging to structured logger |
| apps/web/src/hooks/useSearch.ts | Switches warn/error logging to structured logger |
| apps/web/src/hooks/useFolderNavigation.ts | Switches error logging to structured logger |
| apps/web/src/hooks/useFileVersions.ts | Switches warn logging to structured logger; makes unpin failures visible |
| apps/web/src/hooks/useFileOperations.ts | Switches warn logging to structured logger; makes unpin failures visible |
| apps/web/src/hooks/useFileDownload.ts | Switches error logging to structured logger |
| apps/web/src/hooks/useDropUpload.ts | Switches warn logging to structured logger; makes cleanup failures visible |
| apps/web/src/hooks/useBin.ts | Switches error logging to structured logger |
| apps/web/src/hooks/useAuth.ts | Switches log/warn/error logging to structured logger |
| apps/web/src/components/settings/ConnectionTest.tsx | Switches warn logging to structured logger |
| apps/web/src/components/file-browser/FileBrowser.tsx | Switches error logging to structured logger |
| apps/web/src/components/file-browser/BinBrowser.tsx | Switches error logging to structured logger |
| apps/web/src/components/file-browser/ShareDialog.tsx | Switches error logging to structured logger |
| apps/web/src/components/file-browser/InviteLinkTab.tsx | Switches error logging to structured logger |
| apps/web/src/components/file-browser/ReplaceFileDialog.tsx | Switches error logging to structured logger; makes unpin failures visible |
| apps/web/src/components/file-browser/PdfPreviewDialog.tsx | Switches error logging to structured logger |
| apps/web/src/components/file-browser/AudioPlayerDialog.tsx | Switches warn logging to structured logger; makes AudioContext close failures visible |
| apps/web/src/lib/sw-registration.ts | Switches warn/error logging to structured logger |
| apps/web/src/lib/crypto/key-wrapping.ts | Switches error logging to structured logger |
| apps/web/src/lib/web3auth/hooks.ts | Switches console logging to structured logger for CoreKit debug traces/errors |
| apps/web/src/lib/web3auth/core-kit-provider.tsx | Switches initialization error logging to structured logger |
| apps/web/src/stores/folder.store.ts | Adds typed window.__ZUSTAND_FOLDER_STORE__ for E2E |
| apps/web/src/main.tsx | Types DEV-only error capture globals; avoids any casts |
| apps/web/src/polyfills.ts | Uses typed declare global shims for process/Buffer |
| 00-Preliminary-R&D/ARCHIVED.md | Documents archival of legacy PoC content |
| 00-Preliminary-R&D/poc/.env.example | Deleted legacy PoC artifact |
| 00-Preliminary-R&D/poc/README.md | Deleted legacy PoC artifact |
| 00-Preliminary-R&D/poc/package.json | Deleted legacy PoC artifact |
| 00-Preliminary-R&D/poc/scripts/gen-private-key.ts | Deleted legacy PoC artifact |
| 00-Preliminary-R&D/poc/src/index.ts | Deleted legacy PoC artifact |
| 00-Preliminary-R&D/poc/state/state.json | Deleted legacy PoC artifact |
| 00-Preliminary-R&D/poc/tsconfig.json | Deleted legacy PoC artifact |
| .planning/config.json | Minor formatting fix |
| .planning/STATE.md | Updates planning state to reflect phase completion/progression |
| .planning/ROADMAP.md | Marks Phase 28/29 as complete and records plan breakdown |
| .planning/phases/28-code-hygiene-logging/28-01-PLAN.md | Adds Phase 28 plan doc |
| .planning/phases/28-code-hygiene-logging/28-01-SUMMARY.md | Adds Phase 28 summary doc |
| .planning/phases/28-code-hygiene-logging/28-02-PLAN.md | Adds Phase 28 plan doc |
| .planning/phases/28-code-hygiene-logging/28-02-SUMMARY.md | Adds Phase 28 summary doc |
| .planning/phases/28-code-hygiene-logging/28-03-PLAN.md | Adds Phase 28 plan doc |
| .planning/phases/28-code-hygiene-logging/28-03-SUMMARY.md | Adds Phase 28 summary doc |
| .planning/phases/28-code-hygiene-logging/28-04-PLAN.md | Adds Phase 28 plan doc |
| .planning/phases/28-code-hygiene-logging/28-04-SUMMARY.md | Adds Phase 28 summary doc |
| .planning/phases/28-code-hygiene-logging/28-VERIFICATION.md | Adds Phase 28 verification doc |
| .planning/phases/29-infrastructure-hardening/29-01-PLAN.md | Adds Phase 29 plan doc |
| .planning/phases/29-infrastructure-hardening/29-01-SUMMARY.md | Adds Phase 29 summary doc |
| .planning/phases/29-infrastructure-hardening/29-02-PLAN.md | Adds Phase 29 plan doc |
| .planning/phases/29-infrastructure-hardening/29-02-SUMMARY.md | Adds Phase 29 summary doc |
| .planning/phases/29-infrastructure-hardening/29-03-PLAN.md | Adds Phase 29 plan doc |
| .planning/phases/29-infrastructure-hardening/29-03-SUMMARY.md | Adds Phase 29 summary doc |
| .planning/phases/29-infrastructure-hardening/29-CONTEXT.md | Adds Phase 29 context doc |
| .planning/phases/29-infrastructure-hardening/29-DISCUSSION-LOG.md | Adds Phase 29 discussion log |
| .planning/phases/29-infrastructure-hardening/29-VERIFICATION.md | Adds Phase 29 verification doc |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (7)
apps/web/src/components/file-browser/FileBrowser.tsx (1)
450-452: Avoid duplicate download-failure logging at this layer.
downloadFromIpnsalready logs failures before rethrowing. Logging again here creates duplicate error events.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/file-browser/FileBrowser.tsx` around lines 450 - 452, The catch block in the FileBrowser component is duplicating error logs because downloadFromIpns already logs and rethrows failures; remove the logger.error call from the catch and simply rethrow the caught error (or `throw err`) so only downloadFromIpns handles logging; locate the catch that calls logger.error('Download failed:', err) within the FileBrowser component and delete that logging line, leaving the rethrow behavior intact.apps/web/src/hooks/useBin.ts (1)
59-61: Include the caught error object for non-blocking purge failures.Current logging drops the actual exception, which makes triage harder in staging/prod.
🔧 Proposed fix
- }).catch(() => { - logger.error('[useBin] Auto-purge failed (non-blocking)'); + }).catch((err) => { + logger.error('[useBin] Auto-purge failed (non-blocking):', err); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/hooks/useBin.ts` around lines 59 - 61, In useBin's auto-purge promise catch block (the .catch in apps/web/src/hooks/useBin.ts), stop swallowing the error and pass the caught error into logger.error so the exception details are recorded; change the arrow to accept an error parameter (e.g., .catch((err) => { logger.error('[useBin] Auto-purge failed (non-blocking)', err); })) or include it as metadata (e.g., logger.error('[useBin] Auto-purge failed (non-blocking)', { err })) so the actual exception is logged for triage.apps/web/src/components/file-browser/BinBrowser.tsx (1)
214-214: Consider including the caught error object in bin failure logs.These catch blocks currently log only static messages, which makes root-cause diagnosis harder in production incidents.
♻️ Suggested improvement
- } catch { - logger.error('[Bin] Restore failed'); + } catch (err) { + logger.error('[Bin] Restore failed:', err); }Apply the same pattern to the other updated catch blocks in this file.
Also applies to: 235-235, 263-263, 281-281, 301-301
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/file-browser/BinBrowser.tsx` at line 214, The catch blocks in BinBrowser (file BinBrowser.tsx) that currently call logger.error with static messages (e.g., the '[Bin] Restore failed' call and the other catches at the indicated spots) should be changed to include the caught error object and context (e.g., logger.error('[Bin] Restore failed', { error }) or include error.message / error.stack) so that the actual exception details are logged; update each catch in the component (the restore handler and the catch blocks around the other operations at the other indicated lines) to pass the caught error variable into logger.error along with the existing message.apps/web/src/services/bin.service.ts (2)
718-720: Consider emitting a metric for unpin failures, not only console warnings.These warnings improve local visibility, but they still won’t be centrally alertable without telemetry. Adding a lightweight counter (e.g.,
bin_unpin_failures_total) would make regressions detectable.Also applies to: 751-753, 831-833, 849-851
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/services/bin.service.ts` around lines 718 - 720, The unpinFromIpfs error handlers currently only call logger.warn; modify each catch for unpinFromIpfs (calls to unpinFromIpfs where errors are handled with logger.warn) to also emit/increment a telemetry metric (e.g., bin_unpin_failures_total) so failures are centrally observable; use the existing metrics/telemetry client in this module (or add a lightweight counter wrapper) and include useful labels such as cid and error type/message when incrementing so alerts and dashboards can filter by failure cause.
271-273: Include the caught error in non-blocking publish-failure logs.Line 272 and Line 337 currently emit static messages only, which drops root-cause context.
♻️ Proposed fix
- } catch { - logger.error('[Bin] addToBin publish failed (non-blocking)'); + } catch (err) { + logger.error('[Bin] addToBin publish failed (non-blocking):', err); } ... - } catch { - logger.error('[Bin] addManyToBin publish failed (non-blocking)'); + } catch (err) { + logger.error('[Bin] addManyToBin publish failed (non-blocking):', err); }Also applies to: 336-338
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/services/bin.service.ts` around lines 271 - 273, The catch blocks in addToBin (and the similar non-blocking publish catch later) swallow the exception because they use an empty catch; update those catch clauses to capture the error (e.g., catch (err)) and pass the error object into logger.error so the log includes the error stack/message along with the existing context string like "[Bin] addToBin publish failed (non-blocking)"; ensure you reference both catch sites in bin.service.ts so both logs include the caught error details.apps/api/src/ipns/ipns.controller.ts (1)
146-150: Add a dedicated unenroll metric for endpoint observability.
publish/resolvealready emit metrics; adding an unenroll counter would help capacity tracking and alerting for this new hardening path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api/src/ipns/ipns.controller.ts` around lines 146 - 150, Add a dedicated unenroll metric emission after the call to this.ipnsService.unenrollBatch: emit/increment a counter (e.g., "ipns_unenroll_total" or "unenroll_counter") with the number of unenrolled items (use result.totalUnenrolled) and any relevant labels (e.g., method="unenrollBatch" or tenant/user id) following the pattern used by existing publish/resolve metrics; if the controller doesn't yet have a metrics client, inject/obtain the same metrics service used elsewhere (e.g., metricsService or promClient wrapper) and call its increment/observe method after unenrollBatch returns before constructing the response.packages/sdk/src/client.ts (1)
986-1008: Minor: Folder subtree IPNS names not collected in permanentDelete.Unlike
deleteItemanddeleteToBinwhich callcollectSubtreeIpnsNames()for folders,permanentDeleteonly unenrolls the top-levelfolderEntry.ipnsName(line 1005). Nested folder/file IPNS names within the deleted folder aren't explicitly unenrolled.This is likely acceptable because:
- Bin entries don't store full nested children metadata needed for recursion
- Per existing learnings, orphaned IPNS records naturally expire from the DHT after 48 hours when the TEE republisher stops processing them
Consider adding a brief comment noting that nested IPNS names rely on natural DHT expiry, for clarity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sdk/src/client.ts` around lines 986 - 1008, permanentDeleteFromBin currently only collects the top-level folderEntry.ipnsName for unenrollment (see use of entry, folderEntry and fireAndForgetUnenroll) while deleteItem/deleteToBin use collectSubtreeIpnsNames; update the permanent delete flow by adding a concise code comment in the method handling permanent deletion (around the block that captures entry and calls binOps.permanentDeleteFromBin and fireAndForgetUnenroll) that explains nested folder/file IPNS names are not being collected here and instead rely on natural DHT expiry after the TEE republisher stops, referencing the existing behavior in collectSubtreeIpnsNames and the reason why recursive collection isn’t done for bin entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/src/ipns/ipns.service.ts`:
- Around line 302-304: The loop over republishService.unenrollIpns currently
increments the unenrolled counter unconditionally, overcounting when no rows
were deleted; update RepublishService.unenrollIpns to return an explicit result
(e.g., number of affected rows or a boolean) and change the callers in
ipns.service.ts (the call to republishService.unenrollIpns and the unenrolled
variable) to only increment unenrolled when the returned affected count > 0 (or
true), or alternatively rename the field (e.g., processedAttempts) everywhere if
you prefer to report attempts instead of actual deletions; ensure all other call
sites and the response payload are updated to use the new return value or field
name.
In `@apps/web/src/components/file-browser/FileBrowser.tsx`:
- Around line 549-550: The error log in FileBrowser.tsx currently emits
plaintext filenames via logger.error(`Download failed for ${file.name}:`, err);
— remove the plaintext file.name from logs and replace it with a non-sensitive
identifier (eg. file.id or a client-side encrypted/hashed name) or a redacted
placeholder; update the download error handling code path (where logger.error is
called) to compute/send the encrypted/hash token for the file name (perform
client-side encryption/hashing of file.name before any logging or transmission)
and log that token plus the error object instead of file.name.
In `@apps/web/src/lib/web3auth/hooks.ts`:
- Around line 159-162: The debug call in loginWithJWT currently logs a raw
persistent user identifier via logger.debug([...], { verifier:
loginParams.verifier, verifierId: userId }), which leaks PII; change this to
avoid logging the raw userId by either removing verifierId from the metadata or
replacing it with a non-reversible fingerprint (e.g., call a hashing/redaction
helper like hashIdentifier(userId) or redactIdentifier(userId) before passing it
to logger.debug). Update the call site in loginWithJWT to use the helper (or
omit verifierId) so logs no longer contain the plain userId while retaining
useful debug context (e.g., hashedVerifierId).
In `@docker/grafana/alerts/test-login-rate.json`:
- Line 26: The alert query is filtering cipherbox_auth_logins_total by
method="test" but the instrumented counter in
apps/api/src/metrics/metrics.service.ts only emits method="web3auth", and
noDataState is set to "OK" which masks missing data; either update the alert
expression to use method="web3auth" (modify the expr that references
cipherbox_auth_logins_total{method="test"}) or add instrumentation to emit
method="test" on test-login success paths in metrics.service.ts, and change
noDataState from "OK" to a non-masking state (e.g., "ALERT"/appropriate non-OK
value) so missing series surfaces as an operational alert.
In `@packages/sdk/src/client.ts`:
- Around line 1163-1165: Prettier is failing due to inline formatting of the
promise rejection handler; reformat the sdkCore.unpinFromIpfs call so .catch is
on its own line and the arrow callback uses proper parentheses/line breaks
(refer to sdkCore.unpinFromIpfs and relayResult.cid) — e.g., put .catch(...) on
a new line and break the arrow function body across lines so the file passes
Prettier/CI.
---
Nitpick comments:
In `@apps/api/src/ipns/ipns.controller.ts`:
- Around line 146-150: Add a dedicated unenroll metric emission after the call
to this.ipnsService.unenrollBatch: emit/increment a counter (e.g.,
"ipns_unenroll_total" or "unenroll_counter") with the number of unenrolled items
(use result.totalUnenrolled) and any relevant labels (e.g.,
method="unenrollBatch" or tenant/user id) following the pattern used by existing
publish/resolve metrics; if the controller doesn't yet have a metrics client,
inject/obtain the same metrics service used elsewhere (e.g., metricsService or
promClient wrapper) and call its increment/observe method after unenrollBatch
returns before constructing the response.
In `@apps/web/src/components/file-browser/BinBrowser.tsx`:
- Line 214: The catch blocks in BinBrowser (file BinBrowser.tsx) that currently
call logger.error with static messages (e.g., the '[Bin] Restore failed' call
and the other catches at the indicated spots) should be changed to include the
caught error object and context (e.g., logger.error('[Bin] Restore failed', {
error }) or include error.message / error.stack) so that the actual exception
details are logged; update each catch in the component (the restore handler and
the catch blocks around the other operations at the other indicated lines) to
pass the caught error variable into logger.error along with the existing
message.
In `@apps/web/src/components/file-browser/FileBrowser.tsx`:
- Around line 450-452: The catch block in the FileBrowser component is
duplicating error logs because downloadFromIpns already logs and rethrows
failures; remove the logger.error call from the catch and simply rethrow the
caught error (or `throw err`) so only downloadFromIpns handles logging; locate
the catch that calls logger.error('Download failed:', err) within the
FileBrowser component and delete that logging line, leaving the rethrow behavior
intact.
In `@apps/web/src/hooks/useBin.ts`:
- Around line 59-61: In useBin's auto-purge promise catch block (the .catch in
apps/web/src/hooks/useBin.ts), stop swallowing the error and pass the caught
error into logger.error so the exception details are recorded; change the arrow
to accept an error parameter (e.g., .catch((err) => { logger.error('[useBin]
Auto-purge failed (non-blocking)', err); })) or include it as metadata (e.g.,
logger.error('[useBin] Auto-purge failed (non-blocking)', { err })) so the
actual exception is logged for triage.
In `@apps/web/src/services/bin.service.ts`:
- Around line 718-720: The unpinFromIpfs error handlers currently only call
logger.warn; modify each catch for unpinFromIpfs (calls to unpinFromIpfs where
errors are handled with logger.warn) to also emit/increment a telemetry metric
(e.g., bin_unpin_failures_total) so failures are centrally observable; use the
existing metrics/telemetry client in this module (or add a lightweight counter
wrapper) and include useful labels such as cid and error type/message when
incrementing so alerts and dashboards can filter by failure cause.
- Around line 271-273: The catch blocks in addToBin (and the similar
non-blocking publish catch later) swallow the exception because they use an
empty catch; update those catch clauses to capture the error (e.g., catch (err))
and pass the error object into logger.error so the log includes the error
stack/message along with the existing context string like "[Bin] addToBin
publish failed (non-blocking)"; ensure you reference both catch sites in
bin.service.ts so both logs include the caught error details.
In `@packages/sdk/src/client.ts`:
- Around line 986-1008: permanentDeleteFromBin currently only collects the
top-level folderEntry.ipnsName for unenrollment (see use of entry, folderEntry
and fireAndForgetUnenroll) while deleteItem/deleteToBin use
collectSubtreeIpnsNames; update the permanent delete flow by adding a concise
code comment in the method handling permanent deletion (around the block that
captures entry and calls binOps.permanentDeleteFromBin and
fireAndForgetUnenroll) that explains nested folder/file IPNS names are not being
collected here and instead rely on natural DHT expiry after the TEE republisher
stops, referencing the existing behavior in collectSubtreeIpnsNames and the
reason why recursive collection isn’t done for bin entries.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a046a123-9655-4c87-86aa-046299a464f9
⛔ Files ignored due to path filters (6)
00-Preliminary-R&D/poc/yarn.lockis excluded by!**/yarn.lock,!**/*.lockpackages/api-client/openapi.jsonis excluded by!packages/api-client/**packages/api-client/src/generated/ipns/ipns.tsis excluded by!**/generated/**,!**/generated/**,!packages/api-client/**packages/api-client/src/models/batchUnenrollIpnsDto.tsis excluded by!packages/api-client/**packages/api-client/src/models/batchUnenrollIpnsResponseDto.tsis excluded by!packages/api-client/**packages/api-client/src/models/index.tsis excluded by!packages/api-client/**
📒 Files selected for processing (68)
.planning/ROADMAP.md.planning/STATE.md.planning/config.json.planning/phases/28-code-hygiene-logging/28-01-PLAN.md.planning/phases/28-code-hygiene-logging/28-01-SUMMARY.md.planning/phases/28-code-hygiene-logging/28-02-PLAN.md.planning/phases/28-code-hygiene-logging/28-02-SUMMARY.md.planning/phases/28-code-hygiene-logging/28-03-PLAN.md.planning/phases/28-code-hygiene-logging/28-03-SUMMARY.md.planning/phases/28-code-hygiene-logging/28-04-PLAN.md.planning/phases/28-code-hygiene-logging/28-04-SUMMARY.md.planning/phases/28-code-hygiene-logging/28-VERIFICATION.md.planning/phases/29-infrastructure-hardening/29-01-PLAN.md.planning/phases/29-infrastructure-hardening/29-01-SUMMARY.md.planning/phases/29-infrastructure-hardening/29-02-PLAN.md.planning/phases/29-infrastructure-hardening/29-02-SUMMARY.md.planning/phases/29-infrastructure-hardening/29-03-PLAN.md.planning/phases/29-infrastructure-hardening/29-03-SUMMARY.md.planning/phases/29-infrastructure-hardening/29-CONTEXT.md.planning/phases/29-infrastructure-hardening/29-DISCUSSION-LOG.md.planning/phases/29-infrastructure-hardening/29-VERIFICATION.md00-Preliminary-R&D/ARCHIVED.md00-Preliminary-R&D/poc/.env.example00-Preliminary-R&D/poc/README.md00-Preliminary-R&D/poc/package.json00-Preliminary-R&D/poc/scripts/gen-private-key.ts00-Preliminary-R&D/poc/src/index.ts00-Preliminary-R&D/poc/state/state.json00-Preliminary-R&D/poc/tsconfig.jsonapps/api/src/ipns/dto/index.tsapps/api/src/ipns/dto/unenroll.dto.tsapps/api/src/ipns/ipns.controller.tsapps/api/src/ipns/ipns.service.tsapps/web/src/components/file-browser/AudioPlayerDialog.tsxapps/web/src/components/file-browser/BinBrowser.tsxapps/web/src/components/file-browser/FileBrowser.tsxapps/web/src/components/file-browser/InviteLinkTab.tsxapps/web/src/components/file-browser/PdfPreviewDialog.tsxapps/web/src/components/file-browser/ReplaceFileDialog.tsxapps/web/src/components/file-browser/ShareDialog.tsxapps/web/src/components/settings/ConnectionTest.tsxapps/web/src/hooks/useAuth.tsapps/web/src/hooks/useBin.tsapps/web/src/hooks/useDropUpload.tsapps/web/src/hooks/useFileDownload.tsapps/web/src/hooks/useFileOperations.tsapps/web/src/hooks/useFileVersions.tsapps/web/src/hooks/useFolderNavigation.tsapps/web/src/hooks/useSearch.tsapps/web/src/hooks/useSharedNavigation.tsapps/web/src/lib/crypto/key-wrapping.tsapps/web/src/lib/logger.tsapps/web/src/lib/sw-registration.tsapps/web/src/lib/web3auth/core-kit-provider.tsxapps/web/src/lib/web3auth/hooks.tsapps/web/src/main.tsxapps/web/src/polyfills.tsapps/web/src/services/bin.service.tsapps/web/src/services/delete.service.tsapps/web/src/services/device-registry.service.tsapps/web/src/services/folder.service.tsapps/web/src/services/ipns.service.tsapps/web/src/services/search-index.service.tsapps/web/src/services/share.service.tsapps/web/src/services/upload.service.tsapps/web/src/stores/folder.store.tsdocker/grafana/alerts/test-login-rate.jsonpackages/sdk/src/client.ts
💤 Files with no reviewable changes (7)
- 00-Preliminary-R&D/poc/scripts/gen-private-key.ts
- 00-Preliminary-R&D/poc/tsconfig.json
- 00-Preliminary-R&D/poc/state/state.json
- 00-Preliminary-R&D/poc/package.json
- 00-Preliminary-R&D/poc/.env.example
- 00-Preliminary-R&D/poc/README.md
- 00-Preliminary-R&D/poc/src/index.ts
- Add 200-name chunking in fireAndForgetUnenroll (prevents silent 400 errors) - Extract collectRemovedItemIpnsNames and collectBinEntryIpnsNames helpers - Fix missing subtree collection in permanentDelete/emptyBin (was only unenrolling folder, not nested files) - Use accumulator pattern in collectSubtreeIpnsNames (avoids temp arrays) - Parallelize unenrollBatch with Promise.allSettled (was sequential await) - Reduce Grafana alert maxDataPoints from 43200 to 100 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 29 added unenrollBatch endpoint whose NestJS decorator branches are unreachable in unit tests (same pattern as other controllers). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove file.name from batch download error log (user metadata) - Remove userId/verifierId from CoreKit debug log (PII) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts from Phase 28 squash merge (#382): - Take main's logger module prefixes for all web app files - Keep Phase 29 TODO cleanup in folder.service.ts - Keep Phase 29 SDK unenrollment additions + prettier fix in client.ts - Re-apply privacy fix (remove file.name from batch download log) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #383 +/- ##
==========================================
+ Coverage 60.62% 70.56% +9.93%
==========================================
Files 128 107 -21
Lines 9526 6631 -2895
Branches 925 938 +13
==========================================
- Hits 5775 4679 -1096
+ Misses 3540 1738 -1802
- Partials 211 214 +3
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Prettier and lint-staged produce different trailing newline behavior for openapi.json, causing spurious CI failures on the generated client verification step. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…align MaxLength - Remove IPNS unenrollment from deleteToBin (soft delete preserves items for restore; unenroll only on permanentDelete/emptyBin) - Add @httpcode(200) to unenroll endpoint (NestJS @post defaults to 201) - Align MaxLength from 70 to 76 to match resolve DTO - Fix check-api-client.sh to skip already-up-to-date generated files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…iffs CI prettier and local prettier produce different array formatting (single-line vs multi-line for single-element arrays). Compare the parsed JSON instead of raw text to avoid false failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cover all paths: full success, partial failure, total failure, empty array. Brings ipns.service.ts patch coverage from 0% to covered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies the full unenroll pipeline: upload file → delete (triggers fireAndForgetUnenroll) → call unenroll endpoint directly to confirm it's reachable and idempotent. Exercises SDK client, API endpoint, and RepublishService in a real E2E context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nfig, cleaner test - Return affected count from unenrollIpns() so totalUnenrolled reflects actual DB deletions, not just resolved promises - Align maxDataPoints to 43200 in test-login-rate alert (matches other alerts) - Replace private field access in E2E test with dedicated axios instance - Tighten unenroll assertion to expect 0 (already cleaned up by deleteItem) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/api/src/ipns/ipns.service.spec.ts (1)
1317-1363: Add one edge-case test for fulfilled no-op unenroll (affected = 0).Current cases cover success/failure flow well, but not the fulfilled-zero path that drives
totalUnenrolledsemantics.✅ Suggested test addition
describe('unenrollBatch', () => { @@ it('should handle empty array', async () => { const result = await service.unenrollBatch('user-1', []); expect(result.totalUnenrolled).toBe(0); }); + + it('should not increment totalUnenrolled when unenroll returns 0', async () => { + const republishService = getRepublishMock(); + republishService.unenrollIpns.mockResolvedValueOnce(0); + + const result = await service.unenrollBatch('user-1', ['already-unenrolled']); + + expect(result.totalUnenrolled).toBe(0); + expect(republishService.unenrollIpns).toHaveBeenCalledTimes(1); + }); });As per coding guidelines:
**/*.test.ts: Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api/src/ipns/ipns.service.spec.ts` around lines 1317 - 1363, Add an edge-case unit test in the unenrollBatch suite to cover when republishService.unenrollIpns fulfills with affected = 0 (a no-op) so totalUnenrolled isn't incremented; mock (service as unknown as Record<string, unknown>).republishService.unenrollIpns to resolve to 0 for one or more names, call service.unenrollBatch('user-1', [...]) including that name, and assert result.totalUnenrolled reflects only successful (affected > 0) unenrolls and that unenrollIpns was invoked the expected number of times.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/check-api-client.sh`:
- Around line 35-42: The pre-commit check loop over GENERATED_FILES currently
skips files when `git diff --quiet "$file"` reports no unstaged changes, which
lets staged API source changes bypass required regenerated client updates;
remove the conditional that calls `git diff --quiet` (and its surrounding
comment/continue) so that the script always enforces that each file in
GENERATED_FILES is present in STAGED_FILES (i.e., always require the generated
client to be staged when corresponding sources are staged); update the loop
handling around MISSING_GENERATED, GENERATED_FILES, and STAGED_FILES accordingly
to ensure missing generated files are flagged regardless of unstaged diffs.
---
Nitpick comments:
In `@apps/api/src/ipns/ipns.service.spec.ts`:
- Around line 1317-1363: Add an edge-case unit test in the unenrollBatch suite
to cover when republishService.unenrollIpns fulfills with affected = 0 (a no-op)
so totalUnenrolled isn't incremented; mock (service as unknown as Record<string,
unknown>).republishService.unenrollIpns to resolve to 0 for one or more names,
call service.unenrollBatch('user-1', [...]) including that name, and assert
result.totalUnenrolled reflects only successful (affected > 0) unenrolls and
that unenrollIpns was invoked the expected number of times.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b016087a-5bef-4ed3-b7d9-e84c31eec7d0
📒 Files selected for processing (10)
.github/workflows/ci.ymlapps/api/src/ipns/dto/unenroll.dto.tsapps/api/src/ipns/ipns.controller.tsapps/api/src/ipns/ipns.service.spec.tsapps/api/src/ipns/ipns.service.tsapps/api/src/republish/republish.service.tsdocker/grafana/alerts/test-login-rate.jsonpackages/sdk/src/client.tsscripts/check-api-client.shtests/sdk-e2e/src/suites/ipns-consistency.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- docker/grafana/alerts/test-login-rate.json
- apps/api/src/ipns/ipns.controller.ts
- apps/api/src/ipns/dto/unenroll.dto.ts
- packages/sdk/src/client.ts
Summary
Phase 29: Infrastructure Hardening
Goal: Orphaned IPNS records are cleaned up on deletion, test login endpoint is hardened for staging
Status: Verified ✓
Added batch IPNS unenrollment API endpoint, wired automatic unenrollment into all SDK delete paths, and added Grafana monitoring for the test login endpoint on staging.
Note: This PR includes Phase 28 commits (structured logger, unpin fixes, any cast cleanup, POC archive) as its base. Phase 28 PR #382 should be merged first.
Changes
Plan 29-01: Batch IPNS Unenroll API Endpoint
Created
POST /ipns/unenrollonIpnsControllerwithBatchUnenrollIpnsDto(validates array of IPNS names, max 200).IpnsService.unenrollBatch()delegates to existingRepublishService.unenrollIpns(). API client regenerated to exposeipnsControllerUnenrollBatch.Key files:
apps/api/src/ipns/dto/unenroll.dto.ts(new)apps/api/src/ipns/ipns.controller.ts(new route)apps/api/src/ipns/ipns.service.ts(new method)packages/api-client/src/generated/(regenerated)Plan 29-02: SDK IPNS Unenrollment on Delete
Added
fireAndForgetUnenroll()andcollectSubtreeIpnsNames()toCipherBoxClient. Wired into 4 deletion methods:deleteItem,deleteToBin,permanentDelete,emptyBin. Cleaned up 3 legacy TODO comments infolder.service.tsanddelete.service.ts.Key files:
packages/sdk/src/client.ts(fireAndForgetUnenroll, collectSubtreeIpnsNames)apps/web/src/services/folder.service.ts(TODO cleanup)apps/web/src/services/delete.service.ts(TODO cleanup)Plan 29-03: Test Login Monitoring & Kubo Verification
Created Grafana alert rule (
test-login-rate.json) firing whencipherbox_auth_logins_total{method="test"}exceeds 100/hour on staging. Verified existing production guard (NODE_ENV check + timingSafeEqual) and Kubo 127.0.0.1 binding.Key files:
docker/grafana/alerts/test-login-rate.json(new)Verification
POST /auth/test-loginunreachable when NODE_ENV=production (existing unit tests verify)Key Decisions
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Documentation / Planning