@open-mercato/core 0.4.6-develop-bbfd75b1c8 → 0.4.6-develop-79b76432fd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/integrations/api/[id]/route.js +9 -1
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +5 -1
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +5 -1
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +5 -1
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/health-service.js.map +2 -2
- package/dist/modules/integrations/lib/log-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/sales/api/quotes/convert/route.js +31 -14
- package/dist/modules/sales/api/quotes/convert/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/send/route.js +31 -14
- package/dist/modules/sales/api/quotes/send/route.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/integrations/api/[id]/route.ts +9 -1
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +8 -2
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +8 -2
- package/src/modules/integrations/lib/credentials-service.ts +6 -2
- package/src/modules/integrations/lib/health-service.ts +1 -2
- package/src/modules/integrations/lib/log-service.ts +1 -1
- package/src/modules/integrations/lib/state-service.ts +1 -1
- package/src/modules/sales/api/quotes/convert/route.ts +55 -14
- package/src/modules/sales/api/quotes/send/route.ts +55 -14
- package/dist/modules/integrations/lib/types.js +0 -1
- package/dist/modules/integrations/lib/types.js.map +0 -7
- package/src/modules/integrations/lib/types.ts +0 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/sales/api/quotes/convert/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,kCAAkC;AAE3C,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAE9B;AAAA,EACE;AAAA,EACA;AAAA,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { withScopedPayload } from '../../utils'\n\nconst convertSchema = z.object({\n quoteId: z.string().uuid(),\n orderId: z.string().uuid().optional(),\n orderNumber: z.string().trim().max(191).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage', 'sales.orders.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n if (!legacyGuard) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n return runMutationGuards([legacyGuard], input, {\n userFeatures: resolveUserFeatures(ctx.auth),\n })\n}\n\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = convertSchema.parse(scoped)\n const guardResult = await runGuards(ctx, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { quoteId: string; orderId?: string; orderNumber?: string },\n { orderId: string }\n >('sales.quotes.convert_to_order', { input, ctx })\n\n const orderId = result?.orderId ?? input.orderId ?? input.quoteId\n const jsonResponse = NextResponse.json({ orderId })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n jsonResponse.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'sales.order',\n resourceId: logEntry.resourceId ?? orderId,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n })\n )\n }\n\n if (guardResult.afterSuccessCallbacks.length) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n }\n\n return jsonResponse\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('sales.quotes.convert failed', err)\n return NextResponse.json(\n { error: translate('sales.documents.detail.convertError', 'Failed to convert quote.') },\n { status: 400 }\n )\n }\n}\n\nconst convertResponseSchema = z.object({\n orderId: z.string().uuid(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Convert quote to order',\n methods: {\n POST: {\n summary: 'Convert quote',\n description: 'Creates a sales order from a quote and removes the original quote record.',\n requestBody: {\n contentType: 'application/json',\n schema: convertSchema,\n },\n responses: [\n { status: 200, description: 'Conversion succeeded', schema: convertResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,9 +6,9 @@ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/d
|
|
|
6
6
|
import { resolveTranslations, detectLocale } from "@open-mercato/shared/lib/i18n/server";
|
|
7
7
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
9
|
+
bridgeLegacyGuard,
|
|
10
|
+
runMutationGuards
|
|
11
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard-registry";
|
|
12
12
|
import crypto from "node:crypto";
|
|
13
13
|
import { withScopedPayload } from "../../utils.js";
|
|
14
14
|
import { SalesQuote } from "../../../data/entities.js";
|
|
@@ -19,9 +19,28 @@ import { QuoteSentEmail } from "../../../emails/QuoteSentEmail.js";
|
|
|
19
19
|
const metadata = {
|
|
20
20
|
POST: { requireAuth: true, requireFeatures: ["sales.quotes.manage"] }
|
|
21
21
|
};
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
function resolveUserFeatures(auth) {
|
|
23
|
+
const features = auth?.features;
|
|
24
|
+
if (!Array.isArray(features)) return [];
|
|
25
|
+
return features.filter((value) => typeof value === "string");
|
|
26
|
+
}
|
|
27
|
+
async function runGuards(ctx, input) {
|
|
28
|
+
const legacyGuard = bridgeLegacyGuard(ctx.container);
|
|
29
|
+
if (!legacyGuard) {
|
|
30
|
+
return { ok: true, afterSuccessCallbacks: [] };
|
|
31
|
+
}
|
|
32
|
+
return runMutationGuards([legacyGuard], input, {
|
|
33
|
+
userFeatures: resolveUserFeatures(ctx.auth)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function runGuardAfterSuccessCallbacks(callbacks, input) {
|
|
37
|
+
for (const callback of callbacks) {
|
|
38
|
+
if (!callback.guard.afterSuccess) continue;
|
|
39
|
+
await callback.guard.afterSuccess({
|
|
40
|
+
...input,
|
|
41
|
+
metadata: callback.metadata ?? null
|
|
42
|
+
});
|
|
43
|
+
}
|
|
25
44
|
}
|
|
26
45
|
async function resolveRequestContext(req) {
|
|
27
46
|
const container = await createRequestContainer();
|
|
@@ -64,7 +83,7 @@ async function POST(req) {
|
|
|
64
83
|
const payload = await req.json().catch(() => ({}));
|
|
65
84
|
const scoped = withScopedPayload(payload ?? {}, ctx, translate);
|
|
66
85
|
const input = quoteSendSchema.parse(scoped);
|
|
67
|
-
const
|
|
86
|
+
const guardResult = await runGuards(ctx, {
|
|
68
87
|
tenantId: ctx.auth?.tenantId ?? "",
|
|
69
88
|
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
70
89
|
userId: ctx.auth?.sub ?? "",
|
|
@@ -74,9 +93,8 @@ async function POST(req) {
|
|
|
74
93
|
requestMethod: req.method,
|
|
75
94
|
requestHeaders: req.headers
|
|
76
95
|
});
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
if (lockErrorResponse) return lockErrorResponse;
|
|
96
|
+
if (!guardResult.ok) {
|
|
97
|
+
return NextResponse.json(guardResult.errorBody ?? { error: "Operation blocked by guard" }, { status: guardResult.errorStatus ?? 422 });
|
|
80
98
|
}
|
|
81
99
|
const em = ctx.container.resolve("em").fork();
|
|
82
100
|
const quote = await em.findOne(SalesQuote, { id: input.quoteId, deletedAt: null });
|
|
@@ -132,8 +150,8 @@ async function POST(req) {
|
|
|
132
150
|
subject: translate("sales.quotes.email.subject", "Quote {quoteNumber}", { quoteNumber: quote.quoteNumber }),
|
|
133
151
|
react: QuoteSentEmail({ url, copy })
|
|
134
152
|
});
|
|
135
|
-
if (
|
|
136
|
-
await
|
|
153
|
+
if (guardResult.afterSuccessCallbacks.length) {
|
|
154
|
+
await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {
|
|
137
155
|
tenantId: ctx.auth?.tenantId ?? "",
|
|
138
156
|
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
139
157
|
userId: ctx.auth?.sub ?? "",
|
|
@@ -141,8 +159,7 @@ async function POST(req) {
|
|
|
141
159
|
resourceId: input.quoteId,
|
|
142
160
|
operation: "update",
|
|
143
161
|
requestMethod: req.method,
|
|
144
|
-
requestHeaders: req.headers
|
|
145
|
-
metadata: mutationGuardValidation.metadata ?? null
|
|
162
|
+
requestHeaders: req.headers
|
|
146
163
|
});
|
|
147
164
|
}
|
|
148
165
|
return NextResponse.json({ ok: true });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/sales/api/quotes/send/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations, detectLocale } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n type CrudMutationGuardValidationResult,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport crypto from 'node:crypto'\nimport { withScopedPayload } from '../../utils'\nimport { SalesQuote } from '../../../data/entities'\nimport { quoteSendSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { QuoteSentEmail } from '../../../emails/QuoteSentEmail'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nfunction buildMutationGuardErrorResponse(validation: CrudMutationGuardValidationResult): NextResponse | null {\n if (validation.ok) return null\n return NextResponse.json(validation.body, { status: validation.status })\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nfunction resolveQuoteEmail(quote: SalesQuote): string | null {\n const snapshot = quote.customerSnapshot && typeof quote.customerSnapshot === 'object' ? (quote.customerSnapshot as Record<string, unknown>) : null\n const metadata = quote.metadata && typeof quote.metadata === 'object' ? (quote.metadata as Record<string, unknown>) : null\n const contact = snapshot?.contact as Record<string, unknown> | undefined\n const customer = snapshot?.customer as Record<string, unknown> | undefined\n const candidate =\n (typeof contact?.email === 'string' && contact.email.trim()) ||\n (typeof customer?.primaryEmail === 'string' && customer.primaryEmail.trim()) ||\n (typeof metadata?.customerEmail === 'string' && metadata.customerEmail.trim()) ||\n null\n if (!candidate) return null\n const parsed = z.string().email().safeParse(candidate)\n return parsed.success ? parsed.data : null\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = quoteSendSchema.parse(scoped)\n const mutationGuardValidation = await validateCrudMutationGuard(ctx.container, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (mutationGuardValidation) {\n const lockErrorResponse = buildMutationGuardErrorResponse(mutationGuardValidation)\n if (lockErrorResponse) return lockErrorResponse\n }\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const quote = await em.findOne(SalesQuote, { id: input.quoteId, deletedAt: null })\n if (!quote) {\n throw new CrudHttpError(404, { error: translate('sales.documents.detail.error', 'Document not found or inaccessible.') })\n }\n if (quote.tenantId !== ctx.auth?.tenantId || quote.organizationId !== ctx.selectedOrganizationId) {\n throw new CrudHttpError(403, { error: translate('sales.documents.errors.forbidden', 'Forbidden') })\n }\n\n if ((quote.status ?? null) === 'canceled') {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.canceled', 'Canceled quotes cannot be sent.') })\n }\n\n const email = resolveQuoteEmail(quote)\n if (!email) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.missingEmail', 'Customer email is required to send a quote.') })\n }\n\n const now = new Date()\n const validUntil = new Date(now)\n validUntil.setUTCDate(validUntil.getUTCDate() + input.validForDays)\n\n quote.validUntil = validUntil\n quote.acceptanceToken = crypto.randomUUID()\n quote.sentAt = now\n quote.status = 'sent'\n quote.statusEntryId = await resolveStatusEntryIdByValue(em, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'sent',\n })\n quote.updatedAt = now\n em.persist(quote)\n await em.flush()\n\n const appUrl = process.env.APP_URL || ''\n const url = appUrl ? `${appUrl.replace(/\\/$/, '')}/quote/${quote.acceptanceToken}` : `/quote/${quote.acceptanceToken}`\n\n const locale = await detectLocale()\n const validUntilFormatted = validUntil.toLocaleDateString(locale, {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n })\n\n const copy = {\n preview: translate('sales.quotes.email.preview', 'Quote {quoteNumber} is ready for review', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.email.heading', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n total: translate('sales.quotes.email.total', 'Total: {amount} {currency}', {\n amount: quote.grandTotalGrossAmount ?? quote.grandTotalNetAmount ?? '0',\n currency: quote.currencyCode,\n }),\n validUntil: translate('sales.quotes.email.validUntil', 'Valid until: {date}', { date: validUntilFormatted }),\n cta: translate('sales.quotes.email.cta', 'View quote'),\n footer: translate('sales.quotes.email.footer', 'Open Mercato'),\n }\n\n await sendEmail({\n to: email,\n subject: translate('sales.quotes.email.subject', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n react: QuoteSentEmail({ url, copy }),\n })\n\n if (mutationGuardValidation?.ok && mutationGuardValidation.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: mutationGuardValidation.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('sales.quotes.send failed', err)\n return NextResponse.json(\n { error: translate('sales.quotes.send.failed', 'Failed to send quote.') },\n { status: 400 }\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Send quote to customer',\n methods: {\n POST: {\n summary: 'Send quote',\n requestBody: {\n contentType: 'application/json',\n schema: quoteSendSchema,\n },\n responses: [\n { status: 200, description: 'Email queued', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Not found', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AAEnD,SAAS,qBAAqB,oBAAoB;AAClD,SAAS,qBAAqB;AAE9B;AAAA,EACE;AAAA,EACA;AAAA,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations, detectLocale } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport crypto from 'node:crypto'\nimport { withScopedPayload } from '../../utils'\nimport { SalesQuote } from '../../../data/entities'\nimport { quoteSendSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { QuoteSentEmail } from '../../../emails/QuoteSentEmail'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n if (!legacyGuard) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n return runMutationGuards([legacyGuard], input, {\n userFeatures: resolveUserFeatures(ctx.auth),\n })\n}\n\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nfunction resolveQuoteEmail(quote: SalesQuote): string | null {\n const snapshot = quote.customerSnapshot && typeof quote.customerSnapshot === 'object' ? (quote.customerSnapshot as Record<string, unknown>) : null\n const metadata = quote.metadata && typeof quote.metadata === 'object' ? (quote.metadata as Record<string, unknown>) : null\n const contact = snapshot?.contact as Record<string, unknown> | undefined\n const customer = snapshot?.customer as Record<string, unknown> | undefined\n const candidate =\n (typeof contact?.email === 'string' && contact.email.trim()) ||\n (typeof customer?.primaryEmail === 'string' && customer.primaryEmail.trim()) ||\n (typeof metadata?.customerEmail === 'string' && metadata.customerEmail.trim()) ||\n null\n if (!candidate) return null\n const parsed = z.string().email().safeParse(candidate)\n return parsed.success ? parsed.data : null\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = quoteSendSchema.parse(scoped)\n const guardResult = await runGuards(ctx, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const quote = await em.findOne(SalesQuote, { id: input.quoteId, deletedAt: null })\n if (!quote) {\n throw new CrudHttpError(404, { error: translate('sales.documents.detail.error', 'Document not found or inaccessible.') })\n }\n if (quote.tenantId !== ctx.auth?.tenantId || quote.organizationId !== ctx.selectedOrganizationId) {\n throw new CrudHttpError(403, { error: translate('sales.documents.errors.forbidden', 'Forbidden') })\n }\n\n if ((quote.status ?? null) === 'canceled') {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.canceled', 'Canceled quotes cannot be sent.') })\n }\n\n const email = resolveQuoteEmail(quote)\n if (!email) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.missingEmail', 'Customer email is required to send a quote.') })\n }\n\n const now = new Date()\n const validUntil = new Date(now)\n validUntil.setUTCDate(validUntil.getUTCDate() + input.validForDays)\n\n quote.validUntil = validUntil\n quote.acceptanceToken = crypto.randomUUID()\n quote.sentAt = now\n quote.status = 'sent'\n quote.statusEntryId = await resolveStatusEntryIdByValue(em, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'sent',\n })\n quote.updatedAt = now\n em.persist(quote)\n await em.flush()\n\n const appUrl = process.env.APP_URL || ''\n const url = appUrl ? `${appUrl.replace(/\\/$/, '')}/quote/${quote.acceptanceToken}` : `/quote/${quote.acceptanceToken}`\n\n const locale = await detectLocale()\n const validUntilFormatted = validUntil.toLocaleDateString(locale, {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n })\n\n const copy = {\n preview: translate('sales.quotes.email.preview', 'Quote {quoteNumber} is ready for review', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.email.heading', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n total: translate('sales.quotes.email.total', 'Total: {amount} {currency}', {\n amount: quote.grandTotalGrossAmount ?? quote.grandTotalNetAmount ?? '0',\n currency: quote.currencyCode,\n }),\n validUntil: translate('sales.quotes.email.validUntil', 'Valid until: {date}', { date: validUntilFormatted }),\n cta: translate('sales.quotes.email.cta', 'View quote'),\n footer: translate('sales.quotes.email.footer', 'Open Mercato'),\n }\n\n await sendEmail({\n to: email,\n subject: translate('sales.quotes.email.subject', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n react: QuoteSentEmail({ url, copy }),\n })\n\n if (guardResult.afterSuccessCallbacks.length) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('sales.quotes.send failed', err)\n return NextResponse.json(\n { error: translate('sales.quotes.send.failed', 'Failed to send quote.') },\n { status: 400 }\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Send quote to customer',\n methods: {\n POST: {\n summary: 'Send quote',\n requestBody: {\n contentType: 'application/json',\n schema: quoteSendSchema,\n },\n responses: [\n { status: 200, description: 'Email queued', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Not found', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AAEnD,SAAS,qBAAqB,oBAAoB;AAClD,SAAS,qBAAqB;AAE9B;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAEP,OAAO,YAAY;AACnB,SAAS,yBAAyB;AAClC,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAC1B,SAAS,mCAAmC;AAC5C,SAAS,sBAAsB;AAExB,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;AAMA,SAAS,oBAAoB,MAAyB;AACpD,QAAM,WAAY,MAAiC;AACnD,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO,CAAC;AACtC,SAAO,SAAS,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAC9E;AAEA,eAAe,UACb,KACA,OAMC;AACD,QAAM,cAAc,kBAAkB,IAAI,SAAS;AACnD,MAAI,CAAC,aAAa;AAChB,WAAO,EAAE,IAAI,MAAM,uBAAuB,CAAC,EAAE;AAAA,EAC/C;AAEA,SAAO,kBAAkB,CAAC,WAAW,GAAG,OAAO;AAAA,IAC7C,cAAc,oBAAoB,IAAI,IAAI;AAAA,EAC5C,CAAC;AACH;AAEA,eAAe,8BACb,WACA,OAUe;AACf,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,MAAM,aAAc;AAClC,UAAM,SAAS,MAAM,aAAa;AAAA,MAChC,GAAG;AAAA,MACH,UAAU,SAAS,YAAY;AAAA,IACjC,CAAC;AAAA,EACH;AACF;AAEA,eAAe,sBAAsB,KAAuC;AAC1E,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAEhD,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,uCAAuC,cAAc,EAAE,CAAC;AAAA,EAC1G;AAEA,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,iBAAiB,OAAO,cAAc,KAAK,SAAS;AAC1D,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,cAAc,KAAK;AAAA,MAC3B,OAAO,UAAU,gDAAgD,kCAAkC;AAAA,IACrG,CAAC;AAAA,EACH;AAEA,QAAM,MAA6B;AAAA,IACjC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB;AAAA,IACxB,iBAAiB,OAAO,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAClE,SAAS;AAAA,EACX;AAEA,SAAO,EAAE,IAAI;AACf;AAEA,SAAS,kBAAkB,OAAkC;AAC3D,QAAM,WAAW,MAAM,oBAAoB,OAAO,MAAM,qBAAqB,WAAY,MAAM,mBAA+C;AAC9I,QAAMA,YAAW,MAAM,YAAY,OAAO,MAAM,aAAa,WAAY,MAAM,WAAuC;AACtH,QAAM,UAAU,UAAU;AAC1B,QAAM,WAAW,UAAU;AAC3B,QAAM,YACH,OAAO,SAAS,UAAU,YAAY,QAAQ,MAAM,KAAK,KACzD,OAAO,UAAU,iBAAiB,YAAY,SAAS,aAAa,KAAK,KACzE,OAAOA,WAAU,kBAAkB,YAAYA,UAAS,cAAc,KAAK,KAC5E;AACF,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,SAAS;AACrD,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,EAAE,IAAI,IAAI,MAAM,sBAAsB,GAAG;AAC/C,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACjD,UAAM,SAAS,kBAAkB,WAAW,CAAC,GAAG,KAAK,SAAS;AAC9D,UAAM,QAAQ,gBAAgB,MAAM,MAAM;AAC1C,UAAM,cAAc,MAAM,UAAU,KAAK;AAAA,MACvC,UAAU,IAAI,MAAM,YAAY;AAAA,MAChC,gBAAgB,IAAI,0BAA0B,IAAI,MAAM,SAAS;AAAA,MACjE,QAAQ,IAAI,MAAM,OAAO;AAAA,MACzB,cAAc;AAAA,MACd,YAAY,MAAM;AAAA,MAClB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,IACtB,CAAC;AACD,QAAI,CAAC,YAAY,IAAI;AACnB,aAAO,aAAa,KAAK,YAAY,aAAa,EAAE,OAAO,6BAA6B,GAAG,EAAE,QAAQ,YAAY,eAAe,IAAI,CAAC;AAAA,IACvI;AAEA,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,QAAQ,MAAM,GAAG,QAAQ,YAAY,EAAE,IAAI,MAAM,SAAS,WAAW,KAAK,CAAC;AACjF,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,gCAAgC,qCAAqC,EAAE,CAAC;AAAA,IAC1H;AACA,QAAI,MAAM,aAAa,IAAI,MAAM,YAAY,MAAM,mBAAmB,IAAI,wBAAwB;AAChG,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,oCAAoC,WAAW,EAAE,CAAC;AAAA,IACpG;AAEA,SAAK,MAAM,UAAU,UAAU,YAAY;AACzC,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,8BAA8B,iCAAiC,EAAE,CAAC;AAAA,IACpH;AAEA,UAAM,QAAQ,kBAAkB,KAAK;AACrC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,kCAAkC,6CAA6C,EAAE,CAAC;AAAA,IACpI;AAEA,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,aAAa,IAAI,KAAK,GAAG;AAC/B,eAAW,WAAW,WAAW,WAAW,IAAI,MAAM,YAAY;AAElE,UAAM,aAAa;AACnB,UAAM,kBAAkB,OAAO,WAAW;AAC1C,UAAM,SAAS;AACf,UAAM,SAAS;AACf,UAAM,gBAAgB,MAAM,4BAA4B,IAAI;AAAA,MAC1D,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,OAAO;AAAA,IACT,CAAC;AACD,UAAM,YAAY;AAClB,OAAG,QAAQ,KAAK;AAChB,UAAM,GAAG,MAAM;AAEf,UAAM,SAAS,QAAQ,IAAI,WAAW;AACtC,UAAM,MAAM,SAAS,GAAG,OAAO,QAAQ,OAAO,EAAE,CAAC,UAAU,MAAM,eAAe,KAAK,UAAU,MAAM,eAAe;AAEpH,UAAM,SAAS,MAAM,aAAa;AAClC,UAAM,sBAAsB,WAAW,mBAAmB,QAAQ;AAAA,MAChE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,KAAK;AAAA,IACP,CAAC;AAED,UAAM,OAAO;AAAA,MACX,SAAS,UAAU,8BAA8B,2CAA2C,EAAE,aAAa,MAAM,YAAY,CAAC;AAAA,MAC9H,SAAS,UAAU,8BAA8B,uBAAuB,EAAE,aAAa,MAAM,YAAY,CAAC;AAAA,MAC1G,OAAO,UAAU,4BAA4B,8BAA8B;AAAA,QACzE,QAAQ,MAAM,yBAAyB,MAAM,uBAAuB;AAAA,QACpE,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,MACD,YAAY,UAAU,iCAAiC,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAAA,MAC3G,KAAK,UAAU,0BAA0B,YAAY;AAAA,MACrD,QAAQ,UAAU,6BAA6B,cAAc;AAAA,IAC/D;AAEA,UAAM,UAAU;AAAA,MACd,IAAI;AAAA,MACJ,SAAS,UAAU,8BAA8B,uBAAuB,EAAE,aAAa,MAAM,YAAY,CAAC;AAAA,MAC1G,OAAO,eAAe,EAAE,KAAK,KAAK,CAAC;AAAA,IACrC,CAAC;AAED,QAAI,YAAY,sBAAsB,QAAQ;AAC5C,YAAM,8BAA8B,YAAY,uBAAuB;AAAA,QACrE,UAAU,IAAI,MAAM,YAAY;AAAA,QAChC,gBAAgB,IAAI,0BAA0B,IAAI,MAAM,SAAS;AAAA,QACjE,QAAQ,IAAI,MAAM,OAAO;AAAA,QACzB,cAAc;AAAA,QACd,YAAY,MAAM;AAAA,QAClB,WAAW;AAAA,QACX,eAAe,IAAI;AAAA,QACnB,gBAAgB,IAAI;AAAA,MACtB,CAAC;AAAA,IACH;AAEA,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,KAAK;AACZ,QAAI,eAAe,eAAe;AAChC,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,YAAQ,MAAM,4BAA4B,GAAG;AAC7C,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,4BAA4B,uBAAuB,EAAE;AAAA,MACxE,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,EAAE;AAAA,QACtF,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACvF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACpF,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE;AAAA,QACtH,EAAE,QAAQ,KAAK,aAAa,iBAAiB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE;AAAA,MACpH;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": ["metadata"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.6-develop-
|
|
3
|
+
"version": "0.4.6-develop-79b76432fd",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.6-develop-
|
|
210
|
+
"@open-mercato/shared": "0.4.6-develop-79b76432fd",
|
|
211
211
|
"@types/html-to-text": "^9.0.4",
|
|
212
212
|
"@types/semver": "^7.5.8",
|
|
213
213
|
"@xyflow/react": "^12.6.0",
|
|
@@ -52,9 +52,17 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
52
52
|
? await Promise.all(
|
|
53
53
|
getBundleIntegrations(integration.bundleId).map(async (item) => {
|
|
54
54
|
const itemState = await stateService.get(item.id, scope)
|
|
55
|
+
const resolvedState = {
|
|
56
|
+
isEnabled: itemState?.isEnabled ?? true,
|
|
57
|
+
apiVersion: itemState?.apiVersion ?? null,
|
|
58
|
+
reauthRequired: itemState?.reauthRequired ?? false,
|
|
59
|
+
lastHealthStatus: itemState?.lastHealthStatus ?? null,
|
|
60
|
+
lastHealthCheckedAt: itemState?.lastHealthCheckedAt?.toISOString() ?? null,
|
|
61
|
+
}
|
|
55
62
|
return {
|
|
56
63
|
...item,
|
|
57
|
-
isEnabled:
|
|
64
|
+
isEnabled: resolvedState.isEnabled,
|
|
65
|
+
state: resolvedState,
|
|
58
66
|
}
|
|
59
67
|
}),
|
|
60
68
|
)
|
|
@@ -13,12 +13,18 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primi
|
|
|
13
13
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
14
14
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
15
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
16
|
-
import type { IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
|
|
16
|
+
import type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
|
|
17
17
|
import { LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
18
18
|
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
19
19
|
|
|
20
20
|
type CredentialField = IntegrationCredentialField
|
|
21
21
|
|
|
22
|
+
const UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])
|
|
23
|
+
|
|
24
|
+
function isEditableCredentialField(field: CredentialField): boolean {
|
|
25
|
+
return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
type ApiVersion = {
|
|
23
29
|
id: string
|
|
24
30
|
label?: string
|
|
@@ -261,7 +267,7 @@ export default function IntegrationDetailPage() {
|
|
|
261
267
|
) : (
|
|
262
268
|
<Card>
|
|
263
269
|
<CardContent className="pt-6 space-y-4">
|
|
264
|
-
{credFields.filter(
|
|
270
|
+
{credFields.filter(isEditableCredentialField).map((field) => (
|
|
265
271
|
<div key={field.key} className="space-y-1.5">
|
|
266
272
|
<label className="text-sm font-medium">
|
|
267
273
|
{field.label}{field.required && <span className="text-red-500 ml-0.5">*</span>}
|
|
@@ -12,12 +12,18 @@ import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
|
12
12
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
13
13
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
14
14
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
|
-
import type { IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
|
|
15
|
+
import type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
|
|
16
16
|
import { LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
17
17
|
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
18
18
|
|
|
19
19
|
type CredentialField = IntegrationCredentialField
|
|
20
20
|
|
|
21
|
+
const UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])
|
|
22
|
+
|
|
23
|
+
function isEditableCredentialField(field: CredentialField): boolean {
|
|
24
|
+
return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
type BundleIntegration = {
|
|
22
28
|
id: string
|
|
23
29
|
title: string
|
|
@@ -131,7 +137,7 @@ export default function BundleConfigPage() {
|
|
|
131
137
|
if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>
|
|
132
138
|
if (error || !detail?.bundle) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>
|
|
133
139
|
|
|
134
|
-
const credFields = detail.bundle.credentials?.fields ?? []
|
|
140
|
+
const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)
|
|
135
141
|
|
|
136
142
|
return (
|
|
137
143
|
<Page>
|
|
@@ -3,10 +3,14 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
3
3
|
import { decryptWithAesGcm, encryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'
|
|
4
4
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
5
|
import { createKmsService } from '@open-mercato/shared/lib/encryption/kms'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getBundle,
|
|
8
|
+
getIntegration,
|
|
9
|
+
resolveIntegrationCredentialsSchema,
|
|
10
|
+
type IntegrationScope,
|
|
11
|
+
} from '@open-mercato/shared/modules/integrations/types'
|
|
7
12
|
import { EncryptionMap } from '../../entities/data/entities'
|
|
8
13
|
import { IntegrationCredentials } from '../data/entities'
|
|
9
|
-
import type { IntegrationScope } from './types'
|
|
10
14
|
|
|
11
15
|
const ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'
|
|
12
16
|
const DERIVED_KEY_CONTEXT = 'integrations.credentials'
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { AwilixContainer } from 'awilix'
|
|
2
2
|
import type { IntegrationStateService } from './state-service'
|
|
3
3
|
import type { IntegrationLogService } from './log-service'
|
|
4
|
-
import { getIntegration, getBundle } from '@open-mercato/shared/modules/integrations/types'
|
|
5
|
-
import type { IntegrationScope } from './types'
|
|
4
|
+
import { getIntegration, getBundle, type IntegrationScope } from '@open-mercato/shared/modules/integrations/types'
|
|
6
5
|
|
|
7
6
|
type HealthCheckResult = {
|
|
8
7
|
status: 'healthy' | 'degraded' | 'unhealthy'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
2
2
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'
|
|
3
4
|
import type { ListIntegrationLogsQuery } from '../data/validators'
|
|
4
5
|
import { IntegrationLog } from '../data/entities'
|
|
5
|
-
import type { IntegrationScope } from './types'
|
|
6
6
|
|
|
7
7
|
type LogInput = {
|
|
8
8
|
integrationId: string
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'
|
|
3
4
|
import { IntegrationState } from '../data/entities'
|
|
4
|
-
import type { IntegrationScope } from './types'
|
|
5
5
|
|
|
6
6
|
export function createIntegrationStateService(em: EntityManager) {
|
|
7
7
|
return {
|
|
@@ -9,10 +9,11 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
|
9
9
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
10
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type
|
|
15
|
-
|
|
12
|
+
bridgeLegacyGuard,
|
|
13
|
+
runMutationGuards,
|
|
14
|
+
type MutationGuard,
|
|
15
|
+
type MutationGuardInput,
|
|
16
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard-registry'
|
|
16
17
|
import { withScopedPayload } from '../../utils'
|
|
17
18
|
|
|
18
19
|
const convertSchema = z.object({
|
|
@@ -29,9 +30,51 @@ type RequestContext = {
|
|
|
29
30
|
ctx: CommandRuntimeContext
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
function resolveUserFeatures(auth: unknown): string[] {
|
|
34
|
+
const features = (auth as { features?: unknown })?.features
|
|
35
|
+
if (!Array.isArray(features)) return []
|
|
36
|
+
return features.filter((value): value is string => typeof value === 'string')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function runGuards(
|
|
40
|
+
ctx: CommandRuntimeContext,
|
|
41
|
+
input: MutationGuardInput,
|
|
42
|
+
): Promise<{
|
|
43
|
+
ok: boolean
|
|
44
|
+
errorBody?: Record<string, unknown>
|
|
45
|
+
errorStatus?: number
|
|
46
|
+
afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>
|
|
47
|
+
}> {
|
|
48
|
+
const legacyGuard = bridgeLegacyGuard(ctx.container)
|
|
49
|
+
if (!legacyGuard) {
|
|
50
|
+
return { ok: true, afterSuccessCallbacks: [] }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return runMutationGuards([legacyGuard], input, {
|
|
54
|
+
userFeatures: resolveUserFeatures(ctx.auth),
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runGuardAfterSuccessCallbacks(
|
|
59
|
+
callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,
|
|
60
|
+
input: {
|
|
61
|
+
tenantId: string
|
|
62
|
+
organizationId: string | null
|
|
63
|
+
userId: string
|
|
64
|
+
resourceKind: string
|
|
65
|
+
resourceId: string
|
|
66
|
+
operation: 'create' | 'update' | 'delete'
|
|
67
|
+
requestMethod: string
|
|
68
|
+
requestHeaders: Headers
|
|
69
|
+
},
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
for (const callback of callbacks) {
|
|
72
|
+
if (!callback.guard.afterSuccess) continue
|
|
73
|
+
await callback.guard.afterSuccess({
|
|
74
|
+
...input,
|
|
75
|
+
metadata: callback.metadata ?? null,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
35
78
|
}
|
|
36
79
|
|
|
37
80
|
async function resolveRequestContext(req: Request): Promise<RequestContext> {
|
|
@@ -70,7 +113,7 @@ export async function POST(req: Request) {
|
|
|
70
113
|
const payload = await req.json().catch(() => ({}))
|
|
71
114
|
const scoped = withScopedPayload(payload ?? {}, ctx, translate)
|
|
72
115
|
const input = convertSchema.parse(scoped)
|
|
73
|
-
const
|
|
116
|
+
const guardResult = await runGuards(ctx, {
|
|
74
117
|
tenantId: ctx.auth?.tenantId ?? '',
|
|
75
118
|
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
76
119
|
userId: ctx.auth?.sub ?? '',
|
|
@@ -80,9 +123,8 @@ export async function POST(req: Request) {
|
|
|
80
123
|
requestMethod: req.method,
|
|
81
124
|
requestHeaders: req.headers,
|
|
82
125
|
})
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
if (lockErrorResponse) return lockErrorResponse
|
|
126
|
+
if (!guardResult.ok) {
|
|
127
|
+
return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })
|
|
86
128
|
}
|
|
87
129
|
const commandBus = ctx.container.resolve('commandBus') as CommandBus
|
|
88
130
|
const { result, logEntry } = await commandBus.execute<
|
|
@@ -112,8 +154,8 @@ export async function POST(req: Request) {
|
|
|
112
154
|
)
|
|
113
155
|
}
|
|
114
156
|
|
|
115
|
-
if (
|
|
116
|
-
await
|
|
157
|
+
if (guardResult.afterSuccessCallbacks.length) {
|
|
158
|
+
await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {
|
|
117
159
|
tenantId: ctx.auth?.tenantId ?? '',
|
|
118
160
|
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
119
161
|
userId: ctx.auth?.sub ?? '',
|
|
@@ -122,7 +164,6 @@ export async function POST(req: Request) {
|
|
|
122
164
|
operation: 'update',
|
|
123
165
|
requestMethod: req.method,
|
|
124
166
|
requestHeaders: req.headers,
|
|
125
|
-
metadata: mutationGuardValidation.metadata ?? null,
|
|
126
167
|
})
|
|
127
168
|
}
|
|
128
169
|
|
|
@@ -8,10 +8,11 @@ import { resolveTranslations, detectLocale } from '@open-mercato/shared/lib/i18n
|
|
|
8
8
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type
|
|
14
|
-
|
|
11
|
+
bridgeLegacyGuard,
|
|
12
|
+
runMutationGuards,
|
|
13
|
+
type MutationGuard,
|
|
14
|
+
type MutationGuardInput,
|
|
15
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard-registry'
|
|
15
16
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
16
17
|
import crypto from 'node:crypto'
|
|
17
18
|
import { withScopedPayload } from '../../utils'
|
|
@@ -29,9 +30,51 @@ type RequestContext = {
|
|
|
29
30
|
ctx: CommandRuntimeContext
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
function resolveUserFeatures(auth: unknown): string[] {
|
|
34
|
+
const features = (auth as { features?: unknown })?.features
|
|
35
|
+
if (!Array.isArray(features)) return []
|
|
36
|
+
return features.filter((value): value is string => typeof value === 'string')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function runGuards(
|
|
40
|
+
ctx: CommandRuntimeContext,
|
|
41
|
+
input: MutationGuardInput,
|
|
42
|
+
): Promise<{
|
|
43
|
+
ok: boolean
|
|
44
|
+
errorBody?: Record<string, unknown>
|
|
45
|
+
errorStatus?: number
|
|
46
|
+
afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>
|
|
47
|
+
}> {
|
|
48
|
+
const legacyGuard = bridgeLegacyGuard(ctx.container)
|
|
49
|
+
if (!legacyGuard) {
|
|
50
|
+
return { ok: true, afterSuccessCallbacks: [] }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return runMutationGuards([legacyGuard], input, {
|
|
54
|
+
userFeatures: resolveUserFeatures(ctx.auth),
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runGuardAfterSuccessCallbacks(
|
|
59
|
+
callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,
|
|
60
|
+
input: {
|
|
61
|
+
tenantId: string
|
|
62
|
+
organizationId: string | null
|
|
63
|
+
userId: string
|
|
64
|
+
resourceKind: string
|
|
65
|
+
resourceId: string
|
|
66
|
+
operation: 'create' | 'update' | 'delete'
|
|
67
|
+
requestMethod: string
|
|
68
|
+
requestHeaders: Headers
|
|
69
|
+
},
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
for (const callback of callbacks) {
|
|
72
|
+
if (!callback.guard.afterSuccess) continue
|
|
73
|
+
await callback.guard.afterSuccess({
|
|
74
|
+
...input,
|
|
75
|
+
metadata: callback.metadata ?? null,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
35
78
|
}
|
|
36
79
|
|
|
37
80
|
async function resolveRequestContext(req: Request): Promise<RequestContext> {
|
|
@@ -85,7 +128,7 @@ export async function POST(req: Request) {
|
|
|
85
128
|
const payload = await req.json().catch(() => ({}))
|
|
86
129
|
const scoped = withScopedPayload(payload ?? {}, ctx, translate)
|
|
87
130
|
const input = quoteSendSchema.parse(scoped)
|
|
88
|
-
const
|
|
131
|
+
const guardResult = await runGuards(ctx, {
|
|
89
132
|
tenantId: ctx.auth?.tenantId ?? '',
|
|
90
133
|
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
91
134
|
userId: ctx.auth?.sub ?? '',
|
|
@@ -95,9 +138,8 @@ export async function POST(req: Request) {
|
|
|
95
138
|
requestMethod: req.method,
|
|
96
139
|
requestHeaders: req.headers,
|
|
97
140
|
})
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
if (lockErrorResponse) return lockErrorResponse
|
|
141
|
+
if (!guardResult.ok) {
|
|
142
|
+
return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })
|
|
101
143
|
}
|
|
102
144
|
|
|
103
145
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
@@ -163,8 +205,8 @@ export async function POST(req: Request) {
|
|
|
163
205
|
react: QuoteSentEmail({ url, copy }),
|
|
164
206
|
})
|
|
165
207
|
|
|
166
|
-
if (
|
|
167
|
-
await
|
|
208
|
+
if (guardResult.afterSuccessCallbacks.length) {
|
|
209
|
+
await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {
|
|
168
210
|
tenantId: ctx.auth?.tenantId ?? '',
|
|
169
211
|
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
170
212
|
userId: ctx.auth?.sub ?? '',
|
|
@@ -173,7 +215,6 @@ export async function POST(req: Request) {
|
|
|
173
215
|
operation: 'update',
|
|
174
216
|
requestMethod: req.method,
|
|
175
217
|
requestHeaders: req.headers,
|
|
176
|
-
metadata: mutationGuardValidation.metadata ?? null,
|
|
177
218
|
})
|
|
178
219
|
}
|
|
179
220
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=types.js.map
|