From a2c9f44307ea67be1c0daf5b22bb3ed617d50aeb Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:34:20 -0400 Subject: [PATCH] [auth] Fix "prepareStoredNsecSwitch is not a function" when switching to a saved nsec account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switching (from NIP-07) to an added nsec account threw "this.prepareStoredNsecSwitch is not a function". Root cause: bindCoordinator binds every coordinator method to the Application instance, so inside handleProfileSwitchRequest `this` === app. Its call to the sibling `this.prepareStoredNsecSwitch(pubkey)` therefore resolves against the Application and needs an app-level delegator (like handleProfileSwitchRequest, handleModerationSettingsChange, and waitForIdentityRefresh all have). The #48 work added prepareStoredNsecSwitch as a coordinator sibling but not its delegator, and its tests only exercised authService.switchProfile — not the full app wiring — so it slipped through. Fix: add the app.prepareStoredNsecSwitch delegator (-> this._auth.prepareStoredNsecSwitch). Tests: tests/profile-switch-coordinator-delegators.test.mjs (4) — asserts every sibling handleProfileSwitchRequest calls via this.X exists as an app method. Co-Authored-By: Claude Opus 4.8 --- js/app.js | 11 ++++ ...ile-switch-coordinator-delegators.test.mjs | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/profile-switch-coordinator-delegators.test.mjs diff --git a/js/app.js b/js/app.js index b078395b..3220c254 100644 --- a/js/app.js +++ b/js/app.js @@ -3330,6 +3330,17 @@ class Application { return this._auth.handleProfileSwitchRequest(...args); } + // Coordinator methods are bound to `this` (the Application) by bindCoordinator, + // so a sibling call like `this.prepareStoredNsecSwitch(...)` inside + // handleProfileSwitchRequest resolves against the Application — it needs this + // delegator to exist (like handleProfileSwitchRequest above). Without it, + // switching to a stored-nsec account threw "this.prepareStoredNsecSwitch is not + // a function". + async prepareStoredNsecSwitch(...args) { + this._initCoordinators(); + return this._auth.prepareStoredNsecSwitch(...args); + } + async waitForIdentityRefresh(...args) { this._initCoordinators(); return this._auth.waitForIdentityRefresh(...args); diff --git a/tests/profile-switch-coordinator-delegators.test.mjs b/tests/profile-switch-coordinator-delegators.test.mjs new file mode 100644 index 00000000..b9f3ebfc --- /dev/null +++ b/tests/profile-switch-coordinator-delegators.test.mjs @@ -0,0 +1,50 @@ +// Switching to a saved nsec account threw "this.prepareStoredNsecSwitch is not a +// function". Root cause: bindCoordinator binds every coordinator method to the +// Application (`this` === app), so when a coordinator method calls a SIBLING via +// `this.sibling(...)` it resolves against the Application — which therefore needs +// an app-level delegator for that sibling. handleProfileSwitchRequest calls +// this.prepareStoredNsecSwitch / this.handleModerationSettingsChange / +// this.waitForIdentityRefresh; the nsec one was missing its delegator. +// +// test_integrity_note: +// change_type: ["new_tests"] +// scenarios: +// - id: SCN-profile-switch-coordinator-delegators +// given: "the Application class" +// when: "handleProfileSwitchRequest runs (bound to app) and calls siblings" +// then: "every sibling it calls exists as an app-level method/delegator" +// observable_outcomes: +// - "Application.prototype.prepareStoredNsecSwitch is a function" +// - "the other siblings it invokes are also app methods" +// determinism_controls: +// - "static prototype inspection; no network/clock/DOM" +// anti_cheat_rationale: +// prevents: ["hard-coded return value"] +// relaxation: +// did_relax_any_assertion: false + +import "./test-helpers/setup-localstorage.mjs"; +import test from "node:test"; +import { strict as assert } from "node:assert"; +import { Application } from "../js/app.js"; + +// Every coordinator sibling that handleProfileSwitchRequest calls via `this.X` +// must exist as an app-level method, because bindCoordinator binds the coordinator +// methods to the Application instance. +const REQUIRED_APP_METHODS = [ + "handleProfileSwitchRequest", + "prepareStoredNsecSwitch", // was missing -> the reported bug + "handleModerationSettingsChange", + "waitForIdentityRefresh", +]; + +for (const name of REQUIRED_APP_METHODS) { + test(`Application exposes ${name}() so the bound profile-switch flow can call it`, () => { + assert.equal( + typeof Application.prototype[name], + "function", + `Application.prototype.${name} must be a function (coordinator siblings are ` + + `bound to app; handleProfileSwitchRequest calls it via this.${name}())`, + ); + }); +}