Repository navigation
feat: encryption at rest β encrypted backups + encrypted-volume deploy posture (#67) #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
b77934b
docs(plan): strengthen encryption-at-rest plan from doc review
unclesp1d3r c87748b
docs(operations): encrypted-volume how-to + threat matrix (U9)
unclesp1d3r 9b0ffa0
feat(backup): add Argon2id + secretstream crypto module (U1)
unclesp1d3r a86babd
feat(backup): NDJSON DB export/import + operator_audit table (U3)
unclesp1d3r aada2f3
feat(deploy): docker secrets + hardening + encrypted-volume-ready mouβ¦
unclesp1d3r 94052bb
feat(backup): streaming tar bundle format + zip-slip guard (U2)
unclesp1d3r 9f593c2
feat(backup): backup export service (U4)
unclesp1d3r 839b5a8
feat(backup): stage-then-promote restore service + maintenance (U5)
unclesp1d3r 655e16f
feat(backup): admin backup API routes + operator-event logging (U6)
unclesp1d3r 26cfce2
feat(backup): admin backup UI (U7)
unclesp1d3r 10272fa
refactor(backup): apply simplify + code-review fixes
unclesp1d3r 6aa5333
feat(backup): same-origin CSRF check + minimum backup-password length
unclesp1d3r 8591cd8
feat(backup): snapshot-isolate export + cap NDJSON import line length
unclesp1d3r 9caec0a
feat(backup): harden restore core β unique schemas, crash recovery, mβ¦
unclesp1d3r 19fc7a9
feat(backup): enforce maintenance mode on the write path
unclesp1d3r 32c4922
fix(backup): validate untrusted NDJSON rows + behavioral snapshot-isoβ¦
unclesp1d3r 5b399da
fix(backup): validate KDF alg + copy salt on ingest + relocate cryptoβ¦
unclesp1d3r 6104cbc
fix(backup): unify restore promote envelope + surface rollback failurβ¦
unclesp1d3r 59ff896
fix(backup): audit/stream error handling + CSRF fallback tests + rendβ¦
unclesp1d3r 29b9a7a
chore(backup): run root instrumentation test in CI + keep outcome mesβ¦
unclesp1d3r 17d8cfe
fix(deploy): idempotent + owner-only secret handling, URL-safe DB pasβ¦
unclesp1d3r 52242da
fix(backup): remove dead maintenance helper + correct export doc + teβ¦
unclesp1d3r 455c43f
fix(backup-ui): prevent duplicate concurrent export + restore-panel rβ¦
unclesp1d3r 2dddd2b
feat(docker): add UPLOAD_DIR environment variable for uploads
unclesp1d3r 49ea0c7
feat(docker): make UPLOAD_DIR configurable and format healthcheck comβ¦
unclesp1d3r a46580e
fix(backup-ui): support non-Latin-1 restore passwords via percent-encβ¦
unclesp1d3r File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |
| .env | ||
| .env.* | ||
| !.env.example | ||
| secrets/* | ||
| !secrets/README.md | ||
|
|
||
| node_modules | ||
| .next | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| import { | ||
| afterAll, | ||
| afterEach, | ||
| beforeEach, | ||
| describe, | ||
| expect, | ||
| mock, | ||
| spyOn, | ||
| test, | ||
| } from "bun:test"; | ||
|
|
||
| /** | ||
| * Focused unit tests for `register()` β `instrumentation.ts`'s Next.js | ||
| * server-startup hook. Covers the three real branches called out in that | ||
| * file's own doc comment: | ||
| * 1. `NEXT_RUNTIME !== "nodejs"` β early return, the recovery sweep never runs. | ||
| * 2. `DATABASE_URL` unset β early return, the recovery sweep never runs. | ||
| * 3. The try/catch around `recoverInterruptedRestore` β a recovery failure | ||
| * must be caught and logged, never rethrown (a boot-recovery failure | ||
| * must not crash the server). | ||
| * | ||
| * **Deliberately lives OUTSIDE `src/`** (not `src/__tests__/`) and is run as | ||
| * its own invocation (`bun test __tests__/instrumentation.test.ts`), never | ||
| * bundled into `bun run test`'s `bun test src` / `just ci-check`. Verified | ||
| * empirically against this repo's actual Bun version (1.3.14): `mock.module()` | ||
| * replaces a module specifier for the rest of the **process**, not just this | ||
| * file, and β critically β if any OTHER file anywhere in the same `bun test` | ||
| * invocation has a static `import` of that same module, the real module gets | ||
| * linked into the cache before this file's `mock.module()` call ever runs | ||
| * (regardless of file ordering), which either silently no-ops the mock here | ||
| * or (if this file's mock registers first) corrupts the real module for | ||
| * every other file that statically imports it β reproduced directly: a | ||
| * `mock.module("@/src/backup/maintenance", () => ({ POISONED: true }))` in a | ||
| * file that sorts before `src/backup/__tests__/maintenance.test.ts` made that | ||
| * file fail at load time with `SyntaxError: Export named 'isMaintenanceActive' | ||
| * not found`. `src/backup/__tests__/maintenance.test.ts` and | ||
| * `src/backup/__tests__/write-path-maintenance-guard.test.ts` both statically | ||
| * import the REAL `@/src/backup/maintenance`/`@/src/db/client`, so mocking | ||
| * those specifiers here would be unsafe inside `bun test src`. This file's | ||
| * own `afterAll` "restore" (re-registering `mock.module` with the real | ||
| * exports) does NOT fix this either β restoring a `mock.module()` override | ||
| * does not retroactively repair an already-linked static import in another | ||
| * file, the same constraint `src/backup/__tests__/routes.test.ts` documents | ||
| * for `@/src/db/client`. Living outside `src/` sidesteps the whole class of | ||
| * problem: this file never shares a `bun test` process with those tests. | ||
| */ | ||
|
|
||
| const ORIGINAL_NEXT_RUNTIME = process.env.NEXT_RUNTIME; | ||
| const ORIGINAL_DATABASE_URL = process.env.DATABASE_URL; | ||
|
|
||
| let recoverCalls = 0; | ||
| let recoverShouldThrow: unknown = null; | ||
|
|
||
| mock.module("@/src/db/client", () => ({ | ||
| db: { fake: "db-handle" }, | ||
| })); | ||
| mock.module("@/src/storage", () => ({ | ||
| activeStorageRoot: () => "/fake/storage/root", | ||
| })); | ||
| mock.module("@/src/backup/maintenance", () => ({ | ||
| recoverInterruptedRestore: async () => { | ||
| recoverCalls += 1; | ||
| if (recoverShouldThrow) throw recoverShouldThrow; | ||
| }, | ||
| })); | ||
|
|
||
| // instrumentation.ts has no top-level imports of its own β every dependency | ||
| // is dynamically imported inside `register()` at call time (see its doc | ||
| // comment) β so it's safe to statically import `register` here regardless | ||
| // of ordering relative to the `mock.module()` calls above. | ||
| const { register } = await import("../instrumentation"); | ||
|
|
||
| function restoreEnv(): void { | ||
| if (ORIGINAL_NEXT_RUNTIME === undefined) { | ||
| delete process.env.NEXT_RUNTIME; | ||
| } else { | ||
| process.env.NEXT_RUNTIME = ORIGINAL_NEXT_RUNTIME; | ||
| } | ||
| if (ORIGINAL_DATABASE_URL === undefined) { | ||
| delete process.env.DATABASE_URL; | ||
| } else { | ||
| process.env.DATABASE_URL = ORIGINAL_DATABASE_URL; | ||
| } | ||
| } | ||
|
|
||
| describe("instrumentation.register()", () => { | ||
| beforeEach(() => { | ||
| recoverCalls = 0; | ||
| recoverShouldThrow = null; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| restoreEnv(); | ||
| }); | ||
|
|
||
| afterAll(() => { | ||
| restoreEnv(); | ||
| }); | ||
|
|
||
| test('resolves without running the recovery sweep when NEXT_RUNTIME is not "nodejs"', async () => { | ||
| delete process.env.NEXT_RUNTIME; | ||
| process.env.DATABASE_URL = "postgres://ignored/ignored"; | ||
|
|
||
| await expect(register()).resolves.toBeUndefined(); | ||
| expect(recoverCalls).toBe(0); | ||
| }); | ||
|
|
||
| test("resolves without running the recovery sweep when DATABASE_URL is unset", async () => { | ||
| process.env.NEXT_RUNTIME = "nodejs"; | ||
| delete process.env.DATABASE_URL; | ||
|
|
||
| await expect(register()).resolves.toBeUndefined(); | ||
| expect(recoverCalls).toBe(0); | ||
| }); | ||
|
|
||
| test("runs the recovery sweep exactly once when both NEXT_RUNTIME and DATABASE_URL are set", async () => { | ||
| process.env.NEXT_RUNTIME = "nodejs"; | ||
| process.env.DATABASE_URL = "postgres://ignored/ignored"; | ||
|
|
||
| await expect(register()).resolves.toBeUndefined(); | ||
| expect(recoverCalls).toBe(1); | ||
| }); | ||
|
|
||
| test("swallows a recoverInterruptedRestore failure β register() never rethrows (a boot-recovery failure must not crash the server)", async () => { | ||
| process.env.NEXT_RUNTIME = "nodejs"; | ||
| process.env.DATABASE_URL = "postgres://ignored/ignored"; | ||
| recoverShouldThrow = new Error("simulated recovery failure"); | ||
|
|
||
| const errorSpy = spyOn(console, "error").mockImplementation(() => {}); | ||
| try { | ||
| await expect(register()).resolves.toBeUndefined(); | ||
| expect(recoverCalls).toBe(1); | ||
| expect(errorSpy).toHaveBeenCalledTimes(1); | ||
| expect(errorSpy.mock.calls[0]?.[0]).toContain( | ||
| "crash-recovery sweep failed", | ||
| ); | ||
| } finally { | ||
| errorSpy.mockRestore(); | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| "use client"; | ||
|
|
||
| import { ExportPanel } from "./export-panel"; | ||
| import { RestorePanel } from "./restore-panel"; | ||
|
|
||
| /** | ||
| * Admin backup screen (plan Unit U7). Composes the export and restore panels | ||
| * side by side on wide viewports, stacked on narrow ones β mirrors the | ||
| * `/users` admin surface's create-form + table layout. | ||
| */ | ||
| export function BackupPanel() { | ||
| return ( | ||
| <div className="grid gap-6 lg:grid-cols-2"> | ||
| <ExportPanel /> | ||
| <RestorePanel /> | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.