@open-mercato/core 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.
Files changed (159) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/attachment_partition/index.js +2 -0
  3. package/dist/generated/entities/attachment_partition/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
  7. package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
  8. package/dist/modules/attachments/api/file/[id]/route.js +16 -10
  9. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  10. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
  11. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  12. package/dist/modules/attachments/api/library/[id]/route.js +100 -56
  13. package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
  14. package/dist/modules/attachments/api/library/route.js +56 -17
  15. package/dist/modules/attachments/api/library/route.js.map +2 -2
  16. package/dist/modules/attachments/api/partitions/route.js +2 -0
  17. package/dist/modules/attachments/api/partitions/route.js.map +2 -2
  18. package/dist/modules/attachments/api/route.js +74 -34
  19. package/dist/modules/attachments/api/route.js.map +3 -3
  20. package/dist/modules/attachments/api/transfer/route.js +63 -37
  21. package/dist/modules/attachments/api/transfer/route.js.map +2 -2
  22. package/dist/modules/attachments/data/entities.js +3 -0
  23. package/dist/modules/attachments/data/entities.js.map +2 -2
  24. package/dist/modules/attachments/data/validators.js +41 -0
  25. package/dist/modules/attachments/data/validators.js.map +7 -0
  26. package/dist/modules/attachments/di.js +5 -2
  27. package/dist/modules/attachments/di.js.map +2 -2
  28. package/dist/modules/attachments/generators.js +29 -0
  29. package/dist/modules/attachments/generators.js.map +7 -0
  30. package/dist/modules/attachments/index.js +8 -1
  31. package/dist/modules/attachments/index.js.map +2 -2
  32. package/dist/modules/attachments/lib/access-errors.js +35 -0
  33. package/dist/modules/attachments/lib/access-errors.js.map +7 -0
  34. package/dist/modules/attachments/lib/access-mutation.js +23 -0
  35. package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
  36. package/dist/modules/attachments/lib/access-protection.js +73 -0
  37. package/dist/modules/attachments/lib/access-protection.js.map +7 -0
  38. package/dist/modules/attachments/lib/access-query.js +74 -0
  39. package/dist/modules/attachments/lib/access-query.js.map +7 -0
  40. package/dist/modules/attachments/lib/access-registry.js +87 -0
  41. package/dist/modules/attachments/lib/access-registry.js.map +7 -0
  42. package/dist/modules/attachments/lib/access-runner.js +161 -0
  43. package/dist/modules/attachments/lib/access-runner.js.map +7 -0
  44. package/dist/modules/attachments/lib/access-types.js +1 -0
  45. package/dist/modules/attachments/lib/access-types.js.map +7 -0
  46. package/dist/modules/attachments/lib/attachment-service.js +14 -6
  47. package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
  48. package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
  49. package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
  50. package/dist/modules/attachments/lib/partitions.js +3 -2
  51. package/dist/modules/attachments/lib/partitions.js.map +2 -2
  52. package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
  53. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  54. package/dist/modules/attachments/lib/target-access-service.js +12 -3
  55. package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
  56. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
  57. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
  58. package/dist/modules/attachments/setup.js +10 -0
  59. package/dist/modules/attachments/setup.js.map +2 -2
  60. package/dist/modules/notifications/api/[id]/action/route.js +8 -0
  61. package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
  62. package/dist/modules/notifications/api/route.js +17 -0
  63. package/dist/modules/notifications/api/route.js.map +2 -2
  64. package/dist/modules/notifications/data/validators.js +1 -1
  65. package/dist/modules/notifications/data/validators.js.map +2 -2
  66. package/dist/modules/notifications/lib/notificationService.js +45 -3
  67. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  68. package/dist/modules/notifications/lib/routeHelpers.js +27 -0
  69. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  70. package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
  71. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  72. package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
  73. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  74. package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
  75. package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
  76. package/dist/modules/staff/notifications.js +4 -2
  77. package/dist/modules/staff/notifications.js.map +2 -2
  78. package/dist/modules/workflows/data/entities.js.map +2 -2
  79. package/dist/modules/workflows/lib/activity-executor.js +18 -4
  80. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  81. package/dist/modules/workflows/lib/activity-types.js +2 -2
  82. package/dist/modules/workflows/lib/activity-types.js.map +2 -2
  83. package/dist/modules/workflows/lib/condition-handler.js +7 -1
  84. package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
  85. package/dist/modules/workflows/lib/duration.js +13 -0
  86. package/dist/modules/workflows/lib/duration.js.map +2 -2
  87. package/dist/modules/workflows/lib/step-handler.js +9 -2
  88. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  89. package/dist/modules/workflows/lib/task-quick-action.js +13 -10
  90. package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
  91. package/dist/modules/workflows/lib/transition-handler.js +2 -1
  92. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  93. package/dist/modules/workflows/notifications.js +4 -0
  94. package/dist/modules/workflows/notifications.js.map +2 -2
  95. package/generated/entities/attachment_partition/index.ts +1 -0
  96. package/generated/entity-fields-registry.ts +1 -0
  97. package/package.json +7 -7
  98. package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
  99. package/src/modules/attachments/api/file/[id]/route.ts +12 -9
  100. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
  101. package/src/modules/attachments/api/library/[id]/route.ts +87 -58
  102. package/src/modules/attachments/api/library/route.ts +40 -11
  103. package/src/modules/attachments/api/partitions/route.ts +2 -0
  104. package/src/modules/attachments/api/route.ts +68 -35
  105. package/src/modules/attachments/api/transfer/route.ts +65 -42
  106. package/src/modules/attachments/data/entities.ts +4 -0
  107. package/src/modules/attachments/data/validators.ts +36 -0
  108. package/src/modules/attachments/di.ts +4 -1
  109. package/src/modules/attachments/generators.ts +27 -0
  110. package/src/modules/attachments/i18n/de.json +4 -0
  111. package/src/modules/attachments/i18n/en.json +4 -0
  112. package/src/modules/attachments/i18n/es.json +4 -0
  113. package/src/modules/attachments/i18n/ko.json +4 -0
  114. package/src/modules/attachments/i18n/pl.json +4 -0
  115. package/src/modules/attachments/index.ts +18 -0
  116. package/src/modules/attachments/lib/access-errors.ts +32 -0
  117. package/src/modules/attachments/lib/access-mutation.ts +26 -0
  118. package/src/modules/attachments/lib/access-protection.ts +83 -0
  119. package/src/modules/attachments/lib/access-query.ts +86 -0
  120. package/src/modules/attachments/lib/access-registry.ts +117 -0
  121. package/src/modules/attachments/lib/access-runner.ts +183 -0
  122. package/src/modules/attachments/lib/access-types.ts +78 -0
  123. package/src/modules/attachments/lib/attachment-service.ts +8 -4
  124. package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
  125. package/src/modules/attachments/lib/partitions.ts +3 -2
  126. package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
  127. package/src/modules/attachments/lib/target-access-service.ts +10 -3
  128. package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
  129. package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
  130. package/src/modules/attachments/setup.ts +10 -0
  131. package/src/modules/auth/i18n/de.json +3 -0
  132. package/src/modules/auth/i18n/en.json +3 -0
  133. package/src/modules/auth/i18n/es.json +3 -0
  134. package/src/modules/auth/i18n/ko.json +3 -0
  135. package/src/modules/auth/i18n/pl.json +3 -0
  136. package/src/modules/notifications/api/[id]/action/route.ts +8 -0
  137. package/src/modules/notifications/api/route.ts +17 -0
  138. package/src/modules/notifications/data/validators.ts +1 -1
  139. package/src/modules/notifications/i18n/de.json +2 -0
  140. package/src/modules/notifications/i18n/en.json +2 -0
  141. package/src/modules/notifications/i18n/es.json +2 -0
  142. package/src/modules/notifications/i18n/ko.json +2 -0
  143. package/src/modules/notifications/i18n/pl.json +2 -0
  144. package/src/modules/notifications/lib/notificationService.ts +67 -1
  145. package/src/modules/notifications/lib/routeHelpers.ts +38 -0
  146. package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
  147. package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
  148. package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
  149. package/src/modules/staff/notifications.ts +2 -0
  150. package/src/modules/workflows/AGENTS.md +2 -2
  151. package/src/modules/workflows/data/entities.ts +6 -0
  152. package/src/modules/workflows/lib/activity-executor.ts +36 -4
  153. package/src/modules/workflows/lib/activity-types.ts +2 -2
  154. package/src/modules/workflows/lib/condition-handler.ts +16 -1
  155. package/src/modules/workflows/lib/duration.ts +25 -0
  156. package/src/modules/workflows/lib/step-handler.ts +16 -1
  157. package/src/modules/workflows/lib/task-quick-action.ts +19 -10
  158. package/src/modules/workflows/lib/transition-handler.ts +2 -1
  159. package/src/modules/workflows/notifications.ts +4 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/attachments/lib/access-protection.ts"],
4
+ "sourcesContent": ["import { LockMode } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { AttachmentPartition } from '../data/entities'\nimport { attachmentAccessRequirementsSchema } from '../data/validators'\nimport { getAttachmentAccessRegistry, matchesAttachmentAccessSelector } from './access-registry'\nimport type { AttachmentAccessRequirement } from './access-types'\n\nexport function getDeclaredAttachmentAccessRequirements(partitionCode: string): AttachmentAccessRequirement[] {\n const registry = getAttachmentAccessRegistry()\n if (registry.invalid) throw new Error('[internal] Attachment access registry is invalid')\n return registry.protectedTargets\n .filter((target) => matchesAttachmentAccessSelector(target.targetPartition, partitionCode))\n .map(({ resolverId, targetEntity }) => ({ resolverId, targetEntity }))\n}\n\nexport function mergeAttachmentAccessRequirements(\n current: unknown,\n required: readonly AttachmentAccessRequirement[],\n): AttachmentAccessRequirement[] {\n const existing = attachmentAccessRequirementsSchema.parse(current ?? [])\n const union = new Map<string, AttachmentAccessRequirement>()\n for (const requirement of [...existing, ...required]) {\n union.set(JSON.stringify([requirement.resolverId, requirement.targetEntity]), { ...requirement })\n }\n return [...union.values()]\n}\n\nexport async function syncAttachmentAccessProtection(\n em: EntityManager,\n options: { partitionCode?: string } = {},\n): Promise<number> {\n const registry = getAttachmentAccessRegistry()\n if (registry.invalid) throw new Error('[internal] Attachment access registry is invalid')\n if (!registry.protectedTargets.length) return 0\n const isolated = em.fork({ clear: true, useContext: false })\n const candidates = await findWithDecryption(\n isolated,\n AttachmentPartition,\n options.partitionCode ? { code: options.partitionCode } : {},\n { orderBy: { id: 'asc' } },\n { tenantId: null, organizationId: null },\n )\n if (options.partitionCode && getDeclaredAttachmentAccessRequirements(options.partitionCode).length && !candidates.length) {\n throw new Error('[internal] Protected attachment partition is not durably configured')\n }\n const pending = candidates.filter((partition) => {\n const required = getDeclaredAttachmentAccessRequirements(partition.code)\n if (!required.length) return false\n const existing = attachmentAccessRequirementsSchema.parse(partition.accessResolverRequirements ?? [])\n return JSON.stringify(mergeAttachmentAccessRequirements(existing, required)) !== JSON.stringify(existing)\n })\n if (!pending.length) return 0\n return isolated.transactional(async (transaction) => {\n let changed = 0\n for (const candidate of pending) {\n const partition = await findOneWithDecryption(transaction, AttachmentPartition, { id: candidate.id }, {\n lockMode: LockMode.PESSIMISTIC_WRITE,\n refresh: true,\n }, { tenantId: null, organizationId: null })\n if (!partition) throw new Error('[internal] Attachment partition disappeared during access protection synchronization')\n const existing = attachmentAccessRequirementsSchema.parse(partition.accessResolverRequirements ?? [])\n const merged = mergeAttachmentAccessRequirements(existing, getDeclaredAttachmentAccessRequirements(partition.code))\n if (JSON.stringify(merged) === JSON.stringify(existing)) continue\n partition.accessResolverRequirements = merged\n changed += 1\n }\n if (changed) await transaction.flush()\n return changed\n })\n}\n\nexport async function ensureAttachmentPartitionProtection(\n em: EntityManager,\n partition: AttachmentPartition,\n): Promise<void> {\n const required = getDeclaredAttachmentAccessRequirements(partition.code)\n if (!required.length) return\n const existing = attachmentAccessRequirementsSchema.parse(partition.accessResolverRequirements ?? [])\n const merged = mergeAttachmentAccessRequirements(existing, required)\n if (JSON.stringify(existing) === JSON.stringify(merged)) return\n await syncAttachmentAccessProtection(em, { partitionCode: partition.code })\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,74 @@
1
+ import { ensureAttachmentPartitionProtection } from "./access-protection.js";
2
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { AttachmentPartition } from "../data/entities.js";
4
+ import { attachmentAccessRequirementsSchema } from "../data/validators.js";
5
+ import { getAttachmentAccessRegistry } from "./access-registry.js";
6
+ import { evaluateAttachmentAccess } from "./access-runner.js";
7
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
8
+ import { attachmentAccessErrorBody, throwAttachmentAccessError } from "./access-errors.js";
9
+ import { readAttachmentMetadata } from "./metadata.js";
10
+ function requiresAttachmentAccessScan(partitions) {
11
+ const registry = getAttachmentAccessRegistry();
12
+ return registry.invalid || registry.resolvers.length > 0 || registry.protectedTargets.length > 0 || partitions.some((partition) => {
13
+ const parsed = attachmentAccessRequirementsSchema.safeParse(partition.accessResolverRequirements ?? []);
14
+ return !parsed.success || parsed.data.length > 0;
15
+ });
16
+ }
17
+ async function assertAttachmentOwnerAccess(input) {
18
+ const partition = await findOneWithDecryption(input.em, AttachmentPartition, { code: input.attachment.partitionCode }, void 0, {
19
+ tenantId: input.attachment.tenantId,
20
+ organizationId: input.attachment.organizationId
21
+ });
22
+ if (!partition) await throwAttachmentAccessError(404);
23
+ if (input.persistProtection) await ensureAttachmentPartitionProtection(input.em, partition);
24
+ const result = await evaluateAttachmentAccess({ ...input, partition, requireAuthForPublic: true });
25
+ if (!result.ok) await throwAttachmentAccessError(result.status);
26
+ }
27
+ async function scanAuthorizedAttachments(input) {
28
+ const deadline = input.deadline ?? Date.now() + 15e3;
29
+ const timeoutBody = await attachmentAccessErrorBody(504);
30
+ let timer;
31
+ const scan = async () => {
32
+ const partitions = new Map(input.partitions.map((partition) => [partition.code, partition]));
33
+ const records = [];
34
+ const tags = /* @__PURE__ */ new Set();
35
+ let total = 0;
36
+ for (let batchOffset = 0; ; batchOffset += 100) {
37
+ if (Date.now() >= deadline) await throwAttachmentAccessError(504);
38
+ const batch = await input.loadBatch(batchOffset, 100);
39
+ for (const attachment of batch) {
40
+ if (Date.now() >= deadline) await throwAttachmentAccessError(504);
41
+ const partition = partitions.get(attachment.partitionCode);
42
+ if (!partition) continue;
43
+ const access = await evaluateAttachmentAccess({
44
+ auth: input.auth,
45
+ attachment,
46
+ partition,
47
+ action: "metadata",
48
+ context: input.context,
49
+ requireAuthForPublic: true
50
+ });
51
+ if (Date.now() >= deadline || !access.ok && access.status === 504) await throwAttachmentAccessError(504);
52
+ if (!access.ok) continue;
53
+ if (input.collectTags) for (const tag of readAttachmentMetadata(attachment.storageMetadata).tags ?? []) tags.add(tag);
54
+ if (total >= (input.offset ?? 0) && (input.limit === void 0 || records.length < input.limit)) records.push(attachment);
55
+ total += 1;
56
+ }
57
+ if (batch.length < 100) break;
58
+ }
59
+ return { records, total, tags: [...tags].sort((left, right) => left < right ? -1 : left > right ? 1 : 0) };
60
+ };
61
+ try {
62
+ return await Promise.race([scan(), new Promise((_resolve, reject) => {
63
+ timer = setTimeout(() => reject(new CrudHttpError(504, timeoutBody)), Math.max(1, deadline - Date.now()));
64
+ })]);
65
+ } finally {
66
+ if (timer) clearTimeout(timer);
67
+ }
68
+ }
69
+ export {
70
+ assertAttachmentOwnerAccess,
71
+ requiresAttachmentAccessScan,
72
+ scanAuthorizedAttachments
73
+ };
74
+ //# sourceMappingURL=access-query.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/attachments/lib/access-query.ts"],
4
+ "sourcesContent": ["import { ensureAttachmentPartitionProtection } from './access-protection'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { AttachmentPartition, type Attachment } from '../data/entities'\nimport { attachmentAccessRequirementsSchema } from '../data/validators'\nimport { getAttachmentAccessRegistry } from './access-registry'\nimport { evaluateAttachmentAccess, type AttachmentAccessContext } from './access-runner'\nimport type { AttachmentAccessAction, AttachmentAccessRecord } from './access-types'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { attachmentAccessErrorBody, throwAttachmentAccessError } from './access-errors'\nimport { readAttachmentMetadata } from './metadata'\n\nexport function requiresAttachmentAccessScan(partitions: readonly AttachmentPartition[]): boolean {\n const registry = getAttachmentAccessRegistry()\n return registry.invalid || registry.resolvers.length > 0 || registry.protectedTargets.length > 0\n || partitions.some((partition) => {\n const parsed = attachmentAccessRequirementsSchema.safeParse(partition.accessResolverRequirements ?? [])\n return !parsed.success || parsed.data.length > 0\n })\n}\n\nexport async function assertAttachmentOwnerAccess(input: {\n em: EntityManager\n context: AttachmentAccessContext\n auth: NonNullable<AuthContext>\n attachment: AttachmentAccessRecord\n action: AttachmentAccessAction\n persistProtection?: boolean\n}): Promise<void> {\n const partition = await findOneWithDecryption(input.em, AttachmentPartition, { code: input.attachment.partitionCode }, undefined, {\n tenantId: input.attachment.tenantId, organizationId: input.attachment.organizationId,\n })\n if (!partition) await throwAttachmentAccessError(404)\n if (input.persistProtection) await ensureAttachmentPartitionProtection(input.em, partition!)\n const result = await evaluateAttachmentAccess({ ...input, partition: partition!, requireAuthForPublic: true })\n if (!result.ok) await throwAttachmentAccessError(result.status)\n}\n\nexport async function scanAuthorizedAttachments(input: {\n auth: NonNullable<AuthContext>\n context: AttachmentAccessContext\n partitions: readonly AttachmentPartition[]\n loadBatch(offset: number, limit: number): Promise<Attachment[]>\n offset?: number\n limit?: number\n collectTags?: boolean\n deadline?: number\n}): Promise<{ records: Attachment[]; total: number; tags: string[] }> {\n const deadline = input.deadline ?? Date.now() + 15_000\n const timeoutBody = await attachmentAccessErrorBody(504)\n let timer: ReturnType<typeof setTimeout> | undefined\n const scan = async () => {\n const partitions = new Map(input.partitions.map((partition) => [partition.code, partition]))\n const records: Attachment[] = []\n const tags = new Set<string>()\n let total = 0\n for (let batchOffset = 0; ; batchOffset += 100) {\n if (Date.now() >= deadline) await throwAttachmentAccessError(504)\n const batch = await input.loadBatch(batchOffset, 100)\n for (const attachment of batch) {\n if (Date.now() >= deadline) await throwAttachmentAccessError(504)\n const partition = partitions.get(attachment.partitionCode)\n if (!partition) continue\n const access = await evaluateAttachmentAccess({\n auth: input.auth, attachment, partition, action: 'metadata', context: input.context,\n requireAuthForPublic: true,\n })\n if (Date.now() >= deadline || (!access.ok && access.status === 504)) await throwAttachmentAccessError(504)\n if (!access.ok) continue\n if (input.collectTags) for (const tag of readAttachmentMetadata(attachment.storageMetadata).tags ?? []) tags.add(tag)\n if (total >= (input.offset ?? 0) && (input.limit === undefined || records.length < input.limit)) records.push(attachment)\n total += 1\n }\n if (batch.length < 100) break\n }\n return { records, total, tags: [...tags].sort((left, right) => left < right ? -1 : left > right ? 1 : 0) }\n }\n try {\n return await Promise.race([scan(), new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => reject(new CrudHttpError(504, timeoutBody)), Math.max(1, deadline - Date.now()))\n })])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,2CAA2C;AAGpD,SAAS,6BAA6B;AACtC,SAAS,2BAA4C;AACrD,SAAS,0CAA0C;AACnD,SAAS,mCAAmC;AAC5C,SAAS,gCAA8D;AAEvE,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B,kCAAkC;AACtE,SAAS,8BAA8B;AAEhC,SAAS,6BAA6B,YAAqD;AAChG,QAAM,WAAW,4BAA4B;AAC7C,SAAO,SAAS,WAAW,SAAS,UAAU,SAAS,KAAK,SAAS,iBAAiB,SAAS,KAC1F,WAAW,KAAK,CAAC,cAAc;AAChC,UAAM,SAAS,mCAAmC,UAAU,UAAU,8BAA8B,CAAC,CAAC;AACtG,WAAO,CAAC,OAAO,WAAW,OAAO,KAAK,SAAS;AAAA,EACjD,CAAC;AACL;AAEA,eAAsB,4BAA4B,OAOhC;AAChB,QAAM,YAAY,MAAM,sBAAsB,MAAM,IAAI,qBAAqB,EAAE,MAAM,MAAM,WAAW,cAAc,GAAG,QAAW;AAAA,IAChI,UAAU,MAAM,WAAW;AAAA,IAAU,gBAAgB,MAAM,WAAW;AAAA,EACxE,CAAC;AACD,MAAI,CAAC,UAAW,OAAM,2BAA2B,GAAG;AACpD,MAAI,MAAM,kBAAmB,OAAM,oCAAoC,MAAM,IAAI,SAAU;AAC3F,QAAM,SAAS,MAAM,yBAAyB,EAAE,GAAG,OAAO,WAAuB,sBAAsB,KAAK,CAAC;AAC7G,MAAI,CAAC,OAAO,GAAI,OAAM,2BAA2B,OAAO,MAAM;AAChE;AAEA,eAAsB,0BAA0B,OASsB;AACpE,QAAM,WAAW,MAAM,YAAY,KAAK,IAAI,IAAI;AAChD,QAAM,cAAc,MAAM,0BAA0B,GAAG;AACvD,MAAI;AACJ,QAAM,OAAO,YAAY;AACvB,UAAM,aAAa,IAAI,IAAI,MAAM,WAAW,IAAI,CAAC,cAAc,CAAC,UAAU,MAAM,SAAS,CAAC,CAAC;AAC3F,UAAM,UAAwB,CAAC;AAC/B,UAAM,OAAO,oBAAI,IAAY;AAC7B,QAAI,QAAQ;AACZ,aAAS,cAAc,KAAK,eAAe,KAAK;AAC9C,UAAI,KAAK,IAAI,KAAK,SAAU,OAAM,2BAA2B,GAAG;AAChE,YAAM,QAAQ,MAAM,MAAM,UAAU,aAAa,GAAG;AACpD,iBAAW,cAAc,OAAO;AAC9B,YAAI,KAAK,IAAI,KAAK,SAAU,OAAM,2BAA2B,GAAG;AAChE,cAAM,YAAY,WAAW,IAAI,WAAW,aAAa;AACzD,YAAI,CAAC,UAAW;AAChB,cAAM,SAAS,MAAM,yBAAyB;AAAA,UAC5C,MAAM,MAAM;AAAA,UAAM;AAAA,UAAY;AAAA,UAAW,QAAQ;AAAA,UAAY,SAAS,MAAM;AAAA,UAC5E,sBAAsB;AAAA,QACxB,CAAC;AACD,YAAI,KAAK,IAAI,KAAK,YAAa,CAAC,OAAO,MAAM,OAAO,WAAW,IAAM,OAAM,2BAA2B,GAAG;AACzG,YAAI,CAAC,OAAO,GAAI;AAChB,YAAI,MAAM,YAAa,YAAW,OAAO,uBAAuB,WAAW,eAAe,EAAE,QAAQ,CAAC,EAAG,MAAK,IAAI,GAAG;AACpH,YAAI,UAAU,MAAM,UAAU,OAAO,MAAM,UAAU,UAAa,QAAQ,SAAS,MAAM,OAAQ,SAAQ,KAAK,UAAU;AACxH,iBAAS;AAAA,MACX;AACA,UAAI,MAAM,SAAS,IAAK;AAAA,IAC1B;AACA,WAAO,EAAE,SAAS,OAAO,MAAM,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAC,EAAE;AAAA,EAC3G;AACA,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK,CAAC,KAAK,GAAG,IAAI,QAAe,CAAC,UAAU,WAAW;AAC1E,cAAQ,WAAW,MAAM,OAAO,IAAI,cAAc,KAAK,WAAW,CAAC,GAAG,KAAK,IAAI,GAAG,WAAW,KAAK,IAAI,CAAC,CAAC;AAAA,IAC1G,CAAC,CAAC,CAAC;AAAA,EACL,UAAE;AACA,QAAI,MAAO,cAAa,KAAK;AAAA,EAC/B;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,87 @@
1
+ import { matchesEntity } from "@open-mercato/shared/lib/crud/mutation-guard-registry";
2
+ import { attachmentAccessResolverSchema, protectedAttachmentTargetSchema } from "../data/validators.js";
3
+ const GLOBAL_KEY = "__openMercatoAttachmentAccessRegistry__";
4
+ const emptyRegistry = Object.freeze({ invalid: false, resolvers: [], protectedTargets: [] });
5
+ function getAttachmentAccessRegistry() {
6
+ return globalThis[GLOBAL_KEY] ?? emptyRegistry;
7
+ }
8
+ function registerAttachmentAccessResolvers(entries) {
9
+ const globalState = globalThis;
10
+ globalState[GLOBAL_KEY] = { invalid: true, resolvers: [], protectedTargets: [] };
11
+ const resolvers = [];
12
+ const protectedTargets = [];
13
+ const ids = /* @__PURE__ */ new Set();
14
+ for (const [moduleOrder, entry] of entries.entries()) {
15
+ if (typeof entry.moduleId !== "string" || !entry.moduleId || !Array.isArray(entry.resolvers)) {
16
+ throw new Error("[internal] Invalid attachment access registry entry");
17
+ }
18
+ for (const [declarationOrder, raw] of entry.resolvers.entries()) {
19
+ const resolver = attachmentAccessResolverSchema.parse(raw);
20
+ if (!resolver.id.startsWith(`${entry.moduleId}.`) || ids.has(resolver.id)) {
21
+ throw new Error("[internal] Attachment access resolver IDs must be unique and owned by their module");
22
+ }
23
+ ids.add(resolver.id);
24
+ resolvers.push(Object.freeze({
25
+ ...resolver,
26
+ actions: resolver.actions ? Object.freeze([...resolver.actions]) : void 0,
27
+ moduleOrder,
28
+ declarationOrder
29
+ }));
30
+ }
31
+ for (const raw of entry.protectedTargets ?? []) {
32
+ const target = protectedAttachmentTargetSchema.parse(raw);
33
+ if (!target.resolverId.startsWith(`${entry.moduleId}.`)) {
34
+ throw new Error("[internal] Attachment access requirements must name their module resolver");
35
+ }
36
+ protectedTargets.push(Object.freeze(target));
37
+ }
38
+ }
39
+ resolvers.sort((left, right) => (left.priority ?? 50) - (right.priority ?? 50) || left.moduleOrder - right.moduleOrder || left.declarationOrder - right.declarationOrder);
40
+ globalState[GLOBAL_KEY] = Object.freeze({
41
+ invalid: false,
42
+ resolvers: Object.freeze(resolvers),
43
+ protectedTargets: Object.freeze(protectedTargets)
44
+ });
45
+ }
46
+ function matchesAttachmentAccessSelector(selector, value) {
47
+ return matchesEntity(selector, value);
48
+ }
49
+ function matchesAttachmentAccessTargets(selector, targets) {
50
+ return selector === "*" || targets.some((target) => matchesAttachmentAccessSelector(selector, target.entityId));
51
+ }
52
+ function matchesAttachmentAccessResolver(resolver, partitionCode, targets, action) {
53
+ return matchesAttachmentAccessSelector(resolver.targetPartition, partitionCode) && matchesAttachmentAccessTargets(resolver.targetEntity ?? "*", targets) && (!resolver.actions || resolver.actions.includes(action));
54
+ }
55
+ function isRecord(value) {
56
+ return value !== null && typeof value === "object" && !Array.isArray(value);
57
+ }
58
+ function collectAttachmentAccessTargets(attachment) {
59
+ const targets = [];
60
+ function append(entityId, recordId, origin) {
61
+ if (typeof entityId !== "string" || !entityId.trim()) return;
62
+ const target = Object.freeze({
63
+ entityId: entityId.trim(),
64
+ recordId: typeof recordId === "string" ? recordId.trim() : "",
65
+ origin
66
+ });
67
+ if (!targets.some((candidate) => candidate.entityId === target.entityId && candidate.recordId === target.recordId)) {
68
+ targets.push(target);
69
+ }
70
+ }
71
+ append(attachment.entityId, attachment.recordId, "primary");
72
+ const assignments = isRecord(attachment.storageMetadata) ? attachment.storageMetadata.assignments : void 0;
73
+ const entries = Array.isArray(assignments) ? assignments : isRecord(assignments) ? [assignments] : [];
74
+ for (const assignment of entries) {
75
+ if (isRecord(assignment)) append(assignment.type, assignment.id, "assignment");
76
+ }
77
+ return Object.freeze(targets);
78
+ }
79
+ export {
80
+ collectAttachmentAccessTargets,
81
+ getAttachmentAccessRegistry,
82
+ matchesAttachmentAccessResolver,
83
+ matchesAttachmentAccessSelector,
84
+ matchesAttachmentAccessTargets,
85
+ registerAttachmentAccessResolvers
86
+ };
87
+ //# sourceMappingURL=access-registry.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/attachments/lib/access-registry.ts"],
4
+ "sourcesContent": ["import { matchesEntity } from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { attachmentAccessResolverSchema, protectedAttachmentTargetSchema } from '../data/validators'\nimport type {\n AttachmentAccessAction,\n AttachmentAccessRecord,\n AttachmentAccessResolver,\n AttachmentAccessResolverEntry,\n AttachmentAccessTarget,\n ProtectedAttachmentTarget,\n} from './access-types'\n\nconst GLOBAL_KEY = '__openMercatoAttachmentAccessRegistry__'\n\ntype RegisteredResolver = AttachmentAccessResolver & {\n moduleOrder: number\n declarationOrder: number\n}\n\ntype Registry = {\n invalid: boolean\n resolvers: readonly RegisteredResolver[]\n protectedTargets: readonly ProtectedAttachmentTarget[]\n}\n\nconst emptyRegistry: Registry = Object.freeze({ invalid: false, resolvers: [], protectedTargets: [] })\n\nexport function getAttachmentAccessRegistry(): Registry {\n return (globalThis as Record<string, unknown>)[GLOBAL_KEY] as Registry | undefined ?? emptyRegistry\n}\n\nexport function registerAttachmentAccessResolvers(entries: readonly AttachmentAccessResolverEntry[]): void {\n const globalState = globalThis as Record<string, unknown>\n globalState[GLOBAL_KEY] = { invalid: true, resolvers: [], protectedTargets: [] } satisfies Registry\n const resolvers: RegisteredResolver[] = []\n const protectedTargets: ProtectedAttachmentTarget[] = []\n const ids = new Set<string>()\n for (const [moduleOrder, entry] of entries.entries()) {\n if (typeof entry.moduleId !== 'string' || !entry.moduleId || !Array.isArray(entry.resolvers)) {\n throw new Error('[internal] Invalid attachment access registry entry')\n }\n for (const [declarationOrder, raw] of entry.resolvers.entries()) {\n const resolver = attachmentAccessResolverSchema.parse(raw)\n if (!resolver.id.startsWith(`${entry.moduleId}.`) || ids.has(resolver.id)) {\n throw new Error('[internal] Attachment access resolver IDs must be unique and owned by their module')\n }\n ids.add(resolver.id)\n resolvers.push(Object.freeze({\n ...resolver,\n actions: resolver.actions ? Object.freeze([...resolver.actions]) : undefined,\n moduleOrder,\n declarationOrder,\n }))\n }\n for (const raw of entry.protectedTargets ?? []) {\n const target = protectedAttachmentTargetSchema.parse(raw)\n if (!target.resolverId.startsWith(`${entry.moduleId}.`)) {\n throw new Error('[internal] Attachment access requirements must name their module resolver')\n }\n protectedTargets.push(Object.freeze(target))\n }\n }\n resolvers.sort((left, right) => (\n (left.priority ?? 50) - (right.priority ?? 50)\n || left.moduleOrder - right.moduleOrder\n || left.declarationOrder - right.declarationOrder\n ))\n globalState[GLOBAL_KEY] = Object.freeze({\n invalid: false,\n resolvers: Object.freeze(resolvers),\n protectedTargets: Object.freeze(protectedTargets),\n }) satisfies Registry\n}\n\nexport function matchesAttachmentAccessSelector(selector: string, value: string): boolean {\n return matchesEntity(selector, value)\n}\n\nexport function matchesAttachmentAccessTargets(selector: string, targets: readonly AttachmentAccessTarget[]): boolean {\n return selector === '*' || targets.some((target) => matchesAttachmentAccessSelector(selector, target.entityId))\n}\n\nexport function matchesAttachmentAccessResolver(\n resolver: AttachmentAccessResolver,\n partitionCode: string,\n targets: readonly AttachmentAccessTarget[],\n action: AttachmentAccessAction,\n): boolean {\n return matchesAttachmentAccessSelector(resolver.targetPartition, partitionCode)\n && matchesAttachmentAccessTargets(resolver.targetEntity ?? '*', targets)\n && (!resolver.actions || resolver.actions.includes(action))\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nexport function collectAttachmentAccessTargets(attachment: AttachmentAccessRecord): readonly AttachmentAccessTarget[] {\n const targets: AttachmentAccessTarget[] = []\n function append(entityId: unknown, recordId: unknown, origin: AttachmentAccessTarget['origin']) {\n if (typeof entityId !== 'string' || !entityId.trim()) return\n const target = Object.freeze({\n entityId: entityId.trim(),\n recordId: typeof recordId === 'string' ? recordId.trim() : '',\n origin,\n })\n if (!targets.some((candidate) => candidate.entityId === target.entityId && candidate.recordId === target.recordId)) {\n targets.push(target)\n }\n }\n append(attachment.entityId, attachment.recordId, 'primary')\n const assignments = isRecord(attachment.storageMetadata) ? attachment.storageMetadata.assignments : undefined\n const entries = Array.isArray(assignments) ? assignments : isRecord(assignments) ? [assignments] : []\n for (const assignment of entries) {\n if (isRecord(assignment)) append(assignment.type, assignment.id, 'assignment')\n }\n return Object.freeze(targets)\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,161 @@
1
+ import { createLogger } from "@open-mercato/shared/lib/logger";
2
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
3
+ import { attachmentAccessDecisionSchema, attachmentAccessRequirementsSchema, attachmentAccessSubjectFeaturesSchema } from "../data/validators.js";
4
+ import { checkAttachmentAccess } from "./access.js";
5
+ import {
6
+ collectAttachmentAccessTargets,
7
+ getAttachmentAccessRegistry,
8
+ matchesAttachmentAccessResolver,
9
+ matchesAttachmentAccessSelector,
10
+ matchesAttachmentAccessTargets
11
+ } from "./access-registry.js";
12
+ const logger = createLogger("attachments").child({ component: "access" });
13
+ const DEFAULT_TIMEOUT_MS = 1500;
14
+ function createAttachmentAccessContext(container) {
15
+ return { container, subjects: /* @__PURE__ */ new Map(), resolverCaches: /* @__PURE__ */ new Map() };
16
+ }
17
+ class AccessTimeout extends Error {
18
+ }
19
+ async function withinBudget(work, timeoutMs, controller) {
20
+ let timer;
21
+ try {
22
+ return await Promise.race([
23
+ Promise.resolve().then(work),
24
+ new Promise((_resolve, reject) => {
25
+ timer = setTimeout(() => {
26
+ controller.abort();
27
+ reject(new AccessTimeout("[internal] Attachment access evaluation timed out"));
28
+ }, timeoutMs);
29
+ })
30
+ ]);
31
+ } finally {
32
+ if (timer) clearTimeout(timer);
33
+ }
34
+ }
35
+ async function projectSubject(context, auth, attachment) {
36
+ const scope = {
37
+ tenantId: attachment.tenantId ?? auth.tenantId ?? null,
38
+ organizationId: attachment.organizationId ?? auth.orgId ?? null
39
+ };
40
+ const key = JSON.stringify([auth.sub, scope.tenantId, scope.organizationId]);
41
+ let pending = context.subjects.get(key);
42
+ if (!pending) {
43
+ pending = (async () => {
44
+ const rbac = context.container.resolve("rbacService");
45
+ const acl = await rbac.loadAcl(auth.sub, scope);
46
+ const userFeatures = attachmentAccessSubjectFeaturesSchema.parse(await rbac.getEffectiveFeatures(auth.sub, scope));
47
+ if (typeof acl.isSuperAdmin !== "boolean") {
48
+ throw new Error("[internal] Invalid attachment subject projection");
49
+ }
50
+ if (!acl.isSuperAdmin && acl.organizations && scope.organizationId && !acl.organizations.includes(scope.organizationId) && !acl.organizations.includes("__all__")) {
51
+ throw new Error("[internal] Attachment subject is outside its current organization grants");
52
+ }
53
+ return Object.freeze({
54
+ auth: Object.freeze({
55
+ sub: auth.sub,
56
+ tenantId: scope.tenantId,
57
+ orgId: scope.organizationId,
58
+ isApiKey: auth.isApiKey === true,
59
+ userId: auth.userId,
60
+ keyId: auth.keyId,
61
+ isSuperAdmin: acl.isSuperAdmin
62
+ }),
63
+ ...scope,
64
+ userFeatures: Object.freeze([...userFeatures]),
65
+ isSuperAdmin: acl.isSuperAdmin
66
+ });
67
+ })();
68
+ context.subjects.set(key, pending);
69
+ }
70
+ return pending;
71
+ }
72
+ async function evaluateAttachmentAccess(input) {
73
+ const { auth, attachment, partition, action, context } = input;
74
+ const baseline = checkAttachmentAccess(auth, attachment, partition, { requireAuthForPublic: input.requireAuthForPublic });
75
+ if (!baseline.ok) return { ok: false, status: baseline.status === 401 ? 401 : 403, reason: "scope", protected: false };
76
+ const deny = (status, reason, resolverId) => {
77
+ logger.info("Attachment owner access denied", { resolverId, action, attachmentId: attachment.id, reason });
78
+ return { ok: false, status, reason, protected: true };
79
+ };
80
+ const registry = getAttachmentAccessRegistry();
81
+ if (registry.invalid) return deny(403, "invalid_registry");
82
+ const parsedRequirements = attachmentAccessRequirementsSchema.safeParse(partition.accessResolverRequirements ?? []);
83
+ if (!parsedRequirements.success) return deny(403, "invalid_requirements");
84
+ const targets = collectAttachmentAccessTargets(attachment);
85
+ const requirements = [...parsedRequirements.data, ...registry.protectedTargets.filter((target) => matchesAttachmentAccessSelector(target.targetPartition, partition.code))].filter((requirement) => matchesAttachmentAccessTargets(requirement.targetEntity, targets));
86
+ const resolvers = registry.resolvers.filter((resolver) => matchesAttachmentAccessResolver(resolver, partition.code, targets, action));
87
+ if (!requirements.length && !resolvers.length) return { ok: true, protected: false };
88
+ if (!auth) return deny(401, "authentication_required");
89
+ for (const requirement of requirements) {
90
+ const requiredTargets = targets.filter((target) => matchesAttachmentAccessSelector(requirement.targetEntity, target.entityId));
91
+ const provider = resolvers.find((resolver) => resolver.id === requirement.resolverId);
92
+ if (!provider || (requiredTargets.length ? requiredTargets.some((target) => !matchesAttachmentAccessSelector(provider.targetEntity ?? "*", target.entityId)) : (provider.targetEntity ?? "*") !== "*")) return deny(403, "required_resolver_missing", requirement.resolverId);
93
+ if (targets.some((target) => matchesAttachmentAccessSelector(requirement.targetEntity, target.entityId) && !target.recordId)) {
94
+ return deny(403, "invalid_owner", requirement.resolverId);
95
+ }
96
+ }
97
+ if (!context) return deny(403, "policy_context_missing");
98
+ let subject;
99
+ try {
100
+ subject = await withinBudget(() => projectSubject(context, auth, attachment), DEFAULT_TIMEOUT_MS, new AbortController());
101
+ } catch (error) {
102
+ getTelemetryRuntime()?.reportError(new Error("[internal] Attachment subject projection failed"), {
103
+ module: "attachments",
104
+ code: "attachments.subject_projection_failed",
105
+ attributes: { attachmentId: attachment.id, action }
106
+ });
107
+ return deny(error instanceof AccessTimeout ? 504 : 403, "subject_projection_failed");
108
+ }
109
+ const liveBaseline = checkAttachmentAccess({ ...auth, roles: [], isSuperAdmin: subject.isSuperAdmin }, attachment, partition, { requireAuthForPublic: input.requireAuthForPublic });
110
+ if (!liveBaseline.ok) return deny(403, "current_scope");
111
+ for (const resolver of resolvers) {
112
+ const controller = new AbortController();
113
+ let cache = context.resolverCaches.get(resolver.id);
114
+ if (!cache) {
115
+ cache = /* @__PURE__ */ new Map();
116
+ context.resolverCaches.set(resolver.id, cache);
117
+ }
118
+ try {
119
+ const raw = await withinBudget(() => resolver.resolve(Object.freeze({
120
+ action,
121
+ subject,
122
+ attachment: Object.freeze({
123
+ id: attachment.id,
124
+ entityId: attachment.entityId,
125
+ recordId: attachment.recordId,
126
+ partitionCode: attachment.partitionCode,
127
+ tenantId: attachment.tenantId,
128
+ organizationId: attachment.organizationId,
129
+ fileName: attachment.fileName,
130
+ mimeType: attachment.mimeType
131
+ }),
132
+ partition: Object.freeze({
133
+ code: partition.code,
134
+ isPublic: partition.isPublic,
135
+ tenantId: partition.tenantId,
136
+ organizationId: partition.organizationId
137
+ }),
138
+ targets,
139
+ container: context.container,
140
+ cache,
141
+ signal: controller.signal
142
+ })), resolver.timeoutMs ?? DEFAULT_TIMEOUT_MS, controller);
143
+ const decision = attachmentAccessDecisionSchema.parse(raw);
144
+ if (!decision.ok) return deny(decision.status, decision.reason, resolver.id);
145
+ } catch (error) {
146
+ controller.abort();
147
+ getTelemetryRuntime()?.reportError(new Error(error instanceof AccessTimeout ? "[internal] Attachment resolver exceeded its time budget" : "[internal] Attachment resolver failed or returned an invalid decision"), {
148
+ module: "attachments",
149
+ code: "attachments.resolver_failed",
150
+ attributes: { resolverId: resolver.id, attachmentId: attachment.id, action }
151
+ });
152
+ return deny(error instanceof AccessTimeout ? 504 : 403, error instanceof AccessTimeout ? "resolver_timeout" : "resolver_failed", resolver.id);
153
+ }
154
+ }
155
+ return { ok: true, protected: true };
156
+ }
157
+ export {
158
+ createAttachmentAccessContext,
159
+ evaluateAttachmentAccess
160
+ };
161
+ //# sourceMappingURL=access-runner.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/attachments/lib/access-runner.ts"],
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { Attachment, AttachmentPartition } from '../data/entities'\nimport { attachmentAccessDecisionSchema, attachmentAccessRequirementsSchema, attachmentAccessSubjectFeaturesSchema } from '../data/validators'\nimport { checkAttachmentAccess } from './access'\nimport {\n collectAttachmentAccessTargets,\n getAttachmentAccessRegistry,\n matchesAttachmentAccessResolver,\n matchesAttachmentAccessSelector,\n matchesAttachmentAccessTargets,\n} from './access-registry'\nimport type {\n AttachmentAccessAction, AttachmentAccessDecision, AttachmentAccessPartition,\n AttachmentAccessRecord, AttachmentAccessSubject,\n} from './access-types'\n\nconst logger = createLogger('attachments').child({ component: 'access' })\nconst DEFAULT_TIMEOUT_MS = 1500\n\ntype RbacProjectionService = {\n loadAcl(subject: string, scope: { tenantId: string | null; organizationId: string | null }): Promise<{\n isSuperAdmin: boolean; organizations: string[] | null\n }>\n getEffectiveFeatures(subject: string, scope: { tenantId: string | null; organizationId: string | null }): Promise<string[]>\n}\n\nexport type AttachmentAccessContext = {\n container: AwilixContainer\n subjects: Map<string, Promise<AttachmentAccessSubject>>\n resolverCaches: Map<string, Map<string, unknown>>\n}\n\nexport function createAttachmentAccessContext(container: AwilixContainer): AttachmentAccessContext {\n return { container, subjects: new Map(), resolverCaches: new Map() }\n}\n\nexport type AttachmentAccessResult = AttachmentAccessDecision & { protected: boolean }\n\nclass AccessTimeout extends Error {}\n\nasync function withinBudget<Result>(work: () => Promise<Result>, timeoutMs: number, controller: AbortController): Promise<Result> {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n return await Promise.race([\n Promise.resolve().then(work),\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n controller.abort()\n reject(new AccessTimeout('[internal] Attachment access evaluation timed out'))\n }, timeoutMs)\n }),\n ])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\nasync function projectSubject(\n context: AttachmentAccessContext,\n auth: NonNullable<AuthContext>,\n attachment: AttachmentAccessRecord,\n): Promise<AttachmentAccessSubject> {\n const scope = {\n tenantId: attachment.tenantId ?? auth.tenantId ?? null,\n organizationId: attachment.organizationId ?? auth.orgId ?? null,\n }\n const key = JSON.stringify([auth.sub, scope.tenantId, scope.organizationId])\n let pending = context.subjects.get(key)\n if (!pending) {\n pending = (async () => {\n const rbac = context.container.resolve<RbacProjectionService>('rbacService')\n const acl = await rbac.loadAcl(auth.sub, scope)\n const userFeatures = attachmentAccessSubjectFeaturesSchema.parse(await rbac.getEffectiveFeatures(auth.sub, scope))\n if (typeof acl.isSuperAdmin !== 'boolean') {\n throw new Error('[internal] Invalid attachment subject projection')\n }\n if (!acl.isSuperAdmin && acl.organizations && scope.organizationId\n && !acl.organizations.includes(scope.organizationId) && !acl.organizations.includes('__all__')) {\n throw new Error('[internal] Attachment subject is outside its current organization grants')\n }\n return Object.freeze({\n auth: Object.freeze({\n sub: auth.sub, tenantId: scope.tenantId, orgId: scope.organizationId,\n isApiKey: auth.isApiKey === true, userId: auth.userId, keyId: auth.keyId,\n isSuperAdmin: acl.isSuperAdmin,\n }),\n ...scope,\n userFeatures: Object.freeze([...userFeatures]),\n isSuperAdmin: acl.isSuperAdmin,\n })\n })()\n context.subjects.set(key, pending)\n }\n return pending\n}\n\nexport async function evaluateAttachmentAccess(input: {\n auth: AuthContext | undefined\n attachment: AttachmentAccessRecord\n partition: AttachmentAccessPartition\n action: AttachmentAccessAction\n context?: AttachmentAccessContext | null\n requireAuthForPublic?: boolean\n}): Promise<AttachmentAccessResult> {\n const { auth, attachment, partition, action, context } = input\n const baseline = checkAttachmentAccess(auth, attachment as Attachment, partition as AttachmentPartition, { requireAuthForPublic: input.requireAuthForPublic })\n if (!baseline.ok) return { ok: false, status: baseline.status === 401 ? 401 : 403, reason: 'scope', protected: false }\n const deny = (status: 401 | 403 | 404 | 504, reason: string, resolverId?: string): AttachmentAccessResult => {\n logger.info('Attachment owner access denied', { resolverId, action, attachmentId: attachment.id, reason })\n return { ok: false, status, reason, protected: true }\n }\n const registry = getAttachmentAccessRegistry()\n if (registry.invalid) return deny(403, 'invalid_registry')\n const parsedRequirements = attachmentAccessRequirementsSchema.safeParse(partition.accessResolverRequirements ?? [])\n if (!parsedRequirements.success) return deny(403, 'invalid_requirements')\n const targets = collectAttachmentAccessTargets(attachment)\n const requirements = [...parsedRequirements.data, ...registry.protectedTargets.filter((target) => (\n matchesAttachmentAccessSelector(target.targetPartition, partition.code)\n ))].filter((requirement) => matchesAttachmentAccessTargets(requirement.targetEntity, targets))\n const resolvers = registry.resolvers.filter((resolver) => matchesAttachmentAccessResolver(resolver, partition.code, targets, action))\n if (!requirements.length && !resolvers.length) return { ok: true, protected: false }\n if (!auth) return deny(401, 'authentication_required')\n for (const requirement of requirements) {\n const requiredTargets = targets.filter((target) => matchesAttachmentAccessSelector(requirement.targetEntity, target.entityId))\n const provider = resolvers.find((resolver) => resolver.id === requirement.resolverId)\n if (!provider || (requiredTargets.length\n ? requiredTargets.some((target) => !matchesAttachmentAccessSelector(provider.targetEntity ?? '*', target.entityId))\n : (provider.targetEntity ?? '*') !== '*')) return deny(403, 'required_resolver_missing', requirement.resolverId)\n if (targets.some((target) => matchesAttachmentAccessSelector(requirement.targetEntity, target.entityId) && !target.recordId)) {\n return deny(403, 'invalid_owner', requirement.resolverId)\n }\n }\n if (!context) return deny(403, 'policy_context_missing')\n let subject: AttachmentAccessSubject\n try {\n subject = await withinBudget(() => projectSubject(context, auth, attachment), DEFAULT_TIMEOUT_MS, new AbortController())\n } catch (error) {\n getTelemetryRuntime()?.reportError(new Error('[internal] Attachment subject projection failed'), {\n module: 'attachments', code: 'attachments.subject_projection_failed', attributes: { attachmentId: attachment.id, action },\n })\n return deny(error instanceof AccessTimeout ? 504 : 403, 'subject_projection_failed')\n }\n const liveBaseline = checkAttachmentAccess({ ...auth, roles: [], isSuperAdmin: subject.isSuperAdmin }, attachment as Attachment, partition as AttachmentPartition, { requireAuthForPublic: input.requireAuthForPublic })\n if (!liveBaseline.ok) return deny(403, 'current_scope')\n for (const resolver of resolvers) {\n const controller = new AbortController()\n let cache = context.resolverCaches.get(resolver.id)\n if (!cache) {\n cache = new Map()\n context.resolverCaches.set(resolver.id, cache)\n }\n try {\n const raw = await withinBudget(() => resolver.resolve(Object.freeze({\n action, subject,\n attachment: Object.freeze({\n id: attachment.id, entityId: attachment.entityId, recordId: attachment.recordId,\n partitionCode: attachment.partitionCode, tenantId: attachment.tenantId,\n organizationId: attachment.organizationId, fileName: attachment.fileName, mimeType: attachment.mimeType,\n }),\n partition: Object.freeze({\n code: partition.code, isPublic: partition.isPublic,\n tenantId: partition.tenantId, organizationId: partition.organizationId,\n }),\n targets, container: context.container, cache, signal: controller.signal,\n })), resolver.timeoutMs ?? DEFAULT_TIMEOUT_MS, controller)\n const decision = attachmentAccessDecisionSchema.parse(raw)\n if (!decision.ok) return deny(decision.status, decision.reason, resolver.id)\n } catch (error) {\n controller.abort()\n getTelemetryRuntime()?.reportError(new Error(error instanceof AccessTimeout\n ? '[internal] Attachment resolver exceeded its time budget'\n : '[internal] Attachment resolver failed or returned an invalid decision'), {\n module: 'attachments', code: 'attachments.resolver_failed',\n attributes: { resolverId: resolver.id, attachmentId: attachment.id, action },\n })\n return deny(error instanceof AccessTimeout ? 504 : 403, error instanceof AccessTimeout ? 'resolver_timeout' : 'resolver_failed', resolver.id)\n }\n }\n return { ok: true, protected: true }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=access-types.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -1,9 +1,11 @@
1
+ import { evaluateAttachmentAccess } from "./access-runner.js";
2
+ import { throwAttachmentAccessError } from "./access-errors.js";
1
3
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
2
4
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
5
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
4
6
  import { createLogger } from "@open-mercato/shared/lib/logger";
5
7
  import { Attachment, AttachmentPartition } from "../data/entities.js";
6
- import { assertAttachmentScopeInvariant, checkAttachmentAccess } from "./access.js";
8
+ import { assertAttachmentScopeInvariant } from "./access.js";
7
9
  import { buildAttachmentFileUrl } from "./imageUrls.js";
8
10
  import {
9
11
  isScopedAttachmentUploadError
@@ -126,10 +128,11 @@ class DefaultAttachmentService {
126
128
  * share the one reservation ledger rather than run a parallel quota
127
129
  * mechanism — so its absence surfaces there, at the call that needs it.
128
130
  */
129
- constructor(em, storageDriverFactory, resolveScopedUploadService) {
131
+ constructor(em, storageDriverFactory, resolveScopedUploadService, resolveAccessContext) {
130
132
  this.em = em;
131
133
  this.storageDriverFactory = storageDriverFactory;
132
134
  this.resolveScopedUploadService = resolveScopedUploadService;
135
+ this.resolveAccessContext = resolveAccessContext;
133
136
  }
134
137
  validateUpload(input) {
135
138
  if (!isMultipartRequestWithinUploadLimit(input.contentLength ?? null)) {
@@ -221,10 +224,15 @@ class DefaultAttachmentService {
221
224
  { tenantId: input.auth.tenantId, organizationId: input.auth.orgId }
222
225
  );
223
226
  if (!partition) throw new CrudHttpError(500, { error: "Attachment partition is not configured" });
224
- const access = checkAttachmentAccess(input.auth, attachment, partition, { requireAuthForPublic: true });
225
- if (!access.ok) {
226
- throw new CrudHttpError(access.status, { error: access.status === 401 ? "Unauthorized" : "Forbidden" });
227
- }
227
+ const access = await evaluateAttachmentAccess({
228
+ auth: input.auth,
229
+ attachment,
230
+ partition,
231
+ action: "read",
232
+ context: this.resolveAccessContext?.(),
233
+ requireAuthForPublic: true
234
+ });
235
+ if (!access.ok) await throwAttachmentAccessError(access.status);
228
236
  if (!partitionMatchesScope(partition, input.auth.tenantId, input.auth.orgId)) {
229
237
  throw new CrudHttpError(403, { error: "Attachment partition is not accessible for this scope" });
230
238
  }