feat(cryptify): merge cryptify into the workspace - #277
Merged
Conversation
Remove frontend, add dev API UI, and clean up project
Add linux/arm64 Docker image builds
Remove frontend, improve backend reliability
Replace single QEMU multi-platform job with parallel native runner jobs that merge into a multi-arch manifest. Add cargo-chef to Dockerfile for dependency layer caching. Add release/edge Cargo profiles (lto=true for published releases, lto=false for all other builds).
anchore/scan-action@v3 was pinning grype v0.74.4 (Nov 2023). v7.3.2 brings grype v0.104+ with an up-to-date vulnerability database. github/codeql-action/upload-sarif@v3 was showing a deprecation warning and will be removed in December 2026; upgrade to v4. Both actions are now pinned to their commit SHA per security best practice.
Bumps [keccak](https://github.com/RustCrypto/sponges) from 0.1.5 to 0.1.6. - [Commits](RustCrypto/sponges@keccak-v0.1.5...keccak-v0.1.6) --- updated-dependencies: - dependency-name: keccak dependency-version: 0.1.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.1 to 1.11.1. - [Release notes](https://github.com/tokio-rs/bytes/releases) - [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md) - [Commits](tokio-rs/bytes@v1.10.1...v1.11.1) --- updated-dependencies: - dependency-name: bytes dependency-version: 1.11.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [time](https://github.com/time-rs/time) from 0.3.44 to 0.3.47. - [Release notes](https://github.com/time-rs/time/releases) - [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) - [Commits](time-rs/time@v0.3.44...v0.3.47) --- updated-dependencies: - dependency-name: time dependency-version: 0.3.47 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
…keccak-0.1.6 chore(deps): bump keccak from 0.1.5 to 0.1.6 in /cryptify
…bytes-1.11.1 chore(deps): bump bytes from 1.10.1 to 1.11.1 in /cryptify
…time-0.3.47 chore(deps): bump time from 0.3.44 to 0.3.47 in /cryptify
Many email clients strip out SVGs for security and anti-tracking reasons, causing the PostGuard logo to not render for a large portion of recipients. Replace the inline SVG logo with a hosted PNG image from postguard.eu. Closes encryption4all/postguard-website#29
…site fix: replace SVGs with accessible alternatives in email template
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.8 to 0.103.10. - [Release notes](https://github.com/rustls/webpki/releases) - [Commits](rustls/webpki@v/0.103.8...v/0.103.10) --- updated-dependencies: - dependency-name: rustls-webpki dependency-version: 0.103.10 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
…rustls-webpki-0.103.10 chore(deps): bump rustls-webpki from 0.103.8 to 0.103.10 in /cryptify
…als-separate-fields feat: accept smtp_username and smtp_password as separate config fields
* feat(email): replace unicode checkmark with inline PNG image The signer-verified circle previously rendered a unicode `✓` glyph which is styled inconsistently across mail clients. Embed a PNG of the PostGuard checkmark via `cid:pg-check` to match the shape used in the download page UI. * feat(email): remove circle around signer-verified checkmark
Parse the X-POSTGUARD-CLIENT-VERSION header (host,host_version,app,app_version,
reused from pg-pkg) on uploads. Log the full raw value at init and finalize so
exact client versions are greppable, and expose a cardinality-safe
cryptify_uploads_by_app_total{app} counter (app field only; full version stays
in logs). Add X-POSTGUARD-CLIENT-VERSION to the CORS allowlist so browser
pg-js can send it.
Replace the production PKG URL https://postguard-main.cs.ru.nl/pkg with https://pkg.postguard.eu/ in conf/config.toml and the commented example in conf/config.dev.toml. Since the new URL carries a trailing slash, harden the startup parameter fetch in rocket() to trim a trailing slash before appending /v2/sign/parameters (mirroring the existing trim in PkgClient::validate_api_key), avoiding a // in the request path. Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Refreshes transitive deps to latest compatible versions including aws-lc-rs, bincode-next, rustls, tower-http, wasm-bindgen and others. Closes #167 Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com>
…t:3000) and DELETE method (#179) * fix(cors): allow Office add-in origins + DELETE method Browser callers from the Office add-in were blocked by CORS when reaching /fileupload/* and /filedownload/*: the preflight returned no Access-Control-Allow-Origin because the production allowed_origins regex only matched the postguard.eu/nl website. - conf/config.toml: extend allowed_origins to also match https://addin.postguard.eu (Outlook prod) and https://localhost:3000 (Office add-in dev), keeping the existing postguard.(eu|nl) origins. - build_rocket CORS: add DELETE to allowed_methods so the preflight advertises GET, POST, PUT, DELETE. Content-Type and Authorization are already in the allowed-headers list. - Add integration tests over the real build_rocket CORS fairing asserting the preflight succeeds (echoes Allow-Origin, advertises all four methods and the required headers) for both add-in origins, and is rejected for an unlisted origin. Refs #154 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(test): correct misleading PROD_ALLOWED_ORIGINS comment The comment claimed a config typo would fail the suite, but the CORS tests use this hand-maintained copy and never read conf/config.toml. Reword to state it is a copy that must be kept in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Resolve dependency CVEs and refresh outdated crates. Security fixes: - lettre 0.11.19 -> 0.11.22 (RUSTSEC-2026-0141: inverted-boolean bug disabling TLS hostname verification on the boring-tls backend) - rand 0.10.0 -> 0.10.1 (RUSTSEC-2026-0097: aliased mutable reference / UB when a custom log logger calls rand::rng() during reseeding) minor/patch bumps: - tokio 1.48.0 -> 1.52.3 - uuid 1.18.1 -> 1.23.4 - reqwest 0.13.0 -> 0.13.4 - serde_json 1.0.145 -> 1.0.150 - chrono 0.4.42 -> 0.4.45 - log 0.4.28 -> 0.4.33 - url 2.5.7 -> 2.5.8 - tokio-util 0.7.17 -> 0.7.18 major bump: - minreq 2.14.1 -> 3.0.0: renamed the https-native feature to https-native-tls (3.0 feature rename); call site in rocket() uses only get/with_timeout/send/json which are unchanged. cargo audit reports no vulnerabilities. cargo fmt/clippy/test all pass (127 tests). Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
) The `/usage` route used the `ApiKey` guard, whose `FromRequest` always succeeds (yielding the anonymous default tier when no key is present). The handler's default-tier branch then looked up usage by the caller-supplied `email` query parameter with no ownership check, so any unauthenticated caller could probe usage for an arbitrary address. Introduce a `ValidatedApiKey` request guard that fails the request (401 on NoCredentials/Rejected, 503 on PkgUnreachable) so the "authenticated" intent is enforced by the type system. Switch `/usage` to that guard and key the lookup on the validated tenant; the `email` query param is now optional and only echoed back. Also make the `cryptify_token` comparisons constant-time via `subtle::ConstantTimeEq` (new `cryptify_tokens_match` helper), mirroring `recovery_tokens_match`, in `check_cryptify_token`, `upload_chunk`, and `classify_chunk_request`. Adds a regression test asserting unauthenticated `/usage` returns 401, and updates the OpenAPI description. Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add an endpoint that returns the email template pg-pkg has linked to the caller's API key (issue #54, requirement 10 of postguard#81). The key is validated through the existing `ApiKey` request guard, so the same `Authorization: Bearer PG-…` flow the upload endpoints use applies here. - Extend the pg-pkg `/v2/api-key/validate` response and `ApiKey` guard to carry an optional `email_template`. - `GET /email-template` returns 200 + template for a valid key, 401 for a missing/invalid key, 404 when the key is valid but has no template, and 503 when pg-pkg is unreachable during validation. - Branch mapping extracted into a pure `resolve_email_template` helper for unit testing; full route also covered end-to-end through the real guard against a mock pg-pkg. - Document the endpoint and a bearer security scheme in api-description.yaml. Closes #54 Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…185) The InternalServerError(Some(..)) and ServiceUnavailable(Some(..)) call sites in src/main.rs returned internal diagnostic strings ("could not open file", "couldn't read postguard file", "pg-pkg was unreachable while validating the API key", etc.) directly to HTTP clients, leaking implementation detail (GHSA-r95f-qf3j-xccw, low severity). Each site now returns the generic constant GENERIC_INTERNAL_ERROR_MSG ("an internal error occurred") and writes the detailed diagnostic to a server-side log::error!() call, so operators keep observability without exposing internals. Where the error closures previously discarded the underlying error (|_|), they now capture and log it, improving diagnosability over the prior behaviour. Adds integration test finalize_internal_error_body_is_generic asserting a 500 body is the generic message and contains none of the old leaky detail. Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The config schema already accepts `usage_db = "<path>"`, but `CryptifyConfig` never parsed it and the rolling-quota state lived only in the in-memory `Store.shared.state.usage` map, so any pod restart or redeploy wiped everyone's rolling quota. - Parse `usage_db: Option<String>` in `RawCryptifyConfig` / `CryptifyConfig` with a `usage_db()` accessor. - Back the usage map with SQLite (`rusqlite`, bundled) at that path. The DB is the source of truth; the in-memory map is a cache. On startup existing usage is loaded into the cache; every accounted upload is written through and stale rows outside the rolling window are pruned. - `usage_db` unset → in-memory only, preserving prior behaviour. A configured-but-unopenable DB panics at startup (loud config error). - Tests: persistence across a simulated restart, continued accumulation after restart, rolling-window eviction persisted to the DB, lazy in-memory eviction after reload, and config parsing. Closes #134 Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…email (#187) * fix(email): make recipient download link a prominent selectable code block The notification email's download-link block ({{link_str}}) rendered at 13px — smaller than the 16px primary button above it — and used a low weight/contrast style that was hard to read and select. July 2026 user test (postguard-website#296) flagged it as small and hard to copy. Restyle the {{link_str}} URL as a full-width monospace code block: - font-size 16px (no smaller than the primary button) - monospace family, higher contrast (dark #030E17 text) - more padding, subtle background + border so it reads as a code block - display:block + word-break for easy full selection No inline copy buttons (unreliable across email clients). Plain-text template already renders the label + URL on their own lines and stays consistent. Adds a render assertion guarding the >=16px monospace style. Closes #186 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(email): pin download-link assertion to restyled <a> The prior font-size:16px substring check was satisfied by the primary download button regardless, so it failed to guard the link block. Assert a contiguous substring unique to the restyled <a> (display:block + monospace + font-size:16px) so a font-size regression genuinely fails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add an optional `metrics_token` config (also settable via ROCKET_METRICS_TOKEN). When set, /metrics requires `Authorization: Bearer <token>` (constant-time compared via subtle); when unset it stays open and logs a startup warning, so rolling this out doesn't break existing scrapers. The auth lives in the app, not the ingress, so it travels with cryptify to external hosts that can't be firewalled. /health stays public. Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com>
The website tags its uploads with X-Cryptify-Source for per-channel metrics (postguard-website#228), but the header was never added to the CORS allow-list. Browsers include it in the preflight's Access-Control-Request-Headers, rocket_cors rejects the preflight with a 403 carrying no Access-Control-Allow-Origin, and uploads from the website fail before they start. Staging hits this directly; production only works because its nginx answers preflights itself. Add the header to the allow-list, and extract build_cors() so the preflight smoke tests exercise the production CORS config instead of a test-local copy — the duplicated config is why the existing smoke test couldn't catch this regression.
* docs: add agent & contributor notes (migrated from dobby memory) * docs: correct purge timer notes in CLAUDE.md The Purge timer block described a hard 15-minute eviction from creation at src/store.rs:80. It is actually a resettable 1-hour idle timeout: Store::create (src/store.rs:292) seeds the deadline from idle_ttl (DEFAULT_UPLOAD_SESSION_IDLE_TIMEOUT_SECS = 60 * 60) and Store::touch re-inserts it on each chunk PUT / status check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: retry the startup verifying-key fetch instead of panicking Cryptify fetched the IBS verifying key from pg-pkg once at startup and panicked if the PKG was unreachable or the response unparsable, creating a hard restart-order dependency (cryptify -> pg-pkg -> irma server): a brief PKG unavailability window during a rolling deploy took cryptify down with it. The fetch now retries transient failures with exponential backoff (500ms..10s) for a 120s budget, mirroring the existing /api-key/validate retry pattern, and runs the blocking minreq call via spawn_blocking. If the budget is exhausted the process still exits with a clear error so a misconfigured pkg_url does not fail silently. Fixes #235 * fix: log non-2xx PKG responses distinctly from parse failures in the startup retry
…#193) Part 2 of #236 (the cryptify leg). Finalize required exactly pbdf.sidn-pbdf.email.email in the sender's signing identity; test environments cannot issue pbdf credentials, blocking real end-to-end flows. The `email_attribute` config key (default: the production value, so deployed configs are unaffected) now names the attribute type carrying the sender's email. Companion changes: pg-pkg (#244) and pg-js.
These three dependencies were declared in Cargo.toml but never used anywhere in the crate. Removing them prunes qrcode's image subtree (image, moxcms, pxfm, byteorder-lite) and strum's heck from Cargo.lock. Closes #191 Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore: remove unused irma dependency The `irma` crate was declared in Cargo.toml but never imported anywhere in the crate. Attributes reach cryptify already signed inside the PostGuard-sealed file and are read back through pg-core's Unsealer, so there is no IRMA client here to migrate. `irma` stays in Cargo.lock as a transitive dependency of pg-core. Closes #194 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say irma is still compiled in via pg-core, not just in the lockfile `cargo tree -i irma` shows irma 0.2.1 on the normal dependency edge under pg-core, so it is built into the binary, not lockfile residue. It also brings reqwest 0.11.27 alongside cryptify's own reqwest 0.13.4. The old wording could lead the postguard-e2e knownDrift follow-up to record the pair as resolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* docs: true up api-description.yaml with the mounted routes
The spec had drifted from the service in ways a client reading it would hit:
- `/staging/preview/{uuid}` was mounted but undocumented.
- `PUT /fileupload/{uuid}` and `POST /fileupload/finalize/{uuid}` documented a
409 for a `cryptifytoken` mismatch; both return 400, and no route in the
service returns 409.
- Finalize validates a `cryptifytoken` header, which the spec did not list, so
a client following the spec got a 400 from the extractor.
- 400 bodies are plain-text messages, not `{"message": …}` JSON.
- `/metrics` requires `Authorization: Bearer <metrics_token>` when the
deployment configures one; the spec said firewall-only and listed no 401.
- `/filedownload/{uuid}` answers `Range` requests with 206 or 416 and never
returns the documented 400, and it sets no `Content-Type` at all.
- Missing responses: 400/422/500 on init, 500/503 on chunk PUT, 500 on
finalize, 503 on `/usage`.
- `PayloadTooLarge` gained `resets_at` on the rolling-window 413.
`api_routes()` is now the single mount list, and `mod api_description_tests`
compares it against the spec so a route can no longer be added, removed, or
renamed without the spec following.
Refs #247
* docs: correct four spec statements and document the exclusive Content-Range end
Follow-up on the review of this PR. All five were verified against the
handlers, not inferred from the spec:
- `PUT /fileupload/{uuid}`: the `Content-Range` end byte is exclusive
(`upload_chunk` uses `end - start`), which no version of the spec has ever
said. An external client following RFC 7233 sends one byte too many and gets
a 400. Documented on the parameter, since the spec is the contract pg-js and
pg-dotnet are written against.
- `GET /filedownload/{uuid}`: `Accept-Ranges: bytes` is set on the response
builder after both 404 early returns, so it is not "always" advertised.
Reworded, and added the header to the 416, which does carry it.
- `GET /filedownload/{uuid}`: added the 500 from a failed `file.seek` on the
range path.
- `POST /fileupload/init`: the 400 also covers a syntactically invalid JSON
body, which Rocket answers with its default error page rather than the
plain-text message. Same caveat the chunk and finalize 400s already carry.
- `StagingPreview.confirmation`: null whenever `state.sender` is unset, i.e.
when previewing before finalize — the normal staging flow, and the case the
old wording excluded.
cargo fmt/clippy/test all pass (154 tests); the spec still validates as
OpenAPI 3.0.3 with all $refs resolving.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b2a0cda35e4646797ab2e3e3bc952685236647fb)
#203) * ci: pin the oasdiff gate's settings with a mutation test `fail-on: ERR` with no `include-checks`, the setting the gate landed with in #201, fails open on ten of the changes the spec's contract forbids. Measured against oasdiff v1.26.1 by mutating the real spec one way per rule: a `401` that becomes a `403` and a dropped response enum value rate ERR but are opt-in, and a removed or renamed optional response property, a removed request parameter, a removed request property and the constraint-narrowing `*-set` family all rate WARN. Cryptify's routes are unversioned, so each of those reaches every pinned client the moment it deploys. Adds `mod api_gate_tests` in `src/main.rs`, which runs the engine with the flags the action's entrypoint builds and asserts stop-or-pass per mutation. It fails on the shipped settings and names every gap; it passes on `fail-on: WARN` plus the two opt-in checks. The engine is not on any runner, so it skips in CI while `every_api_gate_mutation_still_applies` still runs there. The workflow itself is in a PR comment: the App has no `workflows: write`. Refs #202. Part of #247 (workstream C). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: assert the workflow runs the settings the gate test pins Review on #203: `FAIL_ON` and `INCLUDE_CHECKS` described settings nothing tied to the committed workflow, which still says `fail-on: ERR` with no `include-checks`, and `cargo test` was green anyway. So the branch shipped a passing test certifying a configuration the gate does not use, which is the fail-open the module exists to catch. `the_workflow_uses_the_settings_this_module_pins` reads `.github/workflows/api-diff.yml` and asserts its two step inputs are the two constants. It needs no `oasdiff`, so it runs on every runner. It is red until a maintainer applies the workflow patch from the PR comment, which makes the merge order enforced rather than documented. Also from the review: CLAUDE.md now says the settings are pending rather than present-tense fact and names where the patch lives; the tier counts are the `-f json` ones (WARN adds 30 on top of ERR's 212, not 31 on 213 — the table output has a header row and a trailing blank); the versioned-route mutation goes through `once` like every other anchor, so a renamed `getUsage` fails loudly instead of no-op'ing; and a root `.gitattributes` marks the spec `-text` so the multi-line anchors survive a Windows checkout. Refs #202. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: assert the api gate still runs, not only what it runs with `the_workflow_uses_the_settings_this_module_pins` pinned the gate's two inputs but not whether the job is ever triggered, which is the one fail-open it could not see: settings that read correctly on a gate that never fires. The workflow's own header comment names the foreseeable edit ("There is deliberately no `on: paths:` filter"), and a later `paths:` would skip the gate on every PR that does not touch the spec with the test still green. Asserts a `pull_request` trigger, no `paths:`/`paths-ignore:` filter, and no `if:` on the job. The trigger check is line-based over the `on:` block rather than a byte-exact match, so a rewrite between the mapping, list and scalar forms of `on:` does not go red for nothing. * ci: tune the oasdiff gate to WARN with the two opt-in ERR checks Dobby's YAML from the #203 comment, applied verbatim (the App cannot push workflows). This is the half its guard test was failing on: the_workflow_uses_the_settings_this_module_pins reads this file and asserts fail-on and include-checks match the constants the test module pins, so the settings and the mutation test that certifies them cannot land apart. At fail-on=ERR with no include-checks the gate passed ten of the changes this spec's contract forbids. --------- Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ruben Hensen <ruben.hensen@protonmail.com>
Second half of the #255 spike. The commit before this one is the history-preserving subtree import; this one makes it build, test and release as a workspace member. Resolver: pg-pkg's sqlx and cryptify's rusqlite both reach libsqlite3-sys, which sets `links = "sqlite3"`, and only one package per resolve graph may claim a given `links` value. Optional dependencies sit in the graph whether or not their feature is on, so pg-pkg pulls sqlx-sqlite in despite enabling only `postgres`. sqlx-sqlite 0.9.0 wants libsqlite3-sys >=0.30.1,<0.38 and rusqlite 0.40.1 wants ^0.38.1: empty intersection, workspace does not resolve. sqlx 0.9 + rusqlite 0.39 is the newest pair that does. - root Cargo.toml: cryptify joins `members` - cryptify: pg-core becomes a path dependency; own Cargo.lock, release-plz.toml, .github/ and duplicate profile blocks dropped - build.rs: walk up for Cargo.lock — a member has none beside its manifest, and the panic would have been at build time, not resolve time - api-diff.yml: one matrix over both specs, not two jobs. Both api-gate suites require exactly one `fail-on:`/`include-checks:` line in the file - build.yml: matrices key on the crate directory so `cryptify` fits; this renames the `Test/Format/Clippy workspace` contexts - Dockerfile: build context moves to the repo root
Root .gitignore's `*.pem` matches the four fixtures #241 committed under pg-pkg/testdata/jwt_rotation/, leaving them tracked *and* ignored. release-plz refuses to run against that, so delivery.yml's `Release-plz PR` job has failed on every push to main since #241 landed on 2026-07-16 (12 runs). `Release-plz release` is unaffected, which is why nothing looked broken. Belongs in its own PR, not the cryptify spike; carried here only so the spike could get far enough to answer the release-plz question on #255.
Neither failure shows up in the test matrix; both are Docker-only. `cargo chef prepare` loads every workspace member's manifest, including ones the build never compiles, so the Dockerfile's member-by-member COPY list has to gain cryptify or the image fails with "failed to load manifest for workspace member /app/cryptify". With that fixed it fails again on MSRV: sqlx 0.9 declares rust-version 1.94.0 and this image pinned rust 1.91.1. sqlx 0.9 is what resolves the libsqlite3-sys `links` conflict, so the pin moves rather than the dependency. 1.96.1-slim-trixie is what cryptify's own Dockerfile already used, so both images now agree. Verified by building both: pg-pkg-spike:edge (192MB), cryptify-spike:edge (170MB).
Brings in the 0.6.2/0.3.7/0.6.0/0.1.3 release (#187) and the .gitignore fix (#274). Two conflicts: - .gitignore: took main's wording, which is what #274 actually shipped. The spike carried an earlier, terser version of the same fix. - cryptify's pg-core requirement moved 0.6.1 -> 0.6.2 by hand. release-plz maintains these reqs on unpublished members too (pg-pkg carries one), but it could not touch cryptify's during #187 because cryptify was not yet a member. Merged rather than rebased on purpose: rebasing replays all 296 imported commits onto main and discards the subtree merge.
Open
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
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.
Closes #255. The decision comment on that issue has the full spike writeup and the measurements; this is the change it recommends.
cryptify becomes a workspace member, compiled against the in-tree
pg-core. All 402 of its commits come across as a history-preserving subtree merge.Why
Today postguard builds pg-core against bincode-next 3.0.0-rc.14 and cryptify builds pg-core against bincode-next 3.1.1, because each repo resolves its own lockfile. Same source, two versions of the serializer that writes the container header, in two services that exchange containers, with nothing comparing them. CLAUDE.md's rule (pin the exact rc, re-audit on any bump) is not reaching cryptify's copy.
That is benign right now, and provably so:
pg-compat's lockfile is also on 3.1.1, so the green wire-compat check already demonstrates rc.14-sealed bytes opening under 3.1.1. But it falls out of another gate's dependency resolution by accident, and the next bincode bump is unguarded. One workspace is one lockfile, and the #127 memory-vs-stream class becomes a PR-time test failure rather than a cross-repo surprise.The price
libsqlite3-syssetslinks = "sqlite3"and only one package per resolve graph may claim it. pg-pkg reaches it throughsqlx, cryptify throughrusqlite. Optional deps are in the graph regardless of features, so pg-pkg pullssqlx-sqlitein despite enabling onlypostgres.Empty intersection. The newest pair that intersects is sqlx 0.9 + rusqlite 0.39, so this PR carries both. pg-pkg needed no source changes for sqlx 0.9.
The standing cost, and the part worth a deliberate yes: every future rusqlite bump now waits on sqlx's
libsqlite3-sysceiling. cryptify is already one release ahead of what sqlx permits.What else had to change, and why
cryptify/build.rsreadCargo.lockbeside its manifest to stampPG_CORE_VERSIONinto theX-PostGuardheader. A member has no lockfile there, so it now walks up fromCARGO_MANIFEST_DIR.api-diff.ymlcovers both specs via a matrix, not two jobs.pg-pkg/tests/api_gate.rsand cryptify'smod api_gate_testseach require exactly onefail-on:and oneinclude-checks:line in that file, so neither can claim a setting the committed job does not use. Two oasdiff steps would give two of each and fail both suites.build.yml's matrices key on the crate directory instead of apg-prefix, which renames theTest/Format/Clippy workspace (…)contexts. Only the twoWire compatcontexts are required onmain, so nothing breaks, but a ruleset added later needs the new names.COPY cryptify(cargo-chef loads every member's manifest even for a build that never compiles it) and its Rust pin moved 1.91.1 → 1.96.1, because sqlx 0.9 declaresrust-version = 1.94.0. Both of those passed the whole test matrix on host stable and failed only in Docker..github/,Cargo.lockandrelease-plz.tomlare gone, replaced by an entry in the rootrelease-plz.toml.Verification
Run locally on this branch, mirroring
build.yml's per-crate invocations:pg-core(test,rust,stream)pg-pkg(all-features)pg-cli(all-features)pg-ffi(all-features)cryptify(all-targets)cryptifyclippy-D warningscargo fmt --all --checkBoth images build:
docker build .anddocker build -f cryptify/Dockerfile ..Deliberately not in this PR
Image publishing still comes from the cryptify repo.
delivery.yml's Docker jobs push toghcr.io/${{ github.repository }}, so publishing cryptify's image from here means either renaming it away fromghcr.io/encryption4all/cryptifyor changing that package's GHCR access. Either affects whatever pulls it, so it is an ops decision rather than a mechanical port.That makes this a coherent intermediate state, not a half-finished one: postguard becomes source of truth for the code and gets the one-lockfile guarantee immediately, while the cryptify repo keeps building and pushing images exactly as it does today. Decommissioning it is a follow-up, and it needs privacybydesign/postguard-ops in the loop.
Also outstanding: release-plz will treat cryptify as an initial release. A subtree import brings commits, not tags, and the old tags were bare
v0.1.27against this workspace'scryptify-v{version}pattern. Without a hand-placedcryptify-v0.1.27tag on the import commit, the first changelog entry covers all 402 imported commits. Worth doing before this lands onmain.Reviewing this
The subtree merge makes the diff large.
git diff main...HEAD -- . ':!cryptify'is the 8-file change to the existing tree; everything undercryptify/is imported as-is apart fromCargo.toml,build.rs,Dockerfileand the oneinclude_str!path insrc/main.rs.