@open-mercato/documents 0.10.1-develop.7741.1.61a2652671 → 0.10.1-develop.7749.1.ea3edf75e3

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.
@@ -1,3 +1,5 @@
1
+ import { resolveActorUserId } from "../lib/actor.js";
2
+ import { resolveActorUserId as resolveActorUserId2 } from "../lib/actor.js";
1
3
  import { NextResponse } from "next/server";
2
4
  import { z } from "zod";
3
5
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
@@ -54,7 +56,6 @@ function withDocumentsContextErrors(doc) {
54
56
  }
55
57
  return { ...doc, methods };
56
58
  }
57
- const actorUuidSchema = z.string().uuid();
58
59
  const ROUTE_ERROR_TRANSLATIONS = {
59
60
  Unauthorized: { key: "api.errors.unauthorized", fallback: "Unauthorized" },
60
61
  Forbidden: { key: "api.errors.forbidden", fallback: "Forbidden" },
@@ -174,14 +175,6 @@ function readAuthScopeId(auth, key) {
174
175
  if (typeof value === "string" && value.trim().length > 0) return value.trim();
175
176
  return null;
176
177
  }
177
- function normalizeActorUuid(value) {
178
- if (typeof value !== "string") return null;
179
- const normalized = value.trim();
180
- return actorUuidSchema.safeParse(normalized).success ? normalized : null;
181
- }
182
- function isApiKeyAuth(auth) {
183
- return auth.isApiKey === true || auth.sub.trim().startsWith("api_key:");
184
- }
185
178
  function hasDocumentsFeature(auth, feature) {
186
179
  if (auth.isSuperAdmin === true) return true;
187
180
  return hasAllFeatures([feature], auth.features);
@@ -310,23 +303,6 @@ async function resolveDocumentsContext(request, requiredFeatures) {
310
303
  request
311
304
  };
312
305
  }
313
- function resolveActorUserId(auth) {
314
- const subject = auth.sub.trim();
315
- if (isApiKeyAuth(auth)) {
316
- const keyId = normalizeActorUuid(auth.keyId);
317
- if (!keyId || subject !== `api_key:${keyId}`) {
318
- throw new CrudHttpError(403, { error: "api.errors.forbidden" });
319
- }
320
- if (auth.userId === void 0 || auth.userId === null) return keyId;
321
- const backingUserId = normalizeActorUuid(auth.userId);
322
- if (backingUserId) return backingUserId;
323
- throw new CrudHttpError(403, { error: "api.errors.forbidden" });
324
- }
325
- const actorUserId = normalizeActorUuid(subject);
326
- const claimedUserId = auth.userId === void 0 || auth.userId === null ? actorUserId : normalizeActorUuid(auth.userId);
327
- if (actorUserId && claimedUserId === actorUserId) return actorUserId;
328
- throw new CrudHttpError(403, { error: "api.errors.forbidden" });
329
- }
330
306
  async function readBody(request, maxBytes = DOCUMENTS_JSON_BODY_LIMITS.standard) {
331
307
  return await readBoundedJsonBody(request, maxBytes, {}) ?? {};
332
308
  }
@@ -492,7 +468,7 @@ export {
492
468
  loadScopedDocument,
493
469
  loadScopedShare,
494
470
  readBody,
495
- resolveActorUserId,
471
+ resolveActorUserId2 as resolveActorUserId,
496
472
  resolveDocumentCapabilityProjection,
497
473
  resolveDocumentsContext,
498
474
  routeErrorSchema,
@@ -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\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
- "names": ["errors", "key", "fallback"]
4
+ "sourcesContent": ["import { resolveActorUserId } from '../lib/actor'\nexport { resolveActorUserId } from '../lib/actor'\nimport { 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\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\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 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
+ "names": ["resolveActorUserId", "errors", "key", "fallback"]
7
7
  }
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
+ import { resolveActorUserId } from "../lib/actor.js";
4
+ import { deriveDocumentCapabilities } from "../lib/capabilities.js";
5
+ import { loadScopedDocument, resolveActiveSubjectRoleIds, resolvePermission } from "../lib/permissions.js";
6
+ const ownerIdSchema = z.string().uuid();
7
+ const resolverId = "documents.document-attachments";
8
+ async function capabilitiesForDocument(input, documentId) {
9
+ const { subject, container, cache, signal } = input;
10
+ const { tenantId, organizationId } = subject;
11
+ if (!tenantId || !organizationId || !ownerIdSchema.safeParse(documentId).success || signal.aborted) return null;
12
+ let actorUserId;
13
+ try {
14
+ actorUserId = resolveActorUserId(subject.auth);
15
+ } catch (error) {
16
+ if (isCrudHttpError(error) && error.status === 403) return null;
17
+ throw error;
18
+ }
19
+ const principalKey = JSON.stringify([subject.auth.sub, tenantId, organizationId]);
20
+ const cacheKey = JSON.stringify(["document", principalKey, documentId]);
21
+ const cached = cache.get(cacheKey);
22
+ if (cached) return cached;
23
+ const pending = (async () => {
24
+ const em = container.resolve("em");
25
+ const document = await loadScopedDocument(em, documentId, { tenantId, organizationId });
26
+ if (!document || signal.aborted) return null;
27
+ const roleKey = JSON.stringify(["roles", principalKey]);
28
+ let roles = cache.get(roleKey);
29
+ if (!roles) {
30
+ roles = resolveActiveSubjectRoleIds(container, { tenantId, organizationId }, subject.auth.sub);
31
+ cache.set(roleKey, roles);
32
+ }
33
+ const projectedAuth = {
34
+ ...subject.auth,
35
+ userId: actorUserId,
36
+ tenantId,
37
+ orgId: organizationId,
38
+ organizationId,
39
+ resolvedRoleIds: await roles,
40
+ features: [...subject.userFeatures],
41
+ isSuperAdmin: subject.isSuperAdmin
42
+ };
43
+ if (signal.aborted) return null;
44
+ const relationshipTier = await resolvePermission(em, documentId, projectedAuth);
45
+ return deriveDocumentCapabilities({
46
+ relationshipTier,
47
+ managerOverride: subject.userFeatures.includes("documents.manage"),
48
+ archived: document.archivedAt !== null && document.archivedAt !== void 0,
49
+ userFeatures: subject.userFeatures
50
+ });
51
+ })();
52
+ cache.set(cacheKey, pending);
53
+ return pending;
54
+ }
55
+ const attachmentAccessResolvers = [{
56
+ id: resolverId,
57
+ targetPartition: "*",
58
+ targetEntity: "documents:document",
59
+ async resolve(input) {
60
+ const targets = input.targets.filter((target) => target.entityId === "documents:document");
61
+ if (targets.some((target) => !ownerIdSchema.safeParse(target.recordId).success)) {
62
+ return { ok: false, status: 404, reason: "document_not_found" };
63
+ }
64
+ const requireEvery = input.action !== "read" && input.action !== "render";
65
+ let visible = false;
66
+ for (const target of targets) {
67
+ const capabilities = await capabilitiesForDocument(input, target.recordId);
68
+ const allowed = input.action === "delete" || input.action === "reassign" ? capabilities?.canEdit === true : capabilities?.canView === true;
69
+ if (!allowed && requireEvery) return { ok: false, status: 404, reason: "document_not_found" };
70
+ if (allowed) visible = true;
71
+ }
72
+ return visible ? { ok: true } : { ok: false, status: 404, reason: "document_not_found" };
73
+ }
74
+ }];
75
+ const protectedAttachmentTargets = [{
76
+ resolverId,
77
+ targetPartition: "*",
78
+ targetEntity: "documents:document"
79
+ }];
80
+ export {
81
+ attachmentAccessResolvers,
82
+ protectedAttachmentTargets
83
+ };
84
+ //# sourceMappingURL=attachment-access.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/documents/data/attachment-access.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { AttachmentAccessInput, AttachmentAccessResolver, ProtectedAttachmentTarget } from '@open-mercato/core/modules/attachments'\nimport { resolveActorUserId } from '../lib/actor'\nimport { deriveDocumentCapabilities, type DocumentCapabilities } from '../lib/capabilities'\nimport { loadScopedDocument, resolveActiveSubjectRoleIds, resolvePermission } from '../lib/permissions'\n\nconst ownerIdSchema = z.string().uuid()\nconst resolverId = 'documents.document-attachments'\n\nasync function capabilitiesForDocument(input: AttachmentAccessInput, documentId: string): Promise<DocumentCapabilities | null> {\n const { subject, container, cache, signal } = input\n const { tenantId, organizationId } = subject\n if (!tenantId || !organizationId || !ownerIdSchema.safeParse(documentId).success || signal.aborted) return null\n let actorUserId: string\n try {\n actorUserId = resolveActorUserId(subject.auth)\n } catch (error) {\n if (isCrudHttpError(error) && error.status === 403) return null\n throw error\n }\n const principalKey = JSON.stringify([subject.auth.sub, tenantId, organizationId])\n const cacheKey = JSON.stringify(['document', principalKey, documentId])\n const cached = cache.get(cacheKey) as Promise<DocumentCapabilities | null> | undefined\n if (cached) return cached\n const pending = (async () => {\n const em = container.resolve<EntityManager>('em')\n const document = await loadScopedDocument(em, documentId, { tenantId, organizationId })\n if (!document || signal.aborted) return null\n const roleKey = JSON.stringify(['roles', principalKey])\n let roles = cache.get(roleKey) as Promise<string[]> | undefined\n if (!roles) {\n roles = resolveActiveSubjectRoleIds(container, { tenantId, organizationId }, subject.auth.sub)\n cache.set(roleKey, roles)\n }\n const projectedAuth = {\n ...subject.auth,\n userId: actorUserId,\n tenantId, orgId: organizationId, organizationId,\n resolvedRoleIds: await roles,\n features: [...subject.userFeatures],\n isSuperAdmin: subject.isSuperAdmin,\n }\n if (signal.aborted) return null\n const relationshipTier = await resolvePermission(em, documentId, projectedAuth)\n return deriveDocumentCapabilities({\n relationshipTier,\n managerOverride: subject.userFeatures.includes('documents.manage'),\n archived: document.archivedAt !== null && document.archivedAt !== undefined,\n userFeatures: subject.userFeatures,\n })\n })()\n cache.set(cacheKey, pending)\n return pending\n}\n\nexport const attachmentAccessResolvers: AttachmentAccessResolver[] = [{\n id: resolverId,\n targetPartition: '*',\n targetEntity: 'documents:document',\n async resolve(input) {\n const targets = input.targets.filter((target) => target.entityId === 'documents:document')\n if (targets.some((target) => !ownerIdSchema.safeParse(target.recordId).success)) {\n return { ok: false, status: 404, reason: 'document_not_found' }\n }\n const requireEvery = input.action !== 'read' && input.action !== 'render'\n let visible = false\n for (const target of targets) {\n const capabilities = await capabilitiesForDocument(input, target.recordId)\n const allowed = input.action === 'delete' || input.action === 'reassign'\n ? capabilities?.canEdit === true\n : capabilities?.canView === true\n if (!allowed && requireEvery) return { ok: false, status: 404, reason: 'document_not_found' }\n if (allowed) visible = true\n }\n return visible ? { ok: true } : { ok: false, status: 404, reason: 'document_not_found' }\n },\n}]\n\nexport const protectedAttachmentTargets: ProtectedAttachmentTarget[] = [{\n resolverId, targetPartition: '*', targetEntity: 'documents:document',\n}]\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
+ const actorUuidSchema = z.string().uuid();
4
+ function normalizeActorUuid(value) {
5
+ if (typeof value !== "string") return null;
6
+ const normalized = value.trim();
7
+ return actorUuidSchema.safeParse(normalized).success ? normalized : null;
8
+ }
9
+ function isApiKeyAuth(auth) {
10
+ return auth.isApiKey === true || auth.sub.trim().startsWith("api_key:");
11
+ }
12
+ function resolveActorUserId(auth) {
13
+ const subject = auth.sub.trim();
14
+ if (isApiKeyAuth(auth)) {
15
+ const keyId = normalizeActorUuid(auth.keyId);
16
+ if (!keyId || subject !== `api_key:${keyId}`) {
17
+ throw new CrudHttpError(403, { error: "api.errors.forbidden" });
18
+ }
19
+ if (auth.userId === void 0 || auth.userId === null) return keyId;
20
+ const backingUserId = normalizeActorUuid(auth.userId);
21
+ if (backingUserId) return backingUserId;
22
+ throw new CrudHttpError(403, { error: "api.errors.forbidden" });
23
+ }
24
+ const actorUserId = normalizeActorUuid(subject);
25
+ const claimedUserId = auth.userId === void 0 || auth.userId === null ? actorUserId : normalizeActorUuid(auth.userId);
26
+ if (actorUserId && claimedUserId === actorUserId) return actorUserId;
27
+ throw new CrudHttpError(403, { error: "api.errors.forbidden" });
28
+ }
29
+ export {
30
+ resolveActorUserId
31
+ };
32
+ //# sourceMappingURL=actor.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/documents/lib/actor.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\n\nconst actorUuidSchema = z.string().uuid()\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 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"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAElB,SAAS,qBAAqB;AAE9B,MAAM,kBAAkB,EAAE,OAAO,EAAE,KAAK;AAExC,SAAS,mBAAmB,OAA+B;AACzD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,aAAa,MAAM,KAAK;AAC9B,SAAO,gBAAgB,UAAU,UAAU,EAAE,UAAU,aAAa;AACtE;AAEA,SAAS,aAAa,MAAyC;AAC7D,SAAO,KAAK,aAAa,QAAQ,KAAK,IAAI,KAAK,EAAE,WAAW,UAAU;AACxE;AAEO,SAAS,mBAAmB,MAAwC;AACzE,QAAM,UAAU,KAAK,IAAI,KAAK;AAC9B,MAAI,aAAa,IAAI,GAAG;AAItB,UAAM,QAAQ,mBAAmB,KAAK,KAAK;AAC3C,QAAI,CAAC,SAAS,YAAY,WAAW,KAAK,IAAI;AAC5C,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,uBAAuB,CAAC;AAAA,IAChE;AACA,QAAI,KAAK,WAAW,UAAa,KAAK,WAAW,KAAM,QAAO;AAC9D,UAAM,gBAAgB,mBAAmB,KAAK,MAAM;AACpD,QAAI,cAAe,QAAO;AAC1B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,uBAAuB,CAAC;AAAA,EAChE;AAEA,QAAM,cAAc,mBAAmB,OAAO;AAC9C,QAAM,gBAAgB,KAAK,WAAW,UAAa,KAAK,WAAW,OAC/D,cACA,mBAAmB,KAAK,MAAM;AAClC,MAAI,eAAe,kBAAkB,YAAa,QAAO;AACzD,QAAM,IAAI,cAAc,KAAK,EAAE,OAAO,uBAAuB,CAAC;AAChE;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/documents",
3
- "version": "0.10.1-develop.7741.1.61a2652671",
3
+ "version": "0.10.1-develop.7749.1.ea3edf75e3",
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.10.1-develop.7741.1.61a2652671",
79
- "@open-mercato/ui": "0.10.1-develop.7741.1.61a2652671",
78
+ "@open-mercato/events": "0.10.1-develop.7749.1.ea3edf75e3",
79
+ "@open-mercato/ui": "0.10.1-develop.7749.1.ea3edf75e3",
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.10.1-develop.7741.1.61a2652671",
116
+ "@open-mercato/shared": "0.10.1-develop.7749.1.ea3edf75e3",
117
117
  "react": "^19.0.0",
118
118
  "react-dom": "^19.0.0"
119
119
  },
120
120
  "devDependencies": {
121
- "@open-mercato/core": "0.10.1-develop.7741.1.61a2652671",
122
- "@open-mercato/shared": "0.10.1-develop.7741.1.61a2652671",
121
+ "@open-mercato/core": "0.10.1-develop.7749.1.ea3edf75e3",
122
+ "@open-mercato/shared": "0.10.1-develop.7749.1.ea3edf75e3",
123
123
  "@types/jest": "^30.0.0",
124
124
  "@types/pg": "^8.23.1",
125
125
  "@types/react": "^19.2.18",
@@ -1,3 +1,5 @@
1
+ import { resolveActorUserId } from '../lib/actor'
2
+ export { resolveActorUserId } from '../lib/actor'
1
3
  import { NextResponse } from 'next/server'
2
4
  import type { EntityManager } from '@mikro-orm/postgresql'
3
5
  import type { AwilixContainer } from 'awilix'
@@ -138,8 +140,6 @@ export function withDocumentsContextErrors(doc: OpenApiRouteDoc): OpenApiRouteDo
138
140
  return { ...doc, methods }
139
141
  }
140
142
 
141
- const actorUuidSchema = z.string().uuid()
142
-
143
143
  export const ROUTE_ERROR_TRANSLATIONS: Record<string, { key: string; fallback: string }> = {
144
144
  Unauthorized: { key: 'api.errors.unauthorized', fallback: 'Unauthorized' },
145
145
  Forbidden: { key: 'api.errors.forbidden', fallback: 'Forbidden' },
@@ -274,16 +274,6 @@ function readAuthScopeId(auth: NonNullable<AuthContext>, key: 'actorTenantId' |
274
274
  return null
275
275
  }
276
276
 
277
- function normalizeActorUuid(value: unknown): string | null {
278
- if (typeof value !== 'string') return null
279
- const normalized = value.trim()
280
- return actorUuidSchema.safeParse(normalized).success ? normalized : null
281
- }
282
-
283
- function isApiKeyAuth(auth: NonNullable<AuthContext>): boolean {
284
- return auth.isApiKey === true || auth.sub.trim().startsWith('api_key:')
285
- }
286
-
287
277
  export function hasDocumentsFeature(auth: DocumentsAuthContext, feature: string): boolean {
288
278
  if (auth.isSuperAdmin === true) return true
289
279
  return hasAllFeatures([feature], auth.features)
@@ -455,30 +445,6 @@ export async function resolveDocumentsContext(
455
445
  }
456
446
  }
457
447
 
458
- export function resolveActorUserId(auth: NonNullable<AuthContext>): string {
459
- const subject = auth.sub.trim()
460
- if (isApiKeyAuth(auth)) {
461
- // `keyId` is populated only by successful API-key authentication. Do not
462
- // recover an actor by parsing an arbitrary prefixed subject when that
463
- // validated field is absent, malformed, or disagrees with the subject.
464
- const keyId = normalizeActorUuid(auth.keyId)
465
- if (!keyId || subject !== `api_key:${keyId}`) {
466
- throw new CrudHttpError(403, { error: 'api.errors.forbidden' })
467
- }
468
- if (auth.userId === undefined || auth.userId === null) return keyId
469
- const backingUserId = normalizeActorUuid(auth.userId)
470
- if (backingUserId) return backingUserId
471
- throw new CrudHttpError(403, { error: 'api.errors.forbidden' })
472
- }
473
-
474
- const actorUserId = normalizeActorUuid(subject)
475
- const claimedUserId = auth.userId === undefined || auth.userId === null
476
- ? actorUserId
477
- : normalizeActorUuid(auth.userId)
478
- if (actorUserId && claimedUserId === actorUserId) return actorUserId
479
- throw new CrudHttpError(403, { error: 'api.errors.forbidden' })
480
- }
481
-
482
448
  export async function readBody(
483
449
  request: Request,
484
450
  maxBytes = DOCUMENTS_JSON_BODY_LIMITS.standard,
@@ -0,0 +1,83 @@
1
+ import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
4
+ import type { AttachmentAccessInput, AttachmentAccessResolver, ProtectedAttachmentTarget } from '@open-mercato/core/modules/attachments'
5
+ import { resolveActorUserId } from '../lib/actor'
6
+ import { deriveDocumentCapabilities, type DocumentCapabilities } from '../lib/capabilities'
7
+ import { loadScopedDocument, resolveActiveSubjectRoleIds, resolvePermission } from '../lib/permissions'
8
+
9
+ const ownerIdSchema = z.string().uuid()
10
+ const resolverId = 'documents.document-attachments'
11
+
12
+ async function capabilitiesForDocument(input: AttachmentAccessInput, documentId: string): Promise<DocumentCapabilities | null> {
13
+ const { subject, container, cache, signal } = input
14
+ const { tenantId, organizationId } = subject
15
+ if (!tenantId || !organizationId || !ownerIdSchema.safeParse(documentId).success || signal.aborted) return null
16
+ let actorUserId: string
17
+ try {
18
+ actorUserId = resolveActorUserId(subject.auth)
19
+ } catch (error) {
20
+ if (isCrudHttpError(error) && error.status === 403) return null
21
+ throw error
22
+ }
23
+ const principalKey = JSON.stringify([subject.auth.sub, tenantId, organizationId])
24
+ const cacheKey = JSON.stringify(['document', principalKey, documentId])
25
+ const cached = cache.get(cacheKey) as Promise<DocumentCapabilities | null> | undefined
26
+ if (cached) return cached
27
+ const pending = (async () => {
28
+ const em = container.resolve<EntityManager>('em')
29
+ const document = await loadScopedDocument(em, documentId, { tenantId, organizationId })
30
+ if (!document || signal.aborted) return null
31
+ const roleKey = JSON.stringify(['roles', principalKey])
32
+ let roles = cache.get(roleKey) as Promise<string[]> | undefined
33
+ if (!roles) {
34
+ roles = resolveActiveSubjectRoleIds(container, { tenantId, organizationId }, subject.auth.sub)
35
+ cache.set(roleKey, roles)
36
+ }
37
+ const projectedAuth = {
38
+ ...subject.auth,
39
+ userId: actorUserId,
40
+ tenantId, orgId: organizationId, organizationId,
41
+ resolvedRoleIds: await roles,
42
+ features: [...subject.userFeatures],
43
+ isSuperAdmin: subject.isSuperAdmin,
44
+ }
45
+ if (signal.aborted) return null
46
+ const relationshipTier = await resolvePermission(em, documentId, projectedAuth)
47
+ return deriveDocumentCapabilities({
48
+ relationshipTier,
49
+ managerOverride: subject.userFeatures.includes('documents.manage'),
50
+ archived: document.archivedAt !== null && document.archivedAt !== undefined,
51
+ userFeatures: subject.userFeatures,
52
+ })
53
+ })()
54
+ cache.set(cacheKey, pending)
55
+ return pending
56
+ }
57
+
58
+ export const attachmentAccessResolvers: AttachmentAccessResolver[] = [{
59
+ id: resolverId,
60
+ targetPartition: '*',
61
+ targetEntity: 'documents:document',
62
+ async resolve(input) {
63
+ const targets = input.targets.filter((target) => target.entityId === 'documents:document')
64
+ if (targets.some((target) => !ownerIdSchema.safeParse(target.recordId).success)) {
65
+ return { ok: false, status: 404, reason: 'document_not_found' }
66
+ }
67
+ const requireEvery = input.action !== 'read' && input.action !== 'render'
68
+ let visible = false
69
+ for (const target of targets) {
70
+ const capabilities = await capabilitiesForDocument(input, target.recordId)
71
+ const allowed = input.action === 'delete' || input.action === 'reassign'
72
+ ? capabilities?.canEdit === true
73
+ : capabilities?.canView === true
74
+ if (!allowed && requireEvery) return { ok: false, status: 404, reason: 'document_not_found' }
75
+ if (allowed) visible = true
76
+ }
77
+ return visible ? { ok: true } : { ok: false, status: 404, reason: 'document_not_found' }
78
+ },
79
+ }]
80
+
81
+ export const protectedAttachmentTargets: ProtectedAttachmentTarget[] = [{
82
+ resolverId, targetPartition: '*', targetEntity: 'documents:document',
83
+ }]
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod'
2
+ import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
3
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
4
+
5
+ const actorUuidSchema = z.string().uuid()
6
+
7
+ function normalizeActorUuid(value: unknown): string | null {
8
+ if (typeof value !== 'string') return null
9
+ const normalized = value.trim()
10
+ return actorUuidSchema.safeParse(normalized).success ? normalized : null
11
+ }
12
+
13
+ function isApiKeyAuth(auth: NonNullable<AuthContext>): boolean {
14
+ return auth.isApiKey === true || auth.sub.trim().startsWith('api_key:')
15
+ }
16
+
17
+ export function resolveActorUserId(auth: NonNullable<AuthContext>): string {
18
+ const subject = auth.sub.trim()
19
+ if (isApiKeyAuth(auth)) {
20
+ // `keyId` is populated only by successful API-key authentication. Do not
21
+ // recover an actor by parsing an arbitrary prefixed subject when that
22
+ // validated field is absent, malformed, or disagrees with the subject.
23
+ const keyId = normalizeActorUuid(auth.keyId)
24
+ if (!keyId || subject !== `api_key:${keyId}`) {
25
+ throw new CrudHttpError(403, { error: 'api.errors.forbidden' })
26
+ }
27
+ if (auth.userId === undefined || auth.userId === null) return keyId
28
+ const backingUserId = normalizeActorUuid(auth.userId)
29
+ if (backingUserId) return backingUserId
30
+ throw new CrudHttpError(403, { error: 'api.errors.forbidden' })
31
+ }
32
+
33
+ const actorUserId = normalizeActorUuid(subject)
34
+ const claimedUserId = auth.userId === undefined || auth.userId === null
35
+ ? actorUserId
36
+ : normalizeActorUuid(auth.userId)
37
+ if (actorUserId && claimedUserId === actorUserId) return actorUserId
38
+ throw new CrudHttpError(403, { error: 'api.errors.forbidden' })
39
+ }