Skip to content

fix(main): harden cloud owner password recovery trust and email sync - #1330

Merged
khaira777 merged 4 commits into
mainfrom
fm/ship-cloud-recovery-hardening
Oct 23, 2026
Merged

khaira777 merged 4 commits into
mainfrom
fm/ship-cloud-recovery-hardening

Conversation

@khaira777

@khaira777 khaira777 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Verify these release-audit findings in the owner email password recovery feature (merged in #1310) and, if true, fix them:

  1. F1 (P1, security): the recovery trust guard can be bypassed. cloudSync.isTrustedRecoveryServerUrl() checks cloud_server_url from SQLite, but the recovery calls (requestOwnerPasswordRecovery, getOwnerPasswordRecoveryStatus, redeemOwnerPasswordRecovery via signedFetch) send requests to the in-memory this.settings.server_url, which can differ. POST /api/settings/cloud/register writes cloud_server_url to SQLite before its cloud_deletion_blocked 409 return and before cloudSync.register() reloads settings, so SQLite and memory can disagree. Also, a manager (who has cloud.manage by default) can overwrite cloud_api_key / cloud_store_id through PUT /api/settings/cloud, which would let recovery run under credentials the manager controls and reset the owner's password.
  2. F2 (P2): changing the owner's email via Staff management (PUT /api/staff/:id) or restoring/importing a backup leaves settings.email (what recovery uses) out of sync with users.email, so recovery fails with 404 after approval; on a stranded store (no active administrator) the complete step matches without role = 'owner' and can promote a non-owner staff member who holds the old email.

What Changed

  • Pin cloud password recovery and email verification requests in CloudSyncService to the trusted FloAdmin server URL verified against both persisted SQLite and active runtime settings, defer cloud_server_url persistence and reload in POST /api/settings/cloud/register until pre-registration checks pass, and restrict cloud_server_url, unmasked cloud_api_key, and cloud_store_id updates on /api/settings/cloud and /api/settings/cloud/register to the owner role.
  • Synchronize settings.email when an active owner's email or role changes via PUT /api/staff/:id or POST /api/staff/:id/deactivate (refreshing the cloud registration profile when updated), and preserve and resync settings.email against active owner accounts during backup restore and import in mergeRestoreProtectedSettings.
  • Require settings.email to match an active owner account in cloudSync.isEmailRecoveryAvailable(), POST /api/auth/recover-password/cloud/request, and POST /api/auth/recover-password/cloud/complete so FloAdmin email recovery never matches or promotes non-owner accounts on stranded stores, with regression tests and updated documentation.

Risk Assessment

✅ Low: All recovery trust-boundary, owner-only cloud credential/endpoint mutation, owner email synchronization, backup/import email reconciliation, and non-owner role-promotion guards are enforced consistently across SQLite and in-memory CloudSync state and covered by behavioral integration tests.

Testing

Ran focused backend test suites (test:cloud-password-recovery, test:cloud-account-status, test:staff-authz, and test:issue-127-password-recovery) and stood up the compiled FloCafe Express server (startServer()) with an isolated SQLite database and disposable HTTP servers to drive all 6 happy-path and adversarial scenarios live over HTTP, recording the full verification report in ~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json (no visual screenshot artifact was captured because this change only modifies backend API routes, SQLite restore helpers, and documentation with no UI surface changes); all checks and live scenarios passed.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Adversarial cloud recovery request when in-memory server_url points to an untrusted server while SQLite holds the default FloAdmin URL is blocked with 403/409 and never dispatches network traffic to t… ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Adversarial POST /api/settings/cloud/register with a custom cloud_server_url while cloud deletion is blocked returns 409 without mutating cloud_server_url in SQLite ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Adversarial manager attempts to overwrite cloud_api_key, cloud_store_id, or cloud_server_url via PUT /api/settings/cloud and POST /api/settings/cloud/register are rejected with 403 while owner updates… ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Updating the active owner's email via PUT /api/staff/:id automatically syncs settings.email and allows end-to-end cloud email password recovery and login under the new email ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Restoring a SQLite backup or importing a JSON database backup with a stale settings.email preserves and resyncs settings.email to the active owner account ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Adversarial cloud password recovery on a stranded store (no active administrator) where settings.email matches a non-owner staff member refuses to request or complete recovery and never promotes the n… ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Evidence: Live HTTP & SQLite cloud recovery hardening verification report

{ "scenario1_memory_sqlite_divergence": { "sqliteServerUrl": "https://blue.flopos.com/",
"inMemoryServerUrl": "http://127.0.0.1:62582",
"setupStatusCloudRecoveryAvailable": false, "httpRequestRecoveryStatus": 409, "directRequestPasswordResetError": { "name": "CloudRequestError", "message": "Password recovery requires the official FloAdmin server", "status": 403 }, "untrustedServerHits": 0 }, "scenario2_blocked_register_no_sqlite_write": { "registerHttpStatus": 409, "storedCloudServerUrlAfter409": "https://blue.flopos.com/",
"inMemoryTrustedAfter409": true }, "scenario3_manager_vs_owner_cloud_settings": { "managerPutApiKeyStatus": 403, "managerPutStoreIdStatus": 403, "managerPutServerUrlStatus": 403, "managerPostRegisterUrlStatus": 403, "managerToggleFlagsStatus": 200, "ownerPutCredentialsStatus": 200, "persistedApiKey": "live-owner-api-key", "persistedStoreId": "live-owner-store-id" }, "scenario4_staff_owner_email_sync_and_recovery": { "putStaffStatus": 200, "syncedSettingsEmail": "updated-owner@example.com", "cloudRecoveryAvailable": true, "recoverRequestStatus": 202, "recoverPollStatus": 200, "recoverCompleteStatus": 200, "loginWithNewPasswordStatus": 200 }, "scenario5_backup_restore_and_json_import_email_sync": { "sqliteRestoreSuccess": true, "emailAfterSqliteRestore": "updated-owner@example.com", "jsonImportStatus": 200, "emailAfterJsonImport": "updated-owner@example.com", "cloudRecoveryAvailableAfterRestoreAndImport": true }, "scenario6_stranded_store_non_owner_guard": { "createCashierStatus": 201, "setupStatusCloudRecoveryAvailable": false, "recoverRequestStatus": 409, "recoverCompletePrecheckStatus": 409, "recoverCompleteInnerLookupStatus": 404, "redeemCallsDuringStrandedAttempt": 0, "cashierRoleAfterAttempt": "cashier" } }

{
  "startedAt": "2026-10-23T15:40:47.467Z",
  "scenarios": {
    "scenario1_memory_sqlite_divergence": {
      "sqliteServerUrl": "https://blue.flopos.com/",
      "inMemoryServerUrl": "http://127.0.0.1:62582",
      "setupStatusCloudRecoveryAvailable": false,
      "httpRequestRecoveryStatus": 409,
      "httpRequestRecoveryBody": {
        "error": "Email recovery is not available on this install."
      },
      "directRequestPasswordResetError": {
        "name": "CloudRequestError",
        "message": "Password recovery requires the official FloAdmin server",
        "status": 403
      },
      "untrustedServerHits": 0
    },
    "scenario2_blocked_register_no_sqlite_write": {
      "registerHttpStatus": 409,
      "registerHttpBody": {
        "error": "Cloud deletion is unresolved; retry or cancel it before re-enabling cloud services."
      },
      "storedCloudServerUrlAfter409": "https://blue.flopos.com/",
      "inMemoryTrustedAfter409": true
    },
    "scenario3_manager_vs_owner_cloud_settings": {
      "managerPutApiKeyStatus": 403,
      "managerPutStoreIdStatus": 403,
      "managerPutServerUrlStatus": 403,
      "managerPostRegisterUrlStatus": 403,
      "managerToggleFlagsStatus": 200,
      "ownerPutCredentialsStatus": 200,
      "persistedApiKey": "live-owner-api-key",
      "persistedStoreId": "live-owner-store-id"
    },
    "scenario4_staff_owner_email_sync_and_recovery": {
      "putStaffStatus": 200,
      "syncedSettingsEmail": "updated-owner@example.com",
      "cloudRecoveryAvailable": true,
      "recoverRequestStatus": 202,
      "recoverRequestBody": {
        "step": "reset",
        "reset_id": "reset-live-9876",
        "expires_at": null,
        "email_hint": "u***r@example.com"
      },
      "recoverPollStatus": 200,
      "recoverPollBody": {
        "status": "approved"
      },
      "recoverCompleteStatus": 200,
      "recoverCompleteBody": {
        "message": "Password reset successfully. You can now log in with your new password."
      },
      "loginWithNewPasswordStatus": 200
    },
    "scenario5_backup_restore_and_json_import_email_sync": {
      "sqliteRestoreSuccess": true,
      "sqliteRestoreError": null,
      "emailAfterSqliteRestore": "updated-owner@example.com",
      "jsonImportStatus": 200,
      "jsonImportBody": {
        "success": true,
        "message": "Database imported successfully",
        "backup": "/var/folders/y_/1ltcxtwj0zd_w1dg9jv4jl580000gn/T/flo-live-cloud-recovery-R2ULw2/backups/flo-backup-2026-10-23T15-40-48-491Z-7e177804.db",
        "schemaVersionMismatch": false,
        "importedSchemaVersion": 116,
        "currentSchemaVersion": 116,
        "placeholderUsersCreated": 0,
        "cleanupPending": false
      },
      "emailAfterJsonImport": "updated-owner@example.com",
      "cloudRecoveryAvailableAfterRestoreAndImport": true
    },
    "scenario6_stranded_store_non_owner_guard": {
      "createCashierStatus": 201,
      "setupStatusCloudRecoveryAvailable": false,
      "recoverRequestStatus": 409,
      "recoverRequestBody": {
        "error": "Email recovery is not available on this install."
      },
      "recoverCompletePrecheckStatus": 409,
      "recoverCompletePrecheckBody": {
        "error": "Email recovery is not available on this install."
      },
      "recoverCompleteInnerLookupStatus": 404,
      "recoverCompleteInnerLookupBody": {
        "error": "No active owner account matches the registered email."
      },
      "redeemCallsDuringStrandedAttempt": 0,
      "cashierRoleAfterAttempt": "cashier"
    }
  },
  "serverUrl": "http://127.0.0.1:3189",
  "completedAt": "2026-10-23T15:40:48.620Z"
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Adversarial cloud recovery request when in-memory server_url points to an untrusted server while SQLite holds the default FloAdmin URL is blocked with 403/409 and never dispatches network traffic to t… ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Adversarial POST /api/settings/cloud/register with a custom cloud_server_url while cloud deletion is blocked returns 409 without mutating cloud_server_url in SQLite ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Adversarial manager attempts to overwrite cloud_api_key, cloud_store_id, or cloud_server_url via PUT /api/settings/cloud and POST /api/settings/cloud/register are rejected with 403 while owner updates… ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Updating the active owner's email via PUT /api/staff/:id automatically syncs settings.email and allows end-to-end cloud email password recovery and login under the new email ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Restoring a SQLite backup or importing a JSON database backup with a stale settings.email preserves and resyncs settings.email to the active owner account ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
Adversarial cloud password recovery on a stranded store (no active administrator) where settings.email matches a non-owner staff member refuses to request or complete recovery and never promotes the n… ✅ pass live Live HTTP & SQLite cloud recovery hardening verification report (~/.no-mistakes/evidence/01M5MN7S76WYVMZ8Z58VS5EJ4X/live-cloud-recovery-hardening-verification.json) and `npm run te…
  • npm run test:cloud-password-recovery
  • npm run test:cloud-account-status
  • npm run test:staff-authz
  • npm run test:issue-127-password-recovery
  • npm run build && ELECTRON_RUN_AS_NODE=1 ./node_modules/.bin/electron <live-server-scenario-driver> driving POST /api/auth/setup/initialize, GET /api/auth/setup/status, POST /api/settings/cloud/register, PUT /api/settings/cloud, POST /api/staff, PUT /api/staff/:id, POST /api/auth/recover-password/cloud/request, GET /api/auth/recover-password/cloud/status, POST /api/auth/recover-password/cloud/complete, POST /api/auth/login, GET /api/db/export, and POST /api/db/import against a live running server on port 3189
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Summary by CodeRabbit

  • Security

    • Cloud account recovery is limited to the official FloAdmin service and requires an active store owner whose email matches the registered address.
    • Only store owners can change cloud server and credential settings; managers can still update sync and order options.
    • FloAdmin recovery can no longer grant owner access when no active owner remains.
  • Bug Fixes

    • The registered email now stays aligned with active owner accounts after owner changes and backup restores.
    • Cloud registration now checks service status before saving a server address.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 48bdedf and 8cbe6ec.

📒 Files selected for processing: 8
  • docs/architecture/authentication-and-authorization.md
  • docs/reference/api.md
  • main/db.ts
  • main/routes/auth.ts
  • main/routes/settings.ts
  • main/routes/staff.ts
  • main/services/cloud-sync.ts
  • tests/cloud-password-recovery.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

Walkthrough

Cloud recovery now checks the trusted server URL and active owner email. Owner email changes and backup restoration synchronize the registered email. Cloud configuration routes restrict selected fields to owners.

Changes

Cloud recovery and owner identity

Layer / File(s) Summary
Cloud endpoint and configuration controls
main/services/cloud-sync.ts, main/routes/settings.ts, docs/reference/api.md, tests/cloud-password-recovery.test.ts
Recovery requests use a trusted server URL. Cloud settings and registration restrict server URLs and credentials to owners. Tests cover URL trust, registration checks, and field permissions.
Registered owner email synchronization
main/db.ts, main/routes/staff.ts, tests/cloud-password-recovery.test.ts
Owner updates and deactivation synchronize the registered email with active owners. Protected-settings merges preserve an active owner email, including during backup restoration.
Recovery owner checks and completion
main/routes/auth.ts, docs/architecture/authentication-and-authorization.md, tests/cloud-password-recovery.test.ts
Recovery requests and completion require an active owner matching the registered email. FloAdmin recovery does not promote a non-owner when no active administrator remains.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 8cbe6

The change limits cloud password recovery to the trusted FloAdmin server and active owners, and keeps the registered email in sync with the owner. No remaining merge-blocking issue was identified. CI is still pending, so normal checks should complete before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 6 files. (2 skipped: 2… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly summarizes the main changes: hardening cloud owner password recovery trust and synchronizing owner email data.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description follows the repository template and is substantive. It states the intent, lists the two verified findings (F1 recovery trust bypass and F2 owner email desync), gives a clear change lis…
Full details: Description check

Explanation

The description follows the repository template and is substantive. It states the intent, lists the two verified findings (F1 recovery trust bypass and F2 owner email desync), gives a clear change list, and reports the focused test suites and six live scenarios with evidence. Several template sections are missing or incomplete. The 'Related work' section has no issue or discussion link. The 'Scope' section is absent. The 'Compatibility & data safety' section is not filled in, so database impact, migration, user-visible behavior, and offline/network impact are unstated. The 'Cross-cutting surfaces' table is blank. The 'Developer documentation' checkboxes are unchecked, although the summary says documentation was updated. The contributor checklist is unchecked. Commands run and results are given as free text under Testing, not under the template's Verification heading. CI is still pending. The core content is clear, so the description is mostly complete.

Full details: Docstring Coverage

Explanation

Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 6 files. (2 skipped: 2 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread tests/cloud-password-recovery.test.ts Fixed
khaira777 and others added 2 commits October 23, 2026 12:00
…:120509151887`, alert `js/missing-rate-limiting` on `tests/cloud-password-recovery.test.ts:80-90`) by removing the ad-hoc inline Express `jwt.verify` middleware in `tests/cloud-password-recovery.test.ts` and reusing `createApp` from `tests/helpers/test-setup.ts` to mount `/api/auth`, `/api/settings`, and `/api/staff`. Verified with: - `node tests/run-electron-node-test.cjs tests/cloud-password-recovery.test.ts` (passed) - `npm run lint` (passed) - `npm run build` (passed) - `npm run docs:check` (passed) - `git diff --check` (passed)
@khaira777
khaira777 enabled auto-merge (squash) October 23, 2026 16:11
@khaira777
khaira777 merged commit 75a018f into main Oct 23, 2026
23 checks passed
@khaira777
khaira777 deleted the fm/ship-cloud-recovery-hardening branch October 23, 2026 16:18
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.

2 participants