@hiai-gg/docsmint 0.3.2 → 0.3.3
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/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/extension.d.ts +35 -2
- 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
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export declare
|
|
8
|
-
export declare
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
export interface CategoryDto { id: string; name: string; order: number; apiMode?: "unavailable" | "global" | "general" | "category" | null; apiPermissionRead?: boolean | null; apiPermissionEdit?: boolean | null; apiPermissionWrite?: boolean | null; createdAt: string; updatedAt: string; documentCount?: number; folderCount?: number; }
|
|
4
|
+
export type Category = CategoryDto;
|
|
5
|
+
export interface CreateCategoryInput { name: string; apiMode?: "unavailable" | "global" | "general" | "category"; apiPermissionRead?: boolean; apiPermissionEdit?: boolean; apiPermissionWrite?: boolean; }
|
|
6
|
+
export interface UpdateCategoryInput { name?: string; order?: number; apiMode?: "unavailable" | "global" | "general" | "category"; apiPermissionRead?: boolean; apiPermissionEdit?: boolean; apiPermissionWrite?: boolean; }
|
|
7
|
+
export declare const createCategoryInputSchema: z.ZodType<CreateCategoryInput>;
|
|
8
|
+
export declare const updateCategoryInputSchema: z.ZodType<UpdateCategoryInput>;
|
|
9
|
+
export declare function listCategories(fetcher?: typeof fetch): Promise<CategoryDto[]>;
|
|
10
|
+
export declare function createCategory(inputOrName: string | CreateCategoryInput): Promise<CategoryDto>;
|
|
11
|
+
export declare function updateCategory(id: string, data: UpdateCategoryInput): Promise<CategoryDto>;
|
|
12
|
+
export declare function deleteCategory(id: string): Promise<void>;
|
|
13
|
+
export declare function setDocumentCategory(documentId: string, categoryId: string | null): Promise<void>;
|
|
14
|
+
export declare function setFolderCategory(folderId: string, categoryId: string | null): Promise<void>;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export declare function
|
|
2
|
+
export interface DocumentTagDto { id: string; name: string; color: string; }
|
|
3
|
+
export interface DocumentDto { id: string; title: string; content: string; contentJson?: unknown; folderId?: string | null; folderName?: string; categoryId?: string | null; tags?: DocumentTagDto[]; excerpt?: string; createdAt: string; updatedAt: string; }
|
|
4
|
+
export type Document = DocumentDto;
|
|
5
|
+
export interface DocumentListResponse { items: DocumentDto[]; total: number; page: number; limit: number; }
|
|
6
|
+
export interface UpdateDocumentInput { title?: string; content?: string; folderId?: string | null; categoryId?: string | null; contentJson?: unknown; expectedUpdatedAt?: string; }
|
|
7
|
+
export interface ImportResult { filename: string; status: "ok" | "error"; document?: DocumentDto; error?: string; }
|
|
8
|
+
export interface ImportResponse { items: ImportResult[]; imported: number; failed: number; }
|
|
9
|
+
export declare function listDocuments(params?: { folderId?: string; tag?: string; page?: number; limit?: number }, fetcher?: typeof fetch): Promise<DocumentListResponse>;
|
|
10
|
+
export declare function getDocument(id: string, fetcher?: typeof fetch): Promise<DocumentDto>;
|
|
11
|
+
export declare function createDocument(data: { title: string; content?: string; folderId?: string; categoryId?: string }, fetcher?: typeof fetch): Promise<DocumentDto>;
|
|
12
|
+
export declare function updateDocument(id: string, data: UpdateDocumentInput): Promise<DocumentDto>;
|
|
13
|
+
export declare function deleteDocument(id: string): Promise<void>;
|
|
14
|
+
export declare function importDocument(file: File, folderId?: string): Promise<DocumentDto>;
|
|
15
|
+
export declare function importDocuments(files: File[], folderId?: string): Promise<ImportResponse>;
|
|
10
16
|
export declare function clearDocumentsCache(...args: readonly unknown[]): void;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
2
|
+
export interface FolderDocumentDto { id: string; title: string; content?: string; folderId: string | null; folderName: string; categoryId?: string | null; tags: string[]; createdAt: string; updatedAt: string; excerpt: string; }
|
|
3
|
+
export interface FolderDto { id: string; name: string; parentId: string | null; categoryId?: string | null; order: number; documentCount: number; subfolderCount: number; children: FolderDto[]; documents: FolderDocumentDto[]; createdAt: string; updatedAt: string; }
|
|
4
|
+
export type Folder = FolderDto;
|
|
5
|
+
export interface CreateFolderData { name: string; parentId?: string | null; categoryId?: string | null; }
|
|
6
|
+
export interface UpdateFolderData { name?: string; parentId?: string | null; categoryId?: string | null; order?: number; }
|
|
7
|
+
export declare function listFolders(parentId?: string | null, all?: boolean, fetcher?: typeof fetch): Promise<FolderDto[]>;
|
|
8
|
+
export declare function getFolder(id: string, fetcher?: typeof fetch): Promise<FolderDto>;
|
|
9
|
+
export declare function getFolderPath(folderId: string, fetcher?: typeof fetch): Promise<Array<{ id: string; name: string }>>;
|
|
10
|
+
export declare function createFolder(data: CreateFolderData): Promise<FolderDto>;
|
|
11
|
+
export declare function updateFolder(id: string, data: UpdateFolderData): Promise<FolderDto>;
|
|
12
|
+
export declare function deleteFolder(id: string): Promise<void>;
|
|
13
|
+
export declare function duplicateDocument(documentId: string): Promise<FolderDocumentDto>;
|
|
14
|
+
export declare function deleteDocument(documentId: string): Promise<void>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
export
|
|
3
|
-
export type
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
2
|
+
export interface ProfileDto { id: string; name: string; email: string; avatar: string | null; }
|
|
3
|
+
export type UserProfile = ProfileDto;
|
|
4
|
+
export interface EmbeddingConfigDto { baseUrl: string; apiKey: string; model: string; fallbackBaseUrl: string | null; fallbackApiKey: string | null; fallbackModel: string | null; }
|
|
5
|
+
export type EmbeddingConfig = EmbeddingConfigDto;
|
|
6
|
+
export declare function getProfile(): Promise<ProfileDto>;
|
|
7
|
+
export declare function updateProfile(data: { name?: string }): Promise<ProfileDto>;
|
|
8
|
+
export declare function getEmbeddingConfig(): EmbeddingConfigDto;
|
|
9
|
+
export declare function updateEmbeddingConfig(data: Partial<EmbeddingConfigDto>): EmbeddingConfigDto;
|
|
10
|
+
export declare function deleteAccount(): Promise<void>;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export declare
|
|
8
|
-
export declare
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
export interface TagDto { id: string; name: string; color: string | null; createdAt: string; documentCount?: number; }
|
|
4
|
+
export type Tag = TagDto;
|
|
5
|
+
export interface CreateTagInput { name: string; color?: string; }
|
|
6
|
+
export interface UpdateTagInput { name?: string; color?: string; }
|
|
7
|
+
export declare const createTagInputSchema: z.ZodType<CreateTagInput>;
|
|
8
|
+
export declare const updateTagInputSchema: z.ZodType<UpdateTagInput>;
|
|
9
|
+
export declare function listTags(fetcher?: typeof fetch): Promise<TagDto[]>;
|
|
10
|
+
export declare function getTag(id: string): Promise<TagDto>;
|
|
11
|
+
export declare function createTag(name: string, color?: string): Promise<TagDto>;
|
|
12
|
+
export declare function updateTag(id: string, data: UpdateTagInput): Promise<TagDto>;
|
|
13
|
+
export declare function deleteTag(id: string): Promise<void>;
|
|
14
|
+
export declare function addTagToDocument(documentId: string, tagId: string): Promise<void>;
|
|
15
|
+
export declare function removeTagFromDocument(documentId: string, tagId: string): Promise<void>;
|
|
@@ -1,2 +1,35 @@
|
|
|
1
|
-
import type { Component } from "svelte";
|
|
2
|
-
export
|
|
1
|
+
import type { Component, ComponentType, Snippet, SvelteComponent } from "svelte";
|
|
2
|
+
export interface ExtensionVisibilityContext {
|
|
3
|
+
userId?: string;
|
|
4
|
+
pathname?: string;
|
|
5
|
+
capabilities?: Readonly<Record<string, boolean>>;
|
|
6
|
+
permissions?: Readonly<Record<string, boolean>>;
|
|
7
|
+
}
|
|
8
|
+
export type ExtensionVisibility = (context: ExtensionVisibilityContext) => boolean;
|
|
9
|
+
export type ExtensionIcon = ComponentType<SvelteComponent>;
|
|
10
|
+
export interface DocTabPanelProps { documentId: string; content: string; contentJson: object | undefined; }
|
|
11
|
+
export interface DocTabDefinition { id: string; label: string; component: Component<DocTabPanelProps>; order?: number; icon?: ExtensionIcon; disabled?: boolean; }
|
|
12
|
+
export interface NavigationExtension { id: string; label: string; href?: string; icon?: ExtensionIcon; order?: number; badge?: string | number; disabled?: boolean; visible?: ExtensionVisibility; }
|
|
13
|
+
export interface DashboardWidgetProps { userId?: string; }
|
|
14
|
+
export interface DashboardWidgetExtension { id: string; title?: string; component: Component<DashboardWidgetProps>; order?: number; colSpan?: 1 | 2 | 3 | 4 | 6 | 12; visible?: ExtensionVisibility; }
|
|
15
|
+
export interface SearchWidgetProps { query: string; loading: boolean; total?: number; }
|
|
16
|
+
export interface SearchWidgetExtension { id: string; title?: string; component: Component<SearchWidgetProps>; order?: number; visible?: ExtensionVisibility; }
|
|
17
|
+
export interface EditorActionContext { documentId: string; content: string; contentJson: object | undefined; selection?: unknown; commands?: Readonly<Record<string, (...args: unknown[]) => unknown>>; }
|
|
18
|
+
export type ExtensionAction = (context: EditorActionContext) => void | Promise<void>;
|
|
19
|
+
export interface EditorActionExtension { id: string; label: string; icon?: ExtensionIcon; order?: number; disabled?: boolean | ((context: EditorActionContext) => boolean); visible?: ExtensionVisibility; run: ExtensionAction; }
|
|
20
|
+
export interface DocumentMenuActionContext extends EditorActionContext { title?: string; }
|
|
21
|
+
export type DocumentMenuAction = (context: DocumentMenuActionContext) => void | Promise<void>;
|
|
22
|
+
export interface DocumentMenuActionExtension { id: string; label: string; icon?: ExtensionIcon; order?: number; destructive?: boolean; disabled?: boolean | ((context: DocumentMenuActionContext) => boolean); visible?: ExtensionVisibility; run: DocumentMenuAction; }
|
|
23
|
+
export interface SettingsSectionProps { userId?: string; }
|
|
24
|
+
export interface SettingsSectionExtension { id: string; label: string; component: Component<SettingsSectionProps>; order?: number; description?: string; visible?: ExtensionVisibility; }
|
|
25
|
+
export interface CommandPaletteActionContext { query?: string; }
|
|
26
|
+
export type CommandPaletteAction = (context: CommandPaletteActionContext) => void | Promise<void>;
|
|
27
|
+
export interface CommandPaletteActionExtension { id: string; label: string; keywords?: readonly string[]; group?: string; shortcut?: string; icon?: ExtensionIcon; order?: number; disabled?: boolean; visible?: ExtensionVisibility; run: CommandPaletteAction; }
|
|
28
|
+
export interface SharedDocumentExtensionContext { shareToken: string; documentId: string; title: string; content: string; contentJson?: object; role: "viewer" | "commenter" | "editor"; permissions: { read: true; annotate: boolean; edit: boolean; export: boolean; }; }
|
|
29
|
+
export interface SharedDocumentExtension { id: string; label: string; icon?: ExtensionIcon; order?: number; permission: "annotate" | "edit"; visible?: (context: SharedDocumentExtensionContext) => boolean; component: Component<{ context: SharedDocumentExtensionContext }>; }
|
|
30
|
+
export interface DocsmintFrontendExtensions { navigation: readonly NavigationExtension[]; dashboardWidgets: readonly DashboardWidgetExtension[]; searchWidgets: readonly SearchWidgetExtension[]; documentTabs: readonly DocTabDefinition[]; editorActions: readonly EditorActionExtension[]; documentMenuActions: readonly DocumentMenuActionExtension[]; settingsSections: readonly SettingsSectionExtension[]; commandPaletteActions: readonly CommandPaletteActionExtension[]; sharedDocumentHeaderActions: readonly SharedDocumentExtension[]; sharedDocumentTabs: readonly SharedDocumentExtension[]; sharedDocumentNotesModes: readonly SharedDocumentExtension[]; sharedDocumentEditorModes: readonly SharedDocumentExtension[]; }
|
|
31
|
+
/** @deprecated Use DocsmintFrontendExtensions. */
|
|
32
|
+
export type HiaiDocsFrontendExtensions = DocsmintFrontendExtensions;
|
|
33
|
+
export type FrontendExtensions = DocsmintFrontendExtensions;
|
|
34
|
+
export interface DocsmintExtensionProviderProps { extensions?: Partial<DocsmintFrontendExtensions>; children: Snippet; }
|
|
35
|
+
export declare const DocsmintExtensionProvider: Component<DocsmintExtensionProviderProps>;
|
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
import type { Component } from "svelte";
|
|
2
2
|
export declare const DocsmintSharedDocumentHost: Component;
|
|
3
|
-
export
|
|
3
|
+
export interface ProseMirrorNode {
|
|
4
|
+
type: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
content?: ProseMirrorNode[];
|
|
7
|
+
attrs?: Record<string, unknown>;
|
|
8
|
+
marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
|
|
9
|
+
}
|
|
10
|
+
export type ProseMirrorDoc = ProseMirrorNode & { content?: ProseMirrorNode[] };
|
|
11
|
+
export type SharedAttachmentObjectUrls = string[];
|
|
12
|
+
export declare function renderSharedDocument(doc: ProseMirrorDoc): string;
|
|
13
|
+
export declare function markMarkdownTaskItems(html: string): string;
|
|
14
|
+
export declare function sharedAttachmentHeaders(token: string, password?: string): HeadersInit;
|
|
15
|
+
export declare function hydrateSharedAttachmentImages(root: ParentNode, token: string, password?: string): Promise<SharedAttachmentObjectUrls>;
|
|
16
|
+
export declare function waitForSharedDocumentImages(root: ParentNode): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hiai-gg/docsmint",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"browser": {
|
|
6
6
|
"./dist/lifecycle.js": false,
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
"import": "./dist/lifecycle.js",
|
|
52
52
|
"types": "./dist/lifecycle.d.ts"
|
|
53
53
|
},
|
|
54
|
+
"./lifecycle/persistent": {
|
|
55
|
+
"browser": "./dist/server-only-browser-entry.js",
|
|
56
|
+
"import": "./dist/lifecycle-persistent.js",
|
|
57
|
+
"types": "./dist/lifecycle-persistent.d.ts"
|
|
58
|
+
},
|
|
54
59
|
"./workspace": {
|
|
55
60
|
"browser": "./dist/server-only-browser-entry.js",
|
|
56
61
|
"import": "./dist/workspace.js",
|
|
@@ -161,6 +166,7 @@
|
|
|
161
166
|
"backend/src/lib/redis-factory.ts",
|
|
162
167
|
"backend/src/lib/storage-factory.ts",
|
|
163
168
|
"backend/src/lib/logger.ts",
|
|
169
|
+
"backend/src/lib/lifecycle-service.ts",
|
|
164
170
|
"README.md",
|
|
165
171
|
"LICENSE"
|
|
166
172
|
],
|
|
@@ -24,7 +24,7 @@ import { registerSearch } from "./commands/search.js";
|
|
|
24
24
|
import { registerSnapshot } from "./commands/snapshot.js";
|
|
25
25
|
import { registerUpdate } from "./commands/update.js";
|
|
26
26
|
|
|
27
|
-
const VERSION = "0.3.
|
|
27
|
+
const VERSION = "0.3.3";
|
|
28
28
|
|
|
29
29
|
const program = new Command();
|
|
30
30
|
program
|