Release: merge development into beta - #126
Merged
Merged
Conversation
Comment on lines
+19
to
+33
| runs-on: ubuntu-latest | ||
| # No observed runs yet; a Go vet+test of a stdlib-only CLI is minutes at most. | ||
| timeout-minutes: 20 | ||
| defaults: | ||
| run: | ||
| working-directory: cli | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-go@v5 | ||
| with: | ||
| go-version: "1.22" | ||
| - run: go vet ./... | ||
| - run: go test ./... | ||
|
|
||
| build: |
Comment on lines
+34
to
+74
| needs: test | ||
| runs-on: ubuntu-latest | ||
| # No observed runs yet; a stdlib-only Go cross-compile is minutes at most. | ||
| timeout-minutes: 20 | ||
| defaults: | ||
| run: | ||
| working-directory: cli | ||
| strategy: | ||
| matrix: | ||
| target: | ||
| - { goos: linux, goarch: amd64 } | ||
| - { goos: linux, goarch: arm64 } | ||
| - { goos: darwin, goarch: amd64 } | ||
| - { goos: darwin, goarch: arm64 } | ||
| - { goos: windows, goarch: amd64 } | ||
| - { goos: windows, goarch: arm64 } | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-go@v5 | ||
| with: | ||
| go-version: "1.22" | ||
| - name: Build static binary | ||
| env: | ||
| GOOS: ${{ matrix.target.goos }} | ||
| GOARCH: ${{ matrix.target.goarch }} | ||
| CGO_ENABLED: "0" | ||
| run: | | ||
| ext="" | ||
| [ "$GOOS" = "windows" ] && ext=".exe" | ||
| out="doriath-${GOOS}-${GOARCH}${ext}" | ||
| go build -trimpath -ldflags "-s -w -X main.version=${GITHUB_REF_NAME}" -o "$out" . | ||
| echo "ARTIFACT=cli/$out" >> "$GITHUB_ENV" | ||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: doriath-${{ matrix.target.goos }}-${{ matrix.target.goarch }} | ||
| path: ${{ env.ARTIFACT }} | ||
| - name: Attach to release | ||
| if: startsWith(github.ref, 'refs/tags/cli-v') | ||
| uses: softprops/action-gh-release@v2 | ||
| with: | ||
| files: ${{ env.ARTIFACT }} |
Two files still declared `SPDX-License-Identifier: AGPL-3.0-or-later` while every other licence signal in the repo (LICENSE, composer.json, package.json, appinfo/info.xml and all 241 lib/**.php @license tags) says EUPL-1.2. - tests/e2e/visual/_visual-helpers.ts - tests/integration/run-newman.sh Header-only change; PHP (710 tests) and vitest (425 tests) suites are byte-identical before and after. gate-28 license-triangle: PASS -> PASS.
…ation-2026-08-05 chore(license): normalise 2 stray AGPL-3.0 SPDX declarations to EUPL-1.2
…its path (#162) * fix(apphost): register OpenRegister's autoloader instead of guessing its path Apps register in sorted order (OC_App::getEnabledApps() sort()s the list, and Coordinator::registerApps() calls registerAutoloading() then register() one app at a time), so doriath's register() runs before OCA\OpenRegister\ is autoloadable. The previous workaround include_once'd ../../../openregister/vendor/autoload.php, which assumes both apps share one apps directory and silently does nothing on a multi-apps_paths install. Resolve the path through IAppManager::getAppPath() and hand it to OC_App::registerAutoloading(), which touches only the autoloader and is idempotent. Deliberately NOT IAppManager::loadApp(), which would mark OpenRegister loaded and boot it before its own register() had run. The class_exists(Bootstrap::class) guard now wraps the call so an absent OpenRegister degrades instead of aborting the rest of register(). * fix(apphost): make the autoload prelude testable and teach the analysers OC_App CI found three real problems with the first version of this change: - psalm and phpstan both fail on \OC_App: it is Nextcloud's legacy bootstrap class, server-private and absent from nextcloud/ocp. There is no OCP interface for registering another app's autoloader. Added to the same ignore lists that already carry OC, OCA\OpenRegister\, Doctrine and Guzzle. - the coverage ratchet dropped 55.78% -> 55.74%, because an inline prelude in Application::register() cannot be reached by a unit test — Application needs a Nextcloud DI container to construct. Moving the prelude into AppInfo\OpenRegisterAutoloader fixes the second properly rather than by lowering the baseline: the contract that actually matters — this NEVER throws, so it can never abort the caller's register(), which is the very defect the prelude exists to prevent — is now directly asserted, along with its idempotence. Verified in the container (PHP 8.4): phpstan OK, psalm no errors, PHPCS clean, PHPMD clean on the new file, and both new tests pass. * fix(spec): point the prelude's @SPEC anchor at doriath's own spec gate-46 (spec-anchor-existence) resolves @SPEC targets inside THIS repo. The anchor named openspec/specs/apphost-boilerplate/spec.md, which is OpenRegister's canonical spec file and does not exist here, so it could never resolve. It also used a ' — Requirement: ...' suffix that no other @SPEC in this repo uses. Points at doriath's own openspec/specs/apphost-adoption/spec.md instead.
…ne (#164) The standing 'Release: merge development into beta' PR has head_ref 'development', so its pull_request run rendered the same concurrency group as a push to development. cancel-in-progress killed the push run, which is the only carrier of the push-only jobs (Coverage Baseline Check, SBOM, Features Extract). Those jobs report 'skipped' on the surviving PR run, which renders like a pass, so the gate never produced a verdict. Suffixes -push on the group for main/development pushes only; feature-branch dedup is unchanged. No gate weakened: no waiver, baseline, threshold or continue-on-error. Same fix as openconnector#1158.
…#166) .coverage-baseline was read as a floor by the phpunit guard and as an exact target by the push-side staleness check. Together they demand equality with a checked-in constant, which against a moving base branch is not satisfiable: closing "stale" means committing the value the tree will measure after the PR lands. Measured on openregister — committed 58.93, development advanced 16030->16038 tests, merge result measured 58.88, guard reported a 0.05% drop. coverage-guard.php gains --against=<clover.xml>, naming a report measured at the merge base. When present it is the only floor; the committed constant is reported but not enforced. Both numbers then come from one driver in one job, so the xdebug/pcov statement-counting difference cancels rather than being baked in, and the merge base cannot go stale. Ratios are compared as exact integer cross-products, not rounded percentages: at two decimals a one-statement regression read as "unchanged" and exited 0. An empty or zero-statement report is now a hard error rather than 0%, which as the merge-base side would set the floor to zero and pass every drop. Verified on real CI clover artifacts: a genuine 1.44% drop fails, an unchanged tree passes, and adding untested code fails while adding tested code passes.
…bles, not OpenRegister Written 2026-07-27 and left uncommitted since. Recording the decision where it belongs rather than leaving it as an untracked file on one machine.
* chore(ci): move hydra-gates-ref v1.3.0 -> v1.4.0 A pinned `hydra-gates-ref` is a silent expiry date on every upstream fix: this repo cannot receive a gate-package change until this line moves. v1.4.0 is the latest tag and the first one that carries `hydra-gates/scripts/axe-run.cjs` (verified absent at v1.3.0), so it is also the first that has ConductionNL/.github#168 axe DOM scoping and ConductionNL/.github#165 gate-46 fix. `enable-axe` is deliberately NOT enabled in this commit. Ordering matters: the ref lands first, enabling axe is a separate decision. * chore(ci): stop pinning hydra-gates — track the package at @main Removes the `hydra-gates-ref` input from the `quality.yml` caller. The shared workflow already defaults it to `main`, and this repo consumes `quality.yml` itself at `@main`, so dropping the override makes both sides move together: a gate-package fix lands here without a commit here. A pin is a silent expiry date on every upstream fix, and we have paid for that twice already: - .github#159 — 22 repos sat on v1.0.1, which predated the gate fixes. 16 gates were dead fleet-wide and every single one reported PASS. A gate that never runs emits a tick identical to one that did, so nothing in any repo's history showed it. - .github#173 — the shared side flipped a default at @main while the package stayed pinned per caller. Old runners lacked the coverage accounting the new default assumed, so they went red on gates they had no subject matter for. Removing the pin closes both shapes at once. Rolling back is a revert on ConductionNL/.github main, which reaches the whole fleet in one commit; holding this one repo still is still possible by setting the input explicitly, with a reason. `enable-hydra-gates: true` is unchanged. `enable-axe` remains unset. The comment block that justified the pin is replaced with a short note saying why there is no pin.
…ommand injection) (#170) quality / Security (composer) is red on every PR here as of today: Advisory ID: PKSA-rdkp-vv9z-mjkg CVE: CVE-2026-67434 — OS Command injection Affected versions: <3.13.6|>=4.0.0,<4.0.2 Reported at: 2026-08-05T23:53:11+00:00 The advisory was published YESTERDAY and roave/security-advisories installs as dev-latest each run, so the same lockfile was clean on 2026-08-05 and is vulnerable on 2026-08-06 with no commit in between. The last green run is evidence of when it ran, not that the lockfile is safe. composer.json's existing constraint already permits the fixed version, so this is a lockfile move only: 1 update, 0 installs, 0 removals. Verified the diff touches exactly two lines, both the version string, and no other file. Part of a fleet sweep — 13 of 16 repos checked were on the affected 3.13.5.
…ontract failures (#171) * fix(tests): remove unbounded RSA-4096 keygen from the timed unit-test path `Frontend Tests (unit)` failed on `development` (run 31083918823): tests/vitest/emergencyEnvelope.spec.js > emergency recovery envelope > builds an envelope the grantee can open, recovering the grantor key Error: Test timed out in 5000ms. The test does not hang. It calls a real WebCrypto RSA-4096 `generateKeyPair()`, which is a random prime search with unbounded runtime — measured here at 105-681ms over 10 samples on an idle machine, and several times that on a contended two-core runner executing 69 spec files across parallel workers. Against vitest's 5000ms per-test default that is a coin flip, and the same construct already lost it once before in run 30884131373 (`tests/store/import.spec.js`). Caching the generated pair per file (#148) removed the repeats but left the FIRST keygen of every file on the timed path, which is the flake that has now surfaced in a second spec. Fix: the fixtures module ships two committed RSA-4096 key pairs (PKCS#8 + SPKI, generated out-of-band with openssl) and imports them with `crypto.subtle.importKey`. Import is bounded and sub-millisecond, so the variance is gone outright. Nothing about the code under test changes: the keys are genuine 4096-bit RSA with 512-byte OAEP blocks, so `src/crypto/rsa.js`'s chunking framing is exercised exactly as before and every assertion still runs real RSA-OAEP. These specs assert round-trip behaviour, never key freshness. Where key *generation* is itself the subject, the spec still generates: `rsa.spec.js` keeps `generateKeyPair emits a valid SPKI PEM re-importable by importPublicKey` calling `generateKeyPair()` directly, so that path stays covered. Net keygens on a 5000ms path: 8 -> 1. Converted alongside, same defect class: - tests/extension/crypto.spec.js — 4 inline 4096-bit keygens, one per test - tests/store/import.spec.js — the residual first-call keygen - tests/vitest/attachment-crypto.spec.js — one keygen per beforeEach Proof it can fail, and that the fix is what stops it — identical 42-way CPU contention (3x oversubscription on 14 cores), same command: before: Tests 1 failed | 3 passed (4) Error: Test timed out in 5000ms. after: Tests 17 passed (17) EXIT=0 Full suite, identically conditioned, before -> after: 69 files / 425 tests passed -> 69 files / 425 tests passed test time 31.80s -> 18.59s Coverage ratchet: 53.32% >= baseline 18.31%, unchanged by this commit. No timeout was raised, no assertion weakened, no test skipped. * fix(api): bind the RFC 7523 snake_case grant_type; provision the suite Newman needs `Integration Tests (Newman)` has failed every run since the gate was enabled on 2026-08-05 (before that it was `skipped`, so these assertions had never once been checked against a live app). 19 assertion failures, two independent causes. Both are fixed at the cause; no assertion was weakened. CAUSE 1 — a real product bug: no client can obtain a machine token ------------------------------------------------------------------ `ApplicationTokenController::exchange(string $grantType = '')` relied on Nextcloud's dispatcher to bind the request parameter, and the dispatcher binds by EXACT name. The wire name is the snake_case `grant_type` — RFC 7523 §2.1, and what this app's own `.well-known/doriath` discovery document tells every client to send. It never bound. `$grantType` was ALWAYS the empty-string default, so the endpoint answered `400 unsupported_grant_type` to every request, including well-formed ones. The bug hid behind a test that passed: 'Unsupported grant type -> 400' expects `unsupported_grant_type`, which is what a broken endpoint returns for ANY input, so it went green while the endpoint was completely unusable. Measured on the CI-equivalent replica (NC 32.0.12 + doriath), same three requests, `origin/development` code vs this commit: request before after grant_type=password unsupported_gt unsupported_gt grant_type=jwt-bearer (no assertion) unsupported_gt invalid_request grant_type=jwt-bearer, assertion=not.a.jwt 400 unsupported 401 invalid_grant The fix reads the canonical wire name off the request and keeps the camelCase argument as a fallback, so callers written against either spelling work. CAUSE 2 — the collection assumed ambient vault state it never created --------------------------------------------------------------------- The other 17 failures are all secret writes. The collection's own description said the CRUD round-trip is drivable 'because the dev vault already has an unlocked suite'. True against a long-lived dev instance; the CI job installs a FRESH Nextcloud where admin has no encryption suite at all, so every write was refused by SecretService::getActiveSuiteOrBlock(). The refusal is invisible from the status line. These are OCSController routes, and Nextcloud's OCSMiddleware rewrites a 403 from an OCSController into an OCS v1 envelope — served as HTTP **200** with the real code in `ocs.meta.statuscode`. Measured against a suite-less user: POST /api/v1/secrets -> HTTP 200, 181 bytes {"ocs":{"meta":{"status":"failure","statuscode":403, "message":"No active encryption suite — it may be revoked or not yet created"},"data":[]}} which is exactly the '200 OK, 864B' run 31083918823 recorded, and why `b.id` was undefined and the follow-up requests addressed `/secrets/null`. The collection now provisions its own precondition instead of inheriting it: 'SETUP: ensure the caller has an active encryption suite' POSTs a real, committed test RSA-4096 public key (it must be real — CaService::signPublicKey parses it and mints a certificate; encryptedPrivateKey is opaque client-side ciphertext, so a placeholder is honest there). It runs ONLY when the caller has no active suite: two active suites are as unusable as zero, because getActiveSuiteOrBlock() resolves exactly one and blocks with the same 403 otherwise — verified, and the reason the request is conditional rather than unconditional. Verification — full runner against a CI-equivalent replica with admin's suite revoked first, i.e. the exact fresh-instance condition: doriath collection 85 assertions / 17 failed -> 95 assertions / 0 failed machine collection 26 assertions / 2 failed -> 26 assertions / 0 failed run-newman.sh EXIT=1 -> EXIT=0 Re-run on the same instance (suite now present) skips the SETUP request and still exits 0 at 91 assertions — the idempotency the collection advertises. phpunit 712 tests / 2287 assertions before and after, identical. phpmd unchanged at 35 findings, byte-identical finding set. phpcs clean on the changed controller. KNOWN, NOT FIXED HERE: POST /api/v1/suites will happily mint a SECOND active suite for a user, which bricks every subsequent secret write with the same masked 403. Filed as a follow-up rather than fixed silently in a CI PR. * fix(security): squizlabs/php_codesniffer 3.13.5 -> 3.13.6 (CVE-2026-67434) `Security (composer)` went red on 2026-08-06 with: Found 1 security vulnerability advisory affecting 1 package: Package: squizlabs/php_codesniffer Advisory ID: PKSA-rdkp-vv9z-mjkg CVE: CVE-2026-67434 Packagist's advisory API is the authority here: affected: <3.13.6 | >=4.0.0,<4.0.2 reported: 2026-08-05 23:53:11 so 3.13.6 is exactly the fixed release, and this is a lockfile-only move inside the existing `^3.9` constraint — 7 insertions, 7 deletions, one package. phpcs re-run at 3.13.6: 242/242 files, EXIT=0. WHY THIS SITS IN THIS PR RATHER THAN ITS OWN The shared quality workflow gates the heavy jobs on the security job: if: ... && needs.security.result != 'failure' (quality.yml:1464, :1720) so while `Security (composer)` is red, PHPUnit, E2E and **Newman** are all SKIPPED. The Newman fix in the previous commit could not be verified by CI at all until this is green — the skip is a cascade, not a verdict.⚠️ A LOCAL `composer audit` IS NOT A CONTROL FOR THIS. Run against the OLD lock (3.13.5) in the same PHP 8.4 container it reported "No security vulnerability advisories found", EXIT=0 — a false negative from a stale local advisory cache, which looks exactly like a fixed repository. The positive control that actually discriminates is the Packagist advisories API (above), and CI itself. * test(token): pin the grant_type binding regression The coverage ratchet caught that the previous commit adds 5 statements to `lib/` with nothing exercising them: Coverage current: 55.77% (6842/12269 statements) Coverage merge base: 55.79% (6842/12264 statements) FAIL: coverage dropped by 0.02% against the merge base. It is right to complain. A bug this quiet deserves a regression test more than most: the ONE negative test that existed asserted `unsupported_grant_type`, which is exactly what a wholly broken endpoint returns for ANY input, so it stayed green while no client could obtain a token at all. These tests pin the DISCRIMINATING cases instead — the snake_case spelling must be ACCEPTED, and a request well-formed apart from a missing/malformed assertion must fall through to `invalid_request` / `invalid_grant` rather than being rejected as an unsupported grant. A negative control (`grant_type=password` is still rejected) sits alongside so the suite cannot pass vacuously. Proof the tests are load-bearing — same 7 tests, controller swapped: against origin/development (buggy): Tests: 7, Failures: 4 testSnakeCaseGrantTypeIsAccepted testSnakeCaseGrantTypeReachesTheAssertionPath testCanonicalSpellingTakesPrecedenceOverTheFallback testValidAssertionReturnsTheTokenPayload against this branch (fixed): Tests: 7, Assertions: 14, OK Full suite 712 -> 719 tests, 2287 -> 2301 assertions, 0 failures. phpcs EXIT=0.
…-2026-67434) (#172) * chore(deps): clear all critical + high security advisories npm (measured on development with `npm audit --package-lock-only`; the Dependabot alert count is computed on the stale default branch `main` and is not the number that matters here): before critical 2 high 2 moderate 12 low 5 (total 21) after critical 0 high 0 moderate 10 low 5 (total 15) - vitest + @vitest/coverage-v8 ^1.6.1 -> ^3.2.7 (kept on the SAME version) Clears the CRITICAL "Vitest UI arbitrary file read/execute" (<=3.2.5) and drags vite 5.4.x -> 6.4.3, vite-node -> 3.2.4 and esbuild -> 0.25.12 forward, which clears the `vite` HIGH (<=6.4.2). Deliberately 3.2.7 and NOT vitest 4: 3.2.7 is already outside every advisory range and is one major less disruptive to the suite. - @cyclonedx/cyclonedx-npm ^4.2.1 -> ^6.0.0 Clears the HIGH "shell injection via unsanitised --workspace" (2.1.0-4.2.1). composer: - squizlabs/php_codesniffer 3.13.5 -> 3.13.6 (composer.lock only; the ^3.9 constraint in composer.json already allowed it). CVE-2026-67434 / GHSA-hmqg-cxww-wqhq, OS command injection, reported 2026-08-05, affects <3.13.6. This advisory landed after the last measurement of this repo, so `composer audit` was clean yesterday and is not clean today. NOT bumped, on purpose: the 10 remaining moderate and 5 low npm advisories all sit in transitive webpack/eslint tooling with no fix that does not force a major on a build-critical package. They are not in the critical/high scope of this sweep. * test(vitest): give the WebCrypto specs timeout headroom on CI `quality / Frontend Tests (unit)` failed on this branch with "Test timed out in 5000ms" — vitest's default `testTimeout`. This is NOT a broken test, and it is not purely the vitest bump either. Both halves matter: 1. The headroom was ALREADY gone at vitest 1.6.1. Run 31083918823 on `development` failed `emergencyEnvelope.spec.js > builds an envelope the grantee can open` with the byte-identical 5000ms timeout, while the runs either side of it passed. 2. The 1.6.1 -> 3.2.7 bump then removed what little was left. Measured on the CI runner, same job, same repo: vitest 1.6.1 vitest 3.2.7 attachment-crypto.spec 2604ms 5391ms import.spec 3937ms TIMED OUT total `tests` time 29.08s 35.66s Likely mechanism: vitest 2 changed the default worker pool from `threads` to `forks` — more per-worker overhead and more contention for two cores on CPU-bound crypto. The tell that this is a marginal timeout and not one bad test is that WHICH spec tips over MOVES between runs: attempt 1 failed emergencyEnvelope, the rerun failed import.spec and let emergencyEnvelope through at 1943ms. These specs do real WebCrypto (RSA keygen, envelope wrap/unwrap, backup round-trips) instead of mocking it, which is the right call for a secrets manager — so the fix is headroom, not weaker tests. 20s is well clear of the slowest observed spec while still failing fast on a genuine hang; the whole suite runs in ~30s. No assertion changes. Positive control that the setting is actually wired, not just present: a probe spec awaiting 8000ms was run twice against this same config — PASSES with `testTimeout: 20000` (rc 0), FAILS with a CLI `--testTimeout=5000` override (rc 1, "Test timed out in 5000ms"). The two arms differ only in the timeout, so the value is demonstrably what controls the outcome. Probe deleted afterwards. Local re-verification: test:unit rc 0 (69 files / 425 tests), test:coverage rc 0 (lines 53.79%), coverage-ratchet rc 0, lint rc 0 (0 errors).
…es (#174) axe-core sat in `dependencies`, declaring an accessibility *testing* library as an application runtime dependency. Measured: nothing under src/ imports axe-core, and the built production bundle does not contain it — 0 hits for `axe-core`, `axe.run` and axe own signature strings (`aria-allowed-attr`, `color-contrast`) across js/, while those strings are present in node_modules/axe-core/axe.min.js (positive control). What put it in every app manifest is @conduction/nextcloud-vue, which declares axe-core as an OPTIONAL peerDependency. nc-vue does use it, but only in `src/testing/a11y.js` — a testing helper never imported from `src/index.js`, so it never reaches an app bundle. nc-vue own file header states axe-core "is a devDependency" and that consumers wanting the a11y assertion "add `axe-core` to their OWN devDependencies". This change follows that instruction. Not a bundle-size fix; the bundle is byte-identical. It stops a test dependency being declared as production surface (SBOM, `npm ci --omit=dev`, advisory triage). An optional peer is satisfied by a devDependency, so nothing breaks. Verified: npm ci + production webpack build both exit 0; CI Frontend Build, Frontend Tests, eslint, License (npm) and Security (npm) all pass. The phpmd and test:l10n failures are pre-existing on unrelated baseline branches.
…er had (#176) `quality / Frontend Check (test:l10n)` has been red on development: l10n-check: FAIL — 352 translation key(s) used in source but MISSING from l10n/en.json Every one is a string the UI already renders through the translate helper with no entry to translate it against, so it falls through as the raw key. Run with the extractor the check itself recommends: `node tests/l10n/check-l10n.js --write`. This is extraction, not translation. In this file the key IS the English source, so all 352 added entries have value === key — verified programmatically, along with zero changes to any pre-existing entry. The diff touches l10n/en.json and nothing else. Deliberately NOT touching the other locales. `test:l10n` is the only l10n script this repo runs in CI (`frontend-checks: ["check:manifest", "test:l10n"]`) and it checks source coverage only; parity across the translated locales lives in a separate `check-l10n-parity.js` that CI does not invoke. Populating 35 locales here would mean inventing translations I cannot verify — that belongs to the translation pipeline, not to this commit. Verified: 555 -> 907 keys, `test:l10n` exits 0.
The lock screen was reached via an App.vue created() redirect that ran after `await initializeStores()`. By the time it fired, vue-router had already resolved the route and CnPageRenderer had mounted the target page, so that page's mounted() had issued its fetches — SecretList calls fetchSecrets() unconditionally. Secret name/url/folder placement are plaintext server-side by design (searchable for their owner), so the vault inventory painted and hit the wire before the lock screen replaced it. Replace the lifecycle redirect with a synchronous router beforeEach guard, so a locked vault never resolves a protected route and no page component is instantiated. Gating navigation behind an awaited settings request is what opened the window in the first place, so the guard takes no async dependency. vue-router 4 still supports the (to, from, next) guard signature; it is kept over the newer return-a-location idiom so the redirect and the pass-through read identically. The isLocked watcher stays, narrowed to the mid-session eviction path (session timeout, "Lock vault" menu entry) where no navigation occurs and the guard therefore cannot run. Recipient-facing token routes (SecretRequestFill, LinkShareAccess, EphemeralSendAccess) stay outside the gate deliberately: those recipients are frequently not the vault owner and may hold no suite at all, and the routes carry token-scoped server-side authorisation instead. The existing e2e "zero-knowledge gate" test passed throughout the bug — it waits up to 20s for the lock heading, asserting only the eventual state, and its path-form URLs never deep-linked into SecretList under hash mode. Add a test asserting the invariant on the wire instead: a locked vault requests no secret-bearing endpoint at all. Assisted-by: ClaudeCode:claude-opus-5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
) The 32 floor was raised on the premise that nothing tested below it. That is false here: this repo's own CI runs stable31, and min-version is enforced at install time, so occ app:enable refuses on 31 and the e2e seed fails with "is not installed or enabled". The original reason for a 32 floor no longer holds either. It came from openregister implementing OCP\ContextChat\IContentProvider, an interface absent before NC 32. openregister#2372 removed every eager reference to that class, so it is only loaded inside interface_exists() guards and the header is never read on an older server. openregister#2380 restored its own 28 floor on that evidence.
eol-last, caught by eslint after the port commit. Assisted-by: ClaudeCode:claude-opus-5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured 55.79% on two independent development push runs (31052650976 and 31074210361); .coverage-baseline said 55.78, so Coverage Baseline Check failed on the non-empty git diff left by --update-baseline. Raises the baseline to the measured value, as the job's own error text asks. No threshold weakened, no waiver.
#167) `Frontend Check / test:l10n` has been red on development: 829 distinct literal keys are used by t()/n() calls under src/, but l10n/en.json held only 555 of them. 352 translatable strings were shipping with no English source entry at all — nothing for a translator to pick up, and the Nextcloud extraction contract broken for every one of them. Fixed with the repo's own sanctioned extraction step, `npm run test:l10n:write` (tests/l10n/check-l10n.js --write), which merges each missing used key in as `"<source>": "<source>"` — English source === key, the Nextcloud convention, so for en.json the value legitimately IS the key. The script appends and sorts only among the new keys, so the diff is provably additive: verified 0 pre-existing keys lost, 0 existing values changed, the original 555-key order preserved as an exact prefix, and all 352 added entries value === key. Reviewed key by key for extraction artefacts (a variable name, a CSS class, a debug string, an empty string, a non-user-facing string) — there are none. The 63 single-token additions were each checked at their call site and are all real UI text: `<th>` table headers (Accessor, Count, Dropped, Failures, Result), `<dt>` terms (Site, Number, Value, Transports), button labels (Re-parse, Re-issue, Renew…, Offboard), status words rendered into the DOM (yes/no/never/disabled/handled/ snoozed/OPEN) and status-label maps (Granted, Approved, Requested). No call site needed correcting. All three n('doriath', singular, plural) pairs landed with BOTH forms and `%n` preserved verbatim: %n item / %n items cannot be represented in CXF and will be skipped. %n secret copy still needs / %n secret copies still need to be … %n secret was / %n secrets were skipped because the successor … Proof the gate can still fail: removing "{count} selected" from en.json turns `npm run test:l10n` RED (exit 1) naming that key and its call site src/views/SecretList.vue:141; restoring it returns exit 0 / 907 keys. Scope: en.json only, which is all this gate reads — doriath's check-l10n.js does not require check-l10n-parity.js. The other 35 locales are NOT touched and a translation backlog remains (nl 494, all others 335, against en 907). That is a translator's job, not this fix's.
…gression ratchet (#181) `tests/l10n/check-l10n-parity.js` shipped in 48b8186 — commit message "i18n: add 35 European locale translations + European parity gate" — and has never been invoked. `grep -rn check-l10n-parity` matched only the script's own docblock, and `git log -S "check-l10n-parity.js" -- tests/l10n/check-l10n.js` is empty on every branch, so it was never wired rather than unwired later. `Frontend Check (test:l10n)` has therefore been reporting success over 20,433 missing translations across 36 locales (nl 413; each of the other 35 at 572 of 907). That is the falsely-GREEN shape of a dead gate. See doriath#180. What changes: * check-l10n.js now `require`s the parity script once the English source set is complete — the three-line seam larpingapp already had and this repo lacked. * The parity script gains two enforcement tiers: - L10N_PARITY_ENFORCED — locales that must be at exact full parity. - tests/l10n/parity-ratchet.json — a per-locale upper bound on the missing count for everything else. Exceeding it fails. * Coverage is printed on EVERY run: the required count, the fully-enforced set, the no-regression-only count, and the outstanding debt (20,433 today). A gate that caps its own coverage silently reads as "covered everything". ENFORCED is empty today, and that is a measurement rather than a shrug: no locale is at full parity (nl, the closest, is 413 short), so nothing can be fully enforced without failing every build. The script now REFUSES to start (exit 2) if L10N_PARITY_ENFORCED names a locale outside the required set — the first draft defaulted to `en`, which looks like enforcement but enforces nothing, because `en` is the source language and is not a member of REQUIRED. This does not clear the debt and does not claim to. It converts a check that measured nothing into one that cannot let the debt grow. #180 stays open with the 20,433 figure until the locales are actually translated. No locale file was padded with English to move a number. Proven able to fail: * delete "Import" from l10n/nl.json -> FAIL naming `nl` and `missing: "Import"`. * add an untranslated key to l10n/en.json -> FAIL on all 36 locales. * same mutation against the PRE-FIX check-l10n.js -> EXIT 0, green. That A/B is the dead gate itself. * L10N_PARITY_ENFORCED=nl -> FAIL [ENFORCED] nl 413 missing; with nl completed -> pass. Unmodified tree -> exit 0. Refs #180
…ken root lockfile (#183) #175 bundles two unrelated things. The 8 advisories it names are all in docs/package-lock.json, and that half is sound. It ALSO regenerates the ROOT package-lock.json, and that half does not install. The root change removes 58 packages and adds none — every @esbuild/* platform binary, @nextcloud/vue's nested vite, and the whole rolldown / lightningcss toolchain — and contains ZERO version bumps, so it carries none of the security value. Under the npm this project actually supports it is fatal: npm error code EUSAGE npm error `npm ci` can only install packages when your package.json and npm error package-lock.json are in sync. npm error Missing: vite@8.2.1 from lock file npm error Missing: esbuild@0.28.1 from lock file (+56 more) That is why #175 fails 8 checks that development passes: Frontend Build, Frontend Tests, both Frontend Checks, Vue Quality, License (npm), Security (npm) and lint-check. Every one of them is downstream of `npm ci`. Measured, one variable at a time, with npm pinned to CI's 10.8.2: arm A #175 root lockfile -> EXIT 1, EUSAGE (reproduces CI exactly) arm B development root lockfile -> EXIT 0, added 1561 packages arm C #175 docs lockfile -> EXIT 0, added 1388 packages Only the lockfile differs between A and B, so the lockfile is the cause; C shows the docs half is unaffected. Worth recording how close this came to being waved through: `npm ci` and a full `npm run build` BOTH succeeded for me locally against #175's root lockfile, and the docs build succeeded too. That green was worthless — I was on npm 11.13.0, while package.json declares `"npm": "^10.0.0"` and CI runs 10.8.2. npm 11 silently re-resolves the pruned entries that npm 10 refuses. A dependency bump verified on an unsupported toolchain is not verified. This commit therefore takes docs/package-lock.json from #175 verbatim and leaves the root lockfile untouched: dompurify 3.4.2 -> 3.4.13 (mXSS / DOM-clobbering fixes) fast-uri 3.1.2 -> 3.1.5 launch-editor 2.13.2 -> 2.14.1 postcss 8.5.14 -> 8.5.26 qs 6.15.1 -> 6.15.3 webpack-dev-server 5.2.4 -> 5.2.6 websocket-driver 0.7.4 -> 0.7.5 ws 7.5.10 -> 7.5.13, 8.20.1 -> 8.21.2 Note for whoever revisits the docs tree: no workflow builds it on a PR into development. documentation.yml triggers only on push/pull_request to the `documentation` branch, so these bumps get no CI build here either way. The docs build was run by hand (npm ci + npm run build, both exit 0, "Generated static files in build") — on npm 11, so treat that as a smoke test rather than a guarantee, exactly like the local green above. Refs #175
…32 (#182) * fix(a11y): clear five accessibility gates, and raise the NC floor to 32 Full-tree hydra-gates on origin/development reported 19 failing gates. Five of them were small, real and unambiguous; this clears all five. Measured with the checkers run directly over the whole tree, because no CI job in this fleet gives a full-tree verdict — push runs scope to ~1 file and PR runs to the diff. gate-39 button-name (3) — the close controls in ShareDialog, ShareRequestForm and ApplicationRegisterDialog contained a bare "×" and nothing else, so they were announced as "times" or as an unlabelled button. Each gains an aria-label reusing the existing "Close" key, with the glyph marked aria-hidden. gate-32 semantic-controls (1) — RecentSecretsWidget rows were <li @click>: unreachable by keyboard and announced as plain list items. The row is now a real <button>, which brings role, focus and Enter/Space for free; the list item keeps the layout. Visual result is unchanged. gate-44 autocomplete-attr (2) — the two ephemeral-send password fields had no autocomplete. Deliberately NOT current-password: neither is the visitor's own credential. The unlock field is autocomplete="off" (a one-shot out-of-band passphrase, useless to save); the authoring field is "new-password" so a manager offers to generate rather than autofill the account password. gate-45 prefers-reduced-motion (2) — PasswordStrengthMeter and TotpDisplay animate without a reduced-motion fallback (WCAG 2.2 AA 2.3.3). Both keep every state change and drop only the tween, so no information is carried by motion alone; TotpDisplay's numeric countdown already carries the same cue. gate-34 window-confirm (1) — ApplicationDetail confirmed a cascading delete with a native browser prompt. That prompt cannot be themed or translated beyond its message, and is suppressed outright in some embedded/kiosk contexts, where it returns false and the delete silently never happens. It is now ApplicationDeleteDialog, in its own file under src/dialogs/ per ADR-004. Also: <nextcloud min-version> 31 -> 32 (fleet-wide floor, PO decision), so PHP 8.3 is guaranteed by the platform. 31 was never deliverable — the CI matrix already tests only stable32, because OpenRegister declares a floor of 32 itself, so the App Store advertised a range no test leg covered and no dependency could satisfy. max-version stays 34, the fleet-wide value everywhere except openconnector (35). Evidence: * full-tree gates 19 failing -> 14, with exactly these five flipped and no other gate changing state in either direction. * each fix proven able to fail INDIVIDUALLY: reverting just that file returns its gate to FAIL with the original finding count (39: 3, 32: 1, 44: 2, 45: 1-of-2, 34: 1), and restoring returns it to PASS. * two regressions I introduced were caught by re-running and fixed: the delete dialog was inline at first (gate-13 5 -> 6) and is now extracted, and the two new methods needed @SPEC anchors (gate-16). Both are back to baseline. * 425 vitest tests pass; eslint clean (exit 0 read directly, not through a pipe); test:l10n still green and adds ZERO new translation keys — 829 used, 907 in en.json, unchanged — so this does not move the doriath#180 debt. Not touched, with reasons: gate-7 is anti-correlated with what it checks (.github#160) and its 3 findings here are a CSPRNG capability-token relay plus a deliberately world-readable password-policy floor; gate-9's single finding resolves its credential via IUserSession::getUser(), which the rule does not recognise; gate-14's 5 findings are all provided by OCA\OpenRegister\AppHost\Routes::standard() at appinfo/routes.php:22; gate-40 measured 58% false-positive fleet-wide. gate-22 and gate-53 fail only when ajv is unresolvable and pass under a real npm ci. * fix(a11y): label the send-link field and repoint two dead @SPEC anchors The PR-scoped Hydra Gates run failed on this branch with 2 gates that the full-tree run on development also reports. Both are PRE-EXISTING findings in files this PR touches for the autocomplete fix — diff-scoping pulls a touched file's existing debt into scope, which is working as designed. Proven pre-existing against the BASE tree with the files in scope, not by comparing counts: the full-scope run at origin/development emits the identical three lines. gate-46 src/views/EphemeralSendAccess.vue -> #requirement-anonymous-access src/modals/NewSendDialog.vue -> #requirement-create-and-store-ciphertext-only gate-40 src/modals/NewSendDialog.vue -> <input :value="link" readonly …> gate-46: both anchors point into openspec/changes/ephemeral-send/…, and that file contains NO `### Requirement:` headings at all, so neither anchor could ever have resolved. The canonical openspec/specs/ephemeral-send/spec.md does have them, so both now target the canonical spec — which is where an @SPEC should point in the first place; a change dir is not a spec's permanent home. #requirement-anonymous-recipient-access-with-no-account #requirement-create-a-standalone-ephemeral-send gate-40: the generated-link field is a read-only display sitting directly under the note card that explains it, so a visible <label> would be redundant to sighted users while a screen reader still needs the control named. Uses aria-label with the EXISTING "Link" key — this branch still adds zero new translation keys, so it does not move the #180 debt. Verified: diff-scoped gates now 58 of 58 applicable gates ran, ZERO failing, over a SCOPE-FILE-COUNT of 11 — a real scope, not a green over zero files. Can-fail control: removing the aria-label again returns gate-40 to FAIL, so the scoped run is genuinely sensitive to this file. 425 vitest tests pass, eslint exit 0, test:l10n green.
…ng (#186) Follow-up to #181, closing a hole I left in it. #181 wired up a gate that had never run. It did not stop that gate from reporting OK on an empty comparison — which is the same defect one level up, and would have been the original doriath#180 finding reintroduced in a form that looks like a fix. Three inputs made the MERGED version print success while comparing nothing. Measured against origin/development's copy, not asserted: L10N_REQUIRED_LOCALES=" " OLD exit 0: "0 required locales … OK — every required locale is at full parity" no l10n/en.json at all OLD exit 0: "no en.js / en.json source set found — nothing to check" en.json present but ZERO keys OLD exit 0: "OK — every required locale is at full parity" The first is the worst: a single env var set to a space silently turned the whole gate green while still printing a pass line. I introduced that. All three now exit 2 and name the empty input. Every run also prints the work actually performed, so a zero is visible rather than inferred: l10n-parity: WORK DONE — 36 locale file(s) compared, 32652 key comparison(s). A zero here means this gate measured nothing. Proven, each input run against BOTH copies: input OLD (merged) NEW empty required set exit 0 "OK" exit 2 "required-locale set is EMPTY" no source set exit 0 "OK" exit 2 "nothing to compare against" source set with 0 keys exit 0 "OK" exit 2 "ZERO key comparisons" unmodified doriath tree exit 0 exit 0, 36 files / 32652 comparisons The last row is the positive control: hardening must not turn a genuinely clean run red, and it does not. Context for why this is worth a commit of its own. In the same week: gate-30 reported PASS fleet-wide having matched nothing, writing a 0-BYTE findings log (.github#213); gates 22 and 53 reported failures that were an unresolvable `ajv` rather than findings, and once resolved gate-53 surfaced a real cross-reference defect it had been hiding; and this file had zero callers for its entire life. Different mechanisms, one family: a check that passes or fails for an environmental or parsing reason is not measuring the code. lint: tests/ is outside CI's eslint scope (`"lint": "eslint src"`). The two added n/no-process-exit reports are the only delta against the base file, which already carries seven of them. Refs #180
…187) * fix(manifest): drop the dead `example` deep link that routes nowhere The manifest declared a deep link to a page that does not exist: {"registerSlug":"doriath","schemaSlug":"example", "urlTemplate":"/apps/doriath/#/examples/{uuid}"} There is no `/examples` route in pages[] — the closest are /secrets/:id and /applications/:id — so ADR-040's deep-link listener would resolve an `example` object to a URL the router cannot serve. It is scaffold residue from b7285f4 ("adopt OpenRegister AppHost engine"), matching the placeholder `example` schema in lib/Settings/doriath_register.json whose sample values are literally "My example" / "This is an example". Doriath has no examples feature. How it stayed hidden is the more interesting half. gate-53 was reporting FAIL — ajv not resolvable from …/hydra-gates/scripts/lib — refusing to run fail-open which reads like tooling noise, and gate-22 was failing the same way with "SCHEMA VALIDATION DID NOT HAPPEN". Both are WIRING failures, not findings. Resolve ajv (NODE_PATH, or npm ci) and the picture changes completely: gate-22 goes GREEN — it was never a real failure — and gate-53 stops degrading and surfaces this, a genuine cross-reference defect it had been sitting on. A gate that cannot run is not a gate that found nothing. Proven able to fail, three ways: dead deepLink restored -> FAIL 1 cross-reference failure fix reapplied -> PASS a DIFFERENT bad deepLink planted -> FAIL 2 cross-reference failures The third arm matters: it shows the gate validates route prefixes generally rather than pattern-matching this one entry, so the PASS above is a real verdict and not an artefact of removing the only thing it knew how to check. Also verified: `npm run check:manifest` PASS (Ajv 0 errors, consistency 0 issues) with `deepLinks: []`, so an empty array is valid and the key does not need removing. 425 vitest tests pass; test:l10n green. * fix(manifest): rename the user-settings entry so it stops rendering "Settings > Settings" The settings foldout contained an entry whose own label was "Settings", so the menu read Settings > Settings (ADR-079 D4). Renamed to "Personal settings", which also distinguishes it from the admin settings surface. This was surfaced by gate-63, and how it stayed hidden is the point. gate-63 reports PASS on a full-tree run — and its findings log on that run says: No changed manifest / menu-layout — gate skipped (ADR-020 diff scoping). The gate SKIPPED and then reported PASS. It only judges the manifest when the manifest is in the diff, so the only run that ever evaluates it is a PR that happens to touch the file. A full-tree audit — the thing you run precisely to find inherited debt — is the one mode where this gate is guaranteed to say nothing and call it success. That is the third falsely-green mechanism found in this repo today, after the l10n parity gate having no callers at all (doriath#180) and gate-30 reporting PASS over a 0-byte log (.github#213). Reported as .github#238. Proven able to fail: revert ONLY the label back to "Settings" and gate-63 returns FAIL — 1 settings-placement violation (ADR-079); restore and it passes. Diff-scoped run is now 0 failing gates over SCOPE-FILE-COUNT 1. Also verified: check:manifest PASS (Ajv 0 errors, consistency 0 issues); 425 vitest tests pass; test:l10n green. manifest.json is not scanned by check-l10n, so this label change adds no translation debt.
… cap (#188) * fix(e2e): retain-on-failure traces + a globalTimeout under the 45m CI cap Fleet-wide Playwright instrument sweep, ConductionNL/.github#188. Neither change can alter a verdict; both change whether you can see why a verdict happened. `trace: 'on-first-retry'` only writes a trace when a retry actually happens, which makes the trace artifact a function of `retries`. `retain-on-failure` captures every test, keeps only the failures, and does not depend on the retry count. No repo in the fleet set `globalTimeout`. The shared quality.yml Playwright job is `timeout-minutes: 45`, and a job cancelled by that cap produces no verdict and no artifacts: the trace upload is `if: failure()` and the report upload is `if: always()`, and neither runs on a cancelled job, while `gh pr checks` still renders it as "fail". Runs cancelled at ~45m16s have been observed in this fleet. Measured overhead in that job before the `Run Playwright tests` step starts is 2.0-2.4 min, so 38m leaves ~7 min of margin while guaranteeing a tally and its artifacts. * fix(e2e): apply the same fix to the config CI actually loads The shared quality.yml resolves its config as `${playwright-test-path}/playwright.config.ts` and only falls back to the app-root `playwright.config.ts` when that file is absent (quality.yml ~L2218). This repo ships tests/e2e/playwright.config.ts, so THAT is the file every CI run has been using — the app-root config fixed in the previous commit is the one developers load by hand, not the one the gate reads. Applies the identical `retain-on-failure` + `globalTimeout: 38 * 60_000` change here. ConductionNL/.github#188.
…andards/phpcsextra-1.5.1 build(deps-dev): bump phpcsstandards/phpcsextra from 1.5.0 to 1.5.1
…ics/phpmetrics-2.11.0 build(deps-dev): bump phpmetrics/phpmetrics from 2.9.1 to 2.11.0
…oken v1.8.0 shipped an ObjectServiceInterface WITHOUT patchObject() and with updateObject() still summarised as "Apply a partial update" — the wording that sent a consumer down the erasing path. The correction landed on main two days after the tag; every app has been pinned to the broken copy since. It is not confined to this repo: hydra-gates claims OCA\OpenRegister\Contract\ in its composer autoload, a LONGER psr-4 prefix than openregister's own OCA\OpenRegister\ -> lib/, so the gate package wins. Nine repos vendor it, so under OC_App::loadApps() whichever app registers first defines the contract for the whole instance. Measured on a running instance, softwarecatalog's vendor directory was supplying openregister's interface, and updating openregister ALONE did not change the winner — which is why this lands across the fleet rather than in one repo.
chore(deps): take hydra-gates v1.8.1 — the contract v1.8.0 shipped broken
All three reproduce locally and all three are this branch's own, which is why they
could not be fixed centrally: `npm run lint` exits 0 on development and 2 here.
**`npm run format`** — CI runs prettier over the WHOLE repo
(`**/*.{js,ts,vue,css,scss}`), not the `src/` and `tests/` subsets I had been
checking, which is why my local sweeps reported clean. One file was unformatted:
tests/views/SecretRequestFill.spec.js. `format:fix` touched only that file.
**`npm run lint`** — exited 2 while printing "0 errors", on "There are suppressions
left that do not occur anymore". Pruning removed exactly one entry:
`src/App.vue @nextcloud/l10n-enforce-ellipsis`. This branch's edits to App.vue
eliminated that violation, so the suppression went stale — which is also why
pruning on development would have been wrong: nothing is stale there, and the entry
is still needed.
**`npm run test:l10n`** — "Revoking…" (src/App.vue:194) was used in source but
missing from en.json. A pre-existing gap that predates this branch; it surfaced here
because this is where App.vue changed. Added with the same 36 translations used on
the later branches, so merging up is a no-op rather than a conflict. Mixed-script
scan clean.
Worth stating plainly: I reported "eslint 0 errors" and "prettier clean" as green in
earlier commit messages on this branch. Both commands actually failed — one by exit
code while printing a reassuring summary line, the other because I checked a
narrower path set than CI does. Exit codes from here on, not summary text.
NOT fixed here, because it is not a code problem: the four PHPUnit matrix jobs fail
on `HTTP/2 504` fetching php-fig/cache while composer-installing the sibling
openregister app, and the job correctly refuses to continue rather than report that
app's missing classes as ours. Same signature on all four legs. Needs a re-run.
Sweep: gates 49/49 applicable, PHPUnit passing, vitest 583, format/lint/test:l10n
and the l10n parity ratchet all exit 0.
Assisted-by: ClaudeCode:claude-opus-5
…y-lifecycle' into feature/268/secret-request-expiry-lifecycle
…ores chore(openspec): archive two finished changes and clear the debt behind them
…re/271/admin-application-request-visibility # Conflicts: # l10n/be.json # l10n/bg.json # l10n/bs.json # l10n/ca.json # l10n/cs.json # l10n/da.json # l10n/de.json # l10n/el.json # l10n/en.json # l10n/es.json # l10n/et.json # l10n/fi.json # l10n/fr.json # l10n/ga.json # l10n/hr.json # l10n/hu.json # l10n/is.json # l10n/it.json # l10n/lb.json # l10n/lt.json # l10n/lv.json # l10n/mk.json # l10n/mt.json # l10n/nb.json # l10n/nl.json # l10n/pl.json # l10n/pt.json # l10n/rm.json # l10n/ro.json # l10n/ru.json # l10n/sk.json # l10n/sl.json # l10n/sq.json # l10n/sr.json # l10n/sv.json # l10n/tr.json # l10n/uk.json
Merged feature/268 in (its l10n fix propagated — `test:l10n` now passes here) and
fixed what is this branch's own.
**`npm run format`** — the four files CI named, all touched by this branch:
SecretRequestList.vue, store/modules/secretRequest.js, and the SecretRequestList
and ApplicationRequestRevokeDialog specs. The fifth CI listed arrived via the 268
merge.
**`npm run lint`** — one stale suppression removed:
`src/components/secretRequest/SecretRequestList.vue eqeqeq {count: 2}`. This branch
replaced those `== null` comparisons with `typeof` checks, so the suppression no
longer had anything to suppress. Same shape as 268's, different entry, which is why
each branch has to prune its own.
**The l10n merge needed care.** Both branches added "Revoking…" to all 37 locale
files, so every one conflicted. Resolved by merging the translation maps rather than
by hand — and one key genuinely differed: the Estonian
"Create a password-protected link…" string, where this branch has the fix for a
Cyrillic т inside `kasutuslimiit` and 268 still had the typo. This branch's version
kept.
A process note worth recording, because it nearly went into the repo: my first
attempt at that resolution staged all 37 files still containing conflict markers.
The script that was supposed to merge them found zero conflicted files, printed
"merged 0", and I ran `git add` anyway without checking its output — 37 invalid JSON
files staged. Nothing was committed; HEAD had not moved, so `git restore` put it
back and the merge was redone with the stage read PROVEN to work first and a
marker/JSON validation before staging. Verify the tool worked before trusting what
it produced.
Sweep: gates 49/49 applicable, PHPUnit passing, vitest 594, phpcs 0 errors,
format/lint/test:l10n all exit 0.
Still failing on #286 and NOT addressed here: the coverage ratchet
(−0.84% on this branch's own touched files). That needs tests, not formatting.
Assisted-by: ClaudeCode:claude-opus-5
…expiry-lifecycle feat(secret-requests): make an expiry something that is acted on, not only checked
…s pointing at #286's PHPUnit job was not failing on a test. It was the coverage ratchet: "coverage of the files this change touches dropped by 0.84%" — 81.27% against a 82.11% base. The honest reading is that I added error-handling code and never exercised it, and the ratchet noticed. Nine tests, all on paths I wrote and left uncovered: **ApplicationRequestAdminService** — an empty applicationId refused before any query runs; a non-pending request refused rather than silently "revoked"; and the two fail-soft rungs of the placeholder cleanup that nothing touched: the linked Secret already gone (revoke still succeeds, nothing to clean up) and the delete itself throwing (an orphan empty Secret is untidy, a revoke the administrator believes failed is worse — the link is already dead and they will go hunting for another way to kill it). **ApplicationRequestAdminController** — `statusFor()`'s fallback, which existed precisely because `InvalidArgumentException` defaults its code to 0 and a JSONResponse with status 0 is uninterpretable, and was never exercised; the service's refusal code surviving on the listing path; an unexpected failure answering 500 while NOT leaking the exception text; and a revoke of an unknown id answering 404 rather than 500, since it arrives as a mapper lookup exception rather than an InvalidArgumentException. Both classes are now at 100% (52/52 and 45/45). Touched-file coverage: 83.46% (535/641) against the 82.11% base. Two measurement notes, because the first number I got was wrong. The initial re-measure said 83.46% off 641 statements where the failing run had 801, with six files silently absent from the report — an incomplete clover file, not an improvement. Re-run with output visible and every touched file confirmed present before believing it. Coverage also needs XDEBUG_MODE=coverage under Xdebug 3; without it phpunit writes no clover at all and says nothing about why. The scope legitimately shrank from 12 files to 9 in the meantime: #282 merged into development, so feature/268's files are now part of the shared base rather than this branch's diff. Still 0% and untouched by this: lib/Db/SecretRequestMapper.php (0/64). Mapper coverage needs a database rather than mocks, so it is a different kind of work and does not belong in a CI fix. Assisted-by: ClaudeCode:claude-opus-5
…tart A before-starting hook (docker/nextcloud/enable-apps.sh) runs as www-data on every container start, after install/upgrade, and enables openregister and doriath — replacing the manual occ app:enable steps after docker compose up. Mounted as a docker-entrypoint hook rather than replacing the image entrypoint, which broke startup before.
…oads
Nextcloud serves JS-side translations exclusively from l10n/<locale>.js
(OC.L10N.register); this repo shipped only the backend l10n/<locale>.json
files, so t('doriath', ...) fell back to English in the browser for
every language regardless of the language setting. Adds a generator
(tests/l10n/generate-js-l10n.js, run it after changing the .json files),
the 37 generated .js files, and the frontend set's parity-ratchet
baseline so the l10n gates hold the new file set to no-regression.
Adds the suite-check strings (Checking your vault, the fail-closed could-not-determine error, Try again) and the password-mismatch and passwords-match messages to every locale, each phrased with the vault vocabulary that locale already uses. The matching frontend .js files landed with the generator commit; this adds the backend .json side.
…feedback The lock screen now covers the app as a login-style overlay (app navigation hidden, themed primary background) instead of rendering beside an interactive sidebar. The setup-vs-unlock decision waits for the suite check and fails closed: the create-a-new-suite form can no longer flash before the check resolves, nor render when it fails; offline falls through to the snapshot-only unlock form. Error placement follows the Nextcloud login screen: systemic errors banner above icon and title, wrong-password and debounced password-mismatch feedback inline under the field (aria-described, layout-stable, with a tooltip on the disabled submit as progressive enhancement). Leaving the app for another Nextcloud app no longer flashes the unlock form mid-transition: the beforeunload key clear stays, only the redirect on a dying page is skipped, with a timer fail-safe when the unload never happens.
Two user-visible defects are fixed by this bump. TWO AI-COMPANION HEXES ON EVERY PAGE. The companion singleton landed in 2.7.0. Below that the host app's own companion never stands down, so any page of this app rendered a second hex 8px from hermiq's — measured on a running instance: openconnector (2.7.1) showed ONE, openbuild (2.6.3) showed TWO, both visible at 52x60, from two separate mounts. THE DETAIL PAGE RECLOSED ITS SIDEBAR WHILE HYDRATING. CnDetailPage set sidebarSeeded and never read it, so 'open' was re-applied on every sync and each reactive change during hydration reset it to the prop default. Fixed in nextcloud-vue#711; that is the cause behind openbuild#268 and, on the evidence, #188. Lockfile only — the existing caret already allowed this. Three-line diff (version, resolved, integrity).
The eslint rule @nextcloud/l10n-enforce-ellipsis flags triple dots in translated strings. Renames the Setting up/Unlocking/Changing keys to their ellipsis form in source and in all 37 locale files, converting the translated values' trailing dots as well, and regenerates the frontend l10n .js files.
…mption docs(gate-7): declare the k-anonymity range proxy exempt, with its reason
chore(deps): take @conduction/nextcloud-vue 2.8.2 (was 2.3.0)
Prettier-formats the two hand-formatted lock-screen components and prunes eslint-suppressions.json entries those edits made unused (ESLint 10 fails on stale suppressions, which is what broke the eslint and lint-check gates despite src/ having no rule errors). Adds a prefers-reduced-motion fallback for the lock screen's feedback fade (hydra gate-45) and @SPEC tags on the new lock-screen methods (gate-16). Updates the four e2e tests that asserted the app navigation is visible on the lock screen — hiding it there is the point of the rework — to assert the nav is hidden while locked and to unlock (dev master password) before asserting menu contents. Also fixes an eqeqeq in the l10n generator.
…he right placeholder Two defects on the expiry path, both raised in review on PR #282. 1. Expiry raced a concurrent fill. `QBMapper::update()` issues `WHERE id = ?` with no status guard, so the sweeper's entity — loaded a whole batch earlier — overwrote whatever the row had become. A recipient who filled a request between the job's SELECT and its write had `fulfilled` replaced by `expired`, with `fulfilled_at` left set: the requester was told their request had lapsed while the credential was already sitting in their vault. The pre-flight status check could not catch this; only the write itself can. `transitionIfPending()` does the transition as `UPDATE ... WHERE id = ? AND status = 'pending'` and reports affected rows, and expire() now does nothing at all when that returns zero — no placeholder delete, no audit event claiming an expiry that never happened. 2. Expiry never cleaned up an application-owned placeholder. It called the user-revoke helper, passing the request's `created_by` as the owner. For an application request that value is `application:<id>`, never a user id, and the helper also required `owner_type === 'user'` — so it failed twice over. Expired application placeholders accumulated indefinitely, which is the exact outcome the expiry job exists to prevent. The comment above the call claimed it passed the Secret's owner; it did not, and the mismatch is what hid the bug. Both cleanup helpers are now one method on a new SecretPlaceholderCleaner, taking the acting user as a parameter: given, it is a user revoke and the Secret must be theirs (an authorization boundary, kept intact); null, the system expired the request and the owner comes off the Secret. Extracting it was not cosmetic — adding the second caller in place pushed SecretRequestService to complexity 53 against phpmd's threshold of 50, and development's baseline is clean, so that was mine to resolve. A third suppression would have hidden it; the class the code came from is the request state machine, and whether a Secret may be destroyed is a different question that now has a name. Both defects were verified to discriminate by reintroducing them: making the transition unconditional fails only the concurrent-fill test, and routing expiry back through the user branch fails only the application-placeholder test. phpcs, phpmd, psalm and php-cs-fixer are clean; 1034 tests pass. Assisted-by: ClaudeCode:claude-opus-5
Brings in the two expiry fixes raised in review on PR #282 (atomic `transitionIfPending` transition; cleanup that reaches an application-owned placeholder), and with them development's newer commits. Two conflicts, both in the expiry-cleanup code the two branches changed for the same reason: - `SecretRequestService`: this branch had moved `holdsNoValues()` onto the Secret entity because two services need one predicate; the fix branch had moved the whole cleanup decision into SecretPlaceholderCleaner. Resolved by keeping both moves — the private wrappers are gone, and the cleaner now calls `Secret::holdsNoValues()` rather than carrying a second copy of a predicate whose duplication is what let this path delete filled secrets in the first place. - `SecretRequestServiceTest`: both sides appended tests at the same point. Both kept. 1058 tests pass. Assisted-by: ClaudeCode:claude-opus-5
Raised in review on PR #286. `revokeForApplication()` read the request, checked it was pending, then wrote `declined` back with `QBMapper::update()` — `WHERE id = ?`, no status guard. A recipient filling the request between the check and the write had `fulfilled` overwritten with `declined`, and the administrator was then handed an audit event recording a revoke of a request that had in fact been answered. The pre-flight check cannot catch this; only the write can. Now `transitionIfPending()` does the transition conditionally and reports affected rows, and nothing downstream runs unless it actually happened — no placeholder delete, no revoke recorded. A lost race returns the same 400 the pre-flight check would have, because it is the same situation observed a moment later. The class also carried a third copy of the placeholder-cleanup logic, differing from the other two only in whose vault it may delete from. Three near-identical hard-delete paths is the fault class that produced both defects reported on #282, so the copies are now one SecretPlaceholderCleaner call and authorization is stated by the caller: 'user' + uid, 'application' + application id, or neither for the system. The check still runs against the SECRET while the expectation comes from the CALLER — two checks against different data, so a mismatched pair cannot destroy a third party's Secret. `secretMapper` and `container` went with it; the cleaner holds them now. `adminUid()` is renamed `requireAdminUid()`. It reads as a getter, which left the authorization hiding behind an assignment — invisible to a reviewer skimning the method, and to the IDOR gate, which looks for the guard at the call site. The guard was always there; now it says so. Verified beyond the unit tests: the conditional SQL was exercised against the live database — it transitions a pending row once, refuses the second attempt, and cannot touch a `fulfilled` row, which is the reported defect. DI resolves the cleaner, both services, the admin controller and the expiry job. The new race test discriminates: reverting the guard fails only that test. 1059 tests pass; all 30 hydra gates green (gate-7 included). Assisted-by: ClaudeCode:claude-opus-5
…quest-visibility' into feature/271/admin-application-request-visibility
feat: fail-closed lock screen, working frontend translations, and a local compose stack
…on-request-visibility feat(application-mgmt): show an administrator what an application is asking people for
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.