@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/attachment_partition/index.js +2 -0
- package/dist/generated/entities/attachment_partition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
- package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
- package/dist/modules/attachments/api/file/[id]/route.js +16 -10
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +100 -56
- package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
- package/dist/modules/attachments/api/library/route.js +56 -17
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +2 -0
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +74 -34
- package/dist/modules/attachments/api/route.js.map +3 -3
- package/dist/modules/attachments/api/transfer/route.js +63 -37
- package/dist/modules/attachments/api/transfer/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -0
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +41 -0
- package/dist/modules/attachments/data/validators.js.map +7 -0
- package/dist/modules/attachments/di.js +5 -2
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/generators.js +29 -0
- package/dist/modules/attachments/generators.js.map +7 -0
- package/dist/modules/attachments/index.js +8 -1
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-errors.js +35 -0
- package/dist/modules/attachments/lib/access-errors.js.map +7 -0
- package/dist/modules/attachments/lib/access-mutation.js +23 -0
- package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
- package/dist/modules/attachments/lib/access-protection.js +73 -0
- package/dist/modules/attachments/lib/access-protection.js.map +7 -0
- package/dist/modules/attachments/lib/access-query.js +74 -0
- package/dist/modules/attachments/lib/access-query.js.map +7 -0
- package/dist/modules/attachments/lib/access-registry.js +87 -0
- package/dist/modules/attachments/lib/access-registry.js.map +7 -0
- package/dist/modules/attachments/lib/access-runner.js +161 -0
- package/dist/modules/attachments/lib/access-runner.js.map +7 -0
- package/dist/modules/attachments/lib/access-types.js +1 -0
- package/dist/modules/attachments/lib/access-types.js.map +7 -0
- package/dist/modules/attachments/lib/attachment-service.js +14 -6
- package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
- package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
- package/dist/modules/attachments/lib/partitions.js +3 -2
- package/dist/modules/attachments/lib/partitions.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/target-access-service.js +12 -3
- package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
- package/dist/modules/attachments/setup.js +10 -0
- package/dist/modules/attachments/setup.js.map +2 -2
- package/dist/modules/notifications/api/[id]/action/route.js +8 -0
- package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
- package/dist/modules/notifications/api/route.js +17 -0
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +1 -1
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +45 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +27 -0
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
- package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
- package/dist/modules/staff/notifications.js +4 -2
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +18 -4
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-types.js +2 -2
- package/dist/modules/workflows/lib/activity-types.js.map +2 -2
- package/dist/modules/workflows/lib/condition-handler.js +7 -1
- package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +13 -0
- package/dist/modules/workflows/lib/duration.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +9 -2
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-quick-action.js +13 -10
- package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +2 -1
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/notifications.js +4 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/attachment_partition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
- package/src/modules/attachments/api/file/[id]/route.ts +12 -9
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
- package/src/modules/attachments/api/library/[id]/route.ts +87 -58
- package/src/modules/attachments/api/library/route.ts +40 -11
- package/src/modules/attachments/api/partitions/route.ts +2 -0
- package/src/modules/attachments/api/route.ts +68 -35
- package/src/modules/attachments/api/transfer/route.ts +65 -42
- package/src/modules/attachments/data/entities.ts +4 -0
- package/src/modules/attachments/data/validators.ts +36 -0
- package/src/modules/attachments/di.ts +4 -1
- package/src/modules/attachments/generators.ts +27 -0
- package/src/modules/attachments/i18n/de.json +4 -0
- package/src/modules/attachments/i18n/en.json +4 -0
- package/src/modules/attachments/i18n/es.json +4 -0
- package/src/modules/attachments/i18n/ko.json +4 -0
- package/src/modules/attachments/i18n/pl.json +4 -0
- package/src/modules/attachments/index.ts +18 -0
- package/src/modules/attachments/lib/access-errors.ts +32 -0
- package/src/modules/attachments/lib/access-mutation.ts +26 -0
- package/src/modules/attachments/lib/access-protection.ts +83 -0
- package/src/modules/attachments/lib/access-query.ts +86 -0
- package/src/modules/attachments/lib/access-registry.ts +117 -0
- package/src/modules/attachments/lib/access-runner.ts +183 -0
- package/src/modules/attachments/lib/access-types.ts +78 -0
- package/src/modules/attachments/lib/attachment-service.ts +8 -4
- package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
- package/src/modules/attachments/lib/partitions.ts +3 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
- package/src/modules/attachments/lib/target-access-service.ts +10 -3
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
- package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
- package/src/modules/attachments/setup.ts +10 -0
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/notifications/api/[id]/action/route.ts +8 -0
- package/src/modules/notifications/api/route.ts +17 -0
- package/src/modules/notifications/data/validators.ts +1 -1
- package/src/modules/notifications/i18n/de.json +2 -0
- package/src/modules/notifications/i18n/en.json +2 -0
- package/src/modules/notifications/i18n/es.json +2 -0
- package/src/modules/notifications/i18n/ko.json +2 -0
- package/src/modules/notifications/i18n/pl.json +2 -0
- package/src/modules/notifications/lib/notificationService.ts +67 -1
- package/src/modules/notifications/lib/routeHelpers.ts +38 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
- package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
- package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
- package/src/modules/staff/notifications.ts +2 -0
- package/src/modules/workflows/AGENTS.md +2 -2
- package/src/modules/workflows/data/entities.ts +6 -0
- package/src/modules/workflows/lib/activity-executor.ts +36 -4
- package/src/modules/workflows/lib/activity-types.ts +2 -2
- package/src/modules/workflows/lib/condition-handler.ts +16 -1
- package/src/modules/workflows/lib/duration.ts +25 -0
- package/src/modules/workflows/lib/step-handler.ts +16 -1
- package/src/modules/workflows/lib/task-quick-action.ts +19 -10
- package/src/modules/workflows/lib/transition-handler.ts +2 -1
- package/src/modules/workflows/notifications.ts +4 -0
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { LockMode } from "@mikro-orm/core";
|
|
2
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { Attachment } from "../../data/entities.js";
|
|
4
|
+
import { createAttachmentAccessContext } from "../../lib/access-runner.js";
|
|
5
|
+
import { assertAttachmentOwnerAccess } from "../../lib/access-query.js";
|
|
6
|
+
import { withAttachmentAccessErrors, throwAttachmentAccessError } from "../../lib/access-errors.js";
|
|
7
|
+
import { prepareAttachmentMutation } from "../../lib/access-mutation.js";
|
|
8
|
+
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
1
9
|
import { NextResponse } from "next/server";
|
|
2
10
|
import { z } from "zod";
|
|
3
11
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
@@ -18,56 +26,72 @@ const transferSchema = z.object({
|
|
|
18
26
|
const metadata = {
|
|
19
27
|
POST: { requireAuth: true, requireFeatures: ["attachments.manage"] }
|
|
20
28
|
};
|
|
21
|
-
async function
|
|
29
|
+
async function transferAttachments(req) {
|
|
22
30
|
const auth = await getAuthFromRequest(req);
|
|
23
31
|
if (!auth || !auth.tenantId || !auth.orgId) {
|
|
24
32
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
25
33
|
}
|
|
26
|
-
const json = await req
|
|
34
|
+
const json = await readJsonSafe(req, null);
|
|
27
35
|
const parsed = transferSchema.safeParse(json);
|
|
28
36
|
if (!parsed.success) {
|
|
29
37
|
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
|
|
30
38
|
}
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
AttachmentEntity = mod.Attachment;
|
|
38
|
-
} catch {
|
|
39
|
-
return NextResponse.json({ error: "Attachment model missing" }, { status: 500 });
|
|
40
|
-
}
|
|
39
|
+
const container = await createRequestContainer();
|
|
40
|
+
const em = container.resolve("em");
|
|
41
|
+
const guard = await prepareAttachmentMutation({ container, req, auth, operation: "update", payload: parsed.data });
|
|
42
|
+
const effective = transferSchema.safeParse(guard.modifiedPayload ?? parsed.data);
|
|
43
|
+
if (!effective.success) return throwAttachmentAccessError(403);
|
|
44
|
+
const { attachmentIds, entityId, fromRecordId } = effective.data;
|
|
41
45
|
const filters = {
|
|
42
|
-
id: { $in: attachmentIds },
|
|
46
|
+
id: { $in: [...new Set(attachmentIds)].sort((left, right) => left < right ? -1 : left > right ? 1 : 0) },
|
|
43
47
|
entityId,
|
|
44
48
|
tenantId: auth.tenantId,
|
|
45
|
-
organizationId: auth.orgId
|
|
49
|
+
organizationId: auth.orgId,
|
|
50
|
+
...fromRecordId ? { recordId: fromRecordId } : {}
|
|
46
51
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
record
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
const accessContext = createAttachmentAccessContext(container);
|
|
53
|
+
const outcome = await em.transactional(async (tx) => {
|
|
54
|
+
const records = await findWithDecryption(tx, Attachment, filters, {
|
|
55
|
+
orderBy: { id: "asc" },
|
|
56
|
+
lockMode: LockMode.PESSIMISTIC_WRITE,
|
|
57
|
+
refresh: true
|
|
58
|
+
}, { tenantId: auth.tenantId, organizationId: auth.orgId });
|
|
59
|
+
if (records.length !== new Set(attachmentIds).size) return throwAttachmentAccessError(404);
|
|
60
|
+
const changes = [];
|
|
61
|
+
for (const record of records) {
|
|
62
|
+
const recordPayload = { ...effective.data, attachmentIds: [record.id] };
|
|
63
|
+
const recordGuard = await prepareAttachmentMutation({
|
|
64
|
+
container,
|
|
65
|
+
req,
|
|
66
|
+
auth,
|
|
67
|
+
recordId: record.id,
|
|
68
|
+
operation: "update",
|
|
69
|
+
payload: recordPayload
|
|
70
|
+
});
|
|
71
|
+
const recordEffective = transferSchema.safeParse(recordGuard.modifiedPayload ?? recordPayload);
|
|
72
|
+
if (!recordEffective.success || recordEffective.data.attachmentIds.length !== 1 || recordEffective.data.attachmentIds[0] !== record.id || recordEffective.data.entityId !== record.entityId || recordEffective.data.fromRecordId && recordEffective.data.fromRecordId !== record.recordId) {
|
|
73
|
+
return throwAttachmentAccessError(403);
|
|
63
74
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
const destinationId = recordEffective.data.toRecordId;
|
|
76
|
+
const metadata2 = readAttachmentMetadata(record.storageMetadata);
|
|
77
|
+
const assignments = metadata2.assignments?.map((assignment) => assignment.type === entityId && assignment.id === (fromRecordId ?? record.recordId) ? { ...assignment, id: destinationId } : assignment) ?? [];
|
|
78
|
+
const storageMetadata = mergeAttachmentMetadata(record.storageMetadata, { assignments });
|
|
79
|
+
await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: "reassign", persistProtection: true });
|
|
80
|
+
await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, recordId: destinationId, storageMetadata }, action: "reassign" });
|
|
81
|
+
changes.push({ record, storageMetadata, destinationId, guard: recordGuard });
|
|
82
|
+
}
|
|
83
|
+
for (const { record, storageMetadata, destinationId } of changes) {
|
|
84
|
+
record.recordId = destinationId;
|
|
85
|
+
record.storageMetadata = storageMetadata;
|
|
86
|
+
}
|
|
87
|
+
await tx.flush();
|
|
88
|
+
return { updated: records.length, guards: changes.map((change) => change.guard) };
|
|
89
|
+
});
|
|
90
|
+
await guard.runAfterSuccess();
|
|
91
|
+
for (const recordGuard of outcome.guards) await recordGuard.runAfterSuccess();
|
|
92
|
+
return NextResponse.json({ ok: true, updated: outcome.updated });
|
|
70
93
|
}
|
|
94
|
+
const POST = withAttachmentAccessErrors(transferAttachments);
|
|
71
95
|
const openApi = {
|
|
72
96
|
tag: attachmentsTag,
|
|
73
97
|
summary: "Transfer attachments between records",
|
|
@@ -86,7 +110,9 @@ const openApi = {
|
|
|
86
110
|
{ status: 400, description: "Invalid payload", schema: attachmentErrorSchema },
|
|
87
111
|
{ status: 401, description: "Unauthorized", schema: attachmentErrorSchema },
|
|
88
112
|
{ status: 404, description: "Attachments not found", schema: attachmentErrorSchema },
|
|
89
|
-
{ status: 500, description: "Attachment model missing", schema: attachmentErrorSchema }
|
|
113
|
+
{ status: 500, description: "Attachment model missing", schema: attachmentErrorSchema },
|
|
114
|
+
{ status: 403, description: "Owner policy denies access", schema: attachmentErrorSchema },
|
|
115
|
+
{ status: 504, description: "Owner authorization timed out", schema: attachmentErrorSchema }
|
|
90
116
|
]
|
|
91
117
|
}
|
|
92
118
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/attachments/api/transfer/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { mergeAttachmentMetadata, readAttachmentMetadata } from '../../lib/metadata'\nimport {\n attachmentsTag,\n transferAttachmentsRequestSchema,\n transferAttachmentsResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst transferSchema = z.object({\n entityId: z.string().min(1),\n attachmentIds: z.array(z.string().uuid()).min(1),\n fromRecordId: z.string().min(1).optional(),\n toRecordId: z.string().min(1),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { LockMode } from '@mikro-orm/core'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Attachment } from '../../data/entities'\nimport { createAttachmentAccessContext } from '../../lib/access-runner'\nimport { assertAttachmentOwnerAccess } from '../../lib/access-query'\nimport { withAttachmentAccessErrors, throwAttachmentAccessError } from '../../lib/access-errors'\nimport { prepareAttachmentMutation } from '../../lib/access-mutation'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { mergeAttachmentMetadata, readAttachmentMetadata } from '../../lib/metadata'\nimport {\n attachmentsTag,\n transferAttachmentsRequestSchema,\n transferAttachmentsResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst transferSchema = z.object({\n entityId: z.string().min(1),\n attachmentIds: z.array(z.string().uuid()).min(1),\n fromRecordId: z.string().min(1).optional(),\n toRecordId: z.string().min(1),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\nasync function transferAttachments(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const json = await readJsonSafe(req, null)\n const parsed = transferSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const guard = await prepareAttachmentMutation({ container, req, auth, operation: 'update', payload: parsed.data })\n const effective = transferSchema.safeParse(guard.modifiedPayload ?? parsed.data)\n if (!effective.success) return throwAttachmentAccessError(403)\n const { attachmentIds, entityId, fromRecordId } = effective.data\n const filters: Record<string, unknown> = {\n id: { $in: [...new Set(attachmentIds)].sort((left, right) => left < right ? -1 : left > right ? 1 : 0) }, entityId,\n tenantId: auth.tenantId, organizationId: auth.orgId,\n ...(fromRecordId ? { recordId: fromRecordId } : {}),\n }\n const accessContext = createAttachmentAccessContext(container)\n const outcome = await em.transactional(async (tx) => {\n const records = await findWithDecryption(tx, Attachment, filters, {\n orderBy: { id: 'asc' }, lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (records.length !== new Set(attachmentIds).size) return throwAttachmentAccessError(404)\n const changes = []\n for (const record of records) {\n const recordPayload = { ...effective.data, attachmentIds: [record.id] }\n const recordGuard = await prepareAttachmentMutation({\n container, req, auth, recordId: record.id, operation: 'update', payload: recordPayload,\n })\n const recordEffective = transferSchema.safeParse(recordGuard.modifiedPayload ?? recordPayload)\n if (!recordEffective.success\n || recordEffective.data.attachmentIds.length !== 1\n || recordEffective.data.attachmentIds[0] !== record.id\n || recordEffective.data.entityId !== record.entityId\n || (recordEffective.data.fromRecordId && recordEffective.data.fromRecordId !== record.recordId)) {\n return throwAttachmentAccessError(403)\n }\n const destinationId = recordEffective.data.toRecordId\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const assignments = metadata.assignments?.map((assignment) => (\n assignment.type === entityId && assignment.id === (fromRecordId ?? record.recordId)\n ? { ...assignment, id: destinationId } : assignment\n )) ?? []\n const storageMetadata = mergeAttachmentMetadata(record.storageMetadata, { assignments })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: 'reassign', persistProtection: true })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, recordId: destinationId, storageMetadata }, action: 'reassign' })\n changes.push({ record, storageMetadata, destinationId, guard: recordGuard })\n }\n for (const { record, storageMetadata, destinationId } of changes) {\n record.recordId = destinationId\n record.storageMetadata = storageMetadata\n }\n await tx.flush()\n return { updated: records.length, guards: changes.map((change) => change.guard) }\n })\n await guard.runAfterSuccess()\n for (const recordGuard of outcome.guards) await recordGuard.runAfterSuccess()\n return NextResponse.json({ ok: true, updated: outcome.updated })\n}\n\nexport const POST = withAttachmentAccessErrors(transferAttachments)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Transfer attachments between records',\n methods: {\n POST: {\n summary: 'Transfer attachments to different record',\n description: 'Transfers one or more attachments from one record to another within the same entity type. Updates attachment assignments and metadata to reflect the new record.',\n requestBody: {\n contentType: 'application/json',\n schema: transferAttachmentsRequestSchema,\n },\n responses: [\n { status: 200, description: 'Attachments transferred successfully', schema: transferAttachmentsResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachments not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Attachment model missing', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["metadata"]
|
|
7
7
|
}
|
|
@@ -39,6 +39,9 @@ __decorateClass([
|
|
|
39
39
|
__decorateClass([
|
|
40
40
|
Property({ name: "config_json", type: "json", nullable: true })
|
|
41
41
|
], AttachmentPartition.prototype, "configJson", 2);
|
|
42
|
+
__decorateClass([
|
|
43
|
+
Property({ name: "access_resolver_requirements", type: "jsonb", nullable: true })
|
|
44
|
+
], AttachmentPartition.prototype, "accessResolverRequirements", 2);
|
|
42
45
|
__decorateClass([
|
|
43
46
|
Property({ name: "is_public", type: "boolean", default: false })
|
|
44
47
|
], AttachmentPartition.prototype, "isPublic", 2);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/data/entities.ts"],
|
|
4
|
-
"sourcesContent": ["import { BigIntType, OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\nimport { resolveDefaultAttachmentOcrEnabled } from '../lib/ocrConfig'\n\n@Entity({ tableName: 'attachment_partitions' })\n@Unique({ name: 'attachment_partitions_code_unique', properties: ['code'] })\nexport class AttachmentPartition {\n [OptionalProps]?: 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n code!: string\n\n @Property({ type: 'text' })\n title!: string\n\n @Property({ type: 'text', nullable: true })\n description?: string | null\n\n @Property({ name: 'storage_driver', type: 'text', default: 'local' })\n storageDriver: string = 'local'\n\n @Property({ name: 'config_json', type: 'json', nullable: true })\n configJson?: Record<string, unknown> | null\n\n @Property({ name: 'is_public', type: 'boolean', default: false })\n isPublic: boolean = false\n\n @Property({ name: 'requires_ocr', type: 'boolean', default: resolveDefaultAttachmentOcrEnabled() })\n requiresOcr: boolean = resolveDefaultAttachmentOcrEnabled()\n\n @Property({ name: 'ocr_model', type: 'text', nullable: true })\n ocrModel?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n @Index({ name: 'attachment_partitions_tenant_idx' })\n tenantId?: string | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'attachments' })\n@Index({ name: 'attachments_storage_reference_idx', properties: ['partitionCode', 'storagePath'] })\nexport class Attachment {\n [OptionalProps]?: 'createdAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'record_id', type: 'text' })\n @Index({ name: 'attachments_entity_record_idx' })\n recordId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'partition_code', type: 'text' })\n @Index({ name: 'attachments_partition_code_idx' })\n partitionCode!: string\n\n @Property({ name: 'file_name', type: 'text' })\n fileName!: string\n\n @Property({ name: 'mime_type', type: 'text' })\n mimeType!: string\n\n @Property({ name: 'file_size', type: 'int' })\n fileSize!: number\n\n @Property({ name: 'storage_driver', type: 'text', default: 'local' })\n storageDriver: string = 'local'\n\n @Property({ name: 'storage_path', type: 'text' })\n storagePath!: string\n\n @Property({ name: 'storage_metadata', type: 'jsonb', nullable: true })\n storageMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'url', type: 'text' })\n url!: string\n\n @Property({ name: 'content', type: 'text', nullable: true })\n content: string | null = null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n}\n\n@Entity({ tableName: 'attachment_quota_reservations' })\n@Unique({\n name: 'attachment_quota_reservations_scope_path_unique',\n properties: ['tenantId', 'storageDriver', 'storagePath'],\n})\n@Index({\n name: 'attachment_quota_reservations_tenant_status_idx',\n properties: ['tenantId', 'status'],\n})\nexport class AttachmentQuotaReservation {\n [OptionalProps]?: 'actualBytes' | 'createdAt' | 'status' | 'updatedAt' | 'uploadTokenHash'\n\n @PrimaryKey({ type: 'uuid' })\n id!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n /** Backed by a `bigint` column; hydrate as `number` (safe under Number.MAX_SAFE_INTEGER for attachment byte counts) instead of MikroORM's default JS `bigint`. */\n @Property({ name: 'reserved_bytes', type: new BigIntType('number') })\n reservedBytes!: number\n\n /** Backed by a `bigint` column; hydrate as `number`, not MikroORM's default JS `bigint`. */\n @Property({ name: 'actual_bytes', type: new BigIntType('number'), nullable: true })\n actualBytes?: number | null\n\n @Property({ type: 'text', default: 'reserved' })\n status: 'reserved' | 'storing' | 'stored' | 'recovering' | 'committed' = 'reserved'\n\n @Property({ type: 'text' })\n source!: string\n\n @Property({ name: 'storage_driver', type: 'text' })\n storageDriver!: string\n\n @Property({ name: 'partition_code', type: 'text', nullable: true })\n partitionCode?: string | null\n\n @Property({ name: 'storage_path', type: 'text' })\n storagePath!: string\n\n @Property({ name: 'lease_token', type: 'uuid' })\n leaseToken!: string\n\n @Property({ name: 'upload_token_hash', type: 'text', nullable: true })\n uploadTokenHash?: string | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n @Index({ name: 'attachment_quota_reservations_expires_idx' })\n expiresAt?: Date | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;AAAA,SAAS,YAAY,qBAAqB;AAC1C,SAAS,QAAQ,OAAO,YAAY,UAAU,cAAc;AAC5D,SAAS,0CAA0C;
|
|
4
|
+
"sourcesContent": ["import { BigIntType, OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\nimport { resolveDefaultAttachmentOcrEnabled } from '../lib/ocrConfig'\nimport type { AttachmentAccessRequirement } from '../lib/access-types'\n\n@Entity({ tableName: 'attachment_partitions' })\n@Unique({ name: 'attachment_partitions_code_unique', properties: ['code'] })\nexport class AttachmentPartition {\n [OptionalProps]?: 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n code!: string\n\n @Property({ type: 'text' })\n title!: string\n\n @Property({ type: 'text', nullable: true })\n description?: string | null\n\n @Property({ name: 'storage_driver', type: 'text', default: 'local' })\n storageDriver: string = 'local'\n\n @Property({ name: 'config_json', type: 'json', nullable: true })\n configJson?: Record<string, unknown> | null\n\n @Property({ name: 'access_resolver_requirements', type: 'jsonb', nullable: true })\n accessResolverRequirements?: AttachmentAccessRequirement[] | null\n\n @Property({ name: 'is_public', type: 'boolean', default: false })\n isPublic: boolean = false\n\n @Property({ name: 'requires_ocr', type: 'boolean', default: resolveDefaultAttachmentOcrEnabled() })\n requiresOcr: boolean = resolveDefaultAttachmentOcrEnabled()\n\n @Property({ name: 'ocr_model', type: 'text', nullable: true })\n ocrModel?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n @Index({ name: 'attachment_partitions_tenant_idx' })\n tenantId?: string | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'attachments' })\n@Index({ name: 'attachments_storage_reference_idx', properties: ['partitionCode', 'storagePath'] })\nexport class Attachment {\n [OptionalProps]?: 'createdAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'record_id', type: 'text' })\n @Index({ name: 'attachments_entity_record_idx' })\n recordId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'partition_code', type: 'text' })\n @Index({ name: 'attachments_partition_code_idx' })\n partitionCode!: string\n\n @Property({ name: 'file_name', type: 'text' })\n fileName!: string\n\n @Property({ name: 'mime_type', type: 'text' })\n mimeType!: string\n\n @Property({ name: 'file_size', type: 'int' })\n fileSize!: number\n\n @Property({ name: 'storage_driver', type: 'text', default: 'local' })\n storageDriver: string = 'local'\n\n @Property({ name: 'storage_path', type: 'text' })\n storagePath!: string\n\n @Property({ name: 'storage_metadata', type: 'jsonb', nullable: true })\n storageMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'url', type: 'text' })\n url!: string\n\n @Property({ name: 'content', type: 'text', nullable: true })\n content: string | null = null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n}\n\n@Entity({ tableName: 'attachment_quota_reservations' })\n@Unique({\n name: 'attachment_quota_reservations_scope_path_unique',\n properties: ['tenantId', 'storageDriver', 'storagePath'],\n})\n@Index({\n name: 'attachment_quota_reservations_tenant_status_idx',\n properties: ['tenantId', 'status'],\n})\nexport class AttachmentQuotaReservation {\n [OptionalProps]?: 'actualBytes' | 'createdAt' | 'status' | 'updatedAt' | 'uploadTokenHash'\n\n @PrimaryKey({ type: 'uuid' })\n id!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n /** Backed by a `bigint` column; hydrate as `number` (safe under Number.MAX_SAFE_INTEGER for attachment byte counts) instead of MikroORM's default JS `bigint`. */\n @Property({ name: 'reserved_bytes', type: new BigIntType('number') })\n reservedBytes!: number\n\n /** Backed by a `bigint` column; hydrate as `number`, not MikroORM's default JS `bigint`. */\n @Property({ name: 'actual_bytes', type: new BigIntType('number'), nullable: true })\n actualBytes?: number | null\n\n @Property({ type: 'text', default: 'reserved' })\n status: 'reserved' | 'storing' | 'stored' | 'recovering' | 'committed' = 'reserved'\n\n @Property({ type: 'text' })\n source!: string\n\n @Property({ name: 'storage_driver', type: 'text' })\n storageDriver!: string\n\n @Property({ name: 'partition_code', type: 'text', nullable: true })\n partitionCode?: string | null\n\n @Property({ name: 'storage_path', type: 'text' })\n storagePath!: string\n\n @Property({ name: 'lease_token', type: 'uuid' })\n leaseToken!: string\n\n @Property({ name: 'upload_token_hash', type: 'text', nullable: true })\n uploadTokenHash?: string | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n @Index({ name: 'attachment_quota_reservations_expires_idx' })\n expiresAt?: Date | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const attachmentAccessActionSchema = z.enum(["read", "render", "metadata", "reassign", "delete", "export"]);
|
|
3
|
+
const attachmentAccessSubjectFeaturesSchema = z.array(z.string());
|
|
4
|
+
const attachmentAccessSelectorSchema = z.string().min(1).max(200).regex(/^(?:\*|[^*\s]+(?:\.\*)?)$/);
|
|
5
|
+
const resolverIdSchema = z.string().min(3).max(200).regex(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_.-]+$/);
|
|
6
|
+
const attachmentAccessRequirementSchema = z.object({
|
|
7
|
+
resolverId: resolverIdSchema,
|
|
8
|
+
targetEntity: attachmentAccessSelectorSchema
|
|
9
|
+
}).strict();
|
|
10
|
+
const protectedAttachmentTargetSchema = attachmentAccessRequirementSchema.extend({
|
|
11
|
+
targetPartition: attachmentAccessSelectorSchema
|
|
12
|
+
});
|
|
13
|
+
const attachmentAccessResolverSchema = z.object({
|
|
14
|
+
id: resolverIdSchema,
|
|
15
|
+
targetPartition: attachmentAccessSelectorSchema,
|
|
16
|
+
targetEntity: attachmentAccessSelectorSchema.optional(),
|
|
17
|
+
actions: z.array(attachmentAccessActionSchema).min(1).optional(),
|
|
18
|
+
priority: z.number().finite().optional(),
|
|
19
|
+
timeoutMs: z.number().int().min(1).max(15e3).optional(),
|
|
20
|
+
resolve: z.custom((value) => typeof value === "function")
|
|
21
|
+
}).strict();
|
|
22
|
+
const attachmentAccessRequirementsSchema = z.array(attachmentAccessRequirementSchema);
|
|
23
|
+
const attachmentAccessDecisionSchema = z.discriminatedUnion("ok", [
|
|
24
|
+
z.object({ ok: z.literal(true) }).strict(),
|
|
25
|
+
z.object({
|
|
26
|
+
ok: z.literal(false),
|
|
27
|
+
status: z.union([z.literal(401), z.literal(403), z.literal(404), z.literal(504)]),
|
|
28
|
+
reason: z.string().min(1).max(200).regex(/^[a-zA-Z0-9_.:-]+$/)
|
|
29
|
+
}).strict()
|
|
30
|
+
]);
|
|
31
|
+
export {
|
|
32
|
+
attachmentAccessActionSchema,
|
|
33
|
+
attachmentAccessDecisionSchema,
|
|
34
|
+
attachmentAccessRequirementSchema,
|
|
35
|
+
attachmentAccessRequirementsSchema,
|
|
36
|
+
attachmentAccessResolverSchema,
|
|
37
|
+
attachmentAccessSelectorSchema,
|
|
38
|
+
attachmentAccessSubjectFeaturesSchema,
|
|
39
|
+
protectedAttachmentTargetSchema
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=validators.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/attachments/data/validators.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { AttachmentAccessResolver } from '../lib/access-types'\n\nexport const attachmentAccessActionSchema = z.enum(['read', 'render', 'metadata', 'reassign', 'delete', 'export'])\nexport const attachmentAccessSubjectFeaturesSchema = z.array(z.string())\nexport const attachmentAccessSelectorSchema = z.string().min(1).max(200).regex(/^(?:\\*|[^*\\s]+(?:\\.\\*)?)$/)\nconst resolverIdSchema = z.string().min(3).max(200).regex(/^[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_.-]+$/)\n\nexport const attachmentAccessRequirementSchema = z.object({\n resolverId: resolverIdSchema,\n targetEntity: attachmentAccessSelectorSchema,\n}).strict()\n\nexport const protectedAttachmentTargetSchema = attachmentAccessRequirementSchema.extend({\n targetPartition: attachmentAccessSelectorSchema,\n})\n\nexport const attachmentAccessResolverSchema = z.object({\n id: resolverIdSchema,\n targetPartition: attachmentAccessSelectorSchema,\n targetEntity: attachmentAccessSelectorSchema.optional(),\n actions: z.array(attachmentAccessActionSchema).min(1).optional(),\n priority: z.number().finite().optional(),\n timeoutMs: z.number().int().min(1).max(15_000).optional(),\n resolve: z.custom<AttachmentAccessResolver['resolve']>((value) => typeof value === 'function'),\n}).strict()\n\nexport const attachmentAccessRequirementsSchema = z.array(attachmentAccessRequirementSchema)\nexport const attachmentAccessDecisionSchema = z.discriminatedUnion('ok', [\n z.object({ ok: z.literal(true) }).strict(),\n z.object({\n ok: z.literal(false),\n status: z.union([z.literal(401), z.literal(403), z.literal(404), z.literal(504)]),\n reason: z.string().min(1).max(200).regex(/^[a-zA-Z0-9_.:-]+$/),\n }).strict(),\n])\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAGX,MAAM,+BAA+B,EAAE,KAAK,CAAC,QAAQ,UAAU,YAAY,YAAY,UAAU,QAAQ,CAAC;AAC1G,MAAM,wCAAwC,EAAE,MAAM,EAAE,OAAO,CAAC;AAChE,MAAM,iCAAiC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,MAAM,2BAA2B;AAC1G,MAAM,mBAAmB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,MAAM,mCAAmC;AAEtF,MAAM,oCAAoC,EAAE,OAAO;AAAA,EACxD,YAAY;AAAA,EACZ,cAAc;AAChB,CAAC,EAAE,OAAO;AAEH,MAAM,kCAAkC,kCAAkC,OAAO;AAAA,EACtF,iBAAiB;AACnB,CAAC;AAEM,MAAM,iCAAiC,EAAE,OAAO;AAAA,EACrD,IAAI;AAAA,EACJ,iBAAiB;AAAA,EACjB,cAAc,+BAA+B,SAAS;AAAA,EACtD,SAAS,EAAE,MAAM,4BAA4B,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EAC/D,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EACvC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,IAAM,EAAE,SAAS;AAAA,EACxD,SAAS,EAAE,OAA4C,CAAC,UAAU,OAAO,UAAU,UAAU;AAC/F,CAAC,EAAE,OAAO;AAEH,MAAM,qCAAqC,EAAE,MAAM,iCAAiC;AACpF,MAAM,iCAAiC,EAAE,mBAAmB,MAAM;AAAA,EACvE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,EAAE,OAAO;AAAA,EACzC,EAAE,OAAO;AAAA,IACP,IAAI,EAAE,QAAQ,KAAK;AAAA,IACnB,QAAQ,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,GAAG,EAAE,QAAQ,GAAG,GAAG,EAAE,QAAQ,GAAG,GAAG,EAAE,QAAQ,GAAG,CAAC,CAAC;AAAA,IAChF,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,MAAM,oBAAoB;AAAA,EAC/D,CAAC,EAAE,OAAO;AACZ,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAttachmentAccessContext } from "./lib/access-runner.js";
|
|
1
2
|
import { asFunction, asValue } from "awilix";
|
|
2
3
|
import { StorageDriverFactory } from "./lib/drivers/driverFactory.js";
|
|
3
4
|
import { DefaultAttachmentService } from "./lib/attachment-service.js";
|
|
@@ -6,13 +7,14 @@ import { scheduleAttachmentQuotaRecovery } from "./lib/quota-recovery-queue.js";
|
|
|
6
7
|
import { AttachmentTargetAccessService } from "./lib/target-access-service.js";
|
|
7
8
|
import { ScopedAttachmentUploadService } from "./lib/scoped-upload-service.js";
|
|
8
9
|
function register(container) {
|
|
10
|
+
const accessContext = () => createAttachmentAccessContext(container);
|
|
9
11
|
container.register({
|
|
10
12
|
attachmentQuotaRecoveryScheduler: asValue(scheduleAttachmentQuotaRecovery),
|
|
11
13
|
attachmentQuotaService: asFunction(
|
|
12
14
|
({ em }) => createAttachmentQuotaService(em)
|
|
13
15
|
).scoped().proxy(),
|
|
14
16
|
attachmentTargetAccessService: asFunction(
|
|
15
|
-
({ em }) => new AttachmentTargetAccessService(em)
|
|
17
|
+
({ em }) => new AttachmentTargetAccessService(em, accessContext)
|
|
16
18
|
).scoped().proxy(),
|
|
17
19
|
attachmentScopedUploadService: asFunction(({
|
|
18
20
|
em,
|
|
@@ -37,7 +39,8 @@ function register(container) {
|
|
|
37
39
|
attachmentService: asFunction((cradle) => new DefaultAttachmentService(
|
|
38
40
|
cradle.em,
|
|
39
41
|
cradle.storageDriverFactory,
|
|
40
|
-
() => cradle.attachmentScopedUploadService ?? null
|
|
42
|
+
() => cradle.attachmentScopedUploadService ?? null,
|
|
43
|
+
accessContext
|
|
41
44
|
)).scoped().proxy()
|
|
42
45
|
});
|
|
43
46
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/attachments/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asFunction, asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { StorageDriverFactory } from './lib/drivers/driverFactory'\nimport { DefaultAttachmentService } from './lib/attachment-service'\nimport { createAttachmentQuotaService, type AttachmentQuotaService } from './lib/quota-service'\nimport { scheduleAttachmentQuotaRecovery } from './lib/quota-recovery-queue'\nimport { AttachmentTargetAccessService } from './lib/target-access-service'\nimport { ScopedAttachmentUploadService } from './lib/scoped-upload-service'\n\nexport function register(container: AppContainer) {\n container.register({\n attachmentQuotaRecoveryScheduler: asValue(scheduleAttachmentQuotaRecovery),\n attachmentQuotaService: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>\n createAttachmentQuotaService(em),\n )\n .scoped()\n .proxy(),\n attachmentTargetAccessService: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>\n new AttachmentTargetAccessService(em),\n )\n .scoped()\n .proxy(),\n attachmentScopedUploadService: asFunction(({\n em,\n dataEngine,\n storageDriverFactory,\n attachmentQuotaService,\n attachmentQuotaRecoveryScheduler,\n }: {\n em: ConstructorParameters<typeof StorageDriverFactory>[0]\n dataEngine: DataEngine\n storageDriverFactory: StorageDriverFactory\n attachmentQuotaService: AttachmentQuotaService\n attachmentQuotaRecoveryScheduler: typeof scheduleAttachmentQuotaRecovery\n }) => new ScopedAttachmentUploadService({\n em,\n dataEngine,\n storageDriverFactory,\n attachmentQuotaService,\n attachmentQuotaRecoveryScheduler,\n }))\n .scoped()\n .proxy(),\n storageDriverFactory: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>\n new StorageDriverFactory(em),\n )\n .singleton()\n .proxy(),\n // The scoped upload service is handed over as a lazy resolver so module\n // uploads share the public attachment route's single fenced reservation\n // ledger, without dragging that service's own dependencies into every\n // container that merely constructs an `attachmentService`.\n attachmentService: asFunction((cradle: {\n em: ConstructorParameters<typeof DefaultAttachmentService>[0]\n storageDriverFactory: ConstructorParameters<typeof DefaultAttachmentService>[1]\n attachmentScopedUploadService: ScopedAttachmentUploadService\n }) => new DefaultAttachmentService(\n cradle.em,\n cradle.storageDriverFactory,\n () => cradle.attachmentScopedUploadService ?? null,\n ))\n .scoped()\n .proxy(),\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY,eAAe;AAGpC,SAAS,4BAA4B;AACrC,SAAS,gCAAgC;AACzC,SAAS,oCAAiE;AAC1E,SAAS,uCAAuC;AAChD,SAAS,qCAAqC;AAC9C,SAAS,qCAAqC;AAEvC,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,kCAAkC,QAAQ,+BAA+B;AAAA,IACzE,wBAAwB;AAAA,MAAW,CAAC,EAAE,GAAG,MACvC,6BAA6B,EAAE;AAAA,IACjC,EACG,OAAO,EACP,MAAM;AAAA,IACT,+BAA+B;AAAA,MAAW,CAAC,EAAE,GAAG,MAC9C,IAAI,8BAA8B,
|
|
4
|
+
"sourcesContent": ["import { createAttachmentAccessContext } from './lib/access-runner'\nimport { asFunction, asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { StorageDriverFactory } from './lib/drivers/driverFactory'\nimport { DefaultAttachmentService } from './lib/attachment-service'\nimport { createAttachmentQuotaService, type AttachmentQuotaService } from './lib/quota-service'\nimport { scheduleAttachmentQuotaRecovery } from './lib/quota-recovery-queue'\nimport { AttachmentTargetAccessService } from './lib/target-access-service'\nimport { ScopedAttachmentUploadService } from './lib/scoped-upload-service'\n\nexport function register(container: AppContainer) {\n const accessContext = () => createAttachmentAccessContext(container)\n container.register({\n attachmentQuotaRecoveryScheduler: asValue(scheduleAttachmentQuotaRecovery),\n attachmentQuotaService: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>\n createAttachmentQuotaService(em),\n )\n .scoped()\n .proxy(),\n attachmentTargetAccessService: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>\n new AttachmentTargetAccessService(em, accessContext),\n )\n .scoped()\n .proxy(),\n attachmentScopedUploadService: asFunction(({\n em,\n dataEngine,\n storageDriverFactory,\n attachmentQuotaService,\n attachmentQuotaRecoveryScheduler,\n }: {\n em: ConstructorParameters<typeof StorageDriverFactory>[0]\n dataEngine: DataEngine\n storageDriverFactory: StorageDriverFactory\n attachmentQuotaService: AttachmentQuotaService\n attachmentQuotaRecoveryScheduler: typeof scheduleAttachmentQuotaRecovery\n }) => new ScopedAttachmentUploadService({\n em,\n dataEngine,\n storageDriverFactory,\n attachmentQuotaService,\n attachmentQuotaRecoveryScheduler,\n }))\n .scoped()\n .proxy(),\n storageDriverFactory: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>\n new StorageDriverFactory(em),\n )\n .singleton()\n .proxy(),\n // The scoped upload service is handed over as a lazy resolver so module\n // uploads share the public attachment route's single fenced reservation\n // ledger, without dragging that service's own dependencies into every\n // container that merely constructs an `attachmentService`.\n attachmentService: asFunction((cradle: {\n em: ConstructorParameters<typeof DefaultAttachmentService>[0]\n storageDriverFactory: ConstructorParameters<typeof DefaultAttachmentService>[1]\n attachmentScopedUploadService: ScopedAttachmentUploadService\n }) => new DefaultAttachmentService(\n cradle.em,\n cradle.storageDriverFactory,\n () => cradle.attachmentScopedUploadService ?? null,\n accessContext,\n ))\n .scoped()\n .proxy(),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,qCAAqC;AAC9C,SAAS,YAAY,eAAe;AAGpC,SAAS,4BAA4B;AACrC,SAAS,gCAAgC;AACzC,SAAS,oCAAiE;AAC1E,SAAS,uCAAuC;AAChD,SAAS,qCAAqC;AAC9C,SAAS,qCAAqC;AAEvC,SAAS,SAAS,WAAyB;AAChD,QAAM,gBAAgB,MAAM,8BAA8B,SAAS;AACnE,YAAU,SAAS;AAAA,IACjB,kCAAkC,QAAQ,+BAA+B;AAAA,IACzE,wBAAwB;AAAA,MAAW,CAAC,EAAE,GAAG,MACvC,6BAA6B,EAAE;AAAA,IACjC,EACG,OAAO,EACP,MAAM;AAAA,IACT,+BAA+B;AAAA,MAAW,CAAC,EAAE,GAAG,MAC9C,IAAI,8BAA8B,IAAI,aAAa;AAAA,IACrD,EACG,OAAO,EACP,MAAM;AAAA,IACT,+BAA+B,WAAW,CAAC;AAAA,MACzC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,MAMM,IAAI,8BAA8B;AAAA,MACtC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC,CAAC,EACC,OAAO,EACP,MAAM;AAAA,IACT,sBAAsB;AAAA,MAAW,CAAC,EAAE,GAAG,MACrC,IAAI,qBAAqB,EAAE;AAAA,IAC7B,EACG,UAAU,EACV,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,IAKT,mBAAmB,WAAW,CAAC,WAIzB,IAAI;AAAA,MACR,OAAO;AAAA,MACP,OAAO;AAAA,MACP,MAAM,OAAO,iCAAiC;AAAA,MAC9C;AAAA,IACF,CAAC,EACE,OAAO,EACP,MAAM;AAAA,EACX,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const accessPlugin = {
|
|
2
|
+
id: "attachments.access-resolvers",
|
|
3
|
+
conventionFile: "data/attachment-access.ts",
|
|
4
|
+
importPrefix: "ATTACHMENT_ACCESS",
|
|
5
|
+
configExpr: (importName, moduleId) => `{ moduleId: '${moduleId}', resolvers: (${importName}.attachmentAccessResolvers ?? []), protectedTargets: (${importName}.protectedAttachmentTargets ?? []) }`,
|
|
6
|
+
outputFileName: "attachment-access.generated.ts",
|
|
7
|
+
buildOutput: ({ importSection, entriesLiteral }) => `// AUTO-GENERATED by mercato generate registry
|
|
8
|
+
import type { AttachmentAccessResolverEntry } from '@open-mercato/core/modules/attachments'
|
|
9
|
+
${importSection}
|
|
10
|
+
|
|
11
|
+
export const attachmentAccessEntries: AttachmentAccessResolverEntry[] = [
|
|
12
|
+
${entriesLiteral ? ` ${entriesLiteral}
|
|
13
|
+
` : ""}]
|
|
14
|
+
`,
|
|
15
|
+
bootstrapRegistration: {
|
|
16
|
+
entriesExportName: "attachmentAccessEntries",
|
|
17
|
+
registrationImports: [
|
|
18
|
+
"import { registerAttachmentAccessResolvers } from '@open-mercato/core/modules/attachments/lib/access-registry'"
|
|
19
|
+
],
|
|
20
|
+
buildCall: (entries) => `registerAttachmentAccessResolvers(${entries})`
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const generatorPlugins = [accessPlugin];
|
|
24
|
+
var generators_default = generatorPlugins;
|
|
25
|
+
export {
|
|
26
|
+
generators_default as default,
|
|
27
|
+
generatorPlugins
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=generators.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/attachments/generators.ts"],
|
|
4
|
+
"sourcesContent": ["import type { GeneratorPlugin } from '@open-mercato/shared/modules/generators'\n\nconst accessPlugin: GeneratorPlugin = {\n id: 'attachments.access-resolvers',\n conventionFile: 'data/attachment-access.ts',\n importPrefix: 'ATTACHMENT_ACCESS',\n configExpr: (importName, moduleId) =>\n `{ moduleId: '${moduleId}', resolvers: (${importName}.attachmentAccessResolvers ?? []), protectedTargets: (${importName}.protectedAttachmentTargets ?? []) }`,\n outputFileName: 'attachment-access.generated.ts',\n buildOutput: ({ importSection, entriesLiteral }) => `// AUTO-GENERATED by mercato generate registry\nimport type { AttachmentAccessResolverEntry } from '@open-mercato/core/modules/attachments'\n${importSection}\n\nexport const attachmentAccessEntries: AttachmentAccessResolverEntry[] = [\n${entriesLiteral ? ` ${entriesLiteral}\\n` : ''}]\n`,\n bootstrapRegistration: {\n entriesExportName: 'attachmentAccessEntries',\n registrationImports: [\n \"import { registerAttachmentAccessResolvers } from '@open-mercato/core/modules/attachments/lib/access-registry'\",\n ],\n buildCall: (entries) => `registerAttachmentAccessResolvers(${entries})`,\n },\n}\n\nexport const generatorPlugins: GeneratorPlugin[] = [accessPlugin]\nexport default generatorPlugins\n"],
|
|
5
|
+
"mappings": "AAEA,MAAM,eAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,YAAY,CAAC,YAAY,aACvB,gBAAgB,QAAQ,kBAAkB,UAAU,yDAAyD,UAAU;AAAA,EACzH,gBAAgB;AAAA,EAChB,aAAa,CAAC,EAAE,eAAe,eAAe,MAAM;AAAA;AAAA,EAEpD,aAAa;AAAA;AAAA;AAAA,EAGb,iBAAiB,KAAK,cAAc;AAAA,IAAO,EAAE;AAAA;AAAA,EAE7C,uBAAuB;AAAA,IACrB,mBAAmB;AAAA,IACnB,qBAAqB;AAAA,MACnB;AAAA,IACF;AAAA,IACA,WAAW,CAAC,YAAY,qCAAqC,OAAO;AAAA,EACtE;AACF;AAEO,MAAM,mBAAsC,CAAC,YAAY;AAChE,IAAO,qBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -6,7 +6,14 @@ const metadata = {
|
|
|
6
6
|
author: "Open Mercato Team",
|
|
7
7
|
license: "MIT"
|
|
8
8
|
};
|
|
9
|
+
import { registerAttachmentAccessResolvers } from "./lib/access-registry.js";
|
|
10
|
+
import { syncAttachmentAccessProtection } from "./lib/access-protection.js";
|
|
11
|
+
import { createAttachmentAccessContext, evaluateAttachmentAccess } from "./lib/access-runner.js";
|
|
9
12
|
export {
|
|
10
|
-
|
|
13
|
+
createAttachmentAccessContext,
|
|
14
|
+
evaluateAttachmentAccess,
|
|
15
|
+
metadata,
|
|
16
|
+
registerAttachmentAccessResolvers,
|
|
17
|
+
syncAttachmentAccessProtection
|
|
11
18
|
};
|
|
12
19
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/attachments/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\nexport const metadata: ModuleInfo = {\n name: 'attachments',\n title: 'Attachments',\n version: '0.1.0',\n description: 'File attachments and media management.',\n author: 'Open Mercato Team',\n license: 'MIT',\n}\n\nexport {\n type AttachmentOwner,\n type AttachmentProviderCleanup,\n type AttachmentService,\n type CreatedScopedAttachment,\n type CreateScopedAttachmentInput,\n type ReadScopedAttachmentInput,\n type ReadScopedAttachmentResult,\n type ReleaseScopedAttachmentInput,\n} from './lib/attachment-service'\n"],
|
|
5
|
-
"mappings": "AAEO,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AACX;",
|
|
4
|
+
"sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\nexport const metadata: ModuleInfo = {\n name: 'attachments',\n title: 'Attachments',\n version: '0.1.0',\n description: 'File attachments and media management.',\n author: 'Open Mercato Team',\n license: 'MIT',\n}\n\nexport {\n type AttachmentOwner,\n type AttachmentProviderCleanup,\n type AttachmentService,\n type CreatedScopedAttachment,\n type CreateScopedAttachmentInput,\n type ReadScopedAttachmentInput,\n type ReadScopedAttachmentResult,\n type ReleaseScopedAttachmentInput,\n} from './lib/attachment-service'\n\nexport type {\n AttachmentAccessAction,\n AttachmentAccessDecision,\n AttachmentAccessInput,\n AttachmentAccessPartition,\n AttachmentAccessRecord,\n AttachmentAccessRequirement,\n AttachmentAccessResolver,\n AttachmentAccessResolverEntry,\n AttachmentAccessSubject,\n AttachmentAccessTarget,\n ProtectedAttachmentTarget,\n} from './lib/access-types'\nexport { registerAttachmentAccessResolvers } from './lib/access-registry'\nexport { syncAttachmentAccessProtection } from './lib/access-protection'\nexport { createAttachmentAccessContext, evaluateAttachmentAccess } from './lib/access-runner'\nexport type { AttachmentAccessContext, AttachmentAccessResult } from './lib/access-runner'\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AACX;AA0BA,SAAS,yCAAyC;AAClD,SAAS,sCAAsC;AAC/C,SAAS,+BAA+B,gCAAgC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
2
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
3
|
+
async function attachmentAccessErrorBody(status) {
|
|
4
|
+
const { t } = await resolveTranslations();
|
|
5
|
+
const key = status === 401 ? "unauthorized" : status === 404 ? "notFound" : status === 504 ? "timeout" : "forbidden";
|
|
6
|
+
return { error: t(`attachments.access.${key}`) };
|
|
7
|
+
}
|
|
8
|
+
async function attachmentAccessErrorResponse(decision) {
|
|
9
|
+
return Response.json(await attachmentAccessErrorBody(decision.status), {
|
|
10
|
+
status: decision.status,
|
|
11
|
+
headers: { "Cache-Control": "private, no-store" }
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
async function throwAttachmentAccessError(status) {
|
|
15
|
+
throw new CrudHttpError(status, await attachmentAccessErrorBody(status));
|
|
16
|
+
}
|
|
17
|
+
function withAttachmentAccessErrors(handler) {
|
|
18
|
+
return async (...args) => {
|
|
19
|
+
try {
|
|
20
|
+
return await handler(...args);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (isCrudHttpError(error)) {
|
|
23
|
+
return Response.json(error.body, { status: error.status, headers: { "Cache-Control": "private, no-store" } });
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
attachmentAccessErrorBody,
|
|
31
|
+
attachmentAccessErrorResponse,
|
|
32
|
+
throwAttachmentAccessError,
|
|
33
|
+
withAttachmentAccessErrors
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=access-errors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/attachments/lib/access-errors.ts"],
|
|
4
|
+
"sourcesContent": ["import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { AttachmentAccessDecision } from './access-types'\n\nexport async function attachmentAccessErrorBody(status: number): Promise<{ error: string }> {\n const { t } = await resolveTranslations()\n const key = status === 401 ? 'unauthorized' : status === 404 ? 'notFound' : status === 504 ? 'timeout' : 'forbidden'\n return { error: t(`attachments.access.${key}`) }\n}\n\nexport async function attachmentAccessErrorResponse(decision: Extract<AttachmentAccessDecision, { ok: false }>): Promise<Response> {\n return Response.json(await attachmentAccessErrorBody(decision.status), {\n status: decision.status, headers: { 'Cache-Control': 'private, no-store' },\n })\n}\n\nexport async function throwAttachmentAccessError(status: number): Promise<never> {\n throw new CrudHttpError(status, await attachmentAccessErrorBody(status))\n}\n\nexport function withAttachmentAccessErrors<Arguments extends unknown[]>(handler: (...args: Arguments) => Promise<Response>) {\n return async (...args: Arguments): Promise<Response> => {\n try {\n return await handler(...args)\n } catch (error) {\n if (isCrudHttpError(error)) {\n return Response.json(error.body, { status: error.status, headers: { 'Cache-Control': 'private, no-store' } })\n }\n throw error\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe,uBAAuB;AAC/C,SAAS,2BAA2B;AAGpC,eAAsB,0BAA0B,QAA4C;AAC1F,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,QAAM,MAAM,WAAW,MAAM,iBAAiB,WAAW,MAAM,aAAa,WAAW,MAAM,YAAY;AACzG,SAAO,EAAE,OAAO,EAAE,sBAAsB,GAAG,EAAE,EAAE;AACjD;AAEA,eAAsB,8BAA8B,UAA+E;AACjI,SAAO,SAAS,KAAK,MAAM,0BAA0B,SAAS,MAAM,GAAG;AAAA,IACrE,QAAQ,SAAS;AAAA,IAAQ,SAAS,EAAE,iBAAiB,oBAAoB;AAAA,EAC3E,CAAC;AACH;AAEA,eAAsB,2BAA2B,QAAgC;AAC/E,QAAM,IAAI,cAAc,QAAQ,MAAM,0BAA0B,MAAM,CAAC;AACzE;AAEO,SAAS,2BAAwD,SAAoD;AAC1H,SAAO,UAAU,SAAuC;AACtD,QAAI;AACF,aAAO,MAAM,QAAQ,GAAG,IAAI;AAAA,IAC9B,SAAS,OAAO;AACd,UAAI,gBAAgB,KAAK,GAAG;AAC1B,eAAO,SAAS,KAAK,MAAM,MAAM,EAAE,QAAQ,MAAM,QAAQ,SAAS,EAAE,iBAAiB,oBAAoB,EAAE,CAAC;AAAA,MAC9G;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { throwAttachmentAccessError } from "./access-errors.js";
|
|
2
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
3
|
+
import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
|
|
4
|
+
async function prepareAttachmentMutation(input) {
|
|
5
|
+
if (!input.auth.tenantId) return throwAttachmentAccessError(401);
|
|
6
|
+
const guard = await runRouteMutationGuards({
|
|
7
|
+
container: input.container,
|
|
8
|
+
req: input.req,
|
|
9
|
+
auth: { tenantId: input.auth.tenantId, organizationId: input.auth.orgId ?? null, userId: input.auth.sub },
|
|
10
|
+
input: {
|
|
11
|
+
resourceKind: "attachments:attachment",
|
|
12
|
+
resourceId: input.recordId,
|
|
13
|
+
operation: input.operation,
|
|
14
|
+
mutationPayload: input.payload
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
if (!guard.ok) throw new CrudHttpError(guard.errorStatus, guard.errorBody);
|
|
18
|
+
return guard;
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
prepareAttachmentMutation
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=access-mutation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/attachments/lib/access-mutation.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { throwAttachmentAccessError } from './access-errors'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\n\nexport async function prepareAttachmentMutation(input: {\n container: AwilixContainer\n req: Request\n auth: NonNullable<AuthContext>\n recordId?: string\n operation: 'update' | 'delete'\n payload?: Record<string, unknown>\n}) {\n if (!input.auth.tenantId) return throwAttachmentAccessError(401)\n const guard = await runRouteMutationGuards({\n container: input.container, req: input.req,\n auth: { tenantId: input.auth.tenantId, organizationId: input.auth.orgId ?? null, userId: input.auth.sub },\n input: {\n resourceKind: 'attachments:attachment', resourceId: input.recordId,\n operation: input.operation, mutationPayload: input.payload,\n },\n })\n if (!guard.ok) throw new CrudHttpError(guard.errorStatus, guard.errorBody)\n return guard\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,kCAAkC;AAC3C,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AAEvC,eAAsB,0BAA0B,OAO7C;AACD,MAAI,CAAC,MAAM,KAAK,SAAU,QAAO,2BAA2B,GAAG;AAC/D,QAAM,QAAQ,MAAM,uBAAuB;AAAA,IACzC,WAAW,MAAM;AAAA,IAAW,KAAK,MAAM;AAAA,IACvC,MAAM,EAAE,UAAU,MAAM,KAAK,UAAU,gBAAgB,MAAM,KAAK,SAAS,MAAM,QAAQ,MAAM,KAAK,IAAI;AAAA,IACxG,OAAO;AAAA,MACL,cAAc;AAAA,MAA0B,YAAY,MAAM;AAAA,MAC1D,WAAW,MAAM;AAAA,MAAW,iBAAiB,MAAM;AAAA,IACrD;AAAA,EACF,CAAC;AACD,MAAI,CAAC,MAAM,GAAI,OAAM,IAAI,cAAc,MAAM,aAAa,MAAM,SAAS;AACzE,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { LockMode } from "@mikro-orm/core";
|
|
2
|
+
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { AttachmentPartition } from "../data/entities.js";
|
|
4
|
+
import { attachmentAccessRequirementsSchema } from "../data/validators.js";
|
|
5
|
+
import { getAttachmentAccessRegistry, matchesAttachmentAccessSelector } from "./access-registry.js";
|
|
6
|
+
function getDeclaredAttachmentAccessRequirements(partitionCode) {
|
|
7
|
+
const registry = getAttachmentAccessRegistry();
|
|
8
|
+
if (registry.invalid) throw new Error("[internal] Attachment access registry is invalid");
|
|
9
|
+
return registry.protectedTargets.filter((target) => matchesAttachmentAccessSelector(target.targetPartition, partitionCode)).map(({ resolverId, targetEntity }) => ({ resolverId, targetEntity }));
|
|
10
|
+
}
|
|
11
|
+
function mergeAttachmentAccessRequirements(current, required) {
|
|
12
|
+
const existing = attachmentAccessRequirementsSchema.parse(current ?? []);
|
|
13
|
+
const union = /* @__PURE__ */ new Map();
|
|
14
|
+
for (const requirement of [...existing, ...required]) {
|
|
15
|
+
union.set(JSON.stringify([requirement.resolverId, requirement.targetEntity]), { ...requirement });
|
|
16
|
+
}
|
|
17
|
+
return [...union.values()];
|
|
18
|
+
}
|
|
19
|
+
async function syncAttachmentAccessProtection(em, options = {}) {
|
|
20
|
+
const registry = getAttachmentAccessRegistry();
|
|
21
|
+
if (registry.invalid) throw new Error("[internal] Attachment access registry is invalid");
|
|
22
|
+
if (!registry.protectedTargets.length) return 0;
|
|
23
|
+
const isolated = em.fork({ clear: true, useContext: false });
|
|
24
|
+
const candidates = await findWithDecryption(
|
|
25
|
+
isolated,
|
|
26
|
+
AttachmentPartition,
|
|
27
|
+
options.partitionCode ? { code: options.partitionCode } : {},
|
|
28
|
+
{ orderBy: { id: "asc" } },
|
|
29
|
+
{ tenantId: null, organizationId: null }
|
|
30
|
+
);
|
|
31
|
+
if (options.partitionCode && getDeclaredAttachmentAccessRequirements(options.partitionCode).length && !candidates.length) {
|
|
32
|
+
throw new Error("[internal] Protected attachment partition is not durably configured");
|
|
33
|
+
}
|
|
34
|
+
const pending = candidates.filter((partition) => {
|
|
35
|
+
const required = getDeclaredAttachmentAccessRequirements(partition.code);
|
|
36
|
+
if (!required.length) return false;
|
|
37
|
+
const existing = attachmentAccessRequirementsSchema.parse(partition.accessResolverRequirements ?? []);
|
|
38
|
+
return JSON.stringify(mergeAttachmentAccessRequirements(existing, required)) !== JSON.stringify(existing);
|
|
39
|
+
});
|
|
40
|
+
if (!pending.length) return 0;
|
|
41
|
+
return isolated.transactional(async (transaction) => {
|
|
42
|
+
let changed = 0;
|
|
43
|
+
for (const candidate of pending) {
|
|
44
|
+
const partition = await findOneWithDecryption(transaction, AttachmentPartition, { id: candidate.id }, {
|
|
45
|
+
lockMode: LockMode.PESSIMISTIC_WRITE,
|
|
46
|
+
refresh: true
|
|
47
|
+
}, { tenantId: null, organizationId: null });
|
|
48
|
+
if (!partition) throw new Error("[internal] Attachment partition disappeared during access protection synchronization");
|
|
49
|
+
const existing = attachmentAccessRequirementsSchema.parse(partition.accessResolverRequirements ?? []);
|
|
50
|
+
const merged = mergeAttachmentAccessRequirements(existing, getDeclaredAttachmentAccessRequirements(partition.code));
|
|
51
|
+
if (JSON.stringify(merged) === JSON.stringify(existing)) continue;
|
|
52
|
+
partition.accessResolverRequirements = merged;
|
|
53
|
+
changed += 1;
|
|
54
|
+
}
|
|
55
|
+
if (changed) await transaction.flush();
|
|
56
|
+
return changed;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function ensureAttachmentPartitionProtection(em, partition) {
|
|
60
|
+
const required = getDeclaredAttachmentAccessRequirements(partition.code);
|
|
61
|
+
if (!required.length) return;
|
|
62
|
+
const existing = attachmentAccessRequirementsSchema.parse(partition.accessResolverRequirements ?? []);
|
|
63
|
+
const merged = mergeAttachmentAccessRequirements(existing, required);
|
|
64
|
+
if (JSON.stringify(existing) === JSON.stringify(merged)) return;
|
|
65
|
+
await syncAttachmentAccessProtection(em, { partitionCode: partition.code });
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
ensureAttachmentPartitionProtection,
|
|
69
|
+
getDeclaredAttachmentAccessRequirements,
|
|
70
|
+
mergeAttachmentAccessRequirements,
|
|
71
|
+
syncAttachmentAccessProtection
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=access-protection.js.map
|