@open-mercato/core 0.6.8-develop.7037.1.ea0277b01e → 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/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/package.json +7 -7
- 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
|
@@ -9,6 +9,7 @@ import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
|
9
9
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
10
10
|
import { Switch } from "@open-mercato/ui/primitives/switch";
|
|
11
11
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
12
|
+
import { PasswordInput } from "@open-mercato/ui/primitives/password-input";
|
|
12
13
|
import {
|
|
13
14
|
Select,
|
|
14
15
|
SelectContent,
|
|
@@ -23,6 +24,10 @@ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuarde
|
|
|
23
24
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
24
25
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
25
26
|
import { LoadingMessage, ErrorMessage, RecordNotFoundState } from "@open-mercato/ui/backend/detail";
|
|
27
|
+
import {
|
|
28
|
+
buildCredentialEditValues,
|
|
29
|
+
buildCredentialSavePayload
|
|
30
|
+
} from "../../credential-secret-fields.js";
|
|
26
31
|
const UNSUPPORTED_CREDENTIAL_FIELD_TYPES = /* @__PURE__ */ new Set(["oauth", "ssh_keypair"]);
|
|
27
32
|
function isEditableCredentialField(field) {
|
|
28
33
|
return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type);
|
|
@@ -46,6 +51,7 @@ function BundleConfigPage({ params }) {
|
|
|
46
51
|
const [error, setError] = React.useState(null);
|
|
47
52
|
const [isNotFound, setIsNotFound] = React.useState(false);
|
|
48
53
|
const [credValues, setCredValues] = React.useState({});
|
|
54
|
+
const [secretFieldsConfigured, setSecretFieldsConfigured] = React.useState({});
|
|
49
55
|
const [credentialsUpdatedAt, setCredentialsUpdatedAt] = React.useState(null);
|
|
50
56
|
const [isSavingCreds, setIsSavingCreds] = React.useState(false);
|
|
51
57
|
const [togglingIds, setTogglingIds] = React.useState(/* @__PURE__ */ new Set());
|
|
@@ -91,6 +97,7 @@ function BundleConfigPage({ params }) {
|
|
|
91
97
|
);
|
|
92
98
|
if (credCall.ok && credCall.result) {
|
|
93
99
|
setCredentialsUpdatedAt(credCall.result.updatedAt ?? null);
|
|
100
|
+
setSecretFieldsConfigured(credCall.result.secretFieldsConfigured ?? {});
|
|
94
101
|
}
|
|
95
102
|
if (credCall.ok && credCall.result?.credentials) {
|
|
96
103
|
const next = { ...credCall.result.credentials };
|
|
@@ -101,7 +108,10 @@ function BundleConfigPage({ params }) {
|
|
|
101
108
|
next.authMode = hasKeys ? "access_keys" : "ambient";
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
|
-
setCredValues(
|
|
111
|
+
setCredValues(buildCredentialEditValues(
|
|
112
|
+
next,
|
|
113
|
+
credCall.result.secretFieldsConfigured ?? {}
|
|
114
|
+
));
|
|
105
115
|
}
|
|
106
116
|
setIsLoading(false);
|
|
107
117
|
}, [resolveCurrentBundleId, t]);
|
|
@@ -113,8 +123,13 @@ function BundleConfigPage({ params }) {
|
|
|
113
123
|
if (!currentBundleId) return;
|
|
114
124
|
setIsSavingCreds(true);
|
|
115
125
|
try {
|
|
126
|
+
const savePayload = buildCredentialSavePayload(
|
|
127
|
+
credValues,
|
|
128
|
+
detail?.bundle?.credentials?.fields ?? [],
|
|
129
|
+
secretFieldsConfigured
|
|
130
|
+
);
|
|
116
131
|
const call = await runMutation({
|
|
117
|
-
mutationPayload: { bundleId: currentBundleId,
|
|
132
|
+
mutationPayload: { bundleId: currentBundleId, ...savePayload },
|
|
118
133
|
context: {
|
|
119
134
|
formId: mutationContextId,
|
|
120
135
|
operation: "update",
|
|
@@ -129,7 +144,7 @@ function BundleConfigPage({ params }) {
|
|
|
129
144
|
() => apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {
|
|
130
145
|
method: "PUT",
|
|
131
146
|
headers: { "Content-Type": "application/json" },
|
|
132
|
-
body: JSON.stringify(
|
|
147
|
+
body: JSON.stringify(savePayload)
|
|
133
148
|
}, { fallback: null })
|
|
134
149
|
)
|
|
135
150
|
});
|
|
@@ -144,7 +159,7 @@ function BundleConfigPage({ params }) {
|
|
|
144
159
|
} finally {
|
|
145
160
|
setIsSavingCreds(false);
|
|
146
161
|
}
|
|
147
|
-
}, [resolveCurrentBundleId, runMutation, mutationContextId, retryLastMutation, credValues, credentialsUpdatedAt, load, t]);
|
|
162
|
+
}, [resolveCurrentBundleId, runMutation, mutationContextId, retryLastMutation, credValues, credentialsUpdatedAt, detail?.bundle?.credentials?.fields, load, secretFieldsConfigured, t]);
|
|
148
163
|
const handleToggle = React.useCallback(async (integrationId, enabled, updatedAt) => {
|
|
149
164
|
setTogglingIds((prev) => new Set(prev).add(integrationId));
|
|
150
165
|
try {
|
|
@@ -224,9 +239,9 @@ function BundleConfigPage({ params }) {
|
|
|
224
239
|
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { children: t("integrations.bundle.sharedCredentials") }) }),
|
|
225
240
|
/* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
|
|
226
241
|
credFields.filter(isFieldVisible).map((field) => /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
227
|
-
/* @__PURE__ */ jsxs("label", { className: "text-sm font-medium", children: [
|
|
242
|
+
/* @__PURE__ */ jsxs("label", { htmlFor: `bundle-credential-${field.key}`, className: "text-sm font-medium", children: [
|
|
228
243
|
field.label,
|
|
229
|
-
field.required && /* @__PURE__ */ jsx("span", { className: "
|
|
244
|
+
field.required && /* @__PURE__ */ jsx("span", { className: "ml-0.5 text-destructive", children: "*" })
|
|
230
245
|
] }),
|
|
231
246
|
field.type === "select" && field.options ? /* @__PURE__ */ jsxs(
|
|
232
247
|
Select,
|
|
@@ -234,25 +249,37 @@ function BundleConfigPage({ params }) {
|
|
|
234
249
|
value: credValues[field.key] || void 0,
|
|
235
250
|
onValueChange: (value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? "" })),
|
|
236
251
|
children: [
|
|
237
|
-
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "\u2014" }) }),
|
|
252
|
+
/* @__PURE__ */ jsx(SelectTrigger, { id: `bundle-credential-${field.key}`, children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "\u2014" }) }),
|
|
238
253
|
/* @__PURE__ */ jsx(SelectContent, { children: field.options.map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })
|
|
239
254
|
]
|
|
240
255
|
}
|
|
241
256
|
) : field.type === "boolean" ? /* @__PURE__ */ jsx(
|
|
242
257
|
Switch,
|
|
243
258
|
{
|
|
259
|
+
id: `bundle-credential-${field.key}`,
|
|
244
260
|
checked: Boolean(credValues[field.key]),
|
|
245
261
|
onCheckedChange: (checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))
|
|
246
262
|
}
|
|
263
|
+
) : field.type === "secret" ? /* @__PURE__ */ jsx(
|
|
264
|
+
PasswordInput,
|
|
265
|
+
{
|
|
266
|
+
id: `bundle-credential-${field.key}`,
|
|
267
|
+
placeholder: field.placeholder,
|
|
268
|
+
value: credValues[field.key] ?? "",
|
|
269
|
+
onChange: (event) => setCredValues((prev) => ({ ...prev, [field.key]: event.target.value })),
|
|
270
|
+
autoComplete: "new-password"
|
|
271
|
+
}
|
|
247
272
|
) : /* @__PURE__ */ jsx(
|
|
248
273
|
Input,
|
|
249
274
|
{
|
|
250
|
-
|
|
275
|
+
id: `bundle-credential-${field.key}`,
|
|
276
|
+
type: "text",
|
|
251
277
|
placeholder: field.placeholder,
|
|
252
278
|
value: credValues[field.key] ?? "",
|
|
253
279
|
onChange: (e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))
|
|
254
280
|
}
|
|
255
|
-
)
|
|
281
|
+
),
|
|
282
|
+
field.type === "secret" && secretFieldsConfigured[field.key] ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("integrations.detail.credentials.secretConfigured") }) : null
|
|
256
283
|
] }, field.key)),
|
|
257
284
|
/* @__PURE__ */ jsxs(Button, { type: "button", onClick: () => void handleSaveCredentials(), disabled: isSavingCreds, children: [
|
|
258
285
|
isSavingCreds ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 h-4 w-4" }) : null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/integrations/backend/integrations/bundle/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\n\ntype CredentialField = IntegrationCredentialField\n\nconst UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])\n\nfunction isEditableCredentialField(field: CredentialField): boolean {\n return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)\n}\n\ntype BundleIntegration = {\n id: string\n title: string\n description?: string\n category?: string\n isEnabled: boolean\n state?: { updatedAt?: string | null }\n}\n\ntype BundleDetail = {\n integration: {\n id: string\n title: string\n description?: string\n bundleId?: string\n }\n bundle?: {\n id: string\n title: string\n description?: string\n credentials?: { fields: CredentialField[] }\n }\n bundleIntegrations: BundleIntegration[]\n state: { isEnabled: boolean }\n hasCredentials: boolean\n credentialsUpdatedAt?: string | null\n}\n\ntype BundleConfigPageProps = {\n params?: {\n id?: string | string[]\n }\n}\n\nfunction resolveRouteId(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0]\n return value\n}\n\nfunction resolvePathnameId(pathname: string): string | undefined {\n const parts = pathname.split('/').filter(Boolean)\n const bundleId = parts.at(-1)\n if (!bundleId || bundleId === 'bundle' || bundleId === 'integrations') return undefined\n return decodeURIComponent(bundleId)\n}\n\nexport default function BundleConfigPage({ params }: BundleConfigPageProps) {\n const pathname = usePathname()\n const bundleId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n\n const [detail, setDetail] = React.useState<BundleDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})\n const [credentialsUpdatedAt, setCredentialsUpdatedAt] = React.useState<string | null>(null)\n const [isSavingCreds, setIsSavingCreds] = React.useState(false)\n const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())\n\n const mutationContextId = React.useMemo(\n () => `integrations.bundle:${bundleId ?? 'unknown'}`,\n [bundleId],\n )\n const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: mutationContextId,\n })\n\n const resolveCurrentBundleId = React.useCallback(() => {\n return bundleId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [bundleId])\n\n const load = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n setError(null)\n setIsNotFound(false)\n const call = await apiCall<BundleDetail>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}`,\n undefined,\n { fallback: null },\n )\n if (!call.ok || !call.result) {\n if (call.status === 404) {\n setIsNotFound(true)\n } else {\n setError(t('integrations.detail.loadError'))\n }\n setIsLoading(false)\n return\n }\n setDetail(call.result)\n\n const credCall = await apiCall<{ credentials: Record<string, unknown>; updatedAt?: string | null }>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,\n undefined,\n { fallback: null },\n )\n if (credCall.ok && credCall.result) {\n setCredentialsUpdatedAt(credCall.result.updatedAt ?? null)\n }\n if (credCall.ok && credCall.result?.credentials) {\n const next = { ...credCall.result.credentials }\n if (currentBundleId === 'storage_s3') {\n const authMode = next.authMode\n if (authMode !== 'access_keys' && authMode !== 'ambient') {\n const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey)\n next.authMode = hasKeys ? 'access_keys' : 'ambient'\n }\n }\n setCredValues(next)\n }\n setIsLoading(false)\n }, [resolveCurrentBundleId, t])\n\n React.useEffect(() => { void load() }, [load])\n\n const handleSaveCredentials = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) return\n setIsSavingCreds(true)\n try {\n const call = await runMutation({\n mutationPayload: { bundleId: currentBundleId, credentials: credValues },\n context: {\n formId: mutationContextId,\n operation: 'update',\n actionId: 'save-credentials',\n resourceKind: 'integrations.bundle',\n resourceId: currentBundleId,\n bundleId: currentBundleId,\n retryLastMutation,\n },\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(credentialsUpdatedAt),\n () => apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentials: credValues }),\n }, { fallback: null }),\n ),\n })\n if (call.ok) {\n flash(t('integrations.detail.credentials.saved'), 'success')\n await load()\n } else {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n }\n } catch {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n } finally {\n setIsSavingCreds(false)\n }\n }, [resolveCurrentBundleId, runMutation, mutationContextId, retryLastMutation, credValues, credentialsUpdatedAt, load, t])\n\n const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean, updatedAt?: string | null) => {\n setTogglingIds((prev) => new Set(prev).add(integrationId))\n try {\n const call = await runMutation({\n mutationPayload: { integrationId, isEnabled: enabled },\n context: {\n formId: mutationContextId,\n operation: 'update',\n actionId: 'toggle-state',\n resourceKind: 'integrations.integration',\n resourceId: integrationId,\n integrationId,\n retryLastMutation,\n },\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () => apiCall<{ updatedAt?: string | null }>(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ isEnabled: enabled }),\n }, { fallback: null }),\n ),\n })\n if (call.ok) {\n const nextUpdatedAt = call.result?.updatedAt ?? null\n setDetail((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n bundleIntegrations: prev.bundleIntegrations.map((item) =>\n item.id === integrationId\n ? { ...item, isEnabled: enabled, state: { updatedAt: nextUpdatedAt ?? item.state?.updatedAt ?? null } }\n : item,\n ),\n }\n })\n } else {\n flash(t('integrations.detail.stateError'), 'error')\n }\n } catch {\n flash(t('integrations.detail.stateError'), 'error')\n } finally {\n setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })\n }\n }, [runMutation, mutationContextId, retryLastMutation, t])\n\n const handleBulkToggle = React.useCallback(async (enabled: boolean) => {\n if (!detail) return\n const targets = detail.bundleIntegrations.filter((item) => item.isEnabled !== enabled)\n await Promise.all(targets.map((item) => handleToggle(item.id, enabled, item.state?.updatedAt)))\n }, [detail, handleToggle])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('integrations.detail.notFound', 'Integration not found.')}\n backHref=\"/backend/integrations\"\n backLabel={t('integrations.detail.backToList', 'Back to integrations')}\n />\n </PageBody>\n </Page>\n )\n }\n if (error || !detail?.bundle) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>\n\n const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)\n\n function isFieldVisible(field: CredentialField): boolean {\n if (!field.visibleWhen) return true\n return credValues[field.visibleWhen.field] === field.visibleWhen.equals\n }\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div>\n <Link href=\"/backend/integrations\" className=\"text-sm text-muted-foreground hover:underline\">\n {t('integrations.detail.back')}\n </Link>\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold\">{detail.bundle.title}</h1>\n {detail.bundle.description && (\n <p className=\"text-muted-foreground mt-1\">{detail.bundle.description}</p>\n )}\n </div>\n\n {credFields.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>{t('integrations.bundle.sharedCredentials')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {credFields.filter(isFieldVisible).map((field) => (\n <div key={field.key} className=\"space-y-1.5\">\n <label className=\"text-sm font-medium\">\n {field.label}{field.required && <span className=\"text-red-500 ml-0.5\">*</span>}\n </label>\n {field.type === 'select' && field.options ? (\n <Select\n value={(credValues[field.key] as string) || undefined}\n onValueChange={(value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? '' }))}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"\u2014\" />\n </SelectTrigger>\n <SelectContent>\n {field.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : field.type === 'boolean' ? (\n <Switch\n checked={Boolean(credValues[field.key])}\n onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}\n />\n ) : (\n <Input\n type={field.type === 'secret' ? 'password' : 'text'}\n placeholder={field.placeholder}\n value={(credValues[field.key] as string) ?? ''}\n onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}\n />\n )}\n </div>\n ))}\n <Button type=\"button\" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>\n {isSavingCreds ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('integrations.detail.credentials.save')}\n </Button>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle>{t('integrations.bundle.integrationToggles')}</CardTitle>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(true)}>\n {t('integrations.marketplace.enableAll')}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(false)}>\n {t('integrations.marketplace.disableAll')}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {detail.bundleIntegrations.map((item) => (\n <div key={item.id} className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <Link\n href={`/backend/integrations/${encodeURIComponent(item.id)}`}\n className=\"text-sm font-medium hover:underline\"\n >\n {item.title}\n </Link>\n {item.category && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">{item.category}</Badge>\n )}\n {item.description && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{item.description}</p>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n <Button asChild variant=\"ghost\" size=\"sm\">\n <Link href={`/backend/integrations/${encodeURIComponent(item.id)}`}>\n {t('integrations.bundle.configureIntegration')}\n </Link>\n </Button>\n <Switch\n checked={item.isEnabled}\n disabled={togglingIds.has(item.id)}\n onCheckedChange={(checked) => void handleToggle(item.id, checked, item.state?.updatedAt)}\n />\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport {\n buildCredentialEditValues,\n buildCredentialSavePayload,\n type SecretFieldsConfigured,\n} from '../../credential-secret-fields'\n\ntype CredentialField = IntegrationCredentialField\n\nconst UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])\n\nfunction isEditableCredentialField(field: CredentialField): boolean {\n return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)\n}\n\ntype BundleIntegration = {\n id: string\n title: string\n description?: string\n category?: string\n isEnabled: boolean\n state?: { updatedAt?: string | null }\n}\n\ntype BundleDetail = {\n integration: {\n id: string\n title: string\n description?: string\n bundleId?: string\n }\n bundle?: {\n id: string\n title: string\n description?: string\n credentials?: { fields: CredentialField[] }\n }\n bundleIntegrations: BundleIntegration[]\n state: { isEnabled: boolean }\n hasCredentials: boolean\n credentialsUpdatedAt?: string | null\n}\n\ntype BundleConfigPageProps = {\n params?: {\n id?: string | string[]\n }\n}\n\nfunction resolveRouteId(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0]\n return value\n}\n\nfunction resolvePathnameId(pathname: string): string | undefined {\n const parts = pathname.split('/').filter(Boolean)\n const bundleId = parts.at(-1)\n if (!bundleId || bundleId === 'bundle' || bundleId === 'integrations') return undefined\n return decodeURIComponent(bundleId)\n}\n\nexport default function BundleConfigPage({ params }: BundleConfigPageProps) {\n const pathname = usePathname()\n const bundleId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n\n const [detail, setDetail] = React.useState<BundleDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})\n const [secretFieldsConfigured, setSecretFieldsConfigured] = React.useState<SecretFieldsConfigured>({})\n const [credentialsUpdatedAt, setCredentialsUpdatedAt] = React.useState<string | null>(null)\n const [isSavingCreds, setIsSavingCreds] = React.useState(false)\n const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())\n\n const mutationContextId = React.useMemo(\n () => `integrations.bundle:${bundleId ?? 'unknown'}`,\n [bundleId],\n )\n const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: mutationContextId,\n })\n\n const resolveCurrentBundleId = React.useCallback(() => {\n return bundleId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [bundleId])\n\n const load = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n setError(null)\n setIsNotFound(false)\n const call = await apiCall<BundleDetail>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}`,\n undefined,\n { fallback: null },\n )\n if (!call.ok || !call.result) {\n if (call.status === 404) {\n setIsNotFound(true)\n } else {\n setError(t('integrations.detail.loadError'))\n }\n setIsLoading(false)\n return\n }\n setDetail(call.result)\n\n const credCall = await apiCall<{\n credentials: Record<string, unknown>\n secretFieldsConfigured?: SecretFieldsConfigured\n updatedAt?: string | null\n }>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,\n undefined,\n { fallback: null },\n )\n if (credCall.ok && credCall.result) {\n setCredentialsUpdatedAt(credCall.result.updatedAt ?? null)\n setSecretFieldsConfigured(credCall.result.secretFieldsConfigured ?? {})\n }\n if (credCall.ok && credCall.result?.credentials) {\n const next = { ...credCall.result.credentials }\n if (currentBundleId === 'storage_s3') {\n const authMode = next.authMode\n if (authMode !== 'access_keys' && authMode !== 'ambient') {\n const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey)\n next.authMode = hasKeys ? 'access_keys' : 'ambient'\n }\n }\n setCredValues(buildCredentialEditValues(\n next,\n credCall.result.secretFieldsConfigured ?? {},\n ))\n }\n setIsLoading(false)\n }, [resolveCurrentBundleId, t])\n\n React.useEffect(() => { void load() }, [load])\n\n const handleSaveCredentials = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) return\n setIsSavingCreds(true)\n try {\n const savePayload = buildCredentialSavePayload(\n credValues,\n detail?.bundle?.credentials?.fields ?? [],\n secretFieldsConfigured,\n )\n const call = await runMutation({\n mutationPayload: { bundleId: currentBundleId, ...savePayload },\n context: {\n formId: mutationContextId,\n operation: 'update',\n actionId: 'save-credentials',\n resourceKind: 'integrations.bundle',\n resourceId: currentBundleId,\n bundleId: currentBundleId,\n retryLastMutation,\n },\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(credentialsUpdatedAt),\n () => apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(savePayload),\n }, { fallback: null }),\n ),\n })\n if (call.ok) {\n flash(t('integrations.detail.credentials.saved'), 'success')\n await load()\n } else {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n }\n } catch {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n } finally {\n setIsSavingCreds(false)\n }\n }, [resolveCurrentBundleId, runMutation, mutationContextId, retryLastMutation, credValues, credentialsUpdatedAt, detail?.bundle?.credentials?.fields, load, secretFieldsConfigured, t])\n\n const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean, updatedAt?: string | null) => {\n setTogglingIds((prev) => new Set(prev).add(integrationId))\n try {\n const call = await runMutation({\n mutationPayload: { integrationId, isEnabled: enabled },\n context: {\n formId: mutationContextId,\n operation: 'update',\n actionId: 'toggle-state',\n resourceKind: 'integrations.integration',\n resourceId: integrationId,\n integrationId,\n retryLastMutation,\n },\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () => apiCall<{ updatedAt?: string | null }>(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ isEnabled: enabled }),\n }, { fallback: null }),\n ),\n })\n if (call.ok) {\n const nextUpdatedAt = call.result?.updatedAt ?? null\n setDetail((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n bundleIntegrations: prev.bundleIntegrations.map((item) =>\n item.id === integrationId\n ? { ...item, isEnabled: enabled, state: { updatedAt: nextUpdatedAt ?? item.state?.updatedAt ?? null } }\n : item,\n ),\n }\n })\n } else {\n flash(t('integrations.detail.stateError'), 'error')\n }\n } catch {\n flash(t('integrations.detail.stateError'), 'error')\n } finally {\n setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })\n }\n }, [runMutation, mutationContextId, retryLastMutation, t])\n\n const handleBulkToggle = React.useCallback(async (enabled: boolean) => {\n if (!detail) return\n const targets = detail.bundleIntegrations.filter((item) => item.isEnabled !== enabled)\n await Promise.all(targets.map((item) => handleToggle(item.id, enabled, item.state?.updatedAt)))\n }, [detail, handleToggle])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('integrations.detail.notFound', 'Integration not found.')}\n backHref=\"/backend/integrations\"\n backLabel={t('integrations.detail.backToList', 'Back to integrations')}\n />\n </PageBody>\n </Page>\n )\n }\n if (error || !detail?.bundle) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>\n\n const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)\n\n function isFieldVisible(field: CredentialField): boolean {\n if (!field.visibleWhen) return true\n return credValues[field.visibleWhen.field] === field.visibleWhen.equals\n }\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div>\n <Link href=\"/backend/integrations\" className=\"text-sm text-muted-foreground hover:underline\">\n {t('integrations.detail.back')}\n </Link>\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold\">{detail.bundle.title}</h1>\n {detail.bundle.description && (\n <p className=\"text-muted-foreground mt-1\">{detail.bundle.description}</p>\n )}\n </div>\n\n {credFields.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>{t('integrations.bundle.sharedCredentials')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {credFields.filter(isFieldVisible).map((field) => (\n <div key={field.key} className=\"space-y-1.5\">\n <label htmlFor={`bundle-credential-${field.key}`} className=\"text-sm font-medium\">\n {field.label}{field.required && <span className=\"ml-0.5 text-destructive\">*</span>}\n </label>\n {field.type === 'select' && field.options ? (\n <Select\n value={(credValues[field.key] as string) || undefined}\n onValueChange={(value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? '' }))}\n >\n <SelectTrigger id={`bundle-credential-${field.key}`}>\n <SelectValue placeholder=\"\u2014\" />\n </SelectTrigger>\n <SelectContent>\n {field.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : field.type === 'boolean' ? (\n <Switch\n id={`bundle-credential-${field.key}`}\n checked={Boolean(credValues[field.key])}\n onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}\n />\n ) : field.type === 'secret' ? (\n <PasswordInput\n id={`bundle-credential-${field.key}`}\n placeholder={field.placeholder}\n value={(credValues[field.key] as string) ?? ''}\n onChange={(event) => setCredValues((prev) => ({ ...prev, [field.key]: event.target.value }))}\n autoComplete=\"new-password\"\n />\n ) : (\n <Input\n id={`bundle-credential-${field.key}`}\n type=\"text\"\n placeholder={field.placeholder}\n value={(credValues[field.key] as string) ?? ''}\n onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}\n />\n )}\n {field.type === 'secret' && secretFieldsConfigured[field.key] ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('integrations.detail.credentials.secretConfigured')}\n </p>\n ) : null}\n </div>\n ))}\n <Button type=\"button\" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>\n {isSavingCreds ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('integrations.detail.credentials.save')}\n </Button>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle>{t('integrations.bundle.integrationToggles')}</CardTitle>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(true)}>\n {t('integrations.marketplace.enableAll')}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(false)}>\n {t('integrations.marketplace.disableAll')}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {detail.bundleIntegrations.map((item) => (\n <div key={item.id} className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <Link\n href={`/backend/integrations/${encodeURIComponent(item.id)}`}\n className=\"text-sm font-medium hover:underline\"\n >\n {item.title}\n </Link>\n {item.category && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">{item.category}</Badge>\n )}\n {item.description && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{item.description}</p>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n <Button asChild variant=\"ghost\" size=\"sm\">\n <Link href={`/backend/integrations/${encodeURIComponent(item.id)}`}>\n {t('integrations.bundle.configureIntegration')}\n </Link>\n </Button>\n <Switch\n checked={item.isEnabled}\n disabled={togglingIds.has(item.id)}\n onCheckedChange={(checked) => void handleToggle(item.id, checked, item.state?.updatedAt)}\n />\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SECRET_CREDENTIAL_FIELD_TYPES } from "../../lib/credentials-masking.js";
|
|
2
|
+
function buildCredentialEditValues(credentials, secretFieldsConfigured) {
|
|
3
|
+
const editValues = { ...credentials };
|
|
4
|
+
for (const [fieldKey, configured] of Object.entries(secretFieldsConfigured)) {
|
|
5
|
+
if (configured) delete editValues[fieldKey];
|
|
6
|
+
}
|
|
7
|
+
return editValues;
|
|
8
|
+
}
|
|
9
|
+
function buildCredentialSavePayload(values, fields, secretFieldsConfigured, deliberatelyClearedSecretFields = /* @__PURE__ */ new Set()) {
|
|
10
|
+
const credentials = { ...values };
|
|
11
|
+
const unchangedSecretFields = /* @__PURE__ */ new Set();
|
|
12
|
+
for (const field of fields) {
|
|
13
|
+
if (!SECRET_CREDENTIAL_FIELD_TYPES.has(field.type)) continue;
|
|
14
|
+
if (deliberatelyClearedSecretFields.has(field.key)) {
|
|
15
|
+
delete credentials[field.key];
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (!secretFieldsConfigured[field.key]) continue;
|
|
19
|
+
const value = credentials[field.key];
|
|
20
|
+
if (value !== void 0 && value !== "") continue;
|
|
21
|
+
delete credentials[field.key];
|
|
22
|
+
unchangedSecretFields.add(field.key);
|
|
23
|
+
}
|
|
24
|
+
return unchangedSecretFields.size > 0 ? { credentials, unchangedSecretFields: [...unchangedSecretFields] } : { credentials };
|
|
25
|
+
}
|
|
26
|
+
function buildIntegrationCredentialSavePayload(integrationId, values, fields, secretFieldsConfigured) {
|
|
27
|
+
const normalizedValues = { ...values };
|
|
28
|
+
const deliberatelyClearedSecretFields = /* @__PURE__ */ new Set();
|
|
29
|
+
if (integrationId === "storage_s3") {
|
|
30
|
+
const authMode = normalizedValues.authMode;
|
|
31
|
+
if (authMode !== "access_keys" && authMode !== "ambient") {
|
|
32
|
+
const hasKeys = Boolean(normalizedValues.accessKeyId || normalizedValues.secretAccessKey);
|
|
33
|
+
normalizedValues.authMode = hasKeys ? "access_keys" : "ambient";
|
|
34
|
+
}
|
|
35
|
+
if (normalizedValues.authMode === "ambient") {
|
|
36
|
+
delete normalizedValues.accessKeyId;
|
|
37
|
+
delete normalizedValues.secretAccessKey;
|
|
38
|
+
delete normalizedValues.sessionToken;
|
|
39
|
+
deliberatelyClearedSecretFields.add("secretAccessKey");
|
|
40
|
+
deliberatelyClearedSecretFields.add("sessionToken");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return buildCredentialSavePayload(
|
|
44
|
+
normalizedValues,
|
|
45
|
+
fields,
|
|
46
|
+
secretFieldsConfigured,
|
|
47
|
+
deliberatelyClearedSecretFields
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
buildCredentialEditValues,
|
|
52
|
+
buildCredentialSavePayload,
|
|
53
|
+
buildIntegrationCredentialSavePayload
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=credential-secret-fields.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/integrations/backend/integrations/credential-secret-fields.ts"],
|
|
4
|
+
"sourcesContent": ["import type { IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'\nimport { SECRET_CREDENTIAL_FIELD_TYPES } from '../../lib/credentials-masking'\n\nexport type SecretFieldsConfigured = Record<string, boolean>\n\nexport type CredentialSavePayload = {\n credentials: Record<string, unknown>\n unchangedSecretFields?: string[]\n}\n\nexport function buildCredentialEditValues(\n credentials: Record<string, unknown>,\n secretFieldsConfigured: SecretFieldsConfigured,\n): Record<string, unknown> {\n const editValues = { ...credentials }\n\n for (const [fieldKey, configured] of Object.entries(secretFieldsConfigured)) {\n if (configured) delete editValues[fieldKey]\n }\n\n return editValues\n}\n\nexport function buildCredentialSavePayload(\n values: Record<string, unknown>,\n fields: readonly IntegrationCredentialField[],\n secretFieldsConfigured: SecretFieldsConfigured,\n deliberatelyClearedSecretFields: ReadonlySet<string> = new Set(),\n): CredentialSavePayload {\n const credentials = { ...values }\n const unchangedSecretFields = new Set<string>()\n\n for (const field of fields) {\n if (!SECRET_CREDENTIAL_FIELD_TYPES.has(field.type)) continue\n\n if (deliberatelyClearedSecretFields.has(field.key)) {\n delete credentials[field.key]\n continue\n }\n\n if (!secretFieldsConfigured[field.key]) continue\n const value = credentials[field.key]\n if (value !== undefined && value !== '') continue\n\n delete credentials[field.key]\n unchangedSecretFields.add(field.key)\n }\n\n return unchangedSecretFields.size > 0\n ? { credentials, unchangedSecretFields: [...unchangedSecretFields] }\n : { credentials }\n}\n\nexport function buildIntegrationCredentialSavePayload(\n integrationId: string,\n values: Record<string, unknown>,\n fields: readonly IntegrationCredentialField[],\n secretFieldsConfigured: SecretFieldsConfigured,\n): CredentialSavePayload {\n const normalizedValues = { ...values }\n const deliberatelyClearedSecretFields = new Set<string>()\n\n if (integrationId === 'storage_s3') {\n const authMode = normalizedValues.authMode\n if (authMode !== 'access_keys' && authMode !== 'ambient') {\n const hasKeys = Boolean(normalizedValues.accessKeyId || normalizedValues.secretAccessKey)\n normalizedValues.authMode = hasKeys ? 'access_keys' : 'ambient'\n }\n if (normalizedValues.authMode === 'ambient') {\n delete normalizedValues.accessKeyId\n delete normalizedValues.secretAccessKey\n delete normalizedValues.sessionToken\n deliberatelyClearedSecretFields.add('secretAccessKey')\n deliberatelyClearedSecretFields.add('sessionToken')\n }\n }\n\n return buildCredentialSavePayload(\n normalizedValues,\n fields,\n secretFieldsConfigured,\n deliberatelyClearedSecretFields,\n )\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,qCAAqC;AASvC,SAAS,0BACd,aACA,wBACyB;AACzB,QAAM,aAAa,EAAE,GAAG,YAAY;AAEpC,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,sBAAsB,GAAG;AAC3E,QAAI,WAAY,QAAO,WAAW,QAAQ;AAAA,EAC5C;AAEA,SAAO;AACT;AAEO,SAAS,2BACd,QACA,QACA,wBACA,kCAAuD,oBAAI,IAAI,GACxC;AACvB,QAAM,cAAc,EAAE,GAAG,OAAO;AAChC,QAAM,wBAAwB,oBAAI,IAAY;AAE9C,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,8BAA8B,IAAI,MAAM,IAAI,EAAG;AAEpD,QAAI,gCAAgC,IAAI,MAAM,GAAG,GAAG;AAClD,aAAO,YAAY,MAAM,GAAG;AAC5B;AAAA,IACF;AAEA,QAAI,CAAC,uBAAuB,MAAM,GAAG,EAAG;AACxC,UAAM,QAAQ,YAAY,MAAM,GAAG;AACnC,QAAI,UAAU,UAAa,UAAU,GAAI;AAEzC,WAAO,YAAY,MAAM,GAAG;AAC5B,0BAAsB,IAAI,MAAM,GAAG;AAAA,EACrC;AAEA,SAAO,sBAAsB,OAAO,IAChC,EAAE,aAAa,uBAAuB,CAAC,GAAG,qBAAqB,EAAE,IACjE,EAAE,YAAY;AACpB;AAEO,SAAS,sCACd,eACA,QACA,QACA,wBACuB;AACvB,QAAM,mBAAmB,EAAE,GAAG,OAAO;AACrC,QAAM,kCAAkC,oBAAI,IAAY;AAExD,MAAI,kBAAkB,cAAc;AAClC,UAAM,WAAW,iBAAiB;AAClC,QAAI,aAAa,iBAAiB,aAAa,WAAW;AACxD,YAAM,UAAU,QAAQ,iBAAiB,eAAe,iBAAiB,eAAe;AACxF,uBAAiB,WAAW,UAAU,gBAAgB;AAAA,IACxD;AACA,QAAI,iBAAiB,aAAa,WAAW;AAC3C,aAAO,iBAAiB;AACxB,aAAO,iBAAiB;AACxB,aAAO,iBAAiB;AACxB,sCAAgC,IAAI,iBAAiB;AACrD,sCAAgC,IAAI,cAAc;AAAA,IACpD;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
const credentialFieldKeySchema = z.string().min(1).max(128);
|
|
2
3
|
const saveCredentialsSchema = z.object({
|
|
3
4
|
credentials: z.record(
|
|
4
|
-
|
|
5
|
+
credentialFieldKeySchema,
|
|
5
6
|
z.union([z.string().max(2e4), z.number(), z.boolean(), z.null()])
|
|
6
|
-
)
|
|
7
|
+
),
|
|
8
|
+
unchangedSecretFields: z.array(credentialFieldKeySchema).max(200).optional()
|
|
7
9
|
}).refine((value) => Object.keys(value.credentials).length <= 200, {
|
|
8
10
|
message: "At most 200 credential fields are allowed"
|
|
9
|
-
})
|
|
11
|
+
}).refine(
|
|
12
|
+
(value) => !value.unchangedSecretFields || new Set(value.unchangedSecretFields).size === value.unchangedSecretFields.length,
|
|
13
|
+
{
|
|
14
|
+
message: "Unchanged secret field names must be unique",
|
|
15
|
+
path: ["unchangedSecretFields"]
|
|
16
|
+
}
|
|
17
|
+
);
|
|
10
18
|
const updateVersionSchema = z.object({
|
|
11
19
|
apiVersion: z.string().min(1)
|
|
12
20
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/integrations/data/validators.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\n\nexport const saveCredentialsSchema = z.object({\n credentials: z.record(\n
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\n\nconst credentialFieldKeySchema = z.string().min(1).max(128)\n\nexport const saveCredentialsSchema = z.object({\n credentials: z.record(\n credentialFieldKeySchema,\n z.union([z.string().max(20_000), z.number(), z.boolean(), z.null()]),\n ),\n unchangedSecretFields: z.array(credentialFieldKeySchema).max(200).optional(),\n}).refine((value) => Object.keys(value.credentials).length <= 200, {\n message: 'At most 200 credential fields are allowed',\n}).refine(\n (value) => !value.unchangedSecretFields\n || new Set(value.unchangedSecretFields).size === value.unchangedSecretFields.length,\n {\n message: 'Unchanged secret field names must be unique',\n path: ['unchangedSecretFields'],\n },\n)\n\nexport type SaveCredentialsInput = z.infer<typeof saveCredentialsSchema>\n\nexport const updateVersionSchema = z.object({\n apiVersion: z.string().min(1),\n})\n\nexport type UpdateVersionInput = z.infer<typeof updateVersionSchema>\n\nexport const updateStateSchema = z.object({\n isEnabled: z.boolean().optional(),\n reauthRequired: z.boolean().optional(),\n}).refine((value) => value.isEnabled !== undefined || value.reauthRequired !== undefined, {\n message: 'At least one state field must be provided',\n})\n\nexport type UpdateStateInput = z.infer<typeof updateStateSchema>\n\nexport const integrationLogLevelSchema = z.enum(['info', 'warn', 'error'])\n\nexport const listIntegrationLogsQuerySchema = z.object({\n integrationId: z.string().min(1).optional(),\n level: integrationLogLevelSchema.optional(),\n runId: z.string().uuid().optional(),\n entityType: z.string().optional(),\n entityId: z.string().uuid().optional(),\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n})\n\nexport type ListIntegrationLogsQuery = z.infer<typeof listIntegrationLogsQuerySchema>\n\nconst optionalBooleanQuery = z.preprocess(\n (value) => {\n if (value === undefined || value === '' || value === null) return undefined\n if (value === true || value === 'true' || value === '1') return true\n if (value === false || value === 'false' || value === '0') return false\n return value\n },\n z.boolean().optional(),\n)\n\nexport const integrationMarketplaceHealthStatusSchema = z.enum(['healthy', 'degraded', 'unhealthy', 'unconfigured'])\n\nexport const listIntegrationsQuerySchema = z.object({\n q: z.string().max(200).optional(),\n category: z.string().max(64).optional(),\n bundleId: z.string().max(128).optional(),\n isEnabled: optionalBooleanQuery,\n healthStatus: integrationMarketplaceHealthStatusSchema.optional(),\n sort: z.enum(['title', 'category', 'enabledAt', 'healthStatus']).optional(),\n order: z.enum(['asc', 'desc']).default('asc'),\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(100),\n})\n\nexport type ListIntegrationsQuery = z.infer<typeof listIntegrationsQuerySchema>\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -44,11 +44,15 @@ function maskSecretCredentials(schema, values) {
|
|
|
44
44
|
}
|
|
45
45
|
return { credentials, secretFieldsConfigured };
|
|
46
46
|
}
|
|
47
|
-
function mergeMaskedSecretCredentials(schema, incoming, existing) {
|
|
47
|
+
function mergeMaskedSecretCredentials(schema, incoming, existing, unchangedSecretFields = []) {
|
|
48
48
|
const merged = { ...incoming };
|
|
49
|
+
const unchangedSecretFieldSet = new Set(unchangedSecretFields);
|
|
49
50
|
for (const field of schema?.fields ?? []) {
|
|
50
51
|
if (!isSecretField(field.type)) continue;
|
|
51
|
-
|
|
52
|
+
const hasIncomingValue = Object.prototype.hasOwnProperty.call(merged, field.key);
|
|
53
|
+
const submittedMask = merged[field.key] === MASKED_SECRET_VALUE;
|
|
54
|
+
const explicitlyUnchanged = !hasIncomingValue && unchangedSecretFieldSet.has(field.key);
|
|
55
|
+
if (!submittedMask && !explicitlyUnchanged) continue;
|
|
52
56
|
if (hasPresentValue(existing[field.key])) {
|
|
53
57
|
merged[field.key] = existing[field.key];
|
|
54
58
|
} else {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/integrations/lib/credentials-masking.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n CredentialFieldType,\n IntegrationCredentialsSchema,\n} from '@open-mercato/shared/modules/integrations/types'\n\n/**\n * Credential field types whose stored value is a secret (API keys, OAuth client\n * secrets/tokens, SSH private keys). These MUST never be returned in plaintext\n * from the credentials API \u2014 they are masked on read and treated as write-only.\n */\nexport const SECRET_CREDENTIAL_FIELD_TYPES: ReadonlySet<CredentialFieldType> = new Set<CredentialFieldType>([\n 'secret',\n 'oauth',\n 'ssh_keypair',\n])\n\n/**\n * Opaque sentinel returned in place of a configured secret value. The client\n * round-trips this value back on save when the user did not change the field;\n * the PUT handler then preserves the existing stored secret instead of writing\n * the sentinel. Chosen to be extremely unlikely to collide with a real secret.\n */\nexport const MASKED_SECRET_VALUE = '__om_secret_unchanged__'\n\nfunction isSecretField(type: CredentialFieldType): boolean {\n return SECRET_CREDENTIAL_FIELD_TYPES.has(type)\n}\n\nfunction redactUrlUserinfo(value: unknown): unknown {\n if (typeof value !== 'string') return value\n try {\n const parsed = new URL(value)\n if (!parsed.username && !parsed.password) return value\n parsed.username = ''\n parsed.password = ''\n return parsed.toString()\n } catch {\n return value\n }\n}\n\nfunction hasPresentValue(value: unknown): boolean {\n if (value === undefined || value === null) return false\n if (typeof value === 'string') return value.length > 0\n if (typeof value === 'object') return Object.keys(value as Record<string, unknown>).length > 0\n return true\n}\n\nexport type MaskSecretCredentialsResult = {\n credentials: Record<string, unknown>\n secretFieldsConfigured: Record<string, boolean>\n}\n\n/**\n * Replace every configured secret-typed field with an opaque sentinel so the\n * decrypted plaintext never reaches the API response (and therefore the\n * browser, devtools, proxies, or editable DOM inputs). Non-secret config fields\n * pass through unchanged. A `secretFieldsConfigured` map reports which secret\n * fields currently hold a value without exposing it.\n */\nexport function maskSecretCredentials(\n schema: IntegrationCredentialsSchema | undefined,\n values: Record<string, unknown>,\n): MaskSecretCredentialsResult {\n const credentials: Record<string, unknown> = { ...values }\n const secretFieldsConfigured: Record<string, boolean> = {}\n\n for (const field of schema?.fields ?? []) {\n if (field.type === 'url') {\n credentials[field.key] = redactUrlUserinfo(credentials[field.key])\n continue\n }\n if (!isSecretField(field.type)) continue\n const configured = hasPresentValue(credentials[field.key])\n secretFieldsConfigured[field.key] = configured\n if (configured) {\n credentials[field.key] = MASKED_SECRET_VALUE\n } else {\n delete credentials[field.key]\n }\n }\n\n return { credentials, secretFieldsConfigured }\n}\n\n/**\n * Reverse of {@link maskSecretCredentials} for the save path.
|
|
5
|
-
"mappings": "AAUO,MAAM,gCAAkE,oBAAI,IAAyB;AAAA,EAC1G;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAQM,MAAM,sBAAsB;AAEnC,SAAS,cAAc,MAAoC;AACzD,SAAO,8BAA8B,IAAI,IAAI;AAC/C;AAEA,SAAS,kBAAkB,OAAyB;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,KAAK;AAC5B,QAAI,CAAC,OAAO,YAAY,CAAC,OAAO,SAAU,QAAO;AACjD,WAAO,WAAW;AAClB,WAAO,WAAW;AAClB,WAAO,OAAO,SAAS;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,gBAAgB,OAAyB;AAChD,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,MAAM,SAAS;AACrD,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK,KAAgC,EAAE,SAAS;AAC7F,SAAO;AACT;AAcO,SAAS,sBACd,QACA,QAC6B;AAC7B,QAAM,cAAuC,EAAE,GAAG,OAAO;AACzD,QAAM,yBAAkD,CAAC;AAEzD,aAAW,SAAS,QAAQ,UAAU,CAAC,GAAG;AACxC,QAAI,MAAM,SAAS,OAAO;AACxB,kBAAY,MAAM,GAAG,IAAI,kBAAkB,YAAY,MAAM,GAAG,CAAC;AACjE;AAAA,IACF;AACA,QAAI,CAAC,cAAc,MAAM,IAAI,EAAG;AAChC,UAAM,aAAa,gBAAgB,YAAY,MAAM,GAAG,CAAC;AACzD,2BAAuB,MAAM,GAAG,IAAI;AACpC,QAAI,YAAY;AACd,kBAAY,MAAM,GAAG,IAAI;AAAA,IAC3B,OAAO;AACL,aAAO,YAAY,MAAM,GAAG;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO,EAAE,aAAa,uBAAuB;AAC/C;
|
|
4
|
+
"sourcesContent": ["import type {\n CredentialFieldType,\n IntegrationCredentialsSchema,\n} from '@open-mercato/shared/modules/integrations/types'\n\n/**\n * Credential field types whose stored value is a secret (API keys, OAuth client\n * secrets/tokens, SSH private keys). These MUST never be returned in plaintext\n * from the credentials API \u2014 they are masked on read and treated as write-only.\n */\nexport const SECRET_CREDENTIAL_FIELD_TYPES: ReadonlySet<CredentialFieldType> = new Set<CredentialFieldType>([\n 'secret',\n 'oauth',\n 'ssh_keypair',\n])\n\n/**\n * Opaque sentinel returned in place of a configured secret value. The client\n * round-trips this value back on save when the user did not change the field;\n * the PUT handler then preserves the existing stored secret instead of writing\n * the sentinel. Chosen to be extremely unlikely to collide with a real secret.\n */\nexport const MASKED_SECRET_VALUE = '__om_secret_unchanged__'\n\nfunction isSecretField(type: CredentialFieldType): boolean {\n return SECRET_CREDENTIAL_FIELD_TYPES.has(type)\n}\n\nfunction redactUrlUserinfo(value: unknown): unknown {\n if (typeof value !== 'string') return value\n try {\n const parsed = new URL(value)\n if (!parsed.username && !parsed.password) return value\n parsed.username = ''\n parsed.password = ''\n return parsed.toString()\n } catch {\n return value\n }\n}\n\nfunction hasPresentValue(value: unknown): boolean {\n if (value === undefined || value === null) return false\n if (typeof value === 'string') return value.length > 0\n if (typeof value === 'object') return Object.keys(value as Record<string, unknown>).length > 0\n return true\n}\n\nexport type MaskSecretCredentialsResult = {\n credentials: Record<string, unknown>\n secretFieldsConfigured: Record<string, boolean>\n}\n\n/**\n * Replace every configured secret-typed field with an opaque sentinel so the\n * decrypted plaintext never reaches the API response (and therefore the\n * browser, devtools, proxies, or editable DOM inputs). Non-secret config fields\n * pass through unchanged. A `secretFieldsConfigured` map reports which secret\n * fields currently hold a value without exposing it.\n */\nexport function maskSecretCredentials(\n schema: IntegrationCredentialsSchema | undefined,\n values: Record<string, unknown>,\n): MaskSecretCredentialsResult {\n const credentials: Record<string, unknown> = { ...values }\n const secretFieldsConfigured: Record<string, boolean> = {}\n\n for (const field of schema?.fields ?? []) {\n if (field.type === 'url') {\n credentials[field.key] = redactUrlUserinfo(credentials[field.key])\n continue\n }\n if (!isSecretField(field.type)) continue\n const configured = hasPresentValue(credentials[field.key])\n secretFieldsConfigured[field.key] = configured\n if (configured) {\n credentials[field.key] = MASKED_SECRET_VALUE\n } else {\n delete credentials[field.key]\n }\n }\n\n return { credentials, secretFieldsConfigured }\n}\n\n/**\n * Reverse of {@link maskSecretCredentials} for the save path. The exact mask\n * sentinel and explicitly listed omitted secret fields mean \"leave unchanged\".\n * Explicit values win over the list, including an empty string used to clear a\n * secret, while plain omission retains the full-replacement contract.\n */\nexport function mergeMaskedSecretCredentials(\n schema: IntegrationCredentialsSchema | undefined,\n incoming: Record<string, unknown>,\n existing: Record<string, unknown>,\n unchangedSecretFields: readonly string[] = [],\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...incoming }\n const unchangedSecretFieldSet = new Set(unchangedSecretFields)\n\n for (const field of schema?.fields ?? []) {\n if (!isSecretField(field.type)) continue\n const hasIncomingValue = Object.prototype.hasOwnProperty.call(merged, field.key)\n const submittedMask = merged[field.key] === MASKED_SECRET_VALUE\n const explicitlyUnchanged = !hasIncomingValue && unchangedSecretFieldSet.has(field.key)\n if (!submittedMask && !explicitlyUnchanged) continue\n\n if (hasPresentValue(existing[field.key])) {\n merged[field.key] = existing[field.key]\n } else {\n delete merged[field.key]\n }\n }\n\n return merged\n}\n"],
|
|
5
|
+
"mappings": "AAUO,MAAM,gCAAkE,oBAAI,IAAyB;AAAA,EAC1G;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAQM,MAAM,sBAAsB;AAEnC,SAAS,cAAc,MAAoC;AACzD,SAAO,8BAA8B,IAAI,IAAI;AAC/C;AAEA,SAAS,kBAAkB,OAAyB;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,KAAK;AAC5B,QAAI,CAAC,OAAO,YAAY,CAAC,OAAO,SAAU,QAAO;AACjD,WAAO,WAAW;AAClB,WAAO,WAAW;AAClB,WAAO,OAAO,SAAS;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,gBAAgB,OAAyB;AAChD,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,MAAM,SAAS;AACrD,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK,KAAgC,EAAE,SAAS;AAC7F,SAAO;AACT;AAcO,SAAS,sBACd,QACA,QAC6B;AAC7B,QAAM,cAAuC,EAAE,GAAG,OAAO;AACzD,QAAM,yBAAkD,CAAC;AAEzD,aAAW,SAAS,QAAQ,UAAU,CAAC,GAAG;AACxC,QAAI,MAAM,SAAS,OAAO;AACxB,kBAAY,MAAM,GAAG,IAAI,kBAAkB,YAAY,MAAM,GAAG,CAAC;AACjE;AAAA,IACF;AACA,QAAI,CAAC,cAAc,MAAM,IAAI,EAAG;AAChC,UAAM,aAAa,gBAAgB,YAAY,MAAM,GAAG,CAAC;AACzD,2BAAuB,MAAM,GAAG,IAAI;AACpC,QAAI,YAAY;AACd,kBAAY,MAAM,GAAG,IAAI;AAAA,IAC3B,OAAO;AACL,aAAO,YAAY,MAAM,GAAG;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO,EAAE,aAAa,uBAAuB;AAC/C;AAQO,SAAS,6BACd,QACA,UACA,UACA,wBAA2C,CAAC,GACnB;AACzB,QAAM,SAAkC,EAAE,GAAG,SAAS;AACtD,QAAM,0BAA0B,IAAI,IAAI,qBAAqB;AAE7D,aAAW,SAAS,QAAQ,UAAU,CAAC,GAAG;AACxC,QAAI,CAAC,cAAc,MAAM,IAAI,EAAG;AAChC,UAAM,mBAAmB,OAAO,UAAU,eAAe,KAAK,QAAQ,MAAM,GAAG;AAC/E,UAAM,gBAAgB,OAAO,MAAM,GAAG,MAAM;AAC5C,UAAM,sBAAsB,CAAC,oBAAoB,wBAAwB,IAAI,MAAM,GAAG;AACtF,QAAI,CAAC,iBAAiB,CAAC,oBAAqB;AAE5C,QAAI,gBAAgB,SAAS,MAAM,GAAG,CAAC,GAAG;AACxC,aAAO,MAAM,GAAG,IAAI,SAAS,MAAM,GAAG;AAAA,IACxC,OAAO;AACL,aAAO,OAAO,MAAM,GAAG;AAAA,IACzB;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.7038.1.ea954afd80",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -252,16 +252,16 @@
|
|
|
252
252
|
"zod": "^4.4.3"
|
|
253
253
|
},
|
|
254
254
|
"peerDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
256
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
257
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.7038.1.ea954afd80",
|
|
256
|
+
"@open-mercato/shared": "0.6.8-develop.7038.1.ea954afd80",
|
|
257
|
+
"@open-mercato/ui": "0.6.8-develop.7038.1.ea954afd80",
|
|
258
258
|
"react": "^19.0.0",
|
|
259
259
|
"react-dom": "^19.0.0"
|
|
260
260
|
},
|
|
261
261
|
"devDependencies": {
|
|
262
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
263
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
264
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.7038.1.ea954afd80",
|
|
263
|
+
"@open-mercato/shared": "0.6.8-develop.7038.1.ea954afd80",
|
|
264
|
+
"@open-mercato/ui": "0.6.8-develop.7038.1.ea954afd80",
|
|
265
265
|
"@testing-library/dom": "^10.4.1",
|
|
266
266
|
"@testing-library/jest-dom": "^7.0.0",
|
|
267
267
|
"@testing-library/react": "^16.3.1",
|
|
@@ -183,11 +183,13 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
try {
|
|
186
|
-
// Secret fields are returned masked on GET; when the client round-trips the
|
|
187
|
-
// mask sentinel it means "unchanged", so restore the existing stored secret
|
|
188
|
-
// instead of overwriting it with the placeholder.
|
|
189
186
|
const existing = await credentialsService.resolve(integration.id, scope)
|
|
190
|
-
const credentialsToSave = mergeMaskedSecretCredentials(
|
|
187
|
+
const credentialsToSave = mergeMaskedSecretCredentials(
|
|
188
|
+
schema,
|
|
189
|
+
payloadData.credentials,
|
|
190
|
+
existing ?? {},
|
|
191
|
+
payloadData.unchangedSecretFields,
|
|
192
|
+
)
|
|
191
193
|
await credentialsService.save(integration.id, credentialsToSave, scope)
|
|
192
194
|
} catch (error) {
|
|
193
195
|
if (isCredentialsEncryptionUnavailableError(error)) {
|