@hiai-gg/docsmint 0.3.2 → 0.3.4
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/backend/src/lib/lifecycle-service.ts +574 -0
- package/dist/frontend/{SettingsDialog-DiAyhzw0.js → SettingsDialog-D4pgFD0D.js} +444 -410
- package/dist/frontend/{ShareDialog-DaZf1raY.js → ShareDialog-Dzh7rV7J.js} +1 -1
- package/dist/frontend/api/categories.d.ts +13 -9
- package/dist/frontend/api/documents.d.ts +14 -8
- package/dist/frontend/api/folders.d.ts +13 -9
- package/dist/frontend/api/settings.d.ts +9 -7
- package/dist/frontend/api/tags.d.ts +14 -10
- package/dist/frontend/components/settings.js +1 -1
- package/dist/frontend/components/sidebar.js +2 -2
- package/dist/frontend/dashboard.js +1 -1
- package/dist/frontend/extension.d.ts +35 -2
- package/dist/frontend/frontend.css +1 -1
- package/dist/frontend/search-preferences-B1GdbdUc.js +17 -0
- package/dist/frontend/search.js +94 -90
- package/dist/frontend/shared-document.d.ts +14 -1
- package/package.json +7 -1
- package/packages/cli/src/index.ts +1 -1
- package/packages/mcp-server/src/index.ts +1 -1
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachments,
|
|
3
|
+
auditLog,
|
|
4
|
+
db,
|
|
5
|
+
documentEmbeddings,
|
|
6
|
+
documents,
|
|
7
|
+
lifecycleOperations,
|
|
8
|
+
shareLinks,
|
|
9
|
+
versions,
|
|
10
|
+
} from "@hiai-docs/db";
|
|
11
|
+
import type {
|
|
12
|
+
AssertPurgeAllowed,
|
|
13
|
+
ExportUserDataContext,
|
|
14
|
+
LifecycleHostStep,
|
|
15
|
+
PurgeUserDataContext,
|
|
16
|
+
PurgeUserDataResult,
|
|
17
|
+
UserDataExportRecord,
|
|
18
|
+
} from "@hiai-docs/sdk";
|
|
19
|
+
import { and, eq, inArray, lt, or, sql } from "drizzle-orm";
|
|
20
|
+
|
|
21
|
+
const LEASE_MS = 60_000;
|
|
22
|
+
const SAFE_ERROR_CODES = new Set([
|
|
23
|
+
"aborted",
|
|
24
|
+
"fence_rejected",
|
|
25
|
+
"lease_lost",
|
|
26
|
+
"object_storage_failed",
|
|
27
|
+
"queue_failed",
|
|
28
|
+
"redis_failed",
|
|
29
|
+
"graph_failed",
|
|
30
|
+
"host_step_failed",
|
|
31
|
+
"persistence_failed",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
type PersistentOperation = typeof lifecycleOperations.$inferSelect;
|
|
35
|
+
|
|
36
|
+
export type LifecycleRuntimeAdapters = Readonly<{
|
|
37
|
+
/** Checks the host-owned fence immediately before the first mutation. */
|
|
38
|
+
verifyPurgeFence: (
|
|
39
|
+
ctx: PurgeUserDataContext,
|
|
40
|
+
fenceToken: string,
|
|
41
|
+
) => Promise<void>;
|
|
42
|
+
deleteObjects: (
|
|
43
|
+
keys: readonly string[],
|
|
44
|
+
signal?: AbortSignal,
|
|
45
|
+
) => Promise<number>;
|
|
46
|
+
cancelAccountJobs: (
|
|
47
|
+
actorUserId: string,
|
|
48
|
+
signal?: AbortSignal,
|
|
49
|
+
) => Promise<number>;
|
|
50
|
+
clearAccountRedisState: (
|
|
51
|
+
actorUserId: string,
|
|
52
|
+
signal?: AbortSignal,
|
|
53
|
+
) => Promise<number>;
|
|
54
|
+
removeCollaborationState: (
|
|
55
|
+
actorUserId: string,
|
|
56
|
+
signal?: AbortSignal,
|
|
57
|
+
) => Promise<number>;
|
|
58
|
+
removeGraphState: (
|
|
59
|
+
documentIds: readonly string[],
|
|
60
|
+
signal?: AbortSignal,
|
|
61
|
+
) => Promise<number>;
|
|
62
|
+
}>;
|
|
63
|
+
|
|
64
|
+
export type PersistentLifecycleService = Readonly<{
|
|
65
|
+
exportUserData(
|
|
66
|
+
ctx: ExportUserDataContext,
|
|
67
|
+
): AsyncIterable<UserDataExportRecord>;
|
|
68
|
+
purgeUserData(
|
|
69
|
+
ctx: PurgeUserDataContext,
|
|
70
|
+
gate: Readonly<{ fenceToken: string }>,
|
|
71
|
+
): Promise<PurgeUserDataResult>;
|
|
72
|
+
}>;
|
|
73
|
+
|
|
74
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
75
|
+
if (signal?.aborted)
|
|
76
|
+
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function safeErrorCode(error: unknown): string {
|
|
80
|
+
if (error instanceof DOMException && error.name === "AbortError")
|
|
81
|
+
return "aborted";
|
|
82
|
+
if (error instanceof Error && SAFE_ERROR_CODES.has(error.message))
|
|
83
|
+
return error.message;
|
|
84
|
+
return "persistence_failed";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tokenHash(token: string): string {
|
|
88
|
+
return new Bun.CryptoHasher("sha256").update(token).digest("hex");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function checksumLine(
|
|
92
|
+
hash: Bun.CryptoHasher,
|
|
93
|
+
record: UserDataExportRecord,
|
|
94
|
+
): void {
|
|
95
|
+
hash.update(`${JSON.stringify(record)}\n`, "utf8");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function counts(value: unknown): Record<string, number> {
|
|
99
|
+
return typeof value === "object" && value !== null
|
|
100
|
+
? (value as Record<string, number>)
|
|
101
|
+
: {};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function operationCounts(
|
|
105
|
+
operation: PersistentOperation,
|
|
106
|
+
): Record<string, number> {
|
|
107
|
+
const result = operation.terminalResult as {
|
|
108
|
+
deletedByDomain?: unknown;
|
|
109
|
+
} | null;
|
|
110
|
+
return counts(result?.deletedByDomain);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Durable OSS-owned lifecycle saga. It does not know workspace membership or
|
|
115
|
+
* billing policy: the caller supplies a host fence and optional host steps.
|
|
116
|
+
*/
|
|
117
|
+
export function createPersistentLifecycleService(
|
|
118
|
+
runtime: LifecycleRuntimeAdapters,
|
|
119
|
+
hostSteps: readonly LifecycleHostStep[] = [],
|
|
120
|
+
): PersistentLifecycleService {
|
|
121
|
+
const orderedSteps = [...hostSteps].sort(
|
|
122
|
+
(a, b) => a.order - b.order || a.id.localeCompare(b.id),
|
|
123
|
+
);
|
|
124
|
+
if (
|
|
125
|
+
new Set(orderedSteps.map((step) => step.id)).size !== orderedSteps.length
|
|
126
|
+
) {
|
|
127
|
+
throw new Error("Lifecycle host step IDs must be globally unique");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function getOrCreateOperation(
|
|
131
|
+
ctx: PurgeUserDataContext,
|
|
132
|
+
): Promise<PersistentOperation> {
|
|
133
|
+
await db
|
|
134
|
+
.insert(lifecycleOperations)
|
|
135
|
+
.values({
|
|
136
|
+
actorUserId: ctx.actorUserId,
|
|
137
|
+
idempotencyKey: ctx.idempotencyKey,
|
|
138
|
+
operationKind: "purge",
|
|
139
|
+
status: "pending",
|
|
140
|
+
})
|
|
141
|
+
.onConflictDoNothing();
|
|
142
|
+
const operation = await db.query.lifecycleOperations.findFirst({
|
|
143
|
+
where: and(
|
|
144
|
+
eq(lifecycleOperations.actorUserId, ctx.actorUserId),
|
|
145
|
+
eq(lifecycleOperations.idempotencyKey, ctx.idempotencyKey),
|
|
146
|
+
),
|
|
147
|
+
});
|
|
148
|
+
if (!operation) throw new Error("persistence_failed");
|
|
149
|
+
if (operation.operationKind !== "purge")
|
|
150
|
+
throw new Error("persistence_failed");
|
|
151
|
+
return operation;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function acquireLease(
|
|
155
|
+
operation: PersistentOperation,
|
|
156
|
+
owner: string,
|
|
157
|
+
): Promise<boolean> {
|
|
158
|
+
const now = new Date();
|
|
159
|
+
const expiry = new Date(now.getTime() + LEASE_MS);
|
|
160
|
+
const updated = await db
|
|
161
|
+
.update(lifecycleOperations)
|
|
162
|
+
.set({
|
|
163
|
+
status: "running",
|
|
164
|
+
leaseOwner: owner,
|
|
165
|
+
leaseExpiresAt: expiry,
|
|
166
|
+
attemptCount: sql`${lifecycleOperations.attemptCount} + 1`,
|
|
167
|
+
updatedAt: now,
|
|
168
|
+
})
|
|
169
|
+
.where(
|
|
170
|
+
and(
|
|
171
|
+
eq(lifecycleOperations.id, operation.id),
|
|
172
|
+
or(
|
|
173
|
+
eq(lifecycleOperations.status, "pending"),
|
|
174
|
+
eq(lifecycleOperations.status, "retryable"),
|
|
175
|
+
and(
|
|
176
|
+
eq(lifecycleOperations.status, "running"),
|
|
177
|
+
lt(lifecycleOperations.leaseExpiresAt, now),
|
|
178
|
+
),
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
.returning({ id: lifecycleOperations.id });
|
|
183
|
+
return updated.length === 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function persistStep(
|
|
187
|
+
operationId: string,
|
|
188
|
+
leaseOwner: string,
|
|
189
|
+
step: string,
|
|
190
|
+
deletedByDomain: Record<string, number>,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
const current = await db.query.lifecycleOperations.findFirst({
|
|
193
|
+
where: and(
|
|
194
|
+
eq(lifecycleOperations.id, operationId),
|
|
195
|
+
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
196
|
+
),
|
|
197
|
+
});
|
|
198
|
+
if (!current) throw new Error("lease_lost");
|
|
199
|
+
if (
|
|
200
|
+
current.status !== "running" ||
|
|
201
|
+
(current.leaseExpiresAt && current.leaseExpiresAt < new Date())
|
|
202
|
+
) {
|
|
203
|
+
throw new Error("lease_lost");
|
|
204
|
+
}
|
|
205
|
+
const completed = Array.isArray(current.completedSteps)
|
|
206
|
+
? current.completedSteps.filter(
|
|
207
|
+
(value): value is string => typeof value === "string",
|
|
208
|
+
)
|
|
209
|
+
: [];
|
|
210
|
+
if (!completed.includes(step)) completed.push(step);
|
|
211
|
+
await db
|
|
212
|
+
.update(lifecycleOperations)
|
|
213
|
+
.set({
|
|
214
|
+
completedSteps: completed,
|
|
215
|
+
terminalResult: { deletedByDomain },
|
|
216
|
+
updatedAt: new Date(),
|
|
217
|
+
})
|
|
218
|
+
.where(
|
|
219
|
+
and(
|
|
220
|
+
eq(lifecycleOperations.id, operationId),
|
|
221
|
+
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function runStep(
|
|
227
|
+
operation: PersistentOperation,
|
|
228
|
+
leaseOwner: string,
|
|
229
|
+
step: string,
|
|
230
|
+
deletedByDomain: Record<string, number>,
|
|
231
|
+
action: () => Promise<number>,
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
const completed = Array.isArray(operation.completedSteps)
|
|
234
|
+
? operation.completedSteps
|
|
235
|
+
: [];
|
|
236
|
+
if (completed.includes(step)) return;
|
|
237
|
+
const count = await action();
|
|
238
|
+
deletedByDomain[step] = count;
|
|
239
|
+
await persistStep(operation.id, leaseOwner, step, deletedByDomain);
|
|
240
|
+
operation.completedSteps = [...completed, step];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
async *exportUserData(ctx) {
|
|
245
|
+
throwIfAborted(ctx.signal);
|
|
246
|
+
const exportId = crypto.randomUUID();
|
|
247
|
+
const hash = new Bun.CryptoHasher("sha256");
|
|
248
|
+
let recordCount = 0;
|
|
249
|
+
const manifest: UserDataExportRecord = {
|
|
250
|
+
type: "manifest",
|
|
251
|
+
schemaVersion: 1,
|
|
252
|
+
exportId,
|
|
253
|
+
actorUserId: ctx.actorUserId,
|
|
254
|
+
generatedAt: new Date().toISOString(),
|
|
255
|
+
};
|
|
256
|
+
checksumLine(hash, manifest);
|
|
257
|
+
recordCount += 1;
|
|
258
|
+
yield manifest;
|
|
259
|
+
const ownedDocuments = await db
|
|
260
|
+
.select()
|
|
261
|
+
.from(documents)
|
|
262
|
+
.where(eq(documents.ownerId, ctx.actorUserId));
|
|
263
|
+
for (const document of ownedDocuments) {
|
|
264
|
+
throwIfAborted(ctx.signal);
|
|
265
|
+
const record: UserDataExportRecord = {
|
|
266
|
+
type: "data",
|
|
267
|
+
domain: "documents",
|
|
268
|
+
resourceType: "document",
|
|
269
|
+
resourceId: document.id,
|
|
270
|
+
workspaceId: document.workspaceId,
|
|
271
|
+
payload: {
|
|
272
|
+
title: document.title,
|
|
273
|
+
content: document.content,
|
|
274
|
+
contentJson: document.contentJson,
|
|
275
|
+
metadata: document.metadata,
|
|
276
|
+
visibility: document.visibility,
|
|
277
|
+
createdAt: document.createdAt,
|
|
278
|
+
updatedAt: document.updatedAt,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
checksumLine(hash, record);
|
|
282
|
+
recordCount += 1;
|
|
283
|
+
yield record;
|
|
284
|
+
}
|
|
285
|
+
const ownedAttachments = await db
|
|
286
|
+
.select({
|
|
287
|
+
id: attachments.id,
|
|
288
|
+
workspaceId: attachments.workspaceId,
|
|
289
|
+
filename: attachments.filename,
|
|
290
|
+
mimeType: attachments.mimeType,
|
|
291
|
+
size: attachments.size,
|
|
292
|
+
})
|
|
293
|
+
.from(attachments)
|
|
294
|
+
.innerJoin(documents, eq(attachments.documentId, documents.id))
|
|
295
|
+
.where(eq(documents.ownerId, ctx.actorUserId));
|
|
296
|
+
for (const attachment of ownedAttachments) {
|
|
297
|
+
throwIfAborted(ctx.signal);
|
|
298
|
+
const record: UserDataExportRecord = {
|
|
299
|
+
type: "attachment",
|
|
300
|
+
attachmentId: attachment.id,
|
|
301
|
+
workspaceId: attachment.workspaceId,
|
|
302
|
+
filename: attachment.filename,
|
|
303
|
+
contentType: attachment.mimeType,
|
|
304
|
+
size: attachment.size,
|
|
305
|
+
sha256: null,
|
|
306
|
+
};
|
|
307
|
+
checksumLine(hash, record);
|
|
308
|
+
recordCount += 1;
|
|
309
|
+
yield record;
|
|
310
|
+
}
|
|
311
|
+
for (const step of orderedSteps) {
|
|
312
|
+
if (!step.export) continue;
|
|
313
|
+
for await (const record of step.export(ctx)) {
|
|
314
|
+
throwIfAborted(ctx.signal);
|
|
315
|
+
if (record.type === "manifest" || record.type === "complete")
|
|
316
|
+
throw new Error("Host export step emitted a reserved record type");
|
|
317
|
+
checksumLine(hash, record);
|
|
318
|
+
recordCount += 1;
|
|
319
|
+
yield record;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
yield { type: "complete", recordCount, checksum: hash.digest("hex") };
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
async purgeUserData(ctx, gate) {
|
|
326
|
+
throwIfAborted(ctx.signal);
|
|
327
|
+
let operation = await getOrCreateOperation(ctx);
|
|
328
|
+
if (operation.status === "completed") {
|
|
329
|
+
return {
|
|
330
|
+
status: "already_completed",
|
|
331
|
+
operationId: operation.id,
|
|
332
|
+
deletedByDomain: operationCounts(operation),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const leaseOwner = crypto.randomUUID();
|
|
336
|
+
if (!(await acquireLease(operation, leaseOwner)))
|
|
337
|
+
throw new Error("lease_lost");
|
|
338
|
+
operation =
|
|
339
|
+
(await db.query.lifecycleOperations.findFirst({
|
|
340
|
+
where: eq(lifecycleOperations.id, operation.id),
|
|
341
|
+
})) ?? operation;
|
|
342
|
+
const deletedByDomain = operationCounts(operation);
|
|
343
|
+
try {
|
|
344
|
+
throwIfAborted(ctx.signal);
|
|
345
|
+
await db
|
|
346
|
+
.update(lifecycleOperations)
|
|
347
|
+
.set({
|
|
348
|
+
fenceTokenHash: tokenHash(gate.fenceToken),
|
|
349
|
+
updatedAt: new Date(),
|
|
350
|
+
})
|
|
351
|
+
.where(
|
|
352
|
+
and(
|
|
353
|
+
eq(lifecycleOperations.id, operation.id),
|
|
354
|
+
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
await runtime.verifyPurgeFence(ctx, gate.fenceToken);
|
|
358
|
+
|
|
359
|
+
await runStep(
|
|
360
|
+
operation,
|
|
361
|
+
leaseOwner,
|
|
362
|
+
"cancel_account_jobs",
|
|
363
|
+
deletedByDomain,
|
|
364
|
+
() => runtime.cancelAccountJobs(ctx.actorUserId, ctx.signal),
|
|
365
|
+
);
|
|
366
|
+
await runStep(
|
|
367
|
+
operation,
|
|
368
|
+
leaseOwner,
|
|
369
|
+
"remove_collaboration_state",
|
|
370
|
+
deletedByDomain,
|
|
371
|
+
() => runtime.removeCollaborationState(ctx.actorUserId, ctx.signal),
|
|
372
|
+
);
|
|
373
|
+
await runStep(
|
|
374
|
+
operation,
|
|
375
|
+
leaseOwner,
|
|
376
|
+
"remove_subject_created_shares",
|
|
377
|
+
deletedByDomain,
|
|
378
|
+
async () =>
|
|
379
|
+
(
|
|
380
|
+
await db
|
|
381
|
+
.delete(shareLinks)
|
|
382
|
+
.where(eq(shareLinks.createdBy, ctx.actorUserId))
|
|
383
|
+
.returning({ id: shareLinks.id })
|
|
384
|
+
).length,
|
|
385
|
+
);
|
|
386
|
+
const owned = await db
|
|
387
|
+
.select({ id: documents.id })
|
|
388
|
+
.from(documents)
|
|
389
|
+
.where(eq(documents.ownerId, ctx.actorUserId));
|
|
390
|
+
const documentIds = owned.map((row) => row.id);
|
|
391
|
+
await runStep(
|
|
392
|
+
operation,
|
|
393
|
+
leaseOwner,
|
|
394
|
+
"remove_document_versions",
|
|
395
|
+
deletedByDomain,
|
|
396
|
+
async () =>
|
|
397
|
+
documentIds.length
|
|
398
|
+
? (
|
|
399
|
+
await db
|
|
400
|
+
.delete(versions)
|
|
401
|
+
.where(inArray(versions.documentId, documentIds))
|
|
402
|
+
.returning({ id: versions.id })
|
|
403
|
+
).length
|
|
404
|
+
: 0,
|
|
405
|
+
);
|
|
406
|
+
await runStep(
|
|
407
|
+
operation,
|
|
408
|
+
leaseOwner,
|
|
409
|
+
"remove_chunks_and_embeddings",
|
|
410
|
+
deletedByDomain,
|
|
411
|
+
async () =>
|
|
412
|
+
documentIds.length
|
|
413
|
+
? (
|
|
414
|
+
await db
|
|
415
|
+
.delete(documentEmbeddings)
|
|
416
|
+
.where(inArray(documentEmbeddings.documentId, documentIds))
|
|
417
|
+
.returning({ id: documentEmbeddings.id })
|
|
418
|
+
).length
|
|
419
|
+
: 0,
|
|
420
|
+
);
|
|
421
|
+
await runStep(
|
|
422
|
+
operation,
|
|
423
|
+
leaseOwner,
|
|
424
|
+
"remove_graph_state",
|
|
425
|
+
deletedByDomain,
|
|
426
|
+
() => runtime.removeGraphState(documentIds, ctx.signal),
|
|
427
|
+
);
|
|
428
|
+
const objectRows = documentIds.length
|
|
429
|
+
? await db
|
|
430
|
+
.select({
|
|
431
|
+
id: attachments.id,
|
|
432
|
+
storageKey: attachments.storageKey,
|
|
433
|
+
})
|
|
434
|
+
.from(attachments)
|
|
435
|
+
.where(inArray(attachments.documentId, documentIds))
|
|
436
|
+
: [];
|
|
437
|
+
await runStep(
|
|
438
|
+
operation,
|
|
439
|
+
leaseOwner,
|
|
440
|
+
"delete_attachment_objects",
|
|
441
|
+
deletedByDomain,
|
|
442
|
+
() =>
|
|
443
|
+
runtime.deleteObjects(
|
|
444
|
+
objectRows.map((row) => row.storageKey),
|
|
445
|
+
ctx.signal,
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
await runStep(
|
|
449
|
+
operation,
|
|
450
|
+
leaseOwner,
|
|
451
|
+
"remove_attachment_rows",
|
|
452
|
+
deletedByDomain,
|
|
453
|
+
async () =>
|
|
454
|
+
objectRows.length
|
|
455
|
+
? (
|
|
456
|
+
await db
|
|
457
|
+
.delete(attachments)
|
|
458
|
+
.where(
|
|
459
|
+
inArray(
|
|
460
|
+
attachments.id,
|
|
461
|
+
objectRows.map((row) => row.id),
|
|
462
|
+
),
|
|
463
|
+
)
|
|
464
|
+
.returning({ id: attachments.id })
|
|
465
|
+
).length
|
|
466
|
+
: 0,
|
|
467
|
+
);
|
|
468
|
+
await runStep(
|
|
469
|
+
operation,
|
|
470
|
+
leaseOwner,
|
|
471
|
+
"remove_subject_documents",
|
|
472
|
+
deletedByDomain,
|
|
473
|
+
async () =>
|
|
474
|
+
(
|
|
475
|
+
await db
|
|
476
|
+
.delete(documents)
|
|
477
|
+
.where(eq(documents.ownerId, ctx.actorUserId))
|
|
478
|
+
.returning({ id: documents.id })
|
|
479
|
+
).length,
|
|
480
|
+
);
|
|
481
|
+
await runStep(
|
|
482
|
+
operation,
|
|
483
|
+
leaseOwner,
|
|
484
|
+
"clear_redis_state",
|
|
485
|
+
deletedByDomain,
|
|
486
|
+
() => runtime.clearAccountRedisState(ctx.actorUserId, ctx.signal),
|
|
487
|
+
);
|
|
488
|
+
for (const step of orderedSteps) {
|
|
489
|
+
if (!step.purge) continue;
|
|
490
|
+
await runStep(
|
|
491
|
+
operation,
|
|
492
|
+
leaseOwner,
|
|
493
|
+
`host:${step.id}`,
|
|
494
|
+
deletedByDomain,
|
|
495
|
+
async () => (await step.purge?.(ctx))?.deletedCount ?? 0,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
await runStep(
|
|
499
|
+
operation,
|
|
500
|
+
leaseOwner,
|
|
501
|
+
"write_deletion_audit",
|
|
502
|
+
deletedByDomain,
|
|
503
|
+
async () => {
|
|
504
|
+
await db.insert(auditLog).values({
|
|
505
|
+
actorId: ctx.actorUserId,
|
|
506
|
+
action: "account_data_purged",
|
|
507
|
+
resourceType: "lifecycle_operation",
|
|
508
|
+
details: {
|
|
509
|
+
operationId: operation.id,
|
|
510
|
+
outcome: "completed",
|
|
511
|
+
deletedByDomain,
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
return 1;
|
|
515
|
+
},
|
|
516
|
+
);
|
|
517
|
+
await db
|
|
518
|
+
.update(lifecycleOperations)
|
|
519
|
+
.set({
|
|
520
|
+
status: "completed",
|
|
521
|
+
terminalResult: { deletedByDomain },
|
|
522
|
+
completedAt: new Date(),
|
|
523
|
+
leaseOwner: null,
|
|
524
|
+
leaseExpiresAt: null,
|
|
525
|
+
updatedAt: new Date(),
|
|
526
|
+
})
|
|
527
|
+
.where(
|
|
528
|
+
and(
|
|
529
|
+
eq(lifecycleOperations.id, operation.id),
|
|
530
|
+
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
531
|
+
),
|
|
532
|
+
);
|
|
533
|
+
return {
|
|
534
|
+
status: "completed",
|
|
535
|
+
operationId: operation.id,
|
|
536
|
+
deletedByDomain,
|
|
537
|
+
};
|
|
538
|
+
} catch (error) {
|
|
539
|
+
const code = safeErrorCode(error);
|
|
540
|
+
await db
|
|
541
|
+
.update(lifecycleOperations)
|
|
542
|
+
.set({
|
|
543
|
+
status: code === "fence_rejected" ? "rejected" : "retryable",
|
|
544
|
+
safeErrorCode: code,
|
|
545
|
+
leaseOwner: null,
|
|
546
|
+
leaseExpiresAt: null,
|
|
547
|
+
completedAt: code === "fence_rejected" ? new Date() : null,
|
|
548
|
+
updatedAt: new Date(),
|
|
549
|
+
})
|
|
550
|
+
.where(
|
|
551
|
+
and(
|
|
552
|
+
eq(lifecycleOperations.id, operation.id),
|
|
553
|
+
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Builds the public facade without allowing the OSS service to own membership policy. */
|
|
563
|
+
export function bindPersistentLifecycle(
|
|
564
|
+
service: PersistentLifecycleService,
|
|
565
|
+
assertPurgeAllowed: AssertPurgeAllowed,
|
|
566
|
+
) {
|
|
567
|
+
return {
|
|
568
|
+
exportUserData: service.exportUserData,
|
|
569
|
+
async purgeUserData(ctx: PurgeUserDataContext) {
|
|
570
|
+
const gate = await assertPurgeAllowed(ctx);
|
|
571
|
+
return service.purgeUserData(ctx, gate);
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
}
|