@payload-auth-link/migrate 0.5.0 → 0.7.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 +20 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +88 -27
- package/dist/cli.js.map +1 -1
- package/dist/doctor/plugins.d.ts.map +1 -1
- package/dist/doctor/plugins.js +6 -2
- package/dist/doctor/plugins.js.map +1 -1
- package/dist/doctor/run-doctor.d.ts.map +1 -1
- package/dist/doctor/run-doctor.js +27 -2
- package/dist/doctor/run-doctor.js.map +1 -1
- package/dist/doctor/users.d.ts +13 -1
- package/dist/doctor/users.d.ts.map +1 -1
- package/dist/doctor/users.js +17 -7
- package/dist/doctor/users.js.map +1 -1
- package/dist/exporters.d.ts +43 -5
- package/dist/exporters.d.ts.map +1 -1
- package/dist/exporters.js +88 -5
- package/dist/exporters.js.map +1 -1
- package/dist/importer/apply.d.ts +2 -0
- package/dist/importer/apply.d.ts.map +1 -1
- package/dist/importer/apply.js +88 -24
- package/dist/importer/apply.js.map +1 -1
- package/dist/importer/guards.d.ts +49 -0
- package/dist/importer/guards.d.ts.map +1 -0
- package/dist/importer/guards.js +175 -0
- package/dist/importer/guards.js.map +1 -0
- package/dist/importer/host-writes.d.ts +6 -2
- package/dist/importer/host-writes.d.ts.map +1 -1
- package/dist/importer/host-writes.js +9 -4
- package/dist/importer/host-writes.js.map +1 -1
- package/dist/importer/lifecycle.d.ts +60 -13
- package/dist/importer/lifecycle.d.ts.map +1 -1
- package/dist/importer/lifecycle.js +130 -24
- package/dist/importer/lifecycle.js.map +1 -1
- package/dist/importer/manifest-mac.d.ts +27 -0
- package/dist/importer/manifest-mac.d.ts.map +1 -0
- package/dist/importer/manifest-mac.js +93 -0
- package/dist/importer/manifest-mac.js.map +1 -0
- package/dist/importer/prelink.d.ts +2 -0
- package/dist/importer/prelink.d.ts.map +1 -1
- package/dist/importer/prelink.js +20 -3
- package/dist/importer/prelink.js.map +1 -1
- package/dist/importer/run-import.d.ts +24 -5
- package/dist/importer/run-import.d.ts.map +1 -1
- package/dist/importer/run-import.js +36 -7
- package/dist/importer/run-import.js.map +1 -1
- package/dist/importer/types.d.ts +12 -1
- package/dist/importer/types.d.ts.map +1 -1
- package/dist/importer/types.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/cli.ts +87 -27
- package/src/doctor/plugins.ts +8 -3
- package/src/doctor/run-doctor.ts +30 -3
- package/src/doctor/users.ts +25 -7
- package/src/exporters.ts +113 -12
- package/src/importer/apply.ts +103 -23
- package/src/importer/guards.ts +223 -0
- package/src/importer/host-writes.ts +15 -4
- package/src/importer/lifecycle.ts +193 -32
- package/src/importer/manifest-mac.ts +125 -0
- package/src/importer/prelink.ts +24 -3
- package/src/importer/run-import.ts +64 -12
- package/src/importer/types.ts +12 -1
- package/src/index.ts +10 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,6 @@ export { applyImport, summarizePlanRows } from './importer/apply.js';
|
|
|
15
15
|
export { countTables, findOrphans, openSource, probeSourceLayout, readFlatRows, readSourceUsers, unverifiedEmails, } from './importer/source.js';
|
|
16
16
|
export { readManifest, writeManifest, verifyImport, rollbackImport, finalizeManifest, defaultManifestPath, defaultPrelinkManifestPath, } from './importer/lifecycle.js';
|
|
17
17
|
export { defaultSourceTables, defaultTargetSlugs, parseSourceMapJson, presentTables, resolveSourceTables, sourceModelNames, } from './importer/types.js';
|
|
18
|
-
export { exportToJsonl, parseExportTables, exportTables } from './exporters.js';
|
|
18
|
+
export { exportToJsonl, parseExportTables, exportTables, optionalAuthTables, resolveAuthTables, } from './exporters.js';
|
|
19
19
|
export { cliMain } from './cli.js';
|
|
20
20
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,SAAS,EACT,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,GACZ,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACxF,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAC5E,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC1F,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAChG,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,WAAW,EACX,eAAe,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,SAAS,EACT,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,GACZ,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACxF,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAC5E,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC1F,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAChG,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,WAAW,EACX,eAAe,GAIhB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,YAAY,EAA2C,MAAM,uBAAuB,CAAA;AAC7F,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,EACL,WAAW,EACX,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,gBAAgB,GAOjB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,GAK3B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,gBAAgB,GAcjB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payload-auth-link/migrate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "payload-auth-link migration kit: read-only doctor/preflight, the payload-auth importer, and exporters.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@payload-auth-link/core": "0.
|
|
44
|
-
"pg": "^8.
|
|
43
|
+
"@payload-auth-link/core": "0.9.0",
|
|
44
|
+
"pg": "^8.23.1",
|
|
45
45
|
"typescript": "^6.0.3"
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
|
@@ -53,17 +53,17 @@
|
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@better-auth/api-key": "1.7.7",
|
|
56
|
-
"@payload-auth-link/core": "0.
|
|
57
|
-
"@payload-auth-link/plugin": "0.
|
|
58
|
-
"@payload-auth-link/provider-better-auth": "0.
|
|
59
|
-
"@payloadcms/db-postgres": "3.
|
|
56
|
+
"@payload-auth-link/core": "0.9.0",
|
|
57
|
+
"@payload-auth-link/plugin": "0.13.0",
|
|
58
|
+
"@payload-auth-link/provider-better-auth": "0.10.0",
|
|
59
|
+
"@payloadcms/db-postgres": "3.91.0",
|
|
60
60
|
"@types/node": "^22",
|
|
61
|
-
"@types/pg": "^8.
|
|
61
|
+
"@types/pg": "^8.23.1",
|
|
62
62
|
"better-auth": "1.7.7",
|
|
63
|
-
"payload": "3.
|
|
63
|
+
"payload": "3.91.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@payload-auth-link/next": "^0.
|
|
66
|
+
"@payload-auth-link/next": "^0.6.0",
|
|
67
67
|
"payload": "^3.83.0 || 4.0.0-beta.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependenciesMeta": {
|
package/src/cli.ts
CHANGED
|
@@ -67,15 +67,29 @@ Commands:
|
|
|
67
67
|
[--source-map '{"accounts":"user_accounts","sessions":"user_sessions"}']
|
|
68
68
|
export --database-url URL [--out file.jsonl]
|
|
69
69
|
[--tables users,accounts | '["users","accounts"]']
|
|
70
|
-
|
|
70
|
+
[--source-map '{"accounts":"user_accounts","sessions":"user_sessions"}']
|
|
71
|
+
(lossless JSONL of every auth table — the off-ramp, #58:
|
|
72
|
+
each mapped model's table, their child tables such as
|
|
73
|
+
users_role / organizations_tags, and every ba_* and
|
|
74
|
+
oauth_* table present; a mapped table that is missing
|
|
75
|
+
exits 1)
|
|
71
76
|
codemod --src ./src [--write]
|
|
72
77
|
from-payload-auth --config ./src/payload.config.ts [--apply] [--sessions=carry|drop]
|
|
73
78
|
[--manifest ./payload-auth-link-migration.json] [--database-url URL] [--json]
|
|
74
79
|
[--source-map '{"accounts":"user_accounts","sessions":"user_sessions"}']
|
|
75
80
|
verify --config ./src/payload.config.ts [--database-url URL] [--json]
|
|
76
81
|
[--source-map '{"accounts":"user_accounts","sessions":"user_sessions"}']
|
|
77
|
-
rollback --config ./src/payload.config.ts [--manifest ...]
|
|
78
|
-
|
|
82
|
+
rollback --config ./src/payload.config.ts [--manifest ...] [--slugs JSON]
|
|
83
|
+
[--allow-unsigned-manifest] (a manifest without a full
|
|
84
|
+
signature: pre-0.6.0, or 0.6.0's which leaves 'finalized'
|
|
85
|
+
unsigned; also accepted by from-payload-auth /
|
|
86
|
+
adopt-prelink / finalize when resuming)
|
|
87
|
+
[--allow-unsigned-deletes] (with an UNSIGNED manifest,
|
|
88
|
+
also delete its created rows and restore plaintext
|
|
89
|
+
values; without it an unsigned rollback deletes nothing)
|
|
90
|
+
finalize --config ./src/payload.config.ts [--manifest ...]
|
|
91
|
+
(records the finalize in the app's database and
|
|
92
|
+
re-signs the manifest; rollback is refused afterwards)
|
|
79
93
|
adopt-prelink --config ./src/payload.config.ts [--apply] [--json]
|
|
80
94
|
[--manifest ./payload-auth-link-prelink.json]
|
|
81
95
|
[--users-slug users] [--verified-field emailVerified]
|
|
@@ -99,13 +113,17 @@ Commands:
|
|
|
99
113
|
writes the payload-auth-link collections through the Payload Local
|
|
100
114
|
API, records every created row and field change in the manifest,
|
|
101
115
|
and never modifies the source tables. verify checks both sides;
|
|
102
|
-
rollback undoes exactly the manifest
|
|
103
|
-
|
|
116
|
+
rollback undoes exactly the manifest (and only ever deletes rows in
|
|
117
|
+
the import's own target collections); finalize records the point of
|
|
118
|
+
no return so the legacy tables may be dropped later, in their own
|
|
119
|
+
migration (adopt mode: those are the live tables — never drop them).
|
|
104
120
|
|
|
105
121
|
The manifest lands in the CURRENT DIRECTORY unless --manifest says
|
|
106
122
|
otherwise. It holds ids and sealed values only (no emails in the
|
|
107
123
|
clear; sealed with the app's PAYLOAD_SECRET, so rollback must run
|
|
108
|
-
against the same app) and is written with mode 0600.
|
|
124
|
+
against the same app) and is written with mode 0600. It is signed
|
|
125
|
+
with the same secret: rollback, finalize, and a resumed --apply
|
|
126
|
+
refuse an edited manifest. Delete it once
|
|
109
127
|
the migration is finalized and your soak window has passed.
|
|
110
128
|
|
|
111
129
|
codemod rewrites the three named payload-auth call-site patterns
|
|
@@ -281,20 +299,40 @@ async function main(
|
|
|
281
299
|
return 3
|
|
282
300
|
}
|
|
283
301
|
}
|
|
302
|
+
// --source-map: the same layout semantics as every other command
|
|
303
|
+
// (issue #110) — renamed tables are exported, null-mapped models
|
|
304
|
+
// are skipped, and a mapped table that is missing exits 1.
|
|
305
|
+
let sourceTables: import('./importer/types.js').ResolvedSourceTables | undefined
|
|
306
|
+
const sourceMapFlag = flags.get('source-map')
|
|
307
|
+
if (typeof sourceMapFlag === 'string') {
|
|
308
|
+
try {
|
|
309
|
+
const { parseSourceMapJson, resolveSourceTables } = await import('./importer/types.js')
|
|
310
|
+
sourceTables = resolveSourceTables(parseSourceMapJson(sourceMapFlag))
|
|
311
|
+
} catch (error) {
|
|
312
|
+
err.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
313
|
+
return 3
|
|
314
|
+
}
|
|
315
|
+
}
|
|
284
316
|
const { exportToJsonl } = await import('./exporters.js')
|
|
285
317
|
const result = await exportToJsonl({
|
|
286
318
|
connectionString: databaseUrl,
|
|
287
319
|
format: 'jsonl',
|
|
288
320
|
...(tables === undefined ? {} : { tables }),
|
|
289
|
-
|
|
321
|
+
...(sourceTables === undefined ? {} : { sourceTables }),
|
|
322
|
+
// Progress goes to stderr: stdout may BE the JSONL document.
|
|
323
|
+
log: (message) => err.write(`${message}\n`),
|
|
290
324
|
})
|
|
291
|
-
// A
|
|
292
|
-
// (
|
|
293
|
-
if (
|
|
325
|
+
// A required table that doesn't exist is an error, not a gap
|
|
326
|
+
// (issues #68, #110): nothing is silently omitted.
|
|
327
|
+
if (result.missing.length > 0) {
|
|
294
328
|
for (const table of result.missing) {
|
|
295
329
|
err.write(`export: table '${table}' does not exist on the source\n`)
|
|
296
330
|
}
|
|
297
|
-
err.write(
|
|
331
|
+
err.write(
|
|
332
|
+
tables === undefined
|
|
333
|
+
? "nothing silently omitted; pass --source-map to name your layout's tables (null for a model that is not installed)\n"
|
|
334
|
+
: 'nothing silently omitted; fix --tables or drop it\n'
|
|
335
|
+
)
|
|
298
336
|
return 1
|
|
299
337
|
}
|
|
300
338
|
if (typeof flags.get('out') === 'string') {
|
|
@@ -369,27 +407,39 @@ async function main(
|
|
|
369
407
|
)
|
|
370
408
|
return 0
|
|
371
409
|
}
|
|
372
|
-
//
|
|
373
|
-
//
|
|
410
|
+
// The remaining commands (from-payload-auth, verify, rollback,
|
|
411
|
+
// finalize, adopt-prelink) all load a Payload instance, so they
|
|
412
|
+
// need the config.
|
|
413
|
+
const configFlag = flags.get('config')
|
|
414
|
+
if (typeof configFlag !== 'string' || configFlag === '') {
|
|
415
|
+
err.write(`${command} requires --config <path to payload.config.ts>\n`)
|
|
416
|
+
return 3
|
|
417
|
+
}
|
|
418
|
+
// finalize --config ... [--manifest ...]: boots the target app to
|
|
419
|
+
// sign the finalized manifest and record the database marker.
|
|
374
420
|
if (command === 'finalize') {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
421
|
+
let finalized: Awaited<ReturnType<typeof runFinalize>>
|
|
422
|
+
try {
|
|
423
|
+
finalized = await runFinalize({
|
|
424
|
+
configPath: new URL(configFlag, `file://${process.cwd()}/`).pathname,
|
|
425
|
+
log,
|
|
426
|
+
...(typeof flags.get('manifest') === 'string'
|
|
427
|
+
? { manifestPath: flags.get('manifest') as string }
|
|
428
|
+
: {}),
|
|
429
|
+
...(typeof flags.get('slugs') === 'string'
|
|
430
|
+
? { slugsJson: flags.get('slugs') as string }
|
|
431
|
+
: {}),
|
|
432
|
+
allowUnsignedManifest: flags.get('allow-unsigned-manifest') === true,
|
|
433
|
+
})
|
|
434
|
+
} catch (error) {
|
|
435
|
+
err.write(`finalize failed: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
436
|
+
return 1
|
|
437
|
+
}
|
|
381
438
|
if (json) {
|
|
382
439
|
out.write(`${JSON.stringify({ finalized: finalized.finalized })}\n`)
|
|
383
440
|
}
|
|
384
441
|
return 0
|
|
385
442
|
}
|
|
386
|
-
// The remaining commands (from-payload-auth, verify, rollback) all
|
|
387
|
-
// load a Payload instance, so they need the config.
|
|
388
|
-
const configFlag = flags.get('config')
|
|
389
|
-
if (typeof configFlag !== 'string' || configFlag === '') {
|
|
390
|
-
err.write(`${command} requires --config <path to payload.config.ts>\n`)
|
|
391
|
-
return 3
|
|
392
|
-
}
|
|
393
443
|
if (command === 'adopt-prelink') {
|
|
394
444
|
const usersSlug = flags.get('users-slug')
|
|
395
445
|
const verifiedField = flags.get('verified-field')
|
|
@@ -403,6 +453,7 @@ async function main(
|
|
|
403
453
|
: {}),
|
|
404
454
|
...(typeof usersSlug === 'string' ? { usersSlug } : {}),
|
|
405
455
|
...(typeof verifiedField === 'string' ? { verifiedField } : {}),
|
|
456
|
+
allowUnsignedManifest: flags.get('allow-unsigned-manifest') === true,
|
|
406
457
|
})
|
|
407
458
|
if (json) {
|
|
408
459
|
out.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
@@ -435,6 +486,8 @@ async function main(
|
|
|
435
486
|
: {}),
|
|
436
487
|
json,
|
|
437
488
|
log,
|
|
489
|
+
allowUnsignedManifest: flags.get('allow-unsigned-manifest') === true,
|
|
490
|
+
allowUnsignedDeletes: flags.get('allow-unsigned-deletes') === true,
|
|
438
491
|
}
|
|
439
492
|
if (command === 'from-payload-auth') {
|
|
440
493
|
const outcome = await runFromPayloadAuth(importOptions)
|
|
@@ -472,7 +525,14 @@ async function main(
|
|
|
472
525
|
return result.ok ? 0 : 1
|
|
473
526
|
}
|
|
474
527
|
if (command === 'rollback') {
|
|
475
|
-
|
|
528
|
+
let undone: Awaited<ReturnType<typeof runRollback>>
|
|
529
|
+
try {
|
|
530
|
+
undone = await runRollback(importOptions)
|
|
531
|
+
} catch (error) {
|
|
532
|
+
// A refused or failed rollback is an error exit, never a quiet 0.
|
|
533
|
+
err.write(`rollback failed: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
534
|
+
return 1
|
|
535
|
+
}
|
|
476
536
|
if (json) {
|
|
477
537
|
out.write(`${JSON.stringify(undone)}\n`)
|
|
478
538
|
}
|
package/src/doctor/plugins.ts
CHANGED
|
@@ -181,10 +181,15 @@ export function pluginsFromCollectionSlugs(slugs: readonly string[]): string[] {
|
|
|
181
181
|
return [...ids].sort()
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
185
|
-
|
|
184
|
+
/**
|
|
185
|
+
* True when a source file actually imports Better Auth — any subpath
|
|
186
|
+
* depth (`better-auth/plugins/generic-oauth`, issue #110), static or
|
|
187
|
+
* dynamic `import()`, or `require()`.
|
|
188
|
+
*/
|
|
186
189
|
const betterAuthImport =
|
|
187
|
-
/(?:from\s*|require\(\s*)['"](?:better-auth(?:\/[\w
|
|
190
|
+
/(?:from\s*|require\(\s*|import\(\s*)['"](?:better-auth(?:\/[\w.-]+)*|@better-auth\/[\w.-]+(?:\/[\w.-]+)*)['"]/
|
|
191
|
+
|
|
192
|
+
/** Plugin call names a config source can name, mapped to plugin ids. */
|
|
188
193
|
|
|
189
194
|
const callNameToPlugin: Readonly<Record<string, string>> = {
|
|
190
195
|
magicLink: 'magic-link',
|
package/src/doctor/run-doctor.ts
CHANGED
|
@@ -124,6 +124,24 @@ export function betterAuthPluginIds(pluginOptions: Record<string, unknown>): str
|
|
|
124
124
|
return ids
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The options of the payload-auth-link plugin in a built (TARGET)
|
|
129
|
+
* config, or `null` when the plugin is not installed. Payload keeps each
|
|
130
|
+
* plugin's `slug` and `options` for cross-plugin discovery.
|
|
131
|
+
*/
|
|
132
|
+
function authLinkPluginOptions(config: unknown): Record<string, unknown> | null {
|
|
133
|
+
if (!isRecord(config) || !Array.isArray(config.plugins)) {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
for (const entry of config.plugins) {
|
|
137
|
+
if (typeof entry === 'function' && (entry as { slug?: unknown }).slug === 'payload-auth-link') {
|
|
138
|
+
const options: unknown = (entry as { options?: unknown }).options
|
|
139
|
+
return isRecord(options) ? options : {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
127
145
|
/** The users collection slug the payload-auth app configured. */
|
|
128
146
|
export function usersSlugOf(pluginOptions: Record<string, unknown>): string {
|
|
129
147
|
const users: unknown = pluginOptions.users
|
|
@@ -285,16 +303,25 @@ export async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {
|
|
|
285
303
|
let users: DoctorReport['users'] = null
|
|
286
304
|
if (configLoaded) {
|
|
287
305
|
const usersSlug = usersSlugOf(plugin?.pluginOptions ?? {})
|
|
306
|
+
const usersCollection = findUsersCollection(config, usersSlug)
|
|
307
|
+
const authLinkOptions = authLinkPluginOptions(config)
|
|
308
|
+
// An adopt-mode TARGET (issue #110): payload-auth-link is installed
|
|
309
|
+
// on the users collection and no engine-owned ba-users collection
|
|
310
|
+
// exists, so Better Auth creates its users in the host collection.
|
|
311
|
+
const adoptTarget = authLinkOptions !== null && !collectionSlugs(config).includes('ba-users')
|
|
312
|
+
const rolesField = authLinkOptions?.['rolesField']
|
|
288
313
|
const { finding, issues: userIssues } = analyzeUsersCollection(
|
|
289
|
-
|
|
290
|
-
'roles'
|
|
314
|
+
usersCollection,
|
|
315
|
+
typeof rolesField === 'string' && rolesField !== '' ? rolesField : 'roles',
|
|
316
|
+
{ adoptTarget }
|
|
291
317
|
)
|
|
292
318
|
users = finding
|
|
293
319
|
for (const issue of userIssues) {
|
|
294
320
|
if (
|
|
295
321
|
issue.code === 'injected-field-collision' ||
|
|
296
322
|
issue.code === 'roles-field-not-select' ||
|
|
297
|
-
issue.code === 'users-collection-missing'
|
|
323
|
+
issue.code === 'users-collection-missing' ||
|
|
324
|
+
issue.code === 'adopt-local-strategy-on'
|
|
298
325
|
) {
|
|
299
326
|
blockers.push(issue)
|
|
300
327
|
} else {
|
package/src/doctor/users.ts
CHANGED
|
@@ -70,11 +70,6 @@ export function findUsersCollection(
|
|
|
70
70
|
return null
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
74
|
-
* Payload's local email+password strategy for a collection. payload-auth
|
|
75
|
-
* leaves it on (its own Better Auth strategy rides alongside), which is
|
|
76
|
-
* exactly the surface 0.7.0 closes for provider-linked documents.
|
|
77
|
-
*/
|
|
78
73
|
/** Names of the custom auth strategies a collection declares. */
|
|
79
74
|
function strategiesOf(collection: Record<string, unknown>): string[] {
|
|
80
75
|
const auth: unknown = collection.auth
|
|
@@ -87,6 +82,11 @@ function strategiesOf(collection: Record<string, unknown>): string[] {
|
|
|
87
82
|
.filter((name): name is string => typeof name === 'string')
|
|
88
83
|
}
|
|
89
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Payload's local email+password strategy for a collection, as the
|
|
87
|
+
* BUILT config has it. payload-auth leaves it on unless the app set
|
|
88
|
+
* `disableDefaultPayloadAuth: true`; either way, this reads the result.
|
|
89
|
+
*/
|
|
90
90
|
export function localStrategyOn(collection: Record<string, unknown>): boolean {
|
|
91
91
|
const auth: unknown = collection.auth
|
|
92
92
|
if (auth === undefined || auth === null) {
|
|
@@ -109,7 +109,15 @@ const pluginStrategyName = 'auth-link'
|
|
|
109
109
|
|
|
110
110
|
export function analyzeUsersCollection(
|
|
111
111
|
collection: Record<string, unknown> | null,
|
|
112
|
-
rolesFieldName: string
|
|
112
|
+
rolesFieldName: string,
|
|
113
|
+
options: {
|
|
114
|
+
/**
|
|
115
|
+
* The config is an adopt-mode TARGET: payload-auth-link is installed
|
|
116
|
+
* and Better Auth serves its users from this collection (no ba-users
|
|
117
|
+
* collection). Enables the target-side adopt checks.
|
|
118
|
+
*/
|
|
119
|
+
readonly adoptTarget?: boolean | undefined
|
|
120
|
+
} = {}
|
|
113
121
|
): { finding: UsersFinding | null; issues: DoctorIssue[] } {
|
|
114
122
|
if (collection === null) {
|
|
115
123
|
return {
|
|
@@ -193,8 +201,18 @@ export function analyzeUsersCollection(
|
|
|
193
201
|
const local = localStrategyOn(collection)
|
|
194
202
|
if (local) {
|
|
195
203
|
notes.push(
|
|
196
|
-
'Payload local strategy is ON
|
|
204
|
+
'Payload local strategy is ON for this collection: since 0.7.0, provider-linked documents refuse local sign-in and forgot/reset by default — break-glass operator accounts keep theirs; allowLocalCredentialsForLinkedDocs: true reopens it only for a migration window'
|
|
197
205
|
)
|
|
206
|
+
if (options.adoptTarget === true) {
|
|
207
|
+
// Adopt mode (issue #110): Better Auth creates its users IN this
|
|
208
|
+
// collection, and Payload's local strategy demands a password on
|
|
209
|
+
// every create — sign-up and anonymous sign-in fail for every
|
|
210
|
+
// new user.
|
|
211
|
+
issues.push({
|
|
212
|
+
code: 'adopt-local-strategy-on',
|
|
213
|
+
detail: `adopt mode serves Better Auth's users from the collection '${typeof collection.slug === 'string' ? collection.slug : 'users'}', but Payload's local strategy is ON there: Payload requires a password on every create, so every NEW user fails (email sign-up 422, anonymous sign-in 500 "field is invalid: password"). Set auth: { disableLocalStrategy: true } on the collection (payload-auth's disableDefaultPayloadAuth: true did the same), or use the importer (ba-* collections) instead of adopt`,
|
|
214
|
+
})
|
|
215
|
+
}
|
|
198
216
|
} else {
|
|
199
217
|
const strategies = Array.isArray(collection.auth) ? [] : strategiesOf(collection)
|
|
200
218
|
notes.push(
|
package/src/exporters.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Client } from 'pg'
|
|
2
|
+
import { presentTables, resolveSourceTables, type ResolvedSourceTables } from './importer/types.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Off-ramp exporters (issue #58): every payload-auth-link collection's
|
|
@@ -14,12 +15,42 @@ export type ExportFormat = 'jsonl'
|
|
|
14
15
|
export type ExportOptions = {
|
|
15
16
|
readonly connectionString: string
|
|
16
17
|
readonly format: ExportFormat
|
|
17
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Tables to export, exactly (`--tables`). Without it, the export
|
|
20
|
+
* covers EVERY auth table (see {@link resolveAuthTables}).
|
|
21
|
+
*/
|
|
18
22
|
readonly tables?: readonly string[] | undefined
|
|
23
|
+
/**
|
|
24
|
+
* The source layout (`--source-map`, issue #110): model → table, or
|
|
25
|
+
* `null` for "not installed". Defaults to payload-auth's layout. A
|
|
26
|
+
* mapped table that does not exist lands in `missing`.
|
|
27
|
+
*/
|
|
28
|
+
readonly sourceTables?: ResolvedSourceTables | undefined
|
|
19
29
|
readonly log?: (message: string) => void
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Better Auth plugin tables payload-auth creates beyond the importer's
|
|
34
|
+
* models — exported whenever present (they are auth data the off-ramp
|
|
35
|
+
* must not lose).
|
|
36
|
+
*/
|
|
37
|
+
export const optionalAuthTables: readonly string[] = [
|
|
38
|
+
'admin_invitations',
|
|
39
|
+
'two_factors',
|
|
40
|
+
'passkeys',
|
|
41
|
+
'sso_providers',
|
|
42
|
+
'organization_roles',
|
|
43
|
+
'teams',
|
|
44
|
+
'team_members',
|
|
45
|
+
'device_code',
|
|
46
|
+
'subscriptions',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The legacy default auth-table list (payload-auth's default layout plus
|
|
51
|
+
* the plugin's users child tables). Kept for callers that name it;
|
|
52
|
+
* {@link resolveAuthTables} is what the export uses.
|
|
53
|
+
*/
|
|
23
54
|
export const exportTables: readonly string[] = [
|
|
24
55
|
'users',
|
|
25
56
|
'users_role',
|
|
@@ -40,9 +71,9 @@ export type ExportResult = {
|
|
|
40
71
|
readonly jsonl: string
|
|
41
72
|
readonly totalRows: number
|
|
42
73
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
74
|
+
* Required tables that do not exist on the source (issues #68, #110):
|
|
75
|
+
* a `--tables` name, or a mapped model's table. The CLI exits 1 on
|
|
76
|
+
* any; optional tables (children, ba_*, oauth_*) never land here.
|
|
46
77
|
*/
|
|
47
78
|
readonly missing: readonly string[]
|
|
48
79
|
}
|
|
@@ -81,32 +112,102 @@ export function parseExportTables(value: string): readonly string[] {
|
|
|
81
112
|
return names
|
|
82
113
|
}
|
|
83
114
|
|
|
115
|
+
/**
|
|
116
|
+
* "Every auth table" (issue #110), resolved against what the database
|
|
117
|
+
* actually holds, in dependency order:
|
|
118
|
+
*
|
|
119
|
+
* 1. each mapped model's table (`--source-map` over payload-auth's
|
|
120
|
+
* layout; `null` = not installed) — REQUIRED: a missing one lands in
|
|
121
|
+
* `missing`;
|
|
122
|
+
* 2. every child table of those (`<table>_*`: `users_role`,
|
|
123
|
+
* `users_identities`, `users_sessions`, `organizations_tags`, `_rels`
|
|
124
|
+
* tables, …) and payload-auth's other Better Auth plugin tables
|
|
125
|
+
* ({@link optionalAuthTables}) — when present;
|
|
126
|
+
* 3. every `ba_*` (payload-auth-link engine) and `oauth_*` (OAuth
|
|
127
|
+
* provider) table — when present.
|
|
128
|
+
*/
|
|
129
|
+
export function resolveAuthTables(
|
|
130
|
+
existing: ReadonlySet<string>,
|
|
131
|
+
sourceTables: ResolvedSourceTables = resolveSourceTables(undefined)
|
|
132
|
+
): { readonly tables: readonly string[]; readonly missing: readonly string[] } {
|
|
133
|
+
const tables: string[] = []
|
|
134
|
+
const missing: string[] = []
|
|
135
|
+
const add = (table: string): void => {
|
|
136
|
+
if (!tables.includes(table)) {
|
|
137
|
+
tables.push(table)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const mapped = presentTables(sourceTables)
|
|
141
|
+
for (const table of mapped) {
|
|
142
|
+
if (existing.has(table)) {
|
|
143
|
+
add(table)
|
|
144
|
+
} else {
|
|
145
|
+
missing.push(table)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const sorted = [...existing].sort()
|
|
149
|
+
for (const table of [...mapped, ...optionalAuthTables]) {
|
|
150
|
+
if (!mapped.includes(table) && existing.has(table)) {
|
|
151
|
+
add(table)
|
|
152
|
+
}
|
|
153
|
+
for (const child of sorted) {
|
|
154
|
+
if (child.startsWith(`${table}_`)) {
|
|
155
|
+
add(child)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const table of sorted) {
|
|
160
|
+
if (table.startsWith('ba_') || table.startsWith('oauth_')) {
|
|
161
|
+
add(table)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { tables, missing }
|
|
165
|
+
}
|
|
166
|
+
|
|
84
167
|
/** Export the auth tables to lossless JSONL (issue #58). */
|
|
85
168
|
export async function exportToJsonl(options: ExportOptions): Promise<ExportResult> {
|
|
86
|
-
const tables = options.tables ?? exportTables
|
|
87
169
|
const client = new Client({ connectionString: options.connectionString })
|
|
88
170
|
await client.connect()
|
|
89
171
|
try {
|
|
172
|
+
const listed = await client.query(
|
|
173
|
+
`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`
|
|
174
|
+
)
|
|
175
|
+
const existing = new Set(
|
|
176
|
+
listed.rows.map((row) => String((row as { table_name: unknown }).table_name))
|
|
177
|
+
)
|
|
90
178
|
const lines: string[] = []
|
|
91
179
|
const exported: { name: string; rows: number }[] = []
|
|
92
180
|
const missing: string[] = []
|
|
181
|
+
let tables: readonly string[]
|
|
182
|
+
if (options.tables === undefined) {
|
|
183
|
+
const resolved = resolveAuthTables(existing, options.sourceTables)
|
|
184
|
+
tables = resolved.tables
|
|
185
|
+
for (const table of resolved.missing) {
|
|
186
|
+
missing.push(table)
|
|
187
|
+
options.log?.(
|
|
188
|
+
`mapped table '${table}' does not exist on the source — not exported (map its model null with --source-map if it is not installed)`
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
tables = options.tables
|
|
193
|
+
}
|
|
93
194
|
let totalRows = 0
|
|
94
195
|
for (const table of tables) {
|
|
95
|
-
|
|
96
|
-
`SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1`,
|
|
97
|
-
[table]
|
|
98
|
-
)
|
|
99
|
-
if (exists.rows.length === 0) {
|
|
196
|
+
if (!existing.has(table)) {
|
|
100
197
|
// Nothing is silently omitted (issue #68): every absent table
|
|
101
198
|
// is logged and lands in `missing` for the caller's policy.
|
|
102
199
|
missing.push(table)
|
|
103
200
|
options.log?.(`table '${table}' does not exist on the source — not exported`)
|
|
104
201
|
continue
|
|
105
202
|
}
|
|
203
|
+
const columns = await client.query(
|
|
204
|
+
`SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 AND column_name = 'id'`,
|
|
205
|
+
[table]
|
|
206
|
+
)
|
|
106
207
|
// Export EVERY column raw (JSON types preserved by ::jsonb cast
|
|
107
208
|
// per row via row_to_json) — lossless by construction.
|
|
108
209
|
const result = await client.query(
|
|
109
|
-
`SELECT row_to_json(t)::jsonb AS row FROM ${table} t ORDER BY t.id`
|
|
210
|
+
`SELECT row_to_json(t)::jsonb AS row FROM "${table}" t${columns.rows.length > 0 ? ' ORDER BY t.id' : ''}`
|
|
110
211
|
)
|
|
111
212
|
exported.push({ name: table, rows: result.rows.length })
|
|
112
213
|
totalRows += result.rows.length
|