payload-better-auth 1.0.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 +225 -0
- package/dist/better-auth/crypto-shared.d.ts +61 -0
- package/dist/better-auth/crypto-shared.js +90 -0
- package/dist/better-auth/crypto-shared.js.map +1 -0
- package/dist/better-auth/databaseHooks.d.ts +5 -0
- package/dist/better-auth/databaseHooks.js +24 -0
- package/dist/better-auth/databaseHooks.js.map +1 -0
- package/dist/better-auth/plugin.d.ts +13 -0
- package/dist/better-auth/plugin.js +159 -0
- package/dist/better-auth/plugin.js.map +1 -0
- package/dist/better-auth/reconcile-queue.d.ts +70 -0
- package/dist/better-auth/reconcile-queue.js +311 -0
- package/dist/better-auth/reconcile-queue.js.map +1 -0
- package/dist/better-auth/sources.d.ts +35 -0
- package/dist/better-auth/sources.js +126 -0
- package/dist/better-auth/sources.js.map +1 -0
- package/dist/collections/Users/index.d.ts +5 -0
- package/dist/collections/Users/index.js +167 -0
- package/dist/collections/Users/index.js.map +1 -0
- package/dist/components/BeforeDashboardClient.d.ts +2 -0
- package/dist/components/BeforeDashboardClient.js +36 -0
- package/dist/components/BeforeDashboardClient.js.map +1 -0
- package/dist/components/BeforeDashboardServer.d.ts +3 -0
- package/dist/components/BeforeDashboardServer.js +22 -0
- package/dist/components/BeforeDashboardServer.js.map +1 -0
- package/dist/components/BeforeDashboardServer.module.css +5 -0
- package/dist/components/BetterAuthLoginServer.d.ts +5 -0
- package/dist/components/BetterAuthLoginServer.js +82 -0
- package/dist/components/BetterAuthLoginServer.js.map +1 -0
- package/dist/components/EmailPasswordFormClient.d.ts +5 -0
- package/dist/components/EmailPasswordFormClient.js +162 -0
- package/dist/components/EmailPasswordFormClient.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +3 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/rsc.d.ts +1 -0
- package/dist/exports/rsc.js +3 -0
- package/dist/exports/rsc.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/payload/plugin.d.ts +7 -0
- package/dist/payload/plugin.js +76 -0
- package/dist/payload/plugin.js.map +1 -0
- package/dist/utils/payload-reconcile.d.ts +6 -0
- package/dist/utils/payload-reconcile.js +37 -0
- package/dist/utils/payload-reconcile.js.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AuthContext } from 'better-auth';
|
|
2
|
+
import type { BAUser, PayloadUser } from './sources.js';
|
|
3
|
+
export interface QueueDeps {
|
|
4
|
+
deleteUserFromPayload: (baId: string) => Promise<void>;
|
|
5
|
+
internalAdapter: AuthContext['internalAdapter'];
|
|
6
|
+
listPayloadUsersPage: (limit: number, page: number) => Promise<{
|
|
7
|
+
hasNextPage: boolean;
|
|
8
|
+
total: number;
|
|
9
|
+
users: PayloadUser[];
|
|
10
|
+
}>;
|
|
11
|
+
log?: (msg: string, extra?: any) => void;
|
|
12
|
+
prunePayloadOrphans?: boolean;
|
|
13
|
+
syncUserToPayload: (baUser: BAUser) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export type TaskSource = 'full-reconcile' | 'user-operation';
|
|
16
|
+
export interface InitOptions {
|
|
17
|
+
forceReset?: boolean;
|
|
18
|
+
reconcileEveryMs?: number;
|
|
19
|
+
runOnBoot?: boolean;
|
|
20
|
+
tickMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class Queue {
|
|
23
|
+
private bootstrapState;
|
|
24
|
+
private deps;
|
|
25
|
+
private failed;
|
|
26
|
+
private keys;
|
|
27
|
+
private lastError;
|
|
28
|
+
private lastSeedAt;
|
|
29
|
+
private processed;
|
|
30
|
+
private processing;
|
|
31
|
+
private q;
|
|
32
|
+
private reconcileEveryMs;
|
|
33
|
+
private reconcileTimeout;
|
|
34
|
+
private reconciling;
|
|
35
|
+
private tickTimer;
|
|
36
|
+
constructor(deps: QueueDeps, opts?: InitOptions);
|
|
37
|
+
private bumpFront;
|
|
38
|
+
/** Clear all full-reconcile tasks from the queue, preserving user-operation tasks */
|
|
39
|
+
private clearFullReconcileTasks;
|
|
40
|
+
private enqueue;
|
|
41
|
+
private listBAUsersPage;
|
|
42
|
+
private runTask;
|
|
43
|
+
private scheduleNextReconcile;
|
|
44
|
+
/** Paginated approach: process users page by page to reduce memory usage */
|
|
45
|
+
private seedFullReconcilePaginated;
|
|
46
|
+
private tick;
|
|
47
|
+
enqueueDelete(baId: string, priority?: boolean, source?: TaskSource, reconcileId?: string): void;
|
|
48
|
+
enqueueEnsure(user: BAUser, priority?: boolean, source?: TaskSource, reconcileId?: string): void;
|
|
49
|
+
getInstanceInfo(): {
|
|
50
|
+
isBootstrapped: boolean;
|
|
51
|
+
};
|
|
52
|
+
/** Seed tasks by comparing users page by page (Better-Auth → Payload). */
|
|
53
|
+
seedFullReconcile(): Promise<void>;
|
|
54
|
+
start({ reconcileEveryMs, tickMs }?: {
|
|
55
|
+
reconcileEveryMs?: number | undefined;
|
|
56
|
+
tickMs?: number | undefined;
|
|
57
|
+
}): void;
|
|
58
|
+
status(): {
|
|
59
|
+
failed: number;
|
|
60
|
+
fullReconcileTasks: number;
|
|
61
|
+
lastError: string | null;
|
|
62
|
+
lastSeedAt: string | null;
|
|
63
|
+
processed: number;
|
|
64
|
+
processing: boolean;
|
|
65
|
+
queueSize: number;
|
|
66
|
+
reconciling: boolean;
|
|
67
|
+
sampleKeys: string[];
|
|
68
|
+
userOperationTasks: number;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
const KEY = (t)=>`${t.kind}:${t.baId}`;
|
|
2
|
+
export class Queue {
|
|
3
|
+
// Bootstrap state stored directly on the queue instance
|
|
4
|
+
bootstrapState = {
|
|
5
|
+
adminHeaders: null,
|
|
6
|
+
bootstrapPromise: null,
|
|
7
|
+
isBootstrapped: false
|
|
8
|
+
};
|
|
9
|
+
deps;
|
|
10
|
+
failed = 0;
|
|
11
|
+
keys = new Map();
|
|
12
|
+
lastError = null;
|
|
13
|
+
lastSeedAt = null;
|
|
14
|
+
processed = 0;
|
|
15
|
+
processing = false;
|
|
16
|
+
q = [];
|
|
17
|
+
reconcileEveryMs = 30 * 60_000 // default 30 minutes
|
|
18
|
+
;
|
|
19
|
+
reconcileTimeout = null;
|
|
20
|
+
reconciling = false;
|
|
21
|
+
tickTimer = null;
|
|
22
|
+
constructor(deps, opts = {}){
|
|
23
|
+
this.deps = deps;
|
|
24
|
+
const log = this.deps?.log ?? (()=>{});
|
|
25
|
+
// Start bootstrap process - but defer heavy operations
|
|
26
|
+
log('Starting bootstrap process...');
|
|
27
|
+
// Start timers but don't run reconcile immediately
|
|
28
|
+
this.start({
|
|
29
|
+
reconcileEveryMs: opts?.reconcileEveryMs ?? 30 * 60_000,
|
|
30
|
+
tickMs: opts?.tickMs ?? 1000
|
|
31
|
+
});
|
|
32
|
+
// Defer the initial reconcile to avoid circular dependency issues
|
|
33
|
+
if (opts?.runOnBoot ?? true) {
|
|
34
|
+
// Use setTimeout instead of queueMicrotask to give more time for initialization
|
|
35
|
+
setTimeout(()=>{
|
|
36
|
+
this.seedFullReconcile().catch((err)=>console.error('[reconcile] seed failed', err));
|
|
37
|
+
}, 2000); // 2 second delay to allow Better Auth and Payload to fully initialize
|
|
38
|
+
}
|
|
39
|
+
log('Bootstrap process completed');
|
|
40
|
+
}
|
|
41
|
+
bumpFront(task) {
|
|
42
|
+
this.q = [
|
|
43
|
+
task,
|
|
44
|
+
...this.q.filter((t)=>t !== task)
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
/** Clear all full-reconcile tasks from the queue, preserving user-operation tasks */ clearFullReconcileTasks() {
|
|
48
|
+
const log = this.deps?.log ?? (()=>{});
|
|
49
|
+
const beforeCount = this.q.length;
|
|
50
|
+
const fullReconcileCount = this.q.filter((t)=>t.source === 'full-reconcile').length;
|
|
51
|
+
// Remove full-reconcile tasks from queue and keys map
|
|
52
|
+
this.q = this.q.filter((task)=>{
|
|
53
|
+
if (task.source === 'full-reconcile') {
|
|
54
|
+
this.keys.delete(KEY(task));
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
const afterCount = this.q.length;
|
|
60
|
+
log('reconcile.clear-previous', {
|
|
61
|
+
afterCount,
|
|
62
|
+
beforeCount,
|
|
63
|
+
clearedFullReconcile: fullReconcileCount,
|
|
64
|
+
preservedUserOps: afterCount
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// ——— Internals ———
|
|
68
|
+
enqueue(task, priority) {
|
|
69
|
+
const k = KEY(task);
|
|
70
|
+
const existing = this.keys.get(k);
|
|
71
|
+
if (existing) {
|
|
72
|
+
if (task.kind === 'ensure' && existing.kind === 'ensure' && !existing.baUser && task.baUser) {
|
|
73
|
+
existing.baUser = task.baUser;
|
|
74
|
+
}
|
|
75
|
+
if (priority) {
|
|
76
|
+
this.bumpFront(existing);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (priority) {
|
|
81
|
+
this.q.unshift(task);
|
|
82
|
+
} else {
|
|
83
|
+
this.q.push(task);
|
|
84
|
+
}
|
|
85
|
+
this.keys.set(k, task);
|
|
86
|
+
}
|
|
87
|
+
async listBAUsersPage({ limit, offset }) {
|
|
88
|
+
// sort by newest (used) first
|
|
89
|
+
// when a delete is happening in the meantime, this will lead to some users not being listed (as the index changes)
|
|
90
|
+
// TODO: fix this by maintaining a delete list.
|
|
91
|
+
const total = await this.deps.internalAdapter.countTotalUsers();
|
|
92
|
+
const users = await this.deps.internalAdapter.listUsers(limit, offset, {
|
|
93
|
+
direction: 'desc',
|
|
94
|
+
field: 'updatedAt'
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
total,
|
|
98
|
+
users
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async runTask(t) {
|
|
102
|
+
const log = this.deps?.log ?? (()=>{});
|
|
103
|
+
if (t.kind === 'ensure') {
|
|
104
|
+
log('queue.ensure', {
|
|
105
|
+
attempts: t.attempts,
|
|
106
|
+
baId: t.baId
|
|
107
|
+
});
|
|
108
|
+
await this.deps.syncUserToPayload(t.baUser ?? {
|
|
109
|
+
id: t.baId
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// delete
|
|
114
|
+
log('queue.delete', {
|
|
115
|
+
attempts: t.attempts,
|
|
116
|
+
baId: t.baId
|
|
117
|
+
});
|
|
118
|
+
await this.deps.deleteUserFromPayload(t.baId);
|
|
119
|
+
}
|
|
120
|
+
scheduleNextReconcile() {
|
|
121
|
+
if (this.reconcileTimeout) {
|
|
122
|
+
clearTimeout(this.reconcileTimeout);
|
|
123
|
+
}
|
|
124
|
+
this.reconcileTimeout = setTimeout(async ()=>{
|
|
125
|
+
if (!this.reconciling) {
|
|
126
|
+
this.reconciling = true;
|
|
127
|
+
try {
|
|
128
|
+
await this.seedFullReconcile();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Error is already logged in seedFullReconcile
|
|
131
|
+
} finally{
|
|
132
|
+
this.reconciling = false;
|
|
133
|
+
// Schedule the next reconcile after this one completes
|
|
134
|
+
this.scheduleNextReconcile();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, this.reconcileEveryMs);
|
|
138
|
+
// Optional unref for Node.js environments to prevent keeping process alive
|
|
139
|
+
if ('unref' in this.reconcileTimeout && typeof this.reconcileTimeout.unref === 'function') {
|
|
140
|
+
this.reconcileTimeout.unref();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Paginated approach: process users page by page to reduce memory usage */ async seedFullReconcilePaginated(reconcileId) {
|
|
144
|
+
const log = this.deps?.log ?? (()=>{});
|
|
145
|
+
const pageSize = 500;
|
|
146
|
+
let baIdSet = null;
|
|
147
|
+
// If we need to prune orphans, we need to collect all BA user IDs
|
|
148
|
+
if (this.deps.prunePayloadOrphans) {
|
|
149
|
+
baIdSet = new Set();
|
|
150
|
+
let baOffset = 0;
|
|
151
|
+
let baTotal = 0;
|
|
152
|
+
do {
|
|
153
|
+
const { total, users: baUsers } = await this.listBAUsersPage({
|
|
154
|
+
limit: pageSize,
|
|
155
|
+
offset: baOffset
|
|
156
|
+
});
|
|
157
|
+
baTotal = total;
|
|
158
|
+
// Enqueue ensure tasks for this page with full-reconcile source
|
|
159
|
+
for (const u of baUsers){
|
|
160
|
+
this.enqueueEnsure(u, false, 'full-reconcile', reconcileId);
|
|
161
|
+
baIdSet.add(u.id);
|
|
162
|
+
}
|
|
163
|
+
baOffset += baUsers.length;
|
|
164
|
+
log('reconcile.seed.ba-page', {
|
|
165
|
+
processed: baOffset,
|
|
166
|
+
reconcileId,
|
|
167
|
+
total: baTotal
|
|
168
|
+
});
|
|
169
|
+
}while (baOffset < baTotal)
|
|
170
|
+
} else {
|
|
171
|
+
// If not pruning, we can process BA users page by page without storing IDs
|
|
172
|
+
let baOffset = 0;
|
|
173
|
+
let baTotal = 0;
|
|
174
|
+
do {
|
|
175
|
+
// TODO: make sure that we dont go past the window through deletes happening
|
|
176
|
+
// (As a user deletes, the total window size becomes smaller)
|
|
177
|
+
const { total, users: baUsers } = await this.listBAUsersPage({
|
|
178
|
+
limit: pageSize,
|
|
179
|
+
offset: baOffset
|
|
180
|
+
});
|
|
181
|
+
baTotal = total;
|
|
182
|
+
// Enqueue ensure tasks for this page with full-reconcile source
|
|
183
|
+
for (const u of baUsers){
|
|
184
|
+
this.enqueueEnsure(u, false, 'full-reconcile', reconcileId);
|
|
185
|
+
}
|
|
186
|
+
baOffset += baUsers.length;
|
|
187
|
+
log('reconcile.seed.ba-page', {
|
|
188
|
+
processed: baOffset,
|
|
189
|
+
reconcileId,
|
|
190
|
+
total: baTotal
|
|
191
|
+
});
|
|
192
|
+
}while (baOffset < baTotal)
|
|
193
|
+
}
|
|
194
|
+
// Process Payload users page by page for orphan pruning
|
|
195
|
+
if (this.deps.prunePayloadOrphans && baIdSet) {
|
|
196
|
+
let payloadPage = 1;
|
|
197
|
+
let hasNextPage = true;
|
|
198
|
+
while(hasNextPage){
|
|
199
|
+
const { hasNextPage: nextPage, users: pUsers } = await this.deps.listPayloadUsersPage(pageSize, payloadPage);
|
|
200
|
+
hasNextPage = nextPage;
|
|
201
|
+
for (const pu of pUsers){
|
|
202
|
+
const ext = pu.externalId?.toString();
|
|
203
|
+
if (ext && !baIdSet.has(ext)) {
|
|
204
|
+
this.enqueueDelete(ext, false, 'full-reconcile', reconcileId);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
payloadPage++;
|
|
208
|
+
log('reconcile.seed.payload-page', {
|
|
209
|
+
page: payloadPage - 1,
|
|
210
|
+
reconcileId
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async tick() {
|
|
216
|
+
if (this.processing) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const idx = this.q.findIndex((t)=>t.nextAt <= now);
|
|
221
|
+
if (idx === -1) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const task = this.q[idx];
|
|
225
|
+
this.processing = true;
|
|
226
|
+
try {
|
|
227
|
+
await this.runTask(task);
|
|
228
|
+
this.q.splice(idx, 1);
|
|
229
|
+
this.keys.delete(KEY(task));
|
|
230
|
+
this.processed++;
|
|
231
|
+
} catch (e) {
|
|
232
|
+
this.failed++;
|
|
233
|
+
this.lastError = e?.message ?? String(e);
|
|
234
|
+
task.attempts += 1;
|
|
235
|
+
const delay = Math.min(60_000, Math.pow(2, task.attempts) * 1000) + Math.floor(Math.random() * 500);
|
|
236
|
+
task.nextAt = now + delay;
|
|
237
|
+
} finally{
|
|
238
|
+
this.processing = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
enqueueDelete(baId, priority = false, source = 'user-operation', reconcileId) {
|
|
242
|
+
this.enqueue({
|
|
243
|
+
attempts: 0,
|
|
244
|
+
baId,
|
|
245
|
+
kind: 'delete',
|
|
246
|
+
nextAt: Date.now(),
|
|
247
|
+
reconcileId,
|
|
248
|
+
source
|
|
249
|
+
}, priority);
|
|
250
|
+
}
|
|
251
|
+
// ——— Public enqueue API ———
|
|
252
|
+
enqueueEnsure(user, priority = false, source = 'user-operation', reconcileId) {
|
|
253
|
+
this.enqueue({
|
|
254
|
+
attempts: 0,
|
|
255
|
+
baId: user.id,
|
|
256
|
+
baUser: user,
|
|
257
|
+
kind: 'ensure',
|
|
258
|
+
nextAt: Date.now(),
|
|
259
|
+
reconcileId,
|
|
260
|
+
source
|
|
261
|
+
}, priority);
|
|
262
|
+
}
|
|
263
|
+
// Get current instance info
|
|
264
|
+
getInstanceInfo() {
|
|
265
|
+
return {
|
|
266
|
+
isBootstrapped: this.bootstrapState.isBootstrapped
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** Seed tasks by comparing users page by page (Better-Auth → Payload). */ async seedFullReconcile() {
|
|
270
|
+
const log = this.deps?.log ?? (()=>{});
|
|
271
|
+
this.lastSeedAt = new Date().toISOString();
|
|
272
|
+
const reconcileId = `reconcile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
273
|
+
log('reconcile.seed.start', {
|
|
274
|
+
reconcileId
|
|
275
|
+
});
|
|
276
|
+
// Clear all previous full-reconcile tasks, but preserve user-operation tasks
|
|
277
|
+
this.clearFullReconcileTasks();
|
|
278
|
+
await this.seedFullReconcilePaginated(reconcileId);
|
|
279
|
+
log('reconcile.seed.done', this.status());
|
|
280
|
+
}
|
|
281
|
+
start({ reconcileEveryMs = 30 * 60_000, tickMs = 1000 } = {}) {
|
|
282
|
+
this.reconcileEveryMs = reconcileEveryMs;
|
|
283
|
+
if (!this.tickTimer) {
|
|
284
|
+
this.tickTimer = setInterval(()=>this.tick(), tickMs);
|
|
285
|
+
// Optional unref for Node.js environments to prevent keeping process alive
|
|
286
|
+
if ('unref' in this.tickTimer && typeof this.tickTimer.unref === 'function') {
|
|
287
|
+
this.tickTimer.unref();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Schedule the first reconcile
|
|
291
|
+
this.scheduleNextReconcile();
|
|
292
|
+
}
|
|
293
|
+
status() {
|
|
294
|
+
const userOpCount = this.q.filter((t)=>t.source === 'user-operation').length;
|
|
295
|
+
const fullReconcileCount = this.q.filter((t)=>t.source === 'full-reconcile').length;
|
|
296
|
+
return {
|
|
297
|
+
failed: this.failed,
|
|
298
|
+
fullReconcileTasks: fullReconcileCount,
|
|
299
|
+
lastError: this.lastError,
|
|
300
|
+
lastSeedAt: this.lastSeedAt,
|
|
301
|
+
processed: this.processed,
|
|
302
|
+
processing: this.processing,
|
|
303
|
+
queueSize: this.q.length,
|
|
304
|
+
reconciling: this.reconciling,
|
|
305
|
+
sampleKeys: Array.from(this.keys.keys()).slice(0, 50),
|
|
306
|
+
userOperationTasks: userOpCount
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
//# sourceMappingURL=reconcile-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/better-auth/reconcile-queue.ts"],"sourcesContent":["import type { AuthContext } from 'better-auth'\n\n// src/reconcile-queue.ts\nimport type { BAUser, PayloadUser } from './sources.js'\n\nexport interface QueueDeps {\n deleteUserFromPayload: (baId: string) => Promise<void> // delete by externalId; ignore missing\n internalAdapter: AuthContext['internalAdapter']\n\n // Paginated loaders (efficient processing)\n listPayloadUsersPage: (\n limit: number,\n page: number,\n ) => Promise<{ hasNextPage: boolean; total: number; users: PayloadUser[] }>\n // Logging\n log?: (msg: string, extra?: any) => void\n\n // Policy\n prunePayloadOrphans?: boolean // default: false\n\n // Idempotent effects (via Payload Local API)\n syncUserToPayload: (baUser: BAUser) => Promise<void> // upsert by externalId=baUser.id\n}\n\nexport type TaskSource = 'full-reconcile' | 'user-operation'\n\n// Bootstrap options interface\nexport interface InitOptions {\n forceReset?: boolean\n reconcileEveryMs?: number\n runOnBoot?: boolean\n tickMs?: number\n}\n\n// Simplified bootstrap state interface (removed processId)\ninterface BootstrapState {\n adminHeaders: Headers | null\n bootstrapPromise: null | Promise<void>\n isBootstrapped: boolean\n}\n\ntype Task =\n | {\n attempts: number\n baId: string\n baUser?: BAUser\n kind: 'ensure'\n nextAt: number\n reconcileId?: string\n source: TaskSource\n }\n | {\n attempts: number\n baId: string\n kind: 'delete'\n nextAt: number\n reconcileId?: string\n source: TaskSource\n }\n\nconst KEY = (t: Task) => `${t.kind}:${t.baId}`\n\nexport class Queue {\n // Bootstrap state stored directly on the queue instance\n private bootstrapState: BootstrapState = {\n adminHeaders: null,\n bootstrapPromise: null,\n isBootstrapped: false,\n }\n private deps!: QueueDeps\n private failed = 0\n private keys = new Map<string, Task>()\n private lastError: null | string = null\n private lastSeedAt: null | string = null\n private processed = 0\n\n private processing = false\n private q: Task[] = []\n private reconcileEveryMs = 30 * 60_000 // default 30 minutes\n private reconcileTimeout: NodeJS.Timeout | null = null\n private reconciling = false\n\n private tickTimer: NodeJS.Timeout | null = null\n\n constructor(deps: QueueDeps, opts: InitOptions = {}) {\n this.deps = deps\n const log = this.deps?.log ?? (() => {})\n // Start bootstrap process - but defer heavy operations\n log('Starting bootstrap process...')\n\n // Start timers but don't run reconcile immediately\n this.start({\n reconcileEveryMs: opts?.reconcileEveryMs ?? 30 * 60_000,\n tickMs: opts?.tickMs ?? 1000,\n })\n\n // Defer the initial reconcile to avoid circular dependency issues\n if (opts?.runOnBoot ?? true) {\n // Use setTimeout instead of queueMicrotask to give more time for initialization\n setTimeout(() => {\n this.seedFullReconcile().catch((err) => console.error('[reconcile] seed failed', err))\n }, 2000) // 2 second delay to allow Better Auth and Payload to fully initialize\n }\n\n log('Bootstrap process completed')\n }\n\n private bumpFront(task: Task) {\n this.q = [task, ...this.q.filter((t) => t !== task)]\n }\n\n /** Clear all full-reconcile tasks from the queue, preserving user-operation tasks */\n private clearFullReconcileTasks() {\n const log = this.deps?.log ?? (() => {})\n const beforeCount = this.q.length\n const fullReconcileCount = this.q.filter((t) => t.source === 'full-reconcile').length\n\n // Remove full-reconcile tasks from queue and keys map\n this.q = this.q.filter((task) => {\n if (task.source === 'full-reconcile') {\n this.keys.delete(KEY(task))\n return false\n }\n return true\n })\n\n const afterCount = this.q.length\n log('reconcile.clear-previous', {\n afterCount,\n beforeCount,\n clearedFullReconcile: fullReconcileCount,\n preservedUserOps: afterCount,\n })\n }\n\n // ——— Internals ———\n private enqueue(task: Task, priority: boolean) {\n const k = KEY(task)\n const existing = this.keys.get(k)\n if (existing) {\n if (task.kind === 'ensure' && existing.kind === 'ensure' && !existing.baUser && task.baUser) {\n existing.baUser = task.baUser\n }\n if (priority) {\n this.bumpFront(existing)\n }\n return\n }\n if (priority) {\n this.q.unshift(task)\n } else {\n this.q.push(task)\n }\n this.keys.set(k, task)\n }\n\n private async listBAUsersPage({ limit, offset }: { limit: number; offset: number }) {\n // sort by newest (used) first\n // when a delete is happening in the meantime, this will lead to some users not being listed (as the index changes)\n // TODO: fix this by maintaining a delete list.\n const total = await this.deps.internalAdapter.countTotalUsers()\n const users = await this.deps.internalAdapter.listUsers(limit, offset, {\n direction: 'desc',\n field: 'updatedAt',\n })\n return { total, users }\n }\n\n private async runTask(t: Task) {\n const log = this.deps?.log ?? (() => {})\n if (t.kind === 'ensure') {\n log('queue.ensure', { attempts: t.attempts, baId: t.baId })\n await this.deps.syncUserToPayload(t.baUser ?? { id: t.baId })\n return\n }\n // delete\n log('queue.delete', { attempts: t.attempts, baId: t.baId })\n await this.deps.deleteUserFromPayload(t.baId)\n }\n private scheduleNextReconcile() {\n if (this.reconcileTimeout) {\n clearTimeout(this.reconcileTimeout)\n }\n\n this.reconcileTimeout = setTimeout(async () => {\n if (!this.reconciling) {\n this.reconciling = true\n try {\n await this.seedFullReconcile()\n } catch (error) {\n // Error is already logged in seedFullReconcile\n } finally {\n this.reconciling = false\n // Schedule the next reconcile after this one completes\n this.scheduleNextReconcile()\n }\n }\n }, this.reconcileEveryMs)\n\n // Optional unref for Node.js environments to prevent keeping process alive\n if ('unref' in this.reconcileTimeout && typeof this.reconcileTimeout.unref === 'function') {\n this.reconcileTimeout.unref()\n }\n }\n\n /** Paginated approach: process users page by page to reduce memory usage */\n private async seedFullReconcilePaginated(reconcileId: string) {\n const log = this.deps?.log ?? (() => {})\n const pageSize = 500\n let baIdSet: null | Set<string> = null\n\n // If we need to prune orphans, we need to collect all BA user IDs\n if (this.deps.prunePayloadOrphans) {\n baIdSet = new Set<string>()\n let baOffset = 0\n let baTotal = 0\n\n do {\n const { total, users: baUsers } = await this.listBAUsersPage({\n limit: pageSize,\n offset: baOffset,\n })\n baTotal = total\n\n // Enqueue ensure tasks for this page with full-reconcile source\n for (const u of baUsers) {\n this.enqueueEnsure(u, false, 'full-reconcile', reconcileId)\n baIdSet.add(u.id)\n }\n\n baOffset += baUsers.length\n log('reconcile.seed.ba-page', { processed: baOffset, reconcileId, total: baTotal })\n } while (baOffset < baTotal)\n } else {\n // If not pruning, we can process BA users page by page without storing IDs\n let baOffset = 0\n let baTotal = 0\n\n do {\n // TODO: make sure that we dont go past the window through deletes happening\n // (As a user deletes, the total window size becomes smaller)\n const { total, users: baUsers } = await this.listBAUsersPage({\n limit: pageSize,\n offset: baOffset,\n })\n baTotal = total\n\n // Enqueue ensure tasks for this page with full-reconcile source\n for (const u of baUsers) {\n this.enqueueEnsure(u, false, 'full-reconcile', reconcileId)\n }\n\n baOffset += baUsers.length\n log('reconcile.seed.ba-page', { processed: baOffset, reconcileId, total: baTotal })\n } while (baOffset < baTotal)\n }\n\n // Process Payload users page by page for orphan pruning\n if (this.deps.prunePayloadOrphans && baIdSet) {\n let payloadPage = 1\n let hasNextPage = true\n\n while (hasNextPage) {\n const { hasNextPage: nextPage, users: pUsers } = await this.deps.listPayloadUsersPage(\n pageSize,\n payloadPage,\n )\n hasNextPage = nextPage\n\n for (const pu of pUsers) {\n const ext = pu.externalId?.toString()\n if (ext && !baIdSet.has(ext)) {\n this.enqueueDelete(ext, false, 'full-reconcile', reconcileId)\n }\n }\n\n payloadPage++\n log('reconcile.seed.payload-page', { page: payloadPage - 1, reconcileId })\n }\n }\n }\n\n private async tick() {\n if (this.processing) {\n return\n }\n const now = Date.now()\n const idx = this.q.findIndex((t) => t.nextAt <= now)\n if (idx === -1) {\n return\n }\n const task = this.q[idx]\n this.processing = true\n try {\n await this.runTask(task)\n this.q.splice(idx, 1)\n this.keys.delete(KEY(task))\n this.processed++\n } catch (e: any) {\n this.failed++\n this.lastError = e?.message ?? String(e)\n task.attempts += 1\n const delay =\n Math.min(60_000, Math.pow(2, task.attempts) * 1000) + Math.floor(Math.random() * 500)\n task.nextAt = now + delay\n } finally {\n this.processing = false\n }\n }\n\n enqueueDelete(\n baId: string,\n priority = false,\n source: TaskSource = 'user-operation',\n reconcileId?: string,\n ) {\n this.enqueue(\n { attempts: 0, baId, kind: 'delete', nextAt: Date.now(), reconcileId, source },\n priority,\n )\n }\n\n // ——— Public enqueue API ———\n enqueueEnsure(\n user: BAUser,\n priority = false,\n source: TaskSource = 'user-operation',\n reconcileId?: string,\n ) {\n this.enqueue(\n {\n attempts: 0,\n baId: user.id,\n baUser: user,\n kind: 'ensure',\n nextAt: Date.now(),\n reconcileId,\n source,\n },\n priority,\n )\n }\n\n // Get current instance info\n getInstanceInfo() {\n return {\n isBootstrapped: this.bootstrapState.isBootstrapped,\n }\n }\n\n /** Seed tasks by comparing users page by page (Better-Auth → Payload). */\n async seedFullReconcile() {\n const log = this.deps?.log ?? (() => {})\n this.lastSeedAt = new Date().toISOString()\n const reconcileId = `reconcile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`\n\n log('reconcile.seed.start', { reconcileId })\n\n // Clear all previous full-reconcile tasks, but preserve user-operation tasks\n this.clearFullReconcileTasks()\n\n await this.seedFullReconcilePaginated(reconcileId)\n\n log('reconcile.seed.done', this.status())\n }\n\n start({ reconcileEveryMs = 30 * 60_000, tickMs = 1000 } = {}) {\n this.reconcileEveryMs = reconcileEveryMs\n\n if (!this.tickTimer) {\n this.tickTimer = setInterval(() => this.tick(), tickMs)\n // Optional unref for Node.js environments to prevent keeping process alive\n if ('unref' in this.tickTimer && typeof this.tickTimer.unref === 'function') {\n this.tickTimer.unref()\n }\n }\n\n // Schedule the first reconcile\n this.scheduleNextReconcile()\n }\n\n status() {\n const userOpCount = this.q.filter((t) => t.source === 'user-operation').length\n const fullReconcileCount = this.q.filter((t) => t.source === 'full-reconcile').length\n\n return {\n failed: this.failed,\n fullReconcileTasks: fullReconcileCount,\n lastError: this.lastError,\n lastSeedAt: this.lastSeedAt,\n processed: this.processed,\n processing: this.processing,\n queueSize: this.q.length,\n reconciling: this.reconciling,\n sampleKeys: Array.from(this.keys.keys()).slice(0, 50),\n userOperationTasks: userOpCount,\n }\n }\n}\n"],"names":["KEY","t","kind","baId","Queue","bootstrapState","adminHeaders","bootstrapPromise","isBootstrapped","deps","failed","keys","Map","lastError","lastSeedAt","processed","processing","q","reconcileEveryMs","reconcileTimeout","reconciling","tickTimer","opts","log","start","tickMs","runOnBoot","setTimeout","seedFullReconcile","catch","err","console","error","bumpFront","task","filter","clearFullReconcileTasks","beforeCount","length","fullReconcileCount","source","delete","afterCount","clearedFullReconcile","preservedUserOps","enqueue","priority","k","existing","get","baUser","unshift","push","set","listBAUsersPage","limit","offset","total","internalAdapter","countTotalUsers","users","listUsers","direction","field","runTask","attempts","syncUserToPayload","id","deleteUserFromPayload","scheduleNextReconcile","clearTimeout","unref","seedFullReconcilePaginated","reconcileId","pageSize","baIdSet","prunePayloadOrphans","Set","baOffset","baTotal","baUsers","u","enqueueEnsure","add","payloadPage","hasNextPage","nextPage","pUsers","listPayloadUsersPage","pu","ext","externalId","toString","has","enqueueDelete","page","tick","now","Date","idx","findIndex","nextAt","splice","e","message","String","delay","Math","min","pow","floor","random","user","getInstanceInfo","toISOString","substr","status","setInterval","userOpCount","fullReconcileTasks","queueSize","sampleKeys","Array","from","slice","userOperationTasks"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type SanitizedConfig } from 'payload';
|
|
2
|
+
export type BAUser = {
|
|
3
|
+
[k: string]: any;
|
|
4
|
+
email?: null | string;
|
|
5
|
+
id: string;
|
|
6
|
+
};
|
|
7
|
+
export type PayloadUser = {
|
|
8
|
+
externalId?: null | string;
|
|
9
|
+
id: number | string;
|
|
10
|
+
};
|
|
11
|
+
export interface BetterAuthUser {
|
|
12
|
+
[k: string]: any;
|
|
13
|
+
email?: null | string;
|
|
14
|
+
id: string;
|
|
15
|
+
name?: null | string;
|
|
16
|
+
}
|
|
17
|
+
/** Create a function to load Payload users page by page via Local API. */
|
|
18
|
+
export declare function createListPayloadUsersPage(config: Promise<SanitizedConfig>): (limit: number, page: number) => Promise<{
|
|
19
|
+
hasNextPage: boolean;
|
|
20
|
+
total: number;
|
|
21
|
+
users: PayloadUser[];
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Sync user from better-auth to Payload
|
|
25
|
+
* This is called from the better-auth hooks
|
|
26
|
+
* Creates a Payload user with externalId, which prevents reverse sync
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Create a function to sync user from better-auth to Payload
|
|
30
|
+
* This is called from the better-auth hooks
|
|
31
|
+
* Creates a Payload user with externalId, which prevents reverse sync
|
|
32
|
+
*/
|
|
33
|
+
export declare function createSyncUserToPayload(config: Promise<SanitizedConfig>): (betterAuthUser: BetterAuthUser) => Promise<void>;
|
|
34
|
+
export declare function createDeleteUserFromPayload(config: Promise<SanitizedConfig>): (betterAuthUserId: string) => Promise<void>;
|
|
35
|
+
export declare function createAttachExternalIdInPayload(config: Promise<SanitizedConfig>): (payloadUserId: number | string, baId: string) => Promise<void>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/sources.ts
|
|
2
|
+
import { getPayload } from 'payload';
|
|
3
|
+
import { signCanonical } from './crypto-shared.js';
|
|
4
|
+
const INTERNAL_SECRET = process.env.BA_TO_PAYLOAD_SECRET;
|
|
5
|
+
/** Create a function to load Payload users page by page via Local API. */ export function createListPayloadUsersPage(config) {
|
|
6
|
+
return async function listPayloadUsersPage(limit, page) {
|
|
7
|
+
const payload = await getPayload({
|
|
8
|
+
config
|
|
9
|
+
});
|
|
10
|
+
const res = await payload.find({
|
|
11
|
+
collection: 'users',
|
|
12
|
+
depth: 0,
|
|
13
|
+
limit,
|
|
14
|
+
overrideAccess: true,
|
|
15
|
+
page
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
hasNextPage: res.hasNextPage || false,
|
|
19
|
+
total: res.totalDocs || 0,
|
|
20
|
+
users: res.docs.map((d)=>({
|
|
21
|
+
id: d.id,
|
|
22
|
+
externalId: d.externalId
|
|
23
|
+
}))
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Better-auth is the single source of truth and manages users through database hooks
|
|
28
|
+
// These functions provide bidirectional validation and sync capabilities
|
|
29
|
+
/**
|
|
30
|
+
* Sync user from better-auth to Payload
|
|
31
|
+
* This is called from the better-auth hooks
|
|
32
|
+
* Creates a Payload user with externalId, which prevents reverse sync
|
|
33
|
+
*/ /**
|
|
34
|
+
* Create a function to sync user from better-auth to Payload
|
|
35
|
+
* This is called from the better-auth hooks
|
|
36
|
+
* Creates a Payload user with externalId, which prevents reverse sync
|
|
37
|
+
*/ export function createSyncUserToPayload(config) {
|
|
38
|
+
return async function syncUserToPayload(betterAuthUser) {
|
|
39
|
+
const payload = await getPayload({
|
|
40
|
+
config
|
|
41
|
+
});
|
|
42
|
+
// idempotency check (keep as-is)
|
|
43
|
+
const existing = await payload.find({
|
|
44
|
+
collection: 'users',
|
|
45
|
+
limit: 1,
|
|
46
|
+
where: {
|
|
47
|
+
externalId: {
|
|
48
|
+
equals: betterAuthUser.id
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (existing.docs.length) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const baBody = {
|
|
56
|
+
op: 'create',
|
|
57
|
+
userId: betterAuthUser.id
|
|
58
|
+
} // keep body minimal & stable
|
|
59
|
+
;
|
|
60
|
+
const baSig = signCanonical(baBody, INTERNAL_SECRET);
|
|
61
|
+
await payload.create({
|
|
62
|
+
collection: 'users',
|
|
63
|
+
context: {
|
|
64
|
+
baBody,
|
|
65
|
+
baSig
|
|
66
|
+
},
|
|
67
|
+
data: {
|
|
68
|
+
name: betterAuthUser.name ?? '',
|
|
69
|
+
externalId: betterAuthUser.id
|
|
70
|
+
},
|
|
71
|
+
overrideAccess: false
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Create a function to delete user from Payload
|
|
76
|
+
export function createDeleteUserFromPayload(config) {
|
|
77
|
+
return async function deleteUserFromPayload(betterAuthUserId) {
|
|
78
|
+
const payload = await getPayload({
|
|
79
|
+
config
|
|
80
|
+
});
|
|
81
|
+
const existing = await payload.find({
|
|
82
|
+
collection: 'users',
|
|
83
|
+
limit: 1,
|
|
84
|
+
where: {
|
|
85
|
+
externalId: {
|
|
86
|
+
equals: betterAuthUserId
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
if (!existing.docs.length) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const baBody = {
|
|
94
|
+
op: 'delete',
|
|
95
|
+
userId: betterAuthUserId
|
|
96
|
+
};
|
|
97
|
+
const baSig = signCanonical(baBody, INTERNAL_SECRET);
|
|
98
|
+
await payload.delete({
|
|
99
|
+
id: existing.docs[0].id,
|
|
100
|
+
collection: 'users',
|
|
101
|
+
context: {
|
|
102
|
+
baBody,
|
|
103
|
+
baSig
|
|
104
|
+
},
|
|
105
|
+
overrideAccess: false
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// ——— Optional: link an existing Payload user (id-matched) to BA id
|
|
110
|
+
export function createAttachExternalIdInPayload(config) {
|
|
111
|
+
return async function attachExternalIdInPayload(payloadUserId, baId) {
|
|
112
|
+
const payload = await getPayload({
|
|
113
|
+
config
|
|
114
|
+
});
|
|
115
|
+
await payload.update({
|
|
116
|
+
id: payloadUserId,
|
|
117
|
+
collection: 'users',
|
|
118
|
+
data: {
|
|
119
|
+
externalId: baId
|
|
120
|
+
},
|
|
121
|
+
overrideAccess: true
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/better-auth/sources.ts"],"sourcesContent":["// src/sources.ts\nimport { getPayload, type SanitizedConfig } from 'payload'\n\nimport { signCanonical } from './crypto-shared.js'\n\nconst INTERNAL_SECRET = process.env.BA_TO_PAYLOAD_SECRET!\n\nexport type BAUser = { [k: string]: any; email?: null | string; id: string }\nexport type PayloadUser = { externalId?: null | string; id: number | string }\n\n// Better Auth user type for sync operations\nexport interface BetterAuthUser {\n [k: string]: any\n email?: null | string\n id: string\n name?: null | string\n}\n\n/** Create a function to load Payload users page by page via Local API. */\nexport function createListPayloadUsersPage(config: Promise<SanitizedConfig>) {\n return async function listPayloadUsersPage(\n limit: number,\n page: number,\n ): Promise<{ hasNextPage: boolean; total: number; users: PayloadUser[] }> {\n const payload = await getPayload({ config })\n const res = await payload.find({\n collection: 'users',\n depth: 0,\n limit,\n overrideAccess: true,\n page,\n })\n return {\n hasNextPage: res.hasNextPage || false,\n total: res.totalDocs || 0,\n users: res.docs.map((d: any) => ({\n id: d.id,\n externalId: d.externalId,\n })),\n }\n }\n}\n\n// Better-auth is the single source of truth and manages users through database hooks\n// These functions provide bidirectional validation and sync capabilities\n/**\n * Sync user from better-auth to Payload\n * This is called from the better-auth hooks\n * Creates a Payload user with externalId, which prevents reverse sync\n */\n\n/**\n * Create a function to sync user from better-auth to Payload\n * This is called from the better-auth hooks\n * Creates a Payload user with externalId, which prevents reverse sync\n */\nexport function createSyncUserToPayload(config: Promise<SanitizedConfig>) {\n return async function syncUserToPayload(betterAuthUser: BetterAuthUser) {\n const payload = await getPayload({ config })\n\n // idempotency check (keep as-is)\n const existing = await payload.find({\n collection: 'users',\n limit: 1,\n where: { externalId: { equals: betterAuthUser.id } },\n })\n if (existing.docs.length) {\n return\n }\n\n const baBody = { op: 'create', userId: betterAuthUser.id } // keep body minimal & stable\n const baSig = signCanonical(baBody, INTERNAL_SECRET)\n\n await payload.create({\n collection: 'users',\n context: { baBody, baSig },\n data: {\n name: betterAuthUser.name ?? '',\n externalId: betterAuthUser.id,\n },\n overrideAccess: false,\n })\n }\n}\n\n// Create a function to delete user from Payload\nexport function createDeleteUserFromPayload(config: Promise<SanitizedConfig>) {\n return async function deleteUserFromPayload(betterAuthUserId: string) {\n const payload = await getPayload({ config })\n\n const existing = await payload.find({\n collection: 'users',\n limit: 1,\n where: { externalId: { equals: betterAuthUserId } },\n })\n if (!existing.docs.length) {\n return\n }\n\n const baBody = { op: 'delete', userId: betterAuthUserId }\n const baSig = signCanonical(baBody, INTERNAL_SECRET)\n\n await payload.delete({\n id: existing.docs[0].id,\n collection: 'users',\n context: { baBody, baSig },\n overrideAccess: false,\n })\n }\n}\n\n// ——— Optional: link an existing Payload user (id-matched) to BA id\nexport function createAttachExternalIdInPayload(config: Promise<SanitizedConfig>) {\n return async function attachExternalIdInPayload(payloadUserId: number | string, baId: string) {\n const payload = await getPayload({ config })\n await payload.update({\n id: payloadUserId,\n collection: 'users',\n data: { externalId: baId },\n overrideAccess: true,\n })\n }\n}\n"],"names":["getPayload","signCanonical","INTERNAL_SECRET","process","env","BA_TO_PAYLOAD_SECRET","createListPayloadUsersPage","config","listPayloadUsersPage","limit","page","payload","res","find","collection","depth","overrideAccess","hasNextPage","total","totalDocs","users","docs","map","d","id","externalId","createSyncUserToPayload","syncUserToPayload","betterAuthUser","existing","where","equals","length","baBody","op","userId","baSig","create","context","data","name","createDeleteUserFromPayload","deleteUserFromPayload","betterAuthUserId","delete","createAttachExternalIdInPayload","attachExternalIdInPayload","payloadUserId","baId","update"],"mappings":"AAAA,iBAAiB;AACjB,SAASA,UAAU,QAA8B,UAAS;AAE1D,SAASC,aAAa,QAAQ,qBAAoB;AAElD,MAAMC,kBAAkBC,QAAQC,GAAG,CAACC,oBAAoB;AAaxD,wEAAwE,GACxE,OAAO,SAASC,2BAA2BC,MAAgC;IACzE,OAAO,eAAeC,qBACpBC,KAAa,EACbC,IAAY;QAEZ,MAAMC,UAAU,MAAMX,WAAW;YAAEO;QAAO;QAC1C,MAAMK,MAAM,MAAMD,QAAQE,IAAI,CAAC;YAC7BC,YAAY;YACZC,OAAO;YACPN;YACAO,gBAAgB;YAChBN;QACF;QACA,OAAO;YACLO,aAAaL,IAAIK,WAAW,IAAI;YAChCC,OAAON,IAAIO,SAAS,IAAI;YACxBC,OAAOR,IAAIS,IAAI,CAACC,GAAG,CAAC,CAACC,IAAY,CAAA;oBAC/BC,IAAID,EAAEC,EAAE;oBACRC,YAAYF,EAAEE,UAAU;gBAC1B,CAAA;QACF;IACF;AACF;AAEA,qFAAqF;AACrF,yEAAyE;AACzE;;;;CAIC,GAED;;;;CAIC,GACD,OAAO,SAASC,wBAAwBnB,MAAgC;IACtE,OAAO,eAAeoB,kBAAkBC,cAA8B;QACpE,MAAMjB,UAAU,MAAMX,WAAW;YAAEO;QAAO;QAE1C,iCAAiC;QACjC,MAAMsB,WAAW,MAAMlB,QAAQE,IAAI,CAAC;YAClCC,YAAY;YACZL,OAAO;YACPqB,OAAO;gBAAEL,YAAY;oBAAEM,QAAQH,eAAeJ,EAAE;gBAAC;YAAE;QACrD;QACA,IAAIK,SAASR,IAAI,CAACW,MAAM,EAAE;YACxB;QACF;QAEA,MAAMC,SAAS;YAAEC,IAAI;YAAUC,QAAQP,eAAeJ,EAAE;QAAC,EAAE,6BAA6B;;QACxF,MAAMY,QAAQnC,cAAcgC,QAAQ/B;QAEpC,MAAMS,QAAQ0B,MAAM,CAAC;YACnBvB,YAAY;YACZwB,SAAS;gBAAEL;gBAAQG;YAAM;YACzBG,MAAM;gBACJC,MAAMZ,eAAeY,IAAI,IAAI;gBAC7Bf,YAAYG,eAAeJ,EAAE;YAC/B;YACAR,gBAAgB;QAClB;IACF;AACF;AAEA,gDAAgD;AAChD,OAAO,SAASyB,4BAA4BlC,MAAgC;IAC1E,OAAO,eAAemC,sBAAsBC,gBAAwB;QAClE,MAAMhC,UAAU,MAAMX,WAAW;YAAEO;QAAO;QAE1C,MAAMsB,WAAW,MAAMlB,QAAQE,IAAI,CAAC;YAClCC,YAAY;YACZL,OAAO;YACPqB,OAAO;gBAAEL,YAAY;oBAAEM,QAAQY;gBAAiB;YAAE;QACpD;QACA,IAAI,CAACd,SAASR,IAAI,CAACW,MAAM,EAAE;YACzB;QACF;QAEA,MAAMC,SAAS;YAAEC,IAAI;YAAUC,QAAQQ;QAAiB;QACxD,MAAMP,QAAQnC,cAAcgC,QAAQ/B;QAEpC,MAAMS,QAAQiC,MAAM,CAAC;YACnBpB,IAAIK,SAASR,IAAI,CAAC,EAAE,CAACG,EAAE;YACvBV,YAAY;YACZwB,SAAS;gBAAEL;gBAAQG;YAAM;YACzBpB,gBAAgB;QAClB;IACF;AACF;AAEA,oEAAoE;AACpE,OAAO,SAAS6B,gCAAgCtC,MAAgC;IAC9E,OAAO,eAAeuC,0BAA0BC,aAA8B,EAAEC,IAAY;QAC1F,MAAMrC,UAAU,MAAMX,WAAW;YAAEO;QAAO;QAC1C,MAAMI,QAAQsC,MAAM,CAAC;YACnBzB,IAAIuB;YACJjC,YAAY;YACZyB,MAAM;gBAAEd,YAAYuB;YAAK;YACzBhC,gBAAgB;QAClB;IACF;AACF"}
|