@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.
- package/README.md +0 -18
- package/backend/src/lib/lifecycle-service.ts +334 -219
- package/dist/backend/index.js +232 -18
- package/dist/backend-launcher.d.ts +15 -0
- package/dist/backend-launcher.js +22 -0
- package/dist/frontend/CategoryDialog--rIE7FrH.js +580 -0
- package/dist/frontend/ConfirmDialog-Cpzyb55f.js +212 -0
- package/dist/frontend/CreateSnapshotDialog-CmkV-n2A.js +216 -0
- package/dist/frontend/DatePicker-Byynca7V.js +257 -0
- package/dist/frontend/DeleteDialog-ap6nOywZ.js +174 -0
- package/dist/frontend/FolderDialog-DmN_trV0.js +247 -0
- package/dist/frontend/FolderNode-C4B8a2fd.js +317 -0
- package/dist/frontend/FolderTreeSelector-D6ijiZTA.js +100 -0
- package/dist/frontend/Icon-DtNo0N1U.js +64 -0
- package/dist/frontend/SettingsDialog-2kTaTXAs.js +992 -0
- package/dist/frontend/ShareDialog-BBDGNF5n.js +165 -0
- package/dist/frontend/TagCreateDialog-DiLZnJo4.js +258 -0
- package/dist/frontend/api/attachments.d.ts +6 -0
- package/dist/frontend/api/attachments.js +2 -0
- package/dist/frontend/api/categories.js +1 -1
- package/dist/frontend/api/documents.js +1 -1
- package/dist/frontend/api/folders.js +1 -1
- package/dist/frontend/api/settings.js +1 -1
- package/dist/frontend/api/share.d.ts +5 -0
- package/dist/frontend/api/share.js +2 -0
- package/dist/frontend/api/tags.js +1 -1
- package/dist/frontend/api-keys-CpkrA8rD.js +35 -0
- package/dist/frontend/attachments-BuILSs56.js +50 -0
- package/dist/frontend/{settings-PCrOtied.js → auth-client-Cygmp1I2.js} +205 -265
- package/dist/frontend/awareness-snQ3RD9f.js +3714 -0
- package/dist/frontend/bundle-mjs-CP6Owz-8.js +1723 -0
- package/dist/frontend/camera-B7dAOd4s.js +28 -0
- package/dist/frontend/{categories-BE6ZpZl8.js → categories-izQovv2w.js} +1 -1
- package/dist/frontend/check-CDwRlG9T.js +24 -0
- package/dist/frontend/chevron-down-B7ZrjUmT.js +24 -0
- package/dist/frontend/circle-check-CDqeMB2d.js +28 -0
- package/dist/frontend/{client-C6QQGb0e.js → client-Bc5Fzx40.js} +833 -814
- package/dist/frontend/{client-B9hzI0W8.js → client-GbtOH90i.js} +1 -1
- package/dist/frontend/clipboard-Cgzxc0_R.js +16 -0
- package/dist/frontend/clock-Ck5Sn-Zl.js +28 -0
- package/dist/frontend/clsx-nmtDPO_P.js +16 -0
- package/dist/frontend/collaboration.d.ts +6 -0
- package/dist/frontend/collaboration.js +182 -0
- package/dist/frontend/components/category-dialog.d.ts +2 -0
- package/dist/frontend/components/category-dialog.js +2 -0
- package/dist/frontend/components/create-snapshot-dialog.d.ts +2 -0
- package/dist/frontend/components/create-snapshot-dialog.js +2 -0
- package/dist/frontend/components/delete-dialog.d.ts +2 -0
- package/dist/frontend/components/delete-dialog.js +2 -0
- package/dist/frontend/components/editor/document-editor.d.ts +3 -0
- package/dist/frontend/components/editor/document-editor.js +3021 -0
- package/dist/frontend/components/editor/document-title.d.ts +2 -0
- package/dist/frontend/components/editor/document-title.js +31 -0
- package/dist/frontend/components/editor/docx-serializer.d.ts +2 -0
- package/dist/frontend/components/editor/docx-serializer.js +14600 -0
- package/dist/frontend/components/editor/extensions.d.ts +2 -0
- package/dist/frontend/components/editor/extensions.js +2 -0
- package/dist/frontend/components/editor/markdown-toggle.d.ts +2 -0
- package/dist/frontend/components/editor/markdown-toggle.js +72 -0
- package/dist/frontend/components/editor/markdown.d.ts +1 -0
- package/dist/frontend/components/editor/markdown.js +2 -0
- package/dist/frontend/components/folder-node.d.ts +3 -0
- package/dist/frontend/components/folder-node.js +2 -0
- package/dist/frontend/components/folder-tree-selector.d.ts +2 -0
- package/dist/frontend/components/folder-tree-selector.js +2 -0
- package/dist/frontend/components/save-as-dialog.d.ts +2 -0
- package/dist/frontend/components/save-as-dialog.js +254 -0
- package/dist/frontend/components/settings.js +1 -1
- package/dist/frontend/components/share-dialog.d.ts +2 -0
- package/dist/frontend/components/share-dialog.js +2 -0
- package/dist/frontend/components/sidebar.js +1206 -3762
- package/dist/frontend/components/tag-create-dialog.d.ts +2 -0
- package/dist/frontend/components/tag-create-dialog.js +2 -0
- package/dist/frontend/components/version-history.d.ts +2 -0
- package/dist/frontend/components/version-history.js +293 -0
- package/dist/frontend/{context-DbnW5yib.js → context-BMD6XTPe.js} +0 -3
- package/dist/frontend/copy-CMAA6GP0.js +31 -0
- package/dist/frontend/dashboard.js +1148 -1224
- package/dist/frontend/{SettingsDialog-D4pgFD0D.js → db-ByrlYfaI.js} +923 -2002
- package/dist/frontend/dialog-BDkiG42a.js +213 -0
- package/dist/frontend/disclose-version-5I57E3LD.js +3 -0
- package/dist/frontend/dndzone-BzsOINtP.js +1199 -0
- package/dist/frontend/doc-tabs.d.ts +7 -0
- package/dist/frontend/doc-tabs.js +14 -0
- package/dist/frontend/document-drop-coordinator-C4xHMjwC.js +68 -0
- package/dist/frontend/document-drop-coordinator.d.ts +2 -0
- package/dist/frontend/document-drop-coordinator.js +2 -0
- package/dist/frontend/{documents-DFVS9SsL.js → documents-BsDkvzCm.js} +1 -1
- package/dist/frontend/editorExtensions-BMHvUbdk.js +27021 -0
- package/dist/frontend/ellipsis-vertical-BBuPldbW.js +519 -0
- package/dist/frontend/extension.d.ts +2 -1
- package/dist/frontend/extension.js +6 -5
- package/dist/frontend/floating-layer-anchor-C51M4l6v.js +4513 -0
- package/dist/frontend/folder-TQjKY4yn.js +44 -0
- package/dist/frontend/folder-refresh.d.ts +8 -0
- package/dist/frontend/folder-refresh.js +2 -0
- package/dist/frontend/{folders-CGe9diAG.js → folders-CTuV_JtU.js} +1 -1
- package/dist/frontend/frontend.css +1 -1
- package/dist/frontend/i18n.js +1 -1
- package/dist/frontend/identity-DyWeNzu_.js +69 -0
- package/dist/frontend/index-client-jobkrWVL.js +86 -0
- package/dist/frontend/input-CBI6p4Xl.js +23 -0
- package/dist/frontend/keyboard.d.ts +14 -0
- package/dist/frontend/keyboard.js +2 -0
- package/dist/frontend/keyboard.svelte-D7H8DihC.js +133 -0
- package/dist/frontend/label-C-GkhEp4.js +30 -0
- package/dist/frontend/loader-circle-Bk-WFz9g.js +267 -0
- package/dist/frontend/markdown-BwnyjITU.js +11 -0
- package/dist/frontend/messages-BpYaLbzE.js +1091 -0
- package/dist/frontend/offline/identity.d.ts +6 -0
- package/dist/frontend/offline/identity.js +2 -0
- package/dist/frontend/resolve-BG4HgzpN.js +19 -0
- package/dist/frontend/rolldown-runtime-CWhphoD1.js +23 -0
- package/dist/frontend/rotate-ccw-CleZtJ5D.js +24 -0
- package/dist/frontend/search.js +637 -628
- package/dist/frontend/select-value-CvrWRkXW.js +1436 -0
- package/dist/frontend/settings-blyobazp.js +62 -0
- package/dist/frontend/share-Cu7SSJuJ.js +28 -0
- package/dist/frontend/shared-document.js +105 -69
- package/dist/frontend/subfolders-refresh-store.svelte-BsheoQma.js +65 -0
- package/dist/frontend/{tag-C5xrg-bb.js → tag-Dx3SmucJ.js} +7 -7
- package/dist/frontend/tag-store.svelte-CIaF4sdZ.js +29 -0
- package/dist/frontend/{tags-DVXTut0p.js → tags-CtCV0Djo.js} +1 -1
- package/dist/frontend/textarea-Dfp78XlL.js +23 -0
- package/dist/frontend/theme.js +1 -1
- package/dist/frontend/{theme.svelte-ei0QxXa8.js → theme.svelte-CROg1Hi5.js} +7 -7
- package/dist/frontend/types.d.ts +6 -0
- package/dist/frontend/types.js +0 -0
- package/dist/frontend/utils/clipboard.d.ts +1 -0
- package/dist/frontend/utils/clipboard.js +2 -0
- package/dist/frontend/utils/dndzone.d.ts +4 -0
- package/dist/frontend/utils/dndzone.js +2 -0
- package/dist/frontend/utils-C0OW2678.js +20 -0
- package/dist/frontend/utils-D8kmM3ub.js +8 -0
- package/dist/frontend/utils.d.ts +4 -0
- package/dist/frontend/utils.js +2 -0
- package/dist/frontend/x-DaTdeDXv.js +24 -0
- package/dist/lifecycle-persistent.d.ts +31 -0
- package/dist/lifecycle-persistent.js +33 -0
- package/dist/server-only-browser-entry.d.ts +0 -0
- package/dist/server-only-browser-entry.js +3 -0
- package/dist/storage-quota.d.ts +36 -0
- package/dist/storage-quota.js +14 -1
- package/dist/types.d.ts +7 -0
- package/dist/workspace.d.ts +4 -1
- package/dist/workspace.js +29 -8
- package/package.json +110 -3
- package/packages/cli/src/index.ts +1 -1
- package/packages/db/src/index.ts +11 -2
- package/packages/db/src/schema.ts +30 -2
- package/packages/db/src/with-tenant.ts +38 -1
- package/packages/mcp-server/src/index.ts +1 -1
- package/dist/frontend/DatePicker-BcMD49PV.js +0 -275
- package/dist/frontend/ShareDialog-Dzh7rV7J.js +0 -1317
- package/dist/frontend/input-ieWIt8I7.js +0 -257
- package/dist/frontend/messages-CTGAsqFu.js +0 -1106
- package/dist/frontend/resolve-Bl6xmJl_.js +0 -2068
- package/dist/frontend/tag-store.svelte-B3839FYq.js +0 -6152
package/dist/backend/index.js
CHANGED
|
@@ -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").
|
|
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 =
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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;
|
package/dist/backend-launcher.js
CHANGED
|
@@ -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";
|