@payload-auth-link/migrate 0.4.3 → 0.6.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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +109 -19
  5. package/dist/cli.js.map +1 -1
  6. package/dist/doctor/database.d.ts.map +1 -1
  7. package/dist/doctor/database.js +31 -4
  8. package/dist/doctor/database.js.map +1 -1
  9. package/dist/doctor/run-doctor.d.ts.map +1 -1
  10. package/dist/doctor/run-doctor.js +67 -0
  11. package/dist/doctor/run-doctor.js.map +1 -1
  12. package/dist/doctor/types.d.ts +12 -0
  13. package/dist/doctor/types.d.ts.map +1 -1
  14. package/dist/doctor/users.d.ts.map +1 -1
  15. package/dist/doctor/users.js +20 -4
  16. package/dist/doctor/users.js.map +1 -1
  17. package/dist/importer/apply.d.ts +25 -5
  18. package/dist/importer/apply.d.ts.map +1 -1
  19. package/dist/importer/apply.js +164 -180
  20. package/dist/importer/apply.js.map +1 -1
  21. package/dist/importer/host-writes.d.ts +47 -0
  22. package/dist/importer/host-writes.d.ts.map +1 -0
  23. package/dist/importer/host-writes.js +198 -0
  24. package/dist/importer/host-writes.js.map +1 -0
  25. package/dist/importer/lifecycle.d.ts +26 -0
  26. package/dist/importer/lifecycle.d.ts.map +1 -1
  27. package/dist/importer/lifecycle.js +107 -22
  28. package/dist/importer/lifecycle.js.map +1 -1
  29. package/dist/importer/manifest-mac.d.ts +15 -0
  30. package/dist/importer/manifest-mac.d.ts.map +1 -0
  31. package/dist/importer/manifest-mac.js +73 -0
  32. package/dist/importer/manifest-mac.js.map +1 -0
  33. package/dist/importer/prelink.d.ts +57 -0
  34. package/dist/importer/prelink.d.ts.map +1 -0
  35. package/dist/importer/prelink.js +159 -0
  36. package/dist/importer/prelink.js.map +1 -0
  37. package/dist/importer/run-import.d.ts +15 -0
  38. package/dist/importer/run-import.d.ts.map +1 -1
  39. package/dist/importer/run-import.js +155 -14
  40. package/dist/importer/run-import.js.map +1 -1
  41. package/dist/importer/source.d.ts +35 -7
  42. package/dist/importer/source.d.ts.map +1 -1
  43. package/dist/importer/source.js +37 -6
  44. package/dist/importer/source.js.map +1 -1
  45. package/dist/importer/types.d.ts +34 -4
  46. package/dist/importer/types.d.ts.map +1 -1
  47. package/dist/importer/types.js.map +1 -1
  48. package/dist/index.d.ts +5 -4
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +3 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/private-file.d.ts +8 -0
  53. package/dist/private-file.d.ts.map +1 -0
  54. package/dist/private-file.js +19 -0
  55. package/dist/private-file.js.map +1 -0
  56. package/package.json +10 -10
  57. package/src/cli.ts +117 -9
  58. package/src/doctor/database.ts +36 -4
  59. package/src/doctor/run-doctor.ts +69 -0
  60. package/src/doctor/types.ts +12 -0
  61. package/src/doctor/users.ts +23 -4
  62. package/src/importer/apply.ts +198 -182
  63. package/src/importer/host-writes.ts +231 -0
  64. package/src/importer/lifecycle.ts +131 -24
  65. package/src/importer/manifest-mac.ts +90 -0
  66. package/src/importer/prelink.ts +237 -0
  67. package/src/importer/run-import.ts +207 -29
  68. package/src/importer/source.ts +72 -14
  69. package/src/importer/types.ts +35 -4
  70. package/src/index.ts +13 -1
  71. package/src/private-file.ts +18 -0
@@ -83,6 +83,14 @@ export function localStrategyOn(collection) {
83
83
  }
84
84
  return isRecord(auth) ? auth.disableLocalStrategy !== true : false;
85
85
  }
86
+ /**
87
+ * The payload-auth-link plugin's own auth strategy name
88
+ * (`packages/plugin/src/strategy.ts`). A users collection carrying it
89
+ * has ALREADY been through `authLink` — its identities/linkable/…
90
+ * fields are the plugin's own injections, not host collisions
91
+ * (issue #88: the doctor run against the TARGET config).
92
+ */
93
+ const pluginStrategyName = 'auth-link';
86
94
  export function analyzeUsersCollection(collection, rolesFieldName) {
87
95
  if (collection === null) {
88
96
  return {
@@ -95,10 +103,18 @@ export function analyzeUsersCollection(collection, rolesFieldName) {
95
103
  const fields = fieldList(collection);
96
104
  const issues = [];
97
105
  const notes = [];
98
- const collisions = fields
99
- .map(fieldName)
100
- .filter((name) => name !== null)
101
- .filter((name) => injectedFieldNames.has(name));
106
+ // On the target config the plugin already injected these fields:
107
+ // they are its own, never a collision (issue #88).
108
+ const pluginInstalled = strategiesOf(collection).includes(pluginStrategyName);
109
+ const collisions = pluginInstalled
110
+ ? []
111
+ : fields
112
+ .map(fieldName)
113
+ .filter((name) => name !== null)
114
+ .filter((name) => injectedFieldNames.has(name));
115
+ if (pluginInstalled) {
116
+ notes.push('payload-auth-link is already installed on this collection (its auth-link strategy is present): the injected fields are the plugin’s own');
117
+ }
102
118
  let roleField = null;
103
119
  const roleCandidates = fields.filter((field) => {
104
120
  const name = fieldName(field);
@@ -1 +1 @@
1
- {"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/doctor/users.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/doctor/users.ts"],"names":[],"mappings":"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"}
@@ -1,21 +1,32 @@
1
1
  import type { Payload } from 'payload';
2
- import { type ImportManifest, type ImportResult, type PlanRow } from './types.js';
2
+ import { type SourceUser } from './source.js';
3
+ import { type ImportManifest, type ImportResult, type PlanRow, type Unmappable } from './types.js';
3
4
  /**
4
5
  * Apply side (issue #50): copy payload-auth rows into the
5
6
  * payload-auth-link collections through the Payload Local API.
6
7
  *
7
8
  * - Password hashes, OAuth tokens, API-key hashes, and jwks carry over
8
9
  * VERBATIM (payload-auth used Better Auth's own hashing).
9
- * - `linkable` + `linkableEmail` are granted from VERIFIED emails only;
10
- * unverified users are reported by the plan, never linked.
10
+ * - Row timestamps (`createdAt`/`updatedAt`) carry from the source rows
11
+ * for every model whose source table has them (issue #88).
12
+ * - Better Auth plugin user state carries (issue #88): admin-plugin
13
+ * `banned`/`banReason`/`banExpires`/`role` and the anonymous plugin's
14
+ * `isAnonymous`, whenever the source has the column AND the target
15
+ * `ba-users` collection declares the field. A banned or anonymous
16
+ * source user whose state the target cannot hold is UNMAPPABLE — the
17
+ * run aborts before writing; a banned user never imports unbanned.
18
+ * - `linkable` + `linkableEmail` are granted from VERIFIED emails only
19
+ * (grant-only: an existing proof is never revoked); unverified users
20
+ * are reported by the plan, never linked.
11
21
  * - Sessions default to `carry` so nobody is logged out: the carried
12
22
  * rows keep their tokens, and with the same `advanced.cookiePrefix`
13
23
  * the existing cookies keep working.
14
24
  * - Idempotent and resumable: every target row carries a deterministic
15
25
  * `baId` derived from the source id (`acct-<source id>`), so a
16
26
  * re-run after a crash finds its own earlier rows and skips them.
17
- * - Every created row and every field write lands in the manifest, so
18
- * `rollback` is exact and the source tables stay untouched.
27
+ * - Every created row and every host field write lands in the
28
+ * manifest — the FIRST captured prior value per field wins, sealed —
29
+ * so `rollback` is exact and the source tables stay untouched.
19
30
  */
20
31
  export type ApplyOptions = {
21
32
  readonly payload: Payload;
@@ -37,7 +48,16 @@ export type ApplyOptions = {
37
48
  /** Injectable clock for deterministic `linkedAt` stamps. */
38
49
  readonly now?: () => Date;
39
50
  readonly log?: (message: string) => void;
51
+ /** Resume from a pre-0.6.0 manifest that has no MAC (`--allow-unsigned-manifest`). */
52
+ readonly allowUnsignedManifest?: boolean | undefined;
40
53
  };
54
+ /**
55
+ * Plugin user state the target must be able to hold (issue #88),
56
+ * checked BEFORE any write: a banned user whose target has no `banned`
57
+ * field would import unbanned; an anonymous user without `isAnonymous`
58
+ * would import as a full account.
59
+ */
60
+ export declare function pluginStateUnmappable(users: readonly SourceUser[], targetFields: ReadonlySet<string>, targetSlug: string): Unmappable[];
41
61
  export declare function applyImport(options: ApplyOptions): Promise<ImportResult>;
42
62
  /** Collapse per-row plan entries into one line per model (dry-run print). */
43
63
  export declare function summarizePlanRows(rows: readonly PlanRow[]): PlanRow[];
@@ -1 +1 @@
1
- {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/importer/apply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAQtC,OAAO,EAOL,KAAK,cAAc,EAEnB,KAAK,YAAY,EACjB,KAAK,OAAO,EACb,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAA;IACvC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IACxC,kEAAkE;IAClE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACvD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAA;IAC7D;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,GAAG,SAAS,CAAA;IAC3E,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC,CAAA;AA0HD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAikB9E;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,CAerE"}
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/importer/apply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAGtC,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,aAAa,CAAA;AACpB,OAAO,EAOL,KAAK,cAAc,EAEnB,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,KAAK,UAAU,EAChB,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAA;IACvC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IACxC,kEAAkE;IAClE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACvD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAA;IAC7D;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,GAAG,SAAS,CAAA;IAC3E,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,sFAAsF;IACtF,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CACrD,CAAA;AAsDD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,UAAU,EAAE,EAC5B,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,EACjC,UAAU,EAAE,MAAM,GACjB,UAAU,EAAE,CAmBd;AAmGD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAsgB9E;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,CAerE"}
@@ -1,3 +1,5 @@
1
+ import { capturedKeys, captureBefore } from './host-writes.js';
2
+ import { authenticateManifest, signManifest } from './manifest-mac.js';
1
3
  import { readFlatRows, openSource, readSourceUsers, } from './source.js';
2
4
  import { defaultTargetSlugs, resolveSourceTables, } from './types.js';
3
5
  const slugsOf = (overrides) => ({
@@ -14,14 +16,80 @@ function numberOrNull(value) {
14
16
  const numeric = typeof value === 'number' ? value : Number(value);
15
17
  return Number.isFinite(numeric) ? numeric : undefined;
16
18
  }
19
+ /** The source row's timestamps, when its layout has them (issue #88). */
20
+ function stamps(row) {
21
+ return {
22
+ ...(row.created_at instanceof Date ? { createdAt: row.created_at.toISOString() } : {}),
23
+ ...(row.updated_at instanceof Date ? { updatedAt: row.updated_at.toISOString() } : {}),
24
+ };
25
+ }
26
+ const timestampColumns = ['created_at', 'updated_at'];
17
27
  /**
18
28
  * Rows for one mapped model. A `null` mapping records the model as
19
29
  * not installed (issue #68) — no read, no rows; anything else goes
20
30
  * through the strict `readFlatRows` (a missing mapped table throws;
21
- * the probe aborts the run first).
31
+ * the probe aborts the run first). Row timestamps are read when the
32
+ * layout has them.
22
33
  */
23
34
  async function readMapped(client, table, columns) {
24
- return table === null ? [] : readFlatRows(client, table, columns);
35
+ return table === null ? [] : readFlatRows(client, table, columns, timestampColumns);
36
+ }
37
+ /** Field names a target collection declares (top level, flattened). */
38
+ function targetFieldNames(payload, slug) {
39
+ const collection = payload.collections[slug];
40
+ const names = new Set();
41
+ for (const field of collection?.config.flattenedFields ?? []) {
42
+ names.add(field.name);
43
+ }
44
+ return names;
45
+ }
46
+ /**
47
+ * Plugin user state the target must be able to hold (issue #88),
48
+ * checked BEFORE any write: a banned user whose target has no `banned`
49
+ * field would import unbanned; an anonymous user without `isAnonymous`
50
+ * would import as a full account.
51
+ */
52
+ export function pluginStateUnmappable(users, targetFields, targetSlug) {
53
+ const found = [];
54
+ for (const user of users) {
55
+ if (user.banned === true && !targetFields.has('banned')) {
56
+ found.push({
57
+ model: 'user',
58
+ key: user.id,
59
+ reason: `user #${user.id} is banned on the source, but '${targetSlug}' declares no 'banned' field — add Better Auth's admin() plugin to the engine (autoDeclare derives banned/banReason/banExpires) so the ban carries`,
60
+ });
61
+ }
62
+ if (user.isAnonymous === true && !targetFields.has('isAnonymous')) {
63
+ found.push({
64
+ model: 'user',
65
+ key: user.id,
66
+ reason: `user #${user.id} is anonymous on the source, but '${targetSlug}' declares no 'isAnonymous' field — add Better Auth's anonymous() plugin to the engine, or remove anonymous users before importing`,
67
+ });
68
+ }
69
+ }
70
+ return found;
71
+ }
72
+ /** Plugin user state carried onto the ba-users row (issue #88). */
73
+ function pluginUserState(user, targetFields) {
74
+ const state = {};
75
+ if (user.banned !== null && targetFields.has('banned')) {
76
+ state.banned = user.banned;
77
+ }
78
+ if (user.banReason !== null && targetFields.has('banReason')) {
79
+ state.banReason = user.banReason;
80
+ }
81
+ if (user.banExpires !== null && targetFields.has('banExpires')) {
82
+ state.banExpires = user.banExpires.toISOString();
83
+ }
84
+ if (user.isAnonymous !== null && targetFields.has('isAnonymous')) {
85
+ state.isAnonymous = user.isAnonymous;
86
+ }
87
+ // Better Auth's admin plugin stores roles as one comma-separated
88
+ // string; payload-auth kept them in the users_role child table.
89
+ if (user.roles.length > 0 && targetFields.has('role')) {
90
+ state.role = user.roles.join(',');
91
+ }
92
+ return state;
25
93
  }
26
94
  /** Find an existing target row by `baId` — the resumable dedupe key. */
27
95
  async function findByBaId(payload, collection, baId) {
@@ -53,19 +121,6 @@ async function createDeduped(payload, collection, baId, data, created, sourceKey
53
121
  }
54
122
  return true;
55
123
  }
56
- /** Capture a host field's current value for the rollback manifest. */
57
- async function captureBefore(payload, hostId, field) {
58
- const doc = await payload.findByID({
59
- collection: 'users',
60
- id: hostId,
61
- depth: 0,
62
- overrideAccess: true,
63
- });
64
- if (typeof doc !== 'object' || doc === null || !(field in doc)) {
65
- return null;
66
- }
67
- return JSON.stringify(doc[field] ?? null);
68
- }
69
124
  /**
70
125
  * The identity rows pre-populated on the host user (issue #69): ONE
71
126
  * `better-auth` identity per imported user, `providerUserId` set to
@@ -98,9 +153,21 @@ export async function applyImport(options) {
98
153
  const { payload, sourceConnectionString, apply, sessions, log = () => { }, now = () => new Date(), } = options;
99
154
  const slugs = slugsOf(options.slugs);
100
155
  const sourceTables = resolveSourceTables(options.sourceTables);
156
+ if (options.manifest !== null) {
157
+ // Resuming merges the old entries and re-signs them: never launder
158
+ // an edited manifest into a signed one.
159
+ authenticateManifest(payload.secret, options.manifest, {
160
+ allowUnsigned: options.allowUnsignedManifest,
161
+ });
162
+ }
101
163
  const created = [...(options.manifest?.created ?? [])];
102
164
  const fieldWrites = [...(options.manifest?.fieldWrites ?? [])];
103
- const totals = { ...(options.manifest?.totals ?? {}) };
165
+ // First capture wins (issue #88): fields captured by an earlier pass
166
+ // keep their pre-import value.
167
+ const captured = capturedKeys(fieldWrites);
168
+ // Per-PASS totals (issue #88): a re-apply reports what this pass
169
+ // planned, never the sum of every pass.
170
+ const totals = {};
104
171
  const planRows = [];
105
172
  const unmappable = [];
106
173
  let createdCount = 0;
@@ -112,6 +179,20 @@ export async function applyImport(options) {
112
179
  const client = await openSource(sourceConnectionString);
113
180
  try {
114
181
  const users = await readSourceUsers(client, sourceTables);
182
+ const baUserFields = targetFieldNames(payload, slugs.user);
183
+ // --- 0. target-capability gate (issue #88) ---------------------------
184
+ // Runs before ANY write: plugin state the target cannot hold aborts
185
+ // the run instead of importing a banned user unbanned.
186
+ const blocked = pluginStateUnmappable(users, baUserFields, slugs.user);
187
+ if (blocked.length > 0) {
188
+ log(`ABORT: ${blocked.length} user(s) carry state the target cannot hold; nothing was written`);
189
+ return {
190
+ created: 0,
191
+ skipped: 0,
192
+ fieldWrites: 0,
193
+ plan: { rows: [], unmappable: blocked, sessions, warnings: [] },
194
+ };
195
+ }
115
196
  const seenUsers = new Set();
116
197
  // --- 1. ba-users + host field writes ---------------------------------
117
198
  for (const user of users) {
@@ -128,6 +209,7 @@ export async function applyImport(options) {
128
209
  name: user.name ?? '',
129
210
  emailVerified: user.emailVerified,
130
211
  ...(user.image === null ? {} : { image: user.image }),
212
+ ...pluginUserState(user, baUserFields),
131
213
  createdAt: user.createdAt.toISOString(),
132
214
  updatedAt: user.updatedAt.toISOString(),
133
215
  }, created, `user:${baId}`)
@@ -141,30 +223,32 @@ export async function applyImport(options) {
141
223
  }
142
224
  // Host-side writes on the EXISTING Payload user (the source
143
225
  // layout's user id IS the host id): identities, roles, and the
144
- // verified-email linking proof. Captured `before` values make
145
- // rollback exact.
226
+ // verified-email linking proof. The prior values (and the
227
+ // doc's updatedAt) are captured first — once — so rollback
228
+ // restores the pre-import document exactly.
146
229
  const hostId = Number(baId);
147
230
  if (Number.isSafeInteger(hostId)) {
231
+ const { present, newlyCaptured } = await captureBefore({
232
+ payload,
233
+ collection: 'users',
234
+ id: hostId,
235
+ fields: ['identities', 'roles', 'linkable', 'linkableEmail'],
236
+ captured,
237
+ into: fieldWrites,
238
+ });
239
+ fieldWriteCount += newlyCaptured;
148
240
  const writes = {
149
241
  identities: identityRows(user, now),
150
- linkable: user.emailVerified,
151
242
  };
152
- const fields = ['identities', 'roles', 'linkable', 'linkableEmail'];
153
- for (const field of fields) {
154
- fieldWrites.push({
155
- collection: 'users',
156
- id: hostId,
157
- field,
158
- before: await captureBefore(payload, hostId, field),
159
- });
160
- }
161
- fieldWriteCount += fields.length;
162
- if (user.roles.length > 0) {
243
+ if (user.roles.length > 0 && present.has('roles')) {
163
244
  // Stored role values pass through unchanged (issue #56: the
164
245
  // storage edge owns translation; the map is the app's).
165
246
  writes.roles = user.roles;
166
247
  }
167
248
  if (user.emailVerified) {
249
+ // Grant-only: the proof is bound to the address (both
250
+ // fields), and an existing proof is never revoked here.
251
+ writes.linkable = true;
168
252
  writes.linkableEmail = user.email;
169
253
  }
170
254
  await payload.update({
@@ -184,6 +268,24 @@ export async function applyImport(options) {
184
268
  unmappable: [],
185
269
  });
186
270
  }
271
+ const tally = (model, sourceTable, target, isNew) => {
272
+ if (apply) {
273
+ if (isNew) {
274
+ createdCount += 1;
275
+ }
276
+ else {
277
+ skippedCount += 1;
278
+ }
279
+ }
280
+ record({
281
+ model,
282
+ sourceTable,
283
+ targetCollection: target,
284
+ rows: 1,
285
+ skipped: isNew ? 0 : apply ? 1 : 0,
286
+ unmappable: [],
287
+ });
288
+ };
187
289
  // --- 2. accounts (hashes verbatim) ------------------------------------
188
290
  const accountsTable = sourceTables['accounts'];
189
291
  for (const user of users) {
@@ -206,24 +308,15 @@ export async function applyImport(options) {
206
308
  ...(account.refreshTokenExpiresAt === null
207
309
  ? {}
208
310
  : { refreshTokenExpiresAt: account.refreshTokenExpiresAt.toISOString() }),
311
+ ...(account.createdAt === null
312
+ ? {}
313
+ : { createdAt: account.createdAt.toISOString() }),
314
+ ...(account.updatedAt === null
315
+ ? {}
316
+ : { updatedAt: account.updatedAt.toISOString() }),
209
317
  }, created, `account:${account.id}`)
210
318
  : false;
211
- if (apply) {
212
- if (isNew) {
213
- createdCount += 1;
214
- }
215
- else {
216
- skippedCount += 1;
217
- }
218
- }
219
- record({
220
- model: 'account',
221
- sourceTable: accountsTable ?? 'accounts',
222
- targetCollection: slugs.account,
223
- rows: 1,
224
- skipped: isNew ? 0 : apply ? 1 : 0,
225
- unmappable: [],
226
- });
319
+ tally('account', accountsTable ?? 'accounts', slugs.account, isNew);
227
320
  }
228
321
  }
229
322
  // --- 3. sessions (carry keeps every token) -----------------------------
@@ -236,8 +329,6 @@ export async function applyImport(options) {
236
329
  'expires_at',
237
330
  'ip_address',
238
331
  'user_agent',
239
- 'created_at',
240
- 'updated_at',
241
332
  ]);
242
333
  for (const row of rows) {
243
334
  totals.session = (totals.session ?? 0) + 1;
@@ -255,30 +346,10 @@ export async function applyImport(options) {
255
346
  ...(textOrNull(row.user_agent) === undefined
256
347
  ? {}
257
348
  : { userAgent: textOrNull(row.user_agent) }),
258
- ...(row.created_at instanceof Date
259
- ? { createdAt: row.created_at.toISOString() }
260
- : {}),
261
- ...(row.updated_at instanceof Date
262
- ? { updatedAt: row.updated_at.toISOString() }
263
- : {}),
349
+ ...stamps(row),
264
350
  }, created, `session:${String(row.id)}`)
265
351
  : false;
266
- if (apply) {
267
- if (isNew) {
268
- createdCount += 1;
269
- }
270
- else {
271
- skippedCount += 1;
272
- }
273
- }
274
- record({
275
- model: 'session',
276
- sourceTable: sessionsTable ?? 'sessions',
277
- targetCollection: slugs.session,
278
- rows: 1,
279
- skipped: isNew ? 0 : apply ? 1 : 0,
280
- unmappable: [],
281
- });
352
+ tally('session', sessionsTable ?? 'sessions', slugs.session, isNew);
282
353
  }
283
354
  }
284
355
  // --- 4. verifications ---------------------------------------------------
@@ -297,24 +368,10 @@ export async function applyImport(options) {
297
368
  identifier: String(row.identifier),
298
369
  value: String(row.value),
299
370
  expiresAt: row.expires_at instanceof Date ? row.expires_at.toISOString() : now().toISOString(),
371
+ ...stamps(row),
300
372
  }, created, `verification:${String(row.id)}`)
301
373
  : false;
302
- if (apply) {
303
- if (isNew) {
304
- createdCount += 1;
305
- }
306
- else {
307
- skippedCount += 1;
308
- }
309
- }
310
- record({
311
- model: 'verification',
312
- sourceTable: verificationsTable ?? 'verifications',
313
- targetCollection: slugs.verification,
314
- rows: 1,
315
- skipped: isNew ? 0 : apply ? 1 : 0,
316
- unmappable: [],
317
- });
374
+ tally('verification', verificationsTable ?? 'verifications', slugs.verification, isNew);
318
375
  }
319
376
  // --- 5. api keys (hash, prefix, metadata, rate-limit state) --------------
320
377
  const apiKeysTable = sourceTables['api_keys'];
@@ -325,8 +382,8 @@ export async function applyImport(options) {
325
382
  'start',
326
383
  'prefix',
327
384
  'key',
328
- // payload-auth stores the satellite's userId as referenceId
329
- // (the #49 field map's source side).
385
+ // payload-auth stores the satellite's owner as reference_id — the
386
+ // same name @better-auth/api-key 1.7 uses (`referenceId`).
330
387
  'reference_id',
331
388
  'refill_interval',
332
389
  'refill_amount',
@@ -352,9 +409,9 @@ export async function applyImport(options) {
352
409
  ...(textOrNull(row.start) === undefined ? {} : { start: textOrNull(row.start) }),
353
410
  ...(textOrNull(row.prefix) === undefined ? {} : { prefix: textOrNull(row.prefix) }),
354
411
  key: String(row.key),
355
- // The satellite's user reference arrives as referenceId in
356
- // payload-auth layouts; BA 1.7's own schema names it the
357
- // same, so the target field is offered both names.
412
+ // @better-auth/api-key 1.7 names the owner `referenceId`;
413
+ // `userId` is offered for 1.6-era target schemas (Payload
414
+ // drops it where the collection has no such field).
358
415
  userId: String(row.reference_id),
359
416
  referenceId: String(row.reference_id),
360
417
  ...(numberOrNull(row.refill_interval) === undefined
@@ -394,24 +451,10 @@ export async function applyImport(options) {
394
451
  ...(textOrNull(row.metadata) === undefined
395
452
  ? {}
396
453
  : { metadata: textOrNull(row.metadata) }),
454
+ ...stamps(row),
397
455
  }, created, `apikey:${String(row.id)}`)
398
456
  : false;
399
- if (apply) {
400
- if (isNew) {
401
- createdCount += 1;
402
- }
403
- else {
404
- skippedCount += 1;
405
- }
406
- }
407
- record({
408
- model: 'apikey',
409
- sourceTable: apiKeysTable ?? 'api_keys',
410
- targetCollection: slugs.apikey,
411
- rows: 1,
412
- skipped: isNew ? 0 : apply ? 1 : 0,
413
- unmappable: [],
414
- });
457
+ tally('apikey', apiKeysTable ?? 'api_keys', slugs.apikey, isNew);
415
458
  }
416
459
  // --- 6. jwks --------------------------------------------------------------
417
460
  const jwksTable = sourceTables['jwks'];
@@ -423,24 +466,10 @@ export async function applyImport(options) {
423
466
  ? await createDeduped(payload, slugs.jwks, baId, {
424
467
  publicKey: String(row.public_key),
425
468
  privateKey: String(row.private_key),
469
+ ...stamps(row),
426
470
  }, created, `jwks:${String(row.id)}`)
427
471
  : false;
428
- if (apply) {
429
- if (isNew) {
430
- createdCount += 1;
431
- }
432
- else {
433
- skippedCount += 1;
434
- }
435
- }
436
- record({
437
- model: 'jwks',
438
- sourceTable: jwksTable ?? 'jwks',
439
- targetCollection: slugs.jwks,
440
- rows: 1,
441
- skipped: isNew ? 0 : apply ? 1 : 0,
442
- unmappable: [],
443
- });
472
+ tally('jwks', jwksTable ?? 'jwks', slugs.jwks, isNew);
444
473
  }
445
474
  // --- 7. organization plugin rows ---------------------------------------
446
475
  const organizationsTable = sourceTables['organizations'];
@@ -462,24 +491,10 @@ export async function applyImport(options) {
462
491
  ...(textOrNull(row.metadata) === undefined
463
492
  ? {}
464
493
  : { metadata: textOrNull(row.metadata) }),
494
+ ...stamps(row),
465
495
  }, created, `organization:${String(row.id)}`)
466
496
  : false;
467
- if (apply) {
468
- if (isNew) {
469
- createdCount += 1;
470
- }
471
- else {
472
- skippedCount += 1;
473
- }
474
- }
475
- record({
476
- model: 'organization',
477
- sourceTable: organizationsTable ?? 'organizations',
478
- targetCollection: slugs.organization,
479
- rows: 1,
480
- skipped: isNew ? 0 : apply ? 1 : 0,
481
- unmappable: [],
482
- });
497
+ tally('organization', organizationsTable ?? 'organizations', slugs.organization, isNew);
483
498
  }
484
499
  const membersTable = sourceTables['members'];
485
500
  const members = await readMapped(client, membersTable, [
@@ -496,24 +511,10 @@ export async function applyImport(options) {
496
511
  organizationId: `org-${String(row.organization_id)}`,
497
512
  userId: String(row.user_id),
498
513
  ...(textOrNull(row.role) === undefined ? {} : { role: textOrNull(row.role) }),
514
+ ...stamps(row),
499
515
  }, created, `member:${String(row.id)}`)
500
516
  : false;
501
- if (apply) {
502
- if (isNew) {
503
- createdCount += 1;
504
- }
505
- else {
506
- skippedCount += 1;
507
- }
508
- }
509
- record({
510
- model: 'member',
511
- sourceTable: membersTable ?? 'members',
512
- targetCollection: slugs.member,
513
- rows: 1,
514
- skipped: isNew ? 0 : apply ? 1 : 0,
515
- unmappable: [],
516
- });
517
+ tally('member', membersTable ?? 'members', slugs.member, isNew);
517
518
  }
518
519
  const invitationsTable = sourceTables['invitations'];
519
520
  const invitations = await readMapped(client, invitationsTable, [
@@ -538,31 +539,18 @@ export async function applyImport(options) {
538
539
  ? { expiresAt: row.expires_at.toISOString() }
539
540
  : {}),
540
541
  ...(typeof row.inviter_id === 'number' ? { inviterId: String(row.inviter_id) } : {}),
542
+ ...stamps(row),
541
543
  }, created, `invitation:${String(row.id)}`)
542
544
  : false;
543
- if (apply) {
544
- if (isNew) {
545
- createdCount += 1;
546
- }
547
- else {
548
- skippedCount += 1;
549
- }
550
- }
551
- record({
552
- model: 'invitation',
553
- sourceTable: invitationsTable ?? 'invitations',
554
- targetCollection: slugs.invitation,
555
- rows: 1,
556
- skipped: isNew ? 0 : apply ? 1 : 0,
557
- unmappable: [],
558
- });
545
+ tally('invitation', invitationsTable ?? 'invitations', slugs.invitation, isNew);
559
546
  }
560
- log(`plan: ${planRows.length} row group(s), ${createdCount} created, ${skippedCount} skipped, ${fieldWriteCount} field writes`);
547
+ log(`plan: ${planRows.length} row group(s), ${createdCount} created, ${skippedCount} skipped, ${fieldWriteCount} field value(s) captured`);
561
548
  if (apply) {
562
549
  // The manifest is the run's contract: merge this pass onto any
563
550
  // resumed run's rows and write it back (issue #50).
564
- const manifest = {
565
- version: 1,
551
+ const manifest = signManifest(payload.secret, {
552
+ version: 3,
553
+ kind: 'import',
566
554
  createdAt: options.manifest?.createdAt ?? now().toISOString(),
567
555
  sourceDatabase: options.sourceDatabase,
568
556
  options: { sessions },
@@ -570,12 +558,8 @@ export async function applyImport(options) {
570
558
  fieldWrites,
571
559
  finalized: options.manifest?.finalized ?? false,
572
560
  sourceCounts: options.sourceCounts,
573
- totals: {
574
- ...options.sourceCounts,
575
- ...(options.manifest?.totals ?? {}),
576
- ...totals,
577
- },
578
- };
561
+ totals,
562
+ });
579
563
  const { writeManifest } = await import('./lifecycle.js');
580
564
  writeManifest(manifest, options.manifestPath);
581
565
  }