Release: merge development into beta - #691
Open
github-actions[bot] wants to merge 59 commits into
Open
github-actions[bot] wants to merge 59 commits into
github-actions[bot] wants to merge 59 commits into
Conversation
OpenSpec change harden-vault-key-material-guards: proposal, design, tasks, and spec deltas for a verified master-password proof (VaultKeyProof) gating the irreversible key-material operations, plus a migration abort route. Reproduced end-to-end against development; findings 1 and 2 documented in the proposal. Spec only — implementation lands in a separate commit. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…674) OpenSpec change migrate-emergency-access-on-rotation: proposal, design, tasks, and spec deltas. A compromise-recovery rotation re-envelopes each reachable emergency contact under the new key (buildRecoveryEnvelope with the new private key + the grantee's current certificate) and invalidates only the residual, correcting the spec's claim that the owner cannot re-wrap it alone. Spec only — implementation lands separately. Refs #674 Assisted-by: ClaudeCode:claude-opus-5
The "any completed rotation silently costs emergency access" open question is resolved by #674 (migrate-emergency-access-on-rotation), which re-envelopes reachable contacts under the new key. The lost-password route's destructive mechanics (the revocation warning and the refuse-while-a-usable-contact-exists gate) are likewise folded into #674; this note records where each piece now lives. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…674 Extends this change to carry #395's lost-password-route safeguard: revoking a user suite still clears its emergency envelopes, but must now warn plainly (secrets gone, emergency access deleted, accessor must retrieve first while the suite is active), refuse while a usable emergency contact exists unless an explicit override is given, and surface the count of usable contacts (never identities). Belongs here because the guard in #673 makes revocation the only forgotten-password route, and the clearing is emergency-access lifecycle on a suite key-state transition — the surface this change owns. Adds spec scenarios (refuse-without-override, proceed-with-override), a design decision D5, a tasks section 4b, and proposal/impact notes. Refs #674 Assisted-by: ClaudeCode:claude-opus-5
…673) Correcting the abort spec discovered during implementation: revoking the unused successor suite would run EncryptionSuiteRevokedListener, which for a user suite sweeps the owner's incoming ShareTargets and promotes their delegations — destroying real state over a migration the abort exists to undo. The successor is brand-new and empty, so it is deleted outright. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
The abort route the compromiseRecovery refusal already promises but that did not exist — the remedy named in the error message. It is the non-destructive terminal: completion carries the vault forward to the new suite and marks the old one compromised; abort carries it back to the old suite, which stays active and readable. Abort is permitted only while no record has been committed to the new suite (MigrationWorkService::countCommitted). Once a record has moved, both outcomes lose data, so the migration stays in_progress and the caller is pointed at resuming — a 409 carrying the committed count. This restriction is also exactly what makes abort safe against the session-only lockout: producing a valid re-encrypted record needs the master password, so a hostile session that never held it can never have committed one and can always be aborted away. On success: status -> aborted, the successor suite is deleted (not revoked, which would cascade the user-suite lost-identity teardown), failure accounting is cleared, the write lock is released, and SuiteMigrationAbortedEvent fires — NOT SuiteMigrationCompletedEvent, so the terminal cascade (compromise-flagging, link-share revocation, emergency-access invalidation) never runs. Its one listener unlocks the SecretRequests locked at start, keeping them on the old suite. Frontend: an "Abort and keep my old key" control on the resume banner plus the abortMigration store action. Backend + store fully unit-tested (abort restores/deletes; refused-after-commit with count; idempotent; aborted-not- completed event); phpmd clean; prettier clean. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
The core of the fix for the session-only lockout (#395). A destructive operation on vault key material now requires a VaultKeyProof: a signature, made with the caller's suite private key, over a server-issued challenge bound to the operation's own parameters. The private key is obtainable only by decrypting its envelope with the master password, so a verified proof is a server-verifiable proof of the master password — a stolen session, a leaked app password, or XSS in an unlocked tab no longer suffices, because the session key is non-extractable and decrypt-only and so cannot sign. - `#[VaultKeyProofRequired(binds, subject, purpose)]` declares the guard on a method; the binding lives on the attribute because the middleware cannot read the request body (the framework decodes JSON and drops the raw bytes), so the proof commits to NAMED parameters, hashed individually in order. - `VaultKeyProofMiddleware` enforces it: reads the attribute by reflection, resolves the subject suite, collects the bound params, delegates to the service, and maps a failure to 403 `key_proof_required`. It consults no auth backend and honours no token scope, so it is not waived for SSO/app-password sessions — its authority is key material, not the login method. - `VaultKeyProofService` issues a STATELESS, expiring, HMAC-authenticated nonce (no ICacheFactory — a null cache on a default install would break the flow) and verifies an RSASSA-PKCS1-v1_5 SHA-256 signature. Replay is a non-issue because the signature commits to the operation's parameters. - Challenge endpoint `GET /api/v1/suites/{id}/proof-challenge` (ungated). - Guard applied to compromiseRecovery, updatePrivateKey, complete, and the emergency-contact destroy. `VaultKeyProofAttributesTest` enumerates them and fails the build if one drops the attribute (a declarative guard fails open by omission), with a documented exclusion list (challenge, abort). Service crypto and middleware dispatch fully unit-tested (valid verifies; wrong key / altered value / tampered nonce / expired / wrong purpose / wrong user / missing all refused; attribute dispatch, subject resolution, foreign suite, 403 mapping). phpmd clean. The client half (proveMasterPassword + wiring the four flows) lands next — until then the guarded routes 403 by design. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…#673) The client half of the guard. `proveMasterPassword` (reauth.js) decrypts the suite envelope with the freshly entered master password, re-imports the PKCS#8 bytes for SIGNING (RSASSA-PKCS1-v1_5 SHA-256 — a distinct capability from the session key, which is non-extractable and decrypt-only and so cannot sign), signs the challenge bound to the operation's parameters, and discards every derived key. `keyProof.js` fetches a challenge and returns the two proof headers, so the four flows do not each re-implement it. Wired the flows that already hold the master password, so they keep working against the now-guarded routes: - compromise-recovery START — proof over the OLD key (old password) bound to the new key material; - migration COMPLETE on the initiate path — proof over the NEW key (new password) bound to the migration id; - routine password change (updatePrivateKey) — proof over the current key (old password) bound to the new envelope; the old key is already materialised there, so no extra prompt. Tested: proveMasterPassword round-trips under RSASSA-PKCS1-v1_5 (verifies over the exact server-rebuilt message; wrong password throws before signing; a changed bound value fails verification), and the session key is pinned non-extractable / decrypt-only. Store tests still green. prettier + eslint clean (0 errors). REMAINING (tracked in tasks §4.7-4.8): the emergency-contact delete and the resume-path completion both need a master-password prompt at the point of action (no password in hand there), plus the 403 re-enter-and-retry UX. Until those land, those two paths return 403 by design. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
… §4.7-4.8) Finishes the client wiring so all four guarded flows work end to end. Emergency-contact delete (§4.7): `emergencyAccess.revoke(id, masterPassword)` builds a proof (subject active, bound to the contact id) and the delete carries it. `EmergencyAccessView` gained a master-password confirm dialog — deleting a contact destroys its recovery envelope, so it must prove the master password, which is why a session alone can no longer do it. Completion (§4.8): completion's proof is now over the OLD (retiring) key rather than the new one, via a new middleware subject `migrationOldSuite` that resolves the migration's old suite. Both suites are active at completion so 'active' was ambiguous, and — the point — the old key is the one BOTH the initiate and resume paths already hold the password for, so a resumed run finalises with no extra prompt. The "Finish anyway" acknowledgement path builds the proof from the retained (or re-entered) old password, and the form re-shows the password field on a `key_proof_required` refusal — the re-enter-and-retry UX. Coverage and middleware tests updated for the new subject; the middleware gains SuiteMigrationMapper to resolve the old suite. Backend + frontend tests green (79 PHP incl. the new migrationOldSuite resolution test; 22 frontend incl. proveMasterPassword). phpmd clean; prettier + eslint 0 errors. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
… §7) §6.3: VaultKeyProofCrossImplTest verifies a signature produced by the browser's scheme (WebCrypto RSASSA-PKCS1-v1_5 SHA-256, the one proveMasterPassword uses) with PHP openssl_verify over VaultKeyProofService::signedMessage — proving the two implementations agree on both the signature scheme and the message construction, the one interop risk a same-language test cannot catch. A tampered bound value breaks it. Fixture at tests/fixtures/vault-key-proof.json, regenerated by generate-vault-key-proof-fixture.mjs. §7: documented the guard in docs/ARCHITECTURE.md §4.2 — the guarded-route table, the attribute contract, the load-bearing design points (sign-not- decrypt; stateless nonce; not waived for any session type; complete proves the old key; abort deliberately unguarded), and the rule that a new destructive route MUST be added to VaultKeyProofAttributesTest. Confirmed gate-110 does not apply (no migration, info.xml version unchanged). Change now at 42/47. Remaining: 6.5/6.6 (a full request-pipeline / live without-proof assertion — belongs with the §7.6 live reproduction and a Newman e2e), and the human submission steps (§7.1 CI gates, §7.5 PR disclosure, §7.6 independent verification). Refs #673 Assisted-by: ClaudeCode:claude-opus-5
Adds a component test for the master-password gate on emergency-contact revocation: clicking Revoke opens the confirmation without calling the store; confirming passes the entered password through to store.revoke (which builds the proof); a key_proof_required refusal is surfaced and the dialog stays open to retry; a successful revoke closes it. There was no prior EmergencyAccessView test, so this is a new file rather than an extension. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
CI quality checks flagged three things the local per-file runs missed: - phpmd: `VaultKeyProofMiddleware::afterException` has unused `$controller`/ `$methodName` (mandated by the Middleware override) — suppressed with the same annotation MigrationController uses. Adding VaultKeyProofService pushed `EncryptionSuiteController` to coupling 13 — suppressed with justification, as two sibling controllers already do. - phpcs: `VaultKeyProofService` called its own `b64url()`/`mac()` with positional args (the codebase requires named params for internal calls), and the `EncryptionSuiteController` constructor docblock was missing the `$proofService` @PARAM. Full `lib/` is back to 0 errors. - test:l10n / l10n-parity: the 8 new UI strings (abort control, emergency revoke dialog, re-auth field) were added to `l10n/en.json` and seeded into all 36 required locales. Non-English values are English placeholders pending Transifex, consistent with how new source strings enter the pipeline. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…stener (#673) The coverage-baseline guard failed because new code in MODIFIED files was untested, dropping their coverage against the merge base: - EncryptionSuiteController::proofChallenge had no test — added three (issue on a valid purpose; 400 on an unknown purpose; 404 on a foreign suite); - MigrationWorkService::countCommitted was only ever mocked (in MigrationServiceTest), so its body was uncovered — added a direct test summing the new-suite rows across the three stores, plus the zero case; - SuiteMigrationAbortedListener (a new file) gained a test: it unlocks the SecretRequests keeping the old suite, and ignores other events. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
The l10n/*.js browser catalogues are compiled from l10n/*.json, so the 8 new UI strings left them stale (check:l10n-js failed). Ran `npm run l10n:build` to regenerate all 37; the diff is purely additive and prettier-clean. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…ds (#673) Three mechanical gates were red on the guard-hardening change: - gate-46 (spec-anchor-existence): the abort @SPEC anchors pointed at openspec/specs/encryption-suites, but that requirement lives in the not-yet-archived change delta. Repoint the six abort anchors to openspec/changes/harden-vault-key-material-guards/specs/... so they resolve. - gate-16 (spec-coverage): add the missing @SPEC tags on VaultKeyProofService::issueChallenge/verify/signedMessage and on EmergencyAccessView's cancelRevoke. - gate-13 (modal-isolation): the revoke-confirmation NcDialog was written inline in EmergencyAccessView. Extract it to src/dialogs/EmergencyRevokeDialog.vue per ADR-004. The guard state (target id, busy flag, refusal message) stays with the view; the dialog is presentational and passes the entered master password back through its confirm event. Assisted-by: ClaudeCode:claude-opus-5
…ke dialog (#673) vue/attributes-order requires the two-way binding to precede plain prop bindings; the extracted EmergencyRevokeDialog had :open first, failing the lint-check and Vue Quality (eslint) CI jobs. Reorder only — no behaviour change. Assisted-by: ClaudeCode:claude-opus-5
Assisted-by: ClaudeCode:claude-opus-5
…on (#674) Backend of migrate-emergency-access-on-rotation, tasks 1.1–1.6 and 4.1–4.2. A compromise-recovery rotation used to invalidate every emergency-access recovery envelope. But the owner holds the new private key mid-rotation and can fetch the grantee's certificate, so the envelope can be MIGRATED, not destroyed: the browser mints a fresh envelope escrowing the new key and posts it here. - New endpoint POST /api/v1/migrations/{id}/emergency-contacts/{contactId} (MigrationController::reEnvelopeEmergencyContact), owner- and old-suite-scoped through the same requireOwnMigration guard as the other migration writes, and deliberately NOT routed through commitRecord: emergency contacts are outside the completion gate (design D2), so a contact the browser cannot carry is left on the old suite for the sweep, never recorded as a gate-blocking failure. - EmergencyEnvelopeInvalidationService::reEnvelopeForRotation re-points the contact to the new suite, keeps it `granted`, clears any invalidated reason, and audits a (re-)grant. The grantor cannot open the envelope, so it is shape-checked (parses, v/alg, non-empty ciphertext fields) and the declared grantee suite is asserted to be the grantee's CURRENT active suite — an envelope sealed to a stale grantee key would be unopenable. - invalidateForGrantorRotation is now documented at its call site as a residual SWEEP: it finds only the contacts the loop could not carry (unreachable grantee), because migrated ones no longer sit on the old suite. Tests: the re-point service (re-point + granted + reason-cleared + audit; foreign grantor / wrong suite / missing contact / malformed envelope / suite mismatch / grantee without an active suite; residual sweep touches only old-suite rows) and the controller endpoint (success, 400 missing params, 409 terminated, 403/404/400 exception mapping). Existing MigrationController / EmergencyAccessService tests updated for the new constructor dependency. Assisted-by: ClaudeCode:claude-opus-5
…#674) Backend of the destructive-revocation safeguard, tasks 4b.1–4b.2. Revoking a user suite deletes its emergency-access recovery envelopes outright (the revocation listener runs clearForGrantorRevocation), and revocation is the last-resort route for an owner who lost their master password — exactly the owner most likely to still need their emergency contact. Today that deletion is silent. - EncryptionSuiteController::revoke gains an acceptEmergencyLoss flag. While a usable (non-invalidated) emergency contact exists and the flag is not set, revocation is refused with 409 and the COUNT of usable contacts — never their identities, which stay grantor-private. The guard sits before revokeSuite, because the envelope clear happens asynchronously in the revocation listener downstream of the event that call dispatches; gating any later would be too late. - EmergencyEnvelopeInvalidationService::countUsableForGrantorSuite counts the non-invalidated contacts bound to the suite. Tests: refusal returns the count and never reaches the service nor discloses identities; the override proceeds and clears; the no-contact case revokes unchanged; the count excludes invalidated contacts. Existing EncryptionSuite- Controller tests updated for the new constructor dependency. Assisted-by: ClaudeCode:claude-opus-5
…oke (#674) Frontend of migrate-emergency-access-on-rotation, tasks 2.x / 3.x / 4.3 / 4.5 / 4b.3, plus the residual-surfacing and revoke-safeguard UI. - initiateCompromiseRecovery now re-envelopes emergency contacts BEFORE completion (migrateEmergencyContacts): for each non-invalidated contact the browser fetches the grantee's current certificate, builds a fresh envelope escrowing the new private key, and posts it to the re-point endpoint. A grantee with no reachable certificate or a transient failure is collected as residual, never fatal — emergency contacts are outside the completion gate. The new private key PEM only ever leaves as envelope ciphertext (ADR-003). Resume cannot re-envelope (it holds only a non-extractable session key), so it keeps the pre-change invalidate-and-prompt fallback, as the design accepts. - CompromiseRecoveryForm surfaces the residual: it names exactly the contacts that could not be carried and prompts re-establishment, and shows nothing when every contact migrated. - The suite-revoke UI (App.vue) now carries the destructive-revocation safeguard: revokeSuite sends acceptEmergencyLoss; on the server's 409 emergency_access_present refusal the UI shows the count and the retrieve-first warning, and the confirm button escalates to an explicit "Revoke and delete emergency access". - Eight new UI strings seeded into en.json and all 36 locales (English placeholders); l10n/*.js catalogues rebuilt. ARCHITECTURE.md documents emergency contacts as a migrated store and invalidateForGrantorRotation as a residual sweep. Tests: the re-envelope loop (reachable → post + no residual; unreachable → residual + no post; invalidated skipped; per-contact failure isolated; index failure safe), the residual prompt, and the revoke store contract (flag carried, 409 refusal propagated without evicting the cache). Assisted-by: ClaudeCode:claude-opus-5
Backend, frontend, surfacing, revoke safeguard, l10n, gates and docs are done and verified. 4.4 (cross-impl sanity) is substantially covered by the service test's JS-shaped envelope; 4.6 (two rotations) composes by construction — both noted as optional follow-ups. 5.5 (PR description) is pending the human-opened PR. Assisted-by: ClaudeCode:claude-opus-5
…he test edits Two CI failures on the PR against development: - Frontend Check (format): the two test files I added cases to were not prettier-formatted (I checked the new files but not these edits). Reformatted; logic unchanged. - Hydra Gates gate-16: inserting cancelRevoke before handleRevoke pushed handleRevoke's docblock above cancelRevoke, so handleRevoke lost the docblock directly above it and gate-16 read it as missing @SPEC. Reordered so each method carries its own docblock. The remaining two gate-16 findings (compromiseRecovery, updatePrivateKey) are #673's guard-attributed methods: they enter scope only when the PR is diffed against development (not against feature/673), and a gate blind spot on the closing `)]` of a multi-line #[VaultKeyProofRequired(...)] attribute then misses their docblock. They pass when the PR is based on feature/673; the clean fix is to merge #673 into development first. Assisted-by: ClaudeCode:claude-opus-5
The 0.3.2 release bumped the version on main. Without this, development stays behind main and the next development -> main promotion conflicts on the version file. Version files resolve to development's side, which is the higher line, so this never moves a version backwards.
…60912202807 chore(release): 0.3.4-unstable.20260912202807
chore(release): sync main back into development
…260913184350 chore(sync): carry beta back into development
Wilco's Strict-review blocker #2: suites/{id}/revoke was the one destructive route outside the guard — session-only, hard-deletes ShareTargets, promotes delegations, blocks every secret read, and reinstate is admin-only, so a stolen cookie could inflict the exact #395 lockout this change exists to close. - New purpose VaultKeyProofService::PURPOSE_REVOKE_SUITE + PROOF_PURPOSE.REVOKE_SUITE. - EncryptionSuiteController::revoke gains #[VaultKeyProofRequired(binds: ['reason'], subject: 'routeParam:id', purpose: PURPOSE_REVOKE_SUITE)] — the verifying key is resolved from the suite being revoked, so a re-aimed id breaks the signature, and the reason is bound so a captured proof can't be replayed against another request. Added to VaultKeyProofAttributesTest so a future drop of the attribute fails CI. - Frontend: revokeSuite(reason, masterPassword) signs the proof and attaches the headers; the revoke confirmation now asks for the master password (which signs and is never sent). A stolen session, lacking the master password, can no longer revoke. An owner who has LOST the password uses the separate admin recovery path (to be designed in its own PR), never this one. Both @SPEC tags kept on the touched methods (retrofit + the new vault-key-proof requirement): a deleted @SPEC would trip gate-16's whole-file re-evaluation, which mis-reads the multi-line #[VaultKeyProofRequired] attribute on the other guarded methods (the checker bug noted on #678) — kept additive to avoid it. Assisted-by: ClaudeCode:claude-opus-5
…#673) Wilco's Strict-review blocker #1: MigrationController::complete bound only `id`, leaving `hasErrors` and `acceptUnrecoverable` unbound and re-aimable within the 300s window — and the 409 body hands the caller the exact required count. A proof captured on a clean completion could be replayed to finalise the migration with an unacknowledged permanent loss, contradicting ARCHITECTURE.md's "a replay only ever re-authorises the byte-identical operation". - complete now binds ['id', 'hasErrors', 'acceptUnrecoverable']; attributes test updated so a future narrowing fails CI. - Client: a boundParam() helper serialises each value exactly as the middleware's (string) cast does — true → '1', false/null/absent → '', else String(value) — and the three completion-proof sites (initiate, resume, acceptMigrationLosses) bind [id, hasErrors, acceptUnrecoverable] accordingly. acceptMigrationLosses, the acknowledgement retry, now commits to the accepted count, so each distinct acknowledgement needs its own proof — closing the replay. - Store test pins acceptMigrationLosses' bound values; ARCHITECTURE.md's guard table updated (complete's full binds, plus the revoke row). Live 403/replay verification remains the same deferred gap as the rest of the guard (no running-pipeline test), noted on #677. Assisted-by: ClaudeCode:claude-opus-5
…rden-vault-key-material-guards # Conflicts: # lib/Controller/EncryptionSuiteController.php
Replace the `{*}` jsdoc type on the completion-proof serialization helper with
the concrete union it actually takes, clearing the one new
jsdoc/reject-any-type warning the blocker-1 change introduced.
Assisted-by: ClaudeCode:claude-opus-5
Wilco's #678 blocker: abort's only gate, countCommitted(), summed secrets + versions + grants but not emergency contacts. A run aborted before the first secret commits (an empty or low-secret vault) therefore passed the gate at 0 even after contacts had been re-enveloped onto the new suite — abort then deleted that suite, stranding every re-enveloped contact on a deleted suite with an envelope escrowing a discarded key. The residual sweep queries the OLD suite so never sees them, and a later revoke counts 0, telling the owner emergency access is fine when it is dead. Exactly the failure this change exists to eliminate, reintroduced through abort. Fix per Wilco's first option (abort refuses, consistent with "nothing may have moved yet"): countCommitted now adds countUsableForGrantorSuite(newSuiteId) — the re-enveloped contacts now bound to the new suite. His alternative (restore the old suite/envelope in the abort listener) cannot work: the re-envelope overwrote the old envelope, so there is nothing to restore. MigrationWorkService gains the EmergencyEnvelopeInvalidationService dependency; a store test pins that a re-enveloped contact makes countCommitted > 0 with no secret moved. Assisted-by: ClaudeCode:claude-opus-5
…erial-guards' into feature/674/migrate-emergency-access-on-rotation # Conflicts: # lib/Controller/EncryptionSuiteController.php # src/App.vue # src/store/modules/encryptionSuite.js # tests/Unit/Controller/EncryptionSuiteControllerTest.php # tests/store/encryptionSuite.spec.js
Wilco's re-review blocker: PURPOSE_REVOKE_SUITE carried the guard attribute but was never added to ALLOWED_PURPOSES, so proofChallenge answered 400 for it, no proof could be built, and the middleware refused every revoke — the guard was present but inert, turning revoke from "reachable by any session" into "reachable by nobody" (the self-service "my laptop was stolen, lock my vault" control). Also close the class of mistake that let 30 green checks miss it: VaultKeyProof- AttributesTest now cross-checks every guarded method's purpose against ALLOWED_PURPOSES. And the standing 🟢: VaultKeyProofService::mac throws when the instance secret is unset instead of degrading to an empty HMAC key. Assisted-by: ClaudeCode:claude-opus-5
…erial-guards' into feature/674/migrate-emergency-access-on-rotation
- 🟡 re-point no longer forces state=granted: reEnvelopeForRotation now PRESERVES the contact's lifecycle state, so an in-flight (requested) or approved break-glass is carried across the rotation rather than silently vetoed, and the carry is audited as a grant only when the result is actually granted (a previously-invalidated contact still re-establishes to granted). Test added. - 🟡 the six re-point refusal tests now assert no row was written (mapper::update expects never), so a refactor that writes before throwing turns them red instead of staying green. - 🟡 the residual list is filtered to this rotation's old suite: EmergencyContact now exposes grantorSuiteId, and migrateEmergencyContacts skips any contact whose grantorSuiteId != the rotating old suite — a contact stranded on a prior suite is no longer posted (the server would refuse it) and mislabelled as lost in this rotation. Test added. Assisted-by: ClaudeCode:claude-opus-5
…e-16 The gate-16 spec-coverage checker associates a docblock with a method by walking up from the declaration, skipping lines that start with `#[` but NOT the `)]` that closes a multi-line PHP attribute. So once revoke()'s body enters a diff, its multi-line #[VaultKeyProofRequired(...)] hid the method's three @SPEC tags and the gate reported a false "missing @SPEC". Collapsing the attribute to one line (147 cols, under the 150 limit) keeps the guard's @SPEC visible to the gate with no behavioural change; the reflection test still reads the same binds/subject/purpose. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y-material-guards feat(vault-key-proof): require a master-password proof for destructive vault operations (#673)
…grate-emergency-access-on-rotation
…cy-access-on-rotation feat(emergency-access): migrate recovery envelopes on suite rotation instead of destroying them (#674)
…-005
ADR-005 and the OpenSpec change artifacts (proposal, design, encryption-
suites spec delta, tasks, plan.json) for administrator suite force-revoke:
an admin-only, sudo-guarded POST /api/v1/suites/{id}/force-revoke that
revokes any suite by id (user- or application-owned), with a transient
markCompromised compromise cascade and unconditional emergency-access
clearing (count audited, not gated). Reuses revoked_reason; no schema
migration, no version bump. Temporary/vacation suspension is deferred to
a future change (design Non-Goals).
Tracking: #702
Assisted-by: ClaudeCode:claude-opus-4-8
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cade
POST /api/v1/suites/{id}/force-revoke — an admin-only, sudo-guarded
(#[AuthorizedAdminSetting] + #[PasswordConfirmationRequired]) endpoint that
revokes any suite by id (user- or application-owned) without a vault-key
proof: the admin counterpart to the owner revoke() guard. A required
free-form reason is stored in the existing revoked_reason column; a
transient markCompromised flag on EncryptionSuiteRevokedEvent drives a new
SuiteCompromiseOnRevokeListener (stamps each secret under the suite
possibly_compromised_at, raises suite_compromise rotation flags, notifies
affected owners). Emergency access is cleared unconditionally with the
destroyed-usable count recorded in SUITE_REVOKED audit metadata and
returned (not gated). No schema migration, no version bump.
Tasks 1.1-2.4, 4.1-4.5 of #702.
Assisted-by: ClaudeCode:claude-opus-4-8
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an admin-only "Encryption suites" settings section (AdminSuiteSection.vue, wired into Settings.vue) to force-revoke a suite by id with a required reason and a "mark compromised" toggle, running the Nextcloud sudo (password-confirmation) flow before the request. Surfaces the returned emergencyContactsDestroyed count and, when compromise is off, the "user may still know these secrets" warning; plus a reinstate action for revoked suites. The store gains forceRevokeSuite (sudo-before-POST) and reinstateSuiteAdmin. Tasks 3.1-3.4, 4.6 of #702. Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All 19 tasks implemented and verified (phpcs/phpmd/phpunit, eslint/ prettier/stylelint/vitest, hydra-gates). Tracking: #702. Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the owner revoke() path — return 401 when the session has no user rather than calling getUID() on null. Defensive: the AuthorizedAdminSetting guard already guarantees an authenticated user, but this matches the owner-path convention. Code-review follow-up on #702. Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ess waivers (#704) The waiver deferred a Playwright run because 'the worktree is not deployed and deploying to the shared dev instance is prohibited'. That reason never held: the E2E job provisions its own Nextcloud, seeded by tests/e2e/ci-seed.sh. The waiver now says what it actually covers (the server-side state machine) and that the routed, testid-hooked DOM flow is uncovered, not excluded. Ported from the unmerged wip/emergency-access-waiver-excuse (b1a66c1); its sibling commit already landed as #207.
…l locales Add the new admin "Encryption suites" UI strings to every required locale (real translations; English fallback for rm/lb/ga/mt) and regenerate the locale .js catalogues, satisfying the test:l10n cross-locale parity gate. No source or behaviour change; existing translations untouched (additions and duplicate-key dedup only). Tracking: #702 Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… integriq's registry (#706) * docs(openspec): adopt the connection registry for breach checks and SIEM export * feat(connections): declare the breach check and SIEM export for integriq * feat(connections): an Integrations page over integriq's connection registry * feat(connections): report breach lookups and SIEM drains to integriq, refresh on save * test(connections): integriq event stubs for PHPUnit, psalm and phpstan * test(connections): cover the reports, the refresh order, the throttle and the privacy rule * test(connections): an e2e spec for the Integrations page, and integriq on the CI instance * test(connections): named arguments and no inline ifs in the connection tests * refactor(connections): build SettingsController in its own factory, keeping the registrar under the coupling limit
Ruben's Transifex translation sync (on development) and this branch's new admin-suite-revocation strings both touched all 74 l10n files. Resolved by taking development's translations as the base and re-deriving the 11 new admin strings on top (en.json re-extracted, translations re-applied to every locale, .js catalogues rebuilt). No code conflicts. Both l10n gates (check:l10n-js, test:l10n parity) pass. Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ConnectionsDeclarationTest read appinfo/info.xml with simplexml_load_file(), which returns false under the Nextcloud bootstrap because lib/base.php nulls libxml's external entity loader. It now parses the contents as a string, as NextcloudFloorMatrixTest does. The Integrations page e2e test looked a row up by an accessible name starting with the connection title, but each row's name starts with its Select row checkbox. It now matches the row by its Connection cell.
keepiq's phpunit.xml sets beStrictAboutCoverageMetadata and failOnRisky, and CI collects coverage, so a test that runs a class it neither covers nor uses is risky and fails the cell. Twelve tests from #706 ran ConnectionReporter, ConnectionObservations, SiemAuditTrail, SiemSink and SiemQueueItem without declaring them. Verified green on run 35014336755.
…tters come from nextcloud-vue 3.2.0 (#721) * feat(connections): the breach check reads disabled through its switch, and so do SIEM sinks that are all off hibp declares switch on breach_check_enabled in place of requiredConfig, so a switched-off check reads disabled and a switched-on one reads Not checked yet until a lookup reports (hydra connection-registry D4 rule 2b, D12 item 9). SIEM stays reportedOnly: when no sink is on and sinks exist, keepiq reports disabled with a host-free message; with no sink at all it still reports unconfigured. * chore(deps): @conduction/nextcloud-vue 2.41.1 -> 3.2.0, and the connection formatters come from the library 3.2.0 ships connectionStatus, with disabled read as Switched off, and connectionSettingsLabel as built-ins (nextcloud-vue#1173, #1175). The local copy in src/services/connectionRegistry.js and App.vue's formatters prop are gone; the handler stays. The spec now checks the manifest's formatter names against the installed library's built-in map. * chore(deps): regenerate the lockfile without --legacy-peer-deps The first regeneration ran with --legacy-peer-deps, which dropped every peer entry (84). A plain npm install resolves the same bump with peers kept: against development only @conduction/nextcloud-vue (2.41.1 -> 3.2.0) moves at the top level, and one Dexie (4.4.5) stays in the tree. * test(connections): declare the built-in formatter reader with the function keyword antfu/top-level-function refuses a top-level arrow function. * test(connections): wrap a reporter assertion past the 150-character limit * test(connections): import the built-in formatters and call them, instead of reading the module source
…min-suite-revocation
…ocation feat(encryption-suites): administrator suite force-revocation with compromise cascade (#702)
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.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.