@hiai-gg/docsmint 0.6.7 → 0.7.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 +27 -4
- package/backend/src/lib/api-keys.ts +14 -6
- package/backend/src/lib/lifecycle-service.ts +397 -73
- package/dist/backend/index.js +13066 -10878
- package/dist/backend-account-runtime-cleanup.js +152 -42
- package/dist/backend-api-key-facade.js +207 -61
- package/dist/backend-pipeline-cancellation.js +144 -42
- package/dist/client.d.ts +12 -2
- package/dist/client.js +83 -30
- package/dist/frontend/{CategoryDialog-CbXBMBZB.js → CategoryDialog-C67_TXf0.js} +4 -4
- package/dist/frontend/{ConfirmDialog-DgMTQBq4.js → ConfirmDialog-_LlrA--f.js} +3 -3
- package/dist/frontend/{CreateSnapshotDialog-FUq-ZfBH.js → CreateSnapshotDialog-BmCRpOMv.js} +3 -3
- package/dist/frontend/{DatePicker-D6E-EEtZ.js → DatePicker-DsoepNdb.js} +8 -8
- package/dist/frontend/{DeleteDialog-8k6JYWCx.js → DeleteDialog-L1A3FhMM.js} +1 -1
- package/dist/frontend/{FolderNode-DAAn_T8_.js → FolderNode-CNwi0ySr.js} +1 -1
- package/dist/frontend/{HiAiEditor-DSVG3GNd.js → HiAiEditor-B1EDGsnA.js} +6 -6
- package/dist/frontend/{SettingsDialog-BDNgf5rv.js → SettingsDialog-AP42xOGg.js} +12 -12
- package/dist/frontend/ShareDialog-wGTXuXFE.js +281 -0
- package/dist/frontend/{Sidebar-DX08utdV.js → Sidebar-lKQT3dEf.js} +17 -16
- package/dist/frontend/{TagCreateDialog-Bb0-Vtzh.js → TagCreateDialog-su8BhLzS.js} +3 -3
- package/dist/frontend/api/attachments.js +1 -1
- package/dist/frontend/api/settings.js +1 -1
- package/dist/frontend/app-shell.js +2 -2
- package/dist/frontend/{attachments-B8AuDjno.js → attachments-B2aH3hXF.js} +10 -9
- package/dist/frontend/{auth-client-Cygmp1I2.js → auth-client-CnDZwPfp.js} +11 -11
- package/dist/frontend/components/category-dialog.js +1 -1
- package/dist/frontend/components/create-snapshot-dialog.js +1 -1
- package/dist/frontend/components/delete-dialog.js +1 -1
- package/dist/frontend/components/editor/compact-editor.js +1 -1
- package/dist/frontend/components/editor/document-editor.js +1 -1
- package/dist/frontend/components/folder-node.js +1 -1
- package/dist/frontend/components/save-as-dialog.js +3 -3
- package/dist/frontend/components/settings.js +1 -1
- package/dist/frontend/components/share-dialog.d.ts +2 -1
- package/dist/frontend/components/share-dialog.js +1 -1
- package/dist/frontend/components/sidebar.js +1 -1
- package/dist/frontend/components/tag-create-dialog.js +1 -1
- package/dist/frontend/components/version-history.js +2 -2
- package/dist/frontend/{confirm-dialog-CzmoZw3a.js → confirm-dialog-BG8y2Pum.js} +4 -4
- package/dist/frontend/dashboard.js +1008 -998
- package/dist/frontend/{db-ByrlYfaI.js → db-Bo1t_76T.js} +1 -1
- package/dist/frontend/{dialog-Ce79C9fd.js → dialog-C2iKObEu.js} +7 -7
- package/dist/frontend/{dropdown-menu-CJ_7V3N0.js → dropdown-menu-BXIl2E26.js} +12 -12
- package/dist/frontend/{floating-layer-anchor-S5RSefGN.js → floating-layer-anchor-BGIndLY-.js} +63 -63
- package/dist/frontend/{identity-DyWeNzu_.js → identity-Ynrglr5i.js} +1 -1
- package/dist/frontend/{input-BWlOML_m.js → input-eHFb2qiL.js} +2 -2
- package/dist/frontend/{label-C2HfJNQ2.js → label-_I1iiaNU.js} +2 -2
- package/dist/frontend/offline/identity.js +1 -1
- package/dist/frontend/search.js +2 -2
- package/dist/frontend/{select-value-JCAp2PmU.js → select-value-CtMyrvDJ.js} +18 -18
- package/dist/frontend/{settings-DSrUl4hW.js → settings-BR48ihB4.js} +6 -23
- package/dist/frontend/{textarea-DOAE-h42.js → textarea-2eslfANY.js} +2 -2
- package/dist/frontend/{utils-BPDRytUU.js → utils-CDBqTgoV.js} +1 -1
- package/dist/frontend-ssr/api/attachments.js +1 -1
- package/dist/frontend-ssr/api/settings.js +1 -1
- package/dist/frontend-ssr/app-shell.js +2 -2
- package/dist/frontend-ssr/assets/{CategoryDialog-QHEv_9N4.js → CategoryDialog-BGuHTb3K.js} +4 -4
- package/dist/frontend-ssr/assets/{ConfirmDialog-BhAelejz.js → ConfirmDialog-D1BDXXll.js} +3 -3
- package/dist/frontend-ssr/assets/{CreateSnapshotDialog-CI8vXXcN.js → CreateSnapshotDialog-BZ45odvJ.js} +3 -3
- package/dist/frontend-ssr/assets/{DatePicker-D84cJ-Eq.js → DatePicker-CPtl6W5E.js} +3 -3
- package/dist/frontend-ssr/assets/{DeleteDialog-DG9Lm38J.js → DeleteDialog-oHT_p_vG.js} +1 -1
- package/dist/frontend-ssr/assets/{FolderNode-kmilhDjO.js → FolderNode-D9kgJG9d.js} +1 -1
- package/dist/frontend-ssr/assets/{HiAiEditor-CUOJmKzT.js → HiAiEditor-DOd2MXjD.js} +5 -5
- package/dist/frontend-ssr/assets/{SettingsDialog-BjJIiVLc.js → SettingsDialog-CIVbMKmf.js} +12 -12
- package/dist/frontend-ssr/assets/{ShareDialog-C9C-XTm4.js → ShareDialog-D2UNUbF7.js} +19 -6
- package/dist/frontend-ssr/assets/{Sidebar-C2sUfGNk.js → Sidebar-Bb-k2R-Z.js} +13 -12
- package/dist/frontend-ssr/assets/{TagCreateDialog-DUPMitlL.js → TagCreateDialog-6Gk7L3D-.js} +3 -3
- package/dist/frontend-ssr/assets/{attachments-Cj3fQyUp.js → attachments-BdkOp8ha.js} +5 -4
- package/dist/frontend-ssr/assets/{auth-client-x26kqY8Z.js → auth-client-CojKIzqy.js} +13 -13
- package/dist/frontend-ssr/assets/{confirm-dialog-XuyaL8sr.js → confirm-dialog-B9MVS_bF.js} +4 -4
- package/dist/frontend-ssr/assets/{db-CnxeO66n.js → db-BhwJPEkn.js} +1 -1
- package/dist/frontend-ssr/assets/{dialog-BOV1u6zP.js → dialog-1SB5neow.js} +8 -8
- package/dist/frontend-ssr/assets/{dropdown-menu-Dn_vQq9b.js → dropdown-menu-DqoOkdqB.js} +12 -12
- package/dist/frontend-ssr/assets/{floating-layer-anchor-BIAu-252.js → floating-layer-anchor-BoiiIEiK.js} +71 -71
- package/dist/frontend-ssr/assets/{identity-B3uHoDtf.js → identity-DcMMBKWg.js} +1 -1
- package/dist/frontend-ssr/assets/{input-DezGsU3y.js → input-Df-QP3Km.js} +2 -2
- package/dist/frontend-ssr/assets/{label-Bj8-TfFN.js → label-CbUOUz6L.js} +2 -2
- package/dist/frontend-ssr/assets/{select-value-DBhbkNcc.js → select-value-CXEDWrZg.js} +18 -18
- package/dist/frontend-ssr/assets/{settings-Bk5Hk9Vq.js → settings-Mp8EuFSn.js} +3 -21
- package/dist/frontend-ssr/assets/{textarea-Dev-qSEc.js → textarea-CHh8NuSS.js} +2 -2
- package/dist/frontend-ssr/assets/{utils-DtlPPXpJ.js → utils-DtX0ta7y.js} +1 -1
- package/dist/frontend-ssr/components/category-dialog.js +1 -1
- package/dist/frontend-ssr/components/create-snapshot-dialog.js +1 -1
- package/dist/frontend-ssr/components/delete-dialog.js +1 -1
- package/dist/frontend-ssr/components/editor/compact-editor.js +1 -1
- package/dist/frontend-ssr/components/editor/document-editor.js +1 -1
- package/dist/frontend-ssr/components/folder-node.js +1 -1
- package/dist/frontend-ssr/components/save-as-dialog.js +3 -3
- package/dist/frontend-ssr/components/settings.js +1 -1
- package/dist/frontend-ssr/components/share-dialog.js +1 -1
- package/dist/frontend-ssr/components/sidebar.js +1 -1
- package/dist/frontend-ssr/components/tag-create-dialog.js +1 -1
- package/dist/frontend-ssr/components/version-history.js +2 -2
- package/dist/frontend-ssr/dashboard.js +31 -29
- package/dist/frontend-ssr/offline/identity.js +1 -1
- package/dist/frontend-ssr/search.js +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/lifecycle-persistent.d.ts +1 -1
- package/dist/lifecycle-runtime.js +52659 -338
- package/dist/mcp-cli.js +1056 -0
- package/dist/mcp-server.d.ts +39 -0
- package/dist/mcp-server.js +1053 -0
- package/dist/storage-quota.d.ts +5 -1
- package/dist/types.d.ts +7 -0
- package/dist/workspace.d.ts +11 -2
- package/dist/workspace.js +82 -10
- package/package.json +7 -13
- package/packages/cli/src/index.ts +1 -1
- package/packages/db/src/client.ts +56 -5
- package/packages/db/src/schema.ts +192 -1
- package/packages/db/src/with-tenant.ts +20 -2
- package/server.json +2 -2
- package/dist/frontend/ShareDialog-2Mty7hm4.js +0 -258
- package/packages/mcp-server/src/capabilities.ts +0 -189
- package/packages/mcp-server/src/client.ts +0 -225
- package/packages/mcp-server/src/index.ts +0 -8
- package/packages/mcp-server/src/server.ts +0 -87
- package/packages/mcp-server/src/tools/create-document.ts +0 -34
- package/packages/mcp-server/src/tools/create-folder.ts +0 -31
- package/packages/mcp-server/src/tools/create-snapshot.ts +0 -27
- package/packages/mcp-server/src/tools/export-document.ts +0 -22
- package/packages/mcp-server/src/tools/get-document.ts +0 -22
- package/packages/mcp-server/src/tools/list-documents.ts +0 -34
- package/packages/mcp-server/src/tools/list-folders.ts +0 -26
- package/packages/mcp-server/src/tools/search.ts +0 -39
- package/packages/mcp-server/src/tools/update-document.ts +0 -38
- package/packages/mcp-server/src/tools/version-history.ts +0 -27
- package/packages/mcp-server/src/types.ts +0 -126
package/README.md
CHANGED
|
@@ -40,7 +40,27 @@ server.
|
|
|
40
40
|
- **Own the full stack**: application data, vectors, graph, queue, and files run
|
|
41
41
|
on infrastructure you control.
|
|
42
42
|
|
|
43
|
-
## What's new in DocsMint 0.
|
|
43
|
+
## What's new in DocsMint 0.7.0?
|
|
44
|
+
|
|
45
|
+
- **Signed workspace assertions.** Trusted server-to-server hosts can restrict
|
|
46
|
+
an assertion to one category with independent `read`, `edit`, and `write`
|
|
47
|
+
permissions; an omitted scope remains workspace-wide and role-derived.
|
|
48
|
+
- **Consistent effective-category authorization.** Direct and folder-inherited
|
|
49
|
+
categories are enforced before counts, pagination, indexing, search, and
|
|
50
|
+
GraphRAG retrieval. Index status is read-only; refresh requires write.
|
|
51
|
+
- **Release-safe indexing.** Context-aware embedding generations and recovery
|
|
52
|
+
prevent stale metadata work from becoming active.
|
|
53
|
+
|
|
54
|
+
This OSS release adds no host RBAC, billing, invitations, or product UI.
|
|
55
|
+
|
|
56
|
+
DocsMint 0.7.0 includes the 0.6.8 reliability fixes:
|
|
57
|
+
|
|
58
|
+
- **Workspace-safe recovery.** Explicit operator reindexing carries the
|
|
59
|
+
document's workspace into every durable pipeline stage, including RLS reads.
|
|
60
|
+
- **Consistent operator authentication.** Both documented admin key headers
|
|
61
|
+
bypass interactive rate limits during maintenance operations.
|
|
62
|
+
|
|
63
|
+
DocsMint 0.6.8 includes the self-host reliability fixes introduced in 0.6.7:
|
|
44
64
|
|
|
45
65
|
- **Reliable HTTPS sessions.** Every protected SvelteKit route accepts Better
|
|
46
66
|
Auth's secure production cookie as well as the local-development cookie.
|
|
@@ -111,9 +131,9 @@ DocsMint 0.6.1 includes the mobile and PWA foundation introduced in 0.6.0:
|
|
|
111
131
|
- **Stable document discovery.** The public API and TypeScript SDK provide
|
|
112
132
|
deterministic cursor pagination and global sorting by title, category,
|
|
113
133
|
folder, or update time, with canonical category and folder metadata.
|
|
114
|
-
- **
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
- **Safe standalone sharing.** Standalone DocsMint exposes public links with
|
|
135
|
+
expiry, optional passwords, Copy, and revoke controls. Host-managed consumers
|
|
136
|
+
retain their existing restricted invitation workflow.
|
|
117
137
|
- **Knowledge built for agents.** REST, the typed SDK, CLI, and MCP server expose
|
|
118
138
|
scoped document operations, while multilingual hybrid search and GraphRAG
|
|
119
139
|
help people and AI agents retrieve connected knowledge instead of isolated
|
|
@@ -216,6 +236,9 @@ The canonical local ports are:
|
|
|
216
236
|
See [Deployment](docs/DEPLOYMENT.md) for domains, TLS, provider tuning,
|
|
217
237
|
backups, and production operation.
|
|
218
238
|
|
|
239
|
+
Embedding provider URLs, models, and credentials are deployment configuration.
|
|
240
|
+
They are never stored in browser settings or local storage.
|
|
241
|
+
|
|
219
242
|
## Use DocsMint from the terminal
|
|
220
243
|
|
|
221
244
|
The published package includes the CLI. It connects to an already running
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ZERO_UUID,
|
|
6
6
|
} from "@hiai-docs/db/with-tenant";
|
|
7
7
|
import { and, desc, eq } from "drizzle-orm";
|
|
8
|
+
import { isAccountPurgeFencedError } from "./account-purge-fence";
|
|
8
9
|
import { decryptApiKey, encryptApiKey } from "./api-key-encryption";
|
|
9
10
|
|
|
10
11
|
const API_KEY_ADMIN_TENANT = adminTenantContext(ZERO_UUID);
|
|
@@ -228,12 +229,19 @@ export async function validateApiKey(key: string): Promise<{
|
|
|
228
229
|
if (!scopes) return null;
|
|
229
230
|
|
|
230
231
|
// Update last_used_at
|
|
231
|
-
|
|
232
|
-
tx
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
232
|
+
try {
|
|
233
|
+
await withTenant(API_KEY_ADMIN_TENANT, (tx) =>
|
|
234
|
+
tx
|
|
235
|
+
.update(apiKeys)
|
|
236
|
+
.set({ lastUsedAt: new Date() })
|
|
237
|
+
.where(eq(apiKeys.id, row.id)),
|
|
238
|
+
);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// Preserve the authenticated principal so guarded routes return the
|
|
241
|
+
// stable public fence error. The key row itself remains frozen until
|
|
242
|
+
// lifecycle cleanup deletes it.
|
|
243
|
+
if (!isAccountPurgeFencedError(error)) throw error;
|
|
244
|
+
}
|
|
237
245
|
|
|
238
246
|
return {
|
|
239
247
|
id: row.id,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
accounts,
|
|
3
|
+
apiKeys,
|
|
4
|
+
attachmentStorageCleanupOutbox,
|
|
3
5
|
attachments,
|
|
4
6
|
auditLog,
|
|
5
7
|
categories,
|
|
@@ -7,6 +9,7 @@ import {
|
|
|
7
9
|
documents,
|
|
8
10
|
folders,
|
|
9
11
|
lifecycleOperations,
|
|
12
|
+
pendingAttachmentUploads,
|
|
10
13
|
sessions,
|
|
11
14
|
shareLinks,
|
|
12
15
|
tags,
|
|
@@ -23,9 +26,21 @@ import type {
|
|
|
23
26
|
UserDataExportRecord,
|
|
24
27
|
UserDataLifecycle,
|
|
25
28
|
} from "@hiai-docs/sdk";
|
|
26
|
-
import { and, eq, inArray, lt, or, sql } from "drizzle-orm";
|
|
29
|
+
import { and, asc, eq, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
|
30
|
+
import {
|
|
31
|
+
drainAttachmentStorageCleanupOutbox,
|
|
32
|
+
stageAttachmentStorageCleanup,
|
|
33
|
+
} from "./attachment-storage-cleanup";
|
|
34
|
+
import {
|
|
35
|
+
type PendingAttachmentUploadRow,
|
|
36
|
+
stagePendingAttachmentCleanup,
|
|
37
|
+
} from "./attachment-upload-cleanup";
|
|
38
|
+
import { acquireDocumentPipelineLocks } from "./document-pipeline-serialization";
|
|
39
|
+
import type { RuntimeAttachmentQuotaAdmission } from "./runtime-options";
|
|
40
|
+
import { getDocsMintRuntimeOptions } from "./runtime-options";
|
|
27
41
|
|
|
28
42
|
const LEASE_MS = 60_000;
|
|
43
|
+
export const LIFECYCLE_CLEANUP_PAGE_SIZE = 100;
|
|
29
44
|
|
|
30
45
|
type PersistentOperation = typeof lifecycleOperations.$inferSelect;
|
|
31
46
|
|
|
@@ -49,6 +64,16 @@ export class LifecycleLeaseLostError extends Error {
|
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
/** An issued direct-upload URL remains usable until its fixed expiry. */
|
|
68
|
+
export class LifecyclePendingAttachmentUploadsError extends Error {
|
|
69
|
+
readonly code = "pending_attachment_uploads";
|
|
70
|
+
|
|
71
|
+
constructor() {
|
|
72
|
+
super("Pending attachment upload URLs have not expired");
|
|
73
|
+
this.name = "LifecyclePendingAttachmentUploadsError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
52
77
|
export type LifecycleRuntimeAdapters = Readonly<{
|
|
53
78
|
/** Checks the host-owned fence immediately before the first mutation. */
|
|
54
79
|
verifyPurgeFence: (
|
|
@@ -75,6 +100,8 @@ export type LifecycleRuntimeAdapters = Readonly<{
|
|
|
75
100
|
documentIds: readonly string[],
|
|
76
101
|
signal?: AbortSignal,
|
|
77
102
|
) => Promise<number>;
|
|
103
|
+
/** Generic OSS quota saga used while settling workspace attachment cleanup. */
|
|
104
|
+
attachmentStorageQuotaAdmission?: RuntimeAttachmentQuotaAdmission;
|
|
78
105
|
}>;
|
|
79
106
|
|
|
80
107
|
/**
|
|
@@ -116,6 +143,8 @@ function safeErrorCode(error: unknown): string {
|
|
|
116
143
|
return "aborted";
|
|
117
144
|
if (error instanceof LifecycleFenceRejectedError) return error.code;
|
|
118
145
|
if (error instanceof LifecycleLeaseLostError) return error.code;
|
|
146
|
+
if (error instanceof LifecyclePendingAttachmentUploadsError)
|
|
147
|
+
return error.code;
|
|
119
148
|
return "persistence_failed";
|
|
120
149
|
}
|
|
121
150
|
|
|
@@ -509,7 +538,17 @@ export function createPersistentLifecycleService(
|
|
|
509
538
|
const deletedByDomain = operationCounts(operation);
|
|
510
539
|
try {
|
|
511
540
|
throwIfAborted(ctx.signal);
|
|
541
|
+
await runtime.verifyPurgeFence(ctx, gate.fenceToken);
|
|
512
542
|
await withActor(ctx.actorUserId, async (tx) => {
|
|
543
|
+
await tx.execute(
|
|
544
|
+
sql`SELECT public.acquire_account_purge_fence_lock(${ctx.actorUserId}::uuid)`,
|
|
545
|
+
);
|
|
546
|
+
const [actor] = await tx
|
|
547
|
+
.select({ id: users.id })
|
|
548
|
+
.from(users)
|
|
549
|
+
.where(eq(users.id, ctx.actorUserId))
|
|
550
|
+
.limit(1);
|
|
551
|
+
if (!actor) throw new LifecycleLeaseLostError();
|
|
513
552
|
const updated = await tx
|
|
514
553
|
.update(lifecycleOperations)
|
|
515
554
|
.set({
|
|
@@ -525,10 +564,21 @@ export function createPersistentLifecycleService(
|
|
|
525
564
|
.returning({ id: lifecycleOperations.id });
|
|
526
565
|
requireLeaseWrite(updated);
|
|
527
566
|
});
|
|
528
|
-
|
|
567
|
+
const withCleanupActor = <T>(
|
|
568
|
+
operationAction: (tx: TenantTransaction) => Promise<T>,
|
|
569
|
+
) =>
|
|
570
|
+
withActor(ctx.actorUserId, async (tx) => {
|
|
571
|
+
await tx.execute(
|
|
572
|
+
sql`SELECT set_config('app.lifecycle_operation_id', ${operation.id}, true)`,
|
|
573
|
+
);
|
|
574
|
+
await tx.execute(
|
|
575
|
+
sql`SELECT set_config('app.lifecycle_lease_owner', ${leaseOwner}, true)`,
|
|
576
|
+
);
|
|
577
|
+
return operationAction(tx);
|
|
578
|
+
});
|
|
529
579
|
const dbDelete = <T extends { id: string }>(
|
|
530
580
|
action: (tx: TenantTransaction) => Promise<T[]>,
|
|
531
|
-
) =>
|
|
581
|
+
) => withCleanupActor(action).then((rows) => rows.length);
|
|
532
582
|
await runStep(
|
|
533
583
|
operation,
|
|
534
584
|
ctx.actorUserId,
|
|
@@ -537,6 +587,126 @@ export function createPersistentLifecycleService(
|
|
|
537
587
|
deletedByDomain,
|
|
538
588
|
() => runtime.cancelAccountJobs(ctx.actorUserId, ctx.signal),
|
|
539
589
|
);
|
|
590
|
+
await runStep(
|
|
591
|
+
operation,
|
|
592
|
+
ctx.actorUserId,
|
|
593
|
+
leaseOwner,
|
|
594
|
+
"settle_pending_attachment_uploads",
|
|
595
|
+
deletedByDomain,
|
|
596
|
+
async () => {
|
|
597
|
+
let staged = 0;
|
|
598
|
+
for (;;) {
|
|
599
|
+
const page = await withCleanupActor(async (tx) => {
|
|
600
|
+
const pending = await tx
|
|
601
|
+
.select({
|
|
602
|
+
id: pendingAttachmentUploads.id,
|
|
603
|
+
documentId: pendingAttachmentUploads.documentId,
|
|
604
|
+
ownerUserId: sql<string>`public.lifecycle_child_document_owner(
|
|
605
|
+
${pendingAttachmentUploads.documentId},
|
|
606
|
+
${ctx.actorUserId}::uuid
|
|
607
|
+
)`,
|
|
608
|
+
actorUserId: pendingAttachmentUploads.actorUserId,
|
|
609
|
+
workspaceId: pendingAttachmentUploads.workspaceId,
|
|
610
|
+
storageKey: pendingAttachmentUploads.storageKey,
|
|
611
|
+
tokenHash: pendingAttachmentUploads.tokenHash,
|
|
612
|
+
filename: pendingAttachmentUploads.filename,
|
|
613
|
+
mimeType: pendingAttachmentUploads.mimeType,
|
|
614
|
+
declaredSize: pendingAttachmentUploads.declaredSize,
|
|
615
|
+
quotaReservationId:
|
|
616
|
+
pendingAttachmentUploads.quotaReservationId,
|
|
617
|
+
quotaOperationKey:
|
|
618
|
+
pendingAttachmentUploads.quotaOperationKey,
|
|
619
|
+
quotaState: pendingAttachmentUploads.quotaState,
|
|
620
|
+
actualSize: pendingAttachmentUploads.actualSize,
|
|
621
|
+
urlIssuedAt: pendingAttachmentUploads.urlIssuedAt,
|
|
622
|
+
expiresAt: pendingAttachmentUploads.expiresAt,
|
|
623
|
+
leaseOwner: pendingAttachmentUploads.leaseOwner,
|
|
624
|
+
})
|
|
625
|
+
.from(pendingAttachmentUploads)
|
|
626
|
+
.where(
|
|
627
|
+
eq(pendingAttachmentUploads.actorUserId, ctx.actorUserId),
|
|
628
|
+
)
|
|
629
|
+
.orderBy(asc(pendingAttachmentUploads.id))
|
|
630
|
+
.limit(LIFECYCLE_CLEANUP_PAGE_SIZE);
|
|
631
|
+
for (const row of pending) {
|
|
632
|
+
await stagePendingAttachmentCleanup(
|
|
633
|
+
tx,
|
|
634
|
+
row as PendingAttachmentUploadRow,
|
|
635
|
+
ctx.actorUserId,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return pending.length;
|
|
639
|
+
});
|
|
640
|
+
staged += page;
|
|
641
|
+
if (page === 0) break;
|
|
642
|
+
}
|
|
643
|
+
const leftover = await withCleanupActor((tx) =>
|
|
644
|
+
tx
|
|
645
|
+
.select({ id: attachmentStorageCleanupOutbox.id })
|
|
646
|
+
.from(attachmentStorageCleanupOutbox)
|
|
647
|
+
.where(
|
|
648
|
+
or(
|
|
649
|
+
eq(
|
|
650
|
+
attachmentStorageCleanupOutbox.actorUserId,
|
|
651
|
+
ctx.actorUserId,
|
|
652
|
+
),
|
|
653
|
+
eq(
|
|
654
|
+
attachmentStorageCleanupOutbox.ownerUserId,
|
|
655
|
+
ctx.actorUserId,
|
|
656
|
+
),
|
|
657
|
+
eq(
|
|
658
|
+
attachmentStorageCleanupOutbox.requestedByUserId,
|
|
659
|
+
ctx.actorUserId,
|
|
660
|
+
),
|
|
661
|
+
),
|
|
662
|
+
)
|
|
663
|
+
.limit(1),
|
|
664
|
+
);
|
|
665
|
+
if (staged > 0 || leftover.length > 0) {
|
|
666
|
+
for (let page = 0; page < 10_000; page += 1) {
|
|
667
|
+
const result = await drainAttachmentStorageCleanupOutbox({
|
|
668
|
+
deleteObjects: runtime.deleteObjects,
|
|
669
|
+
actorUserId: ctx.actorUserId,
|
|
670
|
+
quotaAdmission:
|
|
671
|
+
runtime.attachmentStorageQuotaAdmission ??
|
|
672
|
+
getDocsMintRuntimeOptions()
|
|
673
|
+
?.attachmentStorageQuotaAdmission,
|
|
674
|
+
signal: ctx.signal,
|
|
675
|
+
pageSize: LIFECYCLE_CLEANUP_PAGE_SIZE,
|
|
676
|
+
maxPages: 1,
|
|
677
|
+
});
|
|
678
|
+
if (result.claimed === 0) break;
|
|
679
|
+
if (result.failed > 0 || result.deferred > 0)
|
|
680
|
+
throw new LifecyclePendingAttachmentUploadsError();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const retained = await withCleanupActor((tx) =>
|
|
684
|
+
tx
|
|
685
|
+
.select({ id: attachmentStorageCleanupOutbox.id })
|
|
686
|
+
.from(attachmentStorageCleanupOutbox)
|
|
687
|
+
.where(
|
|
688
|
+
or(
|
|
689
|
+
eq(
|
|
690
|
+
attachmentStorageCleanupOutbox.actorUserId,
|
|
691
|
+
ctx.actorUserId,
|
|
692
|
+
),
|
|
693
|
+
eq(
|
|
694
|
+
attachmentStorageCleanupOutbox.ownerUserId,
|
|
695
|
+
ctx.actorUserId,
|
|
696
|
+
),
|
|
697
|
+
eq(
|
|
698
|
+
attachmentStorageCleanupOutbox.requestedByUserId,
|
|
699
|
+
ctx.actorUserId,
|
|
700
|
+
),
|
|
701
|
+
),
|
|
702
|
+
)
|
|
703
|
+
.limit(1),
|
|
704
|
+
);
|
|
705
|
+
if (retained.length > 0)
|
|
706
|
+
throw new LifecyclePendingAttachmentUploadsError();
|
|
707
|
+
return staged;
|
|
708
|
+
},
|
|
709
|
+
);
|
|
540
710
|
await runStep(
|
|
541
711
|
operation,
|
|
542
712
|
ctx.actorUserId,
|
|
@@ -563,7 +733,12 @@ export function createPersistentLifecycleService(
|
|
|
563
733
|
tx
|
|
564
734
|
.select({ id: documents.id })
|
|
565
735
|
.from(documents)
|
|
566
|
-
.where(
|
|
736
|
+
.where(
|
|
737
|
+
and(
|
|
738
|
+
eq(documents.ownerId, ctx.actorUserId),
|
|
739
|
+
isNull(documents.workspaceId),
|
|
740
|
+
),
|
|
741
|
+
),
|
|
567
742
|
);
|
|
568
743
|
const documentIds = owned.map((row) => row.id);
|
|
569
744
|
await runStep(
|
|
@@ -573,14 +748,19 @@ export function createPersistentLifecycleService(
|
|
|
573
748
|
"remove_document_versions",
|
|
574
749
|
deletedByDomain,
|
|
575
750
|
() =>
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
751
|
+
dbDelete((tx) =>
|
|
752
|
+
tx
|
|
753
|
+
.delete(versions)
|
|
754
|
+
.where(
|
|
755
|
+
documentIds.length
|
|
756
|
+
? or(
|
|
757
|
+
eq(versions.createdBy, ctx.actorUserId),
|
|
758
|
+
inArray(versions.documentId, documentIds),
|
|
759
|
+
)
|
|
760
|
+
: eq(versions.createdBy, ctx.actorUserId),
|
|
582
761
|
)
|
|
583
|
-
|
|
762
|
+
.returning({ id: versions.id }),
|
|
763
|
+
),
|
|
584
764
|
);
|
|
585
765
|
await runStep(
|
|
586
766
|
operation,
|
|
@@ -606,29 +786,183 @@ export function createPersistentLifecycleService(
|
|
|
606
786
|
deletedByDomain,
|
|
607
787
|
() => runtime.removeGraphState(documentIds, ctx.signal),
|
|
608
788
|
);
|
|
609
|
-
const objectRows = documentIds.length
|
|
610
|
-
? await withActor(ctx.actorUserId, (tx) =>
|
|
611
|
-
tx
|
|
612
|
-
.select({
|
|
613
|
-
id: attachments.id,
|
|
614
|
-
storageKey: attachments.storageKey,
|
|
615
|
-
})
|
|
616
|
-
.from(attachments)
|
|
617
|
-
.where(inArray(attachments.documentId, documentIds)),
|
|
618
|
-
)
|
|
619
|
-
: [];
|
|
620
789
|
await runStep(
|
|
621
790
|
operation,
|
|
622
791
|
ctx.actorUserId,
|
|
623
792
|
leaseOwner,
|
|
624
793
|
"delete_attachment_objects",
|
|
625
794
|
deletedByDomain,
|
|
795
|
+
async () => {
|
|
796
|
+
let removed = 0;
|
|
797
|
+
for (;;) {
|
|
798
|
+
const page = await withCleanupActor(async (tx) => {
|
|
799
|
+
const objectRows = await tx
|
|
800
|
+
.select({
|
|
801
|
+
id: attachments.id,
|
|
802
|
+
documentId: attachments.documentId,
|
|
803
|
+
storageKey: attachments.storageKey,
|
|
804
|
+
size: attachments.size,
|
|
805
|
+
uploadedBy: attachments.uploadedBy,
|
|
806
|
+
workspaceId: attachments.workspaceId,
|
|
807
|
+
ownerUserId: sql<string>`public.lifecycle_child_document_owner(
|
|
808
|
+
${attachments.documentId},
|
|
809
|
+
${ctx.actorUserId}::uuid
|
|
810
|
+
)`,
|
|
811
|
+
})
|
|
812
|
+
.from(attachments)
|
|
813
|
+
.where(
|
|
814
|
+
documentIds.length
|
|
815
|
+
? or(
|
|
816
|
+
eq(attachments.uploadedBy, ctx.actorUserId),
|
|
817
|
+
inArray(attachments.documentId, documentIds),
|
|
818
|
+
)
|
|
819
|
+
: eq(attachments.uploadedBy, ctx.actorUserId),
|
|
820
|
+
)
|
|
821
|
+
.orderBy(asc(attachments.id))
|
|
822
|
+
.limit(LIFECYCLE_CLEANUP_PAGE_SIZE);
|
|
823
|
+
if (objectRows.length === 0) return 0;
|
|
824
|
+
for (const row of objectRows) {
|
|
825
|
+
await stageAttachmentStorageCleanup(tx, {
|
|
826
|
+
sourceKind: "attachment",
|
|
827
|
+
sourceId: row.id,
|
|
828
|
+
storageKey: row.storageKey,
|
|
829
|
+
documentId: row.documentId,
|
|
830
|
+
actorUserId: row.uploadedBy,
|
|
831
|
+
ownerUserId: row.ownerUserId,
|
|
832
|
+
requestedByUserId: ctx.actorUserId,
|
|
833
|
+
workspaceId: row.workspaceId,
|
|
834
|
+
size: row.size,
|
|
835
|
+
quotaOperationKey: `attachment:${row.documentId}:${row.storageKey}`,
|
|
836
|
+
quotaReleaseKind: row.workspaceId ? "committed" : "none",
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const deleted = await tx
|
|
840
|
+
.delete(attachments)
|
|
841
|
+
.where(
|
|
842
|
+
inArray(
|
|
843
|
+
attachments.id,
|
|
844
|
+
objectRows.map((row) => row.id),
|
|
845
|
+
),
|
|
846
|
+
)
|
|
847
|
+
.returning({ id: attachments.id });
|
|
848
|
+
return deleted.length;
|
|
849
|
+
});
|
|
850
|
+
removed += page;
|
|
851
|
+
if (page === 0) break;
|
|
852
|
+
}
|
|
853
|
+
return removed;
|
|
854
|
+
},
|
|
855
|
+
);
|
|
856
|
+
await runStep(
|
|
857
|
+
operation,
|
|
858
|
+
ctx.actorUserId,
|
|
859
|
+
leaseOwner,
|
|
860
|
+
"release_attachment_quota",
|
|
861
|
+
deletedByDomain,
|
|
862
|
+
async () => {
|
|
863
|
+
let deleted = 0;
|
|
864
|
+
for (let page = 0; page < 10_000; page += 1) {
|
|
865
|
+
const result = await drainAttachmentStorageCleanupOutbox({
|
|
866
|
+
deleteObjects: runtime.deleteObjects,
|
|
867
|
+
actorUserId: ctx.actorUserId,
|
|
868
|
+
quotaAdmission:
|
|
869
|
+
runtime.attachmentStorageQuotaAdmission ??
|
|
870
|
+
getDocsMintRuntimeOptions()?.attachmentStorageQuotaAdmission,
|
|
871
|
+
signal: ctx.signal,
|
|
872
|
+
pageSize: LIFECYCLE_CLEANUP_PAGE_SIZE,
|
|
873
|
+
maxPages: 1,
|
|
874
|
+
});
|
|
875
|
+
deleted += result.deleted;
|
|
876
|
+
if (result.claimed === 0) break;
|
|
877
|
+
if (result.failed > 0 || result.deferred > 0)
|
|
878
|
+
throw new LifecyclePendingAttachmentUploadsError();
|
|
879
|
+
}
|
|
880
|
+
const retained = await withCleanupActor((tx) =>
|
|
881
|
+
tx
|
|
882
|
+
.select({ id: attachmentStorageCleanupOutbox.id })
|
|
883
|
+
.from(attachmentStorageCleanupOutbox)
|
|
884
|
+
.where(
|
|
885
|
+
or(
|
|
886
|
+
eq(
|
|
887
|
+
attachmentStorageCleanupOutbox.actorUserId,
|
|
888
|
+
ctx.actorUserId,
|
|
889
|
+
),
|
|
890
|
+
eq(
|
|
891
|
+
attachmentStorageCleanupOutbox.ownerUserId,
|
|
892
|
+
ctx.actorUserId,
|
|
893
|
+
),
|
|
894
|
+
eq(
|
|
895
|
+
attachmentStorageCleanupOutbox.requestedByUserId,
|
|
896
|
+
ctx.actorUserId,
|
|
897
|
+
),
|
|
898
|
+
),
|
|
899
|
+
)
|
|
900
|
+
.limit(1),
|
|
901
|
+
);
|
|
902
|
+
if (retained.length > 0)
|
|
903
|
+
throw new LifecyclePendingAttachmentUploadsError();
|
|
904
|
+
return deleted;
|
|
905
|
+
},
|
|
906
|
+
);
|
|
907
|
+
await runStep(
|
|
908
|
+
operation,
|
|
909
|
+
ctx.actorUserId,
|
|
910
|
+
leaseOwner,
|
|
911
|
+
"remove_attachment_rows",
|
|
912
|
+
deletedByDomain,
|
|
913
|
+
() => Promise.resolve(0),
|
|
914
|
+
);
|
|
915
|
+
await runStep(
|
|
916
|
+
operation,
|
|
917
|
+
ctx.actorUserId,
|
|
918
|
+
leaseOwner,
|
|
919
|
+
"clear_redis_state",
|
|
920
|
+
deletedByDomain,
|
|
921
|
+
() => runtime.clearAccountRedisState(ctx.actorUserId, ctx.signal),
|
|
922
|
+
);
|
|
923
|
+
for (const step of orderedSteps)
|
|
924
|
+
if (step.purge)
|
|
925
|
+
await runStep(
|
|
926
|
+
operation,
|
|
927
|
+
ctx.actorUserId,
|
|
928
|
+
leaseOwner,
|
|
929
|
+
`host:${step.id}`,
|
|
930
|
+
deletedByDomain,
|
|
931
|
+
async () => (await step.purge?.(ctx))?.deletedCount ?? 0,
|
|
932
|
+
);
|
|
933
|
+
await runStep(
|
|
934
|
+
operation,
|
|
935
|
+
ctx.actorUserId,
|
|
936
|
+
leaseOwner,
|
|
937
|
+
"remove_subject_documents",
|
|
938
|
+
deletedByDomain,
|
|
626
939
|
() =>
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
940
|
+
dbDelete(async (tx) => {
|
|
941
|
+
const currentDocuments = await tx
|
|
942
|
+
.select({ id: documents.id })
|
|
943
|
+
.from(documents)
|
|
944
|
+
.where(
|
|
945
|
+
and(
|
|
946
|
+
eq(documents.ownerId, ctx.actorUserId),
|
|
947
|
+
isNull(documents.workspaceId),
|
|
948
|
+
),
|
|
949
|
+
);
|
|
950
|
+
const currentDocumentIds = currentDocuments.map(({ id }) => id);
|
|
951
|
+
await acquireDocumentPipelineLocks(tx, currentDocumentIds);
|
|
952
|
+
if (currentDocumentIds.length === 0) return [];
|
|
953
|
+
return tx
|
|
954
|
+
.delete(documents)
|
|
955
|
+
.where(
|
|
956
|
+
and(
|
|
957
|
+
eq(documents.ownerId, ctx.actorUserId),
|
|
958
|
+
isNull(documents.workspaceId),
|
|
959
|
+
),
|
|
960
|
+
)
|
|
961
|
+
.returning({ id: documents.id });
|
|
962
|
+
}),
|
|
631
963
|
);
|
|
964
|
+
// Parent metadata uses ON DELETE SET NULL. Delete documents first so
|
|
965
|
+
// those referential actions do not become fenced document UPDATEs.
|
|
632
966
|
await runStep(
|
|
633
967
|
operation,
|
|
634
968
|
ctx.actorUserId,
|
|
@@ -639,7 +973,12 @@ export function createPersistentLifecycleService(
|
|
|
639
973
|
dbDelete((tx) =>
|
|
640
974
|
tx
|
|
641
975
|
.delete(folders)
|
|
642
|
-
.where(
|
|
976
|
+
.where(
|
|
977
|
+
and(
|
|
978
|
+
eq(folders.ownerId, ctx.actorUserId),
|
|
979
|
+
isNull(folders.workspaceId),
|
|
980
|
+
),
|
|
981
|
+
)
|
|
643
982
|
.returning({ id: folders.id }),
|
|
644
983
|
),
|
|
645
984
|
);
|
|
@@ -653,7 +992,12 @@ export function createPersistentLifecycleService(
|
|
|
653
992
|
dbDelete((tx) =>
|
|
654
993
|
tx
|
|
655
994
|
.delete(tags)
|
|
656
|
-
.where(
|
|
995
|
+
.where(
|
|
996
|
+
and(
|
|
997
|
+
eq(tags.ownerId, ctx.actorUserId),
|
|
998
|
+
isNull(tags.workspaceId),
|
|
999
|
+
),
|
|
1000
|
+
)
|
|
657
1001
|
.returning({ id: tags.id }),
|
|
658
1002
|
),
|
|
659
1003
|
);
|
|
@@ -667,7 +1011,12 @@ export function createPersistentLifecycleService(
|
|
|
667
1011
|
dbDelete((tx) =>
|
|
668
1012
|
tx
|
|
669
1013
|
.delete(categories)
|
|
670
|
-
.where(
|
|
1014
|
+
.where(
|
|
1015
|
+
and(
|
|
1016
|
+
eq(categories.ownerId, ctx.actorUserId),
|
|
1017
|
+
isNull(categories.workspaceId),
|
|
1018
|
+
),
|
|
1019
|
+
)
|
|
671
1020
|
.returning({ id: categories.id }),
|
|
672
1021
|
),
|
|
673
1022
|
);
|
|
@@ -675,55 +1024,16 @@ export function createPersistentLifecycleService(
|
|
|
675
1024
|
operation,
|
|
676
1025
|
ctx.actorUserId,
|
|
677
1026
|
leaseOwner,
|
|
678
|
-
"
|
|
679
|
-
deletedByDomain,
|
|
680
|
-
() =>
|
|
681
|
-
objectRows.length
|
|
682
|
-
? dbDelete((tx) =>
|
|
683
|
-
tx
|
|
684
|
-
.delete(attachments)
|
|
685
|
-
.where(
|
|
686
|
-
inArray(
|
|
687
|
-
attachments.id,
|
|
688
|
-
objectRows.map((row) => row.id),
|
|
689
|
-
),
|
|
690
|
-
)
|
|
691
|
-
.returning({ id: attachments.id }),
|
|
692
|
-
)
|
|
693
|
-
: Promise.resolve(0),
|
|
694
|
-
);
|
|
695
|
-
await runStep(
|
|
696
|
-
operation,
|
|
697
|
-
ctx.actorUserId,
|
|
698
|
-
leaseOwner,
|
|
699
|
-
"remove_subject_documents",
|
|
1027
|
+
"remove_api_keys",
|
|
700
1028
|
deletedByDomain,
|
|
701
1029
|
() =>
|
|
702
1030
|
dbDelete((tx) =>
|
|
703
1031
|
tx
|
|
704
|
-
.delete(
|
|
705
|
-
.where(eq(
|
|
706
|
-
.returning({ id:
|
|
1032
|
+
.delete(apiKeys)
|
|
1033
|
+
.where(eq(apiKeys.ownerId, ctx.actorUserId))
|
|
1034
|
+
.returning({ id: apiKeys.id }),
|
|
707
1035
|
),
|
|
708
1036
|
);
|
|
709
|
-
await runStep(
|
|
710
|
-
operation,
|
|
711
|
-
ctx.actorUserId,
|
|
712
|
-
leaseOwner,
|
|
713
|
-
"clear_redis_state",
|
|
714
|
-
deletedByDomain,
|
|
715
|
-
() => runtime.clearAccountRedisState(ctx.actorUserId, ctx.signal),
|
|
716
|
-
);
|
|
717
|
-
for (const step of orderedSteps)
|
|
718
|
-
if (step.purge)
|
|
719
|
-
await runStep(
|
|
720
|
-
operation,
|
|
721
|
-
ctx.actorUserId,
|
|
722
|
-
leaseOwner,
|
|
723
|
-
`host:${step.id}`,
|
|
724
|
-
deletedByDomain,
|
|
725
|
-
async () => (await step.purge?.(ctx))?.deletedCount ?? 0,
|
|
726
|
-
);
|
|
727
1037
|
await runStep(
|
|
728
1038
|
operation,
|
|
729
1039
|
ctx.actorUserId,
|
|
@@ -752,6 +1062,20 @@ export function createPersistentLifecycleService(
|
|
|
752
1062
|
.returning({ id: accounts.id }),
|
|
753
1063
|
),
|
|
754
1064
|
);
|
|
1065
|
+
await runStep(
|
|
1066
|
+
operation,
|
|
1067
|
+
ctx.actorUserId,
|
|
1068
|
+
leaseOwner,
|
|
1069
|
+
"remove_subject_audit",
|
|
1070
|
+
deletedByDomain,
|
|
1071
|
+
() =>
|
|
1072
|
+
dbDelete((tx) =>
|
|
1073
|
+
tx
|
|
1074
|
+
.delete(auditLog)
|
|
1075
|
+
.where(eq(auditLog.actorId, ctx.actorUserId))
|
|
1076
|
+
.returning({ id: auditLog.id }),
|
|
1077
|
+
),
|
|
1078
|
+
);
|
|
755
1079
|
await runStep(
|
|
756
1080
|
operation,
|
|
757
1081
|
ctx.actorUserId,
|
|
@@ -780,7 +1104,7 @@ export function createPersistentLifecycleService(
|
|
|
780
1104
|
"write_deletion_audit",
|
|
781
1105
|
deletedByDomain,
|
|
782
1106
|
async () => {
|
|
783
|
-
await
|
|
1107
|
+
await withCleanupActor((tx) =>
|
|
784
1108
|
tx.insert(auditLog).values({
|
|
785
1109
|
actorId: ctx.actorUserId,
|
|
786
1110
|
action: "account_data_purged",
|