@payload-auth-link/migrate 0.0.0-stage → 0.2.0
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/README.md +49 -2
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +257 -0
- package/dist/cli.js.map +1 -0
- package/dist/doctor/bands.d.ts +16 -0
- package/dist/doctor/bands.d.ts.map +1 -0
- package/dist/doctor/bands.js +49 -0
- package/dist/doctor/bands.js.map +1 -0
- package/dist/doctor/database.d.ts +3 -0
- package/dist/doctor/database.d.ts.map +1 -0
- package/dist/doctor/database.js +110 -0
- package/dist/doctor/database.js.map +1 -0
- package/dist/doctor/plugins.d.ts +12 -0
- package/dist/doctor/plugins.d.ts.map +1 -0
- package/dist/doctor/plugins.js +202 -0
- package/dist/doctor/plugins.js.map +1 -0
- package/dist/doctor/run-doctor.d.ts +48 -0
- package/dist/doctor/run-doctor.d.ts.map +1 -0
- package/dist/doctor/run-doctor.js +236 -0
- package/dist/doctor/run-doctor.js.map +1 -0
- package/dist/doctor/source-scan.d.ts +10 -0
- package/dist/doctor/source-scan.d.ts.map +1 -0
- package/dist/doctor/source-scan.js +142 -0
- package/dist/doctor/source-scan.js.map +1 -0
- package/dist/doctor/types.d.ts +86 -0
- package/dist/doctor/types.d.ts.map +1 -0
- package/dist/doctor/types.js +7 -0
- package/dist/doctor/types.js.map +1 -0
- package/dist/doctor/users.d.ts +10 -0
- package/dist/doctor/users.d.ts.map +1 -0
- package/dist/doctor/users.js +176 -0
- package/dist/doctor/users.js.map +1 -0
- package/dist/importer/apply.d.ts +39 -0
- package/dist/importer/apply.d.ts.map +1 -0
- package/dist/importer/apply.js +588 -0
- package/dist/importer/apply.js.map +1 -0
- package/dist/importer/lifecycle.d.ts +52 -0
- package/dist/importer/lifecycle.d.ts.map +1 -0
- package/dist/importer/lifecycle.js +265 -0
- package/dist/importer/lifecycle.js.map +1 -0
- package/dist/importer/run-import.d.ts +40 -0
- package/dist/importer/run-import.d.ts.map +1 -0
- package/dist/importer/run-import.js +176 -0
- package/dist/importer/run-import.js.map +1 -0
- package/dist/importer/source.d.ts +49 -0
- package/dist/importer/source.d.ts.map +1 -0
- package/dist/importer/source.js +147 -0
- package/dist/importer/source.js.map +1 -0
- package/dist/importer/types.d.ts +69 -0
- package/dist/importer/types.d.ts.map +1 -0
- package/dist/importer/types.js +13 -0
- package/dist/importer/types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -4
- package/src/cli.ts +287 -0
- package/src/doctor/bands.ts +66 -0
- package/src/doctor/database.ts +130 -0
- package/src/doctor/plugins.ts +216 -0
- package/src/doctor/run-doctor.ts +278 -0
- package/src/doctor/source-scan.ts +148 -0
- package/src/doctor/types.ts +92 -0
- package/src/doctor/users.ts +207 -0
- package/src/importer/apply.ts +731 -0
- package/src/importer/lifecycle.ts +328 -0
- package/src/importer/run-import.ts +237 -0
- package/src/importer/source.ts +211 -0
- package/src/importer/types.ts +96 -0
- package/src/index.ts +53 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth plugin classification (issue #52): for each plugin the
|
|
3
|
+
* app's `betterAuthOptions.plugins` declares, the doctor says whether
|
|
4
|
+
* payload-auth-link's engine supports it out of the box
|
|
5
|
+
* (`autoDeclare` derives its models from Better Auth's own
|
|
6
|
+
* `getAuthTables`), whether it is a probed satellite, or whether it
|
|
7
|
+
* blocks migration.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Core + first-party plugins the engine derives confidently. Mirrors
|
|
11
|
+
* what the derive-schema snapshot tests pin in
|
|
12
|
+
* packages/provider-better-auth; new entries land together with a
|
|
13
|
+
* derived-schema probe.
|
|
14
|
+
*/
|
|
15
|
+
const autoDeclared = new Set([
|
|
16
|
+
'magic-link',
|
|
17
|
+
'email-otp',
|
|
18
|
+
'anonymous',
|
|
19
|
+
'two-factor',
|
|
20
|
+
'admin',
|
|
21
|
+
'organization',
|
|
22
|
+
'jwt',
|
|
23
|
+
'username',
|
|
24
|
+
'phone-number',
|
|
25
|
+
'generic-oauth',
|
|
26
|
+
'one-time-token',
|
|
27
|
+
'multi-session',
|
|
28
|
+
'bearer',
|
|
29
|
+
'expo',
|
|
30
|
+
'siwe',
|
|
31
|
+
'next-cookies',
|
|
32
|
+
'tanstack-start-cookies',
|
|
33
|
+
'svelte-kit-cookies',
|
|
34
|
+
'solid-start-cookies',
|
|
35
|
+
]);
|
|
36
|
+
/** Satellites with a committed schema probe in this repo (compat matrix). */
|
|
37
|
+
const probedSatellites = new Set([
|
|
38
|
+
'@better-auth/passkey',
|
|
39
|
+
'@better-auth/api-key',
|
|
40
|
+
]);
|
|
41
|
+
/** Satellites explicitly unsupported until probed (see compatibility.md). */
|
|
42
|
+
const unsupportedSatellites = new Map([
|
|
43
|
+
[
|
|
44
|
+
'@better-auth/sso',
|
|
45
|
+
'not probed by payload-auth-link; adding it meets UnsupportedSchemaError at wiring time',
|
|
46
|
+
],
|
|
47
|
+
]);
|
|
48
|
+
/**
|
|
49
|
+
* Payload-auth's own plugin additions the engine has no equivalent
|
|
50
|
+
* for. `oidcProvider` is the headline: Better Auth 1.7 removed it, and
|
|
51
|
+
* the replacement (@better-auth/oauth-provider) uses different tables,
|
|
52
|
+
* so the data move is the app's phase-1 prerequisite.
|
|
53
|
+
*/
|
|
54
|
+
const removedUpstream = new Map([
|
|
55
|
+
[
|
|
56
|
+
'oidc-provider',
|
|
57
|
+
'Better Auth 1.7 removed `oidcProvider`; moving to @better-auth/oauth-provider is a schema migration (oauthApplication → oauthClient, token splits, jwks gains alg/crv) — plan it before the auth migration',
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
'mcp',
|
|
61
|
+
'Better Auth 1.7 removed the `mcp` plugin; move its surface to @better-auth/oauth-provider',
|
|
62
|
+
],
|
|
63
|
+
]);
|
|
64
|
+
/** The satellite's own id convention: package name or its short id. */
|
|
65
|
+
function satelliteId(id) {
|
|
66
|
+
return id.startsWith('@better-auth/') ? id : `@better-auth/${id}`;
|
|
67
|
+
}
|
|
68
|
+
export function classifyPlugins(ids) {
|
|
69
|
+
const findings = [];
|
|
70
|
+
for (const raw of ids) {
|
|
71
|
+
const id = raw.trim();
|
|
72
|
+
if (id === '') {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const satellite = satelliteId(id);
|
|
76
|
+
if (removedUpstream.has(id) || removedUpstream.has(satellite)) {
|
|
77
|
+
findings.push({
|
|
78
|
+
id,
|
|
79
|
+
status: 'removed-upstream',
|
|
80
|
+
detail: removedUpstream.get(id) ?? removedUpstream.get(satellite) ?? '',
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (id === 'oidcProvider') {
|
|
85
|
+
const detail = removedUpstream.get('oidc-provider') ?? '';
|
|
86
|
+
findings.push({ id, status: 'removed-upstream', detail });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (unsupportedSatellites.has(satellite)) {
|
|
90
|
+
findings.push({
|
|
91
|
+
id,
|
|
92
|
+
status: 'unsupported',
|
|
93
|
+
detail: unsupportedSatellites.get(satellite) ?? '',
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (probedSatellites.has(satellite)) {
|
|
98
|
+
findings.push({
|
|
99
|
+
id,
|
|
100
|
+
status: 'supported',
|
|
101
|
+
detail: 'probed satellite — schema covered by the committed probe and the compat matrix',
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (autoDeclared.has(id)) {
|
|
106
|
+
findings.push({
|
|
107
|
+
id,
|
|
108
|
+
status: 'auto-declarable',
|
|
109
|
+
detail: 'derived from Better Auth’s own getAuthTables via autoDeclare',
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (satellite !== id) {
|
|
114
|
+
findings.push({
|
|
115
|
+
id,
|
|
116
|
+
status: 'unprobed',
|
|
117
|
+
detail: 'Better Auth satellite without a committed probe; the engine will derive its models but nothing here has validated them',
|
|
118
|
+
});
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
findings.push({
|
|
122
|
+
id,
|
|
123
|
+
status: 'unprobed',
|
|
124
|
+
detail: 'unknown plugin id; the engine derives what Better Auth’s schema reports, but treat it as unvalidated until probed',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return findings;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* payload-auth does not keep the builder's Better Auth options on the
|
|
131
|
+
* built config (the plugin closure holds them), so the doctor derives
|
|
132
|
+
* the plugin set from what IS visible: the collections payload-auth
|
|
133
|
+
* built (each plugin model gets a slug) and the config source's plugin
|
|
134
|
+
* calls (for plugins without dedicated tables — magic link, cookies).
|
|
135
|
+
* Both probes feed the same classifier.
|
|
136
|
+
*/
|
|
137
|
+
/** payload-auth slug (baPluginSlugs) → Better Auth plugin id. */
|
|
138
|
+
const slugToPlugin = {
|
|
139
|
+
apiKeys: 'api-key',
|
|
140
|
+
jwks: 'jwt',
|
|
141
|
+
twoFactors: 'two-factor',
|
|
142
|
+
passkeys: 'passkey',
|
|
143
|
+
oauthApplications: 'oidcProvider',
|
|
144
|
+
oauthAccessTokens: 'oidcProvider',
|
|
145
|
+
oauthConsents: 'oidcProvider',
|
|
146
|
+
ssoProviders: 'sso',
|
|
147
|
+
organizations: 'organization',
|
|
148
|
+
members: 'organization',
|
|
149
|
+
invitations: 'organization',
|
|
150
|
+
organizationRoles: 'organization',
|
|
151
|
+
teams: 'organization',
|
|
152
|
+
teamMembers: 'organization',
|
|
153
|
+
deviceCode: 'device-authorization',
|
|
154
|
+
subscriptions: 'subscriptions',
|
|
155
|
+
};
|
|
156
|
+
/** Plugin ids proven live by the collections payload-auth built. */
|
|
157
|
+
export function pluginsFromCollectionSlugs(slugs) {
|
|
158
|
+
const ids = new Set();
|
|
159
|
+
for (const slug of slugs) {
|
|
160
|
+
const id = slugToPlugin[slug];
|
|
161
|
+
if (id !== undefined) {
|
|
162
|
+
ids.add(id);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return [...ids].sort();
|
|
166
|
+
}
|
|
167
|
+
/** Plugin call names a config source can name, mapped to plugin ids. */
|
|
168
|
+
const callNameToPlugin = {
|
|
169
|
+
magicLink: 'magic-link',
|
|
170
|
+
emailOTP: 'email-otp',
|
|
171
|
+
anonymous: 'anonymous',
|
|
172
|
+
twoFactor: 'two-factor',
|
|
173
|
+
admin: 'admin',
|
|
174
|
+
organization: 'organization',
|
|
175
|
+
jwt: 'jwt',
|
|
176
|
+
username: 'username',
|
|
177
|
+
phoneNumber: 'phone-number',
|
|
178
|
+
genericOAuth: 'generic-oauth',
|
|
179
|
+
nextCookies: 'next-cookies',
|
|
180
|
+
tanstackStartCookies: 'tanstack-start-cookies',
|
|
181
|
+
oidcProvider: 'oidcProvider',
|
|
182
|
+
passkey: 'passkey',
|
|
183
|
+
apiKey: 'api-key',
|
|
184
|
+
sso: 'sso',
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Plugin ids found by scanning the config source for plugin factory
|
|
188
|
+
* calls. Regex on purpose: the loaded config cannot reveal these
|
|
189
|
+
* (closure-held options), and the doctor must not execute app code
|
|
190
|
+
* beyond loading its config module.
|
|
191
|
+
*/
|
|
192
|
+
export function pluginsFromConfigSource(configSource) {
|
|
193
|
+
const ids = new Set();
|
|
194
|
+
for (const [call, id] of Object.entries(callNameToPlugin)) {
|
|
195
|
+
const pattern = new RegExp(`\\b${call}\\s*\\(`);
|
|
196
|
+
if (pattern.test(configSource)) {
|
|
197
|
+
ids.add(id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return [...ids].sort();
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=plugins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugins.js","sourceRoot":"","sources":["../../src/doctor/plugins.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { BehaviourChange, DoctorReport } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The read-only doctor (issue #52): point it at a payload-auth app and
|
|
4
|
+
* get the exact list of what breaks before touching anything. It loads
|
|
5
|
+
* the app's config module, inspects the users collection and Better
|
|
6
|
+
* Auth plugins, scans the source tree for call sites that must
|
|
7
|
+
* migrate, and (with a database URL) reports row counts, unverified
|
|
8
|
+
* users, and orphans. It never writes.
|
|
9
|
+
*/
|
|
10
|
+
export type DoctorOptions = {
|
|
11
|
+
/** Absolute or cwd-relative path to the app's payload config. */
|
|
12
|
+
readonly configPath: string;
|
|
13
|
+
/** Source directory to scan. Defaults to the config's directory. */
|
|
14
|
+
readonly srcDir?: string | undefined;
|
|
15
|
+
/** Postgres connection string. Defaults to `DATABASE_URL`. */
|
|
16
|
+
readonly databaseUrl?: string | undefined;
|
|
17
|
+
/** Config loader, injectable for tests. Defaults to dynamic import. */
|
|
18
|
+
readonly loadModule?: ((path: string) => Promise<unknown>) | undefined;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Behaviour changes in payload-auth-link 0.7.0 that a payload-auth app
|
|
22
|
+
* feels on day one. Static by nature; kept next to the doctor so the
|
|
23
|
+
* report is one artifact.
|
|
24
|
+
*/
|
|
25
|
+
export declare const behaviourChanges: readonly BehaviourChange[];
|
|
26
|
+
/**
|
|
27
|
+
* Detect payload-auth in the built config. The plugin stamps
|
|
28
|
+
* `config.custom.hasBetterAuthPlugin = true` when it runs; older or
|
|
29
|
+
* future builds that instead expose `pluginOptions` on the plugin
|
|
30
|
+
* function are honored too.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findPayloadAuthPlugin(config: unknown): {
|
|
33
|
+
pluginOptions: Record<string, unknown>;
|
|
34
|
+
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* The Better Auth plugin ids the app declared. Reads them off
|
|
37
|
+
* `betterAuthOptions.plugins` when a pluginOptions object is visible;
|
|
38
|
+
* callers otherwise derive them from the built collections and the
|
|
39
|
+
* config source (see {@link pluginsFromCollectionSlugs} and
|
|
40
|
+
* {@link pluginsFromConfigSource}).
|
|
41
|
+
*/
|
|
42
|
+
export declare function betterAuthPluginIds(pluginOptions: Record<string, unknown>): string[];
|
|
43
|
+
/** The users collection slug the payload-auth app configured. */
|
|
44
|
+
export declare function usersSlugOf(pluginOptions: Record<string, unknown>): string;
|
|
45
|
+
/** Collection slugs of the built config. */
|
|
46
|
+
export declare function collectionSlugs(config: unknown): string[];
|
|
47
|
+
export declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
|
|
48
|
+
//# sourceMappingURL=run-doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-doctor.d.ts","sourceRoot":"","sources":["../../src/doctor/run-doctor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAe,YAAY,EAAE,MAAM,YAAY,CAAA;AAE5E;;;;;;;GAOG;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACzC,uEAAuE;IACvE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAAA;CACvE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,eAAe,EAqBtD,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG;IACtD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACvC,GAAG,IAAI,CAmBP;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAYpF;AAED,iEAAiE;AACjE,wBAAgB,WAAW,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAK1E;AAED,4CAA4C;AAC5C,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAQzD;AAcD,wBAAsB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAyI7E"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { isRecord } from '@payload-auth-link/core';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { checkVersions } from './bands.js';
|
|
6
|
+
import { introspectDatabase } from './database.js';
|
|
7
|
+
import { classifyPlugins, pluginsFromCollectionSlugs, pluginsFromConfigSource } from './plugins.js';
|
|
8
|
+
import { scanSource } from './source-scan.js';
|
|
9
|
+
import { analyzeUsersCollection, findUsersCollection } from './users.js';
|
|
10
|
+
/**
|
|
11
|
+
* Behaviour changes in payload-auth-link 0.7.0 that a payload-auth app
|
|
12
|
+
* feels on day one. Static by nature; kept next to the doctor so the
|
|
13
|
+
* report is one artifact.
|
|
14
|
+
*/
|
|
15
|
+
export const behaviourChanges = [
|
|
16
|
+
{
|
|
17
|
+
title: 'Provider-linked documents lose local sign-in and forgot/reset',
|
|
18
|
+
detail: '0.7.0 makes documents with linked identities provider-owned: Payload local login and the self-service forgot/reset flow are refused for them (GHSA-f4hj). Break-glass operator accounts (no linked identities) keep everything. Set allowLocalCredentialsForLinkedDocs: true only for a migration window.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
title: 'Suspension is enforced on every strategy',
|
|
22
|
+
detail: 'suspendedUntil now refuses provider sessions, local sign-ins, existing local JWTs and API keys on the next request — including refresh-token and GraphQL.',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: 'Collection access defaults tighten',
|
|
26
|
+
detail: 'Engine collections default to deny-all API access, and the users collection ships governed defaults for update/delete/unlock. Hosts opening access do so explicitly.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: 'req.user is a projection by default',
|
|
30
|
+
detail: "req.user carries { id, collection, email, roles } plus assurance/actor fields, not the full document. Access functions reading other fields see undefined — audit them or opt into reqUser: 'document' (#57).",
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* Detect payload-auth in the built config. The plugin stamps
|
|
35
|
+
* `config.custom.hasBetterAuthPlugin = true` when it runs; older or
|
|
36
|
+
* future builds that instead expose `pluginOptions` on the plugin
|
|
37
|
+
* function are honored too.
|
|
38
|
+
*/
|
|
39
|
+
export function findPayloadAuthPlugin(config) {
|
|
40
|
+
if (!isRecord(config)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (isRecord(config.custom) && config.custom.hasBetterAuthPlugin === true) {
|
|
44
|
+
return { pluginOptions: {} };
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(config.plugins)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
for (const plugin of config.plugins) {
|
|
50
|
+
if (typeof plugin === 'function' && 'pluginOptions' in plugin) {
|
|
51
|
+
const options = plugin.pluginOptions;
|
|
52
|
+
if (isRecord(options)) {
|
|
53
|
+
return { pluginOptions: options };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The Better Auth plugin ids the app declared. Reads them off
|
|
61
|
+
* `betterAuthOptions.plugins` when a pluginOptions object is visible;
|
|
62
|
+
* callers otherwise derive them from the built collections and the
|
|
63
|
+
* config source (see {@link pluginsFromCollectionSlugs} and
|
|
64
|
+
* {@link pluginsFromConfigSource}).
|
|
65
|
+
*/
|
|
66
|
+
export function betterAuthPluginIds(pluginOptions) {
|
|
67
|
+
const baOptions = pluginOptions.betterAuthOptions;
|
|
68
|
+
if (!isRecord(baOptions) || !Array.isArray(baOptions.plugins)) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const ids = [];
|
|
72
|
+
for (const plugin of baOptions.plugins) {
|
|
73
|
+
if (isRecord(plugin) && typeof plugin.id === 'string' && plugin.id !== '') {
|
|
74
|
+
ids.push(plugin.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return ids;
|
|
78
|
+
}
|
|
79
|
+
/** The users collection slug the payload-auth app configured. */
|
|
80
|
+
export function usersSlugOf(pluginOptions) {
|
|
81
|
+
const users = pluginOptions.users;
|
|
82
|
+
return isRecord(users) && typeof users.slug === 'string' && users.slug !== ''
|
|
83
|
+
? users.slug
|
|
84
|
+
: 'users';
|
|
85
|
+
}
|
|
86
|
+
/** Collection slugs of the built config. */
|
|
87
|
+
export function collectionSlugs(config) {
|
|
88
|
+
if (!isRecord(config) || !Array.isArray(config.collections)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return config.collections
|
|
92
|
+
.filter(isRecord)
|
|
93
|
+
.map((collection) => collection.slug)
|
|
94
|
+
.filter((slug) => typeof slug === 'string');
|
|
95
|
+
}
|
|
96
|
+
/** Default config loader: a dynamic import of the TS/JS config module. */
|
|
97
|
+
async function defaultLoadModule(path) {
|
|
98
|
+
const mod = await import(pathToFileURL(path).href);
|
|
99
|
+
let config = isRecord(mod) && 'default' in mod ? mod.default : mod;
|
|
100
|
+
// Payload configs may be async (buildConfig returns a Promise when the
|
|
101
|
+
// config awaits) — the doctor reads the resolved object either way.
|
|
102
|
+
if (typeof config?.then === 'function') {
|
|
103
|
+
config = await config;
|
|
104
|
+
}
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
107
|
+
export async function runDoctor(options) {
|
|
108
|
+
const configPath = resolve(options.configPath);
|
|
109
|
+
const loadModule = options.loadModule ?? defaultLoadModule;
|
|
110
|
+
const config = await loadModule(configPath);
|
|
111
|
+
const configDir = dirname(configPath);
|
|
112
|
+
const srcDir = options.srcDir === undefined ? configDir : resolve(options.srcDir);
|
|
113
|
+
const versions = checkVersions(pathToFileURL(configPath).href);
|
|
114
|
+
const blockers = [];
|
|
115
|
+
const warnings = [];
|
|
116
|
+
for (const finding of versions) {
|
|
117
|
+
if (finding.status === 'blocker') {
|
|
118
|
+
blockers.push({
|
|
119
|
+
code: `${finding.name}-out-of-band`,
|
|
120
|
+
detail: `${finding.name}: ${finding.detail}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else if (finding.status === 'unknown') {
|
|
124
|
+
warnings.push({
|
|
125
|
+
code: `${finding.name}-version-unknown`,
|
|
126
|
+
detail: `${finding.name}: ${finding.detail}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const plugin = findPayloadAuthPlugin(config);
|
|
131
|
+
if (plugin === null) {
|
|
132
|
+
warnings.push({
|
|
133
|
+
code: 'payload-auth-not-detected',
|
|
134
|
+
detail: 'no payload-auth plugin found in the config; if this app runs payload-auth elsewhere, point --config at that config',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (plugin !== null) {
|
|
138
|
+
blockers.push({
|
|
139
|
+
code: 'payload-auth-installed',
|
|
140
|
+
detail: 'payload-auth is installed and configured: migrate with adopt (#49, zero row moves) or the importer (#50, reversible copy). Its admin views, payload.betterAuth accessor, and user-table layout all change.',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// payload-auth keeps the builder's options in its closure, so the
|
|
144
|
+
// plugin set comes from what the built config proves: the collections
|
|
145
|
+
// payload-auth created and the plugin calls in the config source.
|
|
146
|
+
let configSource;
|
|
147
|
+
try {
|
|
148
|
+
configSource = readFileSync(configPath, 'utf8');
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
configSource = '';
|
|
152
|
+
}
|
|
153
|
+
const pluginIds = [
|
|
154
|
+
...new Set([
|
|
155
|
+
...betterAuthPluginIds(plugin?.pluginOptions ?? {}),
|
|
156
|
+
...pluginsFromCollectionSlugs(collectionSlugs(config)),
|
|
157
|
+
...pluginsFromConfigSource(configSource),
|
|
158
|
+
]),
|
|
159
|
+
];
|
|
160
|
+
const pluginFindings = classifyPlugins(pluginIds);
|
|
161
|
+
for (const finding of pluginFindings) {
|
|
162
|
+
if (finding.status === 'removed-upstream') {
|
|
163
|
+
blockers.push({
|
|
164
|
+
code: 'ba-plugin-removed-upstream',
|
|
165
|
+
detail: `${finding.id}: ${finding.detail}`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else if (finding.status === 'unsupported' || finding.status === 'unprobed') {
|
|
169
|
+
warnings.push({ code: 'ba-plugin-unvalidated', detail: `${finding.id}: ${finding.detail}` });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const usersSlug = usersSlugOf(plugin?.pluginOptions ?? {});
|
|
173
|
+
const { finding: users, issues: userIssues } = analyzeUsersCollection(findUsersCollection(config, usersSlug), 'roles');
|
|
174
|
+
for (const issue of userIssues) {
|
|
175
|
+
if (issue.code === 'injected-field-collision' ||
|
|
176
|
+
issue.code === 'roles-field-not-select' ||
|
|
177
|
+
issue.code === 'users-collection-missing') {
|
|
178
|
+
blockers.push(issue);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
warnings.push(issue);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const source = scanSource(srcDir);
|
|
185
|
+
for (const finding of source) {
|
|
186
|
+
if (finding.kind === 'ba-internals') {
|
|
187
|
+
blockers.push({
|
|
188
|
+
code: 'ba-internals-usage',
|
|
189
|
+
detail: `${finding.files.length} file(s) use Better Auth internals (${finding.files.join(', ')}): these break across Better Auth minors and hand-roll session cookies — rewrite on the supported server API (#54) before migrating`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const databaseUrl = options.databaseUrl ?? process.env.DATABASE_URL;
|
|
194
|
+
let database = null;
|
|
195
|
+
if (databaseUrl !== undefined && databaseUrl !== '') {
|
|
196
|
+
try {
|
|
197
|
+
const found = await introspectDatabase(databaseUrl);
|
|
198
|
+
database = found;
|
|
199
|
+
if (found.unverifiedUsers !== null && found.unverifiedUsers > 0) {
|
|
200
|
+
warnings.push({
|
|
201
|
+
code: 'unverified-users',
|
|
202
|
+
detail: `${found.unverifiedUsers} user(s) have emailVerified = false: they will not JIT-link on a provider sign-in; they need a verified email (or a role manager's manual link) after migration`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
for (const orphan of found.orphans) {
|
|
206
|
+
blockers.push({
|
|
207
|
+
code: 'orphan-rows',
|
|
208
|
+
detail: `${orphan.count} orphaned row(s) of kind '${orphan.kind}': the importer refuses unmappable references rather than silently skipping them`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
warnings.push({
|
|
214
|
+
code: 'database-unreachable',
|
|
215
|
+
detail: `could not inspect the database: ${error instanceof Error ? error.message : String(error)}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
warnings.push({
|
|
221
|
+
code: 'database-not-inspected',
|
|
222
|
+
detail: 'no database URL (pass --database-url or set DATABASE_URL); row counts, unverified users, and orphans were not checked',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
versions,
|
|
227
|
+
plugins: pluginFindings,
|
|
228
|
+
users,
|
|
229
|
+
source,
|
|
230
|
+
database,
|
|
231
|
+
behaviourChanges,
|
|
232
|
+
blockers,
|
|
233
|
+
warnings,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=run-doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-doctor.js","sourceRoot":"","sources":["../../src/doctor/run-doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SourceFinding } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The fields the auth-link strategy projects onto `req.user` by
|
|
4
|
+
* default (`reqUser: 'projection'`). Anything else a payload-auth app
|
|
5
|
+
* reads off `req.user` is `undefined` under the projection — silent
|
|
6
|
+
* access-control drift — until `reqUser: 'document'` lands (#57).
|
|
7
|
+
*/
|
|
8
|
+
export declare const projectedUserFields: ReadonlySet<string>;
|
|
9
|
+
export declare function scanSource(srcDir: string): SourceFinding[];
|
|
10
|
+
//# sourceMappingURL=source-scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-scan.d.ts","sourceRoot":"","sources":["../../src/doctor/source-scan.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AA+B/C;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CASlD,CAAA;AA4BF,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAuE1D"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { extname, join, relative } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Read-only source scan (issue #52): the call sites a payload-auth app
|
|
5
|
+
* must migrate, found by pattern over `--src`. Regex-level on
|
|
6
|
+
* purpose — the doctor must run before any code changes, on apps the
|
|
7
|
+
* compiler cannot type-check against payload-auth-link yet.
|
|
8
|
+
*/
|
|
9
|
+
const scanExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs']);
|
|
10
|
+
const skipDirs = new Set([
|
|
11
|
+
'node_modules',
|
|
12
|
+
'dist',
|
|
13
|
+
'.next',
|
|
14
|
+
'.turbo',
|
|
15
|
+
'coverage',
|
|
16
|
+
'test-results',
|
|
17
|
+
'playwright-report',
|
|
18
|
+
]);
|
|
19
|
+
const payloadAuthImport = /from\s+['"]payload-auth(?:\/[A-Za-z0-9_./-]+)?['"]|import\s*\(\s*['"]payload-auth(?:\/[A-Za-z0-9_./-]+)?['"]/g;
|
|
20
|
+
const betterAuthAccessor = /\bpayload\.betterAuth\b|\bbetterAuth\.api\b/g;
|
|
21
|
+
const getPayloadAuth = /\bgetPayloadAuth\s*\(/g;
|
|
22
|
+
// Holding the Better Auth context at all (`$context`) means reaching
|
|
23
|
+
// past the supported surface — hand-hashed keys, hand-signed cookies —
|
|
24
|
+
// whether or not the call sits adjacent on one line.
|
|
25
|
+
const baInternals = /\$context\b|\binternalAdapter\b/g;
|
|
26
|
+
// `req` and `request` are both idiomatic parameter names.
|
|
27
|
+
const reqUserField = /\b(?:req|request)\.user\.([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
28
|
+
/**
|
|
29
|
+
* The fields the auth-link strategy projects onto `req.user` by
|
|
30
|
+
* default (`reqUser: 'projection'`). Anything else a payload-auth app
|
|
31
|
+
* reads off `req.user` is `undefined` under the projection — silent
|
|
32
|
+
* access-control drift — until `reqUser: 'document'` lands (#57).
|
|
33
|
+
*/
|
|
34
|
+
export const projectedUserFields = new Set([
|
|
35
|
+
'id',
|
|
36
|
+
'collection',
|
|
37
|
+
'email',
|
|
38
|
+
'roles',
|
|
39
|
+
'mfaRequired',
|
|
40
|
+
'assurance',
|
|
41
|
+
'actor',
|
|
42
|
+
'organizationId',
|
|
43
|
+
]);
|
|
44
|
+
function walk(dir, into) {
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(dir);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (skipDirs.has(entry) || entry.startsWith('.')) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const full = join(dir, entry);
|
|
57
|
+
let stats;
|
|
58
|
+
try {
|
|
59
|
+
stats = statSync(full);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (stats.isDirectory()) {
|
|
65
|
+
walk(full, into);
|
|
66
|
+
}
|
|
67
|
+
else if (scanExtensions.has(extname(entry))) {
|
|
68
|
+
into.push(full);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function scanSource(srcDir) {
|
|
73
|
+
const files = [];
|
|
74
|
+
walk(srcDir, files);
|
|
75
|
+
const importFiles = new Set();
|
|
76
|
+
const accessorFiles = new Set();
|
|
77
|
+
const getPayloadAuthFiles = new Set();
|
|
78
|
+
const internalsFiles = new Set();
|
|
79
|
+
const reqUserFiles = new Set();
|
|
80
|
+
const reqUserFields = new Set();
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
let content;
|
|
83
|
+
try {
|
|
84
|
+
content = readFileSync(file, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const track = (pattern, into) => {
|
|
90
|
+
pattern.lastIndex = 0;
|
|
91
|
+
if (pattern.test(content)) {
|
|
92
|
+
into.add(file);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
track(payloadAuthImport, importFiles);
|
|
96
|
+
track(betterAuthAccessor, accessorFiles);
|
|
97
|
+
track(getPayloadAuth, getPayloadAuthFiles);
|
|
98
|
+
track(baInternals, internalsFiles);
|
|
99
|
+
for (const match of content.matchAll(reqUserField)) {
|
|
100
|
+
const field = match[1];
|
|
101
|
+
if (field !== undefined && !projectedUserFields.has(field)) {
|
|
102
|
+
reqUserFields.add(field);
|
|
103
|
+
reqUserFiles.add(file);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const rel = (file) => relative(srcDir, file);
|
|
108
|
+
const findings = [
|
|
109
|
+
{
|
|
110
|
+
kind: 'payload-auth-import',
|
|
111
|
+
files: [...importFiles].map(rel).sort(),
|
|
112
|
+
fields: [],
|
|
113
|
+
detail: 'imports from payload-auth; each becomes a payload-auth-link import (codemod, #53)',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
kind: 'get-payload-auth',
|
|
117
|
+
files: [...getPayloadAuthFiles].map(rel).sort(),
|
|
118
|
+
fields: [],
|
|
119
|
+
detail: 'getPayloadAuth(config) call sites; become getPayload({ config }) (codemod, #53)',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
kind: 'better-auth-accessor',
|
|
123
|
+
files: [...accessorFiles].map(rel).sort(),
|
|
124
|
+
fields: [],
|
|
125
|
+
detail: 'payload.betterAuth accessors; migrate to the supported session API (#54), with the opt-in shim (#53) in between',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
kind: 'ba-internals',
|
|
129
|
+
files: [...internalsFiles].map(rel).sort(),
|
|
130
|
+
fields: [],
|
|
131
|
+
detail: 'Better Auth internals ($context.internalAdapter / $context.adapter) — unsupported; the supported server API replaces each bridge (#54)',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
kind: 'req-user-field',
|
|
135
|
+
files: [...reqUserFiles].map(rel).sort(),
|
|
136
|
+
fields: [...reqUserFields].sort(),
|
|
137
|
+
detail: "req.user.<field> reads outside the session projection; under the default projection they read undefined — audit each or opt into reqUser: 'document' (#57)",
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
return findings.filter((finding) => finding.files.length > 0);
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=source-scan.js.map
|