Skip to content

feat: rename the app from doriath to keepiq - #414

Merged
rubenvdlinde merged 10 commits into
developmentfrom
feat/rename-doriath-to-keepiq
Aug 22, 2026
Merged

feat: rename the app from doriath to keepiq#414
rubenvdlinde merged 10 commits into
developmentfrom
feat/rename-doriath-to-keepiq

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Third app through the full app-id rename, following the merged pilot (planninq#336) and the procedure at renaming-an-app. 935 files, 14 files + 3 directories renamed.

This is the secrets vault, so the interesting part is what does not move.

Frozen — renaming these locks users out

Identifier Consequence of renaming
doriath-passkey-kek-v1 an HKDF info string — one byte derives a different KEK and every registered passkey is locked out of the vault
doriath-cxp-v1 equality-checked and fed into the HPKE binding info
EXPECTED_AUDIENCE = 'doriath' registered applications sign aud=doriath with keys the server does not hold
attachment appdata namespace IAppDataFactory->get(APP_ID) would point at an empty folder: uploads keep working, every existing attachment 404s, nothing logs
.doriath-backup, doriath://, .well-known/doriath, Doriath-Lease-*, doriath_* tables published wire contracts and users' existing encrypted exports

Each is commented at its definition so nobody later "finishes the job".

Browser state needed the opposite treatment

The offline snapshot database and service-worker shell cache did move to the new names — which would have stranded a returning user's cached vault data under the old ones, surviving every lock, logout and rotation, unreachable by the code meant to evict it. That breaks the eviction guarantee the cache is built around, and for a secrets vault it means encrypted vault data left in browsers that the app can no longer purge. purge() now also deletes the legacy database, and the activate handler also matches the legacy cache prefix.

Migration

MigrateUserPreferences does not use the pilot's getUsersForUserValue pattern: three of the eleven per-user keys are open-valued, so no finite value list is exhaustive. It walks callForSeenUsers(). Every reader supplies a default, so loss is silent — an opted-out user gets re-subscribed, offline caching re-enables for someone who disabled it, and session_timeout reverts, potentially lengthening the unlocked-vault window.

Verification

composer check:strict — ALL CHECKS PASSED; Psalm and PHPStan clean; 1086 PHP tests, 652 frontend tests, manifest and l10n-parity pass.

Known gap: the Go CLI's module path and imports were updated but go build was not run (no Go toolchain available here) — worth a check before release. DNS/CNAME move separately.

🤖 Generated with Claude Code

Completes the rename through the app id: id, display names, PHP
namespace, frontend URLs, l10n domain, CLI, CI and docs. 935 files.

Two repair steps carry stored data across, under both <install> and
<post-migration>. MigrateUserPreferences does not use the pilot's
getUsersForUserValue pattern: three of the eleven per-user keys
(session_timeout, expiry_max_age_days, default_secret_type) are
open-valued, so no finite value list is exhaustive. It walks
callForSeenUsers() + getUserKeys() instead. Every reader supplies a
default, so losing these is silent and unsafe: an opted-out user gets
re-subscribed to notifications, offline vault caching re-enables for
someone who turned it off, and session_timeout reverts - potentially
LENGTHENING the unlocked-vault window.

This is a secrets vault, so several identifiers are load-bearing in a
way a rename must not touch. Frozen, each commented at its definition:

- doriath-passkey-kek-v1, an HKDF info string. Changing one byte derives
  a different KEK and locks every registered passkey out of the vault.
- doriath-cxp-v1, equality-checked AND fed into the HPKE binding info.
- EXPECTED_AUDIENCE = 'doriath': registered applications sign aud=doriath
  with keys the server does not hold, so rotating it is a coordinated
  apiVersion bump.
- the attachment appdata namespace. IAppDataFactory->get(APP_ID) would
  have pointed at an empty folder: uploads keep working while every
  existing attachment 404s, and nothing logs it.
- the published wire contracts: .doriath-backup (users' existing
  encrypted exports), doriath://, .well-known/doriath, Doriath-Lease-*,
  and the doriath_* tables, indexes and cache namespaces.

Browser-side state needed the opposite treatment. The offline snapshot
database and the service-worker shell cache DID move to the new names,
which would have stranded a returning user's cached vault data under the
old names - surviving every lock, logout and rotation, unreachable by
the code meant to evict it. purge() now also deletes the legacy database
and the activate handler also matches the legacy cache prefix, so the
eviction guarantee this cache is built around still holds.

References to other apps' ids are untouched: those apps still declare
their old <id>, and the references are duck-typed lookups whose failure
path is a silent no-op.

composer check:strict passes; 1086 PHP tests, 652 frontend tests.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ 91a840e

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

Quality workflow — 2026-08-22 09:13 UTC

Download the full PDF report from the workflow artifacts.

Two CI failures, both genuinely introduced here - development's own
eslint and format checks are green, so these are not inherited.

- src/offline/cache.js: the catch block I added for the legacy-database
  eviction bound an unused `e`. Bare `catch` instead.
- 32 files reformatted with the project's own format:fix. Renaming
  doriath to keepiq changes string lengths, so prettier wants different
  line wrapping. Checked afterwards that it only rewrapped the legacy
  cache-prefix condition rather than altering it, and the 652 frontend
  tests still pass.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ 94f6fea

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

Quality workflow — 2026-08-22 10:02 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot added 2 commits August 22, 2026 12:33
gate-16 is diff-scoped per ADR-020, so a method changed in a PR comes
into scope. The rename edited an identifier inside 192 methods, which
inherited their pre-existing annotation debt. Gate now reports count=0.

186 methods got real requirement anchors and 6 got reasoned exclusions.
Every one of the 130 distinct anchors cited was machine-verified against
a slug table built from the 298 live requirement headings across the 45
specs, so none of them merely looks plausible.

The exclusions each name where the behaviour IS specified rather than
saying "no spec": the two app-id migration steps move oc_appconfig and
oc_preferences rows and add no behaviour of their own, so they cite the
specs for the settings they preserve; the two development seeders are
gated on the debug system value and cite the specs for the real thing
they fake; the two Vue prop default() factories declare a translated
fallback label and cite where the labelled behaviour lives.

Noted for a follow-up, not fixed: 31 change names referenced by EXISTING
@SPEC tags no longer resolve. They point at openspec/changes/<name>/,
but those changes were archived to
openspec/changes/archive/<date>-<name>/, so the date prefix breaks every
path - roughly 250 tags, add-emergency-access alone carrying 42. PHPCS
corroborates it from the other side: its sniff already says to link back
to openspec/specs/{capability}, not a change dir. Every NEW tag here
points at a live promoted spec path.
The eslint job failed on "There are suppressions left that do not occur
anymore" - eslint exits non-zero for unpruned suppressions, even at zero
errors. The rename changed the code those entries pointed at, so three
of them no longer describe a real problem.

Pruned with eslint's own --prune-suppressions rather than hand-edited,
so the file still describes exactly the problems that remain. 0 errors
either way; the 170 warnings are pre-existing.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ c7e413d

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

Quality workflow — 2026-08-22 10:48 UTC

Download the full PDF report from the workflow artifacts.

Coverage on the two repair steps was ~32% of 273 added statements, and
the coverage guard was right to block: these carry every stored setting
across the doriath -> keepiq app-id rename, and their failure mode is
silent. Readers all supply a default, so a lost preference does not
error, it reverts - an opted-out user gets re-subscribed, offline vault
caching re-enables for someone who disabled it, and session_timeout
reverts, potentially LENGTHENING the unlocked-vault window.

16 tests / 23 assertions -> 33 / 80. The later tests run against an
in-memory two-namespace store, so they assert the values that land
rather than that a mock was called. All eleven USER_PREF_KEYS are pinned
verbatim, including the falsy-but-real offline_cache_optin='0' and the
open-valued session_timeout. The enumeration STRATEGY is pinned too:
callForSeenUsers is used and getUsersForUserValue is asserted never to
be called, because three keys are open-valued and the value-enumerating
form would migrate nothing while reporting success.

Every test was mutation-checked: the never-overwrite guard, the reserved
key list, the callForSeenUsers strategy and each catch clause were
broken in turn and the suite confirmed RED, then restored - both lib
files are byte-identical to HEAD.

That exercise found a test that could not fail. testSkipsReservedKeys
stubbed getValueString to return the same value for BOTH namespaces, so
the never-overwrite guard suppressed the write and the test stayed green
with RESERVED_KEYS emptied - it never tested the reserved skip at all.
Fixture corrected; it now fails under that mutation.

One behaviour worth knowing, not a bug: an old value of '' is treated as
nothing-to-migrate in both classes, so a stored empty string is never
copied. Harmless while readers default to '', but it is a decision.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ 5bbdc4b

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

Quality workflow — 2026-08-22 10:59 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ a573a45

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

Quality workflow — 2026-08-22 11:16 UTC

Download the full PDF report from the workflow artifacts.

Same defect filinq had, found by writing the tests: both getValueString
reads in MigrateAppConfigKeys sat OUTSIDE the try that was meant to
contain them. Only the write was guarded, so an unreadable value
propagated out of run().

That matters more here than it looks. This step is registered under
<install> as well as <post-migration>, so a repair step that throws does
not merely fail an upgrade - the app never enables, and every route goes
with it. The class docblock promises the opposite ("every failure is
logged and the loop continues").

Both reads moved inside the try. MigrateUserPreferences was already
correct - its per-user reads, its key enumeration and callForSeenUsers
are each guarded - so only this class changed.

The new test makes the read throw at the IAppConfig seam and asserts
run() RETURNS and the key after the unreadable one still migrates.
Mutation-checked: moving the read back outside the try turns it into an
error, restoring it makes it pass again.

composer check:strict passes; 1104 tests.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ cf8ea67

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

Quality workflow — 2026-08-22 11:33 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot added 2 commits August 22, 2026 13:37
gate-46 (spec-anchor-existence) rejected it: I copied
'@SPEC openspec/specs/app-identity/spec.md' from a sibling app that HAS
that spec, into two that do not. Precisely the failure the gate exists
for, and precisely the rule I had been enforcing elsewhere - verify the
target resolves rather than that the tag looks plausible.

Both now use a reason-bearing @SPEC exclude matching what the class
under test already declares: the step moves config rows between
namespaces and adds no behaviour, so there is no capability spec to
point at; what the test pins is the step's own safety contract.
KeepiqNotifier was DoriathNotifier and had never been tested. The rename
moved the file, and the coverage guard reads a rename as delete+add, so
its 180 uncovered statements became "new untested code" - the whole of
the reported coverage drop, since the two migration classes are at 100%.
The guard documents this as a deliberate incentive: a renamed class is
new code as far as the suite is concerned. So it is now covered.

34 tests, 148 assertions, roughly 98% of the file: both prepare()
rejection paths, all 16 subjects across the four renderers pinned on
exact subject/message/link, every placeholder fallback, and
share_request_result failing closed to "denied" on garbage input. Left
uncovered on purpose: two terminal defensive branches unreachable while
every subject has a renderer. Mutation-checked by renaming a subject
case - 6 errors, assertions 148 -> 88 - then restored.

Fixes a real hazard the tests exposed: withSecretLink() built its route
from the literal 'keepiq.dashboard.page' instead of from APP_ID. Route
names are namespaced by the app id, so that is correct only while APP_ID
happens to equal 'keepiq' - the next rename moves the route and leaves
the literal behind. The catch beside it swallows the failure, so every
secret deep-link would have quietly stopped appearing with no error and
no log. Derived from Application::APP_ID now, with the catch documenting
why it was dangerous and what to do if another failure mode appears.

Also notes, without changing: the class sets only parsed subject/message,
never rich parameters, so notifications render as flat text with no
clickable chips. Behaviour, not a bug.

composer check:strict passes; 1138 tests.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ e677a46

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

Quality workflow — 2026-08-22 11:45 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ d9b827c

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

Quality workflow — 2026-08-22 12:00 UTC

Download the full PDF report from the workflow artifacts.

Newman failed on every run with the discovery document entirely
undefined. The endpoint is frozen at /.well-known/doriath - a published
wire contract that OpenConnector's resolver is pinned against - but the
test was requesting /.well-known/keepiq.

What made this hard to see is where the rename landed. A Postman URL
carries the address TWICE: a human-readable `raw` string and a `path`
array. The rename updated the array and left the raw string alone, so
the diff read correctly - raw still said `doriath`, which is the value a
reviewer checks - while Postman built the request from the array and
went somewhere that does not exist.

Swept every collection for the same divergence by rebuilding each
request URL from its path array and comparing it to its own raw string:
zero remaining.

The earlier symptom pointed elsewhere and was worth chasing anyway: the
first runs 404'd because the app was not enabling at all, which the
repair-step fix resolved. This was the second, quieter cause underneath.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ d9b827c

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

Quality workflow — 2026-08-22 12:13 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ b4a76ad

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

Quality workflow — 2026-08-22 12:29 UTC

Download the full PDF report from the workflow artifacts.

`.secret-list-item__name` wraps `{{ secret.name }}` AND a <StrengthBadge>,
so `textContent` yields "AWS Console Very strong" once the badge resolves.
Both call sites compared that string for EXACT equality against a name from
the API, so the match silently stopped working — folder-sharing failed on
"moved secret must exist" while the move itself had persisted correctly, and
the trace confirms the new folderId was stored. The badge loads
asynchronously, so this was always a race the read happened to win; it is
latent on development too, not something the rename introduced.

Reading only the element's own text nodes makes the extraction independent
of badge timing at both call sites.

Also move keepiq's two `@spec exclude` docblocks onto the `default()`
factories themselves, which is where the spec-coverage gate looks.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ 2aa1cbb

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

Quality workflow — 2026-08-22 13:00 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 0e093e3 into development Aug 22, 2026
53 of 81 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/rename-doriath-to-keepiq branch August 22, 2026 15:00
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.

1 participant