@open-mercato/core 0.10.1-develop.7847.1.7d83dc5794 → 0.10.1-develop.7862.1.ee520f7662
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/modules/attachments/api/file/[id]/route.js +6 -1
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +6 -1
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +16 -1
- package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +11 -3
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/cli.js +2 -0
- package/dist/modules/attachments/cli.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +11 -0
- package/dist/modules/attachments/data/validators.js.map +2 -2
- package/dist/modules/attachments/index.js +10 -0
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-query.js +5 -1
- package/dist/modules/attachments/lib/access-query.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/drivers/driverFactory.js +54 -33
- package/dist/modules/attachments/lib/drivers/driverFactory.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/index.js +11 -1
- package/dist/modules/attachments/lib/drivers/index.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/storageValidation.js +85 -0
- package/dist/modules/attachments/lib/drivers/storageValidation.js.map +7 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js +3 -1
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/storageErrors.js +22 -0
- package/dist/modules/attachments/lib/storageErrors.js.map +7 -0
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +5 -4
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +3 -3
- package/dist/modules/customers/api/companies/[id]/route.js +4 -9
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +4 -9
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskDialog.js +2 -0
- package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +23 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js +1 -4
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js.map +2 -2
- package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js +8 -1
- package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +18 -3
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +2 -2
- package/jest.config.cjs +1 -1
- package/package.json +8 -8
- package/src/modules/attachments/api/file/[id]/route.ts +7 -1
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -1
- package/src/modules/attachments/api/library/[id]/route.ts +14 -1
- package/src/modules/attachments/api/route.ts +11 -3
- package/src/modules/attachments/cli.ts +2 -0
- package/src/modules/attachments/data/validators.ts +11 -0
- package/src/modules/attachments/i18n/de.json +1 -0
- package/src/modules/attachments/i18n/en.json +1 -0
- package/src/modules/attachments/i18n/es.json +1 -0
- package/src/modules/attachments/i18n/ko.json +1 -0
- package/src/modules/attachments/i18n/pl.json +1 -0
- package/src/modules/attachments/index.ts +12 -0
- package/src/modules/attachments/lib/access-query.ts +5 -1
- package/src/modules/attachments/lib/createFromBuffer.ts +2 -0
- package/src/modules/attachments/lib/drivers/driverFactory.ts +79 -55
- package/src/modules/attachments/lib/drivers/index.ts +13 -0
- package/src/modules/attachments/lib/drivers/storageValidation.ts +114 -0
- package/src/modules/attachments/lib/scoped-upload-service.ts +3 -1
- package/src/modules/attachments/lib/storageErrors.ts +21 -0
- package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +3 -1
- package/src/modules/audit_logs/i18n/de.json +1 -0
- package/src/modules/audit_logs/i18n/en.json +1 -0
- package/src/modules/audit_logs/i18n/es.json +1 -0
- package/src/modules/audit_logs/i18n/ko.json +1 -0
- package/src/modules/audit_logs/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/AGENTS.md +1 -1
- package/src/modules/customers/api/companies/[id]/route.ts +6 -13
- package/src/modules/customers/api/people/[id]/route.ts +6 -13
- package/src/modules/customers/components/detail/TaskDialog.tsx +2 -0
- package/src/modules/customers/lib/todoCompatibility.ts +28 -0
- package/src/modules/customers/widgets/dashboard/customer-todos/widget.client.tsx +0 -3
- package/src/modules/sales/AGENTS.md +3 -3
- package/src/modules/warranty_claims/backend/components/WarrantyWorkspace.tsx +8 -1
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +22 -3
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { LocalStorageDriver } from "./localDriver.js";
|
|
2
2
|
import { LegacyPublicStorageDriver } from "./legacyPublicDriver.js";
|
|
3
3
|
import { AttachmentPartition } from "../../data/entities.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import {
|
|
5
|
+
AttachmentStorageConfigurationError,
|
|
6
|
+
assertAttachmentStoragePartitionExists,
|
|
7
|
+
getAttachmentStoragePolicy,
|
|
8
|
+
validateAttachmentStorageConfiguration
|
|
9
|
+
} from "./storageValidation.js";
|
|
10
|
+
const REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/AttachmentStorageDrivers");
|
|
11
|
+
function moduleRegistry() {
|
|
12
|
+
const registry = globalThis;
|
|
13
|
+
return registry[REGISTRY_KEY] ??= { drivers: /* @__PURE__ */ new Map(), enhancers: /* @__PURE__ */ new Map() };
|
|
14
|
+
}
|
|
8
15
|
function registerExternalStorageDriver(key, factory) {
|
|
9
|
-
|
|
16
|
+
moduleRegistry().drivers.set(key, factory);
|
|
10
17
|
}
|
|
11
18
|
function registerExternalCredentialEnhancer(key, enhancer) {
|
|
12
|
-
|
|
19
|
+
moduleRegistry().enhancers.set(key, enhancer);
|
|
13
20
|
}
|
|
14
21
|
class StorageDriverFactory {
|
|
15
22
|
constructor(em) {
|
|
16
23
|
this.em = em;
|
|
17
24
|
this.cache = /* @__PURE__ */ new Map();
|
|
18
|
-
this.localDriver = new LocalStorageDriver();
|
|
19
|
-
this.legacyPublicDriver = new LegacyPublicStorageDriver();
|
|
20
25
|
this.externalDrivers = /* @__PURE__ */ new Map();
|
|
21
26
|
this.credentialEnhancers = /* @__PURE__ */ new Map();
|
|
22
27
|
}
|
|
@@ -27,38 +32,54 @@ class StorageDriverFactory {
|
|
|
27
32
|
this.credentialEnhancers.set(key, enhancer);
|
|
28
33
|
}
|
|
29
34
|
resolveForAttachment(storageDriver, configJson) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return this.localDriver;
|
|
35
|
-
default: {
|
|
36
|
-
const externalFactory = this.externalDrivers.get(storageDriver) ?? moduleDriverRegistry.get(storageDriver)?.factory;
|
|
37
|
-
if (externalFactory) {
|
|
38
|
-
const cacheKey = `${storageDriver}:${JSON.stringify(configJson ?? {})}`;
|
|
39
|
-
const cached = this.cache.get(cacheKey);
|
|
40
|
-
if (cached) return cached;
|
|
41
|
-
const driver = externalFactory(configJson ?? {});
|
|
42
|
-
this.cache.set(cacheKey, driver);
|
|
43
|
-
return driver;
|
|
44
|
-
}
|
|
45
|
-
return this.localDriver;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
35
|
+
const config = configJson ?? {};
|
|
36
|
+
this.validate(storageDriver, config, "configured");
|
|
37
|
+
this.validate(storageDriver, config, "resolved");
|
|
38
|
+
return this.resolveValidated(storageDriver, config);
|
|
48
39
|
}
|
|
49
40
|
async resolveForPartition(partitionCode, scope) {
|
|
41
|
+
getAttachmentStoragePolicy();
|
|
50
42
|
const partition = await this.em.findOne(AttachmentPartition, { code: partitionCode });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
logger.debug("Resolving storage driver for partition", { partition: partitionCode, driverKey, moduleDrivers: [...moduleDriverRegistry.keys()], instanceDrivers: [...this.externalDrivers.keys()] });
|
|
43
|
+
assertAttachmentStoragePartitionExists(Boolean(partition));
|
|
44
|
+
if (!partition) return this.localDriver ??= new LocalStorageDriver();
|
|
54
45
|
let config = partition.configJson ?? {};
|
|
55
|
-
|
|
46
|
+
this.validate(partition.storageDriver, config, "configured", partitionCode, scope);
|
|
47
|
+
const driverKey = partition.storageDriver ?? "local";
|
|
48
|
+
const activeEnhancer = this.credentialEnhancers.get(driverKey) ?? moduleRegistry().enhancers.get(driverKey);
|
|
56
49
|
if (scope && activeEnhancer) {
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
try {
|
|
51
|
+
config = await activeEnhancer(config, scope);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (getAttachmentStoragePolicy() === "strict") {
|
|
54
|
+
throw new AttachmentStorageConfigurationError("enhancement_failed");
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
this.validate(driverKey, config, "resolved", partitionCode, scope);
|
|
59
|
+
const externalFactory = this.externalDrivers.get(driverKey) ?? moduleRegistry().drivers.get(driverKey);
|
|
59
60
|
if (externalFactory) return externalFactory(config);
|
|
61
|
+
} else {
|
|
62
|
+
this.validate(driverKey, config, "resolved", partitionCode, scope);
|
|
63
|
+
}
|
|
64
|
+
return this.resolveValidated(driverKey, config);
|
|
65
|
+
}
|
|
66
|
+
validate(driverKey, config, stage, partitionCode, scope) {
|
|
67
|
+
const registered = typeof driverKey === "string" && (driverKey === "local" || driverKey === "legacyPublic" || this.externalDrivers.has(driverKey) || moduleRegistry().drivers.has(driverKey));
|
|
68
|
+
validateAttachmentStorageConfiguration({ driverKey, config, stage, partitionCode, scope }, registered);
|
|
69
|
+
}
|
|
70
|
+
resolveValidated(driverKey, config) {
|
|
71
|
+
if (driverKey === "legacyPublic") return this.legacyPublicDriver ??= new LegacyPublicStorageDriver();
|
|
72
|
+
if (driverKey === "local") return this.localDriver ??= new LocalStorageDriver();
|
|
73
|
+
const externalFactory = this.externalDrivers.get(driverKey) ?? moduleRegistry().drivers.get(driverKey);
|
|
74
|
+
if (externalFactory) {
|
|
75
|
+
const cacheKey = `${driverKey}:${JSON.stringify(config)}`;
|
|
76
|
+
const cached = this.cache.get(cacheKey);
|
|
77
|
+
if (cached) return cached;
|
|
78
|
+
const driver = externalFactory(config);
|
|
79
|
+
this.cache.set(cacheKey, driver);
|
|
80
|
+
return driver;
|
|
60
81
|
}
|
|
61
|
-
return this.
|
|
82
|
+
return this.localDriver ??= new LocalStorageDriver();
|
|
62
83
|
}
|
|
63
84
|
}
|
|
64
85
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/attachments/lib/drivers/driverFactory.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { StorageDriver } from './types'\nimport { LocalStorageDriver } from './localDriver'\nimport { LegacyPublicStorageDriver } from './legacyPublicDriver'\nimport { AttachmentPartition } from '../../data/entities'\nimport {
|
|
5
|
-
"mappings": "AAEA,SAAS,0BAA0B;AACnC,SAAS,iCAAiC;AAC1C,SAAS,2BAA2B;AACpC,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { StorageDriver } from './types'\nimport { LocalStorageDriver } from './localDriver'\nimport { LegacyPublicStorageDriver } from './legacyPublicDriver'\nimport { AttachmentPartition } from '../../data/entities'\nimport {\n AttachmentStorageConfigurationError,\n assertAttachmentStoragePartitionExists,\n getAttachmentStoragePolicy,\n validateAttachmentStorageConfiguration,\n type AttachmentStorageScope,\n type AttachmentStorageValidationStage,\n} from './storageValidation'\n\ntype DriverScope = AttachmentStorageScope\ntype CredentialEnhancer = (config: Record<string, unknown>, scope: DriverScope) => Promise<Record<string, unknown>>\ntype DriverFactory = (config: Record<string, unknown>) => StorageDriver\n\ntype ModuleRegistry = {\n drivers: Map<string, DriverFactory>\n enhancers: Map<string, CredentialEnhancer>\n}\nconst REGISTRY_KEY = Symbol.for('@open-mercato/AttachmentStorageDrivers')\n\nfunction moduleRegistry(): ModuleRegistry {\n const registry = globalThis as typeof globalThis & { [REGISTRY_KEY]?: ModuleRegistry }\n return registry[REGISTRY_KEY] ??= { drivers: new Map(), enhancers: new Map() }\n}\n\nexport function registerExternalStorageDriver(key: string, factory: DriverFactory): void {\n moduleRegistry().drivers.set(key, factory)\n}\n\nexport function registerExternalCredentialEnhancer(key: string, enhancer: CredentialEnhancer): void {\n moduleRegistry().enhancers.set(key, enhancer)\n}\n\nexport class StorageDriverFactory {\n private readonly cache = new Map<string, StorageDriver>()\n private localDriver?: LocalStorageDriver\n private legacyPublicDriver?: LegacyPublicStorageDriver\n private readonly externalDrivers = new Map<string, DriverFactory>()\n private readonly credentialEnhancers = new Map<string, CredentialEnhancer>()\n\n constructor(private readonly em: EntityManager) {}\n\n registerDriver(key: string, factory: DriverFactory): void {\n this.externalDrivers.set(key, factory)\n }\n\n registerCredentialEnhancer(key: string, enhancer: CredentialEnhancer): void {\n this.credentialEnhancers.set(key, enhancer)\n }\n\n resolveForAttachment(storageDriver: string, configJson?: Record<string, unknown> | null): StorageDriver {\n const config = configJson ?? {}\n this.validate(storageDriver, config, 'configured')\n this.validate(storageDriver, config, 'resolved')\n return this.resolveValidated(storageDriver, config)\n }\n\n async resolveForPartition(partitionCode: string, scope?: DriverScope): Promise<StorageDriver> {\n getAttachmentStoragePolicy()\n const partition = await this.em.findOne(AttachmentPartition, { code: partitionCode })\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) return this.localDriver ??= new LocalStorageDriver()\n\n let config: Record<string, unknown> = partition.configJson ?? {}\n this.validate(partition.storageDriver, config, 'configured', partitionCode, scope)\n const driverKey = partition.storageDriver ?? 'local'\n const activeEnhancer = this.credentialEnhancers.get(driverKey) ?? moduleRegistry().enhancers.get(driverKey)\n if (scope && activeEnhancer) {\n try {\n config = await activeEnhancer(config, scope)\n } catch (error) {\n if (getAttachmentStoragePolicy() === 'strict') {\n throw new AttachmentStorageConfigurationError('enhancement_failed')\n }\n throw error\n }\n this.validate(driverKey, config, 'resolved', partitionCode, scope)\n const externalFactory = this.externalDrivers.get(driverKey) ?? moduleRegistry().drivers.get(driverKey)\n if (externalFactory) return externalFactory(config)\n } else {\n this.validate(driverKey, config, 'resolved', partitionCode, scope)\n }\n return this.resolveValidated(driverKey, config)\n }\n\n private validate(\n driverKey: unknown,\n config: unknown,\n stage: AttachmentStorageValidationStage,\n partitionCode?: string,\n scope?: DriverScope,\n ): void {\n const registered = typeof driverKey === 'string' && (\n driverKey === 'local' || driverKey === 'legacyPublic'\n || this.externalDrivers.has(driverKey) || moduleRegistry().drivers.has(driverKey)\n )\n validateAttachmentStorageConfiguration({ driverKey, config, stage, partitionCode, scope }, registered)\n }\n\n private resolveValidated(driverKey: string, config: Record<string, unknown>): StorageDriver {\n if (driverKey === 'legacyPublic') return this.legacyPublicDriver ??= new LegacyPublicStorageDriver()\n if (driverKey === 'local') return this.localDriver ??= new LocalStorageDriver()\n const externalFactory = this.externalDrivers.get(driverKey) ?? moduleRegistry().drivers.get(driverKey)\n if (externalFactory) {\n const cacheKey = `${driverKey}:${JSON.stringify(config)}`\n const cached = this.cache.get(cacheKey)\n if (cached) return cached\n const driver = externalFactory(config)\n this.cache.set(cacheKey, driver)\n return driver\n }\n return this.localDriver ??= new LocalStorageDriver()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { LocalStorageDriver } from "./localDriver.js";
|
|
2
2
|
import { LegacyPublicStorageDriver } from "./legacyPublicDriver.js";
|
|
3
3
|
import { StorageDriverFactory, registerExternalStorageDriver, registerExternalCredentialEnhancer } from "./driverFactory.js";
|
|
4
|
+
import {
|
|
5
|
+
AttachmentStorageConfigurationError,
|
|
6
|
+
getAttachmentStoragePolicy,
|
|
7
|
+
isAttachmentStorageConfigurationError,
|
|
8
|
+
registerStorageDriverValidator
|
|
9
|
+
} from "./storageValidation.js";
|
|
4
10
|
export {
|
|
11
|
+
AttachmentStorageConfigurationError,
|
|
5
12
|
LegacyPublicStorageDriver,
|
|
6
13
|
LocalStorageDriver,
|
|
7
14
|
StorageDriverFactory,
|
|
15
|
+
getAttachmentStoragePolicy,
|
|
16
|
+
isAttachmentStorageConfigurationError,
|
|
8
17
|
registerExternalCredentialEnhancer,
|
|
9
|
-
registerExternalStorageDriver
|
|
18
|
+
registerExternalStorageDriver,
|
|
19
|
+
registerStorageDriverValidator
|
|
10
20
|
};
|
|
11
21
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/attachments/lib/drivers/index.ts"],
|
|
4
|
-
"sourcesContent": ["export type { PrepareFilePayload, StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\nexport { LocalStorageDriver } from './localDriver'\nexport { LegacyPublicStorageDriver } from './legacyPublicDriver'\nexport { StorageDriverFactory, registerExternalStorageDriver, registerExternalCredentialEnhancer } from './driverFactory'\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,iCAAiC;AAC1C,SAAS,sBAAsB,+BAA+B,0CAA0C;",
|
|
4
|
+
"sourcesContent": ["export type { PrepareFilePayload, StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\nexport { LocalStorageDriver } from './localDriver'\nexport { LegacyPublicStorageDriver } from './legacyPublicDriver'\nexport { StorageDriverFactory, registerExternalStorageDriver, registerExternalCredentialEnhancer } from './driverFactory'\n\nexport {\n AttachmentStorageConfigurationError,\n getAttachmentStoragePolicy,\n isAttachmentStorageConfigurationError,\n registerStorageDriverValidator,\n type AttachmentStoragePolicy,\n type AttachmentStorageScope,\n type AttachmentStorageValidationStage,\n type AttachmentStorageValidationContext,\n type AttachmentStorageValidator,\n type AttachmentStorageConfigurationReason,\n} from './storageValidation'\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,iCAAiC;AAC1C,SAAS,sBAAsB,+BAA+B,0CAA0C;AAExG;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAOK;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
var _a, _b;
|
|
2
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
3
|
+
import { attachmentStorageConfigurationSchema, attachmentStorageScopeSchema } from "../../data/validators.js";
|
|
4
|
+
const ERROR_MARKER = /* @__PURE__ */ Symbol.for("@open-mercato/AttachmentStorageConfigurationError");
|
|
5
|
+
const VALIDATORS_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/AttachmentStorageValidators");
|
|
6
|
+
function validators() {
|
|
7
|
+
const registry = globalThis;
|
|
8
|
+
return registry[VALIDATORS_KEY] ??= /* @__PURE__ */ new Map();
|
|
9
|
+
}
|
|
10
|
+
class AttachmentStorageConfigurationError extends (_b = CrudHttpError, _a = ERROR_MARKER, _b) {
|
|
11
|
+
constructor(reason) {
|
|
12
|
+
super(503, {
|
|
13
|
+
error: "attachments.errors.storageConfigurationInvalid",
|
|
14
|
+
code: "ATTACHMENT_STORAGE_CONFIGURATION_INVALID",
|
|
15
|
+
reason
|
|
16
|
+
});
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
this[_a] = true;
|
|
19
|
+
this.code = "ATTACHMENT_STORAGE_CONFIGURATION_INVALID";
|
|
20
|
+
this.name = "AttachmentStorageConfigurationError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isAttachmentStorageConfigurationError(error) {
|
|
24
|
+
return Boolean(error) && typeof error === "object" && error[ERROR_MARKER] === true;
|
|
25
|
+
}
|
|
26
|
+
function getAttachmentStoragePolicy() {
|
|
27
|
+
const policy = process.env.OM_ATTACHMENT_STORAGE_POLICY?.trim();
|
|
28
|
+
if (!policy || policy === "legacy") return "legacy";
|
|
29
|
+
if (policy === "strict") return "strict";
|
|
30
|
+
throw new AttachmentStorageConfigurationError("invalid_policy");
|
|
31
|
+
}
|
|
32
|
+
function registerStorageDriverValidator(key, validator) {
|
|
33
|
+
const registry = validators();
|
|
34
|
+
registry.set(key, validator);
|
|
35
|
+
return () => {
|
|
36
|
+
if (registry.get(key) === validator) registry.delete(key);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function assertAttachmentStoragePartitionExists(exists) {
|
|
40
|
+
if (getAttachmentStoragePolicy() === "strict" && !exists) {
|
|
41
|
+
throw new AttachmentStorageConfigurationError("partition_missing");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function validateAttachmentStorageConfiguration(context, registered) {
|
|
45
|
+
if (getAttachmentStoragePolicy() === "legacy") return;
|
|
46
|
+
if (typeof context.driverKey !== "string" || !context.driverKey.trim()) {
|
|
47
|
+
throw new AttachmentStorageConfigurationError("driver_missing");
|
|
48
|
+
}
|
|
49
|
+
if (!registered) throw new AttachmentStorageConfigurationError("unknown_driver");
|
|
50
|
+
if (!attachmentStorageConfigurationSchema.safeParse(context.config).success) {
|
|
51
|
+
throw new AttachmentStorageConfigurationError("invalid_config");
|
|
52
|
+
}
|
|
53
|
+
if (context.scope && !attachmentStorageScopeSchema.safeParse(context.scope).success) {
|
|
54
|
+
throw new AttachmentStorageConfigurationError("invalid_scope");
|
|
55
|
+
}
|
|
56
|
+
const validator = validators().get(context.driverKey);
|
|
57
|
+
if (!validator) {
|
|
58
|
+
if (context.driverKey === "local" || context.driverKey === "legacyPublic") return;
|
|
59
|
+
throw new AttachmentStorageConfigurationError("validator_missing");
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const accepted = validator({
|
|
63
|
+
...context,
|
|
64
|
+
driverKey: context.driverKey,
|
|
65
|
+
config: context.config
|
|
66
|
+
});
|
|
67
|
+
if (accepted !== true) {
|
|
68
|
+
if (accepted && typeof accepted === "object" && "then" in accepted) {
|
|
69
|
+
void Promise.resolve(accepted).catch(() => void 0);
|
|
70
|
+
}
|
|
71
|
+
throw new AttachmentStorageConfigurationError("validator_rejected");
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
throw new AttachmentStorageConfigurationError("validator_rejected");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
AttachmentStorageConfigurationError,
|
|
79
|
+
assertAttachmentStoragePartitionExists,
|
|
80
|
+
getAttachmentStoragePolicy,
|
|
81
|
+
isAttachmentStorageConfigurationError,
|
|
82
|
+
registerStorageDriverValidator,
|
|
83
|
+
validateAttachmentStorageConfiguration
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=storageValidation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/attachments/lib/drivers/storageValidation.ts"],
|
|
4
|
+
"sourcesContent": ["import type { z } from 'zod'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { attachmentStorageConfigurationSchema, attachmentStorageScopeSchema } from '../../data/validators'\n\nexport type AttachmentStoragePolicy = 'legacy' | 'strict'\nexport type AttachmentStorageScope = z.infer<typeof attachmentStorageScopeSchema>\nexport type AttachmentStorageValidationStage = 'configured' | 'resolved'\nexport type AttachmentStorageValidationContext = {\n readonly driverKey: string\n readonly config: Readonly<Record<string, unknown>>\n readonly partitionCode?: string\n readonly scope?: Readonly<AttachmentStorageScope>\n readonly stage: AttachmentStorageValidationStage\n}\nexport type AttachmentStorageValidator = (context: AttachmentStorageValidationContext) => boolean\nexport type AttachmentStorageConfigurationReason =\n | 'invalid_policy'\n | 'partition_missing'\n | 'driver_missing'\n | 'unknown_driver'\n | 'invalid_config'\n | 'invalid_scope'\n | 'validator_missing'\n | 'validator_rejected'\n | 'enhancement_failed'\n\nconst ERROR_MARKER = Symbol.for('@open-mercato/AttachmentStorageConfigurationError')\nconst VALIDATORS_KEY = Symbol.for('@open-mercato/AttachmentStorageValidators')\ntype ValidatorRegistry = Map<string, AttachmentStorageValidator>\n\nfunction validators(): ValidatorRegistry {\n const registry = globalThis as typeof globalThis & { [VALIDATORS_KEY]?: ValidatorRegistry }\n return registry[VALIDATORS_KEY] ??= new Map()\n}\n\nexport class AttachmentStorageConfigurationError extends CrudHttpError {\n readonly [ERROR_MARKER] = true\n readonly code = 'ATTACHMENT_STORAGE_CONFIGURATION_INVALID'\n\n constructor(readonly reason: AttachmentStorageConfigurationReason) {\n super(503, {\n error: 'attachments.errors.storageConfigurationInvalid',\n code: 'ATTACHMENT_STORAGE_CONFIGURATION_INVALID',\n reason,\n })\n this.name = 'AttachmentStorageConfigurationError'\n }\n}\n\nexport function isAttachmentStorageConfigurationError(error: unknown): error is AttachmentStorageConfigurationError {\n return Boolean(error) && typeof error === 'object'\n && (error as Record<symbol, unknown>)[ERROR_MARKER] === true\n}\n\nexport function getAttachmentStoragePolicy(): AttachmentStoragePolicy {\n const policy = process.env.OM_ATTACHMENT_STORAGE_POLICY?.trim()\n if (!policy || policy === 'legacy') return 'legacy'\n if (policy === 'strict') return 'strict'\n throw new AttachmentStorageConfigurationError('invalid_policy')\n}\n\nexport function registerStorageDriverValidator(key: string, validator: AttachmentStorageValidator): () => void {\n const registry = validators()\n registry.set(key, validator)\n return () => {\n if (registry.get(key) === validator) registry.delete(key)\n }\n}\n\nexport function assertAttachmentStoragePartitionExists(exists: boolean): void {\n if (getAttachmentStoragePolicy() === 'strict' && !exists) {\n throw new AttachmentStorageConfigurationError('partition_missing')\n }\n}\n\nexport function validateAttachmentStorageConfiguration(\n context: Omit<AttachmentStorageValidationContext, 'driverKey' | 'config'> & {\n driverKey: unknown\n config: unknown\n },\n registered: boolean,\n): void {\n if (getAttachmentStoragePolicy() === 'legacy') return\n if (typeof context.driverKey !== 'string' || !context.driverKey.trim()) {\n throw new AttachmentStorageConfigurationError('driver_missing')\n }\n if (!registered) throw new AttachmentStorageConfigurationError('unknown_driver')\n if (!attachmentStorageConfigurationSchema.safeParse(context.config).success) {\n throw new AttachmentStorageConfigurationError('invalid_config')\n }\n if (context.scope && !attachmentStorageScopeSchema.safeParse(context.scope).success) {\n throw new AttachmentStorageConfigurationError('invalid_scope')\n }\n const validator = validators().get(context.driverKey)\n if (!validator) {\n if (context.driverKey === 'local' || context.driverKey === 'legacyPublic') return\n throw new AttachmentStorageConfigurationError('validator_missing')\n }\n try {\n const accepted: unknown = validator({\n ...context,\n driverKey: context.driverKey,\n config: context.config as Record<string, unknown>,\n })\n if (accepted !== true) {\n if (accepted && typeof accepted === 'object' && 'then' in accepted) {\n void Promise.resolve(accepted).catch(() => undefined)\n }\n throw new AttachmentStorageConfigurationError('validator_rejected')\n }\n } catch {\n throw new AttachmentStorageConfigurationError('validator_rejected')\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AACA,SAAS,qBAAqB;AAC9B,SAAS,sCAAsC,oCAAoC;AAwBnF,MAAM,eAAe,uBAAO,IAAI,mDAAmD;AACnF,MAAM,iBAAiB,uBAAO,IAAI,2CAA2C;AAG7E,SAAS,aAAgC;AACvC,QAAM,WAAW;AACjB,SAAO,SAAS,cAAc,MAAM,oBAAI,IAAI;AAC9C;AAEO,MAAM,6CAA4C,oBAC7C,mBAD6C,IAAc;AAAA,EAIrE,YAAqB,QAA8C;AACjE,UAAM,KAAK;AAAA,MACT,OAAO;AAAA,MACP,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AALkB;AAHrB,SAAU,MAAgB;AAC1B,SAAS,OAAO;AAQd,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,sCAAsC,OAA8D;AAClH,SAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YACpC,MAAkC,YAAY,MAAM;AAC5D;AAEO,SAAS,6BAAsD;AACpE,QAAM,SAAS,QAAQ,IAAI,8BAA8B,KAAK;AAC9D,MAAI,CAAC,UAAU,WAAW,SAAU,QAAO;AAC3C,MAAI,WAAW,SAAU,QAAO;AAChC,QAAM,IAAI,oCAAoC,gBAAgB;AAChE;AAEO,SAAS,+BAA+B,KAAa,WAAmD;AAC7G,QAAM,WAAW,WAAW;AAC5B,WAAS,IAAI,KAAK,SAAS;AAC3B,SAAO,MAAM;AACX,QAAI,SAAS,IAAI,GAAG,MAAM,UAAW,UAAS,OAAO,GAAG;AAAA,EAC1D;AACF;AAEO,SAAS,uCAAuC,QAAuB;AAC5E,MAAI,2BAA2B,MAAM,YAAY,CAAC,QAAQ;AACxD,UAAM,IAAI,oCAAoC,mBAAmB;AAAA,EACnE;AACF;AAEO,SAAS,uCACd,SAIA,YACM;AACN,MAAI,2BAA2B,MAAM,SAAU;AAC/C,MAAI,OAAO,QAAQ,cAAc,YAAY,CAAC,QAAQ,UAAU,KAAK,GAAG;AACtE,UAAM,IAAI,oCAAoC,gBAAgB;AAAA,EAChE;AACA,MAAI,CAAC,WAAY,OAAM,IAAI,oCAAoC,gBAAgB;AAC/E,MAAI,CAAC,qCAAqC,UAAU,QAAQ,MAAM,EAAE,SAAS;AAC3E,UAAM,IAAI,oCAAoC,gBAAgB;AAAA,EAChE;AACA,MAAI,QAAQ,SAAS,CAAC,6BAA6B,UAAU,QAAQ,KAAK,EAAE,SAAS;AACnF,UAAM,IAAI,oCAAoC,eAAe;AAAA,EAC/D;AACA,QAAM,YAAY,WAAW,EAAE,IAAI,QAAQ,SAAS;AACpD,MAAI,CAAC,WAAW;AACd,QAAI,QAAQ,cAAc,WAAW,QAAQ,cAAc,eAAgB;AAC3E,UAAM,IAAI,oCAAoC,mBAAmB;AAAA,EACnE;AACA,MAAI;AACF,UAAM,WAAoB,UAAU;AAAA,MAClC,GAAG;AAAA,MACH,WAAW,QAAQ;AAAA,MACnB,QAAQ,QAAQ;AAAA,IAClB,CAAC;AACD,QAAI,aAAa,MAAM;AACrB,UAAI,YAAY,OAAO,aAAa,YAAY,UAAU,UAAU;AAClE,aAAK,QAAQ,QAAQ,QAAQ,EAAE,MAAM,MAAM,MAAS;AAAA,MACtD;AACA,YAAM,IAAI,oCAAoC,oBAAoB;AAAA,IACpE;AAAA,EACF,QAAQ;AACN,UAAM,IAAI,oCAAoC,oBAAoB;AAAA,EACpE;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -5,6 +5,7 @@ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
|
5
5
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
6
6
|
import { Attachment, AttachmentPartition } from "../data/entities.js";
|
|
7
7
|
import { attachmentCrudEvents, attachmentCrudIndexer } from "./crud.js";
|
|
8
|
+
import { assertAttachmentStoragePartitionExists, getAttachmentStoragePolicy } from "./drivers/storageValidation.js";
|
|
8
9
|
import { buildAttachmentFileUrl } from "./imageUrls.js";
|
|
9
10
|
import { ensureAttachmentPartitionProtection } from "./access-protection.js";
|
|
10
11
|
import { ensureDefaultPartitions, resolveDefaultPartitionCode } from "./partitions.js";
|
|
@@ -54,7 +55,7 @@ class ScopedAttachmentUploadService {
|
|
|
54
55
|
throw new ScopedAttachmentUploadError("active_content", 400);
|
|
55
56
|
}
|
|
56
57
|
const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps;
|
|
57
|
-
await ensureDefaultPartitions(em);
|
|
58
|
+
if (getAttachmentStoragePolicy() === "legacy") await ensureDefaultPartitions(em);
|
|
58
59
|
const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId);
|
|
59
60
|
const partition = await em.findOne(AttachmentPartition, {
|
|
60
61
|
code: partitionCode,
|
|
@@ -63,6 +64,7 @@ class ScopedAttachmentUploadService {
|
|
|
63
64
|
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
64
65
|
]
|
|
65
66
|
});
|
|
67
|
+
assertAttachmentStoragePartitionExists(Boolean(partition));
|
|
66
68
|
if (!partition) throw new ScopedAttachmentUploadError("partition_unavailable", 400);
|
|
67
69
|
if (input.requirePrivatePartition && partition.isPublic) {
|
|
68
70
|
throw new ScopedAttachmentUploadError("partition_unavailable", 403);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/scoped-upload-service.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from './crud'\nimport type { StorageDriverFactory } from './drivers'\nimport { buildAttachmentFileUrl } from './imageUrls'\nimport { ensureAttachmentPartitionProtection } from './access-protection'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'\nimport { extractAttachmentContent } from './textExtraction'\nimport { requestOcrProcessing } from './ocrQueue'\nimport { OcrService, shouldUseLlmOcr } from './ocrService'\nimport { assertAttachmentScopeInvariant } from './access'\nimport {\n mergeAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from './metadata'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from './security'\nimport { resolveAttachmentMaxBytes } from './upload-limits'\nimport type { AttachmentQuotaService } from './quota-service'\n\nconst logger = createLogger('attachments')\n\nexport type ScopedAttachmentUploadErrorCode =\n | 'dangerous_executable'\n | 'max_upload_size'\n | 'active_content'\n | 'partition_unavailable'\n | 'quota_exceeded'\n | 'quota_target_exists'\n | 'quota_unavailable'\n | 'quota_recovery_unsupported'\n | 'storage_failed'\n | 'persistence_failed'\n\n// Use Symbol.for so the marker survives module duplication across bundle\n// boundaries. The production build emits this module into several server\n// chunks, so a consumer's `instanceof` compares against a different copy of the\n// class than the DI-resolved service threw from: the check silently fails, the\n// error escapes the caller's catch, and a deliberate 400/413 surfaces as a 500.\nconst SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER = Symbol.for('@open-mercato/ScopedAttachmentUploadError')\n\nexport class ScopedAttachmentUploadError extends Error {\n readonly [SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] = true\n\n constructor(\n public readonly code: ScopedAttachmentUploadErrorCode,\n public readonly status: number,\n ) {\n super(code)\n this.name = 'ScopedAttachmentUploadError'\n }\n}\n\n/**\n * Bundle-safe check for `ScopedAttachmentUploadError`. Prefer this over\n * `instanceof` at every call site \u2014 the class is duplicated across server\n * chunks, so `instanceof` is only reliable inside this module.\n */\nexport function isScopedAttachmentUploadError(error: unknown): error is ScopedAttachmentUploadError {\n return Boolean(error)\n && typeof error === 'object'\n && (error as Record<symbol, unknown>)[SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] === true\n}\n\nexport type ScopedAttachmentUploadInput = {\n tenantId: string\n organizationId: string\n entityId: string\n recordId: string\n fileName: string\n declaredMimeType?: string | null\n buffer: Buffer\n tags?: string[]\n assignments?: AttachmentAssignment[]\n partitionCode?: string | null\n maxBytes?: number\n /**\n * Reject a partition flagged public, so a module-owned upload (documents,\n * warranty claims) never lands where an anonymous reader can reach it. The\n * tenant/organization half of that guarantee is not this flag's job: the\n * partition lookup below already matches only a global partition or one owned\n * by this tenant and organization, whether or not the flag is set.\n */\n requirePrivatePartition?: boolean\n /**\n * Persists a module-owned link row inside the same transaction as the\n * Attachment, so a link failure cannot leave a committed orphan attachment.\n * The callback receives only the generated id, never the entity or the\n * storage implementation.\n */\n persistLink?: (tx: EntityManager, attachmentId: string) => Promise<void> | void\n}\n\ntype QuotaRecoveryScheduler = (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n) => Promise<void>\n\nexport class ScopedAttachmentUploadService {\n constructor(private readonly deps: {\n em: EntityManager\n dataEngine?: DataEngine | null\n storageDriverFactory: StorageDriverFactory\n attachmentQuotaService: AttachmentQuotaService\n attachmentQuotaRecoveryScheduler: QuotaRecoveryScheduler\n }) {}\n\n async upload(input: ScopedAttachmentUploadInput): Promise<Attachment> {\n const safeName = sanitizeUploadedFileName(input.fileName)\n if (hasDangerousExecutableExtension(safeName)) {\n throw new ScopedAttachmentUploadError('dangerous_executable', 400)\n }\n if (input.buffer.length > (input.maxBytes ?? resolveAttachmentMaxBytes())) {\n throw new ScopedAttachmentUploadError('max_upload_size', 413)\n }\n const mimeType = detectAttachmentMimeType(input.buffer, safeName, input.declaredMimeType ?? '')\n if (isActiveContentAttachment(input.buffer, safeName, mimeType)) {\n throw new ScopedAttachmentUploadError('active_content', 400)\n }\n\n const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps\n await ensureDefaultPartitions(em)\n const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId)\n const partition = await em.findOne(AttachmentPartition, {\n code: partitionCode,\n $or: [\n { tenantId: null, organizationId: null },\n { tenantId: input.tenantId, organizationId: input.organizationId },\n ],\n })\n if (!partition) throw new ScopedAttachmentUploadError('partition_unavailable', 400)\n if (input.requirePrivatePartition && partition.isPublic) {\n throw new ScopedAttachmentUploadError('partition_unavailable', 403)\n }\n\n await ensureAttachmentPartitionProtection(em, partition)\n const driver = await storageDriverFactory.resolveForPartition(partition.code, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const preparedStoragePath = driver.prepareStoragePath?.({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n })\n if (!preparedStoragePath) {\n throw new ScopedAttachmentUploadError('quota_recovery_unsupported', 500)\n }\n\n let reservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n try {\n reservation = await attachmentQuotaService.reserve({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n bytes: input.buffer.length,\n source: 'attachment',\n storageDriver: driver.key,\n storagePath: preparedStoragePath,\n partitionCode: partition.code,\n })\n await attachmentQuotaRecoveryScheduler({\n reservationId: reservation.id,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, Math.max(1_000, reservation.expiresAt.getTime() - Date.now()))\n await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken)\n } catch (error) {\n if (reservation) {\n await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => undefined)\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') throw new ScopedAttachmentUploadError('quota_exceeded', 413)\n if (code === 'quota_target_exists') throw new ScopedAttachmentUploadError('quota_target_exists', 409)\n throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n }\n\n if (!reservation) throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n\n let storedPath = preparedStoragePath\n try {\n const stored = await driver.store({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n buffer: input.buffer,\n storagePath: preparedStoragePath,\n })\n storedPath = stored.storagePath\n await attachmentQuotaService.markStored(reservation.id, reservation.leaseToken)\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment storage failed', { err: error })\n throw new ScopedAttachmentUploadError('storage_failed', 500)\n }\n\n let extractedContent: string | null = null\n const wantsLlmOcr = partition.requiresOcr && shouldUseLlmOcr(mimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n if (partition.requiresOcr && !useLlmOcr) {\n try {\n const { filePath, cleanup } = await driver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({ filePath, mimeType })\n } finally {\n await cleanup().catch(() => undefined)\n }\n } catch (error) {\n logger.error('Scoped attachment text extraction failed', { err: error })\n }\n }\n\n let assignments = input.assignments?.slice() ?? []\n assignments = upsertAssignment(assignments, { type: input.entityId, id: input.recordId })\n const metadata = mergeAttachmentMetadata(null, {\n assignments,\n tags: input.tags ?? [],\n })\n const attachmentId = randomUUID()\n let attachment!: Attachment\n\n try {\n await em.transactional(async (tx) => {\n attachment = tx.create(Attachment, {\n id: attachmentId,\n entityId: input.entityId,\n recordId: input.recordId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n mimeType,\n fileSize: input.buffer.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: storedPath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n assertAttachmentScopeInvariant({ tenantId: attachment.tenantId, organizationId: attachment.organizationId })\n await tx.persist(attachment).flush()\n await input.persistLink?.(tx, attachmentId)\n await attachmentQuotaService.completeAttachment(reservation.id, reservation.leaseToken, tx)\n })\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment persistence failed', { err: error })\n // `persistLink` is where callers run their own authorization (the\n // documents module re-checks the edit capability inside this\n // transaction), so a deliberate HTTP error from it must reach the route\n // with its own status. Flattening it to 500 turns a documented 403 into\n // an apparent server fault.\n if (isCrudHttpError(error)) throw error\n throw new ScopedAttachmentUploadError('persistence_failed', 500)\n }\n\n if (useLlmOcr) {\n try {\n await requestOcrProcessing(em, attachment, driver, storedPath)\n } catch (error) {\n logger.error('Scoped attachment OCR scheduling failed', { err: error })\n }\n }\n if (this.deps.dataEngine) {\n await emitCrudSideEffects({\n dataEngine: this.deps.dataEngine,\n action: 'created',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await this.deps.dataEngine.flushOrmEntityChanges()\n }\n return attachment\n }\n\n private async compensateStorage(\n driver: Awaited<ReturnType<StorageDriverFactory['resolveForPartition']>>,\n partitionCode: string,\n storagePath: string,\n reservation: { id: string; leaseToken: string },\n ): Promise<void> {\n try {\n await (driver.deleteStrict?.(partitionCode, storagePath) ?? driver.delete(partitionCode, storagePath))\n await this.deps.attachmentQuotaService.release(reservation.id, reservation.leaseToken)\n } catch (error) {\n logger.error('Scoped attachment compensation failed', { err: error })\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA;AAAA,SAAS,kBAAkB;AAG3B,SAAS,2BAA2B;AACpC,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAC7B,SAAS,YAAY,2BAA2B;AAChD,SAAS,sBAAsB,6BAA6B;AAE5D,SAAS,8BAA8B;AACvC,SAAS,2CAA2C;AACpD,SAAS,yBAAyB,mCAAmC;AACrE,SAAS,gCAAgC;AACzC,SAAS,4BAA4B;AACrC,SAAS,YAAY,uBAAuB;AAC5C,SAAS,sCAAsC;AAC/C;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAG1C,MAAM,SAAS,aAAa,aAAa;AAmBzC,MAAM,wCAAwC,uBAAO,IAAI,2CAA2C;AAE7F,MAAM,qCAAoC,YACrC,4CADqC,IAAM;AAAA,EAGrD,YACkB,MACA,QAChB;AACA,UAAM,IAAI;AAHM;AACA;AAJlB,SAAU,MAAyC;AAOjD,SAAK,OAAO;AAAA,EACd;AACF;AAOO,SAAS,8BAA8B,OAAsD;AAClG,SAAO,QAAQ,KAAK,KACf,OAAO,UAAU,YAChB,MAAkC,qCAAqC,MAAM;AACrF;AAoCO,MAAM,8BAA8B;AAAA,EACzC,YAA6B,MAM1B;AAN0B;AAAA,EAMzB;AAAA,EAEJ,MAAM,OAAO,OAAyD;AACpE,UAAM,WAAW,yBAAyB,MAAM,QAAQ;AACxD,QAAI,gCAAgC,QAAQ,GAAG;AAC7C,YAAM,IAAI,4BAA4B,wBAAwB,GAAG;AAAA,IACnE;AACA,QAAI,MAAM,OAAO,UAAU,MAAM,YAAY,0BAA0B,IAAI;AACzE,YAAM,IAAI,4BAA4B,mBAAmB,GAAG;AAAA,IAC9D;AACA,UAAM,WAAW,yBAAyB,MAAM,QAAQ,UAAU,MAAM,oBAAoB,EAAE;AAC9F,QAAI,0BAA0B,MAAM,QAAQ,UAAU,QAAQ,GAAG;AAC/D,YAAM,IAAI,4BAA4B,kBAAkB,GAAG;AAAA,IAC7D;AAEA,UAAM,EAAE,IAAI,sBAAsB,wBAAwB,iCAAiC,IAAI,KAAK;AACpG,
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from './crud'\nimport type { StorageDriverFactory } from './drivers'\nimport { assertAttachmentStoragePartitionExists, getAttachmentStoragePolicy } from './drivers/storageValidation'\nimport { buildAttachmentFileUrl } from './imageUrls'\nimport { ensureAttachmentPartitionProtection } from './access-protection'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'\nimport { extractAttachmentContent } from './textExtraction'\nimport { requestOcrProcessing } from './ocrQueue'\nimport { OcrService, shouldUseLlmOcr } from './ocrService'\nimport { assertAttachmentScopeInvariant } from './access'\nimport {\n mergeAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from './metadata'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from './security'\nimport { resolveAttachmentMaxBytes } from './upload-limits'\nimport type { AttachmentQuotaService } from './quota-service'\n\nconst logger = createLogger('attachments')\n\nexport type ScopedAttachmentUploadErrorCode =\n | 'dangerous_executable'\n | 'max_upload_size'\n | 'active_content'\n | 'partition_unavailable'\n | 'quota_exceeded'\n | 'quota_target_exists'\n | 'quota_unavailable'\n | 'quota_recovery_unsupported'\n | 'storage_failed'\n | 'persistence_failed'\n\n// Use Symbol.for so the marker survives module duplication across bundle\n// boundaries. The production build emits this module into several server\n// chunks, so a consumer's `instanceof` compares against a different copy of the\n// class than the DI-resolved service threw from: the check silently fails, the\n// error escapes the caller's catch, and a deliberate 400/413 surfaces as a 500.\nconst SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER = Symbol.for('@open-mercato/ScopedAttachmentUploadError')\n\nexport class ScopedAttachmentUploadError extends Error {\n readonly [SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] = true\n\n constructor(\n public readonly code: ScopedAttachmentUploadErrorCode,\n public readonly status: number,\n ) {\n super(code)\n this.name = 'ScopedAttachmentUploadError'\n }\n}\n\n/**\n * Bundle-safe check for `ScopedAttachmentUploadError`. Prefer this over\n * `instanceof` at every call site \u2014 the class is duplicated across server\n * chunks, so `instanceof` is only reliable inside this module.\n */\nexport function isScopedAttachmentUploadError(error: unknown): error is ScopedAttachmentUploadError {\n return Boolean(error)\n && typeof error === 'object'\n && (error as Record<symbol, unknown>)[SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] === true\n}\n\nexport type ScopedAttachmentUploadInput = {\n tenantId: string\n organizationId: string\n entityId: string\n recordId: string\n fileName: string\n declaredMimeType?: string | null\n buffer: Buffer\n tags?: string[]\n assignments?: AttachmentAssignment[]\n partitionCode?: string | null\n maxBytes?: number\n /**\n * Reject a partition flagged public, so a module-owned upload (documents,\n * warranty claims) never lands where an anonymous reader can reach it. The\n * tenant/organization half of that guarantee is not this flag's job: the\n * partition lookup below already matches only a global partition or one owned\n * by this tenant and organization, whether or not the flag is set.\n */\n requirePrivatePartition?: boolean\n /**\n * Persists a module-owned link row inside the same transaction as the\n * Attachment, so a link failure cannot leave a committed orphan attachment.\n * The callback receives only the generated id, never the entity or the\n * storage implementation.\n */\n persistLink?: (tx: EntityManager, attachmentId: string) => Promise<void> | void\n}\n\ntype QuotaRecoveryScheduler = (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n) => Promise<void>\n\nexport class ScopedAttachmentUploadService {\n constructor(private readonly deps: {\n em: EntityManager\n dataEngine?: DataEngine | null\n storageDriverFactory: StorageDriverFactory\n attachmentQuotaService: AttachmentQuotaService\n attachmentQuotaRecoveryScheduler: QuotaRecoveryScheduler\n }) {}\n\n async upload(input: ScopedAttachmentUploadInput): Promise<Attachment> {\n const safeName = sanitizeUploadedFileName(input.fileName)\n if (hasDangerousExecutableExtension(safeName)) {\n throw new ScopedAttachmentUploadError('dangerous_executable', 400)\n }\n if (input.buffer.length > (input.maxBytes ?? resolveAttachmentMaxBytes())) {\n throw new ScopedAttachmentUploadError('max_upload_size', 413)\n }\n const mimeType = detectAttachmentMimeType(input.buffer, safeName, input.declaredMimeType ?? '')\n if (isActiveContentAttachment(input.buffer, safeName, mimeType)) {\n throw new ScopedAttachmentUploadError('active_content', 400)\n }\n\n const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps\n if (getAttachmentStoragePolicy() === 'legacy') await ensureDefaultPartitions(em)\n const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId)\n const partition = await em.findOne(AttachmentPartition, {\n code: partitionCode,\n $or: [\n { tenantId: null, organizationId: null },\n { tenantId: input.tenantId, organizationId: input.organizationId },\n ],\n })\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) throw new ScopedAttachmentUploadError('partition_unavailable', 400)\n if (input.requirePrivatePartition && partition.isPublic) {\n throw new ScopedAttachmentUploadError('partition_unavailable', 403)\n }\n\n await ensureAttachmentPartitionProtection(em, partition)\n const driver = await storageDriverFactory.resolveForPartition(partition.code, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const preparedStoragePath = driver.prepareStoragePath?.({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n })\n if (!preparedStoragePath) {\n throw new ScopedAttachmentUploadError('quota_recovery_unsupported', 500)\n }\n\n let reservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n try {\n reservation = await attachmentQuotaService.reserve({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n bytes: input.buffer.length,\n source: 'attachment',\n storageDriver: driver.key,\n storagePath: preparedStoragePath,\n partitionCode: partition.code,\n })\n await attachmentQuotaRecoveryScheduler({\n reservationId: reservation.id,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, Math.max(1_000, reservation.expiresAt.getTime() - Date.now()))\n await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken)\n } catch (error) {\n if (reservation) {\n await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => undefined)\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') throw new ScopedAttachmentUploadError('quota_exceeded', 413)\n if (code === 'quota_target_exists') throw new ScopedAttachmentUploadError('quota_target_exists', 409)\n throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n }\n\n if (!reservation) throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n\n let storedPath = preparedStoragePath\n try {\n const stored = await driver.store({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n buffer: input.buffer,\n storagePath: preparedStoragePath,\n })\n storedPath = stored.storagePath\n await attachmentQuotaService.markStored(reservation.id, reservation.leaseToken)\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment storage failed', { err: error })\n throw new ScopedAttachmentUploadError('storage_failed', 500)\n }\n\n let extractedContent: string | null = null\n const wantsLlmOcr = partition.requiresOcr && shouldUseLlmOcr(mimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n if (partition.requiresOcr && !useLlmOcr) {\n try {\n const { filePath, cleanup } = await driver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({ filePath, mimeType })\n } finally {\n await cleanup().catch(() => undefined)\n }\n } catch (error) {\n logger.error('Scoped attachment text extraction failed', { err: error })\n }\n }\n\n let assignments = input.assignments?.slice() ?? []\n assignments = upsertAssignment(assignments, { type: input.entityId, id: input.recordId })\n const metadata = mergeAttachmentMetadata(null, {\n assignments,\n tags: input.tags ?? [],\n })\n const attachmentId = randomUUID()\n let attachment!: Attachment\n\n try {\n await em.transactional(async (tx) => {\n attachment = tx.create(Attachment, {\n id: attachmentId,\n entityId: input.entityId,\n recordId: input.recordId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n mimeType,\n fileSize: input.buffer.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: storedPath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n assertAttachmentScopeInvariant({ tenantId: attachment.tenantId, organizationId: attachment.organizationId })\n await tx.persist(attachment).flush()\n await input.persistLink?.(tx, attachmentId)\n await attachmentQuotaService.completeAttachment(reservation.id, reservation.leaseToken, tx)\n })\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment persistence failed', { err: error })\n // `persistLink` is where callers run their own authorization (the\n // documents module re-checks the edit capability inside this\n // transaction), so a deliberate HTTP error from it must reach the route\n // with its own status. Flattening it to 500 turns a documented 403 into\n // an apparent server fault.\n if (isCrudHttpError(error)) throw error\n throw new ScopedAttachmentUploadError('persistence_failed', 500)\n }\n\n if (useLlmOcr) {\n try {\n await requestOcrProcessing(em, attachment, driver, storedPath)\n } catch (error) {\n logger.error('Scoped attachment OCR scheduling failed', { err: error })\n }\n }\n if (this.deps.dataEngine) {\n await emitCrudSideEffects({\n dataEngine: this.deps.dataEngine,\n action: 'created',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await this.deps.dataEngine.flushOrmEntityChanges()\n }\n return attachment\n }\n\n private async compensateStorage(\n driver: Awaited<ReturnType<StorageDriverFactory['resolveForPartition']>>,\n partitionCode: string,\n storagePath: string,\n reservation: { id: string; leaseToken: string },\n ): Promise<void> {\n try {\n await (driver.deleteStrict?.(partitionCode, storagePath) ?? driver.delete(partitionCode, storagePath))\n await this.deps.attachmentQuotaService.release(reservation.id, reservation.leaseToken)\n } catch (error) {\n logger.error('Scoped attachment compensation failed', { err: error })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
3
|
+
import { isAttachmentStorageConfigurationError } from "./drivers/storageValidation.js";
|
|
4
|
+
function withAttachmentStorageErrors(handler) {
|
|
5
|
+
return async (...args) => {
|
|
6
|
+
try {
|
|
7
|
+
return await handler(...args);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (!isAttachmentStorageConfigurationError(error)) throw error;
|
|
10
|
+
const { t } = await resolveTranslations();
|
|
11
|
+
return NextResponse.json({
|
|
12
|
+
error: t("attachments.errors.storageConfigurationInvalid", "Attachment storage configuration is unavailable."),
|
|
13
|
+
code: error.code,
|
|
14
|
+
reason: error.reason
|
|
15
|
+
}, { status: 503 });
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
withAttachmentStorageErrors
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=storageErrors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/attachments/lib/storageErrors.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { isAttachmentStorageConfigurationError } from './drivers/storageValidation'\n\nexport function withAttachmentStorageErrors<Args extends unknown[]>(\n handler: (...args: Args) => Promise<Response>,\n): (...args: Args) => Promise<Response> {\n return async (...args) => {\n try {\n return await handler(...args)\n } catch (error) {\n if (!isAttachmentStorageConfigurationError(error)) throw error\n const { t } = await resolveTranslations()\n return NextResponse.json({\n error: t('attachments.errors.storageConfigurationInvalid', 'Attachment storage configuration is unavailable.'),\n code: error.code,\n reason: error.reason,\n }, { status: 503 })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,6CAA6C;AAE/C,SAAS,4BACd,SACsC;AACtC,SAAO,UAAU,SAAS;AACxB,QAAI;AACF,aAAO,MAAM,QAAQ,GAAG,IAAI;AAAA,IAC9B,SAAS,OAAO;AACd,UAAI,CAAC,sCAAsC,KAAK,EAAG,OAAM;AACzD,YAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,aAAO,aAAa,KAAK;AAAA,QACvB,OAAO,EAAE,kDAAkD,kDAAkD;AAAA,QAC7G,MAAM,MAAM;AAAA,QACZ,QAAQ,MAAM;AAAA,MAChB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpB;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -4,10 +4,10 @@ import { resolveSingleOrganizationIdOrDeny } from "@open-mercato/shared/lib/auth
|
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
6
6
|
import { isCrudHttpError, translateCrudErrorBody } from "@open-mercato/shared/lib/crud/errors";
|
|
7
|
-
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
8
7
|
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
9
8
|
import { z } from "zod";
|
|
10
9
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
10
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
11
11
|
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
12
12
|
import { authorizeAuditReplayWithEntityManager } from "@open-mercato/core/modules/audit_logs/lib/replayAuthorization";
|
|
13
13
|
import {
|
|
@@ -151,15 +151,16 @@ async function POST(req) {
|
|
|
151
151
|
} catch (err) {
|
|
152
152
|
if (isCrudHttpError(err)) {
|
|
153
153
|
logger.warn("Redo rejected", { err });
|
|
154
|
-
const { translate } = await resolveTranslations();
|
|
155
|
-
return NextResponse.json(translateCrudErrorBody(err.body,
|
|
154
|
+
const { translate: translate2 } = await resolveTranslations();
|
|
155
|
+
return NextResponse.json(translateCrudErrorBody(err.body, translate2), { status: err.status });
|
|
156
156
|
}
|
|
157
157
|
const interceptorRejection = getCommandInterceptorHttpRejection(err);
|
|
158
158
|
if (interceptorRejection) {
|
|
159
159
|
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
160
160
|
}
|
|
161
161
|
logger.error("Redo failed", { err });
|
|
162
|
-
|
|
162
|
+
const { translate } = await resolveTranslations();
|
|
163
|
+
return NextResponse.json({ error: translate("audit_logs.error.redo_failed", "Redo failed") }, { status: 400 });
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
async function createRuntimeContext(container, auth, request) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/audit_logs/api/audit-logs/actions/redo/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError, translateCrudErrorBody } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext, CommandLogMetadata } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { AwilixContainer } from 'awilix'\nimport type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { authorizeAuditReplayWithEntityManager } from '@open-mercato/core/modules/audit_logs/lib/replayAuthorization'\nimport {\n actionLogBelongsToAuth,\n resolveCanonicalAuthSubject,\n resolveActionLogActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'redo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.redo_self'] },\n}\n\ntype RedoRequestBody = {\n logId?: string\n}\n\nconst redoRequestSchema = z.object({\n logId: z.string().min(1).describe('Identifier of the previously undone action log'),\n})\n\nconst redoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().nullable().describe('Identifier of the new redo log entry, if available'),\n undoToken: z.string().nullable().describe('New undo token associated with the redone action'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const authSubject = resolveCanonicalAuthSubject(auth)\n if (!authSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as RedoRequestBody | null\n const logId = typeof body?.logId === 'string' ? body.logId.trim() : ''\n if (!logId) return NextResponse.json({ error: 'Invalid log id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n let singleOrganizationId: string | null\n try {\n singleOrganizationId = organizationId ?? resolveSingleOrganizationIdOrDeny(scope, auth)\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const canRedoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.redo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const scopedOrgId = canRedoTenant ? organizationId ?? null : singleOrganizationId\n const log = await logs.findById(logId)\n\n if (!log || log.executionState !== 'undone') {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n const logActorSubject = resolveActionLogActorSubject(log)\n const isLegacyApiKeyOwner = log.actorUserId === authSubject.storageId\n && authSubject.kind === 'api_key'\n && log.contextJson?.actorSubject === undefined\n if (log.actorUserId && !logActorSubject && !isLegacyApiKeyOwner) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (log.actorUserId && !actionLogBelongsToAuth(log, auth) && !canRedoTenant) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.redo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never redo a tenant-scoped row. Mirrors the\n // hardened undo route (issue #2685, ported in #2931).\n if (log.tenantId && log.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Tenant-level redoers may redo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685, ported in #2931).\n const orgScopeMismatch = canRedoTenant\n ? Boolean(log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId)\n : Boolean(log.organizationId && log.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n const lookupActorId = canRedoTenant\n ? (logActorSubject ?? (isLegacyApiKeyOwner ? authSubject.subject : auth.sub))\n : authSubject.subject\n const latestUndoneOrganizationId = log.organizationId ?? null\n const latestUndone = await logs.latestUndoneForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: latestUndoneOrganizationId,\n })\n if (!latestUndone || latestUndone.id !== log.id) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (!rbac) return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n const replayRbac = rbac\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n ctx.replayTransactionGuard = ({ logEntry, transactionalEm }) =>\n authorizeAuditReplayWithEntityManager(transactionalEm, replayRbac, logEntry, {\n auth,\n organizationId,\n selfFeature: 'audit_logs.redo_self',\n tenantFeature: 'audit_logs.redo_tenant',\n unavailableMessage: 'Redo target not available',\n })\n const contextRecord = log.contextJson && typeof log.contextJson === 'object' ? (log.contextJson as Record<string, unknown>) : null\n const cacheAliasesRaw = Array.isArray(contextRecord?.cacheAliases as unknown[])\n ? (contextRecord!.cacheAliases as unknown[])\n : []\n const cacheAliases = cacheAliasesRaw\n .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)\n .map((value) => value.trim())\n const metadataContext: Record<string, unknown> = {\n historyAction: 'redo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n if (cacheAliases.length) metadataContext.cacheAliases = cacheAliases\n const metadata: CommandLogMetadata = {\n tenantId: log.tenantId,\n organizationId: log.organizationId,\n actorUserId: auth.sub,\n actionLabel: log.actionLabel,\n resourceKind: log.resourceKind,\n resourceId: log.resourceId,\n context: metadataContext,\n }\n const resolvedInput = resolveRedoInput(log.commandPayload, log)\n if (!resolvedInput) {\n return NextResponse.json({ error: 'Redo data unavailable for this action' }, { status: 400 })\n }\n const commandInput = resolvedInput\n const { logEntry, replaySourceFinalized } = await commandBus.execute(log.commandId, {\n input: commandInput,\n ctx,\n metadata,\n redoLogEntry: log,\n })\n if (replaySourceFinalized !== true) await logs.markRedone(log.id)\n const actionLog = asActionLog(logEntry)\n const response = NextResponse.json({\n ok: true,\n logId: actionLog?.id ?? null,\n undoToken: actionLog?.undoToken ?? null,\n })\n if (actionLog?.undoToken && actionLog.id) {\n const createdAt = actionLog.createdAt instanceof Date\n ? actionLog.createdAt.toISOString()\n : (typeof actionLog.createdAt === 'string' ? actionLog.createdAt : new Date().toISOString())\n response.headers.set('x-om-operation', serializeOperationMetadata({\n id: actionLog.id,\n undoToken: actionLog.undoToken,\n commandId: actionLog.commandId ?? log.commandId,\n actionLabel: actionLog.actionLabel ?? log.actionLabel ?? null,\n resourceKind: typeof actionLog.resourceKind === 'string' ? actionLog.resourceKind : log.resourceKind ?? null,\n resourceId: typeof actionLog.resourceId === 'string' ? actionLog.resourceId : log.resourceId ?? null,\n executedAt: createdAt,\n }))\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n logger.warn('Redo rejected', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Redo failed', { err })\n return NextResponse.json({ error: 'Redo failed' }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nfunction asActionLog(entry: unknown): ActionLog | null {\n if (!entry || typeof entry !== 'object') return null\n if (typeof (entry as { id?: unknown }).id !== 'string') return null\n return entry as ActionLog\n}\n\nfunction resolveRedoInput(payload: unknown, log: ActionLog): unknown | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload) && '__redoInput' in payload) {\n const envelope = payload as { __redoInput?: unknown }\n return envelope.__redoInput ?? {}\n }\n const updateFallback = deriveUpdateInput(log)\n if (updateFallback) return updateFallback\n return null\n}\n\nfunction deriveUpdateInput(log: ActionLog): Record<string, unknown> | null {\n if (!log.commandId.endsWith('.update')) return null\n if (!log.resourceId) return null\n const changes = log.changesJson\n if (!changes || typeof changes !== 'object' || Array.isArray(changes)) return { id: log.resourceId }\n const payload: Record<string, unknown> = { id: log.resourceId }\n for (const [key, value] of Object.entries(changes)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && 'to' in value) {\n payload[key] = (value as Record<string, unknown>).to\n } else {\n payload[key] = value\n }\n }\n return payload\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Redo a previously undone action',\n description: 'Replays the command associated with a recently undone action, reapplying the change and issuing a fresh undo token.',\n methods: {\n POST: {\n summary: 'Redo by action log id',\n description:\n 'Redoes the latest undone command owned by the caller. Requires the action to still be eligible for redo within tenant and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: redoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Redo executed successfully', schema: redoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Log not eligible for redo', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Redo blocked by scope checks', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA4C;AACrD,SAAS,yCAAyC;AAClD,SAAS,8BAA8B;AACvC,SAAS,4BAA4B,0CAA0C;AAG/E,SAAS,iBAAiB,8BAA8B;
|
|
6
|
-
"names": ["logEntry", "metadata"]
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError, translateCrudErrorBody } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext, CommandLogMetadata } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { AwilixContainer } from 'awilix'\nimport type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { authorizeAuditReplayWithEntityManager } from '@open-mercato/core/modules/audit_logs/lib/replayAuthorization'\nimport {\n actionLogBelongsToAuth,\n resolveCanonicalAuthSubject,\n resolveActionLogActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'redo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.redo_self'] },\n}\n\ntype RedoRequestBody = {\n logId?: string\n}\n\nconst redoRequestSchema = z.object({\n logId: z.string().min(1).describe('Identifier of the previously undone action log'),\n})\n\nconst redoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().nullable().describe('Identifier of the new redo log entry, if available'),\n undoToken: z.string().nullable().describe('New undo token associated with the redone action'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const authSubject = resolveCanonicalAuthSubject(auth)\n if (!authSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as RedoRequestBody | null\n const logId = typeof body?.logId === 'string' ? body.logId.trim() : ''\n if (!logId) return NextResponse.json({ error: 'Invalid log id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n let singleOrganizationId: string | null\n try {\n singleOrganizationId = organizationId ?? resolveSingleOrganizationIdOrDeny(scope, auth)\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const canRedoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.redo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const scopedOrgId = canRedoTenant ? organizationId ?? null : singleOrganizationId\n const log = await logs.findById(logId)\n\n if (!log || log.executionState !== 'undone') {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n const logActorSubject = resolveActionLogActorSubject(log)\n const isLegacyApiKeyOwner = log.actorUserId === authSubject.storageId\n && authSubject.kind === 'api_key'\n && log.contextJson?.actorSubject === undefined\n if (log.actorUserId && !logActorSubject && !isLegacyApiKeyOwner) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (log.actorUserId && !actionLogBelongsToAuth(log, auth) && !canRedoTenant) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.redo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never redo a tenant-scoped row. Mirrors the\n // hardened undo route (issue #2685, ported in #2931).\n if (log.tenantId && log.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Tenant-level redoers may redo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685, ported in #2931).\n const orgScopeMismatch = canRedoTenant\n ? Boolean(log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId)\n : Boolean(log.organizationId && log.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n const lookupActorId = canRedoTenant\n ? (logActorSubject ?? (isLegacyApiKeyOwner ? authSubject.subject : auth.sub))\n : authSubject.subject\n const latestUndoneOrganizationId = log.organizationId ?? null\n const latestUndone = await logs.latestUndoneForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: latestUndoneOrganizationId,\n })\n if (!latestUndone || latestUndone.id !== log.id) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (!rbac) return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n const replayRbac = rbac\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n ctx.replayTransactionGuard = ({ logEntry, transactionalEm }) =>\n authorizeAuditReplayWithEntityManager(transactionalEm, replayRbac, logEntry, {\n auth,\n organizationId,\n selfFeature: 'audit_logs.redo_self',\n tenantFeature: 'audit_logs.redo_tenant',\n unavailableMessage: 'Redo target not available',\n })\n const contextRecord = log.contextJson && typeof log.contextJson === 'object' ? (log.contextJson as Record<string, unknown>) : null\n const cacheAliasesRaw = Array.isArray(contextRecord?.cacheAliases as unknown[])\n ? (contextRecord!.cacheAliases as unknown[])\n : []\n const cacheAliases = cacheAliasesRaw\n .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)\n .map((value) => value.trim())\n const metadataContext: Record<string, unknown> = {\n historyAction: 'redo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n if (cacheAliases.length) metadataContext.cacheAliases = cacheAliases\n const metadata: CommandLogMetadata = {\n tenantId: log.tenantId,\n organizationId: log.organizationId,\n actorUserId: auth.sub,\n actionLabel: log.actionLabel,\n resourceKind: log.resourceKind,\n resourceId: log.resourceId,\n context: metadataContext,\n }\n const resolvedInput = resolveRedoInput(log.commandPayload, log)\n if (!resolvedInput) {\n return NextResponse.json({ error: 'Redo data unavailable for this action' }, { status: 400 })\n }\n const commandInput = resolvedInput\n const { logEntry, replaySourceFinalized } = await commandBus.execute(log.commandId, {\n input: commandInput,\n ctx,\n metadata,\n redoLogEntry: log,\n })\n if (replaySourceFinalized !== true) await logs.markRedone(log.id)\n const actionLog = asActionLog(logEntry)\n const response = NextResponse.json({\n ok: true,\n logId: actionLog?.id ?? null,\n undoToken: actionLog?.undoToken ?? null,\n })\n if (actionLog?.undoToken && actionLog.id) {\n const createdAt = actionLog.createdAt instanceof Date\n ? actionLog.createdAt.toISOString()\n : (typeof actionLog.createdAt === 'string' ? actionLog.createdAt : new Date().toISOString())\n response.headers.set('x-om-operation', serializeOperationMetadata({\n id: actionLog.id,\n undoToken: actionLog.undoToken,\n commandId: actionLog.commandId ?? log.commandId,\n actionLabel: actionLog.actionLabel ?? log.actionLabel ?? null,\n resourceKind: typeof actionLog.resourceKind === 'string' ? actionLog.resourceKind : log.resourceKind ?? null,\n resourceId: typeof actionLog.resourceId === 'string' ? actionLog.resourceId : log.resourceId ?? null,\n executedAt: createdAt,\n }))\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n logger.warn('Redo rejected', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Redo failed', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('audit_logs.error.redo_failed', 'Redo failed') }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nfunction asActionLog(entry: unknown): ActionLog | null {\n if (!entry || typeof entry !== 'object') return null\n if (typeof (entry as { id?: unknown }).id !== 'string') return null\n return entry as ActionLog\n}\n\nfunction resolveRedoInput(payload: unknown, log: ActionLog): unknown | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload) && '__redoInput' in payload) {\n const envelope = payload as { __redoInput?: unknown }\n return envelope.__redoInput ?? {}\n }\n const updateFallback = deriveUpdateInput(log)\n if (updateFallback) return updateFallback\n return null\n}\n\nfunction deriveUpdateInput(log: ActionLog): Record<string, unknown> | null {\n if (!log.commandId.endsWith('.update')) return null\n if (!log.resourceId) return null\n const changes = log.changesJson\n if (!changes || typeof changes !== 'object' || Array.isArray(changes)) return { id: log.resourceId }\n const payload: Record<string, unknown> = { id: log.resourceId }\n for (const [key, value] of Object.entries(changes)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && 'to' in value) {\n payload[key] = (value as Record<string, unknown>).to\n } else {\n payload[key] = value\n }\n }\n return payload\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Redo a previously undone action',\n description: 'Replays the command associated with a recently undone action, reapplying the change and issuing a fresh undo token.',\n methods: {\n POST: {\n summary: 'Redo by action log id',\n description:\n 'Redoes the latest undone command owned by the caller. Requires the action to still be eligible for redo within tenant and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: redoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Redo executed successfully', schema: redoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Log not eligible for redo', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Redo blocked by scope checks', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["logEntry", "metadata", "translate"]
|
|
7
7
|
}
|