@open-mercato/documents 0.8.1-develop.7296.1.2111d779db → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/documents/api/[id]/route.js +4 -0
- package/dist/modules/documents/api/[id]/route.js.map +2 -2
- package/dist/modules/documents/api/_shared.js +12 -0
- package/dist/modules/documents/api/_shared.js.map +2 -2
- package/package.json +7 -8
- package/src/modules/documents/api/[id]/route.ts +4 -0
- package/src/modules/documents/api/_shared.ts +17 -0
- package/src/modules/documents/i18n/de.json +1 -0
- package/src/modules/documents/i18n/en.json +1 -0
- package/src/modules/documents/i18n/es.json +1 -0
- package/src/modules/documents/i18n/ko.json +1 -0
- package/src/modules/documents/i18n/pl.json +1 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
assertDocumentNotArchived,
|
|
11
11
|
handleDocumentsRouteError,
|
|
12
12
|
hasDocumentsFeature,
|
|
13
|
+
isRemovedDocumentVisibleToActor,
|
|
13
14
|
loadScopedDocument,
|
|
14
15
|
readBody,
|
|
15
16
|
resolveActorUserId,
|
|
@@ -76,6 +77,9 @@ async function GET(request, context) {
|
|
|
76
77
|
const id = await resolveId(context);
|
|
77
78
|
const ctx = await resolveDocumentsContext(request, ["documents.view"]);
|
|
78
79
|
const projection = await resolveDocumentCapabilityProjection(ctx, id);
|
|
80
|
+
if (!projection.relationshipTier && await isRemovedDocumentVisibleToActor(ctx, id)) {
|
|
81
|
+
throw new CrudHttpError(404, { error: "documents.documents.notFound" });
|
|
82
|
+
}
|
|
79
83
|
if (!projection.relationshipTier || !projection.capabilities.canView) {
|
|
80
84
|
throw new CrudHttpError(403, { error: "Forbidden" });
|
|
81
85
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/documents/api/%5Bid%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { documentUpdateSchema } from '../../data/validators'\nimport { DOCUMENTS_ENTITY_IDS } from '../../lib/constants'\nimport { deriveDocumentCapabilities } from '../../lib/capabilities'\nimport { isDocumentFavorite } from '../../lib/favorites'\nimport { isDocumentWatched } from '../../lib/watchers'\nimport {\n assertDocumentNotArchived,\n handleDocumentsRouteError,\n hasDocumentsFeature,\n loadScopedDocument,\n readBody,\n resolveActorUserId,\n resolveDocumentCapabilityProjection,\n resolveDocumentsContext,\n routeErrorSchema,\n runMutationGuardAfterSuccess,\n serializeDocument,\n validateMutationGuard,\n withDocumentsContextErrors,\n} from '../_shared'\nimport {\n attachDocumentsOperationMetadata,\n buildDocumentsCommandRuntimeContext,\n resolveDocumentsCommandBus,\n} from '../_commands'\nimport type {\n DocumentDeleteCommandInput,\n DocumentUpdateCommandInput,\n} from '../../commands/document-crud'\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nconst capabilitiesSchema = z.object({\n canView: z.boolean(),\n canComment: z.boolean(),\n canEdit: z.boolean(),\n canShare: z.boolean(),\n canDelete: z.boolean(),\n canCreate: z.boolean(),\n canManageTemplates: z.boolean(),\n canArchive: z.boolean(),\n canDuplicate: z.boolean(),\n})\n\nconst detailResponseSchema = z.object({\n id: z.string().uuid(),\n title: z.string(),\n folderId: z.string().uuid().nullable(),\n ownerUserId: z.string().uuid(),\n createdByUserId: z.string().uuid(),\n isActive: z.boolean(),\n archivedAt: z.string().nullable(),\n isFavorite: z.boolean(),\n isWatching: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n tier: z.enum(['owner', 'editor', 'commenter', 'viewer']),\n canShare: z.boolean(),\n capabilities: capabilitiesSchema,\n})\n\nconst mutationResponseSchema = z.object({\n id: z.string().uuid(),\n updatedAt: z.string(),\n})\n\nconst deleteResponseSchema = z.object({\n ok: z.boolean(),\n id: z.string().uuid(),\n updatedAt: z.string(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['documents.view'] },\n PUT: { requireAuth: true, requireFeatures: ['documents.edit'] },\n DELETE: { requireAuth: true, requireFeatures: ['documents.delete'] },\n}\n\nasync function resolveId(context: RouteContext): Promise<string> {\n const params = await context.params\n return params.id\n}\n\nexport async function GET(request: Request, context: RouteContext): Promise<Response> {\n try {\n const id = await resolveId(context)\n const ctx = await resolveDocumentsContext(request, ['documents.view'])\n const projection = await resolveDocumentCapabilityProjection(ctx, id)\n if (!projection.relationshipTier || !projection.capabilities.canView) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n const document = await loadScopedDocument(ctx, id)\n const archivedAt = document.archivedAt?.toISOString() ?? null\n const capabilities = deriveDocumentCapabilities({\n relationshipTier: projection.relationshipTier,\n managerOverride: hasDocumentsFeature(ctx.auth, 'documents.manage'),\n archived: archivedAt !== null,\n userFeatures: ctx.auth.features,\n })\n const isFavorite = await isDocumentFavorite(\n ctx.em,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: resolveActorUserId(ctx.auth),\n },\n id,\n )\n const isWatching = await isDocumentWatched(\n ctx.em,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: resolveActorUserId(ctx.auth),\n },\n id,\n )\n return NextResponse.json({\n ...serializeDocument(document),\n archivedAt,\n isFavorite,\n isWatching,\n tier: projection.relationshipTier,\n canShare: capabilities.canShare,\n capabilities,\n })\n } catch (error) {\n return handleDocumentsRouteError(error, 'documents.detail')\n }\n}\n\nexport async function PUT(request: Request, context: RouteContext): Promise<Response> {\n try {\n const id = await resolveId(context)\n const ctx = await resolveDocumentsContext(request, ['documents.edit'])\n const projection = await resolveDocumentCapabilityProjection(ctx, id)\n if (!projection.capabilities.canEdit) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n await assertDocumentNotArchived(ctx, id)\n const body = await readBody(request)\n const input = documentUpdateSchema.parse({ ...body, id })\n const guardResult = await validateMutationGuard(ctx, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: id,\n operation: 'update',\n mutationPayload: input,\n mutationPayloadSchema: documentUpdateSchema,\n })\n\n const commandInput: DocumentUpdateCommandInput = {\n ...input,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n }\n const { result, logEntry } = await resolveDocumentsCommandBus(ctx).execute<\n DocumentUpdateCommandInput,\n { id: string; updatedAt: string }\n >('documents.document.update', {\n input: commandInput,\n ctx: buildDocumentsCommandRuntimeContext(ctx),\n })\n await runMutationGuardAfterSuccess(ctx, guardResult, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: id,\n operation: 'update',\n })\n\n const response = NextResponse.json({ id: result.id, updatedAt: result.updatedAt })\n return attachDocumentsOperationMetadata(response, logEntry, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: result.id,\n })\n } catch (error) {\n return handleDocumentsRouteError(error, 'documents.update')\n }\n}\n\nexport async function DELETE(request: Request, context: RouteContext): Promise<Response> {\n try {\n const id = await resolveId(context)\n const ctx = await resolveDocumentsContext(request, ['documents.delete'])\n const projection = await resolveDocumentCapabilityProjection(ctx, id)\n if (!projection.capabilities.canDelete) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n const guardResult = await validateMutationGuard(ctx, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: id,\n operation: 'delete',\n })\n\n const commandInput: DocumentDeleteCommandInput = {\n id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n }\n const { result, logEntry } = await resolveDocumentsCommandBus(ctx).execute<\n DocumentDeleteCommandInput,\n { id: string; updatedAt: string }\n >('documents.document.delete', {\n input: commandInput,\n ctx: buildDocumentsCommandRuntimeContext(ctx),\n })\n await runMutationGuardAfterSuccess(ctx, guardResult, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: id,\n operation: 'delete',\n })\n\n const response = NextResponse.json({ ok: true, id: result.id, updatedAt: result.updatedAt })\n return attachDocumentsOperationMetadata(response, logEntry, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: result.id,\n })\n } catch (error) {\n return handleDocumentsRouteError(error, 'documents.delete')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = withDocumentsContextErrors({\n tag: 'Documents',\n summary: 'Document detail',\n pathParams: z.object({ id: z.string().uuid() }),\n methods: {\n GET: {\n summary: 'Get document metadata',\n responses: [{ status: 200, description: 'Document metadata', schema: detailResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: routeErrorSchema },\n { status: 403, description: 'Forbidden', schema: routeErrorSchema },\n { status: 404, description: 'Not found', schema: routeErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update document metadata',\n requestBody: { contentType: 'application/json', schema: documentUpdateSchema },\n responses: [{ status: 200, description: 'Document updated', schema: mutationResponseSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: routeErrorSchema },\n { status: 401, description: 'Unauthorized', schema: routeErrorSchema },\n { status: 403, description: 'Forbidden', schema: routeErrorSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: routeErrorSchema },\n { status: 413, description: 'Request body exceeds the safe resource bound', schema: routeErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete document',\n description: 'Transactionally releases every document-owned attachment row and its quota usage, soft-deletes the document record, then runs reference-checked provider cleanup after commit. Because attachment bytes cannot be restored, this operation is intentionally not undoable.',\n responses: [{ status: 200, description: 'Document deleted', schema: deleteResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: routeErrorSchema },\n { status: 403, description: 'Forbidden', schema: routeErrorSchema },\n { status: 404, description: 'Not found', schema: routeErrorSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: routeErrorSchema },\n ],\n },\n },\n})\n\nexport default { GET, PUT, DELETE }\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { documentUpdateSchema } from '../../data/validators'\nimport { DOCUMENTS_ENTITY_IDS } from '../../lib/constants'\nimport { deriveDocumentCapabilities } from '../../lib/capabilities'\nimport { isDocumentFavorite } from '../../lib/favorites'\nimport { isDocumentWatched } from '../../lib/watchers'\nimport {\n assertDocumentNotArchived,\n handleDocumentsRouteError,\n hasDocumentsFeature,\n isRemovedDocumentVisibleToActor,\n loadScopedDocument,\n readBody,\n resolveActorUserId,\n resolveDocumentCapabilityProjection,\n resolveDocumentsContext,\n routeErrorSchema,\n runMutationGuardAfterSuccess,\n serializeDocument,\n validateMutationGuard,\n withDocumentsContextErrors,\n} from '../_shared'\nimport {\n attachDocumentsOperationMetadata,\n buildDocumentsCommandRuntimeContext,\n resolveDocumentsCommandBus,\n} from '../_commands'\nimport type {\n DocumentDeleteCommandInput,\n DocumentUpdateCommandInput,\n} from '../../commands/document-crud'\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nconst capabilitiesSchema = z.object({\n canView: z.boolean(),\n canComment: z.boolean(),\n canEdit: z.boolean(),\n canShare: z.boolean(),\n canDelete: z.boolean(),\n canCreate: z.boolean(),\n canManageTemplates: z.boolean(),\n canArchive: z.boolean(),\n canDuplicate: z.boolean(),\n})\n\nconst detailResponseSchema = z.object({\n id: z.string().uuid(),\n title: z.string(),\n folderId: z.string().uuid().nullable(),\n ownerUserId: z.string().uuid(),\n createdByUserId: z.string().uuid(),\n isActive: z.boolean(),\n archivedAt: z.string().nullable(),\n isFavorite: z.boolean(),\n isWatching: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n tier: z.enum(['owner', 'editor', 'commenter', 'viewer']),\n canShare: z.boolean(),\n capabilities: capabilitiesSchema,\n})\n\nconst mutationResponseSchema = z.object({\n id: z.string().uuid(),\n updatedAt: z.string(),\n})\n\nconst deleteResponseSchema = z.object({\n ok: z.boolean(),\n id: z.string().uuid(),\n updatedAt: z.string(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['documents.view'] },\n PUT: { requireAuth: true, requireFeatures: ['documents.edit'] },\n DELETE: { requireAuth: true, requireFeatures: ['documents.delete'] },\n}\n\nasync function resolveId(context: RouteContext): Promise<string> {\n const params = await context.params\n return params.id\n}\n\nexport async function GET(request: Request, context: RouteContext): Promise<Response> {\n try {\n const id = await resolveId(context)\n const ctx = await resolveDocumentsContext(request, ['documents.view'])\n const projection = await resolveDocumentCapabilityProjection(ctx, id)\n if (!projection.relationshipTier && await isRemovedDocumentVisibleToActor(ctx, id)) {\n throw new CrudHttpError(404, { error: 'documents.documents.notFound' })\n }\n if (!projection.relationshipTier || !projection.capabilities.canView) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n const document = await loadScopedDocument(ctx, id)\n const archivedAt = document.archivedAt?.toISOString() ?? null\n const capabilities = deriveDocumentCapabilities({\n relationshipTier: projection.relationshipTier,\n managerOverride: hasDocumentsFeature(ctx.auth, 'documents.manage'),\n archived: archivedAt !== null,\n userFeatures: ctx.auth.features,\n })\n const isFavorite = await isDocumentFavorite(\n ctx.em,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: resolveActorUserId(ctx.auth),\n },\n id,\n )\n const isWatching = await isDocumentWatched(\n ctx.em,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: resolveActorUserId(ctx.auth),\n },\n id,\n )\n return NextResponse.json({\n ...serializeDocument(document),\n archivedAt,\n isFavorite,\n isWatching,\n tier: projection.relationshipTier,\n canShare: capabilities.canShare,\n capabilities,\n })\n } catch (error) {\n return handleDocumentsRouteError(error, 'documents.detail')\n }\n}\n\nexport async function PUT(request: Request, context: RouteContext): Promise<Response> {\n try {\n const id = await resolveId(context)\n const ctx = await resolveDocumentsContext(request, ['documents.edit'])\n const projection = await resolveDocumentCapabilityProjection(ctx, id)\n if (!projection.capabilities.canEdit) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n await assertDocumentNotArchived(ctx, id)\n const body = await readBody(request)\n const input = documentUpdateSchema.parse({ ...body, id })\n const guardResult = await validateMutationGuard(ctx, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: id,\n operation: 'update',\n mutationPayload: input,\n mutationPayloadSchema: documentUpdateSchema,\n })\n\n const commandInput: DocumentUpdateCommandInput = {\n ...input,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n }\n const { result, logEntry } = await resolveDocumentsCommandBus(ctx).execute<\n DocumentUpdateCommandInput,\n { id: string; updatedAt: string }\n >('documents.document.update', {\n input: commandInput,\n ctx: buildDocumentsCommandRuntimeContext(ctx),\n })\n await runMutationGuardAfterSuccess(ctx, guardResult, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: id,\n operation: 'update',\n })\n\n const response = NextResponse.json({ id: result.id, updatedAt: result.updatedAt })\n return attachDocumentsOperationMetadata(response, logEntry, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: result.id,\n })\n } catch (error) {\n return handleDocumentsRouteError(error, 'documents.update')\n }\n}\n\nexport async function DELETE(request: Request, context: RouteContext): Promise<Response> {\n try {\n const id = await resolveId(context)\n const ctx = await resolveDocumentsContext(request, ['documents.delete'])\n const projection = await resolveDocumentCapabilityProjection(ctx, id)\n if (!projection.capabilities.canDelete) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n const guardResult = await validateMutationGuard(ctx, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: id,\n operation: 'delete',\n })\n\n const commandInput: DocumentDeleteCommandInput = {\n id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n }\n const { result, logEntry } = await resolveDocumentsCommandBus(ctx).execute<\n DocumentDeleteCommandInput,\n { id: string; updatedAt: string }\n >('documents.document.delete', {\n input: commandInput,\n ctx: buildDocumentsCommandRuntimeContext(ctx),\n })\n await runMutationGuardAfterSuccess(ctx, guardResult, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: id,\n operation: 'delete',\n })\n\n const response = NextResponse.json({ ok: true, id: result.id, updatedAt: result.updatedAt })\n return attachDocumentsOperationMetadata(response, logEntry, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: result.id,\n })\n } catch (error) {\n return handleDocumentsRouteError(error, 'documents.delete')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = withDocumentsContextErrors({\n tag: 'Documents',\n summary: 'Document detail',\n pathParams: z.object({ id: z.string().uuid() }),\n methods: {\n GET: {\n summary: 'Get document metadata',\n responses: [{ status: 200, description: 'Document metadata', schema: detailResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: routeErrorSchema },\n { status: 403, description: 'Forbidden', schema: routeErrorSchema },\n { status: 404, description: 'Not found', schema: routeErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update document metadata',\n requestBody: { contentType: 'application/json', schema: documentUpdateSchema },\n responses: [{ status: 200, description: 'Document updated', schema: mutationResponseSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: routeErrorSchema },\n { status: 401, description: 'Unauthorized', schema: routeErrorSchema },\n { status: 403, description: 'Forbidden', schema: routeErrorSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: routeErrorSchema },\n { status: 413, description: 'Request body exceeds the safe resource bound', schema: routeErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete document',\n description: 'Transactionally releases every document-owned attachment row and its quota usage, soft-deletes the document record, then runs reference-checked provider cleanup after commit. Because attachment bytes cannot be restored, this operation is intentionally not undoable.',\n responses: [{ status: 200, description: 'Document deleted', schema: deleteResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: routeErrorSchema },\n { status: 403, description: 'Forbidden', schema: routeErrorSchema },\n { status: 404, description: 'Not found', schema: routeErrorSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: routeErrorSchema },\n ],\n },\n },\n})\n\nexport default { GET, PUT, DELETE }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -384,6 +384,17 @@ async function loadScopedDocument(ctx, id) {
|
|
|
384
384
|
if (!document) throw new CrudHttpError(404, { error: "documents.documents.notFound" });
|
|
385
385
|
return document;
|
|
386
386
|
}
|
|
387
|
+
async function isRemovedDocumentVisibleToActor(ctx, id) {
|
|
388
|
+
const document = await findOneWithDecryption(ctx.em, Document, {
|
|
389
|
+
id,
|
|
390
|
+
tenantId: ctx.tenantId,
|
|
391
|
+
organizationId: ctx.organizationId,
|
|
392
|
+
deletedAt: { $ne: null }
|
|
393
|
+
}, { fields: ["id", "ownerUserId"] }, { tenantId: ctx.tenantId, organizationId: ctx.organizationId });
|
|
394
|
+
if (!document) return false;
|
|
395
|
+
if (hasDocumentsFeature(ctx.auth, "documents.manage")) return true;
|
|
396
|
+
return document.ownerUserId === resolveActorUserId(ctx.auth);
|
|
397
|
+
}
|
|
387
398
|
async function loadScopedShare(ctx, documentId, shareId) {
|
|
388
399
|
const share = await findOneWithDecryption(ctx.em, DocumentShare, {
|
|
389
400
|
id: shareId,
|
|
@@ -476,6 +487,7 @@ export {
|
|
|
476
487
|
handleDocumentsRouteError,
|
|
477
488
|
hasAnyDocumentsFeature,
|
|
478
489
|
hasDocumentsFeature,
|
|
490
|
+
isRemovedDocumentVisibleToActor,
|
|
479
491
|
loadDocumentArchivedState,
|
|
480
492
|
loadScopedDocument,
|
|
481
493
|
loadScopedShare,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/documents/api/_shared.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { z } from 'zod'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'\nimport {\n ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,\n resolveActiveOrganizationId,\n} from '@open-mercato/shared/lib/auth/organizationScope'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport {\n runRouteMutationGuards,\n type RouteMutationGuardPassed,\n} from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { OpenApiHttpMethod, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n Document,\n DocumentContent,\n DocumentFolder,\n DocumentShare,\n} from '../data/entities'\nimport {\n deriveDocumentCapabilities,\n type DocumentCapabilities,\n} from '../lib/capabilities'\nimport {\n resolveActiveSubjectRoleIds,\n resolvePermission,\n type DocumentTier,\n} from '../lib/permissions'\nimport { hasResolvedDocumentsOrganizationAccess } from '../lib/organizationAccess'\nimport { containsCanonicalUuid } from '../lib/displayLabels'\nimport {\n DOCUMENTS_JSON_BODY_LIMITS,\n readBoundedJsonBody,\n} from '../lib/requestBody'\nimport { resolveOrganizationScopeService } from '../lib/platformServices'\n\nconst logger = createLogger('documents').child({ component: 'api' })\n\nexport type DocumentsAuthContext = NonNullable<AuthContext> & {\n features: string[]\n roleIds: string[]\n resolvedRoleIds: string[]\n organizationId: string\n}\n\nexport type DocumentsRouteContext = {\n container: AwilixContainer\n em: EntityManager\n auth: DocumentsAuthContext\n tenantId: string\n organizationId: string\n request: Request\n}\n\ntype RbacServiceLike = {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n organizations: string[] | null\n }>\n}\n\nexport type MutationGuardResult = RouteMutationGuardPassed\n\nexport type DocumentCapabilityProjection = {\n relationshipTier: DocumentTier | null\n capabilities: DocumentCapabilities\n}\n\n/**\n * Every documents error body carries a translated `error` message. Some also\n * carry a machine-readable `code` discriminator: without it a client cannot\n * tell an unresolved organization scope (recoverable \u2014 pick an organization)\n * apart from an ordinary validation rejection, since both are a 400.\n */\nexport const routeErrorSchema = z.object({ error: z.string(), code: z.string().optional() })\n\nexport const ORGANIZATION_SCOPE_REQUIRED_DESCRIPTION =\n `Organization scope could not be resolved (\\`code: ${ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE}\\`)`\n\nexport const ORGANIZATION_SELECTION_INVALID_ERROR_CODE = 'organization_selection_invalid'\n\nexport const ORGANIZATION_SELECTION_INVALID_DESCRIPTION =\n `The selected organization no longer resolves (\\`code: ${ORGANIZATION_SELECTION_INVALID_ERROR_CODE}\\`)`\n\n/**\n * `resolveDocumentsContext` answers 400 with the\n * `organization_scope_required` discriminator whenever an unrestricted caller\n * (a super-admin viewing a foreign tenant with \"all organizations\" selected)\n * has no resolvable organization. Every route that resolves a documents\n * context can therefore return it, including methods whose own contract\n * declares no 400 at all \u2014 so the contract has to say so, or generated clients\n * and the API reference cannot model the recovery path.\n *\n * Responses are keyed by status when the document is built, so a route that\n * already documents a 400 must merge rather than append a second entry.\n */\nexport function withDocumentsContextErrors(doc: OpenApiRouteDoc): OpenApiRouteDoc {\n const methods: OpenApiRouteDoc['methods'] = {}\n for (const method of Object.keys(doc.methods) as OpenApiHttpMethod[]) {\n const methodDoc = doc.methods[method]\n if (!methodDoc) continue\n const merge = (\n errors: NonNullable<typeof methodDoc.errors>,\n status: number,\n description: string,\n ): NonNullable<typeof methodDoc.errors> => (\n errors.some((error) => error.status === status)\n ? errors.map((error) => (error.status === status\n ? {\n ...error,\n description: error.description\n ? `${error.description}, or ${description}`\n : description,\n schema: error.schema ?? routeErrorSchema,\n }\n : error))\n : [...errors, { status, description, schema: routeErrorSchema }]\n )\n // `resolveDocumentsContext` answers 400 `organization_scope_required` and\n // 422 `organization_selection_invalid` from the same call, so both ride on\n // every route that resolves a documents context.\n let errors = merge(methodDoc.errors ?? [], 400, ORGANIZATION_SCOPE_REQUIRED_DESCRIPTION)\n errors = merge(errors, 422, ORGANIZATION_SELECTION_INVALID_DESCRIPTION)\n methods[method] = { ...methodDoc, errors }\n }\n return { ...doc, methods }\n}\n\nconst actorUuidSchema = z.string().uuid()\n\nexport const ROUTE_ERROR_TRANSLATIONS: Record<string, { key: string; fallback: string }> = {\n Unauthorized: { key: 'api.errors.unauthorized', fallback: 'Unauthorized' },\n Forbidden: { key: 'api.errors.forbidden', fallback: 'Forbidden' },\n 'Organization context is required': {\n key: 'documents.errors.organizationRequired',\n fallback: 'Organization context is required',\n },\n 'Validation failed': { key: 'api.errors.invalidPayload', fallback: 'Invalid payload.' },\n 'Internal server error': { key: 'api.errors.internal', fallback: 'Internal server error' },\n 'Record changed by another user': {\n key: 'documents.errors.recordChanged',\n fallback: 'Record changed by another user',\n },\n 'Document not found': { key: 'documents.documents.notFound', fallback: 'Document not found.' },\n 'documents.content.notFound': {\n key: 'documents.content.notFound',\n fallback: 'Document content not found.',\n },\n 'documents.versions.notFound': {\n key: 'documents.versions.notFound',\n fallback: 'Version not found.',\n },\n 'documents.folders.error.invalidPlacement': {\n key: 'documents.folders.error.invalidPlacement',\n fallback: 'This folder cannot be placed there.',\n },\n 'Folder not found': { key: 'documents.folders.notFound', fallback: 'Folder not found.' },\n 'Share not found': { key: 'documents.share.notFound', fallback: 'Share not found.' },\n 'Comment not found': { key: 'documents.comments.notFound', fallback: 'Comment not found.' },\n 'Document comment limit reached': {\n key: 'documents.comments.limitExceeded',\n fallback: 'This document has reached its comment limit.',\n },\n 'Document version history storage limit exceeded': {\n key: 'documents.versions.quotaExceeded',\n fallback: 'Version history could not retain this snapshot within the document storage limit.',\n },\n 'documents.errors.requestBodyTooLarge': {\n key: 'documents.errors.requestBodyTooLarge',\n fallback: 'Request body is too large.',\n },\n 'Share principal not found in this organization': {\n key: 'documents.share.principalNotFound',\n fallback: 'Share principal not found in this organization',\n },\n 'Private attachment partition is not configured': {\n key: 'documents.attachments.partitionUnavailable',\n fallback: 'Private attachment partition is not configured',\n },\n 'Expected multipart/form-data': {\n key: 'documents.attachments.multipartRequired',\n fallback: 'Expected multipart/form-data',\n },\n 'File is required': { key: 'documents.attachments.fileRequired', fallback: 'File is required' },\n 'Attachment exceeds the maximum upload size.': {\n key: 'documents.attachments.tooLarge',\n fallback: 'Attachment exceeds the maximum upload size.',\n },\n 'Executable file types are not allowed as attachments.': {\n key: 'documents.attachments.executableBlocked',\n fallback: 'Executable file types are not allowed as attachments.',\n },\n 'Active content uploads are not allowed.': {\n key: 'documents.attachments.activeContentBlocked',\n fallback: 'Active content uploads are not allowed.',\n },\n 'Attachment storage quota exceeded for this tenant.': {\n key: 'documents.attachments.quotaExceeded',\n fallback: 'Attachment storage quota exceeded for this tenant.',\n },\n 'Attachment not found': { key: 'documents.attachments.notFound', fallback: 'Attachment not found' },\n 'Partition misconfigured': {\n key: 'documents.attachments.partitionMisconfigured',\n fallback: 'Attachment partition is misconfigured',\n },\n 'File not available': {\n key: 'documents.attachments.fileUnavailable',\n fallback: 'File not available',\n },\n 'Unsupported format': { key: 'documents.export.unsupportedFormat', fallback: 'Unsupported format' },\n 'documents.export.runtimeUnavailable': {\n key: 'documents.export.runtimeUnavailable',\n fallback: 'PDF export is temporarily unavailable.',\n },\n 'documents.errors.organizationSelectionInvalid': {\n key: 'documents.errors.organizationSelectionInvalid',\n fallback: 'Your selected organization is no longer available. Please re-select an organization and try again.',\n },\n}\n\nconst ROUTE_ERROR_KEY_FALLBACKS = Object.fromEntries(\n Object.values(ROUTE_ERROR_TRANSLATIONS).map(({ key, fallback }) => [key, fallback]),\n) as Record<string, string>\n\nfunction normalizeStrings(values: unknown): string[] {\n if (!Array.isArray(values)) return []\n return Array.from(\n new Set(\n values\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value) => value.length > 0),\n ),\n )\n}\n\nfunction resolveSelectedOrganization(\n auth: NonNullable<AuthContext>,\n scope: { selectedId?: string | null; filterIds?: string[] | null; allowedIds?: string[] | null; tenantId?: string | null } | null,\n): string | null {\n // Every document row carries one organization, so there is no \"all\n // organizations\" view of this module. When the super-admin switcher clears\n // `auth.orgId`, keep the operator on their own organization (the shared\n // helper refuses the fallback once the tenant override moved them elsewhere)\n // instead of failing every request.\n const activeOrganizationId = resolveActiveOrganizationId(auth)\n const allowed = scope?.filterIds ?? scope?.allowedIds ?? (activeOrganizationId ? [activeOrganizationId] : null)\n const candidates = [\n scope?.selectedId ?? null,\n activeOrganizationId,\n Array.isArray(allowed) && allowed.length > 0 ? allowed[0] : null,\n ]\n for (const candidate of candidates) {\n if (!candidate) continue\n if (!Array.isArray(allowed) || allowed.includes(candidate)) return candidate\n }\n return null\n}\n\nfunction readAuthScopeId(auth: NonNullable<AuthContext>, key: 'actorTenantId' | 'actorOrgId'): string | null {\n const value = auth[key]\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n}\n\nfunction normalizeActorUuid(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const normalized = value.trim()\n return actorUuidSchema.safeParse(normalized).success ? normalized : null\n}\n\nfunction isApiKeyAuth(auth: NonNullable<AuthContext>): boolean {\n return auth.isApiKey === true || auth.sub.trim().startsWith('api_key:')\n}\n\nexport function hasDocumentsFeature(auth: DocumentsAuthContext, feature: string): boolean {\n if (auth.isSuperAdmin === true) return true\n return hasAllFeatures([feature], auth.features)\n}\n\nexport function hasAnyDocumentsFeature(auth: DocumentsAuthContext, features: string[]): boolean {\n if (auth.isSuperAdmin === true) return true\n return features.some((feature) => hasAllFeatures([feature], auth.features))\n}\n\nexport function deriveCapabilitiesForContext(\n ctx: DocumentsRouteContext,\n relationshipTier: DocumentTier | null,\n options: { archived?: boolean } = {},\n): DocumentCapabilities {\n return deriveDocumentCapabilities({\n relationshipTier,\n managerOverride: hasDocumentsFeature(ctx.auth, 'documents.manage'),\n archived: options.archived === true,\n userFeatures: ctx.auth.features,\n })\n}\n\nexport async function resolveDocumentCapabilityProjection(\n ctx: DocumentsRouteContext,\n documentId: string,\n options: { archived?: boolean } = {},\n): Promise<DocumentCapabilityProjection> {\n const relationshipTier = await resolvePermission(ctx.em, documentId, ctx.auth)\n return {\n relationshipTier,\n capabilities: deriveCapabilitiesForContext(ctx, relationshipTier, options),\n }\n}\n\nexport async function loadDocumentArchivedState(\n ctx: DocumentsRouteContext,\n documentId: string,\n): Promise<{ archivedAt: Date | null }> {\n const document = await findOneWithDecryption(\n ctx.em,\n Document,\n {\n id: documentId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n { fields: ['id', 'archivedAt'] },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!document) throw new CrudHttpError(404, { error: 'documents.documents.notFound' })\n return { archivedAt: document.archivedAt ?? null }\n}\n\nexport async function assertDocumentNotArchived(\n ctx: DocumentsRouteContext,\n documentId: string,\n): Promise<void> {\n const state = await loadDocumentArchivedState(ctx, documentId)\n if (state.archivedAt !== null) {\n throw new CrudHttpError(403, { error: 'documents.errors.documentArchived' })\n }\n}\n\nexport async function resolveDocumentsContext(\n request: Request,\n requiredFeatures: string[],\n): Promise<DocumentsRouteContext> {\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const auth = await getAuthFromRequest(request)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: 'api.errors.unauthorized' })\n }\n\n const authenticatedActorUserId = resolveActorUserId(auth)\n const actorTenantId = readAuthScopeId(auth, 'actorTenantId') ?? auth.tenantId\n const actorOrganizationId = readAuthScopeId(auth, 'actorOrgId') ?? auth.orgId\n const rbacService = container.resolve('rbacService') as RbacServiceLike\n const actorAcl = await rbacService.loadAcl(auth.sub, {\n tenantId: actorTenantId,\n organizationId: actorOrganizationId,\n })\n // Organization scope resolution historically accepts the authenticated\n // superadmin bit to support tenant/org switching. Replace that potentially\n // stale token bit with the live RBAC decision before asking the shared scope\n // resolver to expand parent grants into descendant organizations.\n const scopeAuth = {\n ...auth,\n // API-key authentication validates both keyId and any backing user. Keep\n // the prefixed subject for ACL while projecting its UUID domain actor for\n // ownership/share/audit helpers. An unbound key uses its own key UUID.\n userId: authenticatedActorUserId,\n isSuperAdmin: actorAcl.isSuperAdmin === true,\n }\n const organizationScopeService = resolveOrganizationScopeService(container)\n if (!organizationScopeService) {\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n const scope = await organizationScopeService.resolveForRequest({ auth: scopeAuth, request })\n // The caller explicitly selected an organization that no longer resolves to a\n // real, accessible org (stale selected-org cookie after a delete or an access\n // loss). The scope resolver falls back to another allowed org, so continuing\n // would silently read and write against an organization the caller never\n // chose. Fail loud on every documents operation instead \u2014 the same contract\n // `makeCrudRoute` enforces via `rejectInvalidOrgSelection` (#3936).\n if (scope?.selectionRejected) {\n throw new CrudHttpError(422, {\n error: 'documents.errors.organizationSelectionInvalid',\n code: ORGANIZATION_SELECTION_INVALID_ERROR_CODE,\n })\n }\n const tenantId = scope?.tenantId ?? auth.tenantId\n const organizationId = resolveSelectedOrganization(scopeAuth, scope)\n if (!tenantId) {\n throw new CrudHttpError(400, { error: 'documents.errors.organizationRequired' })\n }\n if (!organizationId) {\n // An unrestricted caller with no resolvable organization (a super-admin\n // viewing a foreign tenant with \"all organizations\" selected) is a 400, not\n // a 401 (which `apiFetch` would turn into a refresh loop) and not a 403 (the\n // caller is allowed in; they only need to pick an organization). A caller\n // whose grants expand to no organization at all stays a 403.\n const unrestricted = (scope?.filterIds ?? scope?.allowedIds ?? null) === null\n if (unrestricted) {\n throw new CrudHttpError(400, {\n error: 'documents.errors.organizationRequired',\n code: ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,\n })\n }\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n\n const acl = await rbacService.loadAcl(auth.sub, { tenantId, organizationId })\n if (!hasResolvedDocumentsOrganizationAccess(acl, organizationId, scope)) {\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n const roleIds = await resolveActiveSubjectRoleIds(\n container,\n { tenantId, organizationId },\n auth.sub,\n )\n const documentsAuth: DocumentsAuthContext = {\n ...scopeAuth,\n tenantId,\n orgId: organizationId,\n organizationId,\n roleIds,\n resolvedRoleIds: roleIds,\n // JWT/trusted-request claims are only authentication hints. The current\n // RBAC projection is authoritative for features and superadmin status so\n // a revoked grant cannot survive in a long-lived token.\n features: acl.isSuperAdmin === true ? ['*'] : normalizeStrings(acl.features),\n isSuperAdmin: acl.isSuperAdmin === true,\n }\n\n if (!hasAnyDocumentsFeature(documentsAuth, requiredFeatures)) {\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n\n return {\n container,\n em,\n auth: documentsAuth,\n tenantId,\n organizationId,\n request,\n }\n}\n\nexport function resolveActorUserId(auth: NonNullable<AuthContext>): string {\n const subject = auth.sub.trim()\n if (isApiKeyAuth(auth)) {\n // `keyId` is populated only by successful API-key authentication. Do not\n // recover an actor by parsing an arbitrary prefixed subject when that\n // validated field is absent, malformed, or disagrees with the subject.\n const keyId = normalizeActorUuid(auth.keyId)\n if (!keyId || subject !== `api_key:${keyId}`) {\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n if (auth.userId === undefined || auth.userId === null) return keyId\n const backingUserId = normalizeActorUuid(auth.userId)\n if (backingUserId) return backingUserId\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n\n const actorUserId = normalizeActorUuid(subject)\n const claimedUserId = auth.userId === undefined || auth.userId === null\n ? actorUserId\n : normalizeActorUuid(auth.userId)\n if (actorUserId && claimedUserId === actorUserId) return actorUserId\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n}\n\nexport async function readBody(\n request: Request,\n maxBytes = DOCUMENTS_JSON_BODY_LIMITS.standard,\n): Promise<Record<string, unknown>> {\n return (await readBoundedJsonBody<Record<string, unknown>>(request, maxBytes, {})) ?? {}\n}\n\nasync function localizeRouteErrorBody(body: Record<string, unknown>): Promise<Record<string, unknown>> {\n const rawError = typeof body.error === 'string' ? body.error : null\n if (!rawError) return body\n\n if (containsCanonicalUuid(rawError)) {\n const { key, fallback } = ROUTE_ERROR_TRANSLATIONS['Internal server error']!\n try {\n const { translate } = await resolveTranslations()\n return { ...body, error: translate(key, fallback) }\n } catch {\n return { ...body, error: fallback }\n }\n }\n\n const literal = ROUTE_ERROR_TRANSLATIONS[rawError]\n const key = literal?.key ?? rawError\n const fallback = literal?.fallback ?? ROUTE_ERROR_KEY_FALLBACKS[key]\n // Attachment failures reach these routes as the attachments module's own\n // catalogued keys. The locale dictionary is merged across modules, so they\n // resolve here; without this prefix the raw key would reach the client.\n if (\n !key.startsWith('documents.')\n && !key.startsWith('api.')\n && !key.startsWith('attachments.')\n ) return body\n\n try {\n const { translate } = await resolveTranslations()\n return { ...body, error: translate(key, fallback) }\n } catch {\n return { ...body, error: fallback ?? key }\n }\n}\n\nexport async function handleDocumentsRouteError(error: unknown, label: string): Promise<Response> {\n if (isCrudHttpError(error)) {\n const body = error.body && typeof error.body === 'object' && !Array.isArray(error.body)\n ? error.body as Record<string, unknown>\n : { error: 'api.errors.internal' }\n return NextResponse.json(await localizeRouteErrorBody(body), { status: error.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (error instanceof z.ZodError) {\n return NextResponse.json(\n await localizeRouteErrorBody({ error: 'api.errors.invalidPayload', details: error.issues }),\n { status: 400 },\n )\n }\n logger.error(`${label} failed`, { err: error })\n return NextResponse.json(\n await localizeRouteErrorBody({ error: 'api.errors.internal' }),\n { status: 500 },\n )\n}\n\nexport async function loadScopedDocument(ctx: DocumentsRouteContext, id: string): Promise<Document> {\n const document = await findOneWithDecryption(ctx.em, Document, {\n id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n }, undefined, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })\n if (!document) throw new CrudHttpError(404, { error: 'documents.documents.notFound' })\n return document\n}\n\nexport async function loadScopedShare(\n ctx: DocumentsRouteContext,\n documentId: string,\n shareId: string,\n): Promise<DocumentShare> {\n const share = await findOneWithDecryption(ctx.em, DocumentShare, {\n id: shareId,\n documentId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n }, undefined, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })\n if (!share) throw new CrudHttpError(404, { error: 'documents.share.notFound' })\n return share\n}\n\nexport function serializeDocument(document: Document): Record<string, unknown> {\n return {\n id: document.id,\n title: document.title,\n folderId: document.folderId ?? null,\n ownerUserId: document.ownerUserId,\n createdByUserId: document.createdByUserId,\n isActive: document.isActive,\n createdAt: document.createdAt.toISOString(),\n updatedAt: document.updatedAt.toISOString(),\n }\n}\n\nexport function serializeContent(content: DocumentContent | null): Record<string, unknown> {\n return {\n contentHtml: content?.contentHtml ?? '',\n contentText: content?.contentText ?? '',\n updatedAt: content?.updatedAt ? content.updatedAt.toISOString() : null,\n }\n}\n\nexport function serializeFolder(folder: DocumentFolder): Record<string, unknown> {\n return {\n id: folder.id,\n name: folder.name,\n parentFolderId: folder.parentFolderId ?? null,\n ownerUserId: folder.ownerUserId,\n createdAt: folder.createdAt.toISOString(),\n updatedAt: folder.updatedAt.toISOString(),\n }\n}\n\nexport function serializeShare(share: DocumentShare): Record<string, unknown> {\n return {\n id: share.id,\n documentId: share.documentId,\n principalType: share.principalType,\n principalId: share.principalId,\n permission: share.permission,\n createdByUserId: share.createdByUserId,\n createdAt: share.createdAt.toISOString(),\n updatedAt: share.updatedAt.toISOString(),\n }\n}\n\nexport async function validateMutationGuard(\n ctx: DocumentsRouteContext,\n input: {\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n mutationPayload?: Record<string, unknown> | null\n /**\n * Re-parses a guard's `modifiedPayload` before it reaches the command. A\n * registry guard may rewrite the payload, and the route's own schema is the\n * only thing that still guarantees the command receives a shape it can trust.\n */\n mutationPayloadSchema?: { parse: (value: unknown) => Record<string, unknown> }\n },\n): Promise<MutationGuardResult> {\n // `runRouteMutationGuards` runs EVERY registered guard plus the bridged legacy\n // DI service. The deprecated `validateCrudMutationGuard` /\n // `runCrudMutationGuardAfterSuccess` pair used here before resolved only the\n // single DI-registered guard service, so every guard a module contributed\n // through the registry \u2014 along with its payload transformation and its\n // after-success callback \u2014 was silently skipped on Documents writes.\n const guardResult = await runRouteMutationGuards({\n container: ctx.container,\n req: ctx.request,\n auth: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: resolveActorUserId(ctx.auth),\n userFeatures: ctx.auth.features,\n },\n input: {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n mutationPayload: input.mutationPayload ?? null,\n },\n })\n if (!guardResult.ok) {\n throw new CrudHttpError(guardResult.errorStatus, guardResult.errorBody)\n }\n if (guardResult.modifiedPayload && input.mutationPayload) {\n const transformed = input.mutationPayloadSchema\n ? input.mutationPayloadSchema.parse(guardResult.modifiedPayload)\n : guardResult.modifiedPayload\n // Routes read the object they handed in, so the transformation has to land\n // there rather than only on the guard result.\n for (const key of Object.keys(input.mutationPayload)) delete input.mutationPayload[key]\n Object.assign(input.mutationPayload, transformed)\n }\n return guardResult\n}\n\nexport async function runMutationGuardAfterSuccess(\n _ctx: DocumentsRouteContext,\n guardResult: MutationGuardResult,\n _input: {\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n },\n): Promise<void> {\n await guardResult.runAfterSuccess()\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { z } from 'zod'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'\nimport {\n ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,\n resolveActiveOrganizationId,\n} from '@open-mercato/shared/lib/auth/organizationScope'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport {\n runRouteMutationGuards,\n type RouteMutationGuardPassed,\n} from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { OpenApiHttpMethod, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n Document,\n DocumentContent,\n DocumentFolder,\n DocumentShare,\n} from '../data/entities'\nimport {\n deriveDocumentCapabilities,\n type DocumentCapabilities,\n} from '../lib/capabilities'\nimport {\n resolveActiveSubjectRoleIds,\n resolvePermission,\n type DocumentTier,\n} from '../lib/permissions'\nimport { hasResolvedDocumentsOrganizationAccess } from '../lib/organizationAccess'\nimport { containsCanonicalUuid } from '../lib/displayLabels'\nimport {\n DOCUMENTS_JSON_BODY_LIMITS,\n readBoundedJsonBody,\n} from '../lib/requestBody'\nimport { resolveOrganizationScopeService } from '../lib/platformServices'\n\nconst logger = createLogger('documents').child({ component: 'api' })\n\nexport type DocumentsAuthContext = NonNullable<AuthContext> & {\n features: string[]\n roleIds: string[]\n resolvedRoleIds: string[]\n organizationId: string\n}\n\nexport type DocumentsRouteContext = {\n container: AwilixContainer\n em: EntityManager\n auth: DocumentsAuthContext\n tenantId: string\n organizationId: string\n request: Request\n}\n\ntype RbacServiceLike = {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n organizations: string[] | null\n }>\n}\n\nexport type MutationGuardResult = RouteMutationGuardPassed\n\nexport type DocumentCapabilityProjection = {\n relationshipTier: DocumentTier | null\n capabilities: DocumentCapabilities\n}\n\n/**\n * Every documents error body carries a translated `error` message. Some also\n * carry a machine-readable `code` discriminator: without it a client cannot\n * tell an unresolved organization scope (recoverable \u2014 pick an organization)\n * apart from an ordinary validation rejection, since both are a 400.\n */\nexport const routeErrorSchema = z.object({ error: z.string(), code: z.string().optional() })\n\nexport const ORGANIZATION_SCOPE_REQUIRED_DESCRIPTION =\n `Organization scope could not be resolved (\\`code: ${ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE}\\`)`\n\nexport const ORGANIZATION_SELECTION_INVALID_ERROR_CODE = 'organization_selection_invalid'\n\nexport const ORGANIZATION_SELECTION_INVALID_DESCRIPTION =\n `The selected organization no longer resolves (\\`code: ${ORGANIZATION_SELECTION_INVALID_ERROR_CODE}\\`)`\n\n/**\n * `resolveDocumentsContext` answers 400 with the\n * `organization_scope_required` discriminator whenever an unrestricted caller\n * (a super-admin viewing a foreign tenant with \"all organizations\" selected)\n * has no resolvable organization. Every route that resolves a documents\n * context can therefore return it, including methods whose own contract\n * declares no 400 at all \u2014 so the contract has to say so, or generated clients\n * and the API reference cannot model the recovery path.\n *\n * Responses are keyed by status when the document is built, so a route that\n * already documents a 400 must merge rather than append a second entry.\n */\nexport function withDocumentsContextErrors(doc: OpenApiRouteDoc): OpenApiRouteDoc {\n const methods: OpenApiRouteDoc['methods'] = {}\n for (const method of Object.keys(doc.methods) as OpenApiHttpMethod[]) {\n const methodDoc = doc.methods[method]\n if (!methodDoc) continue\n const merge = (\n errors: NonNullable<typeof methodDoc.errors>,\n status: number,\n description: string,\n ): NonNullable<typeof methodDoc.errors> => (\n errors.some((error) => error.status === status)\n ? errors.map((error) => (error.status === status\n ? {\n ...error,\n description: error.description\n ? `${error.description}, or ${description}`\n : description,\n schema: error.schema ?? routeErrorSchema,\n }\n : error))\n : [...errors, { status, description, schema: routeErrorSchema }]\n )\n // `resolveDocumentsContext` answers 400 `organization_scope_required` and\n // 422 `organization_selection_invalid` from the same call, so both ride on\n // every route that resolves a documents context.\n let errors = merge(methodDoc.errors ?? [], 400, ORGANIZATION_SCOPE_REQUIRED_DESCRIPTION)\n errors = merge(errors, 422, ORGANIZATION_SELECTION_INVALID_DESCRIPTION)\n methods[method] = { ...methodDoc, errors }\n }\n return { ...doc, methods }\n}\n\nconst actorUuidSchema = z.string().uuid()\n\nexport const ROUTE_ERROR_TRANSLATIONS: Record<string, { key: string; fallback: string }> = {\n Unauthorized: { key: 'api.errors.unauthorized', fallback: 'Unauthorized' },\n Forbidden: { key: 'api.errors.forbidden', fallback: 'Forbidden' },\n 'Organization context is required': {\n key: 'documents.errors.organizationRequired',\n fallback: 'Organization context is required',\n },\n 'Validation failed': { key: 'api.errors.invalidPayload', fallback: 'Invalid payload.' },\n 'Internal server error': { key: 'api.errors.internal', fallback: 'Internal server error' },\n 'Record changed by another user': {\n key: 'documents.errors.recordChanged',\n fallback: 'Record changed by another user',\n },\n 'Document not found': { key: 'documents.documents.notFound', fallback: 'Document not found.' },\n 'documents.content.notFound': {\n key: 'documents.content.notFound',\n fallback: 'Document content not found.',\n },\n 'documents.versions.notFound': {\n key: 'documents.versions.notFound',\n fallback: 'Version not found.',\n },\n 'documents.folders.error.invalidPlacement': {\n key: 'documents.folders.error.invalidPlacement',\n fallback: 'This folder cannot be placed there.',\n },\n 'Folder not found': { key: 'documents.folders.notFound', fallback: 'Folder not found.' },\n 'Share not found': { key: 'documents.share.notFound', fallback: 'Share not found.' },\n 'Comment not found': { key: 'documents.comments.notFound', fallback: 'Comment not found.' },\n 'Document comment limit reached': {\n key: 'documents.comments.limitExceeded',\n fallback: 'This document has reached its comment limit.',\n },\n 'Document version history storage limit exceeded': {\n key: 'documents.versions.quotaExceeded',\n fallback: 'Version history could not retain this snapshot within the document storage limit.',\n },\n 'documents.errors.requestBodyTooLarge': {\n key: 'documents.errors.requestBodyTooLarge',\n fallback: 'Request body is too large.',\n },\n 'Share principal not found in this organization': {\n key: 'documents.share.principalNotFound',\n fallback: 'Share principal not found in this organization',\n },\n 'Private attachment partition is not configured': {\n key: 'documents.attachments.partitionUnavailable',\n fallback: 'Private attachment partition is not configured',\n },\n 'Expected multipart/form-data': {\n key: 'documents.attachments.multipartRequired',\n fallback: 'Expected multipart/form-data',\n },\n 'File is required': { key: 'documents.attachments.fileRequired', fallback: 'File is required' },\n 'Attachment exceeds the maximum upload size.': {\n key: 'documents.attachments.tooLarge',\n fallback: 'Attachment exceeds the maximum upload size.',\n },\n 'Executable file types are not allowed as attachments.': {\n key: 'documents.attachments.executableBlocked',\n fallback: 'Executable file types are not allowed as attachments.',\n },\n 'Active content uploads are not allowed.': {\n key: 'documents.attachments.activeContentBlocked',\n fallback: 'Active content uploads are not allowed.',\n },\n 'Attachment storage quota exceeded for this tenant.': {\n key: 'documents.attachments.quotaExceeded',\n fallback: 'Attachment storage quota exceeded for this tenant.',\n },\n 'Attachment not found': { key: 'documents.attachments.notFound', fallback: 'Attachment not found' },\n 'Partition misconfigured': {\n key: 'documents.attachments.partitionMisconfigured',\n fallback: 'Attachment partition is misconfigured',\n },\n 'File not available': {\n key: 'documents.attachments.fileUnavailable',\n fallback: 'File not available',\n },\n 'Unsupported format': { key: 'documents.export.unsupportedFormat', fallback: 'Unsupported format' },\n 'documents.export.runtimeUnavailable': {\n key: 'documents.export.runtimeUnavailable',\n fallback: 'PDF export is temporarily unavailable.',\n },\n 'documents.errors.organizationSelectionInvalid': {\n key: 'documents.errors.organizationSelectionInvalid',\n fallback: 'Your selected organization is no longer available. Please re-select an organization and try again.',\n },\n}\n\nconst ROUTE_ERROR_KEY_FALLBACKS = Object.fromEntries(\n Object.values(ROUTE_ERROR_TRANSLATIONS).map(({ key, fallback }) => [key, fallback]),\n) as Record<string, string>\n\nfunction normalizeStrings(values: unknown): string[] {\n if (!Array.isArray(values)) return []\n return Array.from(\n new Set(\n values\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value) => value.length > 0),\n ),\n )\n}\n\nfunction resolveSelectedOrganization(\n auth: NonNullable<AuthContext>,\n scope: { selectedId?: string | null; filterIds?: string[] | null; allowedIds?: string[] | null; tenantId?: string | null } | null,\n): string | null {\n // Every document row carries one organization, so there is no \"all\n // organizations\" view of this module. When the super-admin switcher clears\n // `auth.orgId`, keep the operator on their own organization (the shared\n // helper refuses the fallback once the tenant override moved them elsewhere)\n // instead of failing every request.\n const activeOrganizationId = resolveActiveOrganizationId(auth)\n const allowed = scope?.filterIds ?? scope?.allowedIds ?? (activeOrganizationId ? [activeOrganizationId] : null)\n const candidates = [\n scope?.selectedId ?? null,\n activeOrganizationId,\n Array.isArray(allowed) && allowed.length > 0 ? allowed[0] : null,\n ]\n for (const candidate of candidates) {\n if (!candidate) continue\n if (!Array.isArray(allowed) || allowed.includes(candidate)) return candidate\n }\n return null\n}\n\nfunction readAuthScopeId(auth: NonNullable<AuthContext>, key: 'actorTenantId' | 'actorOrgId'): string | null {\n const value = auth[key]\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n}\n\nfunction normalizeActorUuid(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const normalized = value.trim()\n return actorUuidSchema.safeParse(normalized).success ? normalized : null\n}\n\nfunction isApiKeyAuth(auth: NonNullable<AuthContext>): boolean {\n return auth.isApiKey === true || auth.sub.trim().startsWith('api_key:')\n}\n\nexport function hasDocumentsFeature(auth: DocumentsAuthContext, feature: string): boolean {\n if (auth.isSuperAdmin === true) return true\n return hasAllFeatures([feature], auth.features)\n}\n\nexport function hasAnyDocumentsFeature(auth: DocumentsAuthContext, features: string[]): boolean {\n if (auth.isSuperAdmin === true) return true\n return features.some((feature) => hasAllFeatures([feature], auth.features))\n}\n\nexport function deriveCapabilitiesForContext(\n ctx: DocumentsRouteContext,\n relationshipTier: DocumentTier | null,\n options: { archived?: boolean } = {},\n): DocumentCapabilities {\n return deriveDocumentCapabilities({\n relationshipTier,\n managerOverride: hasDocumentsFeature(ctx.auth, 'documents.manage'),\n archived: options.archived === true,\n userFeatures: ctx.auth.features,\n })\n}\n\nexport async function resolveDocumentCapabilityProjection(\n ctx: DocumentsRouteContext,\n documentId: string,\n options: { archived?: boolean } = {},\n): Promise<DocumentCapabilityProjection> {\n const relationshipTier = await resolvePermission(ctx.em, documentId, ctx.auth)\n return {\n relationshipTier,\n capabilities: deriveCapabilitiesForContext(ctx, relationshipTier, options),\n }\n}\n\nexport async function loadDocumentArchivedState(\n ctx: DocumentsRouteContext,\n documentId: string,\n): Promise<{ archivedAt: Date | null }> {\n const document = await findOneWithDecryption(\n ctx.em,\n Document,\n {\n id: documentId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n { fields: ['id', 'archivedAt'] },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!document) throw new CrudHttpError(404, { error: 'documents.documents.notFound' })\n return { archivedAt: document.archivedAt ?? null }\n}\n\nexport async function assertDocumentNotArchived(\n ctx: DocumentsRouteContext,\n documentId: string,\n): Promise<void> {\n const state = await loadDocumentArchivedState(ctx, documentId)\n if (state.archivedAt !== null) {\n throw new CrudHttpError(403, { error: 'documents.errors.documentArchived' })\n }\n}\n\nexport async function resolveDocumentsContext(\n request: Request,\n requiredFeatures: string[],\n): Promise<DocumentsRouteContext> {\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const auth = await getAuthFromRequest(request)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: 'api.errors.unauthorized' })\n }\n\n const authenticatedActorUserId = resolveActorUserId(auth)\n const actorTenantId = readAuthScopeId(auth, 'actorTenantId') ?? auth.tenantId\n const actorOrganizationId = readAuthScopeId(auth, 'actorOrgId') ?? auth.orgId\n const rbacService = container.resolve('rbacService') as RbacServiceLike\n const actorAcl = await rbacService.loadAcl(auth.sub, {\n tenantId: actorTenantId,\n organizationId: actorOrganizationId,\n })\n // Organization scope resolution historically accepts the authenticated\n // superadmin bit to support tenant/org switching. Replace that potentially\n // stale token bit with the live RBAC decision before asking the shared scope\n // resolver to expand parent grants into descendant organizations.\n const scopeAuth = {\n ...auth,\n // API-key authentication validates both keyId and any backing user. Keep\n // the prefixed subject for ACL while projecting its UUID domain actor for\n // ownership/share/audit helpers. An unbound key uses its own key UUID.\n userId: authenticatedActorUserId,\n isSuperAdmin: actorAcl.isSuperAdmin === true,\n }\n const organizationScopeService = resolveOrganizationScopeService(container)\n if (!organizationScopeService) {\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n const scope = await organizationScopeService.resolveForRequest({ auth: scopeAuth, request })\n // The caller explicitly selected an organization that no longer resolves to a\n // real, accessible org (stale selected-org cookie after a delete or an access\n // loss). The scope resolver falls back to another allowed org, so continuing\n // would silently read and write against an organization the caller never\n // chose. Fail loud on every documents operation instead \u2014 the same contract\n // `makeCrudRoute` enforces via `rejectInvalidOrgSelection` (#3936).\n if (scope?.selectionRejected) {\n throw new CrudHttpError(422, {\n error: 'documents.errors.organizationSelectionInvalid',\n code: ORGANIZATION_SELECTION_INVALID_ERROR_CODE,\n })\n }\n const tenantId = scope?.tenantId ?? auth.tenantId\n const organizationId = resolveSelectedOrganization(scopeAuth, scope)\n if (!tenantId) {\n throw new CrudHttpError(400, { error: 'documents.errors.organizationRequired' })\n }\n if (!organizationId) {\n // An unrestricted caller with no resolvable organization (a super-admin\n // viewing a foreign tenant with \"all organizations\" selected) is a 400, not\n // a 401 (which `apiFetch` would turn into a refresh loop) and not a 403 (the\n // caller is allowed in; they only need to pick an organization). A caller\n // whose grants expand to no organization at all stays a 403.\n const unrestricted = (scope?.filterIds ?? scope?.allowedIds ?? null) === null\n if (unrestricted) {\n throw new CrudHttpError(400, {\n error: 'documents.errors.organizationRequired',\n code: ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,\n })\n }\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n\n const acl = await rbacService.loadAcl(auth.sub, { tenantId, organizationId })\n if (!hasResolvedDocumentsOrganizationAccess(acl, organizationId, scope)) {\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n const roleIds = await resolveActiveSubjectRoleIds(\n container,\n { tenantId, organizationId },\n auth.sub,\n )\n const documentsAuth: DocumentsAuthContext = {\n ...scopeAuth,\n tenantId,\n orgId: organizationId,\n organizationId,\n roleIds,\n resolvedRoleIds: roleIds,\n // JWT/trusted-request claims are only authentication hints. The current\n // RBAC projection is authoritative for features and superadmin status so\n // a revoked grant cannot survive in a long-lived token.\n features: acl.isSuperAdmin === true ? ['*'] : normalizeStrings(acl.features),\n isSuperAdmin: acl.isSuperAdmin === true,\n }\n\n if (!hasAnyDocumentsFeature(documentsAuth, requiredFeatures)) {\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n\n return {\n container,\n em,\n auth: documentsAuth,\n tenantId,\n organizationId,\n request,\n }\n}\n\nexport function resolveActorUserId(auth: NonNullable<AuthContext>): string {\n const subject = auth.sub.trim()\n if (isApiKeyAuth(auth)) {\n // `keyId` is populated only by successful API-key authentication. Do not\n // recover an actor by parsing an arbitrary prefixed subject when that\n // validated field is absent, malformed, or disagrees with the subject.\n const keyId = normalizeActorUuid(auth.keyId)\n if (!keyId || subject !== `api_key:${keyId}`) {\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n if (auth.userId === undefined || auth.userId === null) return keyId\n const backingUserId = normalizeActorUuid(auth.userId)\n if (backingUserId) return backingUserId\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n }\n\n const actorUserId = normalizeActorUuid(subject)\n const claimedUserId = auth.userId === undefined || auth.userId === null\n ? actorUserId\n : normalizeActorUuid(auth.userId)\n if (actorUserId && claimedUserId === actorUserId) return actorUserId\n throw new CrudHttpError(403, { error: 'api.errors.forbidden' })\n}\n\nexport async function readBody(\n request: Request,\n maxBytes = DOCUMENTS_JSON_BODY_LIMITS.standard,\n): Promise<Record<string, unknown>> {\n return (await readBoundedJsonBody<Record<string, unknown>>(request, maxBytes, {})) ?? {}\n}\n\nasync function localizeRouteErrorBody(body: Record<string, unknown>): Promise<Record<string, unknown>> {\n const rawError = typeof body.error === 'string' ? body.error : null\n if (!rawError) return body\n\n if (containsCanonicalUuid(rawError)) {\n const { key, fallback } = ROUTE_ERROR_TRANSLATIONS['Internal server error']!\n try {\n const { translate } = await resolveTranslations()\n return { ...body, error: translate(key, fallback) }\n } catch {\n return { ...body, error: fallback }\n }\n }\n\n const literal = ROUTE_ERROR_TRANSLATIONS[rawError]\n const key = literal?.key ?? rawError\n const fallback = literal?.fallback ?? ROUTE_ERROR_KEY_FALLBACKS[key]\n // Attachment failures reach these routes as the attachments module's own\n // catalogued keys. The locale dictionary is merged across modules, so they\n // resolve here; without this prefix the raw key would reach the client.\n if (\n !key.startsWith('documents.')\n && !key.startsWith('api.')\n && !key.startsWith('attachments.')\n ) return body\n\n try {\n const { translate } = await resolveTranslations()\n return { ...body, error: translate(key, fallback) }\n } catch {\n return { ...body, error: fallback ?? key }\n }\n}\n\nexport async function handleDocumentsRouteError(error: unknown, label: string): Promise<Response> {\n if (isCrudHttpError(error)) {\n const body = error.body && typeof error.body === 'object' && !Array.isArray(error.body)\n ? error.body as Record<string, unknown>\n : { error: 'api.errors.internal' }\n return NextResponse.json(await localizeRouteErrorBody(body), { status: error.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (error instanceof z.ZodError) {\n return NextResponse.json(\n await localizeRouteErrorBody({ error: 'api.errors.invalidPayload', details: error.issues }),\n { status: 400 },\n )\n }\n logger.error(`${label} failed`, { err: error })\n return NextResponse.json(\n await localizeRouteErrorBody({ error: 'api.errors.internal' }),\n { status: 500 },\n )\n}\n\nexport async function loadScopedDocument(ctx: DocumentsRouteContext, id: string): Promise<Document> {\n const document = await findOneWithDecryption(ctx.em, Document, {\n id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n }, undefined, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })\n if (!document) throw new CrudHttpError(404, { error: 'documents.documents.notFound' })\n return document\n}\n\n/**\n * Only the owner (or a documents manager) may learn that a document was\n * removed, e.g. after undoing its creation; everyone else keeps the\n * existence-hiding 403 so deleted ids stay indistinguishable from foreign ones.\n */\nexport async function isRemovedDocumentVisibleToActor(ctx: DocumentsRouteContext, id: string): Promise<boolean> {\n const document = await findOneWithDecryption(ctx.em, Document, {\n id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: { $ne: null },\n }, { fields: ['id', 'ownerUserId'] }, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })\n if (!document) return false\n if (hasDocumentsFeature(ctx.auth, 'documents.manage')) return true\n return document.ownerUserId === resolveActorUserId(ctx.auth)\n}\n\nexport async function loadScopedShare(\n ctx: DocumentsRouteContext,\n documentId: string,\n shareId: string,\n): Promise<DocumentShare> {\n const share = await findOneWithDecryption(ctx.em, DocumentShare, {\n id: shareId,\n documentId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n }, undefined, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })\n if (!share) throw new CrudHttpError(404, { error: 'documents.share.notFound' })\n return share\n}\n\nexport function serializeDocument(document: Document): Record<string, unknown> {\n return {\n id: document.id,\n title: document.title,\n folderId: document.folderId ?? null,\n ownerUserId: document.ownerUserId,\n createdByUserId: document.createdByUserId,\n isActive: document.isActive,\n createdAt: document.createdAt.toISOString(),\n updatedAt: document.updatedAt.toISOString(),\n }\n}\n\nexport function serializeContent(content: DocumentContent | null): Record<string, unknown> {\n return {\n contentHtml: content?.contentHtml ?? '',\n contentText: content?.contentText ?? '',\n updatedAt: content?.updatedAt ? content.updatedAt.toISOString() : null,\n }\n}\n\nexport function serializeFolder(folder: DocumentFolder): Record<string, unknown> {\n return {\n id: folder.id,\n name: folder.name,\n parentFolderId: folder.parentFolderId ?? null,\n ownerUserId: folder.ownerUserId,\n createdAt: folder.createdAt.toISOString(),\n updatedAt: folder.updatedAt.toISOString(),\n }\n}\n\nexport function serializeShare(share: DocumentShare): Record<string, unknown> {\n return {\n id: share.id,\n documentId: share.documentId,\n principalType: share.principalType,\n principalId: share.principalId,\n permission: share.permission,\n createdByUserId: share.createdByUserId,\n createdAt: share.createdAt.toISOString(),\n updatedAt: share.updatedAt.toISOString(),\n }\n}\n\nexport async function validateMutationGuard(\n ctx: DocumentsRouteContext,\n input: {\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n mutationPayload?: Record<string, unknown> | null\n /**\n * Re-parses a guard's `modifiedPayload` before it reaches the command. A\n * registry guard may rewrite the payload, and the route's own schema is the\n * only thing that still guarantees the command receives a shape it can trust.\n */\n mutationPayloadSchema?: { parse: (value: unknown) => Record<string, unknown> }\n },\n): Promise<MutationGuardResult> {\n // `runRouteMutationGuards` runs EVERY registered guard plus the bridged legacy\n // DI service. The deprecated `validateCrudMutationGuard` /\n // `runCrudMutationGuardAfterSuccess` pair used here before resolved only the\n // single DI-registered guard service, so every guard a module contributed\n // through the registry \u2014 along with its payload transformation and its\n // after-success callback \u2014 was silently skipped on Documents writes.\n const guardResult = await runRouteMutationGuards({\n container: ctx.container,\n req: ctx.request,\n auth: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: resolveActorUserId(ctx.auth),\n userFeatures: ctx.auth.features,\n },\n input: {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n mutationPayload: input.mutationPayload ?? null,\n },\n })\n if (!guardResult.ok) {\n throw new CrudHttpError(guardResult.errorStatus, guardResult.errorBody)\n }\n if (guardResult.modifiedPayload && input.mutationPayload) {\n const transformed = input.mutationPayloadSchema\n ? input.mutationPayloadSchema.parse(guardResult.modifiedPayload)\n : guardResult.modifiedPayload\n // Routes read the object they handed in, so the transformation has to land\n // there rather than only on the guard result.\n for (const key of Object.keys(input.mutationPayload)) delete input.mutationPayload[key]\n Object.assign(input.mutationPayload, transformed)\n }\n return guardResult\n}\n\nexport async function runMutationGuardAfterSuccess(\n _ctx: DocumentsRouteContext,\n guardResult: MutationGuardResult,\n _input: {\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n },\n): Promise<void> {\n await guardResult.runAfterSuccess()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["errors", "key", "fallback"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/documents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"@hocuspocus/server": "^4.4.0",
|
|
76
76
|
"@hocuspocus/transformer": "^4.3.0",
|
|
77
77
|
"@mikro-orm/core": "^7.1.14",
|
|
78
|
-
"@open-mercato/events": "0.
|
|
79
|
-
"@open-mercato/ui": "0.
|
|
78
|
+
"@open-mercato/events": "0.9.0",
|
|
79
|
+
"@open-mercato/ui": "0.9.0",
|
|
80
80
|
"@tiptap/core": "^3",
|
|
81
81
|
"@tiptap/extension-bubble-menu": "^3",
|
|
82
82
|
"@tiptap/extension-collaboration": "^3.27.3",
|
|
@@ -113,13 +113,13 @@
|
|
|
113
113
|
},
|
|
114
114
|
"peerDependencies": {
|
|
115
115
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
116
|
-
"@open-mercato/shared": "0.
|
|
116
|
+
"@open-mercato/shared": "0.9.0",
|
|
117
117
|
"react": "^19.0.0",
|
|
118
118
|
"react-dom": "^19.0.0"
|
|
119
119
|
},
|
|
120
120
|
"devDependencies": {
|
|
121
|
-
"@open-mercato/core": "0.
|
|
122
|
-
"@open-mercato/shared": "0.
|
|
121
|
+
"@open-mercato/core": "0.9.0",
|
|
122
|
+
"@open-mercato/shared": "0.9.0",
|
|
123
123
|
"@types/jest": "^30.0.0",
|
|
124
124
|
"@types/pg": "^8.23.1",
|
|
125
125
|
"@types/react": "^19.2.18",
|
|
@@ -144,6 +144,5 @@
|
|
|
144
144
|
"type": "git",
|
|
145
145
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
146
146
|
"directory": "packages/documents"
|
|
147
|
-
}
|
|
148
|
-
"stableVersion": "0.8.0"
|
|
147
|
+
}
|
|
149
148
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
assertDocumentNotArchived,
|
|
12
12
|
handleDocumentsRouteError,
|
|
13
13
|
hasDocumentsFeature,
|
|
14
|
+
isRemovedDocumentVisibleToActor,
|
|
14
15
|
loadScopedDocument,
|
|
15
16
|
readBody,
|
|
16
17
|
resolveActorUserId,
|
|
@@ -92,6 +93,9 @@ export async function GET(request: Request, context: RouteContext): Promise<Resp
|
|
|
92
93
|
const id = await resolveId(context)
|
|
93
94
|
const ctx = await resolveDocumentsContext(request, ['documents.view'])
|
|
94
95
|
const projection = await resolveDocumentCapabilityProjection(ctx, id)
|
|
96
|
+
if (!projection.relationshipTier && await isRemovedDocumentVisibleToActor(ctx, id)) {
|
|
97
|
+
throw new CrudHttpError(404, { error: 'documents.documents.notFound' })
|
|
98
|
+
}
|
|
95
99
|
if (!projection.relationshipTier || !projection.capabilities.canView) {
|
|
96
100
|
throw new CrudHttpError(403, { error: 'Forbidden' })
|
|
97
101
|
}
|
|
@@ -555,6 +555,23 @@ export async function loadScopedDocument(ctx: DocumentsRouteContext, id: string)
|
|
|
555
555
|
return document
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Only the owner (or a documents manager) may learn that a document was
|
|
560
|
+
* removed, e.g. after undoing its creation; everyone else keeps the
|
|
561
|
+
* existence-hiding 403 so deleted ids stay indistinguishable from foreign ones.
|
|
562
|
+
*/
|
|
563
|
+
export async function isRemovedDocumentVisibleToActor(ctx: DocumentsRouteContext, id: string): Promise<boolean> {
|
|
564
|
+
const document = await findOneWithDecryption(ctx.em, Document, {
|
|
565
|
+
id,
|
|
566
|
+
tenantId: ctx.tenantId,
|
|
567
|
+
organizationId: ctx.organizationId,
|
|
568
|
+
deletedAt: { $ne: null },
|
|
569
|
+
}, { fields: ['id', 'ownerUserId'] }, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })
|
|
570
|
+
if (!document) return false
|
|
571
|
+
if (hasDocumentsFeature(ctx.auth, 'documents.manage')) return true
|
|
572
|
+
return document.ownerUserId === resolveActorUserId(ctx.auth)
|
|
573
|
+
}
|
|
574
|
+
|
|
558
575
|
export async function loadScopedShare(
|
|
559
576
|
ctx: DocumentsRouteContext,
|
|
560
577
|
documentId: string,
|
|
@@ -519,5 +519,6 @@
|
|
|
519
519
|
"documents.versions.snapshot.labelPlaceholder": "Bezeichnung (optional)",
|
|
520
520
|
"documents.versions.snapshotTooLarge": "Diese Version ist zu groß, um sicher verarbeitet zu werden.",
|
|
521
521
|
"documents.versions.title": "Versionsverlauf",
|
|
522
|
+
"notifications.categories.documents": "Dokumente",
|
|
522
523
|
"search.entityType.documents.document": "Dokument"
|
|
523
524
|
}
|
|
@@ -519,5 +519,6 @@
|
|
|
519
519
|
"documents.versions.snapshot.labelPlaceholder": "Label (optional)",
|
|
520
520
|
"documents.versions.snapshotTooLarge": "This version is too large to process safely.",
|
|
521
521
|
"documents.versions.title": "Version history",
|
|
522
|
+
"notifications.categories.documents": "Documents",
|
|
522
523
|
"search.entityType.documents.document": "Document"
|
|
523
524
|
}
|
|
@@ -519,5 +519,6 @@
|
|
|
519
519
|
"documents.versions.snapshot.labelPlaceholder": "Etiqueta (opcional)",
|
|
520
520
|
"documents.versions.snapshotTooLarge": "Esta versión es demasiado grande para procesarla de forma segura.",
|
|
521
521
|
"documents.versions.title": "Historial de versiones",
|
|
522
|
+
"notifications.categories.documents": "Documentos",
|
|
522
523
|
"search.entityType.documents.document": "Documento"
|
|
523
524
|
}
|
|
@@ -519,5 +519,6 @@
|
|
|
519
519
|
"documents.versions.snapshot.labelPlaceholder": "라벨 (선택 사항)",
|
|
520
520
|
"documents.versions.snapshotTooLarge": "이 버전은 너무 커서 안전하게 처리할 수 없습니다.",
|
|
521
521
|
"documents.versions.title": "버전 기록",
|
|
522
|
+
"notifications.categories.documents": "문서",
|
|
522
523
|
"search.entityType.documents.document": "문서"
|
|
523
524
|
}
|
|
@@ -519,5 +519,6 @@
|
|
|
519
519
|
"documents.versions.snapshot.labelPlaceholder": "Etykieta (opcjonalnie)",
|
|
520
520
|
"documents.versions.snapshotTooLarge": "Ta wersja jest zbyt duża, aby można ją było bezpiecznie przetworzyć.",
|
|
521
521
|
"documents.versions.title": "Historia wersji",
|
|
522
|
+
"notifications.categories.documents": "Dokumenty",
|
|
522
523
|
"search.entityType.documents.document": "Dokument"
|
|
523
524
|
}
|