mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-10-27 08:31:21 +00:00
Refuse to overwrite an existing account on register or password change
registerAccount() never checked whether a credential capsule already existed at the locator address it was about to publish to. That address is a NIP-33 replaceable event fully determined by login name + password, so registering with a login name + password combination that was already registered -- by mistake (double-clicking "create account", confusing it with "sign in"), or by coincidence with someone else's account under a shared login name -- silently replaced the existing account's identity binding. The old everyday identity became unreachable through that login unless the original recovery phrase was still on hand, with nothing warning the user beforehand. changePassword() had the identical gap on the new locator: rotating to a password that happened to already be registered under the same login name would silently destroy that other account. Both flows now query for an existing capsule at the target locator before publishing, and refuse with AccountAlreadyExistsError if one is found (or a retry-oriented error if no relay could be reached to check). Two new end-to-end tests reproduce both scenarios and confirm the pre-existing account survives untouched. Bumps docs/spec.md to v0.6 (§15.6, §18.1).
This commit is contained in:
1 parent
8c01a121df
commit
01fc7c5ed3
6 files changed
+158
-15
No files matched your search
@@ -199,16 +199,18 @@ routing.
|
|||||||
This build covers the protocol's Phase 0 (cryptographic core + test vectors)
|
This build covers the protocol's Phase 0 (cryptographic core + test vectors)
|
||||||
and the load-bearing parts of Phase 1 (account MVP), verified two ways:
|
and the load-bearing parts of Phase 1 (account MVP), verified two ways:
|
||||||
|
|
||||||
- **Protocol-level tests** (`npm test`, 77 tests): Argon2id/ScalarExpand/JCS/
|
- **Protocol-level tests** (`npm test`, 79 tests): Argon2id/ScalarExpand/JCS/
|
||||||
padding/timestamp/NIP-44 unit tests with the spec's exact byte layouts
|
padding/timestamp/NIP-44 unit tests with the spec's exact byte layouts
|
||||||
(§11), and full end-to-end scenarios — create → clean-device login, phrase
|
(§11), and full end-to-end scenarios — create → clean-device login, phrase
|
||||||
recovery → new credentials → clean-device login, password change with
|
recovery → new credentials → clean-device login, password change with
|
||||||
mandatory tombstone + NIP-09 deletion, relay-loss → replica repair
|
mandatory tombstone + NIP-09 deletion, relay-loss → replica repair
|
||||||
(including keyless recovery-event rebroadcast), generation-rollback
|
(including keyless recovery-event rebroadcast), generation-rollback
|
||||||
detection failing closed by default rather than only warning (§16.2 step
|
detection failing closed by default rather than only warning (§16.2 step
|
||||||
6), and initial profile/relay-list publication never overwriting an
|
6), initial profile/relay-list publication never overwriting an existing
|
||||||
existing kind 0/10002/10050 event when the everyday identity is an
|
kind 0/10002/10050 event when the everyday identity is an imported nsec
|
||||||
imported nsec (§15.8, §28.1) — run against an in-memory mock relay
|
(§15.8, §28.1), and registration/password-rotation refusing to overwrite
|
||||||
|
another account already registered at the same locator address (§15.6,
|
||||||
|
§18.1) — run against an in-memory mock relay
|
||||||
(`packages/core/src/test-support/mockRelay.ts`), since this sandbox has no
|
(`packages/core/src/test-support/mockRelay.ts`), since this sandbox has no
|
||||||
route to public Nostr relays. NIP-44 has additionally been cross-tested
|
route to public Nostr relays. NIP-44 has additionally been cross-tested
|
||||||
directly against the `nostr-tools` reference implementation (conversation
|
directly against the `nostr-tools` reference implementation (conversation
|
||||||
|
|||||||
+28
-6
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Static, Relay-Backed Portable Account Protocol
|
## Static, Relay-Backed Portable Account Protocol
|
||||||
|
|
||||||
**Document version:** 0.5
|
**Document version:** 0.6
|
||||||
**Status:** Revised MVP specification (incorporates external protocol review)
|
**Status:** Revised MVP specification (incorporates external protocol review)
|
||||||
**Date:** July 21, 2026
|
**Date:** July 21, 2026
|
||||||
**Client architecture:** Static progressive web application
|
**Client architecture:** Static progressive web application
|
||||||
@@ -14,6 +14,12 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Changes from v0.5
|
||||||
|
|
||||||
|
1. **Registration and password rotation refuse to overwrite an existing account.** Neither `registerAccount` nor `changePassword` checked whether a credential capsule already existed at the locator address they were about to publish to. Since that address is a NIP-33 replaceable event fully determined by login name + password, registering (or rotating to a new password) with a login name + password combination that was already registered — by the same user re-registering by mistake, or by coincidence with someone else's account under a shared login name — silently destroyed the pre-existing account's identity binding, with no way back short of its own recovery phrase. Both flows now query for an existing capsule at the target locator first and refuse with `AccountAlreadyExistsError` if one is found, or with a retry-oriented error if no relay could be reached to check (§15.6, §18.1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes from v0.4
|
## Changes from v0.4
|
||||||
|
|
||||||
1. **Initial profile publication no longer overwrites an existing profile.** §15.8's kind `0`/`10002`/`10050` publication previously ran unconditionally after every successful registration, including nsec imports — since all three are NIP-01/NIP-65/NIP-17 *replaceable* events, this silently erased an imported identity's real, pre-existing profile and relay lists, replacing them with BitLogin defaults (just the chosen login name as `name`, and only the vault relays). The client now queries for an existing event of each kind before publishing and only fills in whichever one comes back empty, never overwriting one that already exists (§15.8, §28.1).
|
1. **Initial profile publication no longer overwrites an existing profile.** §15.8's kind `0`/`10002`/`10050` publication previously ran unconditionally after every successful registration, including nsec imports — since all three are NIP-01/NIP-65/NIP-17 *replaceable* events, this silently erased an imported identity's real, pre-existing profile and relay lists, replacing them with BitLogin defaults (just the chosen login name as `name`, and only the vault relays). The client now queries for an existing event of each kind before publishing and only fills in whichever one comes back empty, never overwriting one that already exists (§15.8, §28.1).
|
||||||
@@ -1002,7 +1008,20 @@ The client:
|
|||||||
|
|
||||||
## 15.6 Publication
|
## 15.6 Publication
|
||||||
|
|
||||||
Both events are published to the configured vault relays.
|
Before publishing anything, the client checks whether a credential capsule already exists
|
||||||
|
at the locator address this login name + password derive to. That address is a NIP-33
|
||||||
|
replaceable event: publishing over one that already exists there would silently destroy
|
||||||
|
whatever account is already bound to it, with no way back short of that other account's
|
||||||
|
own recovery phrase. Any validly signed event found at the address — decryptable by this
|
||||||
|
client or not — refuses registration outright (`AccountAlreadyExistsError`); if no relay
|
||||||
|
can be reached to check, registration also refuses rather than proceed on an unverifiable
|
||||||
|
assumption of emptiness. This check requires knowing the same login name + password an
|
||||||
|
existing account was registered with, so it is not subject to §16.3's login/no-account
|
||||||
|
non-disclosure rule — reaching this state is already equivalent to being able to log in
|
||||||
|
as that account.
|
||||||
|
|
||||||
|
Once the check passes, both the recovery and credential events are published to the
|
||||||
|
configured vault relays.
|
||||||
|
|
||||||
The registration succeeds only when:
|
The registration succeeds only when:
|
||||||
|
|
||||||
@@ -1173,10 +1192,11 @@ The client:
|
|||||||
|
|
||||||
1. Unlocks the everyday identity.
|
1. Unlocks the everyday identity.
|
||||||
2. Derives the new locator and capsule key.
|
2. Derives the new locator and capsule key.
|
||||||
3. Publishes a new credential capsule with incremented generation and `created_at` per §24.6.
|
3. Checks whether a credential capsule already exists at the *new* locator address (§15.6's same reasoning: this address is fully determined by login name + the chosen new password, and is just as replaceable). If one is found, this refuses with `AccountAlreadyExistsError` rather than proceed — the new password happens to already be registered under this login name by a different account, and rotating onto it would silently destroy that other account's capsule.
|
||||||
4. **Mandatorily** publishes a tombstone at the old locator: a replacement kind-30078 event with the same `d` tag, signed by the old locator key, whose content is an empty envelope. Addressable replacement propagates the tombstone to cooperative relays.
|
4. Publishes a new credential capsule with incremented generation and `created_at` per §24.6.
|
||||||
5. **Mandatorily** publishes a NIP-09 deletion request from the old locator for the old capsule event.
|
5. **Mandatorily** publishes a tombstone at the old locator: a replacement kind-30078 event with the same `d` tag, signed by the old locator key, whose content is an empty envelope. Addressable replacement propagates the tombstone to cooperative relays.
|
||||||
6. Deletes the old local cache and updates the generation high-water mark.
|
6. **Mandatorily** publishes a NIP-09 deletion request from the old locator for the old capsule event.
|
||||||
|
7. Deletes the old local cache and updates the generation high-water mark.
|
||||||
|
|
||||||
Steps 4 and 5 shrink the exposure window on honest relays. They do not defeat an attacker who already downloaded the old capsule (§18.3), and the interface must not claim otherwise.
|
Steps 4 and 5 shrink the exposure window on honest relays. They do not defeat an attacker who already downloaded the old capsule (§18.3), and the interface must not claim otherwise.
|
||||||
|
|
||||||
@@ -1773,6 +1793,8 @@ Resolved since v0.3: generation-rollback detection now enforced (fail-closed) ra
|
|||||||
|
|
||||||
Resolved since v0.4: initial profile publication (§15.8) now checks for an existing kind `0`/`10002`/`10050` event before publishing and never overwrites one that already exists, closing the profile-clobbering gap for imported identities (§28.1).
|
Resolved since v0.4: initial profile publication (§15.8) now checks for an existing kind `0`/`10002`/`10050` event before publishing and never overwrites one that already exists, closing the profile-clobbering gap for imported identities (§28.1).
|
||||||
|
|
||||||
|
Resolved since v0.5: registration and password rotation (§15.6, §18.1) now check for an existing credential capsule at the target locator address before publishing and refuse with `AccountAlreadyExistsError` rather than silently overwrite another account's identity binding.
|
||||||
|
|
||||||
Still open before implementation:
|
Still open before implementation:
|
||||||
|
|
||||||
1. Which bootstrap relays ship with the client, and who holds the maintainer relay-list key (single key, or threshold)?
|
1. Which bootstrap relays ship with the client, and who holds the maintainer relay-list key (single key, or threshold)?
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { repairReplicas } from "./repair.js";
|
|||||||
import { InMemoryKeyValueStore } from "../storage/interface.js";
|
import { InMemoryKeyValueStore } from "../storage/interface.js";
|
||||||
import { raiseHighWaterMark } from "./highWaterMark.js";
|
import { raiseHighWaterMark } from "./highWaterMark.js";
|
||||||
import { RelayPool } from "../nostr/pool.js";
|
import { RelayPool } from "../nostr/pool.js";
|
||||||
import { AccountNotFoundError, RollbackDetectedError } from "./errors.js";
|
import { AccountAlreadyExistsError, AccountNotFoundError, RollbackDetectedError } from "./errors.js";
|
||||||
import { derivePasswordKeys, normalizeLoginName } from "./normalize.js";
|
import { derivePasswordKeys, normalizeLoginName } from "./normalize.js";
|
||||||
import { buildCredentialCapsuleEvent, decryptCredentialCapsuleEvent } from "../capsules/credentialCapsule.js";
|
import { buildCredentialCapsuleEvent, decryptCredentialCapsuleEvent } from "../capsules/credentialCapsule.js";
|
||||||
import { RelayConnection } from "../nostr/relay.js";
|
import { RelayConnection } from "../nostr/relay.js";
|
||||||
@@ -372,4 +372,53 @@ describe("BitLogin Phase 0 end-to-end scenarios (§32)", () => {
|
|||||||
},
|
},
|
||||||
ARGON2_TIMEOUT
|
ARGON2_TIMEOUT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"registering again with a login name + password that's already taken refuses to overwrite the existing account (§15.6)",
|
||||||
|
async () => {
|
||||||
|
const loginName = "reused";
|
||||||
|
const password = generatePassphrase().secret;
|
||||||
|
const original = await registerAccount({ loginName, password, vaultRelayUrls });
|
||||||
|
|
||||||
|
// Same login name + same password -> the exact same locator address. Without a
|
||||||
|
// pre-publish existence check this would silently replace the original account's
|
||||||
|
// credential capsule with a brand-new, unrelated everyday identity and recovery phrase.
|
||||||
|
await expect(registerAccount({ loginName, password, vaultRelayUrls })).rejects.toThrow(AccountAlreadyExistsError);
|
||||||
|
|
||||||
|
// The original account must still be exactly as it was -- same everyday identity,
|
||||||
|
// same recovery phrase still valid.
|
||||||
|
const login = await loginWithPassword({ loginName, password, vaultRelayUrls });
|
||||||
|
expect(login.everydayPublicKey).toBe(original.everydayPublicKey);
|
||||||
|
expect(login.generation).toBe(0);
|
||||||
|
},
|
||||||
|
ARGON2_TIMEOUT * 2
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"rotating to a new password that collides with another account under the same login name is refused (§18.1)",
|
||||||
|
async () => {
|
||||||
|
const loginName = "sharedname";
|
||||||
|
const passwordA = generatePassphrase().secret;
|
||||||
|
const passwordB = generatePassphrase().secret;
|
||||||
|
const accountA = await registerAccount({ loginName, password: passwordA, vaultRelayUrls });
|
||||||
|
const accountB = await registerAccount({ loginName, password: passwordB, vaultRelayUrls });
|
||||||
|
|
||||||
|
// Rotating account A to password B's exact password would target the exact same
|
||||||
|
// locator address as account B's already-registered capsule.
|
||||||
|
await expect(
|
||||||
|
changePassword({ loginName, oldPassword: passwordA, newPassword: passwordB, vaultRelayUrls })
|
||||||
|
).rejects.toThrow(AccountAlreadyExistsError);
|
||||||
|
|
||||||
|
// Neither account was touched: A's old capsule was never tombstoned, and B's capsule
|
||||||
|
// was never overwritten.
|
||||||
|
const loginA = await loginWithPassword({ loginName, password: passwordA, vaultRelayUrls });
|
||||||
|
expect(loginA.everydayPublicKey).toBe(accountA.everydayPublicKey);
|
||||||
|
expect(loginA.generation).toBe(0);
|
||||||
|
|
||||||
|
const loginB = await loginWithPassword({ loginName, password: passwordB, vaultRelayUrls });
|
||||||
|
expect(loginB.everydayPublicKey).toBe(accountB.everydayPublicKey);
|
||||||
|
expect(loginB.generation).toBe(0);
|
||||||
|
},
|
||||||
|
ARGON2_TIMEOUT * 2
|
||||||
|
);
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,7 @@ import { buildDeletionRequest } from "../nostr/nip09.js";
|
|||||||
import { PROTOCOL_CAPSULE_ENCRYPTION, PROTOCOL_PASSWORD_KDF, PROTOCOL_RECOVERY_DERIVATION } from "../capsules/types.js";
|
import { PROTOCOL_CAPSULE_ENCRYPTION, PROTOCOL_PASSWORD_KDF, PROTOCOL_RECOVERY_DERIVATION } from "../capsules/types.js";
|
||||||
import type { CredentialPayload } from "../capsules/types.js";
|
import type { CredentialPayload } from "../capsules/types.js";
|
||||||
import { publishAndVerify, type PublishVerificationResult } from "./publish.js";
|
import { publishAndVerify, type PublishVerificationResult } from "./publish.js";
|
||||||
import { AccountNotFoundError, RegistrationFailedError, RollbackDetectedError } from "./errors.js";
|
import { AccountAlreadyExistsError, AccountNotFoundError, RegistrationFailedError, RollbackDetectedError } from "./errors.js";
|
||||||
import { getHighWaterMark, raiseHighWaterMark } from "./highWaterMark.js";
|
import { getHighWaterMark, raiseHighWaterMark } from "./highWaterMark.js";
|
||||||
import { InMemoryKeyValueStore, type KeyValueStore } from "../storage/interface.js";
|
import { InMemoryKeyValueStore, type KeyValueStore } from "../storage/interface.js";
|
||||||
import type { NostrEvent } from "../nostr/event.js";
|
import type { NostrEvent } from "../nostr/event.js";
|
||||||
@@ -78,6 +78,30 @@ export async function changePassword(params: ChangePasswordParams): Promise<Chan
|
|||||||
|
|
||||||
const newKeys = await derivePasswordKeys(params.newPassword, normalizedLoginName);
|
const newKeys = await derivePasswordKeys(params.newPassword, normalizedLoginName);
|
||||||
const newLocatorPublicKey = getPublicKeyHex(newKeys.locatorPrivateKey);
|
const newLocatorPublicKey = getPublicKeyHex(newKeys.locatorPrivateKey);
|
||||||
|
|
||||||
|
// §15.6/§18.1 — the new locator address is a NIP-33 replaceable event too, fully
|
||||||
|
// determined by login name + the chosen new password. If some other account already
|
||||||
|
// exists there (this login name rotated to a password that happens to already be
|
||||||
|
// registered under it), publishing would silently destroy that other account exactly
|
||||||
|
// like an unchecked registration would.
|
||||||
|
const newLocatorCheckPool = new RelayPool(params.vaultRelayUrls, { authPrivateKey: newKeys.locatorPrivateKey });
|
||||||
|
let existingAtNewLocator;
|
||||||
|
try {
|
||||||
|
existingAtNewLocator = await readCredentialCapsule(newLocatorCheckPool, newLocatorPublicKey, newKeys.capsuleKey, params.timeoutMs);
|
||||||
|
} finally {
|
||||||
|
newLocatorCheckPool.closeAll();
|
||||||
|
}
|
||||||
|
if (!existingAtNewLocator.quorumMet) {
|
||||||
|
throw new RegistrationFailedError(
|
||||||
|
"Couldn't verify the new password isn't already registered under this login name. Please retry, or add more vault relays."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (existingAtNewLocator.candidates.length > 0) {
|
||||||
|
throw new AccountAlreadyExistsError(
|
||||||
|
"Another account is already registered with this login name and the new password you chose. Pick a different new password."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const newGeneration = oldPayload.generation + 1;
|
const newGeneration = oldPayload.generation + 1;
|
||||||
|
|
||||||
const newPayload: CredentialPayload = {
|
const newPayload: CredentialPayload = {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { buildRecoveryCapsuleEvent } from "../capsules/recoveryCapsule.js";
|
|||||||
import { PROTOCOL_CAPSULE_ENCRYPTION, PROTOCOL_PASSWORD_KDF, PROTOCOL_RECOVERY_DERIVATION } from "../capsules/types.js";
|
import { PROTOCOL_CAPSULE_ENCRYPTION, PROTOCOL_PASSWORD_KDF, PROTOCOL_RECOVERY_DERIVATION } from "../capsules/types.js";
|
||||||
import type { CredentialPayload, RecoveryPayload } from "../capsules/types.js";
|
import type { CredentialPayload, RecoveryPayload } from "../capsules/types.js";
|
||||||
import { publishAndVerify, type PublishVerificationResult } from "./publish.js";
|
import { publishAndVerify, type PublishVerificationResult } from "./publish.js";
|
||||||
import { RegistrationFailedError } from "./errors.js";
|
import { readCredentialCapsule } from "./capsuleReader.js";
|
||||||
|
import { RegistrationFailedError, AccountAlreadyExistsError } from "./errors.js";
|
||||||
import type { NostrEvent } from "../nostr/event.js";
|
import type { NostrEvent } from "../nostr/event.js";
|
||||||
|
|
||||||
export interface RegisterAccountParams {
|
export interface RegisterAccountParams {
|
||||||
@@ -53,6 +54,33 @@ export async function registerAccount(params: RegisterAccountParams): Promise<Re
|
|||||||
const normalizedLoginName = normalizeLoginName(params.loginName);
|
const normalizedLoginName = normalizeLoginName(params.loginName);
|
||||||
const now = params.now ?? Math.floor(Date.now() / 1000);
|
const now = params.now ?? Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// §15.6 — the credential capsule address is fully determined by login name + password,
|
||||||
|
// and is a NIP-33 replaceable event: publishing over one that already exists there would
|
||||||
|
// silently destroy that other account's identity binding, with no way back short of its
|
||||||
|
// own recovery phrase. Derive the locator early and check before generating anything else
|
||||||
|
// for this attempt, so a collision fails fast without discarding a freshly generated
|
||||||
|
// recovery phrase or everyday keypair.
|
||||||
|
const { locatorPrivateKey, capsuleKey: credentialCapsuleKey } = await derivePasswordKeys(params.password, normalizedLoginName);
|
||||||
|
const locatorPublicKey = getPublicKeyHex(locatorPrivateKey);
|
||||||
|
const checkPool = new RelayPool(params.vaultRelayUrls, { authPrivateKey: locatorPrivateKey });
|
||||||
|
let existingCapsule;
|
||||||
|
try {
|
||||||
|
existingCapsule = await readCredentialCapsule(checkPool, locatorPublicKey, credentialCapsuleKey, params.timeoutMs);
|
||||||
|
} finally {
|
||||||
|
checkPool.closeAll();
|
||||||
|
}
|
||||||
|
if (!existingCapsule.quorumMet) {
|
||||||
|
throw new RegistrationFailedError(
|
||||||
|
"Couldn't verify this login name and password aren't already registered. Please retry, or add more vault relays."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Any validly signed event at this address (decryptable by us or not) required
|
||||||
|
// knowing this exact locator private key to publish -- which requires this exact
|
||||||
|
// login name + password -- so its mere presence means an account already exists here.
|
||||||
|
if (existingCapsule.candidates.length > 0) {
|
||||||
|
throw new AccountAlreadyExistsError();
|
||||||
|
}
|
||||||
|
|
||||||
// §15.2 — recovery generation happens first so its signed event can be embedded below.
|
// §15.2 — recovery generation happens first so its signed event can be embedded below.
|
||||||
const recoveryPhrase = entropyToRecoveryPhrase(randomEntropy128());
|
const recoveryPhrase = entropyToRecoveryPhrase(randomEntropy128());
|
||||||
const bip39Seed = await recoveryPhraseToSeed(recoveryPhrase);
|
const bip39Seed = await recoveryPhraseToSeed(recoveryPhrase);
|
||||||
@@ -88,8 +116,8 @@ export async function registerAccount(params: RegisterAccountParams): Promise<Re
|
|||||||
});
|
});
|
||||||
|
|
||||||
// §15.5 — password path, embedding the signed recovery event for keyless rebroadcast repair
|
// §15.5 — password path, embedding the signed recovery event for keyless rebroadcast repair
|
||||||
const { locatorPrivateKey, capsuleKey: credentialCapsuleKey } = await derivePasswordKeys(params.password, normalizedLoginName);
|
// (locatorPrivateKey/credentialCapsuleKey/locatorPublicKey were already derived above for the
|
||||||
const locatorPublicKey = getPublicKeyHex(locatorPrivateKey);
|
// existing-account check).
|
||||||
const credentialPayload: CredentialPayload = {
|
const credentialPayload: CredentialPayload = {
|
||||||
schema: SCHEMA_CREDENTIAL_V1,
|
schema: SCHEMA_CREDENTIAL_V1,
|
||||||
account_id: accountId,
|
account_id: accountId,
|
||||||
|
|||||||
@@ -18,6 +18,24 @@ export class RegistrationFailedError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when a credential capsule already exists at the address a registration or
|
||||||
|
* password change is about to publish to (§15.6, §18.1). That address is a NIP-33
|
||||||
|
* replaceable event fully determined by login name + password (or, for a rotation,
|
||||||
|
* login name + the chosen new password) -- publishing over one that already exists
|
||||||
|
* there would silently destroy whatever account is already bound to it, with no way
|
||||||
|
* back short of that other account's own recovery phrase. Unlike {@link AccountNotFoundError},
|
||||||
|
* this is not subject to §16.3's login/no-account non-disclosure rule: reaching this
|
||||||
|
* error already requires supplying the exact login name + password an existing account
|
||||||
|
* was registered with, which is equivalent to already being able to log in as it.
|
||||||
|
*/
|
||||||
|
export class AccountAlreadyExistsError extends Error {
|
||||||
|
constructor(message = "An account already exists with this login name and password. Sign in instead, or choose different credentials.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "AccountAlreadyExistsError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class RecoveryFailedError extends Error {
|
export class RecoveryFailedError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|||||||
Reference in new issue
Block a user