@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
|
@@ -52,6 +52,11 @@ import {
|
|
|
52
52
|
refreshIntegrationDetailPanels,
|
|
53
53
|
refreshIntegrationRunActivityPanels,
|
|
54
54
|
} from '../detail-page-refresh'
|
|
55
|
+
import {
|
|
56
|
+
buildCredentialEditValues,
|
|
57
|
+
buildIntegrationCredentialSavePayload,
|
|
58
|
+
type SecretFieldsConfigured,
|
|
59
|
+
} from '../credential-secret-fields'
|
|
55
60
|
import { isValidCredentialUrl } from '../../../lib/credentials-field-validation'
|
|
56
61
|
|
|
57
62
|
type CredentialField = IntegrationCredentialField
|
|
@@ -290,19 +295,32 @@ function resolvePathnameId(pathname: string): string | undefined {
|
|
|
290
295
|
return decodeURIComponent(integrationId)
|
|
291
296
|
}
|
|
292
297
|
|
|
293
|
-
function buildCredentialFields(
|
|
298
|
+
function buildCredentialFields(
|
|
299
|
+
credFields: CredentialField[],
|
|
300
|
+
secretFieldsConfigured: SecretFieldsConfigured,
|
|
301
|
+
t: ReturnType<typeof useT>,
|
|
302
|
+
): CrudField[] {
|
|
294
303
|
return credFields.map((field) => {
|
|
304
|
+
const baseDescription = field.helpDetails ? (
|
|
305
|
+
<div className="space-y-1">
|
|
306
|
+
{field.helpText ? <div>{field.helpText}</div> : null}
|
|
307
|
+
<WebhookSetupGuide guide={field.helpDetails} />
|
|
308
|
+
</div>
|
|
309
|
+
) : field.helpText
|
|
310
|
+
const description = field.type === 'secret' && secretFieldsConfigured[field.key] ? (
|
|
311
|
+
<div className="space-y-1">
|
|
312
|
+
{baseDescription ? <div>{baseDescription}</div> : null}
|
|
313
|
+
<p className="text-xs text-muted-foreground">
|
|
314
|
+
{t('integrations.detail.credentials.secretConfigured')}
|
|
315
|
+
</p>
|
|
316
|
+
</div>
|
|
317
|
+
) : baseDescription
|
|
295
318
|
const shared = {
|
|
296
319
|
id: field.key,
|
|
297
320
|
label: field.label,
|
|
298
|
-
description
|
|
299
|
-
<div className="space-y-1">
|
|
300
|
-
{field.helpText ? <div>{field.helpText}</div> : null}
|
|
301
|
-
<WebhookSetupGuide guide={field.helpDetails} buttonLabel="Show details" />
|
|
302
|
-
</div>
|
|
303
|
-
) : field.helpText,
|
|
321
|
+
description,
|
|
304
322
|
placeholder: field.placeholder,
|
|
305
|
-
required: field.required,
|
|
323
|
+
required: field.required && !(field.type === 'secret' && secretFieldsConfigured[field.key]),
|
|
306
324
|
visibleWhen: field.visibleWhen,
|
|
307
325
|
}
|
|
308
326
|
|
|
@@ -317,6 +335,7 @@ function buildCredentialFields(credFields: CredentialField[]): CrudField[] {
|
|
|
317
335
|
value={typeof value === 'string' ? value : ''}
|
|
318
336
|
onChange={(event) => setValue(event.target.value)}
|
|
319
337
|
disabled={disabled}
|
|
338
|
+
autoComplete="new-password"
|
|
320
339
|
/>
|
|
321
340
|
),
|
|
322
341
|
}
|
|
@@ -426,6 +445,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
426
445
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
427
446
|
|
|
428
447
|
const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})
|
|
448
|
+
const [secretFieldsConfigured, setSecretFieldsConfigured] = React.useState<SecretFieldsConfigured>({})
|
|
429
449
|
const [credentialsUpdatedAt, setCredentialsUpdatedAt] = React.useState<string | null>(null)
|
|
430
450
|
const [credentialsFormKey, setCredentialsFormKey] = React.useState(0)
|
|
431
451
|
const [isSavingCredentials, setIsSavingCredentials] = React.useState(false)
|
|
@@ -490,13 +510,18 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
490
510
|
const loadCredentials = React.useCallback(async () => {
|
|
491
511
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
492
512
|
if (!currentIntegrationId) return
|
|
493
|
-
const call = await apiCall<{
|
|
513
|
+
const call = await apiCall<{
|
|
514
|
+
credentials: Record<string, unknown>
|
|
515
|
+
secretFieldsConfigured?: SecretFieldsConfigured
|
|
516
|
+
updatedAt?: string | null
|
|
517
|
+
}>(
|
|
494
518
|
`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`,
|
|
495
519
|
undefined,
|
|
496
520
|
{ fallback: null },
|
|
497
521
|
)
|
|
498
522
|
if (call.ok && call.result) {
|
|
499
523
|
setCredentialsUpdatedAt(call.result.updatedAt ?? null)
|
|
524
|
+
setSecretFieldsConfigured(call.result.secretFieldsConfigured ?? {})
|
|
500
525
|
}
|
|
501
526
|
if (call.ok && call.result?.credentials) {
|
|
502
527
|
const next = { ...call.result.credentials }
|
|
@@ -713,35 +738,32 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
713
738
|
if (!currentIntegrationId) return
|
|
714
739
|
setIsSavingCredentials(true)
|
|
715
740
|
try {
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
}
|
|
741
|
+
const credentialFields = (
|
|
742
|
+
detail?.integration.credentials?.fields
|
|
743
|
+
?? detail?.bundle?.credentials?.fields
|
|
744
|
+
?? []
|
|
745
|
+
)
|
|
746
|
+
const savePayload = buildIntegrationCredentialSavePayload(
|
|
747
|
+
currentIntegrationId,
|
|
748
|
+
values,
|
|
749
|
+
credentialFields,
|
|
750
|
+
secretFieldsConfigured,
|
|
751
|
+
)
|
|
729
752
|
const call = await runMutationWithContext({
|
|
730
753
|
actionId: 'save-credentials',
|
|
731
754
|
tabId: 'credentials',
|
|
732
|
-
mutationPayload: { integrationId: currentIntegrationId,
|
|
755
|
+
mutationPayload: { integrationId: currentIntegrationId, ...savePayload },
|
|
733
756
|
operation: () => withScopedApiRequestHeaders(
|
|
734
757
|
buildOptimisticLockHeader(credentialsUpdatedAt),
|
|
735
758
|
() => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`, {
|
|
736
759
|
method: 'PUT',
|
|
737
760
|
headers: { 'Content-Type': 'application/json' },
|
|
738
|
-
body: JSON.stringify(
|
|
761
|
+
body: JSON.stringify(savePayload),
|
|
739
762
|
}, { fallback: null }),
|
|
740
763
|
),
|
|
741
764
|
})
|
|
742
765
|
|
|
743
766
|
if (call.ok) {
|
|
744
|
-
setCredValues(sanitizedValues)
|
|
745
767
|
setCredentialsFormKey((current) => current + 1)
|
|
746
768
|
flash(t('integrations.detail.credentials.saved'), 'success')
|
|
747
769
|
void loadCredentials()
|
|
@@ -759,7 +781,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
759
781
|
} finally {
|
|
760
782
|
setIsSavingCredentials(false)
|
|
761
783
|
}
|
|
762
|
-
}, [credentialsUpdatedAt, loadCredentials, resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
784
|
+
}, [credentialsUpdatedAt, detail, loadCredentials, resolveCurrentIntegrationId, runMutationWithContext, secretFieldsConfigured, t])
|
|
763
785
|
|
|
764
786
|
const handleVersionChange = React.useCallback(async (version: string) => {
|
|
765
787
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
@@ -837,8 +859,12 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
837
859
|
[detail?.bundle?.credentials?.fields, detail?.integration.credentials?.fields],
|
|
838
860
|
)
|
|
839
861
|
const credentialFormFields = React.useMemo(
|
|
840
|
-
() => buildCredentialFields(editableCredentialFields),
|
|
841
|
-
[editableCredentialFields],
|
|
862
|
+
() => buildCredentialFields(editableCredentialFields, secretFieldsConfigured, t),
|
|
863
|
+
[editableCredentialFields, secretFieldsConfigured, t],
|
|
864
|
+
)
|
|
865
|
+
const credentialFormValues = React.useMemo(
|
|
866
|
+
() => buildCredentialEditValues(credValues, secretFieldsConfigured),
|
|
867
|
+
[credValues, secretFieldsConfigured],
|
|
842
868
|
)
|
|
843
869
|
const credentialSchema = React.useMemo(() => (
|
|
844
870
|
z.object({}).passthrough().superRefine((rawValues, ctx) => {
|
|
@@ -880,7 +906,11 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
880
906
|
|
|
881
907
|
const normalizedValue = typeof value === 'string' ? value : ''
|
|
882
908
|
|
|
883
|
-
if (
|
|
909
|
+
if (
|
|
910
|
+
field.required
|
|
911
|
+
&& normalizedValue.trim().length === 0
|
|
912
|
+
&& !(field.type === 'secret' && secretFieldsConfigured[field.key])
|
|
913
|
+
) {
|
|
884
914
|
ctx.addIssue({
|
|
885
915
|
code: z.ZodIssueCode.custom,
|
|
886
916
|
path: [field.key],
|
|
@@ -922,7 +952,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
922
952
|
}
|
|
923
953
|
})
|
|
924
954
|
})
|
|
925
|
-
) as z.ZodType<Record<string, unknown>>, [editableCredentialFields, t])
|
|
955
|
+
) as z.ZodType<Record<string, unknown>>, [editableCredentialFields, secretFieldsConfigured, t])
|
|
926
956
|
const latestHealthLog = React.useMemo(() => logs.find(isHealthLog) ?? null, [logs])
|
|
927
957
|
const latestOperationalLog = React.useMemo(
|
|
928
958
|
() => logs.find((log) => (
|
|
@@ -1257,7 +1287,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
1257
1287
|
entityId="integrations.integration"
|
|
1258
1288
|
schema={credentialSchema}
|
|
1259
1289
|
fields={credentialFormFields}
|
|
1260
|
-
initialValues={
|
|
1290
|
+
initialValues={credentialFormValues}
|
|
1261
1291
|
onSubmit={handleSaveCredentials}
|
|
1262
1292
|
embedded
|
|
1263
1293
|
hideFooterActions
|
|
@@ -8,6 +8,7 @@ import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
|
8
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
9
9
|
import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
10
10
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
11
|
+
import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
|
|
11
12
|
import {
|
|
12
13
|
Select,
|
|
13
14
|
SelectContent,
|
|
@@ -23,6 +24,11 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
|
23
24
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
24
25
|
import type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
|
|
25
26
|
import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
27
|
+
import {
|
|
28
|
+
buildCredentialEditValues,
|
|
29
|
+
buildCredentialSavePayload,
|
|
30
|
+
type SecretFieldsConfigured,
|
|
31
|
+
} from '../../credential-secret-fields'
|
|
26
32
|
|
|
27
33
|
type CredentialField = IntegrationCredentialField
|
|
28
34
|
|
|
@@ -88,6 +94,7 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
88
94
|
const [error, setError] = React.useState<string | null>(null)
|
|
89
95
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
90
96
|
const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})
|
|
97
|
+
const [secretFieldsConfigured, setSecretFieldsConfigured] = React.useState<SecretFieldsConfigured>({})
|
|
91
98
|
const [credentialsUpdatedAt, setCredentialsUpdatedAt] = React.useState<string | null>(null)
|
|
92
99
|
const [isSavingCreds, setIsSavingCreds] = React.useState(false)
|
|
93
100
|
const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())
|
|
@@ -134,13 +141,18 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
134
141
|
}
|
|
135
142
|
setDetail(call.result)
|
|
136
143
|
|
|
137
|
-
const credCall = await apiCall<{
|
|
144
|
+
const credCall = await apiCall<{
|
|
145
|
+
credentials: Record<string, unknown>
|
|
146
|
+
secretFieldsConfigured?: SecretFieldsConfigured
|
|
147
|
+
updatedAt?: string | null
|
|
148
|
+
}>(
|
|
138
149
|
`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,
|
|
139
150
|
undefined,
|
|
140
151
|
{ fallback: null },
|
|
141
152
|
)
|
|
142
153
|
if (credCall.ok && credCall.result) {
|
|
143
154
|
setCredentialsUpdatedAt(credCall.result.updatedAt ?? null)
|
|
155
|
+
setSecretFieldsConfigured(credCall.result.secretFieldsConfigured ?? {})
|
|
144
156
|
}
|
|
145
157
|
if (credCall.ok && credCall.result?.credentials) {
|
|
146
158
|
const next = { ...credCall.result.credentials }
|
|
@@ -151,7 +163,10 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
151
163
|
next.authMode = hasKeys ? 'access_keys' : 'ambient'
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
|
-
setCredValues(
|
|
166
|
+
setCredValues(buildCredentialEditValues(
|
|
167
|
+
next,
|
|
168
|
+
credCall.result.secretFieldsConfigured ?? {},
|
|
169
|
+
))
|
|
155
170
|
}
|
|
156
171
|
setIsLoading(false)
|
|
157
172
|
}, [resolveCurrentBundleId, t])
|
|
@@ -163,8 +178,13 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
163
178
|
if (!currentBundleId) return
|
|
164
179
|
setIsSavingCreds(true)
|
|
165
180
|
try {
|
|
181
|
+
const savePayload = buildCredentialSavePayload(
|
|
182
|
+
credValues,
|
|
183
|
+
detail?.bundle?.credentials?.fields ?? [],
|
|
184
|
+
secretFieldsConfigured,
|
|
185
|
+
)
|
|
166
186
|
const call = await runMutation({
|
|
167
|
-
mutationPayload: { bundleId: currentBundleId,
|
|
187
|
+
mutationPayload: { bundleId: currentBundleId, ...savePayload },
|
|
168
188
|
context: {
|
|
169
189
|
formId: mutationContextId,
|
|
170
190
|
operation: 'update',
|
|
@@ -179,7 +199,7 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
179
199
|
() => apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {
|
|
180
200
|
method: 'PUT',
|
|
181
201
|
headers: { 'Content-Type': 'application/json' },
|
|
182
|
-
body: JSON.stringify(
|
|
202
|
+
body: JSON.stringify(savePayload),
|
|
183
203
|
}, { fallback: null }),
|
|
184
204
|
),
|
|
185
205
|
})
|
|
@@ -194,7 +214,7 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
194
214
|
} finally {
|
|
195
215
|
setIsSavingCreds(false)
|
|
196
216
|
}
|
|
197
|
-
}, [resolveCurrentBundleId, runMutation, mutationContextId, retryLastMutation, credValues, credentialsUpdatedAt, load, t])
|
|
217
|
+
}, [resolveCurrentBundleId, runMutation, mutationContextId, retryLastMutation, credValues, credentialsUpdatedAt, detail?.bundle?.credentials?.fields, load, secretFieldsConfigured, t])
|
|
198
218
|
|
|
199
219
|
const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean, updatedAt?: string | null) => {
|
|
200
220
|
setTogglingIds((prev) => new Set(prev).add(integrationId))
|
|
@@ -295,15 +315,15 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
295
315
|
<CardContent className="space-y-4">
|
|
296
316
|
{credFields.filter(isFieldVisible).map((field) => (
|
|
297
317
|
<div key={field.key} className="space-y-1.5">
|
|
298
|
-
<label className="text-sm font-medium">
|
|
299
|
-
{field.label}{field.required && <span className="
|
|
318
|
+
<label htmlFor={`bundle-credential-${field.key}`} className="text-sm font-medium">
|
|
319
|
+
{field.label}{field.required && <span className="ml-0.5 text-destructive">*</span>}
|
|
300
320
|
</label>
|
|
301
321
|
{field.type === 'select' && field.options ? (
|
|
302
322
|
<Select
|
|
303
323
|
value={(credValues[field.key] as string) || undefined}
|
|
304
324
|
onValueChange={(value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? '' }))}
|
|
305
325
|
>
|
|
306
|
-
<SelectTrigger>
|
|
326
|
+
<SelectTrigger id={`bundle-credential-${field.key}`}>
|
|
307
327
|
<SelectValue placeholder="—" />
|
|
308
328
|
</SelectTrigger>
|
|
309
329
|
<SelectContent>
|
|
@@ -314,17 +334,32 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
314
334
|
</Select>
|
|
315
335
|
) : field.type === 'boolean' ? (
|
|
316
336
|
<Switch
|
|
337
|
+
id={`bundle-credential-${field.key}`}
|
|
317
338
|
checked={Boolean(credValues[field.key])}
|
|
318
339
|
onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}
|
|
319
340
|
/>
|
|
341
|
+
) : field.type === 'secret' ? (
|
|
342
|
+
<PasswordInput
|
|
343
|
+
id={`bundle-credential-${field.key}`}
|
|
344
|
+
placeholder={field.placeholder}
|
|
345
|
+
value={(credValues[field.key] as string) ?? ''}
|
|
346
|
+
onChange={(event) => setCredValues((prev) => ({ ...prev, [field.key]: event.target.value }))}
|
|
347
|
+
autoComplete="new-password"
|
|
348
|
+
/>
|
|
320
349
|
) : (
|
|
321
350
|
<Input
|
|
322
|
-
|
|
351
|
+
id={`bundle-credential-${field.key}`}
|
|
352
|
+
type="text"
|
|
323
353
|
placeholder={field.placeholder}
|
|
324
354
|
value={(credValues[field.key] as string) ?? ''}
|
|
325
355
|
onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
|
326
356
|
/>
|
|
327
357
|
)}
|
|
358
|
+
{field.type === 'secret' && secretFieldsConfigured[field.key] ? (
|
|
359
|
+
<p className="text-xs text-muted-foreground">
|
|
360
|
+
{t('integrations.detail.credentials.secretConfigured')}
|
|
361
|
+
</p>
|
|
362
|
+
) : null}
|
|
328
363
|
</div>
|
|
329
364
|
))}
|
|
330
365
|
<Button type="button" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
|
|
2
|
+
import { SECRET_CREDENTIAL_FIELD_TYPES } from '../../lib/credentials-masking'
|
|
3
|
+
|
|
4
|
+
export type SecretFieldsConfigured = Record<string, boolean>
|
|
5
|
+
|
|
6
|
+
export type CredentialSavePayload = {
|
|
7
|
+
credentials: Record<string, unknown>
|
|
8
|
+
unchangedSecretFields?: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildCredentialEditValues(
|
|
12
|
+
credentials: Record<string, unknown>,
|
|
13
|
+
secretFieldsConfigured: SecretFieldsConfigured,
|
|
14
|
+
): Record<string, unknown> {
|
|
15
|
+
const editValues = { ...credentials }
|
|
16
|
+
|
|
17
|
+
for (const [fieldKey, configured] of Object.entries(secretFieldsConfigured)) {
|
|
18
|
+
if (configured) delete editValues[fieldKey]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return editValues
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildCredentialSavePayload(
|
|
25
|
+
values: Record<string, unknown>,
|
|
26
|
+
fields: readonly IntegrationCredentialField[],
|
|
27
|
+
secretFieldsConfigured: SecretFieldsConfigured,
|
|
28
|
+
deliberatelyClearedSecretFields: ReadonlySet<string> = new Set(),
|
|
29
|
+
): CredentialSavePayload {
|
|
30
|
+
const credentials = { ...values }
|
|
31
|
+
const unchangedSecretFields = new Set<string>()
|
|
32
|
+
|
|
33
|
+
for (const field of fields) {
|
|
34
|
+
if (!SECRET_CREDENTIAL_FIELD_TYPES.has(field.type)) continue
|
|
35
|
+
|
|
36
|
+
if (deliberatelyClearedSecretFields.has(field.key)) {
|
|
37
|
+
delete credentials[field.key]
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!secretFieldsConfigured[field.key]) continue
|
|
42
|
+
const value = credentials[field.key]
|
|
43
|
+
if (value !== undefined && value !== '') continue
|
|
44
|
+
|
|
45
|
+
delete credentials[field.key]
|
|
46
|
+
unchangedSecretFields.add(field.key)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return unchangedSecretFields.size > 0
|
|
50
|
+
? { credentials, unchangedSecretFields: [...unchangedSecretFields] }
|
|
51
|
+
: { credentials }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildIntegrationCredentialSavePayload(
|
|
55
|
+
integrationId: string,
|
|
56
|
+
values: Record<string, unknown>,
|
|
57
|
+
fields: readonly IntegrationCredentialField[],
|
|
58
|
+
secretFieldsConfigured: SecretFieldsConfigured,
|
|
59
|
+
): CredentialSavePayload {
|
|
60
|
+
const normalizedValues = { ...values }
|
|
61
|
+
const deliberatelyClearedSecretFields = new Set<string>()
|
|
62
|
+
|
|
63
|
+
if (integrationId === 'storage_s3') {
|
|
64
|
+
const authMode = normalizedValues.authMode
|
|
65
|
+
if (authMode !== 'access_keys' && authMode !== 'ambient') {
|
|
66
|
+
const hasKeys = Boolean(normalizedValues.accessKeyId || normalizedValues.secretAccessKey)
|
|
67
|
+
normalizedValues.authMode = hasKeys ? 'access_keys' : 'ambient'
|
|
68
|
+
}
|
|
69
|
+
if (normalizedValues.authMode === 'ambient') {
|
|
70
|
+
delete normalizedValues.accessKeyId
|
|
71
|
+
delete normalizedValues.secretAccessKey
|
|
72
|
+
delete normalizedValues.sessionToken
|
|
73
|
+
deliberatelyClearedSecretFields.add('secretAccessKey')
|
|
74
|
+
deliberatelyClearedSecretFields.add('sessionToken')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return buildCredentialSavePayload(
|
|
79
|
+
normalizedValues,
|
|
80
|
+
fields,
|
|
81
|
+
secretFieldsConfigured,
|
|
82
|
+
deliberatelyClearedSecretFields,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
const credentialFieldKeySchema = z.string().min(1).max(128)
|
|
4
|
+
|
|
3
5
|
export const saveCredentialsSchema = z.object({
|
|
4
6
|
credentials: z.record(
|
|
5
|
-
|
|
7
|
+
credentialFieldKeySchema,
|
|
6
8
|
z.union([z.string().max(20_000), z.number(), z.boolean(), z.null()]),
|
|
7
9
|
),
|
|
10
|
+
unchangedSecretFields: z.array(credentialFieldKeySchema).max(200).optional(),
|
|
8
11
|
}).refine((value) => Object.keys(value.credentials).length <= 200, {
|
|
9
12
|
message: 'At most 200 credential fields are allowed',
|
|
10
|
-
})
|
|
13
|
+
}).refine(
|
|
14
|
+
(value) => !value.unchangedSecretFields
|
|
15
|
+
|| new Set(value.unchangedSecretFields).size === value.unchangedSecretFields.length,
|
|
16
|
+
{
|
|
17
|
+
message: 'Unchanged secret field names must be unique',
|
|
18
|
+
path: ['unchangedSecretFields'],
|
|
19
|
+
},
|
|
20
|
+
)
|
|
11
21
|
|
|
12
22
|
export type SaveCredentialsInput = z.infer<typeof saveCredentialsSchema>
|
|
13
23
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"integrations.detail.credentials.save": "Zugangsdaten speichern",
|
|
16
16
|
"integrations.detail.credentials.saveError": "Zugangsdaten konnten nicht gespeichert werden",
|
|
17
17
|
"integrations.detail.credentials.saved": "Zugangsdaten gespeichert",
|
|
18
|
+
"integrations.detail.credentials.secretConfigured": "Konfiguriert. Geben Sie einen neuen Wert ein, um ihn zu ersetzen.",
|
|
18
19
|
"integrations.detail.credentials.validation.boolean": "Select a valid value.",
|
|
19
20
|
"integrations.detail.credentials.validation.option": "Select one of the available options.",
|
|
20
21
|
"integrations.detail.credentials.validation.required": "{field} is required.",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"integrations.detail.credentials.save": "Save Credentials",
|
|
16
16
|
"integrations.detail.credentials.saveError": "Failed to save credentials",
|
|
17
17
|
"integrations.detail.credentials.saved": "Credentials saved",
|
|
18
|
+
"integrations.detail.credentials.secretConfigured": "Configured. Enter a new value to replace it.",
|
|
18
19
|
"integrations.detail.credentials.validation.boolean": "Select a valid value.",
|
|
19
20
|
"integrations.detail.credentials.validation.option": "Select one of the available options.",
|
|
20
21
|
"integrations.detail.credentials.validation.required": "{field} is required.",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"integrations.detail.credentials.save": "Guardar credenciales",
|
|
16
16
|
"integrations.detail.credentials.saveError": "No se pudieron guardar las credenciales",
|
|
17
17
|
"integrations.detail.credentials.saved": "Credenciales guardadas",
|
|
18
|
+
"integrations.detail.credentials.secretConfigured": "Configurado. Introduce un valor nuevo para reemplazarlo.",
|
|
18
19
|
"integrations.detail.credentials.validation.boolean": "Select a valid value.",
|
|
19
20
|
"integrations.detail.credentials.validation.option": "Select one of the available options.",
|
|
20
21
|
"integrations.detail.credentials.validation.required": "{field} is required.",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"integrations.detail.credentials.save": "자격 증명 저장",
|
|
16
16
|
"integrations.detail.credentials.saveError": "자격 증명 저장에 실패했습니다",
|
|
17
17
|
"integrations.detail.credentials.saved": "자격 증명이 저장되었습니다",
|
|
18
|
+
"integrations.detail.credentials.secretConfigured": "구성됨. 바꾸려면 새 값을 입력하세요.",
|
|
18
19
|
"integrations.detail.credentials.validation.boolean": "유효한 값을 선택하세요.",
|
|
19
20
|
"integrations.detail.credentials.validation.option": "사용 가능한 옵션 중 하나를 선택하세요.",
|
|
20
21
|
"integrations.detail.credentials.validation.required": "{field}은(는) 필수입니다.",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"integrations.detail.credentials.save": "Zapisz dane",
|
|
16
16
|
"integrations.detail.credentials.saveError": "Nie udało się zapisać danych",
|
|
17
17
|
"integrations.detail.credentials.saved": "Dane zapisane",
|
|
18
|
+
"integrations.detail.credentials.secretConfigured": "Skonfigurowano. Wpisz nową wartość, aby ją zastąpić.",
|
|
18
19
|
"integrations.detail.credentials.validation.boolean": "Select a valid value.",
|
|
19
20
|
"integrations.detail.credentials.validation.option": "Select one of the available options.",
|
|
20
21
|
"integrations.detail.credentials.validation.required": "{field} is required.",
|
|
@@ -84,22 +84,26 @@ export function maskSecretCredentials(
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Reverse of {@link maskSecretCredentials} for the save path.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* value (including an empty string, which clears the secret) is written as-is.
|
|
87
|
+
* Reverse of {@link maskSecretCredentials} for the save path. The exact mask
|
|
88
|
+
* sentinel and explicitly listed omitted secret fields mean "leave unchanged".
|
|
89
|
+
* Explicit values win over the list, including an empty string used to clear a
|
|
90
|
+
* secret, while plain omission retains the full-replacement contract.
|
|
92
91
|
*/
|
|
93
92
|
export function mergeMaskedSecretCredentials(
|
|
94
93
|
schema: IntegrationCredentialsSchema | undefined,
|
|
95
94
|
incoming: Record<string, unknown>,
|
|
96
95
|
existing: Record<string, unknown>,
|
|
96
|
+
unchangedSecretFields: readonly string[] = [],
|
|
97
97
|
): Record<string, unknown> {
|
|
98
98
|
const merged: Record<string, unknown> = { ...incoming }
|
|
99
|
+
const unchangedSecretFieldSet = new Set(unchangedSecretFields)
|
|
99
100
|
|
|
100
101
|
for (const field of schema?.fields ?? []) {
|
|
101
102
|
if (!isSecretField(field.type)) continue
|
|
102
|
-
|
|
103
|
+
const hasIncomingValue = Object.prototype.hasOwnProperty.call(merged, field.key)
|
|
104
|
+
const submittedMask = merged[field.key] === MASKED_SECRET_VALUE
|
|
105
|
+
const explicitlyUnchanged = !hasIncomingValue && unchangedSecretFieldSet.has(field.key)
|
|
106
|
+
if (!submittedMask && !explicitlyUnchanged) continue
|
|
103
107
|
|
|
104
108
|
if (hasPresentValue(existing[field.key])) {
|
|
105
109
|
merged[field.key] = existing[field.key]
|