diff --git a/.github/scripts/notify-publish-safety.rb b/.github/scripts/notify-publish-safety.rb index 8ca43d9..817a7f4 100644 --- a/.github/scripts/notify-publish-safety.rb +++ b/.github/scripts/notify-publish-safety.rb @@ -616,10 +616,10 @@ def resume_action(existing_digest, expected_digest, mutable:) "publish gate must fail closed and export the finalized public-release state") assert_policy(spec.fetch("format_version") == 1, "release manifest format changed") -assert_policy(spec.fetch("version") == "2.0.0", "release version must remain 2.0.0") -assert_policy(spec.fetch("tag") == "notify-v2.0.0", "release tag must remain notify-v2.0.0") +assert_policy(spec.fetch("version") == "2.0.1", "release version must remain 2.0.1") +assert_policy(spec.fetch("tag") == "notify-v2.0.1", "release tag must remain notify-v2.0.1") assert_policy(spec.fetch("image") == "ghcr.io/psimaker/vaultsync-notify", "image repository changed") -assert_policy(spec.fetch("version_image") == "ghcr.io/psimaker/vaultsync-notify:2.0.0", +assert_policy(spec.fetch("version_image") == "ghcr.io/psimaker/vaultsync-notify:2.0.1", "version image changed") assert_policy(spec.fetch("binaries") == EXPECTED_BINARIES, "expected binary set or order changed") assert_policy(spec.fetch("release_assets").sort == EXPECTED_ASSETS.sort, "release asset set changed") @@ -639,9 +639,9 @@ def resume_action(existing_digest, expected_digest, mutable:) install_text = File.read(INSTALL_PATH) compose_text = File.read(COMPOSE_PATH) -assert_policy(install_text.include?("ghcr.io/psimaker/vaultsync-notify:2.0.0"), +assert_policy(install_text.include?("ghcr.io/psimaker/vaultsync-notify:2.0.1"), "installer default is not the reviewed version tag") -assert_policy(compose_text.include?("ghcr.io/psimaker/vaultsync-notify:2.0.0"), +assert_policy(compose_text.include?("ghcr.io/psimaker/vaultsync-notify:2.0.1"), "Compose default is not the reviewed version tag") assert_policy(!install_text.include?("ghcr.io/psimaker/vaultsync-notify:latest") && !compose_text.include?("ghcr.io/psimaker/vaultsync-notify:latest"), @@ -656,7 +656,16 @@ def resume_action(existing_digest, expected_digest, mutable:) install_text.include?("non-canonical checksum"), "binary installer no longer fails closed on checksum prerequisites") -security_text = flattened_step_text(security.fetch("jobs").fetch("image-scan")) +security_image_scan = security.fetch("jobs").fetch("image-scan") +security_text = flattened_step_text(security_image_scan) +security_resolve_step = steps(security_image_scan).find do |step| + step["name"] == "Resolve the published helper digest" +end +assert_policy(!security_resolve_step.nil?, "scheduled scan release resolver is missing") +security_release_env = security_resolve_step.fetch("env", {}) +assert_policy(security_release_env.fetch("TARGET_RELEASE_TAG", nil) == "notify-v2.0.1" && + security_release_env.fetch("FALLBACK_RELEASE_TAG", nil) == "notify-v2.0.0", + "scheduled scan must use the reviewed target and last public fallback releases") assert_policy(security_text.include?("IMAGE-DIGESTS"), "scheduled scan must resolve the release digest") assert_policy(security_text.include?("vaultsync-notify@${{ steps.release-image.outputs.digest }}"), "scheduled scan must use the exact digest") @@ -668,6 +677,13 @@ def resume_action(existing_digest, expected_digest, mutable:) security_text.include?("^sha256:[0-9a-f]{64}$") && security_text.include?("printf 'digest=%s\\n'"), "scheduled scan must export exactly one canonical image index digest") +assert_policy(security_text.include?("releases/tags/${TARGET_RELEASE_TAG}") && + security_text.include?("HTTP 404") && + security_text.include?("release_tag=$FALLBACK_RELEASE_TAG") && + security_text.include?(".draft == false") && + security_text.include?(".prerelease == false") && + security_text.include?("releases/tags/${release_tag}"), + "scheduled scan must fall back only while the reviewed release is not public") ci_notify_steps = steps(ci.fetch("jobs").fetch("notify-tests")) assert_policy(ci_notify_steps.any? { |step| step["run"] == "ruby .github/scripts/notify-publish-safety.rb" }, @@ -692,7 +708,7 @@ def resume_action(existing_digest, expected_digest, mutable:) base.merge(recovery_run_id: "29324314809"), base.merge(ref_type: "branch", ref_name: "main", recovery_run_id: "0"), base.merge(ref_type: "branch", ref_name: "main", recovery_run_id: "failed-run"), - base.merge(release_tag: "notify-v2.0.1"), + base.merge(release_tag: "notify-v2.0.0"), base.merge(confirmation: "publish"), base.merge(actor: "maintainer"), base.merge(triggering_actor: "maintainer"), diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 065ded4..366d305 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1428,7 +1428,7 @@ jobs: Helper-first rollout - The real explicit installer passed published 1.8.0 -> 2.0.0 -> 1.8.0 -> the same 2.0.0 digest on a standard Linux rootful-Docker runner. Credential-state bytes and the TLS SPKI pin survived rollback and forward recovery. The proof also records the known 1.8.0 endpoint-log behavior and requires 2.0.0 to emit none of the configured test credential, identifier, path, or URL values. See ROLLOUT-EVIDENCE.txt. + The real explicit installer passed published 1.8.0 -> ${RELEASE_VERSION} -> 1.8.0 -> the same ${RELEASE_VERSION} digest on a standard Linux rootful-Docker runner. Credential-state bytes and the TLS SPKI pin survived rollback and forward recovery. The proof also records the known 1.8.0 endpoint-log behavior and requires ${RELEASE_VERSION} to emit none of the configured test credential, identifier, path, or URL values. See ROLLOUT-EVIDENCE.txt. Compatibility and limits diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 7438600..2606e8a 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -126,11 +126,44 @@ jobs: id: release-image env: GH_TOKEN: ${{ github.token }} - RELEASE_TAG: notify-v2.0.0 + TARGET_RELEASE_TAG: notify-v2.0.1 + FALLBACK_RELEASE_TAG: notify-v2.0.0 run: | set -euo pipefail mkdir -p /tmp/vaultsync-notify-release - gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ + + target_json=/tmp/vaultsync-notify-release/target-release.json + target_error=/tmp/vaultsync-notify-release/target-release.error + if gh api \ + "repos/${GITHUB_REPOSITORY}/releases/tags/${TARGET_RELEASE_TAG}" \ + > "$target_json" 2> "$target_error"; then + if jq -e --arg tag "$TARGET_RELEASE_TAG" \ + '.tag_name == $tag and .draft == false and .prerelease == false' \ + "$target_json" > /dev/null; then + release_tag=$TARGET_RELEASE_TAG + elif jq -e '.draft == true or .prerelease == true' \ + "$target_json" > /dev/null; then + release_tag=$FALLBACK_RELEASE_TAG + else + echo "Target helper release metadata is not canonical" >&2 + exit 1 + fi + elif grep -Fq 'HTTP 404' "$target_error"; then + release_tag=$FALLBACK_RELEASE_TAG + else + echo "Target helper release lookup failed" >&2 + exit 1 + fi + + selected_json=/tmp/vaultsync-notify-release/selected-release.json + gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${release_tag}" \ + > "$selected_json" + jq -e --arg tag "$release_tag" \ + '.tag_name == $tag and .draft == false and .prerelease == false' \ + "$selected_json" > /dev/null + echo "Scanning immutable helper release ${release_tag}" + + gh release download "$release_tag" --repo "$GITHUB_REPOSITORY" \ --pattern IMAGE-DIGESTS --dir /tmp/vaultsync-notify-release mapfile -t digests < <( sed -n 's/^index_digest=//p' /tmp/vaultsync-notify-release/IMAGE-DIGESTS diff --git a/docs/helper-publication-rollout.md b/docs/helper-publication-rollout.md index e45cdf1..92a52c8 100644 --- a/docs/helper-publication-rollout.md +++ b/docs/helper-publication-rollout.md @@ -1,7 +1,9 @@ -# Helper 2.0.0 publication and helper-first rollout +# Helper 2.0.1 publication and helper-first rollout This document defines the owner-gated publication, compatibility, rollback, -monitoring, and recovery contract for `vaultsync-notify` 2.0.0. It does not by +monitoring, and recovery contract for `vaultsync-notify` 2.0.1. This patch +supersedes 2.0.0 because that release did not expose D022's pending transcript +fingerprint to the local operator; no 2.0.0 artifact or tag is changed. It does not by itself claim that publication ran. The GitHub release, its `RELEASE-MANIFEST.json`, the exact workflow run, and the registry digest are the canonical post-publication evidence. @@ -15,9 +17,9 @@ upload, download, or roundtrip product evidence follows from publication. The reviewed source manifest is [`../notify/release.json`](../notify/release.json). It fixes: -- version `2.0.0` and tag `notify-v2.0.0`; +- version `2.0.1` and tag `notify-v2.0.1`; - image repository `ghcr.io/psimaker/vaultsync-notify`, with the only new tag - `2.0.0`; + `2.0.1`; - five expected binaries for Linux amd64/arm64, macOS amd64/arm64, and Windows amd64; - the previous public rollback tag, commit, and multi-platform image digest; @@ -51,13 +53,13 @@ repository security setting requires separate authorization. | Artifact | Required proof | |---|---| -| OCI index for Linux amd64 and arm64 | Exact index and platform SHA-256 digests; source/version labels; embedded `vaultsync-notify 2.0.0`; BuildKit provenance/SBOM plus GitHub repository-bound provenance and SPDX SBOM attestations. | +| OCI index for Linux amd64 and arm64 | Exact index and platform SHA-256 digests; source/version labels; embedded `vaultsync-notify 2.0.1`; BuildKit provenance/SBOM plus GitHub repository-bound provenance and SPDX SBOM attestations. | | Five static binaries | Rebuilt twice byte-identically from the tag commit, listed in `SHA256SUMS`, scanned, and covered by GitHub provenance and SPDX SBOM attestations. | | `SHA256SUMS` | Exact checksum for each binary; installers fail closed if this asset or a local SHA-256 implementation is unavailable. | | `SBOM.spdx.json` | SPDX 2.3 inventory for the release binary set. | | `IMAGE-DIGESTS` | Image repository, version tag, OCI index digest, and Linux amd64/arm64 manifest digests. | | `RELEASE-MANIFEST.json` | Release version/tag, exact source commit, image digest, rollback baseline, binary sizes/digests, SBOM digest, and expected asset set. The exact workflow run is recorded separately in rollout evidence. | -| `ROLLOUT-EVIDENCE.txt` | Exact old/new references and the supported-host upgrade, rollback, and forward-recovery results, including the observed legacy 1.8.0 endpoint-log boundary and the 2.0.0 sensitive-log assertion. | +| `ROLLOUT-EVIDENCE.txt` | Exact old/new references and the supported-host upgrade, rollback, and forward-recovery results, including the observed legacy 1.8.0 endpoint-log boundary and the 2.0.1 sensitive-log assertion. | The image runtime is a static scratch image built from a digest-pinned Go builder. Its CA bundle is copied from that same pinned builder. There is no @@ -79,8 +81,8 @@ Publication is staged in this order: release without replacing any asset. 4. On a fresh GitHub-hosted standard Linux runner using rootful Docker, execute the real explicit diagnostics installer against only digest references: - published 1.8.0 → candidate 2.0.0 → published 1.8.0 → the same candidate - 2.0.0. + published 1.8.0 → candidate 2.0.1 → published 1.8.0 → the same candidate + 2.0.1. 5. Require the old helper to keep diagnostics unavailable, the new helper to expose the TLS listener, the rollback to preserve credential-state bytes, and forward recovery to preserve both those bytes and the TLS SPKI pin. The @@ -105,14 +107,14 @@ Diagnostics is additive and opt-in; Trigger v1 and Relay v1 remain unchanged. | App | Helper | Relay | Result | |---|---|---|---| | Released old app | Published old helper 1.8.0 | Existing old Relay | Existing Trigger-v1 behavior only. | -| Released old app | Published helper 2.0.0, diagnostics unset | Existing old Relay | Byte-compatible Trigger-v1 behavior. No listener, credential, namespace, trust, mapping, or diagnostics artifact is created. | -| Released old app | Helper 2.0.0 explicitly configured | Existing old Relay | Helper capability exists locally, but the old app never calls it. Upload, download, and roundtrip remain unset. | +| Released old app | Published helper 2.0.1, diagnostics unset | Existing old Relay | Byte-compatible Trigger-v1 behavior. No listener, credential, namespace, trust, mapping, or diagnostics artifact is created. | +| Released old app | Helper 2.0.1 explicitly configured | Existing old Relay | Helper capability exists locally, but the old app never calls it. Upload, download, and roundtrip remain unset. | | Future capable app | Published old helper 1.8.0 | Any supported Relay v1 | Honest capability unavailable. The app must not pair, create, trust, or transfer. | -| Future capable app | Helper 2.0.0, unconfigured/unpaired/unauthorized | Any supported Relay v1 | Honest unavailable/unsupported response; no fallback evidence and no automatic action. | -| Future capable app | Helper 2.0.0, exact explicit pairing and namespace authorization | Existing old Relay | The local D022–D024 helper contract may be used. Relay remains outside upload/download/roundtrip correlation. | -| Future capable app | Helper 2.0.0 | Future new Relay | Same local diagnostics contract; Relay version does not strengthen sync evidence. | -| Any app | Helper 2.0.0 → 1.8.0 | Any Relay v1 | Diagnostics becomes unavailable. Credentials, namespace content, mappings, backups, versions, conflicts, and tombstones are not deleted or rewritten. The rollback also restores 1.8.0's legacy configured-endpoint startup log fields; operators must apply their existing log-access controls. | -| Any app | Helper 2.0.0 → 1.8.0 → exact 2.0.0 digest | Any Relay v1 | Forward recovery revalidates preserved state and requires current exact credentials/authorization. No operation resumes automatically. | +| Future capable app | Helper 2.0.1, unconfigured/unpaired/unauthorized | Any supported Relay v1 | Honest unavailable/unsupported response; no fallback evidence and no automatic action. | +| Future capable app | Helper 2.0.1, exact explicit pairing and namespace authorization | Existing old Relay | The local D022–D024 helper contract may be used only after the explicit local transcript comparison. Relay remains outside upload/download/roundtrip correlation. | +| Future capable app | Helper 2.0.1 | Future new Relay | Same local diagnostics contract; Relay version does not strengthen sync evidence. | +| Any app | Helper 2.0.1 → 1.8.0 | Any Relay v1 | Diagnostics becomes unavailable. Credentials, namespace content, mappings, backups, versions, conflicts, and tombstones are not deleted or rewritten. The rollback also restores 1.8.0's legacy configured-endpoint startup log fields; operators must apply their existing log-access controls. | +| Any app | Helper 2.0.1 → 1.8.0 → exact 2.0.1 digest | Any Relay v1 | Forward recovery revalidates preserved state and requires current exact credentials/authorization. No operation resumes automatically. | The five downloadable binaries do not expand diagnostics packaging support. Docker Host-Bind on a standard Linux host with rootful Docker remains the only @@ -131,7 +133,7 @@ Publication aborts before the GitHub release becomes public on any of: - missing/extra release asset, binary non-reproducibility, checksum mismatch, or image architecture/version/label mismatch; - failure of upgrade, rollback, forward recovery, state-byte preservation, - TLS-pin preservation, mount constraints, 2.0.0 sensitive-log exclusion, or + TLS-pin preservation, mount constraints, 2.0.1 sensitive-log exclusion, or exact installer use. The separately asserted legacy endpoint fields from the immutable 1.8.0 rollback image are a documented rollback boundary, not candidate evidence. @@ -153,12 +155,12 @@ or user-vault progress. Existing installs are not migrated, paired, restarted, or reconfigured by publication. The historical `latest` image tag is not moved. A user or operator -must explicitly rerun the installer, select the reviewed `2.0.0` version tag, +must explicitly rerun the installer, select the reviewed `2.0.1` version tag, or pull the documented digest. The installer re-resolves the version tag and runs the resulting local content ID; a network failure does not silently reuse a stale tag. -Without both diagnostics configuration paths, 2.0.0 behaves as the prior +Without both diagnostics configuration paths, 2.0.1 behaves as the prior Trigger-v1 helper and creates no diagnostics state. With explicit configuration, pairing, namespace enablement, and later app operations remain separate signed actions. No helper publication discovers Syncthing, changes its configuration, @@ -174,8 +176,8 @@ delivery, or global health. Emergency rollback also returns to 1.8.0's existing startup logging of configured endpoint values. It does not log the API key in this proof, but endpoint values can still be private deployment metadata. Restrict old-helper -log access and forward-recover to the exact 2.0.0 digest when the abort cause is -cleared; 2.0.0's rollout gate rejects those configured values in candidate +log access and forward-recover to the exact 2.0.1 digest when the abort cause is +cleared; 2.0.1's rollout gate rejects those configured values in candidate logs. ## Evidence boundary after helper rollout diff --git a/docs/helper-runtime-packaging-readiness.md b/docs/helper-runtime-packaging-readiness.md index 3bf511a..18f5a71 100644 --- a/docs/helper-runtime-packaging-readiness.md +++ b/docs/helper-runtime-packaging-readiness.md @@ -118,8 +118,14 @@ set either diagnostics environment variable and do not activate the runtime. and separate private state directory, then starts the capable helper with no namespace mount. 3. Run `diagnostics-docker.sh pair`. The one-time QR value is emitted only to - that terminal. Pairing remains pending until every D022 signed step and the - explicit app fingerprint comparison complete. + that terminal. After the app has authenticated the type-`2` acceptance, run + `diagnostics-docker.sh list` in the same operator-controlled environment. + The exact pending row includes `transcript=<12 uppercase hex characters>`; + compare it with the app before approving type `3`. The transcript value is + emitted only by this explicit local command and must not be redirected into + service logs or support bundles. Pairing remains pending until every D022 + signed step and this exact comparison complete. Active rows deliberately no + longer expose a transcript value. 4. After the app sends a signed D023 enablement request, run `diagnostics-docker.sh enable` with the exact canonical folder host path and explicit supported-host confirmation. The command displays the exact diff --git a/docs/relay-spec.md b/docs/relay-spec.md index f37c5ac..7c2af4e 100644 --- a/docs/relay-spec.md +++ b/docs/relay-spec.md @@ -1,6 +1,6 @@ # VaultSync Cloud Relay — Specification -> **Status:** Cloud Relay 1.3.0 and the matching app support are implemented and locally verified, but the Relay production rollout and VaultSync 2.0 app release are still pending. Once deployed, Relay 1.3 provisioning requires a verified StoreKit signed transaction and gives exact pre-existing legacy registrations a bounded compatibility window through October 31, 2026. Helper publication state is determined only by the newest public `notify-v*` release and its exact manifest; `notify-v1.8.0` remains the fixed rollback baseline for helper 2.0.0. Even a published capable helper provides no app upload, download, or correlated-roundtrip evidence until the separate app milestones succeed. A Relay-observed signal proves only accepted Relay processing: not helper identity, APNs delivery, background start, local data progress, upload, download, or a roundtrip. Existing Relay v1 provisioning, trigger, and push contracts remain unchanged. This document is the protocol and architecture reference for the relay, the `vaultsync-notify` sidecar, and the iOS client. +> **Status:** Cloud Relay 1.3.0 and the matching app support are implemented and locally verified, but the Relay production rollout and VaultSync 2.0 app release are still pending. Once deployed, Relay 1.3 provisioning requires a verified StoreKit signed transaction and gives exact pre-existing legacy registrations a bounded compatibility window through October 31, 2026. Helper publication state is determined only by the newest public `notify-v*` release and its exact manifest; `notify-v1.8.0` remains the fixed rollback baseline for helper 2.0.1. Even a published capable helper provides no app upload, download, or correlated-roundtrip evidence until the separate app milestones succeed. A Relay-observed signal proves only accepted Relay processing: not helper identity, APNs delivery, background start, local data progress, upload, download, or a roundtrip. Existing Relay v1 provisioning, trigger, and push contracts remain unchanged. This document is the protocol and architecture reference for the relay, the `vaultsync-notify` sidecar, and the iOS client. ## Overview diff --git a/notify/README.md b/notify/README.md index 50aefce..06ca532 100644 --- a/notify/README.md +++ b/notify/README.md @@ -72,7 +72,7 @@ docker run -d --name vaultsync-notify --restart unless-stopped \ -v /PATH/TO/syncthing:/config:ro \ -e SYNCTHING_CONFIG=/config/config.xml \ -e RELAY_URL=https://relay.vaultsync.eu \ - ghcr.io/psimaker/vaultsync-notify:2.0.0 + ghcr.io/psimaker/vaultsync-notify:2.0.1 ``` Replace `/PATH/TO/syncthing` with your Syncthing config folder (often `~/.local/state/syncthing` or `~/.config/syncthing`). Permission error? Add `-u :` for the user that owns `config.xml`. @@ -224,7 +224,14 @@ Validates: Syncthing API reachable · API key valid · Device ID readable · rel **Runtime healthcheck** — the image's `HEALTHCHECK` runs `vaultsync-notify --healthcheck`, validating real readiness (Syncthing API, credentials, Device ID, relay health), not just process liveness. Peer state is deliberately excluded here: a legitimately offline peer must never flip the container to unhealthy. -**Version** — `vaultsync-notify --version` prints the installed helper version (needs no configuration). The installer shows old → new on upgrades. Docker installs pull the reviewed `2.0.0` tag and run only its resolved local content ID; failed pulls do not fall back to a stale tag. Binary installs select the newest published `notify-v*` release, require its `SHA256SUMS`, replace the binary, and restart the service. Future Docker upgrades require another reviewed version tag or an explicit `VAULTSYNC_NOTIFY_IMAGE` override. +**Version** — `vaultsync-notify --version` prints the installed helper version (needs no configuration). The installer shows old → new on upgrades. Docker installs pull the reviewed `2.0.1` tag and run only its resolved local content ID; failed pulls do not fall back to a stale tag. Binary installs select the newest published `notify-v*` release, require its `SHA256SUMS`, replace the binary, and restart the service. Future Docker upgrades require another reviewed version tag or an explicit `VAULTSYNC_NOTIFY_IMAGE` override. + +**Diagnostics pairing comparison** — after the app has authenticated the +pending helper acceptance, run `diagnostics-docker.sh list` locally. The +pending row includes the exact D022 `transcript=` fingerprint that must match +the value in the app before activation. It is local comparison output, not a +credential; do not redirect it into service logs or support bundles. Active +rows omit it. --- diff --git a/notify/diagnostics_operator.go b/notify/diagnostics_operator.go index 1ee9702..6677df4 100644 --- a/notify/diagnostics_operator.go +++ b/notify/diagnostics_operator.go @@ -332,14 +332,11 @@ func runDiagnosticsAdminOperatorLocked(runtime *diagnosticsRuntime, command diag if command.action == "list" { lines := make([]string, 0, len(authorizations)) for _, authorization := range authorizations { - namespaceState := "no" - if len(authorization.NamespaceInitialAppKeyID) == 32 { - namespaceState = "yes" + line, err := diagnosticsAdminListLine(authorization) + if err != nil { + return "", err } - lines = append(lines, fmt.Sprintf( - "%s state=%s namespace=%s\n", - diagnosticsAdminAppFingerprint(authorization.AppKeyID), authorization.State, namespaceState, - )) + lines = append(lines, line) } sort.Strings(lines) return strings.Join(lines, ""), nil @@ -379,6 +376,28 @@ func runDiagnosticsAdminOperatorLocked(runtime *diagnosticsRuntime, command diag } } +func diagnosticsAdminListLine(authorization diagnosticsPairingAuthorization) (string, error) { + namespaceState := "no" + if len(authorization.NamespaceInitialAppKeyID) == 32 { + namespaceState = "yes" + } + line := fmt.Sprintf( + "%s state=%s namespace=%s", + diagnosticsAdminAppFingerprint(authorization.AppKeyID), authorization.State, namespaceState, + ) + if authorization.State == "pending" { + fingerprint, err := diagnosticsPairingFingerprint( + authorization.AppRequestDigest, + authorization.CurrentStateDigest, + ) + if err != nil { + return "", errDiagnosticsPairingUnavailable + } + line += " transcript=" + fingerprint + } + return line + "\n", nil +} + func diagnosticsAdminRotationReady( runtime *diagnosticsRuntime, authorization diagnosticsPairingAuthorization, diff --git a/notify/diagnostics_pairing_test.go b/notify/diagnostics_pairing_test.go index 33538b4..b3044c9 100644 --- a/notify/diagnostics_pairing_test.go +++ b/notify/diagnostics_pairing_test.go @@ -314,6 +314,19 @@ func TestDiagnosticsPairingPendingFinalizeReceiptActivateAbortAndReplay(t *testi if err != nil || len(fingerprint) != 12 || fingerprint != strings.ToUpper(fingerprint) { t.Fatalf("fingerprint = %q, err=%v", fingerprint, err) } + state, err := manager.store.snapshot() + if err != nil || len(state.Authorizations) != 1 { + t.Fatalf("pending authorization snapshot = %d, %v", len(state.Authorizations), err) + } + pendingLine, err := diagnosticsAdminListLine(state.Authorizations[0]) + if err != nil || !strings.Contains(pendingLine, " state=pending namespace=no transcript="+fingerprint+"\n") { + t.Fatalf("pending transcript fingerprint unavailable to operator: %q, %v", pendingLine, err) + } + invalidPending := state.Authorizations[0] + invalidPending.CurrentStateDigest = nil + if _, err := diagnosticsAdminListLine(invalidPending); err == nil { + t.Fatal("operator list accepted a pending authorization without an exact helper-accept digest") + } transition := func(prior diagnosticsPairingMessage, messageType uint64) diagnosticsPairingMessage { now := uint64(clock.current().Unix()) @@ -340,7 +353,7 @@ func TestDiagnosticsPairingPendingFinalizeReceiptActivateAbortAndReplay(t *testi readyAck := transition(finalizeAck, diagnosticsPairingReceipt) activeAck := transition(readyAck, diagnosticsPairingActivate) _ = transition(activeAck, diagnosticsPairingAbort) - state, err := manager.store.snapshot() + state, err = manager.store.snapshot() if err != nil { t.Fatal(err) } diff --git a/notify/diagnostics_runtime_integration_test.go b/notify/diagnostics_runtime_integration_test.go index fb634ee..2c57736 100644 --- a/notify/diagnostics_runtime_integration_test.go +++ b/notify/diagnostics_runtime_integration_test.go @@ -314,6 +314,9 @@ func TestDiagnosticsAdminActionsRequireExactLocalFingerprint(t *testing.T) { if err != nil || !strings.Contains(listing, fingerprint+" state=active namespace=no") { t.Fatalf("admin list did not return the exact local fingerprint: %v", err) } + if strings.Contains(listing, " transcript=") { + t.Fatal("active admin list retained a pending transcript fingerprint") + } if _, err := runDiagnosticsAdminOperator(context.Background(), appConfig, diagnosticsAdminCommand{ action: "rotate-helper", folderID: folderID, appFingerprint: "000000000000", }); err == nil { diff --git a/notify/docker-compose.yml b/notify/docker-compose.yml index a40b0d9..7f70052 100644 --- a/notify/docker-compose.yml +++ b/notify/docker-compose.yml @@ -16,7 +16,7 @@ services: restart: unless-stopped vaultsync-notify: - image: ghcr.io/psimaker/vaultsync-notify:2.0.0 + image: ghcr.io/psimaker/vaultsync-notify:2.0.1 # Run as the same uid/gid that owns Syncthing's config.xml. config.xml is mode # 0600, so a mismatched uid cannot read the API key off the shared volume — even # read-only. The 1000:1000 default fits the OFFICIAL syncthing/syncthing image; diff --git a/notify/release.json b/notify/release.json index e7e2a08..f49fc41 100644 --- a/notify/release.json +++ b/notify/release.json @@ -1,9 +1,9 @@ { "format_version": 1, - "version": "2.0.0", - "tag": "notify-v2.0.0", + "version": "2.0.1", + "tag": "notify-v2.0.1", "image": "ghcr.io/psimaker/vaultsync-notify", - "version_image": "ghcr.io/psimaker/vaultsync-notify:2.0.0", + "version_image": "ghcr.io/psimaker/vaultsync-notify:2.0.1", "rollback": { "tag": "notify-v1.8.0", "commit": "e4f9e3088d7b7bc47943ff59db73de369c16c543", diff --git a/notify/scripts/install.sh b/notify/scripts/install.sh index 019a483..21caf1a 100755 --- a/notify/scripts/install.sh +++ b/notify/scripts/install.sh @@ -35,7 +35,7 @@ set -eu RELAY_URL="${RELAY_URL:-https://relay.vaultsync.eu}" MODE="${VAULTSYNC_NOTIFY_MODE:-auto}" -IMAGE="${VAULTSYNC_NOTIFY_IMAGE:-ghcr.io/psimaker/vaultsync-notify:2.0.0}" +IMAGE="${VAULTSYNC_NOTIFY_IMAGE:-ghcr.io/psimaker/vaultsync-notify:2.0.1}" REPO="psimaker/vaultsync" CONTAINER_NAME="vaultsync-notify" DRY_RUN=0 diff --git a/notify/scripts/tests/install-dry-run-test.sh b/notify/scripts/tests/install-dry-run-test.sh index 6438568..9245713 100755 --- a/notify/scripts/tests/install-dry-run-test.sh +++ b/notify/scripts/tests/install-dry-run-test.sh @@ -117,8 +117,8 @@ grep "would run: docker run -d" "$OUT_DOCKER" | grep -q -- "-u $EXPECTED_OWNER " # silently keeps the old local version-tag target. grep -q 'would run: docker pull ' "$OUT_DOCKER" \ || fail "docker mode: no docker pull before start — a re-run would keep the old image (#87)" -grep -q 'would run: docker pull ghcr.io/psimaker/vaultsync-notify:2.0.0' "$OUT_DOCKER" \ - || fail "docker mode: default image is not the reviewed 2.0.0 tag" +grep -q 'would run: docker pull ghcr.io/psimaker/vaultsync-notify:2.0.1' "$OUT_DOCKER" \ + || fail "docker mode: default image is not the reviewed 2.0.1 tag" grep -q 'runtime_image=[$]new_image_id' "$INSTALL_SH" \ || fail "docker mode: the successful pull is not converted to an immutable local image ID" if grep -q 'continuing with the LOCAL image' "$INSTALL_SH"; then