@open-mercato/core 0.6.8-develop.7032.1.70d74d4bfe → 0.6.8-develop.7038.1.ea954afd80
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/customers/ai-tools/deal-analyzer-pack.js +13 -2
- package/dist/modules/customers/ai-tools/deal-analyzer-pack.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +6 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +38 -29
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +36 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/credential-secret-fields.js +55 -0
- package/dist/modules/integrations/backend/integrations/credential-secret-fields.js.map +7 -0
- package/dist/modules/integrations/data/validators.js +11 -3
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-masking.js +6 -2
- package/dist/modules/integrations/lib/credentials-masking.js.map +2 -2
- package/dist/modules/wms/api/zones/route.js +15 -2
- package/dist/modules/wms/api/zones/route.js.map +2 -2
- package/dist/modules/wms/commands/configuration.js +64 -6
- package/dist/modules/wms/commands/configuration.js.map +2 -2
- package/dist/modules/wms/commands/shared.js +5 -1
- package/dist/modules/wms/commands/shared.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js +43 -8
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +3 -3
- package/package.json +7 -7
- package/src/modules/customers/ai-tools/deal-analyzer-pack.ts +17 -2
- package/src/modules/integrations/api/[id]/credentials/route.ts +6 -4
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +61 -31
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +44 -9
- package/src/modules/integrations/backend/integrations/credential-secret-fields.ts +84 -0
- package/src/modules/integrations/data/validators.ts +12 -2
- package/src/modules/integrations/i18n/de.json +1 -0
- package/src/modules/integrations/i18n/en.json +1 -0
- package/src/modules/integrations/i18n/es.json +1 -0
- package/src/modules/integrations/i18n/ko.json +1 -0
- package/src/modules/integrations/i18n/pl.json +1 -0
- package/src/modules/integrations/lib/credentials-masking.ts +10 -6
- package/src/modules/wms/api/zones/route.ts +16 -0
- package/src/modules/wms/commands/configuration.ts +89 -6
- package/src/modules/wms/commands/shared.ts +8 -0
- package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +65 -8
package/.turbo/turbo-build.log
CHANGED
|
@@ -106,14 +106,25 @@ const analyzeDealsToolDefinition = {
|
|
|
106
106
|
lastActivityByDeal.set(refId, activityDate);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
const dealById = new Map(deals.map((deal) => [deal.id, deal]));
|
|
110
|
+
const scopedDealIds = Array.from(dealById.keys());
|
|
109
111
|
const personLinkWhere = {
|
|
110
|
-
deal: { $in:
|
|
112
|
+
deal: { $in: scopedDealIds }
|
|
111
113
|
};
|
|
112
|
-
const
|
|
114
|
+
const personLinksRaw = await em.find(
|
|
113
115
|
CustomerDealPersonLink,
|
|
114
116
|
personLinkWhere,
|
|
115
117
|
{ limit: 500 }
|
|
116
118
|
);
|
|
119
|
+
const personLinks = personLinksRaw.filter((link) => {
|
|
120
|
+
const dealRefId = refIdOf(link.deal);
|
|
121
|
+
if (!dealRefId) return false;
|
|
122
|
+
const scopedDeal = dealById.get(dealRefId);
|
|
123
|
+
if (!scopedDeal) return false;
|
|
124
|
+
if (scopedDeal.tenantId !== tenantId) return false;
|
|
125
|
+
if (ctx.organizationId && scopedDeal.organizationId !== ctx.organizationId) return false;
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
117
128
|
const linksByDeal = /* @__PURE__ */ new Map();
|
|
118
129
|
for (const link of personLinks) {
|
|
119
130
|
const dealRefId = refIdOf(link.deal);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/ai-tools/deal-analyzer-pack.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `customers.analyze_deals` \u2014 analytical read-only tool for the deal_analyzer\n * agent (Step d1 of the deal-analyzer-demo).\n *\n * Reads deals scoped to the caller tenant/org, enriches each deal with the\n * most recent activity timestamp (days-since-last-activity), computes a simple\n * healthScore, and returns a ranked list. The handler issues two bounded DB\n * reads (deals + activities) \u2014 no N+1.\n *\n * `customers.update_deal_stage` already exists in deals-pack.ts and is\n * imported and re-exported here so the deal_analyzer agent's `allowedTools`\n * list can reference both under a single pack. The tool itself is NOT\n * redeclared here.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerActivity,\n CustomerDealPersonLink,\n CustomerEntity,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n} from './types'\n\nfunction refIdOf(ref: unknown): string | undefined {\n if (!ref) return undefined\n if (typeof ref === 'string') return ref\n if (typeof ref === 'object' && typeof (ref as { id?: unknown }).id === 'string') {\n return (ref as { id: string }).id\n }\n return undefined\n}\n\nfunction resolveEm(ctx: CustomersToolContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * Compute a health score for a deal:\n * healthScore = clamp((30 - daysSinceLastActivity) / 30 * 100, 0, 100)\n * A deal with activity within the last 30 days scores > 0. Deals with no\n * activity default to daysSinceLastActivity = activityWindow so they score 0.\n * Exported for unit testing.\n */\nexport function computeHealthScore(daysSinceLastActivity: number): number {\n return clamp(((30 - daysSinceLastActivity) / 30) * 100, 0, 100)\n}\n\nconst analyzeDealsInput = z.object({\n dealStageFilter: z\n .string()\n .optional()\n .describe(\n 'Optional pipeline stage name or status slug to restrict results (e.g. \"open\", \"qualification\", \"negotiation\").',\n ),\n daysOfActivityWindow: z\n .number()\n .int()\n .min(1)\n .max(365)\n .default(30)\n .describe(\n 'Number of days to look back for \"last activity\" calculation. Deals with no activity in this window are flagged as stalled. Default 30.',\n ),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .default(25)\n .describe('Maximum number of deals to return, ranked by healthScore ascending (most at-risk first). Default 25.'),\n})\n\ntype AnalyzeDealsInput = z.infer<typeof analyzeDealsInput>\n\ntype DealAnalysisItem = {\n id: string\n title: string\n value: number | null\n valueCurrency: string | null\n stage: string | null\n status: string\n daysSinceLastActivity: number\n primaryContact: string | null\n healthScore: number\n}\n\ntype AnalyzeDealsOutput = {\n deals: DealAnalysisItem[]\n totalAnalyzed: number\n stalledCount: number\n windowDays: number\n}\n\nexport function msTodays(ms: number): number {\n return Math.floor(ms / (1000 * 60 * 60 * 24))\n}\n\nconst analyzeDealsToolDefinition: CustomersAiToolDefinition<AnalyzeDealsInput, AnalyzeDealsOutput> = {\n name: 'customers.analyze_deals',\n displayName: 'Analyze deals',\n description:\n 'Fetch and analyze open deals for the caller tenant. Returns each deal with its days-since-last-activity and a health score (0\u2013100, lower = more stalled). Results are ranked most-at-risk first.',\n inputSchema: analyzeDealsInput as z.ZodType<AnalyzeDealsInput>,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers', 'analytics'],\n isMutation: false,\n handler: async (rawInput, ctx): Promise<AnalyzeDealsOutput> => {\n const { tenantId } = assertTenantScope(ctx)\n const input = analyzeDealsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const scope = { tenantId, organizationId: ctx.organizationId }\n\n // Build deal filter\n const dealWhere: Record<string, unknown> = {\n tenantId,\n deletedAt: null,\n }\n if (ctx.organizationId) dealWhere.organizationId = ctx.organizationId\n // Apply stage/status filter when provided. `$or` is composed with the\n // outer `deletedAt: null` filter \u2014 both must hold \u2014 so the caller-supplied\n // value matches either the canonical status slug or the stage label.\n if (input.dealStageFilter) {\n const filterValue = input.dealStageFilter.trim()\n if (filterValue.length) {\n dealWhere.$or = [\n { status: filterValue },\n { pipelineStage: filterValue },\n ]\n }\n }\n\n const deals = await findWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n dealWhere as any,\n {\n orderBy: { updatedAt: 'desc' },\n limit: Math.min(input.limit * 4, 400), // Fetch extra so we can rank properly\n },\n scope,\n )\n\n if (!deals.length) {\n return { deals: [], totalAnalyzed: 0, stalledCount: 0, windowDays: input.daysOfActivityWindow }\n }\n\n const dealIds = deals.map((d) => d.id)\n\n // Fetch most recent activity per deal within the window in one batch.\n // CustomerActivity has encrypted columns (subject, body); use the\n // tenant-aware helper even though this query only reads occurredAt/deal,\n // so future readers cannot accidentally surface ciphertext.\n const activityWhere: Record<string, unknown> = {\n deal: { $in: dealIds },\n tenantId,\n }\n if (ctx.organizationId) activityWhere.organizationId = ctx.organizationId\n const activities = await findWithDecryption<CustomerActivity>(\n em,\n CustomerActivity,\n activityWhere as any,\n {\n orderBy: { occurredAt: 'desc' },\n limit: 1000,\n },\n scope,\n )\n\n const lastActivityByDeal = new Map<string, Date>()\n for (const activity of activities) {\n const refId = refIdOf((activity as { deal?: unknown }).deal)\n if (!refId) continue\n const activityDate = activity.occurredAt ?? activity.createdAt\n if (!activityDate) continue\n const existing = lastActivityByDeal.get(refId)\n if (!existing || activityDate > existing) {\n lastActivityByDeal.set(refId, activityDate)\n }\n }\n\n // Fetch primary contacts for deals in one batch. CustomerDealPersonLink\n // has no encrypted columns, but the linked CustomerEntity.display_name is\n // encrypted, so resolve names via findWithDecryption rather than\n // populating the relation through raw em.find.\n const personLinkWhere: Record<string, unknown> = {\n deal: { $in: dealIds },\n }\n const personLinks = await em.find(\n CustomerDealPersonLink,\n personLinkWhere as any,\n { limit: 500 },\n )\n\n const linksByDeal = new Map<string, string>() // dealId \u2192 personId (first link wins)\n for (const link of personLinks) {\n const dealRefId = refIdOf((link as { deal?: unknown }).deal)\n const personRefId = refIdOf((link as { person?: unknown }).person)\n if (!dealRefId || !personRefId) continue\n if (!linksByDeal.has(dealRefId)) linksByDeal.set(dealRefId, personRefId)\n }\n\n const primaryContactByDeal = new Map<string, string>()\n const personIds = Array.from(new Set(linksByDeal.values()))\n if (personIds.length) {\n const personWhere: Record<string, unknown> = {\n id: { $in: personIds },\n tenantId,\n }\n if (ctx.organizationId) personWhere.organizationId = ctx.organizationId\n const persons = await findWithDecryption<CustomerEntity>(\n em,\n CustomerEntity,\n personWhere as any,\n undefined,\n scope,\n )\n const nameByPersonId = new Map<string, string>()\n for (const person of persons) {\n const name = person.displayName\n if (typeof name === 'string' && name.length) {\n nameByPersonId.set(person.id, name)\n }\n }\n for (const [dealId, personId] of linksByDeal) {\n const name = nameByPersonId.get(personId)\n if (name) primaryContactByDeal.set(dealId, name)\n }\n }\n\n const now = Date.now()\n const analyzed: DealAnalysisItem[] = deals.map((deal) => {\n const lastActivity = lastActivityByDeal.get(deal.id)\n const daysSinceLastActivity = lastActivity\n ? msTodays(now - lastActivity.getTime())\n : input.daysOfActivityWindow // Treat as fully stalled when no activity\n const value = deal.valueAmount ? parseFloat(String(deal.valueAmount)) : null\n return {\n id: deal.id,\n title: deal.title,\n value: Number.isFinite(value) ? value : null,\n valueCurrency: deal.valueCurrency ?? null,\n stage: deal.pipelineStage ?? deal.status ?? null,\n status: deal.status ?? 'open',\n daysSinceLastActivity,\n primaryContact: primaryContactByDeal.get(deal.id) ?? null,\n healthScore: Math.round(computeHealthScore(daysSinceLastActivity)),\n }\n })\n\n // Sort most-at-risk first (lowest health score first, then by value desc)\n analyzed.sort((a, b) => {\n if (a.healthScore !== b.healthScore) return a.healthScore - b.healthScore\n const aVal = a.value ?? 0\n const bVal = b.value ?? 0\n return bVal - aVal\n })\n\n const truncated = analyzed.slice(0, input.limit)\n const stalledCount = truncated.filter(\n (d) => d.daysSinceLastActivity >= input.daysOfActivityWindow,\n ).length\n\n return {\n deals: truncated,\n totalAnalyzed: deals.length,\n stalledCount,\n windowDays: input.daysOfActivityWindow,\n }\n },\n}\n\nexport const dealAnalyzerAiTools: CustomersAiToolDefinition[] = [\n analyzeDealsToolDefinition as unknown as CustomersAiToolDefinition,\n]\n\nexport default dealAnalyzerAiTools\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * `customers.analyze_deals` \u2014 analytical read-only tool for the deal_analyzer\n * agent (Step d1 of the deal-analyzer-demo).\n *\n * Reads deals scoped to the caller tenant/org, enriches each deal with the\n * most recent activity timestamp (days-since-last-activity), computes a simple\n * healthScore, and returns a ranked list. The handler issues two bounded DB\n * reads (deals + activities) \u2014 no N+1.\n *\n * `customers.update_deal_stage` already exists in deals-pack.ts and is\n * imported and re-exported here so the deal_analyzer agent's `allowedTools`\n * list can reference both under a single pack. The tool itself is NOT\n * redeclared here.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerActivity,\n CustomerDealPersonLink,\n CustomerEntity,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n} from './types'\n\nfunction refIdOf(ref: unknown): string | undefined {\n if (!ref) return undefined\n if (typeof ref === 'string') return ref\n if (typeof ref === 'object' && typeof (ref as { id?: unknown }).id === 'string') {\n return (ref as { id: string }).id\n }\n return undefined\n}\n\nfunction resolveEm(ctx: CustomersToolContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * Compute a health score for a deal:\n * healthScore = clamp((30 - daysSinceLastActivity) / 30 * 100, 0, 100)\n * A deal with activity within the last 30 days scores > 0. Deals with no\n * activity default to daysSinceLastActivity = activityWindow so they score 0.\n * Exported for unit testing.\n */\nexport function computeHealthScore(daysSinceLastActivity: number): number {\n return clamp(((30 - daysSinceLastActivity) / 30) * 100, 0, 100)\n}\n\nconst analyzeDealsInput = z.object({\n dealStageFilter: z\n .string()\n .optional()\n .describe(\n 'Optional pipeline stage name or status slug to restrict results (e.g. \"open\", \"qualification\", \"negotiation\").',\n ),\n daysOfActivityWindow: z\n .number()\n .int()\n .min(1)\n .max(365)\n .default(30)\n .describe(\n 'Number of days to look back for \"last activity\" calculation. Deals with no activity in this window are flagged as stalled. Default 30.',\n ),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .default(25)\n .describe('Maximum number of deals to return, ranked by healthScore ascending (most at-risk first). Default 25.'),\n})\n\ntype AnalyzeDealsInput = z.infer<typeof analyzeDealsInput>\n\ntype DealAnalysisItem = {\n id: string\n title: string\n value: number | null\n valueCurrency: string | null\n stage: string | null\n status: string\n daysSinceLastActivity: number\n primaryContact: string | null\n healthScore: number\n}\n\ntype AnalyzeDealsOutput = {\n deals: DealAnalysisItem[]\n totalAnalyzed: number\n stalledCount: number\n windowDays: number\n}\n\nexport function msTodays(ms: number): number {\n return Math.floor(ms / (1000 * 60 * 60 * 24))\n}\n\nconst analyzeDealsToolDefinition: CustomersAiToolDefinition<AnalyzeDealsInput, AnalyzeDealsOutput> = {\n name: 'customers.analyze_deals',\n displayName: 'Analyze deals',\n description:\n 'Fetch and analyze open deals for the caller tenant. Returns each deal with its days-since-last-activity and a health score (0\u2013100, lower = more stalled). Results are ranked most-at-risk first.',\n inputSchema: analyzeDealsInput as z.ZodType<AnalyzeDealsInput>,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers', 'analytics'],\n isMutation: false,\n handler: async (rawInput, ctx): Promise<AnalyzeDealsOutput> => {\n const { tenantId } = assertTenantScope(ctx)\n const input = analyzeDealsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const scope = { tenantId, organizationId: ctx.organizationId }\n\n // Build deal filter\n const dealWhere: Record<string, unknown> = {\n tenantId,\n deletedAt: null,\n }\n if (ctx.organizationId) dealWhere.organizationId = ctx.organizationId\n // Apply stage/status filter when provided. `$or` is composed with the\n // outer `deletedAt: null` filter \u2014 both must hold \u2014 so the caller-supplied\n // value matches either the canonical status slug or the stage label.\n if (input.dealStageFilter) {\n const filterValue = input.dealStageFilter.trim()\n if (filterValue.length) {\n dealWhere.$or = [\n { status: filterValue },\n { pipelineStage: filterValue },\n ]\n }\n }\n\n const deals = await findWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n dealWhere as any,\n {\n orderBy: { updatedAt: 'desc' },\n limit: Math.min(input.limit * 4, 400), // Fetch extra so we can rank properly\n },\n scope,\n )\n\n if (!deals.length) {\n return { deals: [], totalAnalyzed: 0, stalledCount: 0, windowDays: input.daysOfActivityWindow }\n }\n\n const dealIds = deals.map((d) => d.id)\n\n // Fetch most recent activity per deal within the window in one batch.\n // CustomerActivity has encrypted columns (subject, body); use the\n // tenant-aware helper even though this query only reads occurredAt/deal,\n // so future readers cannot accidentally surface ciphertext.\n const activityWhere: Record<string, unknown> = {\n deal: { $in: dealIds },\n tenantId,\n }\n if (ctx.organizationId) activityWhere.organizationId = ctx.organizationId\n const activities = await findWithDecryption<CustomerActivity>(\n em,\n CustomerActivity,\n activityWhere as any,\n {\n orderBy: { occurredAt: 'desc' },\n limit: 1000,\n },\n scope,\n )\n\n const lastActivityByDeal = new Map<string, Date>()\n for (const activity of activities) {\n const refId = refIdOf((activity as { deal?: unknown }).deal)\n if (!refId) continue\n const activityDate = activity.occurredAt ?? activity.createdAt\n if (!activityDate) continue\n const existing = lastActivityByDeal.get(refId)\n if (!existing || activityDate > existing) {\n lastActivityByDeal.set(refId, activityDate)\n }\n }\n\n // Fetch primary contacts for deals in one batch. CustomerDealPersonLink\n // has no encrypted columns, but the linked CustomerEntity.display_name is\n // encrypted, so resolve names via findWithDecryption rather than\n // populating the relation through raw em.find.\n // Defense-in-depth: `customer_deal_people` carries no tenant/org columns,\n // so DB-level scoping is via the tenant-filtered `dealIds` set. Filter the\n // returned rows against the caller-scoped deals map so a future refactor\n // that widens `dealIds` cannot surface cross-tenant links.\n const dealById = new Map<string, CustomerDeal>(deals.map((deal) => [deal.id, deal]))\n const scopedDealIds = Array.from(dealById.keys())\n const personLinkWhere: Record<string, unknown> = {\n deal: { $in: scopedDealIds },\n }\n const personLinksRaw = await em.find(\n CustomerDealPersonLink,\n personLinkWhere as any,\n { limit: 500 },\n )\n const personLinks = personLinksRaw.filter((link) => {\n const dealRefId = refIdOf((link as { deal?: unknown }).deal)\n if (!dealRefId) return false\n const scopedDeal = dealById.get(dealRefId)\n if (!scopedDeal) return false\n if (scopedDeal.tenantId !== tenantId) return false\n if (ctx.organizationId && scopedDeal.organizationId !== ctx.organizationId) return false\n return true\n })\n\n const linksByDeal = new Map<string, string>() // dealId \u2192 personId (first link wins)\n for (const link of personLinks) {\n const dealRefId = refIdOf((link as { deal?: unknown }).deal)\n const personRefId = refIdOf((link as { person?: unknown }).person)\n if (!dealRefId || !personRefId) continue\n if (!linksByDeal.has(dealRefId)) linksByDeal.set(dealRefId, personRefId)\n }\n\n const primaryContactByDeal = new Map<string, string>()\n const personIds = Array.from(new Set(linksByDeal.values()))\n if (personIds.length) {\n const personWhere: Record<string, unknown> = {\n id: { $in: personIds },\n tenantId,\n }\n if (ctx.organizationId) personWhere.organizationId = ctx.organizationId\n const persons = await findWithDecryption<CustomerEntity>(\n em,\n CustomerEntity,\n personWhere as any,\n undefined,\n scope,\n )\n const nameByPersonId = new Map<string, string>()\n for (const person of persons) {\n const name = person.displayName\n if (typeof name === 'string' && name.length) {\n nameByPersonId.set(person.id, name)\n }\n }\n for (const [dealId, personId] of linksByDeal) {\n const name = nameByPersonId.get(personId)\n if (name) primaryContactByDeal.set(dealId, name)\n }\n }\n\n const now = Date.now()\n const analyzed: DealAnalysisItem[] = deals.map((deal) => {\n const lastActivity = lastActivityByDeal.get(deal.id)\n const daysSinceLastActivity = lastActivity\n ? msTodays(now - lastActivity.getTime())\n : input.daysOfActivityWindow // Treat as fully stalled when no activity\n const value = deal.valueAmount ? parseFloat(String(deal.valueAmount)) : null\n return {\n id: deal.id,\n title: deal.title,\n value: Number.isFinite(value) ? value : null,\n valueCurrency: deal.valueCurrency ?? null,\n stage: deal.pipelineStage ?? deal.status ?? null,\n status: deal.status ?? 'open',\n daysSinceLastActivity,\n primaryContact: primaryContactByDeal.get(deal.id) ?? null,\n healthScore: Math.round(computeHealthScore(daysSinceLastActivity)),\n }\n })\n\n // Sort most-at-risk first (lowest health score first, then by value desc)\n analyzed.sort((a, b) => {\n if (a.healthScore !== b.healthScore) return a.healthScore - b.healthScore\n const aVal = a.value ?? 0\n const bVal = b.value ?? 0\n return bVal - aVal\n })\n\n const truncated = analyzed.slice(0, input.limit)\n const stalledCount = truncated.filter(\n (d) => d.daysSinceLastActivity >= input.daysOfActivityWindow,\n ).length\n\n return {\n deals: truncated,\n totalAnalyzed: deals.length,\n stalledCount,\n windowDays: input.daysOfActivityWindow,\n }\n },\n}\n\nexport const dealAnalyzerAiTools: CustomersAiToolDefinition[] = [\n analyzeDealsToolDefinition as unknown as CustomersAiToolDefinition,\n]\n\nexport default dealAnalyzerAiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -162,7 +162,12 @@ async function PUT(req, ctx) {
|
|
|
162
162
|
}
|
|
163
163
|
try {
|
|
164
164
|
const existing = await credentialsService.resolve(integration.id, scope);
|
|
165
|
-
const credentialsToSave = mergeMaskedSecretCredentials(
|
|
165
|
+
const credentialsToSave = mergeMaskedSecretCredentials(
|
|
166
|
+
schema,
|
|
167
|
+
payloadData.credentials,
|
|
168
|
+
existing ?? {},
|
|
169
|
+
payloadData.unchangedSecretFields
|
|
170
|
+
);
|
|
166
171
|
await credentialsService.save(integration.id, credentialsToSave, scope);
|
|
167
172
|
} catch (error) {
|
|
168
173
|
if (isCredentialsEncryptionUnavailableError(error)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/integrations/api/%5Bid%5D/credentials/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { emitIntegrationsEvent } from '../../../events'\nimport { saveCredentialsSchema } from '../../../data/validators'\nimport {\n isCredentialsEncryptionUnavailableError,\n type CredentialsService,\n} from '../../../lib/credentials-service'\nimport { collectCredentialUrlValidationErrors } from '../../../lib/credentials-field-validation'\nimport {\n maskSecretCredentials,\n mergeMaskedSecretCredentials,\n} from '../../../lib/credentials-masking'\nimport {\n resolveUserFeatures,\n runIntegrationMutationGuardAfterSuccess,\n runIntegrationMutationGuards,\n} from '../../guards'\nimport { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\n\nconst idParamsSchema = z.object({ id: z.string().min(1) })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['integrations.credentials.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['integrations.credentials.manage'] },\n}\n\nexport const openApi = {\n tags: ['Integrations'],\n summary: 'Get or save integration credentials',\n}\n\nfunction resolveParams(ctx: { params?: Promise<{ id?: string }> | { id?: string } }): Promise<{ id?: string } | undefined> | { id?: string } | undefined {\n if (!ctx.params) return undefined\n if (typeof (ctx.params as Promise<unknown>).then === 'function') {\n return ctx.params as Promise<{ id?: string }>\n }\n return ctx.params as { id?: string }\n}\n\nexport async function GET(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const rawParams = await resolveParams(ctx)\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const scope = { organizationId: organizationId, tenantId: auth.tenantId }\n\n let values: Record<string, unknown> | null\n let updatedAt: Date | null\n try {\n values = await credentialsService.resolve(integration.id, scope)\n updatedAt = await credentialsService.resolveUpdatedAt(integration.id, scope)\n } catch (error) {\n if (isCredentialsEncryptionUnavailableError(error)) {\n return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })\n }\n throw error\n }\n\n const schema = credentialsService.getSchema(integration.id)\n const { credentials, secretFieldsConfigured } = maskSecretCredentials(schema, values ?? {})\n\n return NextResponse.json({\n integrationId: integration.id,\n schema,\n credentials,\n secretFieldsConfigured,\n updatedAt: updatedAt?.toISOString() ?? null,\n })\n}\n\nexport async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const rawParams = await resolveParams(ctx)\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const payload = await req.json().catch(() => null)\n const parsedBody = saveCredentialsSchema.safeParse(payload)\n if (!parsedBody.success) {\n return NextResponse.json({ error: 'Invalid credentials payload', details: parsedBody.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const guardResult = await runIntegrationMutationGuards(\n container,\n {\n tenantId: auth.tenantId,\n organizationId,\n userId: auth.sub ?? '',\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsedBody.data as Record<string, unknown>,\n },\n resolveUserFeatures(auth),\n )\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n\n let payloadData = parsedBody.data\n if (guardResult.modifiedPayload) {\n const mergedPayload = { ...parsedBody.data, ...guardResult.modifiedPayload }\n const reparsed = saveCredentialsSchema.safeParse(mergedPayload)\n if (!reparsed.success) {\n return NextResponse.json({ error: 'Invalid credentials payload after guard transform', details: reparsed.error.flatten() }, { status: 422 })\n }\n payloadData = reparsed.data\n }\n\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const scope = { organizationId: organizationId, tenantId: auth.tenantId }\n const schema = credentialsService.getSchema(integration.id)\n\n try {\n const currentUpdatedAt = await credentialsService.resolveUpdatedAt(integration.id, scope)\n enforceCommandOptimisticLock({\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n if (isCredentialsEncryptionUnavailableError(error)) {\n return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })\n }\n throw error\n }\n\n const credentialFieldErrors = collectCredentialUrlValidationErrors(\n schema,\n payloadData.credentials,\n )\n if (Object.keys(credentialFieldErrors).length > 0) {\n return NextResponse.json(\n { error: 'Invalid credentials payload', details: { fieldErrors: credentialFieldErrors } },\n { status: 422 },\n )\n }\n\n try {\n
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { emitIntegrationsEvent } from '../../../events'\nimport { saveCredentialsSchema } from '../../../data/validators'\nimport {\n isCredentialsEncryptionUnavailableError,\n type CredentialsService,\n} from '../../../lib/credentials-service'\nimport { collectCredentialUrlValidationErrors } from '../../../lib/credentials-field-validation'\nimport {\n maskSecretCredentials,\n mergeMaskedSecretCredentials,\n} from '../../../lib/credentials-masking'\nimport {\n resolveUserFeatures,\n runIntegrationMutationGuardAfterSuccess,\n runIntegrationMutationGuards,\n} from '../../guards'\nimport { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\n\nconst idParamsSchema = z.object({ id: z.string().min(1) })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['integrations.credentials.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['integrations.credentials.manage'] },\n}\n\nexport const openApi = {\n tags: ['Integrations'],\n summary: 'Get or save integration credentials',\n}\n\nfunction resolveParams(ctx: { params?: Promise<{ id?: string }> | { id?: string } }): Promise<{ id?: string } | undefined> | { id?: string } | undefined {\n if (!ctx.params) return undefined\n if (typeof (ctx.params as Promise<unknown>).then === 'function') {\n return ctx.params as Promise<{ id?: string }>\n }\n return ctx.params as { id?: string }\n}\n\nexport async function GET(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const rawParams = await resolveParams(ctx)\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const scope = { organizationId: organizationId, tenantId: auth.tenantId }\n\n let values: Record<string, unknown> | null\n let updatedAt: Date | null\n try {\n values = await credentialsService.resolve(integration.id, scope)\n updatedAt = await credentialsService.resolveUpdatedAt(integration.id, scope)\n } catch (error) {\n if (isCredentialsEncryptionUnavailableError(error)) {\n return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })\n }\n throw error\n }\n\n const schema = credentialsService.getSchema(integration.id)\n const { credentials, secretFieldsConfigured } = maskSecretCredentials(schema, values ?? {})\n\n return NextResponse.json({\n integrationId: integration.id,\n schema,\n credentials,\n secretFieldsConfigured,\n updatedAt: updatedAt?.toISOString() ?? null,\n })\n}\n\nexport async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const rawParams = await resolveParams(ctx)\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const payload = await req.json().catch(() => null)\n const parsedBody = saveCredentialsSchema.safeParse(payload)\n if (!parsedBody.success) {\n return NextResponse.json({ error: 'Invalid credentials payload', details: parsedBody.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const guardResult = await runIntegrationMutationGuards(\n container,\n {\n tenantId: auth.tenantId,\n organizationId,\n userId: auth.sub ?? '',\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsedBody.data as Record<string, unknown>,\n },\n resolveUserFeatures(auth),\n )\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n\n let payloadData = parsedBody.data\n if (guardResult.modifiedPayload) {\n const mergedPayload = { ...parsedBody.data, ...guardResult.modifiedPayload }\n const reparsed = saveCredentialsSchema.safeParse(mergedPayload)\n if (!reparsed.success) {\n return NextResponse.json({ error: 'Invalid credentials payload after guard transform', details: reparsed.error.flatten() }, { status: 422 })\n }\n payloadData = reparsed.data\n }\n\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const scope = { organizationId: organizationId, tenantId: auth.tenantId }\n const schema = credentialsService.getSchema(integration.id)\n\n try {\n const currentUpdatedAt = await credentialsService.resolveUpdatedAt(integration.id, scope)\n enforceCommandOptimisticLock({\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n if (isCredentialsEncryptionUnavailableError(error)) {\n return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })\n }\n throw error\n }\n\n const credentialFieldErrors = collectCredentialUrlValidationErrors(\n schema,\n payloadData.credentials,\n )\n if (Object.keys(credentialFieldErrors).length > 0) {\n return NextResponse.json(\n { error: 'Invalid credentials payload', details: { fieldErrors: credentialFieldErrors } },\n { status: 422 },\n )\n }\n\n try {\n const existing = await credentialsService.resolve(integration.id, scope)\n const credentialsToSave = mergeMaskedSecretCredentials(\n schema,\n payloadData.credentials,\n existing ?? {},\n payloadData.unchangedSecretFields,\n )\n await credentialsService.save(integration.id, credentialsToSave, scope)\n } catch (error) {\n if (isCredentialsEncryptionUnavailableError(error)) {\n return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })\n }\n throw error\n }\n\n await emitIntegrationsEvent('integrations.credentials.updated', {\n integrationId: integration.id,\n tenantId: auth.tenantId,\n organizationId,\n userId: auth.sub,\n })\n\n await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: auth.tenantId,\n organizationId,\n userId: auth.sub ?? '',\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n\n return NextResponse.json({ ok: true })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -46,6 +46,10 @@ import {
|
|
|
46
46
|
refreshIntegrationDetailPanels,
|
|
47
47
|
refreshIntegrationRunActivityPanels
|
|
48
48
|
} from "../detail-page-refresh.js";
|
|
49
|
+
import {
|
|
50
|
+
buildCredentialEditValues,
|
|
51
|
+
buildIntegrationCredentialSavePayload
|
|
52
|
+
} from "../credential-secret-fields.js";
|
|
49
53
|
import { isValidCredentialUrl } from "../../../lib/credentials-field-validation.js";
|
|
50
54
|
const UNSUPPORTED_CREDENTIAL_FIELD_TYPES = /* @__PURE__ */ new Set(["oauth", "ssh_keypair"]);
|
|
51
55
|
function isEditableCredentialField(field) {
|
|
@@ -170,17 +174,22 @@ function resolvePathnameId(pathname) {
|
|
|
170
174
|
if (!integrationId || integrationId === "integrations" || integrationId === "bundle") return void 0;
|
|
171
175
|
return decodeURIComponent(integrationId);
|
|
172
176
|
}
|
|
173
|
-
function buildCredentialFields(credFields) {
|
|
177
|
+
function buildCredentialFields(credFields, secretFieldsConfigured, t) {
|
|
174
178
|
return credFields.map((field) => {
|
|
179
|
+
const baseDescription = field.helpDetails ? /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
180
|
+
field.helpText ? /* @__PURE__ */ jsx("div", { children: field.helpText }) : null,
|
|
181
|
+
/* @__PURE__ */ jsx(WebhookSetupGuide, { guide: field.helpDetails })
|
|
182
|
+
] }) : field.helpText;
|
|
183
|
+
const description = field.type === "secret" && secretFieldsConfigured[field.key] ? /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
184
|
+
baseDescription ? /* @__PURE__ */ jsx("div", { children: baseDescription }) : null,
|
|
185
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("integrations.detail.credentials.secretConfigured") })
|
|
186
|
+
] }) : baseDescription;
|
|
175
187
|
const shared = {
|
|
176
188
|
id: field.key,
|
|
177
189
|
label: field.label,
|
|
178
|
-
description
|
|
179
|
-
field.helpText ? /* @__PURE__ */ jsx("div", { children: field.helpText }) : null,
|
|
180
|
-
/* @__PURE__ */ jsx(WebhookSetupGuide, { guide: field.helpDetails, buttonLabel: "Show details" })
|
|
181
|
-
] }) : field.helpText,
|
|
190
|
+
description,
|
|
182
191
|
placeholder: field.placeholder,
|
|
183
|
-
required: field.required,
|
|
192
|
+
required: field.required && !(field.type === "secret" && secretFieldsConfigured[field.key]),
|
|
184
193
|
visibleWhen: field.visibleWhen
|
|
185
194
|
};
|
|
186
195
|
if (field.type === "secret") {
|
|
@@ -194,7 +203,8 @@ function buildCredentialFields(credFields) {
|
|
|
194
203
|
placeholder: field.placeholder,
|
|
195
204
|
value: typeof value === "string" ? value : "",
|
|
196
205
|
onChange: (event) => setValue(event.target.value),
|
|
197
|
-
disabled
|
|
206
|
+
disabled,
|
|
207
|
+
autoComplete: "new-password"
|
|
198
208
|
}
|
|
199
209
|
)
|
|
200
210
|
};
|
|
@@ -280,6 +290,7 @@ function IntegrationDetailPage({ params }) {
|
|
|
280
290
|
const [error, setError] = React.useState(null);
|
|
281
291
|
const [isNotFound, setIsNotFound] = React.useState(false);
|
|
282
292
|
const [credValues, setCredValues] = React.useState({});
|
|
293
|
+
const [secretFieldsConfigured, setSecretFieldsConfigured] = React.useState({});
|
|
283
294
|
const [credentialsUpdatedAt, setCredentialsUpdatedAt] = React.useState(null);
|
|
284
295
|
const [credentialsFormKey, setCredentialsFormKey] = React.useState(0);
|
|
285
296
|
const [isSavingCredentials, setIsSavingCredentials] = React.useState(false);
|
|
@@ -341,6 +352,7 @@ function IntegrationDetailPage({ params }) {
|
|
|
341
352
|
);
|
|
342
353
|
if (call.ok && call.result) {
|
|
343
354
|
setCredentialsUpdatedAt(call.result.updatedAt ?? null);
|
|
355
|
+
setSecretFieldsConfigured(call.result.secretFieldsConfigured ?? {});
|
|
344
356
|
}
|
|
345
357
|
if (call.ok && call.result?.credentials) {
|
|
346
358
|
const next = { ...call.result.credentials };
|
|
@@ -546,34 +558,27 @@ function IntegrationDetailPage({ params }) {
|
|
|
546
558
|
if (!currentIntegrationId) return;
|
|
547
559
|
setIsSavingCredentials(true);
|
|
548
560
|
try {
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (sanitizedValues.authMode === "ambient") {
|
|
557
|
-
delete sanitizedValues.accessKeyId;
|
|
558
|
-
delete sanitizedValues.secretAccessKey;
|
|
559
|
-
delete sanitizedValues.sessionToken;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
561
|
+
const credentialFields = detail?.integration.credentials?.fields ?? detail?.bundle?.credentials?.fields ?? [];
|
|
562
|
+
const savePayload = buildIntegrationCredentialSavePayload(
|
|
563
|
+
currentIntegrationId,
|
|
564
|
+
values,
|
|
565
|
+
credentialFields,
|
|
566
|
+
secretFieldsConfigured
|
|
567
|
+
);
|
|
562
568
|
const call = await runMutationWithContext({
|
|
563
569
|
actionId: "save-credentials",
|
|
564
570
|
tabId: "credentials",
|
|
565
|
-
mutationPayload: { integrationId: currentIntegrationId,
|
|
571
|
+
mutationPayload: { integrationId: currentIntegrationId, ...savePayload },
|
|
566
572
|
operation: () => withScopedApiRequestHeaders(
|
|
567
573
|
buildOptimisticLockHeader(credentialsUpdatedAt),
|
|
568
574
|
() => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`, {
|
|
569
575
|
method: "PUT",
|
|
570
576
|
headers: { "Content-Type": "application/json" },
|
|
571
|
-
body: JSON.stringify(
|
|
577
|
+
body: JSON.stringify(savePayload)
|
|
572
578
|
}, { fallback: null })
|
|
573
579
|
)
|
|
574
580
|
});
|
|
575
581
|
if (call.ok) {
|
|
576
|
-
setCredValues(sanitizedValues);
|
|
577
582
|
setCredentialsFormKey((current) => current + 1);
|
|
578
583
|
flash(t("integrations.detail.credentials.saved"), "success");
|
|
579
584
|
void loadCredentials();
|
|
@@ -586,7 +591,7 @@ function IntegrationDetailPage({ params }) {
|
|
|
586
591
|
} finally {
|
|
587
592
|
setIsSavingCredentials(false);
|
|
588
593
|
}
|
|
589
|
-
}, [credentialsUpdatedAt, loadCredentials, resolveCurrentIntegrationId, runMutationWithContext, t]);
|
|
594
|
+
}, [credentialsUpdatedAt, detail, loadCredentials, resolveCurrentIntegrationId, runMutationWithContext, secretFieldsConfigured, t]);
|
|
590
595
|
const handleVersionChange = React.useCallback(async (version) => {
|
|
591
596
|
const currentIntegrationId = resolveCurrentIntegrationId();
|
|
592
597
|
if (!currentIntegrationId) return;
|
|
@@ -661,8 +666,12 @@ function IntegrationDetailPage({ params }) {
|
|
|
661
666
|
[detail?.bundle?.credentials?.fields, detail?.integration.credentials?.fields]
|
|
662
667
|
);
|
|
663
668
|
const credentialFormFields = React.useMemo(
|
|
664
|
-
() => buildCredentialFields(editableCredentialFields),
|
|
665
|
-
[editableCredentialFields]
|
|
669
|
+
() => buildCredentialFields(editableCredentialFields, secretFieldsConfigured, t),
|
|
670
|
+
[editableCredentialFields, secretFieldsConfigured, t]
|
|
671
|
+
);
|
|
672
|
+
const credentialFormValues = React.useMemo(
|
|
673
|
+
() => buildCredentialEditValues(credValues, secretFieldsConfigured),
|
|
674
|
+
[credValues, secretFieldsConfigured]
|
|
666
675
|
);
|
|
667
676
|
const credentialSchema = React.useMemo(() => z.object({}).passthrough().superRefine((rawValues, ctx) => {
|
|
668
677
|
const values = rawValues;
|
|
@@ -698,7 +707,7 @@ function IntegrationDetailPage({ params }) {
|
|
|
698
707
|
return;
|
|
699
708
|
}
|
|
700
709
|
const normalizedValue = typeof value === "string" ? value : "";
|
|
701
|
-
if (field.required && normalizedValue.trim().length === 0) {
|
|
710
|
+
if (field.required && normalizedValue.trim().length === 0 && !(field.type === "secret" && secretFieldsConfigured[field.key])) {
|
|
702
711
|
ctx.addIssue({
|
|
703
712
|
code: z.ZodIssueCode.custom,
|
|
704
713
|
path: [field.key],
|
|
@@ -727,7 +736,7 @@ function IntegrationDetailPage({ params }) {
|
|
|
727
736
|
});
|
|
728
737
|
}
|
|
729
738
|
});
|
|
730
|
-
}), [editableCredentialFields, t]);
|
|
739
|
+
}), [editableCredentialFields, secretFieldsConfigured, t]);
|
|
731
740
|
const latestHealthLog = React.useMemo(() => logs.find(isHealthLog) ?? null, [logs]);
|
|
732
741
|
const latestOperationalLog = React.useMemo(
|
|
733
742
|
() => logs.find((log) => typeof log.payload?.operationalStatus === "string" || typeof log.payload?.summary === "string") ?? null,
|
|
@@ -939,7 +948,7 @@ function IntegrationDetailPage({ params }) {
|
|
|
939
948
|
entityId: "integrations.integration",
|
|
940
949
|
schema: credentialSchema,
|
|
941
950
|
fields: credentialFormFields,
|
|
942
|
-
initialValues:
|
|
951
|
+
initialValues: credentialFormValues,
|
|
943
952
|
onSubmit: handleSaveCredentials,
|
|
944
953
|
embedded: true,
|
|
945
954
|
hideFooterActions: true
|