@payload-auth-link/migrate 0.4.3 → 0.5.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 (66) 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 +103 -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 +23 -5
  18. package/dist/importer/apply.d.ts.map +1 -1
  19. package/dist/importer/apply.js +154 -178
  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 +24 -0
  26. package/dist/importer/lifecycle.d.ts.map +1 -1
  27. package/dist/importer/lifecycle.js +104 -22
  28. package/dist/importer/lifecycle.js.map +1 -1
  29. package/dist/importer/prelink.d.ts +55 -0
  30. package/dist/importer/prelink.d.ts.map +1 -0
  31. package/dist/importer/prelink.js +152 -0
  32. package/dist/importer/prelink.js.map +1 -0
  33. package/dist/importer/run-import.d.ts +13 -0
  34. package/dist/importer/run-import.d.ts.map +1 -1
  35. package/dist/importer/run-import.js +147 -14
  36. package/dist/importer/run-import.js.map +1 -1
  37. package/dist/importer/source.d.ts +35 -7
  38. package/dist/importer/source.d.ts.map +1 -1
  39. package/dist/importer/source.js +37 -6
  40. package/dist/importer/source.js.map +1 -1
  41. package/dist/importer/types.d.ts +26 -4
  42. package/dist/importer/types.d.ts.map +1 -1
  43. package/dist/importer/types.js.map +1 -1
  44. package/dist/index.d.ts +5 -4
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +3 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/private-file.d.ts +8 -0
  49. package/dist/private-file.d.ts.map +1 -0
  50. package/dist/private-file.js +19 -0
  51. package/dist/private-file.js.map +1 -0
  52. package/package.json +6 -6
  53. package/src/cli.ts +111 -9
  54. package/src/doctor/database.ts +36 -4
  55. package/src/doctor/run-doctor.ts +69 -0
  56. package/src/doctor/types.ts +12 -0
  57. package/src/doctor/users.ts +23 -4
  58. package/src/importer/apply.ts +186 -180
  59. package/src/importer/host-writes.ts +231 -0
  60. package/src/importer/lifecycle.ts +126 -24
  61. package/src/importer/prelink.ts +228 -0
  62. package/src/importer/run-import.ts +196 -29
  63. package/src/importer/source.ts +72 -14
  64. package/src/importer/types.ts +27 -4
  65. package/src/index.ts +13 -1
  66. 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;
@@ -38,6 +49,13 @@ export type ApplyOptions = {
38
49
  readonly now?: () => Date;
39
50
  readonly log?: (message: string) => void;
40
51
  };
52
+ /**
53
+ * Plugin user state the target must be able to hold (issue #88),
54
+ * checked BEFORE any write: a banned user whose target has no `banned`
55
+ * field would import unbanned; an anonymous user without `isAnonymous`
56
+ * would import as a full account.
57
+ */
58
+ export declare function pluginStateUnmappable(users: readonly SourceUser[], targetFields: ReadonlySet<string>, targetSlug: string): Unmappable[];
41
59
  export declare function applyImport(options: ApplyOptions): Promise<ImportResult>;
42
60
  /** Collapse per-row plan entries into one line per model (dry-run print). */
43
61
  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;AAEtC,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;CACzC,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,CA+f9E;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,CAerE"}
@@ -1,3 +1,4 @@
1
+ import { capturedKeys, captureBefore } from './host-writes.js';
1
2
  import { readFlatRows, openSource, readSourceUsers, } from './source.js';
2
3
  import { defaultTargetSlugs, resolveSourceTables, } from './types.js';
3
4
  const slugsOf = (overrides) => ({
@@ -14,14 +15,80 @@ function numberOrNull(value) {
14
15
  const numeric = typeof value === 'number' ? value : Number(value);
15
16
  return Number.isFinite(numeric) ? numeric : undefined;
16
17
  }
18
+ /** The source row's timestamps, when its layout has them (issue #88). */
19
+ function stamps(row) {
20
+ return {
21
+ ...(row.created_at instanceof Date ? { createdAt: row.created_at.toISOString() } : {}),
22
+ ...(row.updated_at instanceof Date ? { updatedAt: row.updated_at.toISOString() } : {}),
23
+ };
24
+ }
25
+ const timestampColumns = ['created_at', 'updated_at'];
17
26
  /**
18
27
  * Rows for one mapped model. A `null` mapping records the model as
19
28
  * not installed (issue #68) — no read, no rows; anything else goes
20
29
  * through the strict `readFlatRows` (a missing mapped table throws;
21
- * the probe aborts the run first).
30
+ * the probe aborts the run first). Row timestamps are read when the
31
+ * layout has them.
22
32
  */
23
33
  async function readMapped(client, table, columns) {
24
- return table === null ? [] : readFlatRows(client, table, columns);
34
+ return table === null ? [] : readFlatRows(client, table, columns, timestampColumns);
35
+ }
36
+ /** Field names a target collection declares (top level, flattened). */
37
+ function targetFieldNames(payload, slug) {
38
+ const collection = payload.collections[slug];
39
+ const names = new Set();
40
+ for (const field of collection?.config.flattenedFields ?? []) {
41
+ names.add(field.name);
42
+ }
43
+ return names;
44
+ }
45
+ /**
46
+ * Plugin user state the target must be able to hold (issue #88),
47
+ * checked BEFORE any write: a banned user whose target has no `banned`
48
+ * field would import unbanned; an anonymous user without `isAnonymous`
49
+ * would import as a full account.
50
+ */
51
+ export function pluginStateUnmappable(users, targetFields, targetSlug) {
52
+ const found = [];
53
+ for (const user of users) {
54
+ if (user.banned === true && !targetFields.has('banned')) {
55
+ found.push({
56
+ model: 'user',
57
+ key: user.id,
58
+ 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`,
59
+ });
60
+ }
61
+ if (user.isAnonymous === true && !targetFields.has('isAnonymous')) {
62
+ found.push({
63
+ model: 'user',
64
+ key: user.id,
65
+ 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`,
66
+ });
67
+ }
68
+ }
69
+ return found;
70
+ }
71
+ /** Plugin user state carried onto the ba-users row (issue #88). */
72
+ function pluginUserState(user, targetFields) {
73
+ const state = {};
74
+ if (user.banned !== null && targetFields.has('banned')) {
75
+ state.banned = user.banned;
76
+ }
77
+ if (user.banReason !== null && targetFields.has('banReason')) {
78
+ state.banReason = user.banReason;
79
+ }
80
+ if (user.banExpires !== null && targetFields.has('banExpires')) {
81
+ state.banExpires = user.banExpires.toISOString();
82
+ }
83
+ if (user.isAnonymous !== null && targetFields.has('isAnonymous')) {
84
+ state.isAnonymous = user.isAnonymous;
85
+ }
86
+ // Better Auth's admin plugin stores roles as one comma-separated
87
+ // string; payload-auth kept them in the users_role child table.
88
+ if (user.roles.length > 0 && targetFields.has('role')) {
89
+ state.role = user.roles.join(',');
90
+ }
91
+ return state;
25
92
  }
26
93
  /** Find an existing target row by `baId` — the resumable dedupe key. */
27
94
  async function findByBaId(payload, collection, baId) {
@@ -53,19 +120,6 @@ async function createDeduped(payload, collection, baId, data, created, sourceKey
53
120
  }
54
121
  return true;
55
122
  }
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
123
  /**
70
124
  * The identity rows pre-populated on the host user (issue #69): ONE
71
125
  * `better-auth` identity per imported user, `providerUserId` set to
@@ -100,7 +154,12 @@ export async function applyImport(options) {
100
154
  const sourceTables = resolveSourceTables(options.sourceTables);
101
155
  const created = [...(options.manifest?.created ?? [])];
102
156
  const fieldWrites = [...(options.manifest?.fieldWrites ?? [])];
103
- const totals = { ...(options.manifest?.totals ?? {}) };
157
+ // First capture wins (issue #88): fields captured by an earlier pass
158
+ // keep their pre-import value.
159
+ const captured = capturedKeys(fieldWrites);
160
+ // Per-PASS totals (issue #88): a re-apply reports what this pass
161
+ // planned, never the sum of every pass.
162
+ const totals = {};
104
163
  const planRows = [];
105
164
  const unmappable = [];
106
165
  let createdCount = 0;
@@ -112,6 +171,20 @@ export async function applyImport(options) {
112
171
  const client = await openSource(sourceConnectionString);
113
172
  try {
114
173
  const users = await readSourceUsers(client, sourceTables);
174
+ const baUserFields = targetFieldNames(payload, slugs.user);
175
+ // --- 0. target-capability gate (issue #88) ---------------------------
176
+ // Runs before ANY write: plugin state the target cannot hold aborts
177
+ // the run instead of importing a banned user unbanned.
178
+ const blocked = pluginStateUnmappable(users, baUserFields, slugs.user);
179
+ if (blocked.length > 0) {
180
+ log(`ABORT: ${blocked.length} user(s) carry state the target cannot hold; nothing was written`);
181
+ return {
182
+ created: 0,
183
+ skipped: 0,
184
+ fieldWrites: 0,
185
+ plan: { rows: [], unmappable: blocked, sessions, warnings: [] },
186
+ };
187
+ }
115
188
  const seenUsers = new Set();
116
189
  // --- 1. ba-users + host field writes ---------------------------------
117
190
  for (const user of users) {
@@ -128,6 +201,7 @@ export async function applyImport(options) {
128
201
  name: user.name ?? '',
129
202
  emailVerified: user.emailVerified,
130
203
  ...(user.image === null ? {} : { image: user.image }),
204
+ ...pluginUserState(user, baUserFields),
131
205
  createdAt: user.createdAt.toISOString(),
132
206
  updatedAt: user.updatedAt.toISOString(),
133
207
  }, created, `user:${baId}`)
@@ -141,30 +215,32 @@ export async function applyImport(options) {
141
215
  }
142
216
  // Host-side writes on the EXISTING Payload user (the source
143
217
  // layout's user id IS the host id): identities, roles, and the
144
- // verified-email linking proof. Captured `before` values make
145
- // rollback exact.
218
+ // verified-email linking proof. The prior values (and the
219
+ // doc's updatedAt) are captured first — once — so rollback
220
+ // restores the pre-import document exactly.
146
221
  const hostId = Number(baId);
147
222
  if (Number.isSafeInteger(hostId)) {
223
+ const { present, newlyCaptured } = await captureBefore({
224
+ payload,
225
+ collection: 'users',
226
+ id: hostId,
227
+ fields: ['identities', 'roles', 'linkable', 'linkableEmail'],
228
+ captured,
229
+ into: fieldWrites,
230
+ });
231
+ fieldWriteCount += newlyCaptured;
148
232
  const writes = {
149
233
  identities: identityRows(user, now),
150
- linkable: user.emailVerified,
151
234
  };
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) {
235
+ if (user.roles.length > 0 && present.has('roles')) {
163
236
  // Stored role values pass through unchanged (issue #56: the
164
237
  // storage edge owns translation; the map is the app's).
165
238
  writes.roles = user.roles;
166
239
  }
167
240
  if (user.emailVerified) {
241
+ // Grant-only: the proof is bound to the address (both
242
+ // fields), and an existing proof is never revoked here.
243
+ writes.linkable = true;
168
244
  writes.linkableEmail = user.email;
169
245
  }
170
246
  await payload.update({
@@ -184,6 +260,24 @@ export async function applyImport(options) {
184
260
  unmappable: [],
185
261
  });
186
262
  }
263
+ const tally = (model, sourceTable, target, isNew) => {
264
+ if (apply) {
265
+ if (isNew) {
266
+ createdCount += 1;
267
+ }
268
+ else {
269
+ skippedCount += 1;
270
+ }
271
+ }
272
+ record({
273
+ model,
274
+ sourceTable,
275
+ targetCollection: target,
276
+ rows: 1,
277
+ skipped: isNew ? 0 : apply ? 1 : 0,
278
+ unmappable: [],
279
+ });
280
+ };
187
281
  // --- 2. accounts (hashes verbatim) ------------------------------------
188
282
  const accountsTable = sourceTables['accounts'];
189
283
  for (const user of users) {
@@ -206,24 +300,15 @@ export async function applyImport(options) {
206
300
  ...(account.refreshTokenExpiresAt === null
207
301
  ? {}
208
302
  : { refreshTokenExpiresAt: account.refreshTokenExpiresAt.toISOString() }),
303
+ ...(account.createdAt === null
304
+ ? {}
305
+ : { createdAt: account.createdAt.toISOString() }),
306
+ ...(account.updatedAt === null
307
+ ? {}
308
+ : { updatedAt: account.updatedAt.toISOString() }),
209
309
  }, created, `account:${account.id}`)
210
310
  : 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
- });
311
+ tally('account', accountsTable ?? 'accounts', slugs.account, isNew);
227
312
  }
228
313
  }
229
314
  // --- 3. sessions (carry keeps every token) -----------------------------
@@ -236,8 +321,6 @@ export async function applyImport(options) {
236
321
  'expires_at',
237
322
  'ip_address',
238
323
  'user_agent',
239
- 'created_at',
240
- 'updated_at',
241
324
  ]);
242
325
  for (const row of rows) {
243
326
  totals.session = (totals.session ?? 0) + 1;
@@ -255,30 +338,10 @@ export async function applyImport(options) {
255
338
  ...(textOrNull(row.user_agent) === undefined
256
339
  ? {}
257
340
  : { 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
- : {}),
341
+ ...stamps(row),
264
342
  }, created, `session:${String(row.id)}`)
265
343
  : 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
- });
344
+ tally('session', sessionsTable ?? 'sessions', slugs.session, isNew);
282
345
  }
283
346
  }
284
347
  // --- 4. verifications ---------------------------------------------------
@@ -297,24 +360,10 @@ export async function applyImport(options) {
297
360
  identifier: String(row.identifier),
298
361
  value: String(row.value),
299
362
  expiresAt: row.expires_at instanceof Date ? row.expires_at.toISOString() : now().toISOString(),
363
+ ...stamps(row),
300
364
  }, created, `verification:${String(row.id)}`)
301
365
  : 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
- });
366
+ tally('verification', verificationsTable ?? 'verifications', slugs.verification, isNew);
318
367
  }
319
368
  // --- 5. api keys (hash, prefix, metadata, rate-limit state) --------------
320
369
  const apiKeysTable = sourceTables['api_keys'];
@@ -325,8 +374,8 @@ export async function applyImport(options) {
325
374
  'start',
326
375
  'prefix',
327
376
  'key',
328
- // payload-auth stores the satellite's userId as referenceId
329
- // (the #49 field map's source side).
377
+ // payload-auth stores the satellite's owner as reference_id — the
378
+ // same name @better-auth/api-key 1.7 uses (`referenceId`).
330
379
  'reference_id',
331
380
  'refill_interval',
332
381
  'refill_amount',
@@ -352,9 +401,9 @@ export async function applyImport(options) {
352
401
  ...(textOrNull(row.start) === undefined ? {} : { start: textOrNull(row.start) }),
353
402
  ...(textOrNull(row.prefix) === undefined ? {} : { prefix: textOrNull(row.prefix) }),
354
403
  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.
404
+ // @better-auth/api-key 1.7 names the owner `referenceId`;
405
+ // `userId` is offered for 1.6-era target schemas (Payload
406
+ // drops it where the collection has no such field).
358
407
  userId: String(row.reference_id),
359
408
  referenceId: String(row.reference_id),
360
409
  ...(numberOrNull(row.refill_interval) === undefined
@@ -394,24 +443,10 @@ export async function applyImport(options) {
394
443
  ...(textOrNull(row.metadata) === undefined
395
444
  ? {}
396
445
  : { metadata: textOrNull(row.metadata) }),
446
+ ...stamps(row),
397
447
  }, created, `apikey:${String(row.id)}`)
398
448
  : 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
- });
449
+ tally('apikey', apiKeysTable ?? 'api_keys', slugs.apikey, isNew);
415
450
  }
416
451
  // --- 6. jwks --------------------------------------------------------------
417
452
  const jwksTable = sourceTables['jwks'];
@@ -423,24 +458,10 @@ export async function applyImport(options) {
423
458
  ? await createDeduped(payload, slugs.jwks, baId, {
424
459
  publicKey: String(row.public_key),
425
460
  privateKey: String(row.private_key),
461
+ ...stamps(row),
426
462
  }, created, `jwks:${String(row.id)}`)
427
463
  : 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
- });
464
+ tally('jwks', jwksTable ?? 'jwks', slugs.jwks, isNew);
444
465
  }
445
466
  // --- 7. organization plugin rows ---------------------------------------
446
467
  const organizationsTable = sourceTables['organizations'];
@@ -462,24 +483,10 @@ export async function applyImport(options) {
462
483
  ...(textOrNull(row.metadata) === undefined
463
484
  ? {}
464
485
  : { metadata: textOrNull(row.metadata) }),
486
+ ...stamps(row),
465
487
  }, created, `organization:${String(row.id)}`)
466
488
  : 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
- });
489
+ tally('organization', organizationsTable ?? 'organizations', slugs.organization, isNew);
483
490
  }
484
491
  const membersTable = sourceTables['members'];
485
492
  const members = await readMapped(client, membersTable, [
@@ -496,24 +503,10 @@ export async function applyImport(options) {
496
503
  organizationId: `org-${String(row.organization_id)}`,
497
504
  userId: String(row.user_id),
498
505
  ...(textOrNull(row.role) === undefined ? {} : { role: textOrNull(row.role) }),
506
+ ...stamps(row),
499
507
  }, created, `member:${String(row.id)}`)
500
508
  : 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
- });
509
+ tally('member', membersTable ?? 'members', slugs.member, isNew);
517
510
  }
518
511
  const invitationsTable = sourceTables['invitations'];
519
512
  const invitations = await readMapped(client, invitationsTable, [
@@ -538,31 +531,18 @@ export async function applyImport(options) {
538
531
  ? { expiresAt: row.expires_at.toISOString() }
539
532
  : {}),
540
533
  ...(typeof row.inviter_id === 'number' ? { inviterId: String(row.inviter_id) } : {}),
534
+ ...stamps(row),
541
535
  }, created, `invitation:${String(row.id)}`)
542
536
  : 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
- });
537
+ tally('invitation', invitationsTable ?? 'invitations', slugs.invitation, isNew);
559
538
  }
560
- log(`plan: ${planRows.length} row group(s), ${createdCount} created, ${skippedCount} skipped, ${fieldWriteCount} field writes`);
539
+ log(`plan: ${planRows.length} row group(s), ${createdCount} created, ${skippedCount} skipped, ${fieldWriteCount} field value(s) captured`);
561
540
  if (apply) {
562
541
  // The manifest is the run's contract: merge this pass onto any
563
542
  // resumed run's rows and write it back (issue #50).
564
543
  const manifest = {
565
- version: 1,
544
+ version: 2,
545
+ kind: 'import',
566
546
  createdAt: options.manifest?.createdAt ?? now().toISOString(),
567
547
  sourceDatabase: options.sourceDatabase,
568
548
  options: { sessions },
@@ -570,11 +550,7 @@ export async function applyImport(options) {
570
550
  fieldWrites,
571
551
  finalized: options.manifest?.finalized ?? false,
572
552
  sourceCounts: options.sourceCounts,
573
- totals: {
574
- ...options.sourceCounts,
575
- ...(options.manifest?.totals ?? {}),
576
- ...totals,
577
- },
553
+ totals,
578
554
  };
579
555
  const { writeManifest } = await import('./lifecycle.js');
580
556
  writeManifest(manifest, options.manifestPath);