@hiai-gg/docsmint 0.3.5 → 0.3.7

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 (158) hide show
  1. package/README.md +0 -18
  2. package/backend/src/lib/lifecycle-service.ts +334 -219
  3. package/dist/backend/index.js +232 -18
  4. package/dist/backend-launcher.d.ts +15 -0
  5. package/dist/backend-launcher.js +22 -0
  6. package/dist/frontend/CategoryDialog--rIE7FrH.js +580 -0
  7. package/dist/frontend/ConfirmDialog-Cpzyb55f.js +212 -0
  8. package/dist/frontend/CreateSnapshotDialog-CmkV-n2A.js +216 -0
  9. package/dist/frontend/DatePicker-Byynca7V.js +257 -0
  10. package/dist/frontend/DeleteDialog-ap6nOywZ.js +174 -0
  11. package/dist/frontend/FolderDialog-DmN_trV0.js +247 -0
  12. package/dist/frontend/FolderNode-C4B8a2fd.js +317 -0
  13. package/dist/frontend/FolderTreeSelector-D6ijiZTA.js +100 -0
  14. package/dist/frontend/Icon-DtNo0N1U.js +64 -0
  15. package/dist/frontend/SettingsDialog-2kTaTXAs.js +992 -0
  16. package/dist/frontend/ShareDialog-BBDGNF5n.js +165 -0
  17. package/dist/frontend/TagCreateDialog-DiLZnJo4.js +258 -0
  18. package/dist/frontend/api/attachments.d.ts +6 -0
  19. package/dist/frontend/api/attachments.js +2 -0
  20. package/dist/frontend/api/categories.js +1 -1
  21. package/dist/frontend/api/documents.js +1 -1
  22. package/dist/frontend/api/folders.js +1 -1
  23. package/dist/frontend/api/settings.js +1 -1
  24. package/dist/frontend/api/share.d.ts +5 -0
  25. package/dist/frontend/api/share.js +2 -0
  26. package/dist/frontend/api/tags.js +1 -1
  27. package/dist/frontend/api-keys-CpkrA8rD.js +35 -0
  28. package/dist/frontend/attachments-BuILSs56.js +50 -0
  29. package/dist/frontend/{settings-PCrOtied.js → auth-client-Cygmp1I2.js} +205 -265
  30. package/dist/frontend/awareness-snQ3RD9f.js +3714 -0
  31. package/dist/frontend/bundle-mjs-CP6Owz-8.js +1723 -0
  32. package/dist/frontend/camera-B7dAOd4s.js +28 -0
  33. package/dist/frontend/{categories-BE6ZpZl8.js → categories-izQovv2w.js} +1 -1
  34. package/dist/frontend/check-CDwRlG9T.js +24 -0
  35. package/dist/frontend/chevron-down-B7ZrjUmT.js +24 -0
  36. package/dist/frontend/circle-check-CDqeMB2d.js +28 -0
  37. package/dist/frontend/{client-C6QQGb0e.js → client-Bc5Fzx40.js} +833 -814
  38. package/dist/frontend/{client-B9hzI0W8.js → client-GbtOH90i.js} +1 -1
  39. package/dist/frontend/clipboard-Cgzxc0_R.js +16 -0
  40. package/dist/frontend/clock-Ck5Sn-Zl.js +28 -0
  41. package/dist/frontend/clsx-nmtDPO_P.js +16 -0
  42. package/dist/frontend/collaboration.d.ts +6 -0
  43. package/dist/frontend/collaboration.js +182 -0
  44. package/dist/frontend/components/category-dialog.d.ts +2 -0
  45. package/dist/frontend/components/category-dialog.js +2 -0
  46. package/dist/frontend/components/create-snapshot-dialog.d.ts +2 -0
  47. package/dist/frontend/components/create-snapshot-dialog.js +2 -0
  48. package/dist/frontend/components/delete-dialog.d.ts +2 -0
  49. package/dist/frontend/components/delete-dialog.js +2 -0
  50. package/dist/frontend/components/editor/document-editor.d.ts +3 -0
  51. package/dist/frontend/components/editor/document-editor.js +3021 -0
  52. package/dist/frontend/components/editor/document-title.d.ts +2 -0
  53. package/dist/frontend/components/editor/document-title.js +31 -0
  54. package/dist/frontend/components/editor/docx-serializer.d.ts +2 -0
  55. package/dist/frontend/components/editor/docx-serializer.js +14600 -0
  56. package/dist/frontend/components/editor/extensions.d.ts +2 -0
  57. package/dist/frontend/components/editor/extensions.js +2 -0
  58. package/dist/frontend/components/editor/markdown-toggle.d.ts +2 -0
  59. package/dist/frontend/components/editor/markdown-toggle.js +72 -0
  60. package/dist/frontend/components/editor/markdown.d.ts +1 -0
  61. package/dist/frontend/components/editor/markdown.js +2 -0
  62. package/dist/frontend/components/folder-node.d.ts +3 -0
  63. package/dist/frontend/components/folder-node.js +2 -0
  64. package/dist/frontend/components/folder-tree-selector.d.ts +2 -0
  65. package/dist/frontend/components/folder-tree-selector.js +2 -0
  66. package/dist/frontend/components/save-as-dialog.d.ts +2 -0
  67. package/dist/frontend/components/save-as-dialog.js +254 -0
  68. package/dist/frontend/components/settings.js +1 -1
  69. package/dist/frontend/components/share-dialog.d.ts +2 -0
  70. package/dist/frontend/components/share-dialog.js +2 -0
  71. package/dist/frontend/components/sidebar.js +1206 -3762
  72. package/dist/frontend/components/tag-create-dialog.d.ts +2 -0
  73. package/dist/frontend/components/tag-create-dialog.js +2 -0
  74. package/dist/frontend/components/version-history.d.ts +2 -0
  75. package/dist/frontend/components/version-history.js +293 -0
  76. package/dist/frontend/{context-DbnW5yib.js → context-BMD6XTPe.js} +0 -3
  77. package/dist/frontend/copy-CMAA6GP0.js +31 -0
  78. package/dist/frontend/dashboard.js +1148 -1224
  79. package/dist/frontend/{SettingsDialog-D4pgFD0D.js → db-ByrlYfaI.js} +923 -2002
  80. package/dist/frontend/dialog-BDkiG42a.js +213 -0
  81. package/dist/frontend/disclose-version-5I57E3LD.js +3 -0
  82. package/dist/frontend/dndzone-BzsOINtP.js +1199 -0
  83. package/dist/frontend/doc-tabs.d.ts +7 -0
  84. package/dist/frontend/doc-tabs.js +14 -0
  85. package/dist/frontend/document-drop-coordinator-C4xHMjwC.js +68 -0
  86. package/dist/frontend/document-drop-coordinator.d.ts +2 -0
  87. package/dist/frontend/document-drop-coordinator.js +2 -0
  88. package/dist/frontend/{documents-DFVS9SsL.js → documents-BsDkvzCm.js} +1 -1
  89. package/dist/frontend/editorExtensions-BMHvUbdk.js +27021 -0
  90. package/dist/frontend/ellipsis-vertical-BBuPldbW.js +519 -0
  91. package/dist/frontend/extension.d.ts +2 -1
  92. package/dist/frontend/extension.js +6 -5
  93. package/dist/frontend/floating-layer-anchor-C51M4l6v.js +4513 -0
  94. package/dist/frontend/folder-TQjKY4yn.js +44 -0
  95. package/dist/frontend/folder-refresh.d.ts +8 -0
  96. package/dist/frontend/folder-refresh.js +2 -0
  97. package/dist/frontend/{folders-CGe9diAG.js → folders-CTuV_JtU.js} +1 -1
  98. package/dist/frontend/frontend.css +1 -1
  99. package/dist/frontend/i18n.js +1 -1
  100. package/dist/frontend/identity-DyWeNzu_.js +69 -0
  101. package/dist/frontend/index-client-jobkrWVL.js +86 -0
  102. package/dist/frontend/input-CBI6p4Xl.js +23 -0
  103. package/dist/frontend/keyboard.d.ts +14 -0
  104. package/dist/frontend/keyboard.js +2 -0
  105. package/dist/frontend/keyboard.svelte-D7H8DihC.js +133 -0
  106. package/dist/frontend/label-C-GkhEp4.js +30 -0
  107. package/dist/frontend/loader-circle-Bk-WFz9g.js +267 -0
  108. package/dist/frontend/markdown-BwnyjITU.js +11 -0
  109. package/dist/frontend/messages-BpYaLbzE.js +1091 -0
  110. package/dist/frontend/offline/identity.d.ts +6 -0
  111. package/dist/frontend/offline/identity.js +2 -0
  112. package/dist/frontend/resolve-BG4HgzpN.js +19 -0
  113. package/dist/frontend/rolldown-runtime-CWhphoD1.js +23 -0
  114. package/dist/frontend/rotate-ccw-CleZtJ5D.js +24 -0
  115. package/dist/frontend/search.js +637 -628
  116. package/dist/frontend/select-value-CvrWRkXW.js +1436 -0
  117. package/dist/frontend/settings-blyobazp.js +62 -0
  118. package/dist/frontend/share-Cu7SSJuJ.js +28 -0
  119. package/dist/frontend/shared-document.js +105 -69
  120. package/dist/frontend/subfolders-refresh-store.svelte-BsheoQma.js +65 -0
  121. package/dist/frontend/{tag-C5xrg-bb.js → tag-Dx3SmucJ.js} +7 -7
  122. package/dist/frontend/tag-store.svelte-CIaF4sdZ.js +29 -0
  123. package/dist/frontend/{tags-DVXTut0p.js → tags-CtCV0Djo.js} +1 -1
  124. package/dist/frontend/textarea-Dfp78XlL.js +23 -0
  125. package/dist/frontend/theme.js +1 -1
  126. package/dist/frontend/{theme.svelte-ei0QxXa8.js → theme.svelte-CROg1Hi5.js} +7 -7
  127. package/dist/frontend/types.d.ts +6 -0
  128. package/dist/frontend/types.js +0 -0
  129. package/dist/frontend/utils/clipboard.d.ts +1 -0
  130. package/dist/frontend/utils/clipboard.js +2 -0
  131. package/dist/frontend/utils/dndzone.d.ts +4 -0
  132. package/dist/frontend/utils/dndzone.js +2 -0
  133. package/dist/frontend/utils-C0OW2678.js +20 -0
  134. package/dist/frontend/utils-D8kmM3ub.js +8 -0
  135. package/dist/frontend/utils.d.ts +4 -0
  136. package/dist/frontend/utils.js +2 -0
  137. package/dist/frontend/x-DaTdeDXv.js +24 -0
  138. package/dist/lifecycle-persistent.d.ts +31 -0
  139. package/dist/lifecycle-persistent.js +33 -0
  140. package/dist/server-only-browser-entry.d.ts +0 -0
  141. package/dist/server-only-browser-entry.js +3 -0
  142. package/dist/storage-quota.d.ts +36 -0
  143. package/dist/storage-quota.js +14 -1
  144. package/dist/types.d.ts +7 -0
  145. package/dist/workspace.d.ts +4 -1
  146. package/dist/workspace.js +29 -8
  147. package/package.json +110 -3
  148. package/packages/cli/src/index.ts +1 -1
  149. package/packages/db/src/index.ts +11 -2
  150. package/packages/db/src/schema.ts +30 -2
  151. package/packages/db/src/with-tenant.ts +38 -1
  152. package/packages/mcp-server/src/index.ts +1 -1
  153. package/dist/frontend/DatePicker-BcMD49PV.js +0 -275
  154. package/dist/frontend/ShareDialog-Dzh7rV7J.js +0 -1317
  155. package/dist/frontend/input-ieWIt8I7.js +0 -257
  156. package/dist/frontend/messages-CTGAsqFu.js +0 -1106
  157. package/dist/frontend/resolve-Bl6xmJl_.js +0 -2068
  158. package/dist/frontend/tag-store.svelte-B3839FYq.js +0 -6152
@@ -42744,6 +42744,7 @@ __export(exports_schema, {
42744
42744
  documentPipelineBatches: () => documentPipelineBatches,
42745
42745
  documentEmbeddings: () => documentEmbeddings,
42746
42746
  documentEmbeddingRelations: () => documentEmbeddingRelations,
42747
+ documentCreateOperations: () => documentCreateOperations,
42747
42748
  categoryRelations: () => categoryRelations,
42748
42749
  categories: () => categories,
42749
42750
  auditLog: () => auditLog,
@@ -42753,7 +42754,7 @@ __export(exports_schema, {
42753
42754
  apiKeyRelations: () => apiKeyRelations,
42754
42755
  accounts: () => accounts
42755
42756
  });
42756
- var vector4, tsvector, documentVisibilityEnum, shareRoleEnum, embeddingStatusEnum, pipelineStageEnum, pipelineStatusEnum, lifecycleOperationKindEnum, lifecycleOperationStatusEnum, users, sessions, accounts, verifications, folders, folderRelations, documents, documentRelations, documentPipelineRuns, documentPipelineBatches, tags, tagRelations, categories, categoryRelations, documentTags, documentTagRelations, shareLinks, shareLinkRelations, guestAccess, guestAccessRelations, attachments, attachmentRelations, versions, documentEmbeddings, documentEmbeddingRelations, apiKeys, apiKeyRelations, auditLog, lifecycleOperations, versionRelations;
42757
+ var vector4, tsvector, documentVisibilityEnum, shareRoleEnum, embeddingStatusEnum, pipelineStageEnum, pipelineStatusEnum, lifecycleOperationKindEnum, lifecycleOperationStatusEnum, users, sessions, accounts, verifications, folders, folderRelations, documents, documentRelations, documentPipelineRuns, documentPipelineBatches, tags, tagRelations, categories, categoryRelations, documentTags, documentTagRelations, shareLinks, shareLinkRelations, guestAccess, guestAccessRelations, attachments, attachmentRelations, versions, documentEmbeddings, documentEmbeddingRelations, apiKeys, apiKeyRelations, auditLog, lifecycleOperations, documentCreateOperations, versionRelations;
42757
42758
  var init_schema2 = __esm(() => {
42758
42759
  init_pg_core();
42759
42760
  init_drizzle_orm();
@@ -43209,7 +43210,8 @@ var init_schema2 = __esm(() => {
43209
43210
  ]);
43210
43211
  lifecycleOperations = pgTable("lifecycle_operations", {
43211
43212
  id: uuid("id").primaryKey().defaultRandom(),
43212
- actorUserId: uuid("actor_user_id").notNull().references(() => users.id, { onDelete: "restrict" }),
43213
+ actorUserId: uuid("actor_user_id").references(() => users.id, { onDelete: "set null" }),
43214
+ actorSubjectHash: text("actor_subject_hash").notNull(),
43213
43215
  idempotencyKey: text("idempotency_key").notNull(),
43214
43216
  operationKind: lifecycleOperationKindEnum("operation_kind").notNull(),
43215
43217
  status: lifecycleOperationStatusEnum("status").notNull().default("pending"),
@@ -43227,7 +43229,19 @@ var init_schema2 = __esm(() => {
43227
43229
  uniqueIndex("lifecycle_operations_actor_idempotency_idx").on(table3.actorUserId, table3.idempotencyKey),
43228
43230
  index("lifecycle_operations_status_lease_idx").on(table3.status, table3.leaseExpiresAt),
43229
43231
  index("lifecycle_operations_actor_idx").on(table3.actorUserId),
43230
- index("lifecycle_operations_retryable_idx").on(table3.status).where(sql`${table3.status} = 'retryable'`)
43232
+ index("lifecycle_operations_retryable_idx").on(table3.status).where(sql`${table3.status} = 'retryable'`),
43233
+ check2("lifecycle_operations_actor_subject_hash", sql`${table3.actorSubjectHash} ~ '^[a-f0-9]{64}$'`)
43234
+ ]);
43235
+ documentCreateOperations = pgTable("document_create_operations", {
43236
+ id: uuid("id").primaryKey().defaultRandom(),
43237
+ workspaceId: text("workspace_id").notNull(),
43238
+ actorUserId: uuid("actor_user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
43239
+ idempotencyKey: text("idempotency_key").notNull(),
43240
+ documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
43241
+ createdAt: timestamp("created_at").defaultNow().notNull()
43242
+ }, (table3) => [
43243
+ uniqueIndex("document_create_operations_workspace_actor_key_idx").on(table3.workspaceId, table3.actorUserId, table3.idempotencyKey),
43244
+ uniqueIndex("document_create_operations_document_idx").on(table3.documentId)
43231
43245
  ]);
43232
43246
  versionRelations = relations(versions, ({ one }) => ({
43233
43247
  document: one(documents, {
@@ -198271,7 +198285,8 @@ init_config();
198271
198285
  var DOCSMINT_WORKSPACE_CONTEXT_HEADER = "x-docsmint-workspace-context";
198272
198286
  var EXTERNAL_TENANT_CONTEXT_HEADER = "x-hiai-tenant-context";
198273
198287
  var WORKSPACE_CONTEXT_MAX_LENGTH = 128;
198274
- var WORKSPACE_CONTEXT_MAX_TTL_SECONDS = 300;
198288
+ var WORKSPACE_CONTEXT_MAX_TTL_SECONDS = 60;
198289
+ var WORKSPACE_CONTEXT_CLOCK_SKEW_SECONDS = 5;
198275
198290
 
198276
198291
  class ExternalTenantContextError extends Error {
198277
198292
  status = 401;
@@ -198334,14 +198349,14 @@ async function verifyExternalTenantAssertion(assertion, options) {
198334
198349
  if (decoded.issuer !== options.issuer)
198335
198350
  throw new Error("Invalid tenant assertion issuer");
198336
198351
  const now2 = options.nowSeconds ?? Math.floor(Date.now() / 1000);
198337
- const skew = options.clockSkewSeconds ?? 5;
198352
+ const skew = Math.min(options.clockSkewSeconds ?? WORKSPACE_CONTEXT_CLOCK_SKEW_SECONDS, WORKSPACE_CONTEXT_CLOCK_SKEW_SECONDS);
198338
198353
  if (decoded.issuedAt > now2 + skew)
198339
198354
  throw new Error("Tenant assertion is not yet valid");
198340
198355
  if (decoded.expiresAt <= now2 - skew)
198341
198356
  throw new Error("Tenant assertion is expired");
198342
198357
  if (decoded.expiresAt <= decoded.issuedAt)
198343
198358
  throw new Error("Invalid tenant assertion lifetime");
198344
- if (decoded.expiresAt - decoded.issuedAt > (options.maxTtlSeconds ?? WORKSPACE_CONTEXT_MAX_TTL_SECONDS))
198359
+ if (decoded.expiresAt - decoded.issuedAt > Math.min(options.maxTtlSeconds ?? WORKSPACE_CONTEXT_MAX_TTL_SECONDS, WORKSPACE_CONTEXT_MAX_TTL_SECONDS))
198345
198360
  throw new Error("Tenant assertion lifetime exceeds maximum TTL");
198346
198361
  return decoded;
198347
198362
  }
@@ -199640,6 +199655,27 @@ async function fetchRemoteImage(source, fetchImpl = fetch) {
199640
199655
  throw new Error("Remote image could not be fetched");
199641
199656
  }
199642
199657
 
199658
+ // ../../backend/src/lib/runtime-options.ts
199659
+ init_config();
199660
+ var RUNTIME_OPTIONS = Symbol.for("@hiai-gg/docsmint/runtime-options");
199661
+ function configureDocsMintRuntime(options = {}) {
199662
+ const globals = globalThis;
199663
+ const existing = globals[RUNTIME_OPTIONS];
199664
+ if (existing)
199665
+ return;
199666
+ if (config3.DOCSMINT_WORKSPACE_ENABLED && !options.attachmentStorageQuotaAdmission) {
199667
+ throw new Error("Attachment storage quota admission is required when workspace tenancy is enabled");
199668
+ }
199669
+ Object.defineProperty(globals, RUNTIME_OPTIONS, {
199670
+ value: Object.freeze({ ...options }),
199671
+ configurable: false,
199672
+ writable: false
199673
+ });
199674
+ }
199675
+ function getDocsMintRuntimeOptions() {
199676
+ return globalThis[RUNTIME_OPTIONS];
199677
+ }
199678
+
199643
199679
  // ../../backend/src/lib/share-access.ts
199644
199680
  init_schema2();
199645
199681
  init_drizzle_orm();
@@ -199919,6 +199955,31 @@ function attachmentKeyPrefix(ctx, userId, documentId) {
199919
199955
  }
199920
199956
  var PRESIGN_EXPIRY_SECONDS = config3.ATTACHMENT_PRESIGN_EXPIRY_SECONDS;
199921
199957
  var INTEGRITY_PROBE_BYTES = 8;
199958
+ function attachmentQuotaContext(ctx, actorUserId, documentId, storageKey, proposedSize) {
199959
+ if (!config3.DOCSMINT_WORKSPACE_ENABLED)
199960
+ return null;
199961
+ if (!ctx.workspaceId)
199962
+ return null;
199963
+ return Object.freeze({
199964
+ workspaceId: ctx.workspaceId,
199965
+ actorUserId,
199966
+ documentId,
199967
+ storageKey,
199968
+ proposedSize,
199969
+ requestId: crypto.randomUUID(),
199970
+ idempotencyKey: `attachment:${documentId}:${storageKey}`
199971
+ });
199972
+ }
199973
+ function attachmentQuotaAdmission(set2) {
199974
+ if (!config3.DOCSMINT_WORKSPACE_ENABLED)
199975
+ return null;
199976
+ const admission = getDocsMintRuntimeOptions()?.attachmentStorageQuotaAdmission;
199977
+ if (!admission) {
199978
+ set2.status = 503;
199979
+ return;
199980
+ }
199981
+ return admission;
199982
+ }
199922
199983
  async function readStorageBody(body) {
199923
199984
  if (body == null) {
199924
199985
  return Buffer.alloc(0);
@@ -200069,15 +200130,42 @@ var attachmentRoutes = new Elysia({ prefix: "/api" }).post("/documents/:id/attac
200069
200130
  }
200070
200131
  const ext = filename.split(".").pop() ?? "bin";
200071
200132
  const key = `${attachmentKeyPrefix(ctx, userId, documentId)}/${nanoid3()}.${ext}`;
200133
+ const quotaContext = attachmentQuotaContext(ctx, userId, documentId, key, size2);
200134
+ const admission = attachmentQuotaAdmission(set2);
200135
+ if (admission === undefined) {
200136
+ return { error: "Attachment storage quota admission is unavailable" };
200137
+ }
200138
+ if (config3.DOCSMINT_WORKSPACE_ENABLED && !quotaContext) {
200139
+ set2.status = 503;
200140
+ return {
200141
+ error: "Workspace context is required for attachment storage"
200142
+ };
200143
+ }
200144
+ let reservationId;
200145
+ if (admission && quotaContext) {
200146
+ try {
200147
+ reservationId = (await admission.reserve(quotaContext)).id;
200148
+ } catch (error53) {
200149
+ logger3.warn({ err: error53, documentId }, "Attachment storage quota reservation rejected");
200150
+ set2.status = 413;
200151
+ return { error: "Storage quota exceeded" };
200152
+ }
200153
+ }
200072
200154
  try {
200073
200155
  const url2 = await getSignedUrl(storagePublic, new PutObjectCommand({ Bucket: BUCKET, Key: key }), { expiresIn: PRESIGN_EXPIRY_SECONDS });
200074
200156
  return {
200075
200157
  url: url2,
200076
200158
  key,
200159
+ ...reservationId ? { quotaReservationId: reservationId } : {},
200077
200160
  maxSize: ATTACHMENT_MAX_SIZE_BYTES,
200078
200161
  expiresIn: PRESIGN_EXPIRY_SECONDS
200079
200162
  };
200080
200163
  } catch (err) {
200164
+ if (admission && quotaContext && reservationId) {
200165
+ await admission.releaseReservation(quotaContext, reservationId).catch(() => {
200166
+ return;
200167
+ });
200168
+ }
200081
200169
  logger3.error({ err, key }, "Failed to presign attachment upload");
200082
200170
  set2.status = 500;
200083
200171
  return { error: "Failed to generate upload URL" };
@@ -200110,6 +200198,7 @@ var attachmentRoutes = new Elysia({ prefix: "/api" }).post("/documents/:id/attac
200110
200198
  const filename = typeof payload?.filename === "string" ? payload.filename : "";
200111
200199
  const contentType = typeof payload?.contentType === "string" ? payload.contentType : "";
200112
200200
  const size2 = typeof payload?.size === "number" ? payload.size : -1;
200201
+ const quotaReservationId = typeof payload?.quotaReservationId === "string" ? payload.quotaReservationId : "";
200113
200202
  if (!key) {
200114
200203
  set2.status = 400;
200115
200204
  return { error: "key is required" };
@@ -200137,15 +200226,50 @@ var attachmentRoutes = new Elysia({ prefix: "/api" }).post("/documents/:id/attac
200137
200226
  error: `File too large. Maximum size: ${ATTACHMENT_MAX_SIZE_BYTES / 1024 / 1024}MB`
200138
200227
  };
200139
200228
  }
200229
+ const quotaContext = attachmentQuotaContext(ctx, userId, documentId, key, size2);
200230
+ const admission = attachmentQuotaAdmission(set2);
200231
+ if (admission === undefined) {
200232
+ return { error: "Attachment storage quota admission is unavailable" };
200233
+ }
200234
+ if (config3.DOCSMINT_WORKSPACE_ENABLED && !quotaContext) {
200235
+ set2.status = 503;
200236
+ return {
200237
+ error: "Workspace context is required for attachment storage"
200238
+ };
200239
+ }
200240
+ if (admission && quotaContext && !quotaReservationId) {
200241
+ set2.status = 400;
200242
+ return { error: "quotaReservationId is required" };
200243
+ }
200140
200244
  let headOutput;
200141
200245
  try {
200142
200246
  headOutput = await storage.send(new HeadObjectCommand({ Bucket: BUCKET, Key: key }));
200143
200247
  } catch (err) {
200248
+ if (admission && quotaContext && quotaReservationId) {
200249
+ await admission.releaseReservation(quotaContext, quotaReservationId).catch(() => {
200250
+ return;
200251
+ });
200252
+ }
200144
200253
  logger3.warn({ err, key }, "Confirm failed: object not in storage");
200145
200254
  set2.status = 409;
200146
200255
  return { error: "Upload not found in storage \u2014 please retry upload" };
200147
200256
  }
200148
200257
  const storedSize = headOutput.ContentLength ?? size2;
200258
+ if (admission && quotaContext && quotaReservationId) {
200259
+ try {
200260
+ await admission.finalize(quotaContext, {
200261
+ reservationId: quotaReservationId,
200262
+ actualSize: storedSize
200263
+ });
200264
+ } catch (error53) {
200265
+ await admission.releaseReservation(quotaContext, quotaReservationId).catch(() => {
200266
+ return;
200267
+ });
200268
+ logger3.error({ err: error53, documentId }, "Attachment storage quota finalization failed");
200269
+ set2.status = 503;
200270
+ return { error: "Attachment storage quota finalization failed" };
200271
+ }
200272
+ }
200149
200273
  try {
200150
200274
  const created = await withTenant(ctx, async (tx) => {
200151
200275
  const [row] = await tx.insert(attachments).values({
@@ -200158,6 +200282,11 @@ var attachmentRoutes = new Elysia({ prefix: "/api" }).post("/documents/:id/attac
200158
200282
  return row ?? null;
200159
200283
  });
200160
200284
  if (!created) {
200285
+ if (admission && quotaContext && quotaReservationId) {
200286
+ await admission.releaseCommitted(quotaContext).catch(() => {
200287
+ return;
200288
+ });
200289
+ }
200161
200290
  set2.status = 500;
200162
200291
  return { error: "Failed to save attachment record" };
200163
200292
  }
@@ -200170,6 +200299,11 @@ var attachmentRoutes = new Elysia({ prefix: "/api" }).post("/documents/:id/attac
200170
200299
  url: `/api/attachments/${created.id}/raw`
200171
200300
  };
200172
200301
  } catch (err) {
200302
+ if (admission && quotaContext && quotaReservationId) {
200303
+ await admission.releaseCommitted(quotaContext).catch(() => {
200304
+ return;
200305
+ });
200306
+ }
200173
200307
  logger3.error({ err, key }, "Confirm failed: DB insert error");
200174
200308
  set2.status = 500;
200175
200309
  return { error: "Failed to save attachment record" };
@@ -200194,6 +200328,12 @@ var attachmentRoutes = new Elysia({ prefix: "/api" }).post("/documents/:id/attac
200194
200328
  }
200195
200329
  const ctx = authorization.access.ctx;
200196
200330
  const userId = authorization.access.userId;
200331
+ if (config3.DOCSMINT_WORKSPACE_ENABLED) {
200332
+ set2.status = 503;
200333
+ return {
200334
+ error: "Legacy attachment uploads are unavailable with workspace storage enforcement"
200335
+ };
200336
+ }
200197
200337
  const documentId = params.id;
200198
200338
  let file3;
200199
200339
  try {
@@ -200320,7 +200460,9 @@ var attachmentRoutes = new Elysia({ prefix: "/api" }).post("/documents/:id/attac
200320
200460
  const row = await withTenant(ctx, async (tx) => {
200321
200461
  const [r4] = await tx.select({
200322
200462
  id: attachments.id,
200463
+ documentId: attachments.documentId,
200323
200464
  storageKey: attachments.storageKey,
200465
+ size: attachments.size,
200324
200466
  ownerId: documents.ownerId,
200325
200467
  categoryId: documents.categoryId,
200326
200468
  folderCategoryId: folders.categoryId
@@ -200339,6 +200481,15 @@ var attachmentRoutes = new Elysia({ prefix: "/api" }).post("/documents/:id/attac
200339
200481
  set2.status = 403;
200340
200482
  return { error: "Forbidden" };
200341
200483
  }
200484
+ const quotaContext = attachmentQuotaContext(ctx, userId, row.documentId, row.storageKey, row.size);
200485
+ const admission = attachmentQuotaAdmission(set2);
200486
+ if (admission === undefined) {
200487
+ return { error: "Attachment storage quota admission is unavailable" };
200488
+ }
200489
+ if (config3.DOCSMINT_WORKSPACE_ENABLED && !quotaContext) {
200490
+ set2.status = 503;
200491
+ return { error: "Workspace context is required for attachment storage" };
200492
+ }
200342
200493
  try {
200343
200494
  await storage.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: row.storageKey }));
200344
200495
  } catch (err) {
@@ -200347,6 +200498,13 @@ var attachmentRoutes = new Elysia({ prefix: "/api" }).post("/documents/:id/attac
200347
200498
  await withTenant(ctx, async (tx) => {
200348
200499
  await tx.delete(attachments).where(eq(attachments.id, attachmentId));
200349
200500
  });
200501
+ if (admission && quotaContext) {
200502
+ try {
200503
+ await admission.releaseCommitted(quotaContext);
200504
+ } catch (error53) {
200505
+ logger3.error({ err: error53, attachmentId }, "Attachment storage quota release failed after delete");
200506
+ }
200507
+ }
200350
200508
  return { success: true };
200351
200509
  }).get("/attachments/:id/raw", async ({ params, request, set: set2 }) => {
200352
200510
  try {
@@ -206949,6 +207107,18 @@ async function invalidateDocCache(docId) {
206949
207107
  }
206950
207108
  }
206951
207109
 
207110
+ // ../../backend/src/lib/document-create-idempotency.ts
207111
+ var IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
207112
+ function documentCreateIdempotencyKey(request) {
207113
+ const value = request.headers.get("idempotency-key");
207114
+ if (value === null)
207115
+ return null;
207116
+ return IDEMPOTENCY_KEY_PATTERN.test(value) ? value : "invalid";
207117
+ }
207118
+ function documentCreateWorkspaceIdentity(userId, workspaceId) {
207119
+ return workspaceId ?? `personal:${userId}`;
207120
+ }
207121
+
206952
207122
  // ../../backend/src/lib/docx-parser.ts
206953
207123
  var import_mammoth = __toESM(require_lib7(), 1);
206954
207124
 
@@ -207404,14 +207574,6 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
207404
207574
  return { error: "Failed to list documents" };
207405
207575
  }
207406
207576
  }).post("/documents", async ({ request, set: set2 }) => {
207407
- const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "unknown";
207408
- const rl = await writeRateLimiter(ip, request);
207409
- if (!rl.allowed) {
207410
- set2.status = 429;
207411
- set2.headers = rateLimitHeaders(0, rl.retryAfter);
207412
- return { error: "Too many requests" };
207413
- }
207414
- set2.headers = rateLimitHeaders(rl.remaining);
207415
207577
  const access = await resolveContentAccess(request);
207416
207578
  const ctx = access.ctx;
207417
207579
  if (ctx.role === "none") {
@@ -207423,6 +207585,30 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
207423
207585
  return { error: "Forbidden" };
207424
207586
  }
207425
207587
  const userId = ctx.userId;
207588
+ const idempotencyKey = documentCreateIdempotencyKey(request);
207589
+ if (idempotencyKey === "invalid") {
207590
+ set2.status = 400;
207591
+ return { error: "Invalid Idempotency-Key" };
207592
+ }
207593
+ if (idempotencyKey) {
207594
+ const workspaceIdentity = documentCreateWorkspaceIdentity(userId, ctx.workspaceId);
207595
+ const replay = await withTenant(ctx, async (tx) => {
207596
+ const [existing] = await tx.select({ document: documents }).from(documentCreateOperations).innerJoin(documents, eq(documentCreateOperations.documentId, documents.id)).where(and(eq(documentCreateOperations.workspaceId, workspaceIdentity), eq(documentCreateOperations.actorUserId, userId), eq(documentCreateOperations.idempotencyKey, idempotencyKey))).limit(1);
207597
+ return existing?.document ?? null;
207598
+ });
207599
+ if (replay) {
207600
+ set2.status = 200;
207601
+ return replay;
207602
+ }
207603
+ }
207604
+ const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "unknown";
207605
+ const rl = await writeRateLimiter(ip, request);
207606
+ if (!rl.allowed) {
207607
+ set2.status = 429;
207608
+ set2.headers = rateLimitHeaders(0, rl.retryAfter);
207609
+ return { error: "Too many requests" };
207610
+ }
207611
+ set2.headers = rateLimitHeaders(rl.remaining);
207426
207612
  const body = createDocumentSchema.safeParse(await request.json());
207427
207613
  if (!body.success) {
207428
207614
  set2.status = 400;
@@ -207441,9 +207627,17 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
207441
207627
  set2.status = 403;
207442
207628
  return { error: "Forbidden" };
207443
207629
  }
207444
- const created = await withTenant(ctx, async (tx) => {
207630
+ const createdResult = await withTenant(ctx, async (tx) => {
207631
+ const workspaceIdentity = documentCreateWorkspaceIdentity(userId, ctx.workspaceId);
207632
+ if (idempotencyKey) {
207633
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${`${workspaceIdentity}:${userId}:${idempotencyKey}`}))`);
207634
+ const [existing] = await tx.select({ document: documents }).from(documentCreateOperations).innerJoin(documents, eq(documentCreateOperations.documentId, documents.id)).where(and(eq(documentCreateOperations.workspaceId, workspaceIdentity), eq(documentCreateOperations.actorUserId, userId), eq(documentCreateOperations.idempotencyKey, idempotencyKey))).limit(1);
207635
+ if (existing)
207636
+ return { row: existing.document, replayed: true };
207637
+ }
207445
207638
  const [row] = await tx.insert(documents).values({
207446
207639
  ownerId: userId,
207640
+ workspaceId: ctx.workspaceId ?? null,
207447
207641
  title: body.data.title,
207448
207642
  content: initialContent,
207449
207643
  contentHash: initialHash,
@@ -207461,8 +207655,21 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
207461
207655
  contentJson: null,
207462
207656
  createdBy: userId
207463
207657
  });
207464
- return row;
207658
+ if (idempotencyKey) {
207659
+ await tx.insert(documentCreateOperations).values({
207660
+ workspaceId: workspaceIdentity,
207661
+ actorUserId: userId,
207662
+ idempotencyKey,
207663
+ documentId: row.id
207664
+ });
207665
+ }
207666
+ return { row, replayed: false };
207465
207667
  });
207668
+ const created = createdResult.row;
207669
+ if (createdResult.replayed) {
207670
+ set2.status = 200;
207671
+ return created;
207672
+ }
207466
207673
  enqueueDocumentPipeline({
207467
207674
  documentId: created.id,
207468
207675
  ownerId: userId,
@@ -213875,6 +214082,7 @@ async function closeRuntime(options, workers) {
213875
214082
  }
213876
214083
 
213877
214084
  // ../../backend/src/index.ts
214085
+ configureDocsMintRuntime();
213878
214086
  await ensureApiKeyOwner();
213879
214087
  configureDefaultJobOptions({
213880
214088
  attempts: config3.QUEUE_JOB_ATTEMPTS,
@@ -213941,7 +214149,7 @@ var swaggerConfig = {
213941
214149
  documentation: {
213942
214150
  info: {
213943
214151
  title: "DocsMint API",
213944
- version: "0.3.5",
214152
+ version: "0.3.7",
213945
214153
  description: "Self-hosted AI-first documentation platform. Full-text + semantic search, version history, sharing, and folder organization.",
213946
214154
  contact: { name: "HiAi-gg", url: "https://github.com/HiAi-gg/docsmint" },
213947
214155
  license: {
@@ -214010,11 +214218,17 @@ app.listen({
214010
214218
  idleTimeout: 30
214011
214219
  });
214012
214220
  logger3.info({ port: config3.API_PORT }, "hiai-docs API started");
214013
- var shutdown = async () => {
214221
+ var stopDocsMintApi = async () => {
214014
214222
  logger3.info("Shutting down...");
214015
214223
  await pipelineRuntime.close();
214016
214224
  await app.stop();
214225
+ };
214226
+ var shutdown = async () => {
214227
+ await stopDocsMintApi();
214017
214228
  process.exit(0);
214018
214229
  };
214019
214230
  process.on("SIGTERM", shutdown);
214020
214231
  process.on("SIGINT", shutdown);
214232
+ export {
214233
+ stopDocsMintApi
214234
+ };
@@ -35,6 +35,21 @@ export type DocsmintBackendHandle = Readonly<{
35
35
  export type DocsmintBackendLauncher = Readonly<{
36
36
  launch(options?: LaunchDocsmintBackendOptions): DocsmintBackendHandle;
37
37
  }>;
38
+ import type { AttachmentStorageQuotaAdmission } from "./storage-quota";
39
+ export type { AttachmentStorageQuotaAdmission } from "./storage-quota";
40
+ export type DocsMintRuntimeOptions = Readonly<{
41
+ attachmentStorageQuotaAdmission?: AttachmentStorageQuotaAdmission;
42
+ }>;
43
+ export type DocsMintInProcessHandle = Readonly<{
44
+ ready: Promise<void>;
45
+ stop(): Promise<void>;
46
+ }>;
47
+ /**
48
+ * Starts the bundled OSS API in this Bun process. Runtime options are frozen
49
+ * and installed before importing the backend graph, so routes/workers cannot
50
+ * observe an unconfigured tenancy-enabled process.
51
+ */
52
+ export declare function launchDocsMintApi(options?: DocsMintRuntimeOptions): Promise<DocsMintInProcessHandle>;
38
53
  export declare function resolveDocsmintBackendEntrypoint(launcherModuleUrl?: string | URL): URL;
39
54
  export declare function createDocsmintBackendLauncher(runtime: DocsmintBackendLauncherRuntime): DocsmintBackendLauncher;
40
55
  export declare function launchDocsmintBackend(options?: LaunchDocsmintBackendOptions): DocsmintBackendHandle;
@@ -1,4 +1,26 @@
1
1
  /** Server-only launcher for the bundled DocsMint backend runtime. */
2
+ const RUNTIME_OPTIONS = Symbol.for("@hiai-gg/docsmint/runtime-options");
3
+ /**
4
+ * Starts the bundled OSS API in this Bun process. Runtime options are frozen
5
+ * and installed before importing the backend graph, so routes/workers cannot
6
+ * observe an unconfigured tenancy-enabled process.
7
+ */
8
+ export async function launchDocsMintApi(options = {}) {
9
+ if (process.env.DOCSMINT_WORKSPACE_ENABLED === "true" &&
10
+ !options.attachmentStorageQuotaAdmission) {
11
+ throw new Error("Attachment storage quota admission is required when workspace tenancy is enabled");
12
+ }
13
+ const globals = globalThis;
14
+ if (globals[RUNTIME_OPTIONS])
15
+ throw new Error("DocsMint runtime is already configured");
16
+ globals[RUNTIME_OPTIONS] = Object.freeze({ ...options });
17
+ const backendUrl = new URL("./backend/index.js", import.meta.url).href;
18
+ const backend = (await import(backendUrl));
19
+ return Object.freeze({
20
+ ready: Promise.resolve(),
21
+ stop: async () => backend.stopDocsMintApi?.(),
22
+ });
23
+ }
2
24
  const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
3
25
  const DEFAULT_POLL_INTERVAL_MS = 200;
4
26
  const DEFAULT_API_PORT = "50700";