Skip to content

Release: merge development into beta - #834

Merged
rjzondervan merged 366 commits into
betafrom
development
Oct 7, 2026
Merged

rjzondervan merged 366 commits into
betafrom
development

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

Comment thread .github/workflows/browser-extension.yml Fixed
)

* feat(generator): generate keys and passphrases in the browser, never on the server

* feat(generator): passphrases, an admin switch for them, spec, docs and translations

The generator dialog offers Password or Passphrase. A passphrase is 4 to
12 words from the EFF large word list, with a separator, capitals and a
digit, and is extended to meet the org policy. Administrators can switch
passphrases off. The change supersedes health-passphrase-generator, as
that change's own D1 asked once generation moved to the browser, and
row health-08 is built.
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
Comment thread tests/extension/accounts.spec.js Fixed
Comment thread tests/extension/accounts.spec.js Fixed
rubenvdlinde and others added 5 commits October 2, 2026 16:01
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
Comment thread .github/workflows/cli-release.yml Fixed
Comment thread sdk/js/src/client.ts Fixed
rubenvdlinde and others added 17 commits October 2, 2026 16:10
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
* 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
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ d4a3ecb

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-05 22:19 UTC

Download the full PDF report from the workflow artifacts.

rjzondervan and others added 3 commits October 6, 2026 09:12
- 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
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ 50a0cf3

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-06 08:03 UTC

Download the full PDF report from the workflow artifacts.

rjzondervan and others added 11 commits October 6, 2026 10:41
…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>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ 3e5f683

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-06 14:05 UTC

Download the full PDF report from the workflow artifacts.

fix(ci): resolve the open code-scanning alerts on development
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ ff8ddca

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-06 14:42 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 2 commits October 7, 2026 09:49
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>
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ 99bd9eb

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 08:57 UTC

Download the full PDF report from the workflow artifacts.

rjzondervan and others added 3 commits October 7, 2026 11:00
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
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ ba79da7

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 11:34 UTC

Download the full PDF report from the workflow artifacts.

@rjzondervan
rjzondervan merged commit 1181e45 into beta Oct 7, 2026
149 of 156 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.

5 participants