@lenne.tech/nest-server 11.41.7 → 11.41.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/better-auth.md +26 -22
- package/.claude/rules/testing.md +4 -4
- package/CLAUDE.md +5 -3
- package/FRAMEWORK-API.md +1 -1
- package/dist/core/modules/better-auth/core-better-auth-account-issuer.helper.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth-account-issuer.helper.js +13 -0
- package/dist/core/modules/better-auth/core-better-auth-account-issuer.helper.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +3 -2
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth.constants.js +2 -1
- package/dist/core/modules/better-auth/core-better-auth.constants.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth.service.js +46 -3
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.service.js +3 -2
- package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/migration-guides/11.41.7-to-11.41.8.md +135 -0
- package/package.json +7 -9
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +2 -2
- package/src/core/modules/better-auth/README.md +20 -6
- package/src/core/modules/better-auth/core-better-auth-account-issuer.helper.ts +25 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -7
- package/src/core/modules/better-auth/core-better-auth.constants.ts +20 -11
- package/src/core/modules/better-auth/core-better-auth.service.ts +87 -3
- package/src/core/modules/system-setup/core-system-setup.service.ts +12 -8
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Migration Guide: 11.41.7 → 11.41.8
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
| --------------------- | --------------------------------------------------------------------------------------------- |
|
|
7
|
+
| **Security** | better-auth 1.7.7 is now the minimum — critical account takeover in < 1.7.7 (§1) |
|
|
8
|
+
| **Breaking Changes** | None at runtime: 11.41.8 still works on better-auth 1.7.1 (§2). The peer floor rises to 1.7.7, so pnpm reports an unmet peer until you raise the pins |
|
|
9
|
+
| **Data** | Nothing to migrate. The boot drops one index that better-auth 1.7.0–1.7.2 left behind (§2) |
|
|
10
|
+
| **Tooling** | The override guard reports unused overrides; husky and its hooks are gone (§3) |
|
|
11
|
+
| **Migration Effort** | Raise three pins in the API — and two in the app of a fullstack project, in the same change |
|
|
12
|
+
|
|
13
|
+
## Quick Migration
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm update @lenne.tech/nest-server
|
|
17
|
+
pnpm add better-auth@1.7.7 @better-auth/passkey@1.7.7 @better-auth/core@1.7.7
|
|
18
|
+
pnpm run build && pnpm test
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Fullstack projects raise the app in the same change — `@lenne.tech/nuxt-extensions` 1.18.4 and
|
|
22
|
+
`better-auth` / `@better-auth/passkey` 1.7.7 in `projects/app` — see §1.
|
|
23
|
+
|
|
24
|
+
Vendor-mode projects pick up the core update, and raise the same pins.
|
|
25
|
+
|
|
26
|
+
## What's New in 11.41.8
|
|
27
|
+
|
|
28
|
+
### 1. better-auth 1.7.7 is the minimum
|
|
29
|
+
|
|
30
|
+
[GHSA-965c-763c-88jm](https://github.com/better-auth/better-auth/security/advisories/GHSA-965c-763c-88jm)
|
|
31
|
+
(critical): an OAuth state value can be used as a Magic Link to sign in as another user. It covers
|
|
32
|
+
`better-auth >= 1.4.0-beta.18, < 1.7.7`; 1.7.7 is fixed. A project is exposed when it registers the
|
|
33
|
+
`magicLink` plugin — through `betterAuth.options.plugins` — next to a social or Generic OAuth
|
|
34
|
+
provider, with OAuth state stored in the database (the default when a database is configured). An
|
|
35
|
+
attacker who knows a user's email address then gets a normal session for that user, without access
|
|
36
|
+
to the mailbox.
|
|
37
|
+
|
|
38
|
+
At the time of writing the advisory lives only in better-auth's own repository, not yet in the
|
|
39
|
+
database `pnpm audit` reads, so **your audit does not report it**. Do not wait for it to.
|
|
40
|
+
|
|
41
|
+
The framework's peer range is now `>=1.7.7 <1.8.0` for `better-auth`, `@better-auth/passkey` and
|
|
42
|
+
`@better-auth/core` (the floor is forced anyway: `@better-auth/passkey` 1.7.7 itself requires
|
|
43
|
+
`better-auth ^1.7.7`). A project still on 1.7.1 gets an unmet-peer warning, and stays vulnerable
|
|
44
|
+
if it uses Magic Link.
|
|
45
|
+
|
|
46
|
+
**What to do:**
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# projects/api (or a standalone API)
|
|
50
|
+
pnpm add better-auth@1.7.7 @better-auth/passkey@1.7.7 @better-auth/core@1.7.7
|
|
51
|
+
|
|
52
|
+
# projects/app of a fullstack project — in the SAME change
|
|
53
|
+
pnpm add @lenne.tech/nuxt-extensions@1.18.4 better-auth@1.7.7 @better-auth/passkey@1.7.7
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
API and app must resolve the same better-auth version: they speak one protocol, and a split
|
|
57
|
+
breaks flows like 2FA activation with errors that name neither side. In a monorepo,
|
|
58
|
+
`check-workspace-consistency` fails the check when they differ.
|
|
59
|
+
|
|
60
|
+
Upstream's upgrade note for 1.7.7, worth passing on to whoever deploys: upgrade every server that
|
|
61
|
+
shares verification storage at the same time, have users request new Magic Links, and expect
|
|
62
|
+
OAuth / SAML sign-ins that were in progress during the deploy to need a restart. No database
|
|
63
|
+
migration is required. Custom `verification.storeIdentifier.overrides` rules, and storage code that
|
|
64
|
+
inspects verification identifiers, must now expect the `magic-link:` and `auth-state:` prefixes.
|
|
65
|
+
|
|
66
|
+
### 2. better-auth dropped its issuer schema again — what that means for your data
|
|
67
|
+
|
|
68
|
+
better-auth 1.7.0 keyed accounts by `(issuer, accountId)`, and nest-server 11.37–11.41.7 followed:
|
|
69
|
+
every account it wrote carried an `issuer`, and the boot backfilled it on older rows. better-auth
|
|
70
|
+
1.7.3 restored the 1.6 schema — accounts keyed by `(providerId, accountId)` — and no longer reads or
|
|
71
|
+
writes the field. 11.41.8 follows whichever line is INSTALLED, so the upgrade stays compatible in
|
|
72
|
+
both directions:
|
|
73
|
+
|
|
74
|
+
| You run | nest-server 11.41.8 does |
|
|
75
|
+
| ------- | ------------------------ |
|
|
76
|
+
| better-auth 1.7.7 (the target) | writes no `issuer`; on boot drops the unique `(issuer, accountId)` index better-auth 1.7.0–1.7.2 left behind, and forgets the old backfill marker |
|
|
77
|
+
| better-auth 1.7.1 (pins not raised yet) | behaves exactly like 11.41.7: writes the `issuer`, runs the backfill. Still vulnerable (§1) — raise the pins |
|
|
78
|
+
|
|
79
|
+
**Why the index has to go.** With the field no longer written, the index treats every new account
|
|
80
|
+
as having the same, missing issuer, so a user who holds the same account ID at two providers can no
|
|
81
|
+
longer link the second one. MongoDB has no `NOT NULL`, so dropping the index is the whole cleanup —
|
|
82
|
+
upstream's 1.7 upgrade guide says the same. The log shows it once:
|
|
83
|
+
`Dropped the unique index "account_issuer_accountId_uidx" on account(issuer, accountId)`.
|
|
84
|
+
|
|
85
|
+
**Existing `issuer` values stay.** better-auth 1.7.3+ ignores them, and keeping them is what makes a
|
|
86
|
+
rollback safe.
|
|
87
|
+
|
|
88
|
+
**Rolling back to 11.41.7** (better-auth 1.7.1) works for password users: that version's backfill
|
|
89
|
+
runs again on boot — 11.41.8 deleted its completion marker for exactly this — and gives the issuer
|
|
90
|
+
back to every credential account written in the meantime. better-auth recreates its index on first
|
|
91
|
+
use. Social accounts LINKED while on 11.41.8 stay without an issuer; 11.41.7 reports them at boot,
|
|
92
|
+
and they need the issuer set per provider before their next social sign-in.
|
|
93
|
+
|
|
94
|
+
**Vendor mode on better-auth 1.7.1** builds as well: the core compiles against both lines.
|
|
95
|
+
|
|
96
|
+
**Code that imported `createLocalAccountIssuer`** from `@better-auth/core/db` stops compiling with
|
|
97
|
+
better-auth 1.7.3+, which removed it. Remove the call; accounts no longer carry an issuer.
|
|
98
|
+
|
|
99
|
+
### 3. Tooling (this repository)
|
|
100
|
+
|
|
101
|
+
- **The override guard reports unused overrides again.** `scripts/check-overrides.mjs` searched the
|
|
102
|
+
whole lockfile, where pnpm echoes every override into a top-level `overrides:` block — so an
|
|
103
|
+
override whose package had left the tree still counted as "in the tree". It now reads only the
|
|
104
|
+
resolved part. A project that copied the guard (nest-server-starter ships it since 11.41.7) may
|
|
105
|
+
see new `UNUSED` lines after updating it: warnings, not failures — remove the dead entry.
|
|
106
|
+
- **husky is gone.** Its `prepare` script had been removed in 2024, so the hooks never ran; the
|
|
107
|
+
`check` script is the gate. `.husky/`, `.lintstagedrc.json`, `extras/sync-version.ts` and the
|
|
108
|
+
devDependencies `husky` and `find-file-up` (used only by that hook) were removed. Nothing ships
|
|
109
|
+
in the package from any of them.
|
|
110
|
+
|
|
111
|
+
## Compatibility Notes
|
|
112
|
+
|
|
113
|
+
| Pattern | Status |
|
|
114
|
+
| ------- | ------ |
|
|
115
|
+
| `better-auth` / `@better-auth/*` pinned at 1.7.1 | Unmet peer — raise to 1.7.7 (§1) |
|
|
116
|
+
| API on 1.7.7, app still on 1.7.1 | Breaks auth flows — raise both in one change (§1) |
|
|
117
|
+
| `magicLink` plugin with an OAuth provider, on < 1.7.7 | Account takeover — raise now (§1) |
|
|
118
|
+
| nest-server 11.41.8 with better-auth still at 1.7.1 | Works as before (§2); unmet-peer warning, still vulnerable |
|
|
119
|
+
| Rollback to 11.41.7 after upgrading | Works — the backfill runs again (§2) |
|
|
120
|
+
| Own code calling `createLocalAccountIssuer` | Remove it — gone in better-auth 1.7.3+ (§2) |
|
|
121
|
+
|
|
122
|
+
## Troubleshooting
|
|
123
|
+
|
|
124
|
+
| Symptom | Cause | Fix |
|
|
125
|
+
| ------- | ----- | --- |
|
|
126
|
+
| `unmet peer better-auth@">=1.7.7 <1.8.0": found 1.7.1` | Old pin | `pnpm add better-auth@1.7.7 @better-auth/passkey@1.7.7 @better-auth/core@1.7.7` |
|
|
127
|
+
| 2FA activation or passkey sign-in fails with a generic client error after updating | API and app on different better-auth versions | Pin the same version in both |
|
|
128
|
+
| `check-workspace-consistency` fails on better-auth | Same | Same |
|
|
129
|
+
| New `UNUSED` lines from `check:overrides` | The guard sees dead overrides now (§3) | Remove the entry it names |
|
|
130
|
+
| `Could not drop the legacy unique (issuer, accountId) index` at boot | The database user may not drop indexes | `db.account.dropIndex("account_issuer_accountId_uidx")` once, by hand |
|
|
131
|
+
| `E11000 duplicate key … account_issuer_accountId_uidx` when linking an account | The legacy index is still there | Same |
|
|
132
|
+
|
|
133
|
+
## Module Documentation
|
|
134
|
+
|
|
135
|
+
- [BetterAuth INTEGRATION-CHECKLIST](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md) — Step 0, the peer dependencies
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.41.
|
|
3
|
+
"version": "11.41.8",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -144,10 +144,10 @@
|
|
|
144
144
|
"@aws-sdk/client-s3": ">=3.1045.0 <4",
|
|
145
145
|
"@aws-sdk/lib-storage": ">=3.1045.0 <4",
|
|
146
146
|
"@aws-sdk/s3-request-presigner": ">=3.1045.0 <4",
|
|
147
|
-
"@better-auth/core": ">=1.7.
|
|
148
|
-
"@better-auth/passkey": ">=1.7.
|
|
147
|
+
"@better-auth/core": ">=1.7.7 <1.8.0",
|
|
148
|
+
"@better-auth/passkey": ">=1.7.7 <1.8.0",
|
|
149
149
|
"@tus/s3-store": ">=2.0.5 <3",
|
|
150
|
-
"better-auth": ">=1.7.
|
|
150
|
+
"better-auth": ">=1.7.7 <1.8.0",
|
|
151
151
|
"bullmq": ">=5.16.0 <7",
|
|
152
152
|
"ioredis": ">=5.0.0 <7"
|
|
153
153
|
},
|
|
@@ -184,8 +184,8 @@
|
|
|
184
184
|
"@aws-sdk/client-s3": "3.1145.0",
|
|
185
185
|
"@aws-sdk/lib-storage": "3.1145.0",
|
|
186
186
|
"@aws-sdk/s3-request-presigner": "3.1145.0",
|
|
187
|
-
"@better-auth/core": "1.7.
|
|
188
|
-
"@better-auth/passkey": "1.7.
|
|
187
|
+
"@better-auth/core": "1.7.7",
|
|
188
|
+
"@better-auth/passkey": "1.7.7",
|
|
189
189
|
"@compodoc/compodoc": "2.0.0",
|
|
190
190
|
"@nestjs/cli": "11.0.24",
|
|
191
191
|
"@nestjs/schematics": "11.1.0",
|
|
@@ -203,11 +203,9 @@
|
|
|
203
203
|
"@types/passport": "1.0.17",
|
|
204
204
|
"@vitest/coverage-v8": "4.1.11",
|
|
205
205
|
"ansi-colors": "4.1.3",
|
|
206
|
-
"better-auth": "1.7.
|
|
206
|
+
"better-auth": "1.7.7",
|
|
207
207
|
"bullmq": "6.3.11",
|
|
208
208
|
"cross-env": "10.1.0",
|
|
209
|
-
"find-file-up": "2.0.1",
|
|
210
|
-
"husky": "9.1.7",
|
|
211
209
|
"ioredis": "6.0.0",
|
|
212
210
|
"nodemon": "3.1.14",
|
|
213
211
|
"npm-watch": "0.13.0",
|
|
@@ -18,14 +18,14 @@ From `@lenne.tech/nest-server` 11.37.0, better-auth is **not** a dependency of t
|
|
|
18
18
|
non-optional **peer dependency**, so your project declares and owns the version:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
pnpm add better-auth@1.7.
|
|
21
|
+
pnpm add better-auth@1.7.7 @better-auth/passkey@1.7.7 @better-auth/core@1.7.7
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
**Pin all three to the same exact version** — no `^`, no `~`. They are one release train, and a
|
|
25
25
|
mixed set fails in ways the type checker does not catch.
|
|
26
26
|
|
|
27
27
|
> **Do not rely on `pnpm install` to remind you.** On pnpm's default `autoInstallPeers: true` the
|
|
28
|
-
> packages are installed silently at whatever version satisfies `>=1.7.
|
|
28
|
+
> packages are installed silently at whatever version satisfies `>=1.7.7 <1.8.0` — your build stays
|
|
29
29
|
> green while the version is unpinned and free to drift. Verify with `pnpm why better-auth`.
|
|
30
30
|
|
|
31
31
|
**Why peers rather than dependencies:** in a fullstack project the frontend talks to better-auth
|
|
@@ -1028,12 +1028,26 @@ Idempotent indices on `session`, `users`, `account` and `verification`, includin
|
|
|
1028
1028
|
expires unconsumed verification and password-reset documents. Failures are logged at `warn` and do
|
|
1029
1029
|
not block the boot: these help speed, not correctness.
|
|
1030
1030
|
|
|
1031
|
-
### 2.
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1031
|
+
### 2. The account issuer — correctness (depends on the INSTALLED better-auth)
|
|
1032
|
+
|
|
1033
|
+
better-auth 1.7.0–1.7.2 keyed accounts by `(issuer, accountId)`; 1.7.3 restored the 1.6 schema,
|
|
1034
|
+
`(providerId, accountId)`. The boot runs ONE of two steps, decided by what is installed
|
|
1035
|
+
(`core-better-auth-account-issuer.helper.ts`):
|
|
1036
|
+
|
|
1037
|
+
- **better-auth 1.7.3+ (11.41.8+ requires it): `dropLegacyAccountIssuerIndex()`.** Databases that ran
|
|
1038
|
+
1.7.0–1.7.2 (nest-server 11.37–11.41.7) carry a unique `(issuer, accountId)` index. With the field
|
|
1039
|
+
no longer written it refuses a second provider with the same account ID, so the boot drops it —
|
|
1040
|
+
recognised by shape, so a renamed collection or field is covered. Existing `issuer` values stay;
|
|
1041
|
+
they are inert. The backfill marker is deleted, so a rollback to a better-auth that needs the
|
|
1042
|
+
issuer runs the backfill again. Log line: `Dropped the unique index "…" on account(issuer,
|
|
1043
|
+
accountId)`. A failure is logged at `error` and the boot continues.
|
|
1044
|
+
- **better-auth 1.7.0–1.7.2: `backfillAccountIssuers()`, described below.** Kept for a project that
|
|
1045
|
+
has not raised its better-auth pin yet.
|
|
1046
|
+
|
|
1047
|
+
From better-auth 1.7.0 to 1.7.2 an account is keyed by `(issuer, accountId)` and the sign-in route
|
|
1048
|
+
filters on it verbatim. Rows written by better-auth 1.6 have no `issuer` field, so **every existing
|
|
1049
|
+
password user would be locked out by the upgrade**, with a bare 401 and nothing in the log to
|
|
1050
|
+
explain it. The backfill repairs those rows on the first boot after the upgrade.
|
|
1037
1051
|
|
|
1038
1052
|
| Property | Behaviour |
|
|
1039
1053
|
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as betterAuthCoreDb from '@better-auth/core/db';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The synthetic issuer better-auth 1.7.0–1.7.2 stores on credential accounts — or `undefined` when
|
|
5
|
+
* the installed better-auth no longer uses one.
|
|
6
|
+
*
|
|
7
|
+
* better-auth 1.7.0 keyed accounts by `(issuer, accountId)` and exported `createLocalAccountIssuer`
|
|
8
|
+
* to derive the credential issuer. 1.7.3 restored the 1.6 schema — accounts keyed by
|
|
9
|
+
* `(providerId, accountId)` — and removed both. This framework works with either line: its peer
|
|
10
|
+
* range starts at 1.7.7, but a project that has not raised its pin yet must keep signing users in.
|
|
11
|
+
* So every place that writes or repairs an account asks THIS function instead of importing the
|
|
12
|
+
* helper: a static import does not even compile against 1.7.3+, and an issuer written there would
|
|
13
|
+
* be dead data.
|
|
14
|
+
*
|
|
15
|
+
* Resolved at call time from the module namespace, never cached at load, so a test can swap it.
|
|
16
|
+
*/
|
|
17
|
+
export function legacyCredentialAccountIssuer(): string | undefined {
|
|
18
|
+
const create = (betterAuthCoreDb as unknown as Record<string, unknown>).createLocalAccountIssuer;
|
|
19
|
+
return typeof create === 'function' ? (create as (providerId: string) => string)('credential') : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Whether the installed better-auth keys accounts by `(issuer, accountId)` — 1.7.0 to 1.7.2. */
|
|
23
|
+
export function usesLegacyAccountIssuer(): boolean {
|
|
24
|
+
return legacyCredentialAccountIssuer() !== undefined;
|
|
25
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createLocalAccountIssuer } from '@better-auth/core/db';
|
|
2
1
|
import { Injectable, Logger, Optional } from '@nestjs/common';
|
|
3
2
|
import { InjectConnection } from '@nestjs/mongoose';
|
|
4
3
|
import * as bcrypt from 'bcrypt';
|
|
@@ -19,6 +18,7 @@ const scryptPromise = (password: string, salt: string, keylen: number, options:
|
|
|
19
18
|
|
|
20
19
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
21
20
|
import { maskEmail } from '../../common/helpers/logging.helper';
|
|
21
|
+
import { legacyCredentialAccountIssuer } from './core-better-auth-account-issuer.helper';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Interface for Better-Auth session user
|
|
@@ -531,16 +531,16 @@ export class CoreBetterAuthUserMapper {
|
|
|
531
531
|
// Store account matching Better-Auth's format:
|
|
532
532
|
// - userId: ObjectId referencing users._id
|
|
533
533
|
// - accountId: string version of users._id
|
|
534
|
-
// - issuer:
|
|
535
|
-
// accountId), and
|
|
536
|
-
// the
|
|
537
|
-
//
|
|
538
|
-
|
|
534
|
+
// - issuer: ONLY while the installed better-auth is 1.7.0–1.7.2, which key accounts by
|
|
535
|
+
// (issuer, accountId) — a row without it is not found, and the migrated user gets a 401 on
|
|
536
|
+
// the very sign-in that triggered the migration. 1.7.3+ went back to (providerId,
|
|
537
|
+
// accountId) and writes no issuer. Always derived from the helper, never hand-written.
|
|
538
|
+
const credentialIssuer = legacyCredentialAccountIssuer();
|
|
539
539
|
await accountsCollection.insertOne({
|
|
540
540
|
accountId: userIdHex,
|
|
541
541
|
createdAt: now,
|
|
542
542
|
id: this.generateId(),
|
|
543
|
-
issuer:
|
|
543
|
+
...(credentialIssuer ? { issuer: credentialIssuer } : {}),
|
|
544
544
|
password: passwordHash,
|
|
545
545
|
providerId: 'credential',
|
|
546
546
|
updatedAt: now,
|
|
@@ -73,27 +73,36 @@ export const BETTER_AUTH_CONFIG = 'BETTER_AUTH_CONFIG';
|
|
|
73
73
|
export const BETTER_AUTH_COOKIE_DOMAIN = 'BETTER_AUTH_COOKIE_DOMAIN';
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* better-auth's default
|
|
76
|
+
* better-auth's default names for the account collection and for fields inside it.
|
|
77
77
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
78
|
+
* All are overridable by a consumer through `betterAuth.options.account.modelName` / `.fields.*`,
|
|
79
|
+
* which `better-auth.config.ts` spreads onto the resolved config verbatim. Any framework code
|
|
80
|
+
* touching that collection directly MUST resolve the real names from the running instance and fall
|
|
81
|
+
* back to these — a hardcoded name silently addresses a collection or an index better-auth does not
|
|
82
|
+
* use, and the operation then reports success by saying nothing.
|
|
83
|
+
*
|
|
84
|
+
* `issuer` is only used by better-auth 1.7.0–1.7.2; 1.7.3 restored the 1.6 schema (accounts keyed by
|
|
85
|
+
* `(providerId, accountId)` again). Its name is still needed on both lines: for the backfill while a
|
|
86
|
+
* project runs 1.7.0–1.7.2, and afterwards to recognise the unique index those versions left behind —
|
|
87
|
+
* see `CoreBetterAuthService.dropLegacyAccountIssuerIndex()`.
|
|
84
88
|
*/
|
|
85
89
|
export const DEFAULT_ACCOUNT_MODEL_NAME = 'account';
|
|
86
90
|
export const DEFAULT_ACCOUNT_ISSUER_FIELD = 'issuer';
|
|
91
|
+
export const DEFAULT_ACCOUNT_ACCOUNT_ID_FIELD = 'accountId';
|
|
87
92
|
|
|
88
93
|
/**
|
|
89
94
|
* Collection holding one-shot completion markers for boot-time data migrations, and the marker id
|
|
90
|
-
* of the `account.issuer` backfill
|
|
95
|
+
* of the `account.issuer` backfill.
|
|
96
|
+
*
|
|
97
|
+
* The backfill only runs while the installed better-auth still keys accounts by issuer (1.7.0–1.7.2,
|
|
98
|
+
* see `core-better-auth-account-issuer.helper.ts`). On 1.7.3+ the boot DELETES this marker instead:
|
|
99
|
+
* should the project roll back to a better-auth that needs the issuer, the backfill then runs again
|
|
100
|
+
* and repairs the accounts written in the meantime, rather than being skipped by a stale marker.
|
|
91
101
|
*
|
|
92
102
|
* The marker exists for cost, not for correctness: the backfill is idempotent and safe to repeat,
|
|
93
103
|
* but its filters (`$exists: false`, `$ne`) cannot use an index, so without a marker every boot of
|
|
94
|
-
* every replica pays a full pass over the account collection
|
|
95
|
-
*
|
|
96
|
-
* again without clearing this one.
|
|
104
|
+
* every replica pays a full pass over the account collection. Versioned in the id so a future
|
|
105
|
+
* backfill of the same field can run again without clearing this one.
|
|
97
106
|
*/
|
|
98
107
|
export const BACKFILL_MARKER_COLLECTION = 'better-auth-backfills';
|
|
99
108
|
export const ACCOUNT_ISSUER_BACKFILL_ID = 'account-issuer-backfill-v1';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createLocalAccountIssuer } from '@better-auth/core/db';
|
|
2
1
|
import { BadRequestException, Inject, Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common';
|
|
3
2
|
import { InjectConnection } from '@nestjs/mongoose';
|
|
4
3
|
import { Request } from 'express';
|
|
@@ -12,6 +11,7 @@ import { ConfigService } from '../../common/services/config.service';
|
|
|
12
11
|
import { ErrorCode } from '../error-code/error-codes';
|
|
13
12
|
import { resolveBetterAuthCookiePrefix } from './better-auth-cookie-prefix.helper';
|
|
14
13
|
import { BetterAuthInstance } from './better-auth.config';
|
|
14
|
+
import { legacyCredentialAccountIssuer, usesLegacyAccountIssuer } from './core-better-auth-account-issuer.helper';
|
|
15
15
|
import { isJwtShaped } from './core-better-auth-token.helper';
|
|
16
16
|
import { BetterAuthSessionUser } from './core-better-auth-user.mapper';
|
|
17
17
|
import { convertExpressHeaders, parseCookieHeader, signCookieValueIfNeeded } from './core-better-auth-web.helper';
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
BETTER_AUTH_CONFIG,
|
|
22
22
|
BETTER_AUTH_COOKIE_DOMAIN,
|
|
23
23
|
BETTER_AUTH_INSTANCE,
|
|
24
|
+
DEFAULT_ACCOUNT_ACCOUNT_ID_FIELD,
|
|
24
25
|
DEFAULT_ACCOUNT_ISSUER_FIELD,
|
|
25
26
|
DEFAULT_ACCOUNT_MODEL_NAME,
|
|
26
27
|
} from './core-better-auth.constants';
|
|
@@ -113,12 +114,21 @@ export class CoreBetterAuthService implements OnModuleInit {
|
|
|
113
114
|
*
|
|
114
115
|
* Order matters: the backfill's update can use the `{ providerId: 1, userId: 1 }` index created
|
|
115
116
|
* below, so indices come first.
|
|
117
|
+
*
|
|
118
|
+
* Which account step runs depends on the INSTALLED better-auth, not on this package's version:
|
|
119
|
+
* 1.7.0–1.7.2 key accounts by issuer and need the backfill; 1.7.3+ dropped the issuer and need the
|
|
120
|
+
* unique index those versions left behind removed instead. A project that has not raised its
|
|
121
|
+
* better-auth pin yet therefore keeps working exactly as before.
|
|
116
122
|
*/
|
|
117
123
|
async onModuleInit(): Promise<void> {
|
|
118
124
|
if (!this.isEnabled() || !this.connection?.db) return;
|
|
119
125
|
|
|
120
126
|
await this.ensureIndices();
|
|
121
|
-
|
|
127
|
+
if (usesLegacyAccountIssuer()) {
|
|
128
|
+
await this.backfillAccountIssuers();
|
|
129
|
+
} else {
|
|
130
|
+
await this.dropLegacyAccountIssuerIndex();
|
|
131
|
+
}
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
/**
|
|
@@ -195,6 +205,11 @@ export class CoreBetterAuthService implements OnModuleInit {
|
|
|
195
205
|
protected async backfillAccountIssuers(): Promise<void> {
|
|
196
206
|
if (!this.isEnabled() || !this.connection?.db) return;
|
|
197
207
|
|
|
208
|
+
// Only better-auth 1.7.0–1.7.2 key accounts by issuer. On 1.7.3+ there is nothing to backfill —
|
|
209
|
+
// and nothing to derive the value from.
|
|
210
|
+
const credentialIssuer = legacyCredentialAccountIssuer();
|
|
211
|
+
if (!credentialIssuer) return;
|
|
212
|
+
|
|
198
213
|
const db = this.connection.db;
|
|
199
214
|
|
|
200
215
|
// The consumer can rename both of these through `betterAuth.options.account`, which
|
|
@@ -243,7 +258,7 @@ export class CoreBetterAuthService implements OnModuleInit {
|
|
|
243
258
|
pending.map((doc) => ({
|
|
244
259
|
updateOne: {
|
|
245
260
|
filter: { _id: doc._id },
|
|
246
|
-
update: { $set: { [issuerField]:
|
|
261
|
+
update: { $set: { [issuerField]: credentialIssuer } },
|
|
247
262
|
},
|
|
248
263
|
})),
|
|
249
264
|
{ ordered: false },
|
|
@@ -306,6 +321,75 @@ export class CoreBetterAuthService implements OnModuleInit {
|
|
|
306
321
|
}
|
|
307
322
|
}
|
|
308
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Drops the unique `(issuer, accountId)` index better-auth 1.7.0–1.7.2 created on the account
|
|
326
|
+
* collection, and forgets the issuer-backfill marker. Runs on better-auth 1.7.3+ only.
|
|
327
|
+
*
|
|
328
|
+
* better-auth 1.7.0 keyed accounts by `(issuer, accountId)`, and its MongoDB adapter created that
|
|
329
|
+
* index lazily. 1.7.3 restored the 1.6 schema — `(providerId, accountId)` — and no longer writes
|
|
330
|
+
* `issuer`. The index stays behind in every database that ran those versions (this framework
|
|
331
|
+
* required them from 11.37.0 to 11.41.7), and with the field gone it treats every new account as
|
|
332
|
+
* having the same, missing issuer: a user who holds the same account ID at two providers can no
|
|
333
|
+
* longer link the second one. MongoDB has no `NOT NULL`, so dropping the index is the whole
|
|
334
|
+
* cleanup — upstream's 1.7 upgrade guide, "Account identity keeps the provider key". Existing
|
|
335
|
+
* `issuer` values are inert and stay, which also keeps a rollback possible.
|
|
336
|
+
*
|
|
337
|
+
* The marker is deleted for the same rollback: back on a better-auth that needs the issuer, the
|
|
338
|
+
* backfill must run again and repair the accounts written in the meantime instead of being
|
|
339
|
+
* skipped as "already completed".
|
|
340
|
+
*
|
|
341
|
+
* The index is recognised by its SHAPE — unique, keyed exactly on the issuer and account-id fields
|
|
342
|
+
* — not by name: better-auth derives the name from the collection and field names (by default
|
|
343
|
+
* `account_issuer_accountId_uidx`), and a consumer can rename both through
|
|
344
|
+
* `betterAuth.options.account`.
|
|
345
|
+
*
|
|
346
|
+
* Idempotent and cheap: one `listIndexes` and one `deleteOne` per boot. Never fatal — a failure is
|
|
347
|
+
* logged as an error and the server still starts.
|
|
348
|
+
*/
|
|
349
|
+
protected async dropLegacyAccountIssuerIndex(): Promise<void> {
|
|
350
|
+
if (!this.isEnabled() || !this.connection?.db) return;
|
|
351
|
+
|
|
352
|
+
const db = this.connection.db;
|
|
353
|
+
const accountOptions = (this.authInstance as any)?.options?.account;
|
|
354
|
+
const modelName: string = accountOptions?.modelName ?? DEFAULT_ACCOUNT_MODEL_NAME;
|
|
355
|
+
const issuerField: string = accountOptions?.fields?.issuer ?? DEFAULT_ACCOUNT_ISSUER_FIELD;
|
|
356
|
+
const accountIdField: string = accountOptions?.fields?.accountId ?? DEFAULT_ACCOUNT_ACCOUNT_ID_FIELD;
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await db.collection(BACKFILL_MARKER_COLLECTION).deleteOne({ _id: ACCOUNT_ISSUER_BACKFILL_ID as any });
|
|
360
|
+
|
|
361
|
+
const accounts = db.collection(modelName);
|
|
362
|
+
let indexes: { key?: Record<string, unknown>; name?: string; unique?: boolean }[];
|
|
363
|
+
try {
|
|
364
|
+
indexes = await accounts.indexes();
|
|
365
|
+
} catch (error) {
|
|
366
|
+
// A collection that does not exist yet has no index to drop (NamespaceNotFound).
|
|
367
|
+
if ((error as { code?: number })?.code === 26) return;
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const legacy = indexes.filter(
|
|
372
|
+
(index) =>
|
|
373
|
+
index.unique === true &&
|
|
374
|
+
!!index.name &&
|
|
375
|
+
Object.keys(index.key ?? {}).join(',') === `${issuerField},${accountIdField}`,
|
|
376
|
+
);
|
|
377
|
+
for (const index of legacy) {
|
|
378
|
+
await accounts.dropIndex(index.name as string);
|
|
379
|
+
this.logger.log(
|
|
380
|
+
`Dropped the unique index "${index.name}" on ${modelName}(${issuerField}, ${accountIdField}): ` +
|
|
381
|
+
'better-auth >= 1.7.3 no longer writes the issuer, and the index would refuse new accounts.',
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
this.logger.error(
|
|
386
|
+
`Could not drop the legacy unique (${issuerField}, ${accountIdField}) index on "${modelName}": ` +
|
|
387
|
+
`${error instanceof Error ? error.message : 'unknown'}. Linking an account whose ID another ` +
|
|
388
|
+
'provider already uses will fail until it is dropped by hand.',
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
309
393
|
/**
|
|
310
394
|
* Checks if better-auth is enabled and initialized
|
|
311
395
|
* Returns true only if:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createLocalAccountIssuer } from '@better-auth/core/db';
|
|
2
1
|
import { ForbiddenException, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
|
3
2
|
import { InjectConnection } from '@nestjs/mongoose';
|
|
4
3
|
import { isEmail } from 'class-validator';
|
|
5
4
|
import { Connection } from 'mongoose';
|
|
6
5
|
|
|
7
6
|
import { ConfigService } from '../../common/services/config.service';
|
|
7
|
+
import { legacyCredentialAccountIssuer } from '../better-auth/core-better-auth-account-issuer.helper';
|
|
8
8
|
import { CoreBetterAuthUserMapper } from '../better-auth/core-better-auth-user.mapper';
|
|
9
9
|
import { CoreBetterAuthService } from '../better-auth/core-better-auth.service';
|
|
10
10
|
import { ErrorCode } from '../error-code/error-codes';
|
|
@@ -305,18 +305,22 @@ export class CoreSystemSetupService implements OnApplicationBootstrap {
|
|
|
305
305
|
|
|
306
306
|
// Hash password and create credential account
|
|
307
307
|
const hashedPassword = await context.password.hash(normalizedPassword);
|
|
308
|
-
// better-auth
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
308
|
+
// better-auth 1.7.0–1.7.2 key accounts by (issuer, accountId) and require the issuer;
|
|
309
|
+
// credential accounts carry a synthetic one, always derived via the helper, never a
|
|
310
|
+
// hand-written literal. better-auth 1.7.3+ went back to (providerId, accountId) and knows no
|
|
311
|
+
// issuer field, so the helper answers `undefined` there and nothing is written.
|
|
312
|
+
//
|
|
313
|
+
// Cast to the adapter's OWN parameter type: against better-auth 1.7.0–1.7.2 that type requires
|
|
314
|
+
// `issuer`, and a conditional spread reads as optional — a vendored core compiled against
|
|
315
|
+
// those versions would otherwise not build.
|
|
316
|
+
const credentialIssuer = legacyCredentialAccountIssuer();
|
|
313
317
|
await context.internalAdapter.linkAccount({
|
|
314
318
|
accountId: iamUser.id,
|
|
315
|
-
issuer:
|
|
319
|
+
...(credentialIssuer ? { issuer: credentialIssuer } : {}),
|
|
316
320
|
password: hashedPassword,
|
|
317
321
|
providerId: 'credential',
|
|
318
322
|
userId: iamUser.id,
|
|
319
|
-
});
|
|
323
|
+
} as Parameters<typeof context.internalAdapter.linkAccount>[0]);
|
|
320
324
|
|
|
321
325
|
// Sync to nest-server users collection
|
|
322
326
|
const syncedUser = await this.userMapper.linkOrCreateUser({
|