@contractspec/bundle.library 3.9.10 → 3.10.0

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 (44) hide show
  1. package/.turbo/turbo-build.log +54 -40
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +1 -0
  4. package/dist/components/integrations/atoms/IntegrationCredentialFieldList.d.ts +6 -0
  5. package/dist/components/integrations/atoms/IntegrationCredentialFieldList.js +2 -0
  6. package/dist/components/integrations/atoms/IntegrationCredentialModeTabs.d.ts +7 -0
  7. package/dist/components/integrations/atoms/IntegrationCredentialModeTabs.js +2 -0
  8. package/dist/components/integrations/atoms/IntegrationEnvAliasPreview.d.ts +7 -0
  9. package/dist/components/integrations/atoms/IntegrationEnvAliasPreview.js +2 -0
  10. package/dist/components/integrations/blocks/IntegrationCredentialSetupBlock.d.ts +11 -0
  11. package/dist/components/integrations/blocks/IntegrationCredentialSetupBlock.js +2 -0
  12. package/dist/components/integrations/byok-env-ui-kit.test.d.ts +1 -0
  13. package/dist/components/integrations/helpers/credentialSetupAliases.d.ts +3 -0
  14. package/dist/components/integrations/helpers/credentialSetupAliases.js +2 -0
  15. package/dist/components/integrations/helpers/credentialSetupModel.d.ts +58 -0
  16. package/dist/components/integrations/helpers/credentialSetupModel.js +2 -0
  17. package/dist/components/integrations/index.d.ts +5 -0
  18. package/dist/components/integrations/index.js +2 -2
  19. package/dist/components/integrations/organisms/IntegrationSettings.d.ts +3 -1
  20. package/dist/components/integrations/organisms/IntegrationSettings.js +2 -2
  21. package/dist/components/integrations/organisms/IntegrationSettingsSecretReference.d.ts +8 -0
  22. package/dist/components/integrations/organisms/IntegrationSettingsSecretReference.js +2 -0
  23. package/dist/index.js +302 -302
  24. package/dist/node/components/integrations/atoms/IntegrationCredentialFieldList.js +1 -0
  25. package/dist/node/components/integrations/atoms/IntegrationCredentialModeTabs.js +1 -0
  26. package/dist/node/components/integrations/atoms/IntegrationEnvAliasPreview.js +1 -0
  27. package/dist/node/components/integrations/blocks/IntegrationCredentialSetupBlock.js +1 -0
  28. package/dist/node/components/integrations/helpers/credentialSetupAliases.js +1 -0
  29. package/dist/node/components/integrations/helpers/credentialSetupModel.js +1 -0
  30. package/dist/node/components/integrations/index.js +2 -2
  31. package/dist/node/components/integrations/organisms/IntegrationSettings.js +2 -2
  32. package/dist/node/components/integrations/organisms/IntegrationSettingsSecretReference.js +1 -0
  33. package/dist/node/index.js +302 -302
  34. package/package.json +107 -23
  35. package/src/components/integrations/atoms/IntegrationCredentialFieldList.tsx +51 -0
  36. package/src/components/integrations/atoms/IntegrationCredentialModeTabs.tsx +44 -0
  37. package/src/components/integrations/atoms/IntegrationEnvAliasPreview.tsx +56 -0
  38. package/src/components/integrations/blocks/IntegrationCredentialSetupBlock.tsx +95 -0
  39. package/src/components/integrations/byok-env-ui-kit.test.tsx +194 -0
  40. package/src/components/integrations/helpers/credentialSetupAliases.ts +137 -0
  41. package/src/components/integrations/helpers/credentialSetupModel.ts +218 -0
  42. package/src/components/integrations/index.ts +5 -0
  43. package/src/components/integrations/organisms/IntegrationSettings.tsx +91 -97
  44. package/src/components/integrations/organisms/IntegrationSettingsSecretReference.tsx +84 -0
@@ -0,0 +1,137 @@
1
+ import type {
2
+ IntegrationCredentialAlias,
3
+ IntegrationCredentialModeManifest,
4
+ } from '@contractspec/lib.contracts-integrations';
5
+ import type {
6
+ EnvironmentConfig,
7
+ EnvVariableAlias,
8
+ } from '@contractspec/lib.contracts-spec/workspace-config/environment';
9
+ import type {
10
+ BuildIntegrationCredentialSetupModelInput,
11
+ CredentialSetupAliasRow,
12
+ CredentialSetupWarning,
13
+ } from './credentialSetupModel';
14
+
15
+ export function buildAliases(
16
+ manifest: IntegrationCredentialModeManifest | undefined,
17
+ input: BuildIntegrationCredentialSetupModelInput,
18
+ secretKeys: Set<string>,
19
+ warnings: CredentialSetupWarning[]
20
+ ): CredentialSetupAliasRow[] {
21
+ return [
22
+ ...fromManifestAliases(manifest?.envAliases ?? [], secretKeys, warnings),
23
+ ...fromEnvironmentAliases(input.environment, input, secretKeys, warnings),
24
+ ];
25
+ }
26
+
27
+ function fromManifestAliases(
28
+ aliases: IntegrationCredentialAlias[],
29
+ secretKeys: Set<string>,
30
+ warnings: CredentialSetupWarning[]
31
+ ) {
32
+ return aliases.flatMap((alias): CredentialSetupAliasRow[] => {
33
+ if (
34
+ secretKeys.has(alias.logicalKey) &&
35
+ isPublicAlias(alias.envVar, alias.public)
36
+ ) {
37
+ warnings.push({
38
+ level: 'error',
39
+ fieldKey: alias.logicalKey,
40
+ message: `Unsafe public alias was omitted for secret field ${alias.logicalKey}.`,
41
+ });
42
+ return [];
43
+ }
44
+ return [
45
+ {
46
+ logicalKey: alias.logicalKey,
47
+ envName: alias.envVar,
48
+ targetId: alias.targetId,
49
+ public: isPublicAlias(alias.envVar, alias.public),
50
+ warning: alias.public ? 'Public client alias' : undefined,
51
+ },
52
+ ];
53
+ });
54
+ }
55
+
56
+ function fromEnvironmentAliases(
57
+ environment: EnvironmentConfig | undefined,
58
+ input: BuildIntegrationCredentialSetupModelInput,
59
+ secretKeys: Set<string>,
60
+ warnings: CredentialSetupWarning[]
61
+ ) {
62
+ if (!environment) return [];
63
+ return Object.values(environment?.variables ?? {}).flatMap((definition) =>
64
+ (definition.aliases ?? [])
65
+ .filter((alias) => aliasMatchesInput(alias, environment, input))
66
+ .flatMap((alias): CredentialSetupAliasRow[] => {
67
+ const publicAlias =
68
+ isPublicAlias(alias.name) || isPublicExposure(alias.exposure);
69
+ if (secretKeys.has(definition.key) && publicAlias) {
70
+ warnings.push({
71
+ level: 'error',
72
+ fieldKey: definition.key,
73
+ message: `Unsafe public alias was omitted for secret field ${definition.key}.`,
74
+ });
75
+ return [];
76
+ }
77
+ return [
78
+ {
79
+ logicalKey: definition.key,
80
+ envName: alias.name,
81
+ targetId: alias.targetId,
82
+ targetLabel: targetLabel(environment, alias.targetId),
83
+ profile: alias.profile,
84
+ framework: alias.framework,
85
+ public: publicAlias,
86
+ warning: publicAlias ? 'Public client alias' : undefined,
87
+ },
88
+ ];
89
+ })
90
+ );
91
+ }
92
+
93
+ function aliasMatchesInput(
94
+ alias: EnvVariableAlias,
95
+ environment: EnvironmentConfig,
96
+ input: BuildIntegrationCredentialSetupModelInput
97
+ ) {
98
+ if (
99
+ input.targetIds?.length &&
100
+ alias.targetId &&
101
+ !input.targetIds.includes(alias.targetId)
102
+ )
103
+ return false;
104
+ if (input.profile && alias.profile && alias.profile !== input.profile)
105
+ return false;
106
+ if (
107
+ !input.targetIds?.length &&
108
+ (alias.targetId || alias.profile || alias.framework)
109
+ )
110
+ return false;
111
+ if (alias.framework && alias.targetId) {
112
+ const framework = environment.targets?.[alias.targetId]?.framework;
113
+ if (framework && framework !== alias.framework) return false;
114
+ }
115
+ return true;
116
+ }
117
+
118
+ function targetLabel(
119
+ environment: EnvironmentConfig,
120
+ targetId: string | undefined
121
+ ) {
122
+ if (!targetId) return undefined;
123
+ const target = environment.targets?.[targetId];
124
+ return target?.packageName ?? target?.appId ?? target?.id ?? targetId;
125
+ }
126
+
127
+ function isPublicExposure(exposure: string | undefined) {
128
+ return exposure === 'public-client' || exposure === 'native-client';
129
+ }
130
+
131
+ function isPublicAlias(name: string, explicit?: boolean) {
132
+ return (
133
+ explicit === true ||
134
+ name.startsWith('NEXT_PUBLIC_') ||
135
+ name.startsWith('EXPO_PUBLIC_')
136
+ );
137
+ }
@@ -0,0 +1,218 @@
1
+ import type {
2
+ IntegrationCredentialFieldRef,
3
+ IntegrationCredentialManifest,
4
+ IntegrationCredentialModeManifest,
5
+ IntegrationOwnershipMode,
6
+ IntegrationSpec,
7
+ } from '@contractspec/lib.contracts-integrations';
8
+ import { getIntegrationCredentialManifest } from '@contractspec/lib.contracts-integrations';
9
+ import type { EnvironmentConfig } from '@contractspec/lib.contracts-spec/workspace-config/environment';
10
+ import { buildAliases } from './credentialSetupAliases';
11
+
12
+ export type CredentialSetupFieldKind = 'config' | 'secret';
13
+ export type CredentialSetupFieldStatus = 'configured' | 'missing' | 'optional';
14
+ export type CredentialSetupWarningLevel = 'info' | 'warning' | 'error';
15
+
16
+ export interface CredentialSetupFieldRow {
17
+ kind: CredentialSetupFieldKind;
18
+ key: string;
19
+ label: string;
20
+ description?: string;
21
+ required: boolean;
22
+ status: CredentialSetupFieldStatus;
23
+ secretRef?: string;
24
+ envVar?: string;
25
+ }
26
+
27
+ export interface CredentialSetupAliasRow {
28
+ logicalKey: string;
29
+ envName: string;
30
+ targetId?: string;
31
+ targetLabel?: string;
32
+ profile?: string;
33
+ framework?: string;
34
+ public: boolean;
35
+ warning?: string;
36
+ }
37
+
38
+ export interface CredentialSetupWarning {
39
+ level: CredentialSetupWarningLevel;
40
+ message: string;
41
+ fieldKey?: string;
42
+ }
43
+
44
+ export interface CredentialSetupModeOption {
45
+ mode: IntegrationOwnershipMode;
46
+ label: string;
47
+ available: boolean;
48
+ fieldCount: number;
49
+ missingRequiredCount: number;
50
+ docsUrl?: string;
51
+ setupSteps?: string[];
52
+ }
53
+
54
+ export interface IntegrationCredentialSetupModel {
55
+ selectedMode: IntegrationOwnershipMode;
56
+ modes: CredentialSetupModeOption[];
57
+ fields: CredentialSetupFieldRow[];
58
+ aliases: CredentialSetupAliasRow[];
59
+ warnings: CredentialSetupWarning[];
60
+ }
61
+
62
+ export interface BuildIntegrationCredentialSetupModelInput {
63
+ integration?: IntegrationSpec;
64
+ credentialManifest?: IntegrationCredentialManifest;
65
+ supportedModes?: IntegrationOwnershipMode[];
66
+ selectedMode?: IntegrationOwnershipMode;
67
+ environment?: EnvironmentConfig;
68
+ configValues?: Record<string, unknown>;
69
+ secretRefs?: Record<string, string | undefined>;
70
+ targetIds?: string[];
71
+ profile?: string;
72
+ }
73
+
74
+ export function buildIntegrationCredentialSetupModel(
75
+ input: BuildIntegrationCredentialSetupModelInput
76
+ ): IntegrationCredentialSetupModel {
77
+ const manifest = input.credentialManifest ?? manifestFromIntegration(input);
78
+ const supportedModes = resolveSupportedModes(input, manifest);
79
+ const selectedMode = input.selectedMode ?? supportedModes[0] ?? 'managed';
80
+ const modeManifest = manifest.modes?.[selectedMode];
81
+ const secretKeys = new Set(
82
+ (modeManifest?.secretFields ?? []).map((f) => f.key)
83
+ );
84
+ const warnings: CredentialSetupWarning[] = [];
85
+ const fields = buildFields(modeManifest, input, warnings);
86
+ const aliases = buildAliases(modeManifest, input, secretKeys, warnings);
87
+ const modes = supportedModes.map((mode) =>
88
+ buildModeOption(mode, manifest.modes?.[mode], input)
89
+ );
90
+
91
+ return { selectedMode, modes, fields, aliases, warnings };
92
+ }
93
+
94
+ function manifestFromIntegration(
95
+ input: BuildIntegrationCredentialSetupModelInput
96
+ ) {
97
+ return input.integration
98
+ ? getIntegrationCredentialManifest(input.integration)
99
+ : {};
100
+ }
101
+
102
+ function resolveSupportedModes(
103
+ input: BuildIntegrationCredentialSetupModelInput,
104
+ manifest: IntegrationCredentialManifest
105
+ ): IntegrationOwnershipMode[] {
106
+ return (
107
+ input.supportedModes ??
108
+ input.integration?.supportedModes ??
109
+ (Object.keys(manifest.modes ?? {}) as IntegrationOwnershipMode[])
110
+ );
111
+ }
112
+
113
+ function buildModeOption(
114
+ mode: IntegrationOwnershipMode,
115
+ manifest: IntegrationCredentialModeManifest | undefined,
116
+ input: BuildIntegrationCredentialSetupModelInput
117
+ ): CredentialSetupModeOption {
118
+ const fields = [
119
+ ...(manifest?.configFields ?? []),
120
+ ...(manifest?.secretFields ?? []),
121
+ ];
122
+ const missingRequiredCount = fields.filter(
123
+ (field) => field.required && !hasConfiguredField(field, input)
124
+ ).length;
125
+ return {
126
+ mode,
127
+ label: mode === 'byok' ? 'BYOK' : 'Managed',
128
+ available: Boolean(manifest),
129
+ fieldCount: fields.length,
130
+ missingRequiredCount,
131
+ docsUrl: manifest?.docsUrl,
132
+ setupSteps: manifest?.setupSteps,
133
+ };
134
+ }
135
+
136
+ function buildFields(
137
+ manifest: IntegrationCredentialModeManifest | undefined,
138
+ input: BuildIntegrationCredentialSetupModelInput,
139
+ warnings: CredentialSetupWarning[]
140
+ ) {
141
+ const configRows = (manifest?.configFields ?? []).map((field) =>
142
+ buildFieldRow('config', field, input, warnings)
143
+ );
144
+ const secretRows = (manifest?.secretFields ?? []).map((field) =>
145
+ buildFieldRow('secret', field, input, warnings)
146
+ );
147
+ return [...configRows, ...secretRows];
148
+ }
149
+
150
+ function buildFieldRow(
151
+ kind: CredentialSetupFieldKind,
152
+ field: IntegrationCredentialFieldRef,
153
+ input: BuildIntegrationCredentialSetupModelInput,
154
+ warnings: CredentialSetupWarning[]
155
+ ): CredentialSetupFieldRow {
156
+ const configured = hasConfiguredField(field, input);
157
+ const secretRef =
158
+ kind === 'secret' ? safeSecretRef(field, input, warnings) : undefined;
159
+ if (kind === 'secret' && field.required === true && !configured) {
160
+ warnings.push({
161
+ level: 'warning',
162
+ fieldKey: field.key,
163
+ message: `${toLabel(field.key)} is required for BYOK secret reference setup.`,
164
+ });
165
+ }
166
+ return {
167
+ kind,
168
+ key: field.key,
169
+ label: toLabel(field.key),
170
+ description: field.description,
171
+ required: field.required === true,
172
+ status: configured ? 'configured' : field.required ? 'missing' : 'optional',
173
+ secretRef,
174
+ envVar: field.envVar,
175
+ };
176
+ }
177
+
178
+ function hasConfiguredField(
179
+ field: IntegrationCredentialFieldRef,
180
+ input: BuildIntegrationCredentialSetupModelInput
181
+ ) {
182
+ const configValue = input.configValues?.[field.key];
183
+ const secretRef =
184
+ input.secretRefs?.[field.key] ?? input.secretRefs?.[field.envVar ?? ''];
185
+ return hasPresence(configValue) || hasPresence(secretRef);
186
+ }
187
+
188
+ function safeSecretRef(
189
+ field: IntegrationCredentialFieldRef,
190
+ input: BuildIntegrationCredentialSetupModelInput,
191
+ warnings: CredentialSetupWarning[]
192
+ ) {
193
+ const value =
194
+ input.secretRefs?.[field.key] ?? input.secretRefs?.[field.envVar ?? ''];
195
+ if (!value) return undefined;
196
+ if (
197
+ /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ||
198
+ /^[A-Z][A-Z0-9_]*$/.test(value)
199
+ ) {
200
+ return value;
201
+ }
202
+ warnings.push({
203
+ level: 'warning',
204
+ fieldKey: field.key,
205
+ message: `${toLabel(field.key)} has a value, but it is hidden because it is not a secret reference.`,
206
+ });
207
+ return undefined;
208
+ }
209
+
210
+ function hasPresence(value: unknown) {
211
+ return value !== undefined && value !== null && value !== '';
212
+ }
213
+
214
+ function toLabel(key: string) {
215
+ return key
216
+ .replace(/[_-]+/g, ' ')
217
+ .replace(/\b\w/g, (char) => char.toUpperCase());
218
+ }
@@ -1,3 +1,8 @@
1
+ export * from './atoms/IntegrationCredentialFieldList';
2
+ export * from './atoms/IntegrationCredentialModeTabs';
3
+ export * from './atoms/IntegrationEnvAliasPreview';
4
+ export * from './blocks/IntegrationCredentialSetupBlock';
5
+ export * from './helpers/credentialSetupModel';
1
6
  export * from './molecules/IntegrationCard';
2
7
  export * from './organisms/IntegrationMarketplace';
3
8
  export * from './organisms/IntegrationSettings';
@@ -1,4 +1,14 @@
1
- import { Button, Input, Textarea } from '@contractspec/lib.design-system';
1
+ import {
2
+ Box,
3
+ Button,
4
+ HStack,
5
+ Input,
6
+ Muted,
7
+ Small,
8
+ Text,
9
+ Textarea,
10
+ VStack,
11
+ } from '@contractspec/lib.design-system';
2
12
  import { Checkbox } from '@contractspec/lib.ui-kit-web/ui/checkbox';
3
13
  import { Label } from '@contractspec/lib.ui-kit-web/ui/label';
4
14
  import {
@@ -10,6 +20,9 @@ import {
10
20
  } from '@contractspec/lib.ui-kit-web/ui/select';
11
21
  import { Key, ShieldCheck, TestTube2 } from 'lucide-react';
12
22
  import * as React from 'react';
23
+ import { IntegrationCredentialSetupBlock } from '../blocks/IntegrationCredentialSetupBlock';
24
+ import type { BuildIntegrationCredentialSetupModelInput } from '../helpers/credentialSetupModel';
25
+ import { IntegrationSettingsSecretReference } from './IntegrationSettingsSecretReference';
13
26
 
14
27
  export interface IntegrationSettingsForm {
15
28
  apiKey: string;
@@ -27,6 +40,10 @@ export interface IntegrationSettingsProps {
27
40
  onSave?: (values: IntegrationSettingsForm) => Promise<void> | void;
28
41
  isSaving?: boolean;
29
42
  isTesting?: boolean;
43
+ credentialSetup?: Omit<
44
+ BuildIntegrationCredentialSetupModelInput,
45
+ 'selectedMode'
46
+ >;
30
47
  }
31
48
 
32
49
  export function IntegrationSettings({
@@ -36,6 +53,7 @@ export function IntegrationSettings({
36
53
  onSave,
37
54
  isSaving,
38
55
  isTesting,
56
+ credentialSetup,
39
57
  }: IntegrationSettingsProps) {
40
58
  const [values, setValues] = React.useState<IntegrationSettingsForm>({
41
59
  apiKey: initialValues?.apiKey ?? '',
@@ -49,31 +67,31 @@ export function IntegrationSettings({
49
67
  const handleChange = (
50
68
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
51
69
  ) => {
52
- const target = event.target;
53
- const { name, value } = target;
54
- setValues((prev) => ({
55
- ...prev,
56
- [name]: value,
57
- }));
70
+ const { name, value } = event.target;
71
+ setValues((prev) => ({ ...prev, [name]: value }));
58
72
  };
59
73
 
60
74
  return (
61
- <div className="space-y-4 rounded-2xl border border-border bg-card p-4">
62
- <header className="flex flex-wrap items-center justify-between gap-3">
63
- <div>
64
- <p className="font-semibold text-sm uppercase tracking-wide">
65
- {provider} credentials
66
- </p>
67
- <p className="text-muted-foreground text-sm">
75
+ <VStack
76
+ align="stretch"
77
+ gap="md"
78
+ className="rounded-2xl border border-border bg-card p-4"
79
+ >
80
+ <HStack wrap="wrap" justify="between" gap="md">
81
+ <VStack gap="sm" align="start">
82
+ <Small className="uppercase tracking-wide">{`${provider} credentials`}</Small>
83
+ <Muted>
68
84
  Store encrypted keys with BYOK and run safe connection tests.
69
- </p>
70
- </div>
85
+ </Muted>
86
+ </VStack>
71
87
  <ShieldCheck className="h-5 w-5 text-muted-foreground" />
72
- </header>
88
+ </HStack>
73
89
 
74
- <div className="grid gap-4 md:grid-cols-2">
75
- <div className="space-y-2">
76
- <Label htmlFor="ownershipMode">Ownership</Label>
90
+ <Box className="grid gap-4 md:grid-cols-2">
91
+ <VStack gap="sm" align="stretch">
92
+ <Label htmlFor="ownershipMode">
93
+ <Text>Ownership</Text>
94
+ </Label>
77
95
  <Select
78
96
  value={values.ownershipMode ?? 'managed'}
79
97
  onValueChange={(next) =>
@@ -87,21 +105,34 @@ export function IntegrationSettings({
87
105
  <SelectValue />
88
106
  </SelectTrigger>
89
107
  <SelectContent>
90
- <SelectItem value="managed">Managed (store encrypted)</SelectItem>
108
+ <SelectItem value="managed">
109
+ <Text>Managed (store encrypted)</Text>
110
+ </SelectItem>
91
111
  <SelectItem value="byok">
92
- BYOK (store secret reference)
112
+ <Text>BYOK (store secret reference)</Text>
93
113
  </SelectItem>
94
114
  </SelectContent>
95
115
  </Select>
96
- </div>
97
- </div>
116
+ </VStack>
117
+ </Box>
118
+
119
+ {credentialSetup ? (
120
+ <IntegrationCredentialSetupBlock
121
+ {...credentialSetup}
122
+ selectedMode={values.ownershipMode}
123
+ title={`${provider} setup`}
124
+ onModeChange={(mode) =>
125
+ setValues((prev) => ({ ...prev, ownershipMode: mode }))
126
+ }
127
+ />
128
+ ) : null}
98
129
 
99
- <div className="grid gap-4 md:grid-cols-2">
100
- <div className="space-y-1 text-sm">
130
+ <Box className="grid gap-4 md:grid-cols-2">
131
+ <VStack gap="sm" align="stretch">
101
132
  <Label htmlFor="apiKey" className="font-semibold">
102
- API key
133
+ <Text>API key</Text>
103
134
  </Label>
104
- <div className="relative">
135
+ <Box className="relative">
105
136
  <Key className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
106
137
  <Input
107
138
  type="text"
@@ -112,11 +143,11 @@ export function IntegrationSettings({
112
143
  onChange={handleChange}
113
144
  required
114
145
  />
115
- </div>
116
- </div>
117
- <div className="space-y-1 text-sm">
146
+ </Box>
147
+ </VStack>
148
+ <VStack gap="sm" align="stretch">
118
149
  <Label htmlFor="secret" className="font-semibold">
119
- Secret
150
+ <Text>Secret</Text>
120
151
  </Label>
121
152
  <Input
122
153
  type="password"
@@ -126,71 +157,34 @@ export function IntegrationSettings({
126
157
  value={values.secret}
127
158
  onChange={handleChange}
128
159
  />
129
- </div>
130
- </div>
160
+ </VStack>
161
+ </Box>
131
162
 
132
163
  {values.ownershipMode === 'byok' ? (
133
- <div className="space-y-4 rounded-xl border border-blue-500/20 bg-blue-500/5 p-4">
134
- <p className="font-semibold text-sm">BYOK secret reference</p>
135
- <div className="grid gap-4 md:grid-cols-2">
136
- <div className="space-y-2">
137
- <Label htmlFor="secretProvider">Secret provider</Label>
138
- <Select
139
- value={values.secretProvider ?? 'env'}
140
- onValueChange={(next) =>
141
- setValues((prev) => ({
142
- ...prev,
143
- secretProvider:
144
- next as IntegrationSettingsForm['secretProvider'],
145
- }))
146
- }
147
- >
148
- <SelectTrigger id="secretProvider" className="w-full">
149
- <SelectValue />
150
- </SelectTrigger>
151
- <SelectContent>
152
- <SelectItem value="env">Environment</SelectItem>
153
- <SelectItem value="gcp">GCP Secret Manager</SelectItem>
154
- </SelectContent>
155
- </Select>
156
- </div>
157
- <div className="space-y-2">
158
- <Label htmlFor="secretRef">Secret reference</Label>
159
- <Input
160
- id="secretRef"
161
- name="secretRef"
162
- placeholder={
163
- values.secretProvider === 'gcp'
164
- ? 'gcp://projects/.../secrets/...'
165
- : 'env://MY_TOKEN_ENV_VAR'
166
- }
167
- value={values.secretRef ?? ''}
168
- onChange={handleChange}
169
- />
170
- </div>
171
- </div>
172
- </div>
164
+ <IntegrationSettingsSecretReference
165
+ values={values}
166
+ onChange={handleChange}
167
+ onSecretProviderChange={(secretProvider) =>
168
+ setValues((prev) => ({ ...prev, secretProvider }))
169
+ }
170
+ />
173
171
  ) : (
174
- <div className="flex items-center gap-3 rounded-xl border border-emerald-500/20 bg-emerald-500/5 p-4 text-sm">
175
- <Checkbox
176
- checked
177
- onCheckedChange={() => {
178
- /* no-op */
179
- }}
180
- aria-label="Managed"
181
- />
182
- <div>
183
- <p className="font-semibold">Managed encryption enabled</p>
184
- <p className="text-muted-foreground">
185
- Secrets are encrypted server-side for this tenant.
186
- </p>
187
- </div>
188
- </div>
172
+ <HStack
173
+ align="center"
174
+ gap="md"
175
+ className="rounded-xl border border-emerald-500/20 bg-emerald-500/5 p-4 text-sm"
176
+ >
177
+ <Checkbox checked onCheckedChange={() => {}} aria-label="Managed" />
178
+ <VStack gap="sm" align="start">
179
+ <Small>Managed encryption enabled</Small>
180
+ <Muted>Secrets are encrypted server-side for this tenant.</Muted>
181
+ </VStack>
182
+ </HStack>
189
183
  )}
190
184
 
191
- <div className="space-y-1 text-sm">
185
+ <VStack gap="sm" align="stretch">
192
186
  <Label htmlFor="config" className="font-semibold">
193
- Configuration (JSON)
187
+ <Text>Configuration (JSON)</Text>
194
188
  </Label>
195
189
  <Textarea
196
190
  id="config"
@@ -199,25 +193,25 @@ export function IntegrationSettings({
199
193
  value={values.config}
200
194
  onChange={handleChange}
201
195
  />
202
- </div>
196
+ </VStack>
203
197
 
204
- <div className="flex flex-wrap items-center gap-3">
198
+ <HStack wrap="wrap" gap="md" align="center">
205
199
  <Button
206
200
  variant="ghost"
207
201
  onPress={() => onTestConnection?.(values)}
208
202
  disabled={isTesting}
209
203
  >
210
204
  <TestTube2 className="h-4 w-4" />
211
- Test connection
205
+ <Text>Test connection</Text>
212
206
  </Button>
213
207
  <Button
214
208
  variant="default"
215
209
  onPress={() => onSave?.(values)}
216
210
  disabled={isSaving}
217
211
  >
218
- Save settings
212
+ <Text>Save settings</Text>
219
213
  </Button>
220
- </div>
221
- </div>
214
+ </HStack>
215
+ </VStack>
222
216
  );
223
217
  }