@hiai-gg/docsmint 0.6.8 → 0.7.1

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.
Files changed (138) hide show
  1. package/README.md +90 -99
  2. package/backend/src/lib/api-keys.ts +14 -6
  3. package/backend/src/lib/lifecycle-service.ts +397 -73
  4. package/dist/backend/index.js +13039 -10865
  5. package/dist/backend-account-runtime-cleanup.js +152 -42
  6. package/dist/backend-api-key-facade.js +207 -61
  7. package/dist/backend-pipeline-cancellation.js +144 -42
  8. package/dist/client.d.ts +12 -2
  9. package/dist/client.js +83 -30
  10. package/dist/frontend/{CategoryDialog-CbXBMBZB.js → CategoryDialog-t_WIvFAG.js} +5 -5
  11. package/dist/frontend/{ConfirmDialog-DgMTQBq4.js → ConfirmDialog-_LlrA--f.js} +3 -3
  12. package/dist/frontend/{CreateSnapshotDialog-FUq-ZfBH.js → CreateSnapshotDialog-DW3PwAt5.js} +4 -4
  13. package/dist/frontend/{DatePicker-D6E-EEtZ.js → DatePicker-CAw0wZZK.js} +9 -9
  14. package/dist/frontend/{DeleteDialog-8k6JYWCx.js → DeleteDialog-L1A3FhMM.js} +1 -1
  15. package/dist/frontend/{FolderNode-DAAn_T8_.js → FolderNode-DzTqitR8.js} +2 -2
  16. package/dist/frontend/{HiAiEditor-DSVG3GNd.js → HiAiEditor-B7MK19_j.js} +7 -7
  17. package/dist/frontend/{SettingsDialog-BDNgf5rv.js → SettingsDialog-CS5L4nnU.js} +13 -13
  18. package/dist/frontend/ShareDialog-CwFa8mlk.js +281 -0
  19. package/dist/frontend/{Sidebar-DX08utdV.js → Sidebar-CDaFFmaQ.js} +18 -17
  20. package/dist/frontend/{TagCreateDialog-Bb0-Vtzh.js → TagCreateDialog-C7Avn2_h.js} +4 -4
  21. package/dist/frontend/api/attachments.js +1 -1
  22. package/dist/frontend/api/settings.js +1 -1
  23. package/dist/frontend/app-shell.js +3 -3
  24. package/dist/frontend/{attachments-B8AuDjno.js → attachments-B2aH3hXF.js} +10 -9
  25. package/dist/frontend/{auth-client-Cygmp1I2.js → auth-client-CnDZwPfp.js} +11 -11
  26. package/dist/frontend/components/category-dialog.js +1 -1
  27. package/dist/frontend/components/create-snapshot-dialog.js +1 -1
  28. package/dist/frontend/components/delete-dialog.js +1 -1
  29. package/dist/frontend/components/editor/compact-editor.js +1 -1
  30. package/dist/frontend/components/editor/document-editor.js +1 -1
  31. package/dist/frontend/components/editor/document-title.js +1 -1
  32. package/dist/frontend/components/editor/markdown-toggle.js +1 -1
  33. package/dist/frontend/components/folder-node.js +1 -1
  34. package/dist/frontend/components/save-as-dialog.js +4 -4
  35. package/dist/frontend/components/settings.js +1 -1
  36. package/dist/frontend/components/share-dialog.d.ts +2 -1
  37. package/dist/frontend/components/share-dialog.js +1 -1
  38. package/dist/frontend/components/sidebar.js +1 -1
  39. package/dist/frontend/components/tag-create-dialog.js +1 -1
  40. package/dist/frontend/components/version-history.js +3 -3
  41. package/dist/frontend/{confirm-dialog-CzmoZw3a.js → confirm-dialog-BW2h2NE8.js} +5 -5
  42. package/dist/frontend/dashboard.js +1008 -998
  43. package/dist/frontend/{db-ByrlYfaI.js → db-Bo1t_76T.js} +1 -1
  44. package/dist/frontend/{dialog-Ce79C9fd.js → dialog-C2iKObEu.js} +7 -7
  45. package/dist/frontend/{dropdown-menu-CJ_7V3N0.js → dropdown-menu-BXIl2E26.js} +12 -12
  46. package/dist/frontend/{floating-layer-anchor-S5RSefGN.js → floating-layer-anchor-BGIndLY-.js} +63 -63
  47. package/dist/frontend/i18n.js +1 -1
  48. package/dist/frontend/{identity-DyWeNzu_.js → identity-Ynrglr5i.js} +1 -1
  49. package/dist/frontend/{input-BWlOML_m.js → input-eHFb2qiL.js} +2 -2
  50. package/dist/frontend/{label-C2HfJNQ2.js → label-_I1iiaNU.js} +2 -2
  51. package/dist/frontend/messages-DTRe4U2V.js +1093 -0
  52. package/dist/frontend/offline/identity.js +1 -1
  53. package/dist/frontend/search.js +3 -3
  54. package/dist/frontend/{select-value-JCAp2PmU.js → select-value-CtMyrvDJ.js} +18 -18
  55. package/dist/frontend/{settings-DSrUl4hW.js → settings-BR48ihB4.js} +6 -23
  56. package/dist/frontend/{textarea-DOAE-h42.js → textarea-2eslfANY.js} +2 -2
  57. package/dist/frontend/{utils-BPDRytUU.js → utils-CDBqTgoV.js} +1 -1
  58. package/dist/frontend-ssr/api/attachments.js +1 -1
  59. package/dist/frontend-ssr/api/settings.js +1 -1
  60. package/dist/frontend-ssr/app-shell.js +3 -3
  61. package/dist/frontend-ssr/assets/{CategoryDialog-QHEv_9N4.js → CategoryDialog-B3MrZUho.js} +5 -5
  62. package/dist/frontend-ssr/assets/{ConfirmDialog-BhAelejz.js → ConfirmDialog-D1BDXXll.js} +3 -3
  63. package/dist/frontend-ssr/assets/{CreateSnapshotDialog-CI8vXXcN.js → CreateSnapshotDialog-NM0n1Whr.js} +4 -4
  64. package/dist/frontend-ssr/assets/{DatePicker-D84cJ-Eq.js → DatePicker-Dcok9XVm.js} +4 -4
  65. package/dist/frontend-ssr/assets/{DeleteDialog-DG9Lm38J.js → DeleteDialog-oHT_p_vG.js} +1 -1
  66. package/dist/frontend-ssr/assets/{FolderNode-kmilhDjO.js → FolderNode-B84NL8Rg.js} +2 -2
  67. package/dist/frontend-ssr/assets/{HiAiEditor-CUOJmKzT.js → HiAiEditor-tU0jmp6L.js} +6 -6
  68. package/dist/frontend-ssr/assets/{SettingsDialog-BjJIiVLc.js → SettingsDialog-Dkp79L1B.js} +13 -13
  69. package/dist/frontend-ssr/assets/{ShareDialog-C9C-XTm4.js → ShareDialog-B1-30sRR.js} +20 -7
  70. package/dist/frontend-ssr/assets/{Sidebar-C2sUfGNk.js → Sidebar-CGp45ngo.js} +14 -13
  71. package/dist/frontend-ssr/assets/{TagCreateDialog-DUPMitlL.js → TagCreateDialog-CGGtYSso.js} +4 -4
  72. package/dist/frontend-ssr/assets/{attachments-Cj3fQyUp.js → attachments-BdkOp8ha.js} +5 -4
  73. package/dist/frontend-ssr/assets/{auth-client-x26kqY8Z.js → auth-client-CojKIzqy.js} +13 -13
  74. package/dist/frontend-ssr/assets/{confirm-dialog-XuyaL8sr.js → confirm-dialog-B-oV5wYk.js} +5 -5
  75. package/dist/frontend-ssr/assets/{db-CnxeO66n.js → db-BhwJPEkn.js} +1 -1
  76. package/dist/frontend-ssr/assets/{dialog-BOV1u6zP.js → dialog-1SB5neow.js} +8 -8
  77. package/dist/frontend-ssr/assets/{dropdown-menu-Dn_vQq9b.js → dropdown-menu-DqoOkdqB.js} +12 -12
  78. package/dist/frontend-ssr/assets/{floating-layer-anchor-BIAu-252.js → floating-layer-anchor-BoiiIEiK.js} +71 -71
  79. package/dist/frontend-ssr/assets/{identity-B3uHoDtf.js → identity-DcMMBKWg.js} +1 -1
  80. package/dist/frontend-ssr/assets/{input-DezGsU3y.js → input-Df-QP3Km.js} +2 -2
  81. package/dist/frontend-ssr/assets/{label-Bj8-TfFN.js → label-CbUOUz6L.js} +2 -2
  82. package/dist/frontend-ssr/assets/{messages-BAQzILs4.js → messages-B1TjVIH8.js} +2 -2
  83. package/dist/frontend-ssr/assets/{select-value-DBhbkNcc.js → select-value-CXEDWrZg.js} +18 -18
  84. package/dist/frontend-ssr/assets/{settings-Bk5Hk9Vq.js → settings-Mp8EuFSn.js} +3 -21
  85. package/dist/frontend-ssr/assets/{textarea-Dev-qSEc.js → textarea-CHh8NuSS.js} +2 -2
  86. package/dist/frontend-ssr/assets/{utils-DtlPPXpJ.js → utils-DtX0ta7y.js} +1 -1
  87. package/dist/frontend-ssr/components/category-dialog.js +1 -1
  88. package/dist/frontend-ssr/components/create-snapshot-dialog.js +1 -1
  89. package/dist/frontend-ssr/components/delete-dialog.js +1 -1
  90. package/dist/frontend-ssr/components/editor/compact-editor.js +1 -1
  91. package/dist/frontend-ssr/components/editor/document-editor.js +1 -1
  92. package/dist/frontend-ssr/components/editor/document-title.js +1 -1
  93. package/dist/frontend-ssr/components/editor/markdown-toggle.js +1 -1
  94. package/dist/frontend-ssr/components/folder-node.js +1 -1
  95. package/dist/frontend-ssr/components/save-as-dialog.js +4 -4
  96. package/dist/frontend-ssr/components/settings.js +1 -1
  97. package/dist/frontend-ssr/components/share-dialog.js +1 -1
  98. package/dist/frontend-ssr/components/sidebar.js +1 -1
  99. package/dist/frontend-ssr/components/tag-create-dialog.js +1 -1
  100. package/dist/frontend-ssr/components/version-history.js +3 -3
  101. package/dist/frontend-ssr/dashboard.js +32 -30
  102. package/dist/frontend-ssr/i18n.js +1 -1
  103. package/dist/frontend-ssr/offline/identity.js +1 -1
  104. package/dist/frontend-ssr/search.js +3 -3
  105. package/dist/index.d.ts +5 -5
  106. package/dist/index.js +2 -2
  107. package/dist/lifecycle-persistent.d.ts +1 -1
  108. package/dist/lifecycle-runtime.js +52659 -338
  109. package/dist/mcp-cli.js +1056 -0
  110. package/dist/mcp-server.d.ts +39 -0
  111. package/dist/mcp-server.js +1053 -0
  112. package/dist/storage-quota.d.ts +5 -1
  113. package/dist/types.d.ts +7 -0
  114. package/dist/workspace.d.ts +11 -2
  115. package/dist/workspace.js +82 -10
  116. package/package.json +7 -13
  117. package/packages/cli/src/index.ts +4 -3
  118. package/packages/db/src/client.ts +56 -5
  119. package/packages/db/src/schema.ts +192 -1
  120. package/packages/db/src/with-tenant.ts +20 -2
  121. package/server.json +42 -5
  122. package/dist/frontend/ShareDialog-2Mty7hm4.js +0 -258
  123. package/dist/frontend/messages-DIovIZrK.js +0 -1093
  124. package/packages/mcp-server/src/capabilities.ts +0 -189
  125. package/packages/mcp-server/src/client.ts +0 -225
  126. package/packages/mcp-server/src/index.ts +0 -8
  127. package/packages/mcp-server/src/server.ts +0 -87
  128. package/packages/mcp-server/src/tools/create-document.ts +0 -34
  129. package/packages/mcp-server/src/tools/create-folder.ts +0 -31
  130. package/packages/mcp-server/src/tools/create-snapshot.ts +0 -27
  131. package/packages/mcp-server/src/tools/export-document.ts +0 -22
  132. package/packages/mcp-server/src/tools/get-document.ts +0 -22
  133. package/packages/mcp-server/src/tools/list-documents.ts +0 -34
  134. package/packages/mcp-server/src/tools/list-folders.ts +0 -26
  135. package/packages/mcp-server/src/tools/search.ts +0 -39
  136. package/packages/mcp-server/src/tools/update-document.ts +0 -38
  137. package/packages/mcp-server/src/tools/version-history.ts +0 -27
  138. package/packages/mcp-server/src/types.ts +0 -126
@@ -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
- await runtime.verifyPurgeFence(ctx, gate.fenceToken);
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
- ) => withActor(ctx.actorUserId, action).then((rows) => rows.length);
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(eq(documents.ownerId, ctx.actorUserId)),
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
- documentIds.length
577
- ? dbDelete((tx) =>
578
- tx
579
- .delete(versions)
580
- .where(inArray(versions.documentId, documentIds))
581
- .returning({ id: versions.id }),
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
- : Promise.resolve(0),
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
- runtime.deleteObjects(
628
- objectRows.map((row) => row.storageKey),
629
- ctx.signal,
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(eq(folders.ownerId, ctx.actorUserId))
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(eq(tags.ownerId, ctx.actorUserId))
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(eq(categories.ownerId, ctx.actorUserId))
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
- "remove_attachment_rows",
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(documents)
705
- .where(eq(documents.ownerId, ctx.actorUserId))
706
- .returning({ id: documents.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 withActor(ctx.actorUserId, (tx) =>
1107
+ await withCleanupActor((tx) =>
784
1108
  tx.insert(auditLog).values({
785
1109
  actorId: ctx.actorUserId,
786
1110
  action: "account_data_purged",