@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.
Files changed (25) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/integrations/api/[id]/credentials/route.js +6 -1
  3. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  4. package/dist/modules/integrations/backend/integrations/[id]/page.js +38 -29
  5. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  6. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +36 -9
  7. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  8. package/dist/modules/integrations/backend/integrations/credential-secret-fields.js +55 -0
  9. package/dist/modules/integrations/backend/integrations/credential-secret-fields.js.map +7 -0
  10. package/dist/modules/integrations/data/validators.js +11 -3
  11. package/dist/modules/integrations/data/validators.js.map +2 -2
  12. package/dist/modules/integrations/lib/credentials-masking.js +6 -2
  13. package/dist/modules/integrations/lib/credentials-masking.js.map +2 -2
  14. package/package.json +7 -7
  15. package/src/modules/integrations/api/[id]/credentials/route.ts +6 -4
  16. package/src/modules/integrations/backend/integrations/[id]/page.tsx +61 -31
  17. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +44 -9
  18. package/src/modules/integrations/backend/integrations/credential-secret-fields.ts +84 -0
  19. package/src/modules/integrations/data/validators.ts +12 -2
  20. package/src/modules/integrations/i18n/de.json +1 -0
  21. package/src/modules/integrations/i18n/en.json +1 -0
  22. package/src/modules/integrations/i18n/es.json +1 -0
  23. package/src/modules/integrations/i18n/ko.json +1 -0
  24. package/src/modules/integrations/i18n/pl.json +1 -0
  25. 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(next);
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, credentials: credValues },
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({ credentials: credValues })
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: "text-red-500 ml-0.5", children: "*" })
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
- type: field.type === "secret" ? "password" : "text",
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": "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",
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
- z.string().min(1).max(128),
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 z.string().min(1).max(128),\n z.union([z.string().max(20_000), z.number(), z.boolean(), z.null()]),\n ),\n}).refine((value) => Object.keys(value.credentials).length <= 200, {\n message: 'At most 200 credential fields are allowed',\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": "AAAA,SAAS,SAAS;AAEX,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,aAAa,EAAE;AAAA,IACb,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,IACzB,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,GAAM,GAAG,EAAE,OAAO,GAAG,EAAE,QAAQ,GAAG,EAAE,KAAK,CAAC,CAAC;AAAA,EACrE;AACF,CAAC,EAAE,OAAO,CAAC,UAAU,OAAO,KAAK,MAAM,WAAW,EAAE,UAAU,KAAK;AAAA,EACjE,SAAS;AACX,CAAC;AAIM,MAAM,sBAAsB,EAAE,OAAO;AAAA,EAC1C,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAC9B,CAAC;AAIM,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACxC,WAAW,EAAE,QAAQ,EAAE,SAAS;AAAA,EAChC,gBAAgB,EAAE,QAAQ,EAAE,SAAS;AACvC,CAAC,EAAE,OAAO,CAAC,UAAU,MAAM,cAAc,UAAa,MAAM,mBAAmB,QAAW;AAAA,EACxF,SAAS;AACX,CAAC;AAIM,MAAM,4BAA4B,EAAE,KAAK,CAAC,QAAQ,QAAQ,OAAO,CAAC;AAElE,MAAM,iCAAiC,EAAE,OAAO;AAAA,EACrD,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EAC1C,OAAO,0BAA0B,SAAS;AAAA,EAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAClC,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EAC9C,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAC9D,CAAC;AAID,MAAM,uBAAuB,EAAE;AAAA,EAC7B,CAAC,UAAU;AACT,QAAI,UAAU,UAAa,UAAU,MAAM,UAAU,KAAM,QAAO;AAClE,QAAI,UAAU,QAAQ,UAAU,UAAU,UAAU,IAAK,QAAO;AAChE,QAAI,UAAU,SAAS,UAAU,WAAW,UAAU,IAAK,QAAO;AAClE,WAAO;AAAA,EACT;AAAA,EACA,EAAE,QAAQ,EAAE,SAAS;AACvB;AAEO,MAAM,2CAA2C,EAAE,KAAK,CAAC,WAAW,YAAY,aAAa,cAAc,CAAC;AAE5G,MAAM,8BAA8B,EAAE,OAAO;AAAA,EAClD,GAAG,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAChC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACtC,UAAU,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACvC,WAAW;AAAA,EACX,cAAc,yCAAyC,SAAS;AAAA,EAChE,MAAM,EAAE,KAAK,CAAC,SAAS,YAAY,aAAa,cAAc,CAAC,EAAE,SAAS;AAAA,EAC1E,OAAO,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,QAAQ,KAAK;AAAA,EAC5C,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EAC9C,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,GAAG;AAC/D,CAAC;",
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
- if (merged[field.key] !== MASKED_SECRET_VALUE) continue;
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. When the client\n * submits the mask sentinel for a secret field it means \"leave it unchanged\":\n * restore the existing stored secret, or drop the field entirely when nothing\n * was previously stored (so the literal sentinel is never persisted). Any other\n * value (including an empty string, which clears the secret) is written as-is.\n */\nexport function mergeMaskedSecretCredentials(\n schema: IntegrationCredentialsSchema | undefined,\n incoming: Record<string, unknown>,\n existing: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...incoming }\n\n for (const field of schema?.fields ?? []) {\n if (!isSecretField(field.type)) continue\n if (merged[field.key] !== MASKED_SECRET_VALUE) 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;AASO,SAAS,6BACd,QACA,UACA,UACyB;AACzB,QAAM,SAAkC,EAAE,GAAG,SAAS;AAEtD,aAAW,SAAS,QAAQ,UAAU,CAAC,GAAG;AACxC,QAAI,CAAC,cAAc,MAAM,IAAI,EAAG;AAChC,QAAI,OAAO,MAAM,GAAG,MAAM,oBAAqB;AAE/C,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;",
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.7037.1.ea0277b01e",
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.7037.1.ea0277b01e",
256
- "@open-mercato/shared": "0.6.8-develop.7037.1.ea0277b01e",
257
- "@open-mercato/ui": "0.6.8-develop.7037.1.ea0277b01e",
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.7037.1.ea0277b01e",
263
- "@open-mercato/shared": "0.6.8-develop.7037.1.ea0277b01e",
264
- "@open-mercato/ui": "0.6.8-develop.7037.1.ea0277b01e",
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(schema, payloadData.credentials, existing ?? {})
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)) {