diff --git a/.github/workflows/browser-extension.yml b/.github/workflows/browser-extension.yml index 1310697e0..c9197617a 100644 --- a/.github/workflows/browser-extension.yml +++ b/.github/workflows/browser-extension.yml @@ -1,46 +1,56 @@ name: Browser extension -# Builds the extension for Chromium and Firefox and loads each package in a -# headless browser to check its background starts and answers the popup -# (clients-browser-builds). Safari needs a macOS runner and stays an open task. +# Tests, typechecks, lints and builds the WXT extension in browser-extension/ +# for Chromium MV3 and Firefox MV2. The crypto tests read the web app's vectors, +# so a change to those runs this too. on: push: branches: [main, development] - paths: ["browser-extension/**", "src/crypto/**", "src/totp/**", ".github/workflows/browser-extension.yml"] + paths: ["browser-extension/**", "tests/vectors/crypto/**", ".github/workflows/browser-extension.yml"] pull_request: branches: [main, development, beta] - paths: ["browser-extension/**", "src/crypto/**", "src/totp/**", ".github/workflows/browser-extension.yml"] + paths: ["browser-extension/**", "tests/vectors/crypto/**", ".github/workflows/browser-extension.yml"] workflow_dispatch: permissions: contents: read jobs: - build-and-load: + check-and-build: runs-on: ubuntu-latest - # A build and two browser starts; minutes at most. - timeout-minutes: 20 + # Tests, two builds and a lint; minutes at most. + timeout-minutes: 15 + defaults: + run: + working-directory: browser-extension steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "24" cache: npm + cache-dependency-path: browser-extension/package-lock.json + # No install scripts from dependencies; `wxt prepare` is the one postinstall we need. - run: npm ci --ignore-scripts - - run: npm run build:extension - - name: Chromium loads the package + - run: npx wxt prepare + - name: No chrome.* in executable code run: | - npx playwright install --with-deps chromium - node browser-extension/load-check/chromium.mjs - - name: Firefox loads the package - run: | - npm install --no-save selenium-webdriver@4 - firefox --version - geckodriver --version - node browser-extension/load-check/firefox.mjs + if grep -rnE '(^|[^.[:alnum:]_])chrome\.' src entrypoints --include='*.ts' --include='*.tsx' | grep -vE '^\S+:[0-9]+:\s*(//|\*|/\*)'; then + echo "::error::Use browser.* instead of chrome.* (WXT-AND-BROWSERS.md § 1)" + exit 1 + fi + - run: npm test + - run: npm run typecheck + - run: npm run lint + - run: npm run build + - run: npm run build:firefox + - name: Lint the Firefox package + run: npx web-ext lint --source-dir .output/firefox-mv2 --warnings-as-errors=false - uses: actions/upload-artifact@v4 with: name: browser-extension - path: browser-extension/dist/ + path: | + browser-extension/.output/chrome-mv3/ + browser-extension/.output/firefox-mv2/ retention-days: 14 diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index c7a1396e7..b9e7da617 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -183,8 +183,7 @@ jobs: enable-eslint: true enable-phpunit: true enable-newman: true - # Keepiq needs no other app (ADR-006); standalone.yml proves that on an - # instance with Keepiq alone. OpenRegister is installed HERE so this + # Keepiq needs no other app (ADR-006). OpenRegister is installed HERE so this # suite covers the integrations that need it: the Flows pages and the # MCP tools, which appear only when OpenRegister is enabled. # diff --git a/.github/workflows/docs-media.yml b/.github/workflows/docs-media.yml deleted file mode 100644 index 3cbba7c5f..000000000 --- a/.github/workflows/docs-media.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Docs media - -# Captures the screenshots and videos of the browser extension for the user -# documentation (browser-extension/capture/). One Nextcloud with Keepiq alone -# (it needs no other app, ADR-006), seeded with the development vault; the capture fills it with -# demo items and visits local demo sites only. The media is uploaded as an -# artifact; committing it to docs/static/media/browser-extension/ stays a -# reviewed step. - -on: - push: - branches: [development] - paths: ["browser-extension/capture/**", ".github/workflows/docs-media.yml"] - workflow_dispatch: - -permissions: - contents: read - -jobs: - capture: - runs-on: ubuntu-latest - # Two builds, one Nextcloud install and two short browser runs. - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - with: - path: keepiq - persist-credentials: false - - uses: shivammathur/setup-php@v2 - with: - php-version: "8.3" - tools: composer:v2 - - uses: actions/setup-node@v4 - with: - # npm 11, which the lockfile needs; Node 22 ships npm 10. - node-version: "24" - - - name: Build Keepiq and the extension - working-directory: keepiq - run: | - composer install --no-dev --prefer-dist --no-interaction - npm ci - npm run build - npm run build:extension - - - name: Start the instance - run: docker compose -p kq-media -f keepiq/browser-extension/capture/compose.yaml up -d - - name: Provision it - run: bash keepiq/browser-extension/capture/setup.sh kq-media keepiq - - - name: Chromium screenshots and videos - working-directory: keepiq - run: | - npx playwright install --with-deps chromium - node browser-extension/capture/chromium.mjs - - name: Firefox screenshots - working-directory: keepiq - run: | - npm install --no-save selenium-webdriver@4 - firefox --version - geckodriver --version - node browser-extension/capture/firefox.mjs - - - name: Server log - if: failure() - run: docker exec kq-media-nc-1 tail -n 200 /var/www/html/data/nextcloud.log || true - - uses: actions/upload-artifact@v4 - if: always() - with: - name: docs-media - path: keepiq/browser-extension/capture/out/media/ - if-no-files-found: ignore - retention-days: 14 diff --git a/.github/workflows/extension-release.yml b/.github/workflows/extension-release.yml deleted file mode 100644 index 71e6be158..000000000 --- a/.github/workflows/extension-release.yml +++ /dev/null @@ -1,230 +0,0 @@ -name: Extension release - -# Builds, tests and packs the browser extension per store target -# (extension-store-release). On pull requests and pushes it proves the -# packages build twice to the same bytes and lints the Firefox one; store -# credentials are not reachable there. On an `extension-v` tag a -# second job, bound to the protected `extension-stores` environment that a -# maintainer approves, submits the packages to the Chrome Web Store, Firefox -# Add-ons and Edge Add-ons and attaches them to a GitHub release. -# -# Firefox Add-ons signs the listed package only after its review, which takes -# hours to days. A third job, `attach-amo-signed`, runs daily and on demand: once -# a released version is approved, it downloads the file Firefox Add-ons signed, -# checks it holds exactly the files of the package the release job built, and -# attaches it to the same GitHub release for administrators who host it -# themselves. It reads only public Firefox Add-ons data, so it needs no store -# credential and no environment approval. -# -# Store credentials and who approves the environment: docs/browser-extension-release.md. - -on: - push: - branches: [main, development] - tags: ["extension-v*"] - paths: ["browser-extension/**", "src/crypto/**", "src/totp/**", "src/policy/**", "src/health/**", ".github/workflows/extension-release.yml"] - pull_request: - branches: [main, development, beta] - paths: ["browser-extension/**", "src/crypto/**", "src/totp/**", "src/policy/**", "src/health/**", ".github/workflows/extension-release.yml"] - schedule: - # Daily check for Firefox Add-ons approvals of released versions. - - cron: "23 5 * * *" - workflow_dispatch: - inputs: - amo_version: - description: "Attach the Firefox Add-ons signed file for this released version (for example 1.2.0); leave empty to build" - required: false - default: "" - -permissions: - contents: read - -jobs: - build: - if: github.event_name != 'schedule' && (github.event_name != 'workflow_dispatch' || inputs.amo_version == '') - runs-on: ubuntu-latest - # Tests, two builds per target, a lint and a zip; minutes at most. - timeout-minutes: 20 - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "24" - cache: npm - - run: npm ci --ignore-scripts - - name: Extension tests - run: npx vitest run tests/extension - - name: Version from the tag - id: version - run: | - if [[ "$GITHUB_REF" == refs/tags/extension-v* ]]; then - v="${GITHUB_REF#refs/tags/extension-v}" - else - v="$(node -p "require('./browser-extension/manifest.json').version")" - fi - echo "version=$v" >> "$GITHUB_OUTPUT" - - name: Build each target twice and compare - env: - EXTENSION_VERSION: ${{ steps.version.outputs.version }} - run: | - for run in a b; do - for target in chrome firefox; do - node browser-extension/build.mjs --target "$target" --outdir "out/$run" - done - done - ( cd out/a && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 sha256sum ) > a.sha - ( cd out/b && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 sha256sum ) > b.sha - diff -u a.sha b.sha - - name: Lint the Firefox package - run: npx --yes web-ext@8.3.0 lint --source-dir out/a/firefox --warnings-as-errors=false - - name: Pack - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - mkdir -p packages - for dir in chromium firefox; do - ( cd "out/a/$dir" \ - && find . -exec touch -h -d '1980-01-01T00:00:00Z' {} + \ - && find . -type f | LC_ALL=C sort | zip -X -q -@ "../../../packages/keepiq-$dir-$VERSION.zip" ) - done - # Firefox Add-ons reviews bundled code against its source. - git archive --format=zip -o "packages/keepiq-extension-source-$VERSION.zip" HEAD \ - browser-extension src/crypto src/totp src/policy src/health package.json package-lock.json - sha256sum packages/* | tee packages/SHA256SUMS - - uses: actions/upload-artifact@v4 - with: - name: keepiq-extension-${{ steps.version.outputs.version }} - path: packages/ - retention-days: 30 - - publish: - needs: build - if: startsWith(github.ref, 'refs/tags/extension-v') - runs-on: ubuntu-latest - # Uploads and store API calls; store review itself happens later. - timeout-minutes: 30 - environment: extension-stores - permissions: - contents: write - env: - VERSION: ${{ needs.build.outputs.version }} - steps: - - uses: actions/download-artifact@v4 - with: - name: keepiq-extension-${{ needs.build.outputs.version }} - path: packages - - name: Chrome Web Store - env: - EXTENSION_ID: ${{ vars.CHROME_EXTENSION_ID }} - CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} - CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} - REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} - run: | - npx --yes chrome-webstore-upload-cli@3 upload --auto-publish \ - --source "packages/keepiq-chromium-$VERSION.zip" \ - --extension-id "$EXTENSION_ID" --client-id "$CLIENT_ID" \ - --client-secret "$CLIENT_SECRET" --refresh-token "$REFRESH_TOKEN" - - name: Firefox Add-ons (listed) - env: - AMO_JWT_ISSUER: ${{ secrets.AMO_JWT_ISSUER }} - AMO_JWT_SECRET: ${{ secrets.AMO_JWT_SECRET }} - run: | - mkdir -p firefox && unzip -q "packages/keepiq-firefox-$VERSION.zip" -d firefox - npx --yes web-ext@8.3.0 sign --channel listed --source-dir firefox \ - --upload-source-code "packages/keepiq-extension-source-$VERSION.zip" \ - --api-key "$AMO_JWT_ISSUER" --api-secret "$AMO_JWT_SECRET" \ - --approval-timeout 0 --artifacts-dir signed - - name: Edge Add-ons - env: - PRODUCT_ID: ${{ vars.EDGE_PRODUCT_ID }} - CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }} - API_KEY: ${{ secrets.EDGE_API_KEY }} - run: | - api="https://api.addons.microsoftedge.microsoft.com/v1/products/$PRODUCT_ID/submissions" - auth=(-H "Authorization: ApiKey $API_KEY" -H "X-ClientID: $CLIENT_ID") - op=$(curl -fsS -D - -o /dev/null "${auth[@]}" -H "Content-Type: application/zip" \ - --data-binary "@packages/keepiq-chromium-$VERSION.zip" "$api/draft/package" \ - | awk 'tolower($1)=="location:"{print $2}' | tr -d '\r') - for i in $(seq 1 30); do - status=$(curl -fsS "${auth[@]}" "$api/draft/package/operations/$op" | jq -r .status) - [ "$status" = "Succeeded" ] && break - [ "$status" = "Failed" ] && { echo "Edge package upload failed"; exit 1; } - sleep 10 - done - curl -fsS "${auth[@]}" -X POST -H "Content-Type: application/json" \ - -d "{\"notes\":\"Keepiq $VERSION\"}" "$api" - - name: GitHub release - uses: softprops/action-gh-release@v2 - with: - files: | - packages/* - signed/* - - attach-amo-signed: - if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.amo_version != '') - runs-on: ubuntu-latest - # A few small downloads per release; the scheduled run looks at the last five. - timeout-minutes: 15 - permissions: - contents: write - env: - GH_TOKEN: ${{ github.token }} - REQUESTED: ${{ inputs.amo_version }} - # The add-on id in the Firefox manifest (browser-extension/manifests/browsers.mjs). - GECKO_ID: keepiq@conduction.nl - steps: - - name: Attach the signed Firefox file to approved releases - run: | - set -euo pipefail - if [ -n "$REQUESTED" ]; then - versions="$REQUESTED" - else - versions=$(gh release list -R "$GITHUB_REPOSITORY" --limit 30 --json tagName \ - -q '[.[].tagName | select(startswith("extension-v")) | ltrimstr("extension-v")] | .[0:5][]') - fi - for v in $versions; do - tag="extension-v$v" - asset="keepiq-firefox-$v-amo-signed.xpi" - if gh release view "$tag" -R "$GITHUB_REPOSITORY" --json assets -q '.assets[].name' | grep -qx "$asset"; then - echo "$tag already holds $asset" - continue - fi - # The public version detail answers only for a listed version that passed review. - code=$(curl -sS -o amo.json -w '%{http_code}' \ - "https://addons.mozilla.org/api/v5/addons/addon/$GECKO_ID/versions/v$v/") - status=$(jq -r '.file.status // empty' amo.json 2>/dev/null || true) - if [ "$code" != "200" ] || [ "$status" != "public" ]; then - echo "::notice::Firefox Add-ons has not approved $v yet (HTTP $code, file status '${status:-none}')" - if [ -n "$REQUESTED" ]; then exit 1; fi - continue - fi - url=$(jq -r '.file.url' amo.json) - expected=$(jq -r '.file.hash' amo.json) - case "$url" in - https://addons.mozilla.org/*) ;; - *) echo "::error::Unexpected download host in $url"; exit 1 ;; - esac - curl -fsSL -o "$asset" "$url" - actual="sha256:$(sha256sum "$asset" | cut -d' ' -f1)" - if [ "$actual" != "$expected" ]; then - echo "::error::$asset hash $actual does not match Firefox Add-ons ($expected)" - exit 1 - fi - # The signed file must be the package this release built, plus the signature. - gh release download "$tag" -R "$GITHUB_REPOSITORY" -p "keepiq-firefox-$v.zip" --clobber - rm -rf built signed && mkdir built signed - unzip -q "keepiq-firefox-$v.zip" -d built - unzip -q "$asset" -d signed - ls signed/META-INF/mozilla.rsa signed/META-INF/cose.sig > /dev/null - ( cd built && find . -type f | LC_ALL=C sort | xargs sha256sum ) > built.sha - ( cd signed && find . -type f ! -path './META-INF/*' | LC_ALL=C sort | xargs sha256sum ) > signed.sha - if ! diff -u built.sha signed.sha; then - echo "::error::$asset does not hold exactly the files of keepiq-firefox-$v.zip" - exit 1 - fi - sha256sum "$asset" > "$asset.sha256" - gh release upload "$tag" -R "$GITHUB_REPOSITORY" "$asset" "$asset.sha256" - echo "Attached $asset to $tag" - done diff --git a/.github/workflows/mobile-e2e.yml b/.github/workflows/mobile-e2e.yml index f86be37c1..8e780a56f 100644 --- a/.github/workflows/mobile-e2e.yml +++ b/.github/workflows/mobile-e2e.yml @@ -3,9 +3,8 @@ name: Mobile e2e # End-to-end tests of the native apps (clients-mobile-apps), with # screenshots and video of the flows as artifacts. # -# Android: the Nextcloud with Keepiq (alone, ADR-006) of -# browser-extension/capture (the same setup docs-media.yml uses), behind the -# https front of mobile/e2e/server.mjs, and an emulator that reaches it at +# Android: the Nextcloud with Keepiq (alone, ADR-006) of tests/server, behind +# the https front of mobile/e2e/server.mjs, and an emulator that reaches it at # https://10.0.2.2:8443. The e2e build type trusts that run's certificate; # debug and release builds never do. # @@ -19,8 +18,7 @@ on: branches: [main, development, beta] paths: - "mobile/**" - - "browser-extension/capture/compose.yaml" - - "browser-extension/capture/setup.sh" + - "tests/server/**" - ".github/workflows/mobile-e2e.yml" workflow_dispatch: @@ -71,9 +69,9 @@ jobs: run: | composer install --no-dev --prefer-dist --no-interaction --working-dir=keepiq - name: Start the test server - run: docker compose -p "$PROJECT" -f keepiq/browser-extension/capture/compose.yaml up -d + run: docker compose -p "$PROJECT" -f keepiq/tests/server/compose.yaml up -d - name: Provision it - run: bash keepiq/browser-extension/capture/setup.sh "$PROJECT" keepiq + run: bash keepiq/tests/server/setup.sh "$PROJECT" keepiq # The demo vault the vault tests read: example.com names, all fake. - name: Seed the demo vault run: node keepiq/mobile/e2e/server.mjs seed --upstream http://localhost:8188 @@ -150,7 +148,7 @@ jobs: profile: pixel_6 disable-animations: true emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - script: bash keepiq/mobile/e2e/android-run.sh "$OUT" "$PROJECT" keepiq/browser-extension/capture/out/app-password + script: bash keepiq/mobile/e2e/android-run.sh "$OUT" "$PROJECT" keepiq/tests/server/out/app-password - name: Server logs if: failure() run: | @@ -165,7 +163,7 @@ jobs: retention-days: 14 - name: Stop the test server if: always() - run: docker compose -p "$PROJECT" -f keepiq/browser-extension/capture/compose.yaml down -v + run: docker compose -p "$PROJECT" -f keepiq/tests/server/compose.yaml down -v ios: runs-on: macos-latest diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index de6c1bce8..bc451a987 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -19,10 +19,6 @@ on: - "src/passkey/**" - "src/generator/**" - "tests/vitest/generator-vectors.spec.js" - - "tests/vitest/autofill-vectors.spec.js" - - "browser-extension/src/lib/match.js" - - "browser-extension/src/lib/useOnly.js" - - "browser-extension/src/lib/capture.js" - ".github/workflows/mobile.yml" pull_request: branches: [main, development, beta] @@ -36,10 +32,6 @@ on: - "src/passkey/**" - "src/generator/**" - "tests/vitest/generator-vectors.spec.js" - - "tests/vitest/autofill-vectors.spec.js" - - "browser-extension/src/lib/match.js" - - "browser-extension/src/lib/useOnly.js" - - "browser-extension/src/lib/capture.js" - ".github/workflows/mobile.yml" workflow_dispatch: @@ -90,7 +82,7 @@ jobs: npm ci --ignore-scripts KEEPIQ_KOTLIN_VECTORS=mobile/shared/build/vectors/kotlin-output.json \ npx vitest run tests/vitest/crypto-vectors.spec.js tests/vitest/crypto-vectors-kotlin.spec.js \ - tests/vitest/generator-vectors.spec.js tests/vitest/autofill-vectors.spec.js + tests/vitest/generator-vectors.spec.js # What the core wrote in this run. When its format changes on purpose, # this file replaces tests/vectors/crypto/kotlin-output.json. - uses: actions/upload-artifact@v4 @@ -122,7 +114,7 @@ jobs: cd ios/KeepiqApp xcodebuild -scheme KeepiqApp -destination "generic/platform=iOS Simulator" build # The SwiftUI app itself (mobile/ios/project.yml), so a Swift compile - # error shows here and not only in mobile-e2e.yml. + # error shows in CI. - name: The iOS app compiles against the shared framework run: | ./gradlew --no-daemon :shared:assembleKeepiqSharedDebugXCFramework diff --git a/.github/workflows/standalone.yml b/.github/workflows/standalone.yml index 92af72862..e6b5d85da 100644 --- a/.github/workflows/standalone.yml +++ b/.github/workflows/standalone.yml @@ -3,7 +3,7 @@ name: Standalone (no other app) # Keepiq must install and work with no other Conduction app (ADR-006, # standalone-app-shell). This job pins that promise: one Nextcloud with Keepiq # ALONE (no OpenRegister, no integriq, no Hermiq), provisioned by the same -# browser-extension/capture setup docs-media.yml uses, then +# tests/server setup, then # # - a smoke check: the other apps are not enabled, the login page and # Keepiq's health answer 200, metrics answers an admin; @@ -22,7 +22,7 @@ on: - "src/**" - "appinfo/**" - "tests/e2e/**" - - "browser-extension/capture/**" + - "tests/server/**" - ".github/workflows/standalone.yml" push: branches: [development] @@ -31,7 +31,7 @@ on: - "src/**" - "appinfo/**" - "tests/e2e/**" - - "browser-extension/capture/**" + - "tests/server/**" - ".github/workflows/standalone.yml" workflow_dispatch: @@ -44,7 +44,7 @@ jobs: timeout-minutes: 45 env: PROJECT: kq-standalone - KQ_MEDIA_PORT: "8188" + KQ_TEST_PORT: "8188" BASE: http://localhost:8188 steps: - uses: actions/checkout@v4 @@ -68,10 +68,10 @@ jobs: npm run build - name: Start the instance - run: docker compose -p "$PROJECT" -f keepiq/browser-extension/capture/compose.yaml up -d + run: docker compose -p "$PROJECT" -f keepiq/tests/server/compose.yaml up -d # No OpenRegister directory: the setup copies in and enables Keepiq only. - name: Provision it with Keepiq alone - run: bash keepiq/browser-extension/capture/setup.sh "$PROJECT" keepiq + run: bash keepiq/tests/server/setup.sh "$PROJECT" keepiq - name: Smoke check run: | diff --git a/.prettierignore b/.prettierignore index 13231e070..071ddd510 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,6 +12,8 @@ test-results/ # `docs/` is the Docusaurus documentation site. It has its own conventions (its # CSS is space-indented by Docusaurus' own scaffolding) and its own toolchain. docs/ +# The browser extension is its own WXT project with its own lint setup. +browser-extension/ # Written by the translation workflow — a formatter here would fight its own # generator on every run. l10n/ diff --git a/.procedure-code-baseline.json b/.procedure-code-baseline.json new file mode 100644 index 000000000..57e568c36 --- /dev/null +++ b/.procedure-code-baseline.json @@ -0,0 +1 @@ +{"count": 0} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d95bcbfd..77619aa23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Added - AI assistants can read vault metadata through three MCP tools: `listEntries`, `expiryReport` and `rotationStatus`. The tools return names, addresses, types, folders and dates, never a password, login or other secret value. They act only for the signed-in user and change nothing. Every call is written to the audit log with the tool name and the number of results. The tools appear only when OpenRegister is installed. (hermiq-ai-tooling) +- A change to a shared secret's value, login or additional fields now reaches every copy in one request, `PUT /api/v1/secrets/{id}/share-save`: the editor's row, every recipient's copy and every partner share are saved together or not at all. A save made from an older version, or one that misses a recipient added in the meantime, is refused and nothing changes. Saves for more than 100 copies are sent in chunks and committed at once; the chunks wait in a new table and can be used for 30 minutes. Partner instances are told to pull only after the save is committed. The old `/sync` and `PUT /api/v1/federated-shares/{id}` routes keep working for older web-app tabs and API clients. `POST /api/v1/versions/{id}/restore` takes an optional `baseUpdatedAt` and answers 409 when the secret changed since, and suspending a partner share that is no longer active now answers 404. (sharing-atomic-share-save, #1254) ### Fixed +- Editing a shared secret in the web app updates the recipients' copies again. The copies were never updated, a write-grade member who changed only the password cleared the login and additional fields on the source, and a failed update showed as saved. A failed save now shows an error and changes nothing. (sharing-atomic-share-save, #1253) +- A save through `PUT /api/v1/secrets/{id}`, a sync through `PUT /api/v1/secrets/{id}/sync` and a version restore could overwrite a change that landed between their read and their write, such as a share-save from the web app while the browser extension saved. They now read and write in one step, so a change that lands meanwhile is kept in the version history instead of overwritten. A save or sync with a base version answers 409, as do a sync or restore without one; a save without one, filling a secret request for a user's secret and setting an expiry try once more on the current version, and answer 409 only if it moves again. (sharing-atomic-share-save) +- A delegate or write-grade member who saves a shared secret through their copy is now recorded in the audit log on the owner's secret too, with the holders that were left out. A restore made through a share-save is recorded as the version the client restored from. (sharing-atomic-share-save) +- Revoking a partner share while an update to that partner was still being sent could leave the partner's copy in place: the slower update replaced the pending unshare, so the unshare was never sent. An update now never replaces a pending unshare. (sharing-atomic-share-save) +- Suspending a partner share, also by saving without an unreachable partner or by removing the partner, now drops an update still waiting for its retry. The retry job kept contacting that partner and then showed the suspended share as failed. The retry job also checks each share again just before it sends, so only a send already under way can still arrive once. (sharing-atomic-share-save) +- When the partner deletes its copy of a share whose revocation is still waiting to reach it, or whose revocation was given up, the revocation is now complete and the share is removed. It used to keep waiting for an unshare the partner no longer needed, or stay behind as failed once the retries gave up. (sharing-atomic-share-save) +- A decline and a later accept from the partner no longer make a suspended partner share live again. Only the owner ends a suspension, by revoking the share or sharing again. The decline is still recorded in the owner's audit log. (sharing-atomic-share-save) +- Partner shares now pass to the new owner when a secret is handed over, through a delegation when the owner's account is deleted or through team offboarding. They stayed with the previous owner: the new owner could not revoke or suspend them, while after team offboarding the previous owner still could. The upgrade hands shares left behind this way to their secret's owner, and a partner share that isn't the secret owner's is no longer served. (sharing-atomic-share-save) - `keepiq ssh-agent` works as a systemd user service. Under systemd the agent moved to the background and left the unit, which systemd then stopped, so the agent never kept running. The new `--foreground` option keeps it in place, and the unit in `cli/README.md` now uses it. ### Removed @@ -20,4 +29,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Security +- Sharing with another organisation verifies the partner's certificate chain more strictly. The chain must be exactly the partner's intermediate and its root. Every certificate above the recipient's must be a CA allowed to sign certificates, within its path length, and every certificate in the chain must be valid now. The owner's server refuses any other chain before it reaches the browser and says the certificate could not be verified. Upgrading every instance that shares with partners is what protects its users. New user and application certificates are marked CA:FALSE, and the root and intermediate carry their CA constraints whatever the server's OpenSSL configuration says. Some user certificates issued before are marked CA:TRUE; an upgraded instance never accepts them as an issuer, and such a certificate becomes CA:FALSE when re-issued. The web app no longer accepts one as the recipient's own certificate either: the next save of a secret shared with such a user suspends that partner share, and it stays suspended until the owner shares again, also after the certificate is re-issued. If an unusual OpenSSL configuration issued an instance's root without CA constraints, upgraded partners refuse its chain until that root is renewed, which changes the fingerprint the partners have pinned. (federated-sharing) +- The web app checks a partner recipient's certificate more strictly. It refuses a lookup answer for another cloud id than the one asked for, a self-issued intermediate, a recipient certificate marked as a CA, a recipient name that isn't a cloud id, and encodings it used to read leniently: a signature algorithm that differs from the one in the signed part, a signature with unused bits, more than one common name, one of an unexpected string type or one that isn't valid UTF-8, and RSA-PSS parameters that are incomplete, out of order or repeated, name a mask other than MGF1 or a trailer other than 1, or carry unknown fields. The case of a cloud id's path now counts, so a certificate for `bob@host/nc` no longer passes for a share with `bob@host/NC`. When saving a shared secret, a partner share refused this way is suspended, as one whose certificate no longer verifies. A failed partner lookup now suspends a share only when its reason comes with its own status, so an error page from a proxy no longer suspends shares for good. (federated-sharing, #1257) +- Sharing with someone at another organisation and enrolling in account recovery now encrypt to the public key of the certificate the browser verified. The browser accepts a certificate or key only as exactly one PEM block with nothing but whitespace around it and nothing after its encoded value, and refuses anything else. (federated-sharing, organisation-account-recovery) - Keepiq records its decision not to take part in OpenRegister integration leaves: secret material and vault structure stay inside the vault's own access rules. A test now fails when a register schema declares `linkedTypes` or `mailObjectTemplate`, so the boundary cannot be crossed by accident. (leaf-integrations) diff --git a/appinfo/info.xml b/appinfo/info.xml index 0e9a461a1..e91f22584 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -36,7 +36,7 @@ Vrij en open source onder de EUPL-1.2-licentie. **Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl. ]]> - 0.3.8 + 0.3.9-unstable.20261014120000 EUPL-1.2 Conduction Keepiq @@ -117,6 +117,7 @@ Vrij en open source onder de EUPL-1.2-licentie. OCA\Keepiq\BackgroundJob\ExpireSecretRequestsJob OCA\Keepiq\BackgroundJob\ExpireSharesJob OCA\Keepiq\BackgroundJob\ExpireDeviceApprovalsJob + OCA\Keepiq\BackgroundJob\ExpireShareSaveStagesJob OCA\Keepiq\BackgroundJob\ExpireRecoveryRequestsJob OCA\Keepiq\BackgroundJob\ApproveElapsedEmergencyRequests OCA\Keepiq\BackgroundJob\PruneSecretVersionsJob @@ -193,6 +194,12 @@ Vrij en open source onder de EUPL-1.2-licentie. OCA\Keepiq\Repair\BootstrapCertificateAuthority OCA\Keepiq\Repair\InitializeSettings OCA\Keepiq\Repair\SeedSecretTypes + + OCA\Keepiq\Repair\ReownTransferredPartnerShares OCA\Keepiq\Repair\SeedDevelopmentData OCA\Keepiq\Repair\SeedDevelopmentSecrets OCA\Keepiq\Repair\SeedDevelopmentApplications diff --git a/appinfo/routes.php b/appinfo/routes.php index 49a6a046e..5894c1c66 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -180,6 +180,8 @@ // and the sharee-search pages these ids come from can be long. ['name' => 'share#recipientCertificates', 'url' => '/api/v1/shares/recipient-certificates', 'verb' => 'POST'], ['name' => 'share#sync', 'url' => '/api/v1/secrets/{secretId}/sync', 'verb' => 'PUT'], + // The row, every copy and every partner share in one transaction (#1254). + ['name' => 'shareSave#save', 'url' => '/api/v1/secrets/{id}/share-save', 'verb' => 'PUT'], ['name' => 'share#destroy', 'url' => '/api/v1/shares/{id}', 'verb' => 'DELETE'], // Use-only flag and end date of a direct share (sharing-use-only-and-expiring-shares §2.1). ['name' => 'share#update', 'url' => '/api/v1/shares/{id}', 'verb' => 'PATCH'], diff --git a/browser-extension/.claude/commands/opsx/apply.md b/browser-extension/.claude/commands/opsx/apply.md new file mode 100644 index 000000000..f540757f3 --- /dev/null +++ b/browser-extension/.claude/commands/opsx/apply.md @@ -0,0 +1,155 @@ +--- +name: "OPSX: Apply" +description: Implement tasks from an OpenSpec change (Experimental) +category: Workflow +tags: [workflow, artifacts, experimental] +--- + +Implement tasks from an OpenSpec change. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +**Steps** + +1. **Select the change** + + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select + + Always announce: "Using change: " and how to override (e.g., `/opsx:apply `). + +2. **Check status to understand the schema** + ```bash + openspec status --change "" --json + ``` + Parse the JSON to understand: + - `schemaName`: The workflow being used (e.g., "spec-driven") + - `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints + - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others) + +3. **Get apply instructions** + + ```bash + openspec instructions apply --change "" --json + ``` + + This returns: + - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema) + - Progress (total, complete, remaining) + - Task list with status + - Dynamic instruction based on current state + + **Handle states:** + - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue` + - If `state: "all_done"`: congratulate, suggest archive + - Otherwise: proceed to implementation + +4. **Read context files** + + Read every file path listed under `contextFiles` from the apply instructions output. + The files depend on the schema being used: + - **spec-driven**: proposal, specs, design, tasks + - Other schemas: follow the contextFiles from CLI output + +5. **Show current progress** + + Display: + - Schema being used + - Progress: "N/M tasks complete" + - Remaining tasks overview + - Dynamic instruction from CLI + +6. **Implement tasks (loop until done or blocked)** + + For each pending task: + - Show which task is being worked on + - Make the code changes required + - Keep changes minimal and focused + - Mark task complete in the tasks file: `- [ ]` → `- [x]` + - Continue to next task + + **Pause if:** + - Task is unclear → ask for clarification + - Implementation reveals a design issue → suggest updating artifacts + - Error or blocker encountered → report and wait for guidance + - User interrupts + +7. **On completion or pause, show status** + + Display: + - Tasks completed this session + - Overall progress: "N/M tasks complete" + - If all done: suggest archive + - If paused: explain why and wait for guidance + +**Output During Implementation** + +``` +## Implementing: (schema: ) + +Working on task 3/7: +[...implementation happening...] +✓ Task complete + +Working on task 4/7: +[...implementation happening...] +✓ Task complete +``` + +**Output On Completion** + +``` +## Implementation Complete + +**Change:** +**Schema:** +**Progress:** 7/7 tasks complete ✓ + +### Completed This Session +- [x] Task 1 +- [x] Task 2 +... + +All tasks complete! You can archive this change with `/opsx:archive`. +``` + +**Output On Pause (Issue Encountered)** + +``` +## Implementation Paused + +**Change:** +**Schema:** +**Progress:** 4/7 tasks complete + +### Issue Encountered + + +**Options:** +1.