Skip to content

Release: merge beta into main - #1204

Merged
rjzondervan merged 369 commits into
mainfrom
beta
Oct 7, 2026
Merged

rjzondervan merged 369 commits into
mainfrom
beta

Conversation

@rjzondervan

Copy link
Copy Markdown
Member

Closes #1189.

Release: beta (1181e45, which includes #834) into main (last updated 2026-09-29 by #828). 369 commits, about 150 merged PRs. Supported versions are unchanged: PHP 8.3+, Nextcloud 32–35. 13 new database migrations.

Issues this closes

The PRs below merged into development, which is not the default branch, so their Closes lines closed nothing. Of the issues they claim to fix, only one is still open:

Issue Fixed by
#1189 Compromise force-revoke: four paths still fail open (follow-up to #863, #864) #1190

The #691 beta-review issues (#858–#882) were already closed by hand. Two of their follow-ups stay open and are not fixed by this release: #1187 (GDPR deletion leaves recovery enrolments and sessions) and #1188 (Ephemeral Sends and federated shares survive a compromise containment).

Upgrade notes

What's new

Vault

Sharing

Federation (share with other organisations over OCM, Nextcloud 33+)

Administration

Integrations and tooling

Browser extension (separate artefact, still in progress)

Mobile apps (separate artefact, still in progress)

Security fixes

Other fixes and maintenance

Known red checks

These are pre-existing and not caused by this release:

  • Browser extension build-and-load: Firefox 156 on the runner refuses the extension popup navigation in the load check.
  • CLI Release image: the image tag is built from the ref name, which is <n>/merge on a pull request.

AI disclosure: description drafted with Claude Code; reviewed by the author.

🤖 Generated with Claude Code

rubenvdlinde and others added 30 commits October 2, 2026 15:37
Both sides kept: the fill sends the tab host (#934) and the use-only flag,
and a use-only copy is checked against that same host; the notifier runs
its renderers as a list, so the emergency renderer (#931) and the
access-end renderer both stay and prepare() stays simple; SecretService
keeps the spec tag and #931's suppression; l10n is the union; info.xml
keeps 20261002161000.
…t or face unlock (#921)

* feat(ca): renew the root from the CA health section, behind a confirmation

The renew-root endpoint had no caller, so an admin warned about the
root's expiry could not act on it. The CA health section now has a
renew root button that opens a confirmation dialog; only the confirm
posts, and the result names how many suites were signed again.
A controller test pins the admin-only posture of every CA write.

Fixes #741

* fix(extension): apply the org password policy before saving a login

The extension saved and updated logins without the strength floor or the
breach block that the web app applies. The rules now live in
src/policy/rules.js and src/health/hibpMatch.js, free of Nextcloud page
imports, and the extension bundles the same code. The worker reads the
same policy endpoint, refuses before encrypting, and the in-page bar
explains the refusal instead of offering a save.

Refs #746

* feat(leases): view and set an application's lease policy on its detail page

The per-application lease-policy override had a PUT and no screen. A new
GET /api/v1/applications/{id}/lease-policy answers the policy in force,
the stored override and the instance values, for an admin or the
application's registrant (404 for anyone else). The leases panel now
shows a form: an admin saves the override, an empty field inherits the
instance value, and the registrant sees it read-only.

Refs #753

* fix(extension): post the breach-check prefix in the body, as the route now expects

Development moved the range route to POST with the prefix in the body
(keepiq#866). The extension's new lookup follows it, with a test that
the prefix never appears in the URL.

Refs #746

* feat(extension): idle lock maximum endpoint and per-client passkey credentials

GET /api/v1/extension/policy returns the administrator's maximum idle lock
delay for the browser extension (default 240 minutes). Passkey credentials
gain client_kind and rp_id, and the login options are scoped to the asking
client, so the web app never gets an extension credential and the reverse.

Refs #784

* feat(extension): several accounts, per-account locks, and a sender check on worker messages

The extension holds up to five paired accounts, each with its own key, idle
timer and match cache. The old single pairing becomes the first account.
Matching, filling, the one-time code and saving use the active account, and
the worker refuses a fill whose id did not come from the active account's own
match for the current site. Only the four content-script messages are
accepted from a tab; everything else needs an extension page as sender.

The idle delay is per account, capped at the organisation maximum read at
each unlock.

Refs #784

* feat(extension): fingerprint or face unlock, account switcher and idle delay settings

The unlock window enrols a platform passkey with PRF for one account (the
extension's own origin as relying party, user verification required) and
unlocks with it: the raw unlock key is unwrapped in the window and handed to
the worker, which imports the key and drops the bytes. The server gets only
the wrapped key, the PRF salt and the credential metadata. The option shows
only where a user-verifying platform authenticator exists.

The popup header switches between accounts and shows each one's lock state;
a settings view picks the idle delay, with delays above the organisation's
maximum shown as unavailable.

Refs #784

* feat(extension): admin idle maximum field, extension passkeys in the web list; archive the change

The admin settings get a Browser extension section for the longest idle
delay users may pick. The web app's passkey list labels extension
credentials, which the owner revokes there as any other. The change
clients-extension-unlock-lock-and-accounts is archived: its spec deltas are
synced into openspec/specs and the parity rows clients-06, clients-21 and
crypto-09 are set to built.

Closes #784

* chore(quality): reasoned complexity suppression on the admin settings service, @SPEC on the extension section
Keeps both notifier families (emergency and device approval) in the
renderer list, both App.vue mounts, info.xml at 163000, and adds
device_approvals to the backup table registry.
…and-ci

feat: client libraries for Go, Python and TypeScript, GitHub Action and GitLab CI template
…w-device-approval

# Conflicts:
#	appinfo/info.xml
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
#	src/views/settings/Settings.vue
The extension's handlers moved to router.js (#921), so the use-only
match filter, fill refusal, owner report and save-prompt block move
there too. info.xml goes to 181000, past development's 180000.
…ackend)

Register five concrete admin settings classes (General, Policies,
Applications and machine access, People and offboarding, Audit and
compliance) in Keepiq's own container, so a Nextcloud delegation of a
class satisfies the #[AuthorizedAdminSetting] guard that names it.

Split the admin settings endpoint per area (decision of 2 Oct): one GET
and one PUT per area under /api/settings/admin/{general,policies,
applications,audit}, each guarded by its own area class and writing only
that area's keys; a key of another area answers 400. Version and trash
retention belong to Policies. Move the attribute guards and the inline
isAdmin checks to AdminAreaAuthorizer::holds(), and route admin handover
and team offboarding through the People area (vault_admin stays an alias).

Refs #774
…export (#936)

* feat(siem): connector columns on SIEM sinks

Version001004Date20261002170000 adds format (default json),
credential_enc and connector_options to keepiq_siem_sinks; app version
0.3.4-unstable.20261002170000. SiemSink serializes format,
connectorOptions and hasCredential, never the credential.

Refs #782

* feat(siem): accept Splunk HEC and Sentinel sinks and CEF syslog

SiemSinkService accepts splunk_hec and sentinel, requires https://
endpoints and a credential for them, validates connector options per
type (Sentinel tenant, client, data collection endpoint and rule
required; stream and authority host defaulted; https authority), and
allows format cef on syslog only. The credential is encrypted with
ICrypto and write-only (blank keeps it). The sink update audit names
the connector type. The controller passes format, credential and
connectorOptions. Red before, green after: SiemSinkServiceConnectorTest.

Refs #782

* feat(siem): JSON, CEF, Splunk HEC and Sentinel formatters

Pure formatters under lib/Service/Siem read the payload only through
PayloadView, which returns the eight known fields and drops every
forbidden metadata key again. CEF escapes header and extension values
and takes severity from a fixed category map. A guard test plants every
forbidden key and an extra top-level field and fails if any formatter
output carries them or any value that is not from the payload or a
fixed vendor constant.

Refs #782

* style(siem): named arguments and spec anchors in the formatters

Refs #782

* feat(siem): Splunk HEC and Sentinel delivery, CEF over syslog

SiemTransport sends a cef syslog sink's CEF line in the existing RFC
5424 frame and a json one byte for byte as before. Splunk HEC posts one
event with Authorization: Splunk <token> and accepts only HTTP 200 with
code 0. Sentinel gets a client-credentials token for scope
https://monitor.azure.com//.default, holds it in the process for the
run, posts one row to the data collection rule stream, accepts 204 and
retries once with a new token after a 401. Every failure goes through
the existing retry and dead-letter path; test-fire works for every
connector. Tested over a real TCP socket and a mocked HTTP client.

Refs #782

* feat(siem): connector picker on the SIEM section

The admin form picks Splunk HTTP Event Collector, Microsoft Sentinel,
CEF over syslog, Syslog JSON or Webhook JSON and shows only the fields
that connector needs. The credential field is write-only: never
prefilled, blank keeps the stored one. siemConnectors.js holds the
mapping, validation and request body; vitest covers the field set per
connector, the body, and the empty credential on edit. New strings in
every catalogue.

Refs #782

* test(siem): Playwright flow for a Splunk HEC sink and its test-fire

Refs #782

* test(siem): do not depend on translated quote marks in the e2e

Refs #782

* feat(siem): Sentinel template, Splunk sourcetype and connector docs

integrations/siem/sentinel/keepiq-dcr.json creates KeepiqAudit_CL and
the data collection rule with stream Custom-KeepiqAudit; a test keeps
its columns equal to SentinelRowFormatter. integrations/siem/splunk
holds props.conf for keepiq:audit. Both READMEs name the least
privilege each credential needs; docs/siem-connectors.md covers every
connector.

Refs #782

* docs(openspec): archive audit-siem-vendor-connectors

Ticks every task with its test, syncs the siem-vendor-connectors spec,
points the @SPEC and @e2e anchors at it, and sets parity row audit-14
to built.

Closes #782

* refactor(siem): request object and connector settings class

SiemSinkController create() and update() read the sink fields through
SiemSinkRequest instead of eleven router-bound arguments; the HTTP
contract and defaults are unchanged (SiemSinkRequestTest). Connector
validation moves from SiemSinkService into SinkConnectorSettings, with
the Sentinel rules in their own method, and updateSink() delegates the
generic fields to applyGeneralChanges(). SiemService::updateSink()
declares the InvalidArgumentException the controller catches. Spec tags
on the picker binding; prettier and eslint on the SIEM files.

Refs #782
…949)

Four EFF words carry a hyphen (drop-down, felt-tip, t-shirt, yo-yo), so
splitting a random default passphrase on '-' failed now and then. The
default is checked with a fixed random source; the content checks use a
space as separator.
Version001008Date20261002181000, so it sorts after #921's migration.
…on of 2 Oct)

Design, spec and tasks now describe one settings route pair per area,
the concrete area classes registered by Keepiq itself, one initial-state
key and mount element per area, and version and trash retention under
Policies. Tasks 1.1, 1.3, 2.1 to 2.5 and 3.1 to 3.3 are done; 1.2 and 3.4
need a live instance, 4.1 is due one minor release later.

Refs #774
…se-only-and-expiring-shares

# Conflicts:
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
Version001009Date20261002182000 and 0.3.4-unstable.20261002182000; the
previous merge had kept development's 180000, which would skip the upgrade.
…ped-roles

# Conflicts:
#	appinfo/info.xml
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
#	lib/Controller/SiemSinkController.php
#	lib/Service/AdminSettingsService.php
#	src/views/settings/Settings.vue
* feat(offline): baseUpdatedAt precondition on secret update and delete, offline edits setting

PUT and DELETE /api/v1/secrets/{id} accept an optional baseUpdatedAt. When
the secret changed since, nothing is written and the answer is 409 with the
current row. The admin setting offline_edits_enabled (default off) travels in
the offline manifest as offlineEditsEnabled.

Refs #785

* style(offline): named argument for StaleWriteException

* feat(offline): sealed edit queue store with coalescing

A queue store in the offline IndexedDB database (schema version 2). Each
entry's values and index fields are sealed to the owner's own certificate
with the hybrid envelope; only ids, the operation, the base version, the
queue time, the suite and the status are plain. Changes to one secret
coalesce. The snapshot now carries each secret's updatedAt and the
administrator's offline edits rule.

Refs #785

* feat(offline): queue offline edits and replay them with a fresh fan-out

While served from the cache with offline edits on, the secret store queues
creates, edits, moves and deletes instead of calling the server, and the
cached view shows them at once. Back online and unlocked, the queue replays
oldest first: owner rows through POST, PUT and DELETE with the base version,
then the recipient fan-out with shares and certificates fetched at that
moment. A 409 waits for the user's choice, 403 and 404 go to a failed list,
423 and network errors stop and keep the rest, and a failed recipient sync
retries only that step. A compromise recovery waits until the queue is empty.

Refs #785

* style(offline): lint

* feat(offline): offline edit screens, sync panel, conflict dialog and admin switch

Offline with offline edits on, the secret sidebar offers edit, move and
delete, New secret stays available, and queued changes show as not synced
yet. Sharing and attachments stay online-only and say so. A sync panel
under the banner shows how many changes wait, lets the user settle a
conflict (both versions decrypted in the browser), copy or discard a
refused change, and reopen changes made before a key change elsewhere with
the previous master password. Leaving the page with pending changes asks
first. The admin offline cache section gets the offline edits switch.

Refs #785

* docs(openspec): archive clients-offline-edits

Spec deltas synced: the new offline-edit-queue capability, and the
strictly read-only requirement removed from offline-readonly-cache. Parity
row clients-19 set to built. Code tags that named the removed requirement
point at the online-only requirement that replaces it.

Closes #785

* refactor(offline): OfflineEditGuard, a separate update controller, @SPEC on the queue methods

The baseUpdatedAt precondition moves out of SecretService into a small
OfflineEditGuard, used by update() and by findOwned() for the trash path,
so SecretService keeps its method count and the trash service needs no
static call. PUT /api/v1/secrets/{id} moves to SecretUpdateController,
which reads the changed fields from the request, so SecretController's
coupling and update()'s parameter list stay within the limits. The offline
switches leave getAdminSettings() for their own reader. Seven queue methods
get their @SPEC tag.

Refs #785
…w-device-approval

# Conflicts:
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
#	lib/Service/AdminSettingsService.php
feat: Kubernetes operator for Keepiq application secrets (stacked on #914)
…red by their own registrar

Move the per-area settings routes to AdminAreaSettingsController (with a
user rate limit on each write), report adminAreas from SettingsController
instead of SettingsService, register the area classes in AdminAreaRegistrar,
and name areas through AdminAreaAuthorizer constants where a controller was
over the coupling limit. Place development's new extension idle setting in
the Policies area.

Refs #774
…ped-roles

# Conflicts:
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
…se-only-and-expiring-shares

# Conflicts:
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
#	src/components/SecretDetailSidebar.vue
… fill (replaces #937) (#950)

* build(extension): a package per store target with the tag version; drop the unused scripting permission

build.mjs takes --target chrome|firefox and --outdir, and EXTENSION_VERSION
sets the manifest version. The build clears only its own package folder.
The manifest loses scripting, which nothing calls, and its description is
rewritten as store copy. Tests check both manifests, that two builds are
byte-identical, and that every permission has a call site.

Refs #783

(cherry picked from commit 2795d51)

* feat(extension): version handshake with the Keepiq server

The pair response carries the server's app version. The extension stores
it per account, checks it again at unlock, and with a server older than
its minimum shows an update message instead of the vault and sends no
match request.

Refs #783

(cherry picked from commit fb74568)

* feat(extension): fill the one-time code on the step after the login

After a login fill whose site has a totp secret and no code field on the
page, the worker keeps a one-shot intent in chrome.storage.session: tab
id, registrable domain, totp secret id and an expiry five minutes out, no
seed and no code. The content script reports a code field that appears
later (on load or through a throttled MutationObserver), once per field.
The worker fills it only for that tab and site, before the expiry, while
the vault is unlocked, in the reporting frame, and deletes the intent
first. Lock and unpair clear every intent.

Refs #783

(cherry picked from commit ab09c48)

* ci(extension): release workflow, store credentials, privacy policy and rollout docs

extension-release.yml runs the extension tests, builds each target twice
and fails when the builds differ, lints the Firefox package and packs both
with a source archive. On an extension-v tag a job in the protected
extension-stores environment submits to the Chrome Web Store, Firefox
Add-ons and Edge Add-ons and attaches the packages to a GitHub release.
docs/browser-extension holds the privacy policy, one line per permission,
the enterprise rollout and the release steps with placeholder credentials.

Refs #783
Refs #756

(cherry picked from commit ba76b9c)

* test(extension): point the next-step code fill test at the change's own requirement

The requirement lives in the clients-extension-store-release spec delta until
that change is archived, so gate-46 could not resolve the main-spec anchor.
…compares

ApplicationSecretsController reached phpmd's class complexity threshold with
the If-Match precondition. The comparison (any listed strong tag or *) moves
to MachineSecretEnvelopeService::ifMatchHolds(), next to etag(); the
controller keeps the 412 response. Unit test covers current, *, a list,
stale and weak tags.
rjzondervan and others added 28 commits October 5, 2026 16:19
6.2 with OpenRegister disabled: Nextcloud pages 200, health 200 ok,
metrics 200 for an admin, preferences round-trip, settings/load 404,
WASM CSP still sent; the UI checks were done in the browser. The
cleanup step removed the doriath/keepiq registers, the example schema
and its empty data table (51->49 registers, 191->190 schemas), and a
second run removed nothing.
6.3 with OpenRegister enabled: the flows API answers 200 and the MCP
endpoint lists exactly keepiq.expiryReport, listEntries, rotationStatus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README now lists Keepiq's own monitoring and preferences endpoints
with their callers and responses, and the demo-environment page says
OpenRegister is required by the demo suite, not by Keepiq.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependabot reports DOMPurify below 3.4.16 as vulnerable, and the app's own
lockfile resolved 3.4.15. The range now starts at 3.4.16 and the lockfile
resolves it; no other package changes.
- appinfo/info.xml <version> moves to 0.3.4-unstable.20261005160000.
  Without it `occ upgrade` answers "No upgrade required" and the new
  RemoveLegacyRegisterRows repair step never runs on existing instances
  (hydra gate-110).
- StaticRouteTableTest no longer assumes OpenRegister is absent: CI
  installs it. The table test now holds on either instance shape.
- Prettier formatting for App.vue, standalone-shell.spec.ts and
  navEntries.spec.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Legacy-row cleanup (RemoveLegacyRegisterRows):
- Another app's register that lists a removed schema loses only that
  entry; every other entry is kept exactly as stored (no intval rewrite).
- Rows are deleted first and empty data tables dropped last, so a
  failure partway never leaves a schema pointing at a dropped table.
- A keepiq/doriath register is kept while objects are stored under it.
- Keepiq's openregister_configurations entries are removed once nothing
  they list still exists, also on an instance an earlier run already
  cleaned up (verified on the dev instance: configurations 9 and 14).

Shell:
- Metrics keeps serving keepiq_info and keepiq_up when the suite count
  fails, as the AppHost engine did; the series is left out and logged.
- Health no longer reports a failed filesystem when only removing the
  probe file fails.
- One isAppEnabled/currentAppsWebRoots helper in navEntries.js decides
  app presence for the menu gates and the AI companion.
- The unused `openregisters` settings flag, its store field and the
  IAppManager it needed are removed.
- docker-compose.yml says what the unconditional ../openregister mount
  really does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blockers from the PR review, each verified in OpenRegister's source:
- Data tables are found by their physical name in the database catalogue
  (as OpenRegister's SchemaDeletionService does), not from a register's
  schemas list, which can lose entries. When the catalogue cannot be
  read, nothing is removed.
- Emptiness counts every data table of a schema or register, plus the
  legacy openregister_objects table only while it still exists
  (OpenRegister drops it after its blob migration).
- Registers are selected by `application`, never by slug: OpenRegister's
  key is (organisation, application, slug). A register without an
  application is never deleted.

Concerns and minors:
- All row changes run in one transaction with rollback; empty tables
  are dropped after the commit, each after a final row count.
- Every removal is logged first with table, id, slug, application and
  the counts that justified it.
- The planning moved into a read-only LegacyRegisterPlanner (phpmd
  class complexity); the step applies the plan.
- Tests cover a lost register link, a Keepiq register with populated
  tables, another app's keepiq-slug register, a failure followed by a
  converging run, and a row written before the drop. Mutation-checked.
- Health checks the temp directory without writing a file; preference
  values are capped at 4 KB; the README recommends a dedicated,
  General-area-delegated account for Prometheus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(mobile): fdroid and play flavours, free-software check and reproducible build in CI

Tasks 6.1 and 6.2 of clients-mobile-apps. The flavours differ only in the
update check, and neither has one: Play Core is not free software, and both
stores update the app themselves. mobile-fdroid.yml fails on any non-free
artifact in the fdroid or play release runtime classpath and builds the
unsigned fdroid release APK twice, in two clean checkouts at different
paths, and compares them byte for byte. Build tools pinned, vcsInfo off.
The preview, e2e and unit test workflows use the flavoured task names.

* feat(mobile): store texts and screenshots for F-Droid and Google Play (draft)

Task 6.3. English and Dutch title, short and full description and the first
changelog in mobile/fastlane/metadata/android, with eight screenshots from
docs/static/media/mobile. Draft for the product owner's review.

* docs(mobile): privacy policy for the mobile apps, FEATURES.md to current state

Task 7.3. docs/mobile/privacy.md says what the app sends where (your
Nextcloud, the server of a Send link you open, a website's public
assetlinks.json when another app asks for a login) and what it stores.
using.md links it.

* docs(mobile): developer README

Task 7.2. Layout, building the shared core, Android and iOS, the crypto
vectors, the e2e harness, the flavours and the F-Droid rules.

* test(mobile): control, a Google Play Services dependency in the fdroid flavour

Must make the free-software job of mobile-fdroid.yml fail and name the
artifact. Reverted in the next commit.

* Revert "test(mobile): control, a Google Play Services dependency in the fdroid flavour"

The control has run (mobile-fdroid.yml run 37354683074); the fdroid
flavour is free software again.

* docs(openspec): tick mobile tasks 6.1, 6.2, 6.3 (draft), 7.2 and 7.3 with their CI runs
- AdminAreaSettingsTest::testInfoXmlRegistersEveryArea read info.xml
  with simplexml_load_file(). Under the Nextcloud bootstrap in CI,
  lib/base.php blocks libxml's external entity loader, which
  simplexml_load_file() also uses to open the file, so it got false and
  failed on iterator_to_array(null). It now parses the file contents,
  as NextcloudFloorMatrixTest and ConnectionsDeclarationTest already do.
- admin-api.postman_collection.json declared only its auditor variables,
  so all 23 requests went to the literal `{{baseUrl}}` (getaddrinfo
  ENOTFOUND). It now declares baseUrl, noAuthBase, adminUser and
  adminPass with the same values as the other two collections.

Both failures predate this branch (#1173 shows the same).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
phpunit.xml sets beStrictAboutCoverageMetadata and failOnRisky, so a
test that runs a class its @Covers does not declare is risky, and a
risky test fails the run. Ten tests did (the same ten on development),
which was hidden while AdminAreaSettingsTest errored first:

- ExtensionControllerTest uses AdminSettingsService and
  PasswordPolicyService.
- SettingsControllerConnectionRefreshTest uses AdminAreaSettingsController.
- SiemConnectionReportCallersTest uses Siem\SinkConnectorSettings.

Each @uses names exactly the classes CI reported for that test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(app-shell): Keepiq runs without any other app
…nment

Closes the gaps of #863/#864 left on development (keepiq#1189):

- A share-target lookup that throws while resolving a copy's source is
  counted as a containment failure; the source owner was not warned but
  the cascade reported complete.
- When revoking the other end of a migration throws, the named suite is
  already revoked: its containment now runs in a finally on the radius
  collected before any revoke. Before, it was skipped, and a retry
  collected after the revoke and lost the source owners.
- The containment tally is audited as suite.compromise_contained, flagged
  incomplete when a step failed, instead of reaching only the response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PARAM_DATE is deprecated since NC 31 and is defined as
Types::DATETIME_MUTABLE, exactly what PARAM_DATETIME_MUTABLE is, so this
is a pure rename. All bound values are \DateTime and the compared columns
(keepiq_audit_log.occurred_at, keepiq_ca_certs.expires_at) are
Types::DATETIME.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M7DxSg7d7wqqZsUppLWf7J
…not refused

Self-review of #1190:

- A failure to revoke the other end or end the migration, after the named
  suite was revoked and contained, fell into forceRevoke's RuntimeException
  branch: a 403, a suite.revoke_refused (forbidden) audit for a revoked
  suite, and the containment tally dropped. It is now caught, counted as a
  failed step, audited as migrationEndFailed and answered with a 500
  `migration_end_failed` that carries the tally.
- A retry on a suite left revoked with its migration open only finishes
  the migration. Only a compromise force-revoke revokes a migrating suite,
  so its containment already ran; running it again warned everyone twice.
- A failed source lookup no longer also attempts the outbound lookup, so it
  counts as one failure.
- The admin force-revoke moved into AdminSuiteRevocationService; the
  controller had outgrown phpmd's class-length and parameter limits.
- Tests pin the exact failure status, body and audit, assert no refusal is
  recorded, and cover a failed named revoke and a retry that fails again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refactor(nc35): replace deprecated IQueryBuilder::PARAM_DATE
…te containment

Re-review of #1190:

- The retry shortcut took "revoked with the migration still open" as
  proof that containment ran. An attempt can stop between the revoke and
  contain() (a throw after the status update, a killed worker, a failed
  audit write), and every later retry then skipped containment and
  answered success. The shortcut now requires a suite.compromise_contained
  audit entry for the suite since the migration started, with no failed
  containment step. No entry, a failed step or an unreadable trail runs
  the whole path again, the revoke included, so a revoke cascade that
  stopped is redone too.
- The migration_end_failed message no longer claims containment when
  containment steps failed; the admin UI renders the result and cascade
  cards for that 500 instead of only the message.
- A retry whose migration end fails again is recorded as
  suite.revoke_refused with reason code migration_end_failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(mobile): give the apps Keepiq's lock as launcher and store icon

Android: an adaptive icon (brand blue background, the lock from
img/pwa-icon-maskable.svg as a VectorDrawable foreground inside the 66 dp
safe zone, the same layer as the monochrome icon for themed icons),
legacy PNG mipmaps, and android:icon/roundIcon on the application.
Store: a 512 px icon.png for en-US and nl-NL in the fastlane metadata.
iOS: an AppIcon asset catalog with one 1024 px icon.

* fix(mobile): pad the phone screenshots to Google Play's 2:1 limit

The screenshots were 2.22:1 and Play takes at most 2:1. Each one is padded
evenly left and right, each padding row filled with that row's edge
pixel, to 557x1100 (1-7) and 1214x2400 (8), 1.98:1. No content is cut,
and the one set in the fastlane layout serves F-Droid and Play alike.

* docs(openspec): note the store icon and Play-sized screenshots under task 6.3

* fix(mobile): draw the legacy launcher PNGs straight from img/pwa-icon.svg
…mpt after Keepiq's forms (tasks 1.6.1, 3.2.1) (#1184)

* feat(mobile): encrypted offline store on iOS (task 1.6.1)

SQLCipher Community Edition 4.19.0, built from the pinned release source
(SHA-256 checked) into the KeepiqShared klib per iOS target, linked by the
SQLDelight native driver instead of the system SQLite. The database key is
32 random bytes per account in a non-biometric Keychain item
(AfterFirstUnlockThisDeviceOnly, through KeychainStorage). The store refuses
to open when SQLCipher is missing (PRAGMA cipher_version), and a file whose
key is gone starts again empty. Unpair deletes file and key; suite change
and offline caching off empty it through VaultSync, as on Android.

The iOS app opens its sessions through IosVaultSession with the store and
sync; the lock and wrap deletion of a sync reach AppModel. Tests: the store
on the iOS simulator (IosEncryptedStoreTest) and OfflineUITests, which reads
the stored vault after the replay drops every connection
(POST /__e2e/offline).

* fix(mobile): no "Save Password?" after Keepiq's own iOS forms (task 3.2.1)

iOS reads the user name and password fields when they leave the view
hierarchy and offers to save what it finds (WWDC 2018 session 204: "Make
sure to only clear the username and password fields after they've been
removed from the view hierarchy. This way, we can read out the data and
save it into credential."). The item form, the Send form and the AutoFill
sheet's add-login form now empty their secret fields once the save went
through, wait a moment for the fields to update, and only then leave. The
fields stay SecureFields, so VoiceOver announces them as secure as before.

VaultFlowsUITests.expectNoSavePasswordPrompt fails again when the prompt
comes; AutofillUITests records whether it came after the add-login form.

* test(mobile): opt in to the foreign API in the iOS store test

* fix(mobile): MobileSession keeps its store to close it on lock

* test(mobile): answer the save prompt before the offline test types

* build(mobile): name each iOS simulator test in the log

* test(mobile): the offline test taps until the field has focus, five tries
…proven records

Third review pass of #1190:

- The audit fake now asserts the exact lookup (this event, object type,
  suite and the migration's start, limit 1), and the migration fixture
  has a start, so dropping or changing a filter fails the tests.
- A record without an integer failed count no longer passes as complete:
  {"migrationEndFailed": true} computed 1 - 1 = 0 and skipped containment.
  Found by the new data provider (no metadata, empty, a failed step, no
  count); all of them now contain again.
- A migration without a start contains again instead of querying with no
  time bound.
- AuditEntryMapperTest runs the real findFiltered query: an entry before
  the migration, another event type and another suite are excluded, and
  the newest match is returned. It skips without a database, like
  SecretRequestMapperTest, and runs in CI.
- The store only treats a 500 as migration_end_failed; the store and
  controller docblocks no longer say the suite was contained.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nment

fix(encryption-suites): count, always run and audit compromise containment
The 17 alerts GitHub Advanced Security reports on development (and on the
development -> beta release PR #834):

- actions/missing-workflow-permissions (12): the repository's default
  token is write-all. Every flagged workflow now declares its permissions:
  contents: read for the self-contained ones, contents: write only on the
  cli-release jobs that attach to a release, and for callers of the shared
  ConductionNL/.github workflows exactly the union the called jobs declare
  (release, sync-to-beta: contents + pull-requests write; documentation:
  contents + packages write; issue-triage, openspec-sync: issues write).
- js/polynomial-redos (sdk/js): the trailing-slash strip of the caller's
  address backtracked quadratically on a long run of slashes; replaced by
  a loop, with a test for an address with trailing slashes.
- js/incomplete-url-substring-sanitization (3, extension tests): the
  assertions compared URL prefixes, which also matched other hosts; they
  now compare the parsed origin.
- js/insecure-randomness (e2e helper): the run-unique user name suffix
  uses crypto.randomInt instead of Math.random.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(ci): resolve the open code-scanning alerts on development
The generic Reports entry leaves the footer for the settings foldout,
which nextcloud-vue now labels Advanced.
…enable

The push runs on development test Nextcloud stable32-34; pull requests
only run stable35, so these failures never showed on a PR.

- FederatedSharesMigrationTest and SiemConnectorColumnsMigrationTest
  mocked stdClass when ISchemaWrapper::getTable() declares no return type
  (Nextcloud 34 and older), and stdClass has no hasColumn(). They now use
  a plain recording table there, as ConsolidatedSchemaMigrationTest does.
  The migrations were fine: getTable() returns a Doctrine Table there.
- The federation tests that use the OCM APIs of Nextcloud 33 skip on 32,
  where FederationRootService::isSupported() is false and federation is
  off by design. The skip uses the same check.
- The standalone and two-instances setups hit a transient 404 on the
  first API call right after app:enable, once each on 4934a64 (and green
  on the other run of the same commit). They now wait for the API first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same three failed, retry included, on every E2E run since at least
4f942f7:

- l10n-browser-catalogue picked the first entry whose value differs from
  its key as the t() probe. Plural entries (`_singular_::_plural_`) hold
  an array of forms, so they always qualified, and t() cannot resolve
  them. The probe now takes a plain string entry.
- member-overview-offboarding expected "1 team folders."; the summary is
  pluralised now and says "1 team folder.".
- vault-unlock walks every manifest route while locked. Each goto is a
  full page load, about 4.6 s a route in CI (measured from the trace), so
  the 17 routes ran out of the default timeout at route 15. The timeout
  now scales with the number of routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With its timeout fixed, the locked-vault leak test reaches its assertion
for the first time since device approval came to the lock screen (#1024),
and finds two requests the lock screen itself makes:

- device-approvals/status: checkSuite() asks, whenever online, whether to
  offer "Approve from another device". Answers {enabled}.
- recovery/enrolment: with device approval on, DeviceApprovalRequest
  reads the user's own enrolment to decide whether to offer the
  administrator path. Answers {enrolled, policy}.

Neither carries vault material. Run locally with Playwright against an
instance with device approval enabled: all three specs fixed on this
branch pass (10 passed, 1 skipped), and the development versions of the
l10n and offboarding specs fail exactly as in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(ci): green PHPUnit on Nextcloud 32-34 and Playwright on development
Release: merge development into beta
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ 93d30da

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
test-l10n ✅
format ✅
check-l10n-js ✅
check-schema-l10n ✅
composer ✅ ✅ 114/114
npm ✅ ✅ 660/660
app:check-code ⏭️
info.xml ✅
REUSE ✅
lockfile sync ✅
PHPUnit ✅
Newman ✅
Playwright ✅
Hydra gates ✅

Quality workflow — 2026-10-07 12:51 UTC

Download the full PDF report from the workflow artifacts.

@rjzondervan
rjzondervan merged commit 26fc076 into main Oct 7, 2026
97 of 100 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compromise force-revoke: four paths still fail open (follow-up to #863, #864)

4 participants