feat: GET /idp/account/export — self-service pod data download (#353) - #449
Merged
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GET /idp/account/export— authenticated owner downloads their pod tree as a streamed tar.gz. The L0-3 backup MVP from the Credible Exit ladder (#448), and the third leg of the user-rights trio (#351 password change, #352 account delete, this).Archive shape
In single-user root pod mode (
podDir = dataRoot),ROOT_POD_EXCLUDEkeeps server-internal directories out of the archive:.idp/(IDP accounts incl. passwordHash for every user, IDP signing keys, OIDC adapter state) and.private/(pay handler's Bitcoin keypair + UTXO state — drainable). Adding a new server-managed dotfile dir at the data root WITHOUT updating this set is a security bug; the regression test pins the property "no IdP secrets in the export" against on-disk seeded files.Authorization
findByWebId); else 403./private/privkey.jsonld.Design call: include
/private/privkey.jsonld?Yes. Per the Credible Exit framing in #448, the user's secret IS theirs and must leave with them. Refusing to export it would make L4+ identity migration impossible. The endpoint is owner-authenticated; the secret never leaves the WAC perimeter to anyone but the owner.
Streaming
tar.pack → zlib.createGzip → reply. Memory stays constant regardless of pod size — a multi-GB pod doesn't OOM. Stream-error handler destroys the pipeline on mid-pack failures (so clients see an aborted transfer rather than a silently-truncated archive). Client-disconnect handler onreply.raw.on('close')short-circuits the walk so we don't keep reading every file in a multi-GB pod into a dead socket.Rate limiting
3/min, keyed by source IP, consistent with the other
/idp/endpoints. Per-WebID keying isn't possible in the current code path: the global auth hook insrc/server.jsskips/idp/*(sorequest.webIdis unset at the rate-limit phase) and@fastify/rate-limit'skeyGeneratoris sync, so we can't await token verification inline. Per-user keying is a follow-up that needs apreParsinghook resolving auth before the limiter runs. Operators behind a NAT: the limit is shared across all users on the source IP.Tests
test/idp-export.test.js— 6/6 passing locally, full suite green.401unauthenticated200for the authenticated owner — valid tar.gz with manifest + account + pod treeaccount.jsonallowlist (nopasswordHash).idp/accounts/*.jsonand.private/keypair.jsonsynthesized on disk pre-export, asserted absent from archive--provision-keys: archive contains the on-disk/private/privkey.jsonld(Credible Exit), manifest carriesusername/email/createdAtparity with multi-userattacker.example.comWebID gets 403, not the operator's podprocess.env.DATA_ROOTis snapshotted/restored instartServer/stopServerto prevent cross-test leakage to subsequent test files in the same run.New dep
tar-stream@^3.2.0. Pure JS, no native bindings. Functional install footprint on Node is justtar-stream(~60K) — but the npm install also dropsstreamx,b4a, and thebare-*family (bare-fs,bare-os,bare-path,bare-stream,bare-url,bare-events) totaling ~4.4 MB on disk. Thosebare-*packages exist for the Bare runtime and are lazy-loaded only when running outside Node — on Node they sit on disk but never load, contributing zero runtime weight. Verified install + tests run cleanly on the Termux/mobile target.The disk footprint matters more for our mobile/embedded targets than I'd want — tracked as #450 (replace
tar-streamwith a leaner producer, e.g. ~200 lines of hand-rolled POSIX ustar). Out of scope here; ship the working endpoint, optimize the dep tree as a focused follow-up.Out of scope (per #353)
/idp/account(endpoint first, UI follow-up)Closes #353. First slice of #448.