@hiai-gg/docsmint 0.3.4 → 0.3.6
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 +334 -219
- package/dist/backend/index.js +214234 -0
- package/dist/backend-launcher.d.ts +55 -0
- package/dist/backend-launcher.js +136 -0
- package/dist/frontend/extension.d.ts +2 -1
- package/dist/frontend/shared-document.js +89 -54
- package/dist/lifecycle-persistent.d.ts +31 -0
- package/dist/lifecycle-persistent.js +33 -0
- package/dist/server-only-browser-entry.d.ts +0 -0
- package/dist/server-only-browser-entry.js +3 -0
- package/dist/storage-quota.d.ts +92 -0
- package/dist/storage-quota.js +104 -0
- package/dist/types.d.ts +7 -0
- package/dist/workspace.d.ts +4 -1
- package/dist/workspace.js +29 -8
- package/package.json +14 -3
- package/packages/cli/src/index.ts +1 -1
- package/packages/db/src/index.ts +11 -2
- package/packages/db/src/schema.ts +30 -2
- package/packages/db/src/with-tenant.ts +38 -1
- package/packages/mcp-server/src/index.ts +1 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
attachments,
|
|
3
3
|
auditLog,
|
|
4
|
-
db,
|
|
5
4
|
documentEmbeddings,
|
|
6
5
|
documents,
|
|
7
6
|
lifecycleOperations,
|
|
8
7
|
shareLinks,
|
|
9
8
|
versions,
|
|
10
|
-
} from "@hiai-docs/db";
|
|
9
|
+
} from "@hiai-docs/db/schema";
|
|
10
|
+
import type { TenantTransaction } from "@hiai-docs/db/with-tenant";
|
|
11
11
|
import type {
|
|
12
12
|
AssertPurgeAllowed,
|
|
13
13
|
ExportUserDataContext,
|
|
@@ -15,24 +15,34 @@ import type {
|
|
|
15
15
|
PurgeUserDataContext,
|
|
16
16
|
PurgeUserDataResult,
|
|
17
17
|
UserDataExportRecord,
|
|
18
|
+
UserDataLifecycle,
|
|
18
19
|
} from "@hiai-docs/sdk";
|
|
19
20
|
import { and, eq, inArray, lt, or, sql } from "drizzle-orm";
|
|
20
21
|
|
|
21
22
|
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
23
|
|
|
34
24
|
type PersistentOperation = typeof lifecycleOperations.$inferSelect;
|
|
35
25
|
|
|
26
|
+
/** A terminal host-fence denial. Never infer this from an Error message. */
|
|
27
|
+
export class LifecycleFenceRejectedError extends Error {
|
|
28
|
+
readonly code = "fence_rejected";
|
|
29
|
+
|
|
30
|
+
constructor(message = "Purge fence rejected") {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "LifecycleFenceRejectedError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The durable operation was reclaimed or expired during this worker's run. */
|
|
37
|
+
export class LifecycleLeaseLostError extends Error {
|
|
38
|
+
readonly code = "lease_lost";
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
super("Lifecycle operation lease was lost");
|
|
42
|
+
this.name = "LifecycleLeaseLostError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
export type LifecycleRuntimeAdapters = Readonly<{
|
|
37
47
|
/** Checks the host-owned fence immediately before the first mutation. */
|
|
38
48
|
verifyPurgeFence: (
|
|
@@ -61,6 +71,18 @@ export type LifecycleRuntimeAdapters = Readonly<{
|
|
|
61
71
|
) => Promise<number>;
|
|
62
72
|
}>;
|
|
63
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The host supplies this executor so every persistent lifecycle query runs in
|
|
76
|
+
* a short transaction with transaction-local `app.current_user_id` GUCs.
|
|
77
|
+
* The lifecycle saga never imports or owns a process-global database client.
|
|
78
|
+
*/
|
|
79
|
+
export type LifecycleScopedDatabaseExecutor = Readonly<{
|
|
80
|
+
withActorTransaction<T>(
|
|
81
|
+
actorUserId: string,
|
|
82
|
+
operation: (tx: TenantTransaction) => Promise<T>,
|
|
83
|
+
): Promise<T>;
|
|
84
|
+
}>;
|
|
85
|
+
|
|
64
86
|
export type PersistentLifecycleService = Readonly<{
|
|
65
87
|
exportUserData(
|
|
66
88
|
ctx: ExportUserDataContext,
|
|
@@ -71,6 +93,13 @@ export type PersistentLifecycleService = Readonly<{
|
|
|
71
93
|
): Promise<PurgeUserDataResult>;
|
|
72
94
|
}>;
|
|
73
95
|
|
|
96
|
+
export type PersistentLifecycleRuntimeOptions = Readonly<{
|
|
97
|
+
runtime: LifecycleRuntimeAdapters;
|
|
98
|
+
database: LifecycleScopedDatabaseExecutor;
|
|
99
|
+
assertPurgeAllowed: AssertPurgeAllowed;
|
|
100
|
+
hostSteps?: readonly LifecycleHostStep[];
|
|
101
|
+
}>;
|
|
102
|
+
|
|
74
103
|
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
75
104
|
if (signal?.aborted)
|
|
76
105
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
@@ -79,8 +108,8 @@ function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
|
79
108
|
function safeErrorCode(error: unknown): string {
|
|
80
109
|
if (error instanceof DOMException && error.name === "AbortError")
|
|
81
110
|
return "aborted";
|
|
82
|
-
if (error instanceof
|
|
83
|
-
|
|
111
|
+
if (error instanceof LifecycleFenceRejectedError) return error.code;
|
|
112
|
+
if (error instanceof LifecycleLeaseLostError) return error.code;
|
|
84
113
|
return "persistence_failed";
|
|
85
114
|
}
|
|
86
115
|
|
|
@@ -88,6 +117,10 @@ function tokenHash(token: string): string {
|
|
|
88
117
|
return new Bun.CryptoHasher("sha256").update(token).digest("hex");
|
|
89
118
|
}
|
|
90
119
|
|
|
120
|
+
function subjectHash(actorUserId: string): string {
|
|
121
|
+
return new Bun.CryptoHasher("sha256").update(actorUserId).digest("hex");
|
|
122
|
+
}
|
|
123
|
+
|
|
91
124
|
function checksumLine(
|
|
92
125
|
hash: Bun.CryptoHasher,
|
|
93
126
|
record: UserDataExportRecord,
|
|
@@ -110,12 +143,20 @@ function operationCounts(
|
|
|
110
143
|
return counts(result?.deletedByDomain);
|
|
111
144
|
}
|
|
112
145
|
|
|
146
|
+
/** Throw on a zero-row lease-fenced write; a stale worker must stop immediately. */
|
|
147
|
+
export function requireLeaseWrite<T extends { id: string }>(
|
|
148
|
+
rows: readonly T[],
|
|
149
|
+
): void {
|
|
150
|
+
if (rows.length !== 1) throw new LifecycleLeaseLostError();
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
/**
|
|
114
|
-
* Durable OSS-owned lifecycle saga. It
|
|
115
|
-
*
|
|
154
|
+
* Durable OSS-owned lifecycle saga. It knows only OSS-owned data; workspace
|
|
155
|
+
* membership and final-owner policy remain in the injected host fence.
|
|
116
156
|
*/
|
|
117
157
|
export function createPersistentLifecycleService(
|
|
118
158
|
runtime: LifecycleRuntimeAdapters,
|
|
159
|
+
database: LifecycleScopedDatabaseExecutor,
|
|
119
160
|
hostSteps: readonly LifecycleHostStep[] = [],
|
|
120
161
|
): PersistentLifecycleService {
|
|
121
162
|
const orderedSteps = [...hostSteps].sort(
|
|
@@ -126,105 +167,119 @@ export function createPersistentLifecycleService(
|
|
|
126
167
|
) {
|
|
127
168
|
throw new Error("Lifecycle host step IDs must be globally unique");
|
|
128
169
|
}
|
|
170
|
+
const withActor = <T>(
|
|
171
|
+
actorUserId: string,
|
|
172
|
+
operation: (tx: TenantTransaction) => Promise<T>,
|
|
173
|
+
) => database.withActorTransaction(actorUserId, operation);
|
|
129
174
|
|
|
130
175
|
async function getOrCreateOperation(
|
|
131
176
|
ctx: PurgeUserDataContext,
|
|
132
177
|
): Promise<PersistentOperation> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
178
|
+
return withActor(ctx.actorUserId, async (tx) => {
|
|
179
|
+
await tx
|
|
180
|
+
.insert(lifecycleOperations)
|
|
181
|
+
.values({
|
|
182
|
+
actorUserId: ctx.actorUserId,
|
|
183
|
+
actorSubjectHash: subjectHash(ctx.actorUserId),
|
|
184
|
+
idempotencyKey: ctx.idempotencyKey,
|
|
185
|
+
operationKind: "purge",
|
|
186
|
+
status: "pending",
|
|
187
|
+
})
|
|
188
|
+
.onConflictDoNothing();
|
|
189
|
+
const operation = await tx.query.lifecycleOperations.findFirst({
|
|
190
|
+
where: and(
|
|
191
|
+
eq(lifecycleOperations.actorUserId, ctx.actorUserId),
|
|
192
|
+
eq(lifecycleOperations.idempotencyKey, ctx.idempotencyKey),
|
|
193
|
+
),
|
|
194
|
+
});
|
|
195
|
+
if (operation?.operationKind !== "purge")
|
|
196
|
+
throw new Error("persistence_failed");
|
|
197
|
+
return operation;
|
|
147
198
|
});
|
|
148
|
-
if (!operation) throw new Error("persistence_failed");
|
|
149
|
-
if (operation.operationKind !== "purge")
|
|
150
|
-
throw new Error("persistence_failed");
|
|
151
|
-
return operation;
|
|
152
199
|
}
|
|
153
200
|
|
|
154
201
|
async function acquireLease(
|
|
155
202
|
operation: PersistentOperation,
|
|
203
|
+
actorUserId: string,
|
|
156
204
|
owner: string,
|
|
157
205
|
): Promise<boolean> {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
206
|
+
return withActor(actorUserId, async (tx) => {
|
|
207
|
+
const now = new Date();
|
|
208
|
+
const expiry = new Date(now.getTime() + LEASE_MS);
|
|
209
|
+
const updated = await tx
|
|
210
|
+
.update(lifecycleOperations)
|
|
211
|
+
.set({
|
|
212
|
+
status: "running",
|
|
213
|
+
leaseOwner: owner,
|
|
214
|
+
leaseExpiresAt: expiry,
|
|
215
|
+
attemptCount: sql`${lifecycleOperations.attemptCount} + 1`,
|
|
216
|
+
updatedAt: now,
|
|
217
|
+
})
|
|
218
|
+
.where(
|
|
219
|
+
and(
|
|
220
|
+
eq(lifecycleOperations.id, operation.id),
|
|
221
|
+
or(
|
|
222
|
+
eq(lifecycleOperations.status, "pending"),
|
|
223
|
+
eq(lifecycleOperations.status, "retryable"),
|
|
224
|
+
and(
|
|
225
|
+
eq(lifecycleOperations.status, "running"),
|
|
226
|
+
lt(lifecycleOperations.leaseExpiresAt, now),
|
|
227
|
+
),
|
|
178
228
|
),
|
|
179
229
|
),
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
|
|
230
|
+
)
|
|
231
|
+
.returning({ id: lifecycleOperations.id });
|
|
232
|
+
return updated.length === 1;
|
|
233
|
+
});
|
|
184
234
|
}
|
|
185
235
|
|
|
186
236
|
async function persistStep(
|
|
237
|
+
actorUserId: string,
|
|
187
238
|
operationId: string,
|
|
188
239
|
leaseOwner: string,
|
|
189
240
|
step: string,
|
|
190
241
|
deletedByDomain: Record<string, number>,
|
|
191
242
|
): Promise<void> {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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(
|
|
243
|
+
await withActor(actorUserId, async (tx) => {
|
|
244
|
+
const current = await tx.query.lifecycleOperations.findFirst({
|
|
245
|
+
where: and(
|
|
220
246
|
eq(lifecycleOperations.id, operationId),
|
|
221
247
|
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
222
248
|
),
|
|
223
|
-
);
|
|
249
|
+
});
|
|
250
|
+
if (
|
|
251
|
+
current?.status !== "running" ||
|
|
252
|
+
(current.leaseExpiresAt && current.leaseExpiresAt < new Date())
|
|
253
|
+
) {
|
|
254
|
+
throw new LifecycleLeaseLostError();
|
|
255
|
+
}
|
|
256
|
+
const completed = Array.isArray(current.completedSteps)
|
|
257
|
+
? current.completedSteps.filter(
|
|
258
|
+
(value): value is string => typeof value === "string",
|
|
259
|
+
)
|
|
260
|
+
: [];
|
|
261
|
+
if (!completed.includes(step)) completed.push(step);
|
|
262
|
+
const updated = await tx
|
|
263
|
+
.update(lifecycleOperations)
|
|
264
|
+
.set({
|
|
265
|
+
completedSteps: completed,
|
|
266
|
+
terminalResult: { deletedByDomain },
|
|
267
|
+
updatedAt: new Date(),
|
|
268
|
+
})
|
|
269
|
+
.where(
|
|
270
|
+
and(
|
|
271
|
+
eq(lifecycleOperations.id, operationId),
|
|
272
|
+
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
.returning({ id: lifecycleOperations.id });
|
|
276
|
+
requireLeaseWrite(updated);
|
|
277
|
+
});
|
|
224
278
|
}
|
|
225
279
|
|
|
226
280
|
async function runStep(
|
|
227
281
|
operation: PersistentOperation,
|
|
282
|
+
actorUserId: string,
|
|
228
283
|
leaseOwner: string,
|
|
229
284
|
step: string,
|
|
230
285
|
deletedByDomain: Record<string, number>,
|
|
@@ -236,7 +291,13 @@ export function createPersistentLifecycleService(
|
|
|
236
291
|
if (completed.includes(step)) return;
|
|
237
292
|
const count = await action();
|
|
238
293
|
deletedByDomain[step] = count;
|
|
239
|
-
await persistStep(
|
|
294
|
+
await persistStep(
|
|
295
|
+
actorUserId,
|
|
296
|
+
operation.id,
|
|
297
|
+
leaseOwner,
|
|
298
|
+
step,
|
|
299
|
+
deletedByDomain,
|
|
300
|
+
);
|
|
240
301
|
operation.completedSteps = [...completed, step];
|
|
241
302
|
}
|
|
242
303
|
|
|
@@ -256,10 +317,12 @@ export function createPersistentLifecycleService(
|
|
|
256
317
|
checksumLine(hash, manifest);
|
|
257
318
|
recordCount += 1;
|
|
258
319
|
yield manifest;
|
|
259
|
-
const ownedDocuments = await
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
320
|
+
const ownedDocuments = await withActor(ctx.actorUserId, (tx) =>
|
|
321
|
+
tx
|
|
322
|
+
.select()
|
|
323
|
+
.from(documents)
|
|
324
|
+
.where(eq(documents.ownerId, ctx.actorUserId)),
|
|
325
|
+
);
|
|
263
326
|
for (const document of ownedDocuments) {
|
|
264
327
|
throwIfAborted(ctx.signal);
|
|
265
328
|
const record: UserDataExportRecord = {
|
|
@@ -282,17 +345,19 @@ export function createPersistentLifecycleService(
|
|
|
282
345
|
recordCount += 1;
|
|
283
346
|
yield record;
|
|
284
347
|
}
|
|
285
|
-
const ownedAttachments = await
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
348
|
+
const ownedAttachments = await withActor(ctx.actorUserId, (tx) =>
|
|
349
|
+
tx
|
|
350
|
+
.select({
|
|
351
|
+
id: attachments.id,
|
|
352
|
+
workspaceId: attachments.workspaceId,
|
|
353
|
+
filename: attachments.filename,
|
|
354
|
+
mimeType: attachments.mimeType,
|
|
355
|
+
size: attachments.size,
|
|
356
|
+
})
|
|
357
|
+
.from(attachments)
|
|
358
|
+
.innerJoin(documents, eq(attachments.documentId, documents.id))
|
|
359
|
+
.where(eq(documents.ownerId, ctx.actorUserId)),
|
|
360
|
+
);
|
|
296
361
|
for (const attachment of ownedAttachments) {
|
|
297
362
|
throwIfAborted(ctx.signal);
|
|
298
363
|
const record: UserDataExportRecord = {
|
|
@@ -325,39 +390,48 @@ export function createPersistentLifecycleService(
|
|
|
325
390
|
async purgeUserData(ctx, gate) {
|
|
326
391
|
throwIfAborted(ctx.signal);
|
|
327
392
|
let operation = await getOrCreateOperation(ctx);
|
|
328
|
-
if (operation.status === "completed")
|
|
393
|
+
if (operation.status === "completed")
|
|
329
394
|
return {
|
|
330
395
|
status: "already_completed",
|
|
331
396
|
operationId: operation.id,
|
|
332
397
|
deletedByDomain: operationCounts(operation),
|
|
333
398
|
};
|
|
334
|
-
}
|
|
335
399
|
const leaseOwner = crypto.randomUUID();
|
|
336
|
-
if (!(await acquireLease(operation, leaseOwner)))
|
|
337
|
-
throw new
|
|
338
|
-
operation =
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
400
|
+
if (!(await acquireLease(operation, ctx.actorUserId, leaseOwner)))
|
|
401
|
+
throw new LifecycleLeaseLostError();
|
|
402
|
+
operation = await withActor(
|
|
403
|
+
ctx.actorUserId,
|
|
404
|
+
async (tx) =>
|
|
405
|
+
(await tx.query.lifecycleOperations.findFirst({
|
|
406
|
+
where: eq(lifecycleOperations.id, operation.id),
|
|
407
|
+
})) ?? operation,
|
|
408
|
+
);
|
|
342
409
|
const deletedByDomain = operationCounts(operation);
|
|
343
410
|
try {
|
|
344
411
|
throwIfAborted(ctx.signal);
|
|
345
|
-
await
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
412
|
+
await withActor(ctx.actorUserId, async (tx) => {
|
|
413
|
+
const updated = await tx
|
|
414
|
+
.update(lifecycleOperations)
|
|
415
|
+
.set({
|
|
416
|
+
fenceTokenHash: tokenHash(gate.fenceToken),
|
|
417
|
+
updatedAt: new Date(),
|
|
418
|
+
})
|
|
419
|
+
.where(
|
|
420
|
+
and(
|
|
421
|
+
eq(lifecycleOperations.id, operation.id),
|
|
422
|
+
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
423
|
+
),
|
|
424
|
+
)
|
|
425
|
+
.returning({ id: lifecycleOperations.id });
|
|
426
|
+
requireLeaseWrite(updated);
|
|
427
|
+
});
|
|
357
428
|
await runtime.verifyPurgeFence(ctx, gate.fenceToken);
|
|
358
|
-
|
|
429
|
+
const dbDelete = <T extends { id: string }>(
|
|
430
|
+
action: (tx: TenantTransaction) => Promise<T[]>,
|
|
431
|
+
) => withActor(ctx.actorUserId, action).then((rows) => rows.length);
|
|
359
432
|
await runStep(
|
|
360
433
|
operation,
|
|
434
|
+
ctx.actorUserId,
|
|
361
435
|
leaseOwner,
|
|
362
436
|
"cancel_account_jobs",
|
|
363
437
|
deletedByDomain,
|
|
@@ -365,6 +439,7 @@ export function createPersistentLifecycleService(
|
|
|
365
439
|
);
|
|
366
440
|
await runStep(
|
|
367
441
|
operation,
|
|
442
|
+
ctx.actorUserId,
|
|
368
443
|
leaseOwner,
|
|
369
444
|
"remove_collaboration_state",
|
|
370
445
|
deletedByDomain,
|
|
@@ -372,70 +447,79 @@ export function createPersistentLifecycleService(
|
|
|
372
447
|
);
|
|
373
448
|
await runStep(
|
|
374
449
|
operation,
|
|
450
|
+
ctx.actorUserId,
|
|
375
451
|
leaseOwner,
|
|
376
452
|
"remove_subject_created_shares",
|
|
377
453
|
deletedByDomain,
|
|
378
|
-
|
|
379
|
-
(
|
|
380
|
-
|
|
454
|
+
() =>
|
|
455
|
+
dbDelete((tx) =>
|
|
456
|
+
tx
|
|
381
457
|
.delete(shareLinks)
|
|
382
458
|
.where(eq(shareLinks.createdBy, ctx.actorUserId))
|
|
383
|
-
.returning({ id: shareLinks.id })
|
|
384
|
-
)
|
|
459
|
+
.returning({ id: shareLinks.id }),
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
const owned = await withActor(ctx.actorUserId, (tx) =>
|
|
463
|
+
tx
|
|
464
|
+
.select({ id: documents.id })
|
|
465
|
+
.from(documents)
|
|
466
|
+
.where(eq(documents.ownerId, ctx.actorUserId)),
|
|
385
467
|
);
|
|
386
|
-
const owned = await db
|
|
387
|
-
.select({ id: documents.id })
|
|
388
|
-
.from(documents)
|
|
389
|
-
.where(eq(documents.ownerId, ctx.actorUserId));
|
|
390
468
|
const documentIds = owned.map((row) => row.id);
|
|
391
469
|
await runStep(
|
|
392
470
|
operation,
|
|
471
|
+
ctx.actorUserId,
|
|
393
472
|
leaseOwner,
|
|
394
473
|
"remove_document_versions",
|
|
395
474
|
deletedByDomain,
|
|
396
|
-
|
|
475
|
+
() =>
|
|
397
476
|
documentIds.length
|
|
398
|
-
? (
|
|
399
|
-
|
|
477
|
+
? dbDelete((tx) =>
|
|
478
|
+
tx
|
|
400
479
|
.delete(versions)
|
|
401
480
|
.where(inArray(versions.documentId, documentIds))
|
|
402
|
-
.returning({ id: versions.id })
|
|
403
|
-
)
|
|
404
|
-
: 0,
|
|
481
|
+
.returning({ id: versions.id }),
|
|
482
|
+
)
|
|
483
|
+
: Promise.resolve(0),
|
|
405
484
|
);
|
|
406
485
|
await runStep(
|
|
407
486
|
operation,
|
|
487
|
+
ctx.actorUserId,
|
|
408
488
|
leaseOwner,
|
|
409
489
|
"remove_chunks_and_embeddings",
|
|
410
490
|
deletedByDomain,
|
|
411
|
-
|
|
491
|
+
() =>
|
|
412
492
|
documentIds.length
|
|
413
|
-
? (
|
|
414
|
-
|
|
493
|
+
? dbDelete((tx) =>
|
|
494
|
+
tx
|
|
415
495
|
.delete(documentEmbeddings)
|
|
416
496
|
.where(inArray(documentEmbeddings.documentId, documentIds))
|
|
417
|
-
.returning({ id: documentEmbeddings.id })
|
|
418
|
-
)
|
|
419
|
-
: 0,
|
|
497
|
+
.returning({ id: documentEmbeddings.id }),
|
|
498
|
+
)
|
|
499
|
+
: Promise.resolve(0),
|
|
420
500
|
);
|
|
421
501
|
await runStep(
|
|
422
502
|
operation,
|
|
503
|
+
ctx.actorUserId,
|
|
423
504
|
leaseOwner,
|
|
424
505
|
"remove_graph_state",
|
|
425
506
|
deletedByDomain,
|
|
426
507
|
() => runtime.removeGraphState(documentIds, ctx.signal),
|
|
427
508
|
);
|
|
428
509
|
const objectRows = documentIds.length
|
|
429
|
-
? await
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
510
|
+
? await withActor(ctx.actorUserId, (tx) =>
|
|
511
|
+
tx
|
|
512
|
+
.select({
|
|
513
|
+
id: attachments.id,
|
|
514
|
+
storageKey: attachments.storageKey,
|
|
515
|
+
})
|
|
516
|
+
.from(attachments)
|
|
517
|
+
.where(inArray(attachments.documentId, documentIds)),
|
|
518
|
+
)
|
|
436
519
|
: [];
|
|
437
520
|
await runStep(
|
|
438
521
|
operation,
|
|
522
|
+
ctx.actorUserId,
|
|
439
523
|
leaseOwner,
|
|
440
524
|
"delete_attachment_objects",
|
|
441
525
|
deletedByDomain,
|
|
@@ -447,13 +531,14 @@ export function createPersistentLifecycleService(
|
|
|
447
531
|
);
|
|
448
532
|
await runStep(
|
|
449
533
|
operation,
|
|
534
|
+
ctx.actorUserId,
|
|
450
535
|
leaseOwner,
|
|
451
536
|
"remove_attachment_rows",
|
|
452
537
|
deletedByDomain,
|
|
453
|
-
|
|
538
|
+
() =>
|
|
454
539
|
objectRows.length
|
|
455
|
-
? (
|
|
456
|
-
|
|
540
|
+
? dbDelete((tx) =>
|
|
541
|
+
tx
|
|
457
542
|
.delete(attachments)
|
|
458
543
|
.where(
|
|
459
544
|
inArray(
|
|
@@ -461,114 +546,144 @@ export function createPersistentLifecycleService(
|
|
|
461
546
|
objectRows.map((row) => row.id),
|
|
462
547
|
),
|
|
463
548
|
)
|
|
464
|
-
.returning({ id: attachments.id })
|
|
465
|
-
)
|
|
466
|
-
: 0,
|
|
549
|
+
.returning({ id: attachments.id }),
|
|
550
|
+
)
|
|
551
|
+
: Promise.resolve(0),
|
|
467
552
|
);
|
|
468
553
|
await runStep(
|
|
469
554
|
operation,
|
|
555
|
+
ctx.actorUserId,
|
|
470
556
|
leaseOwner,
|
|
471
557
|
"remove_subject_documents",
|
|
472
558
|
deletedByDomain,
|
|
473
|
-
|
|
474
|
-
(
|
|
475
|
-
|
|
559
|
+
() =>
|
|
560
|
+
dbDelete((tx) =>
|
|
561
|
+
tx
|
|
476
562
|
.delete(documents)
|
|
477
563
|
.where(eq(documents.ownerId, ctx.actorUserId))
|
|
478
|
-
.returning({ id: documents.id })
|
|
479
|
-
)
|
|
564
|
+
.returning({ id: documents.id }),
|
|
565
|
+
),
|
|
480
566
|
);
|
|
481
567
|
await runStep(
|
|
482
568
|
operation,
|
|
569
|
+
ctx.actorUserId,
|
|
483
570
|
leaseOwner,
|
|
484
571
|
"clear_redis_state",
|
|
485
572
|
deletedByDomain,
|
|
486
573
|
() => runtime.clearAccountRedisState(ctx.actorUserId, ctx.signal),
|
|
487
574
|
);
|
|
488
|
-
for (const step of orderedSteps)
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
575
|
+
for (const step of orderedSteps)
|
|
576
|
+
if (step.purge)
|
|
577
|
+
await runStep(
|
|
578
|
+
operation,
|
|
579
|
+
ctx.actorUserId,
|
|
580
|
+
leaseOwner,
|
|
581
|
+
`host:${step.id}`,
|
|
582
|
+
deletedByDomain,
|
|
583
|
+
async () => (await step.purge?.(ctx))?.deletedCount ?? 0,
|
|
584
|
+
);
|
|
498
585
|
await runStep(
|
|
499
586
|
operation,
|
|
587
|
+
ctx.actorUserId,
|
|
500
588
|
leaseOwner,
|
|
501
589
|
"write_deletion_audit",
|
|
502
590
|
deletedByDomain,
|
|
503
591
|
async () => {
|
|
504
|
-
await
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
592
|
+
await withActor(ctx.actorUserId, (tx) =>
|
|
593
|
+
tx.insert(auditLog).values({
|
|
594
|
+
actorId: ctx.actorUserId,
|
|
595
|
+
action: "account_data_purged",
|
|
596
|
+
resourceType: "lifecycle_operation",
|
|
597
|
+
details: {
|
|
598
|
+
operationId: operation.id,
|
|
599
|
+
outcome: "completed",
|
|
600
|
+
deletedByDomain,
|
|
601
|
+
},
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
514
604
|
return 1;
|
|
515
605
|
},
|
|
516
606
|
);
|
|
517
|
-
await
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
607
|
+
await withActor(ctx.actorUserId, async (tx) => {
|
|
608
|
+
const updated = await tx
|
|
609
|
+
.update(lifecycleOperations)
|
|
610
|
+
.set({
|
|
611
|
+
status: "completed",
|
|
612
|
+
terminalResult: { deletedByDomain },
|
|
613
|
+
completedAt: new Date(),
|
|
614
|
+
leaseOwner: null,
|
|
615
|
+
leaseExpiresAt: null,
|
|
616
|
+
updatedAt: new Date(),
|
|
617
|
+
})
|
|
618
|
+
.where(
|
|
619
|
+
and(
|
|
620
|
+
eq(lifecycleOperations.id, operation.id),
|
|
621
|
+
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
.returning({ id: lifecycleOperations.id });
|
|
625
|
+
requireLeaseWrite(updated);
|
|
626
|
+
});
|
|
533
627
|
return {
|
|
534
628
|
status: "completed",
|
|
535
629
|
operationId: operation.id,
|
|
536
630
|
deletedByDomain,
|
|
537
631
|
};
|
|
538
632
|
} catch (error) {
|
|
633
|
+
if (error instanceof LifecycleLeaseLostError) throw error;
|
|
539
634
|
const code = safeErrorCode(error);
|
|
540
|
-
await
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
635
|
+
await withActor(ctx.actorUserId, async (tx) => {
|
|
636
|
+
const updated = await tx
|
|
637
|
+
.update(lifecycleOperations)
|
|
638
|
+
.set({
|
|
639
|
+
status:
|
|
640
|
+
error instanceof LifecycleFenceRejectedError
|
|
641
|
+
? "rejected"
|
|
642
|
+
: "retryable",
|
|
643
|
+
safeErrorCode: code,
|
|
644
|
+
leaseOwner: null,
|
|
645
|
+
leaseExpiresAt: null,
|
|
646
|
+
completedAt:
|
|
647
|
+
error instanceof LifecycleFenceRejectedError
|
|
648
|
+
? new Date()
|
|
649
|
+
: null,
|
|
650
|
+
updatedAt: new Date(),
|
|
651
|
+
})
|
|
652
|
+
.where(
|
|
653
|
+
and(
|
|
654
|
+
eq(lifecycleOperations.id, operation.id),
|
|
655
|
+
eq(lifecycleOperations.leaseOwner, leaseOwner),
|
|
656
|
+
),
|
|
657
|
+
)
|
|
658
|
+
.returning({ id: lifecycleOperations.id });
|
|
659
|
+
requireLeaseWrite(updated);
|
|
660
|
+
});
|
|
556
661
|
throw error;
|
|
557
662
|
}
|
|
558
663
|
},
|
|
559
664
|
};
|
|
560
665
|
}
|
|
561
666
|
|
|
562
|
-
/** Builds the public facade without allowing the OSS service to own membership policy. */
|
|
563
667
|
export function bindPersistentLifecycle(
|
|
564
668
|
service: PersistentLifecycleService,
|
|
565
669
|
assertPurgeAllowed: AssertPurgeAllowed,
|
|
566
|
-
) {
|
|
670
|
+
): UserDataLifecycle {
|
|
567
671
|
return {
|
|
568
672
|
exportUserData: service.exportUserData,
|
|
569
|
-
async purgeUserData(ctx
|
|
673
|
+
async purgeUserData(ctx) {
|
|
570
674
|
const gate = await assertPurgeAllowed(ctx);
|
|
571
675
|
return service.purgeUserData(ctx, gate);
|
|
572
676
|
},
|
|
573
677
|
};
|
|
574
678
|
}
|
|
679
|
+
|
|
680
|
+
export function createPersistentLifecycleRuntime(
|
|
681
|
+
options: PersistentLifecycleRuntimeOptions,
|
|
682
|
+
): UserDataLifecycle {
|
|
683
|
+
const service = createPersistentLifecycleService(
|
|
684
|
+
options.runtime,
|
|
685
|
+
options.database,
|
|
686
|
+
options.hostSteps ?? [],
|
|
687
|
+
);
|
|
688
|
+
return bindPersistentLifecycle(service, options.assertPurgeAllowed);
|
|
689
|
+
}
|