ethagent 2.1.1 → 2.2.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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/auth/openaiOAuth/credentials.ts +47 -0
  3. package/src/auth/openaiOAuth/crypto.ts +23 -0
  4. package/src/auth/openaiOAuth/index.ts +238 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +125 -0
  6. package/src/auth/openaiOAuth/listener.ts +151 -0
  7. package/src/auth/openaiOAuth/refresh.ts +70 -0
  8. package/src/auth/openaiOAuth/shared.ts +115 -0
  9. package/src/chat/chatSessionState.ts +2 -1
  10. package/src/chat/commands.ts +2 -1
  11. package/src/identity/ens/agentRecords.ts +5 -19
  12. package/src/identity/ens/ensAutomation/setup.ts +0 -1
  13. package/src/identity/ens/ensAutomation/types.ts +0 -1
  14. package/src/identity/hub/OperationalRoutes.tsx +2 -11
  15. package/src/identity/hub/components/IdentitySummary.tsx +8 -3
  16. package/src/identity/hub/components/MenuScreen.tsx +1 -2
  17. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +1 -3
  18. package/src/identity/hub/effects/ens/transactions.ts +15 -15
  19. package/src/identity/hub/effects/index.ts +0 -1
  20. package/src/identity/hub/effects/profile/profileState.ts +12 -4
  21. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +37 -159
  22. package/src/identity/hub/effects/rebackup/runRebackup.ts +2 -2
  23. package/src/identity/hub/effects/restoreAdmin.ts +2 -61
  24. package/src/identity/hub/effects/shared/sync.ts +3 -44
  25. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +1 -39
  26. package/src/identity/hub/flows/custody/custodyFlowActions.ts +5 -3
  27. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +1 -1
  28. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +80 -175
  29. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +20 -75
  30. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +16 -56
  31. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +0 -18
  32. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +0 -136
  33. package/src/identity/hub/flows/ens/EnsEditShared.tsx +5 -4
  34. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +56 -205
  35. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +7 -0
  36. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +0 -31
  37. package/src/identity/hub/flows/ens/ensEditCopy.ts +1 -1
  38. package/src/identity/hub/flows/ens/ensEditTypes.ts +6 -20
  39. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +7 -0
  40. package/src/identity/hub/flows/restore/RestoreFlow.tsx +5 -5
  41. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +0 -1
  42. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +1 -34
  43. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +0 -4
  44. package/src/identity/hub/reconciliation/index.ts +0 -7
  45. package/src/identity/hub/reconciliation/walletSetup.ts +1 -194
  46. package/src/identity/wallet/browserWallet/types.ts +0 -5
  47. package/src/identity/wallet/page/copy.ts +1 -31
  48. package/src/identity/wallet/walletPurposeCompat.ts +0 -2
  49. package/src/models/ModelPicker.tsx +246 -8
  50. package/src/models/catalog.ts +28 -1
  51. package/src/models/modelPickerOptions.ts +15 -1
  52. package/src/providers/openai-responses-format.ts +156 -0
  53. package/src/providers/openai-responses.ts +276 -0
  54. package/src/providers/registry.ts +85 -8
  55. package/src/runtime/systemPrompt.ts +1 -1
  56. package/src/runtime/turn.ts +0 -1
  57. package/src/storage/secrets.ts +4 -1
  58. package/src/tools/privateContinuityEditTool.ts +6 -0
  59. package/src/utils/openExternal.ts +20 -10
  60. package/src/identity/ens/ensRegistration.ts +0 -199
@@ -16,10 +16,10 @@ import {
16
16
  unlinkEnsLinkOptions,
17
17
  } from './ensEditCopy.js'
18
18
  import {
19
- AssignEnsCurrentSetup,
20
19
  EnsSetupRow,
21
20
  footerHint,
22
21
  } from './EnsEditShared.js'
22
+ import { IdentitySummary } from '../../components/IdentitySummary.js'
23
23
  import { UnlinkEnsReviewScreen } from './EnsEditReviewScreens.js'
24
24
  import {
25
25
  DeleteSubdomainTxRunner,
@@ -32,6 +32,7 @@ import type {
32
32
 
33
33
  type MaintenanceScreenProps = {
34
34
  phase: EnsPhase
35
+ identity: EnsEditProps['identity']
35
36
  currentEnsName: string
36
37
  currentEnsCanDelete: boolean
37
38
  savedCustodyMode: CustodyMode | undefined
@@ -49,11 +50,11 @@ type MaintenanceScreenProps = {
49
50
  onBack: () => void
50
51
  onEnsUnlink: EnsEditProps['onEnsUnlink']
51
52
  onEnsRecordsUpdate: EnsEditProps['onEnsRecordsUpdate']
52
- onManageOperatorWalletAccess: EnsEditProps['onManageOperatorWalletAccess']
53
53
  }
54
54
 
55
55
  export function renderEnsMaintenancePhase({
56
56
  phase,
57
+ identity,
57
58
  currentEnsName,
58
59
  currentEnsCanDelete,
59
60
  savedCustodyMode,
@@ -71,10 +72,9 @@ export function renderEnsMaintenancePhase({
71
72
  onBack,
72
73
  onEnsUnlink,
73
74
  onEnsRecordsUpdate,
74
- onManageOperatorWalletAccess,
75
75
  }: MaintenanceScreenProps): React.ReactNode | null {
76
76
  if (phase.kind === 'mode-select') {
77
- type EnsAction = 'link' | 'unlink' | 'delete-subdomain' | 'manage-operator-wallets' | 'back'
77
+ type EnsAction = 'link' | 'unlink' | 'back'
78
78
  const isAdvanced = savedCustodyMode === 'advanced'
79
79
  const multiNeedsCustodySetup = isAdvanced && !savedOwnerAddress
80
80
  const subtitle = currentEnsName
@@ -83,19 +83,11 @@ export function renderEnsMaintenancePhase({
83
83
  const linkHint = multiNeedsCustodySetup
84
84
  ? 'Set Advanced custody first via Custody Mode'
85
85
  : isAdvanced
86
- ? 'Walks you through Root, Name, Operator Wallet, Review, and Apply'
86
+ ? 'Walks you through Root, Name, Review, and Apply'
87
87
  : 'Walks you through Root, Name, Review, and Apply'
88
88
  const options: Array<{ value: EnsAction; role?: 'section' | 'utility'; label: string; hint?: string; disabled?: boolean }> = []
89
- options.push({ value: 'link', role: 'section', label: 'Name' })
90
89
  if (currentEnsName) {
91
90
  options.push({ value: 'unlink', label: 'Unlink Name', hint: 'Removes this name from the token. Set up a different name afterward by linking again.' })
92
- if (currentEnsCanDelete) {
93
- options.push({
94
- value: 'delete-subdomain',
95
- label: 'Delete Subdomain',
96
- hint: 'Clear the onchain subdomain entry at the parent name. The label is freed for reuse, and this token unlinks from it.',
97
- })
98
- }
99
91
  } else {
100
92
  options.push({
101
93
  value: 'link',
@@ -104,36 +96,16 @@ export function renderEnsMaintenancePhase({
104
96
  disabled: multiNeedsCustodySetup,
105
97
  })
106
98
  }
107
- if (isAdvanced && savedOwnerAddress) {
108
- options.push({ value: 'manage-operator-wallets', role: 'section', label: 'Operator Wallets' })
109
- options.push({
110
- value: 'manage-operator-wallets',
111
- label: 'Manage Operator Wallets',
112
- hint: "Authorize or revoke wallets that can update this name's token and profile text records on your behalf",
113
- })
114
- }
115
99
  options.push({ value: 'back', role: 'section', label: 'Navigation' })
116
100
  options.push({ value: 'back', label: 'Back', hint: 'Return to Identity Hub', role: 'utility' })
117
101
  return (
118
102
  <Surface
119
103
  title="ENS Name"
120
104
  subtitle={subtitle}
121
- footer={footerHint('picking an action starts a stepped flow · enter select · esc back')}
105
+ footer={footerHint('enter select · esc back')}
122
106
  >
123
- <AssignEnsCurrentSetup
124
- currentEnsName={currentEnsName}
125
- currentMode={savedCustodyMode}
126
- ownerAddress={savedOwnerAddress}
127
- operatorAddress={savedOperator}
128
- tokenNetworkLabel={registryNetworkLabel}
129
- />
130
- {!currentEnsName ? (
131
- <Box marginTop={1}>
132
- <Text color={theme.dim}>No subdomain linked yet. Discovery falls back to the token ID + network pair until one is set.</Text>
133
- </Box>
134
- ) : null}
135
- {validationError ? <Text color={theme.accentError}>{validationError}</Text> : null}
136
- <Box marginTop={1}>
107
+ {validationError ? <Box marginBottom={1}><Text color={theme.accentError}>{validationError}</Text></Box> : null}
108
+ <Box>
137
109
  <Select<EnsAction>
138
110
  options={options}
139
111
  hintLayout="inline"
@@ -143,14 +115,6 @@ export function renderEnsMaintenancePhase({
143
115
  runUnlinkEnsLoading(currentEnsName)
144
116
  return
145
117
  }
146
- if (choice === 'delete-subdomain' && currentEnsName && currentEnsCanDelete) {
147
- runDeleteSubdomainPreflight(currentEnsName)
148
- return
149
- }
150
- if (choice === 'manage-operator-wallets') {
151
- onManageOperatorWalletAccess()
152
- return
153
- }
154
118
  if (choice === 'link') {
155
119
  if (multiNeedsCustodySetup) return
156
120
  if (isAdvanced && savedOwnerAddress) {
@@ -175,9 +139,7 @@ export function renderEnsMaintenancePhase({
175
139
  subtitle={`Reading ethagent records from ${phase.fullName}`}
176
140
  footer={footerHint('esc back')}
177
141
  >
178
- <Box marginTop={1}>
179
- <Spinner label="reading current ENS record values..." />
180
- </Box>
142
+ <Spinner label="reading current ENS record values..." />
181
143
  <EscCancel onCancel={() => setPhase({ kind: 'mode-select' })} />
182
144
  </Surface>
183
145
  )
@@ -210,9 +172,7 @@ export function renderEnsMaintenancePhase({
210
172
  subtitle={`Verifying the parent of ${phase.fullName} on Ethereum mainnet.`}
211
173
  footer={footerHint('esc back')}
212
174
  >
213
- <Box marginTop={1}>
214
- <Spinner label="reading parent owner from ENS..." />
215
- </Box>
175
+ <Spinner label="reading parent owner from ENS..." />
216
176
  <EscCancel onCancel={() => setPhase({ kind: 'mode-select' })} />
217
177
  </Surface>
218
178
  )
@@ -225,10 +185,10 @@ export function renderEnsMaintenancePhase({
225
185
  subtitle={`Onchain check for ${phase.fullName} did not pass.`}
226
186
  footer={footerHint('enter select · esc back')}
227
187
  >
228
- <Box flexDirection="column" marginTop={1}>
188
+ <Box flexDirection="column" marginBottom={1}>
229
189
  <Text color={theme.accentError}>{phase.reason}</Text>
230
190
  </Box>
231
- <Box marginTop={1}>
191
+ <Box>
232
192
  <Select<'back'>
233
193
  options={[
234
194
  { value: 'back', role: 'section', label: 'Navigation' },
@@ -251,7 +211,7 @@ export function renderEnsMaintenancePhase({
251
211
  subtitle={`Clear the onchain entry for ${plan.fullName} at ${plan.parentName}.`}
252
212
  footer={footerHint('enter select · esc back')}
253
213
  >
254
- <Box flexDirection="column" marginTop={1}>
214
+ <Box flexDirection="column" marginBottom={1}>
255
215
  <EnsSetupRow label="Subdomain" value={plan.fullName} />
256
216
  <EnsSetupRow label="Parent" value={plan.parentName} />
257
217
  <EnsSetupRow label="Owner wallet" value={shortAddress(plan.parentOwnerAddress)} />
@@ -264,7 +224,7 @@ export function renderEnsMaintenancePhase({
264
224
  value="Onchain: subdomain owner and resolver set to 0. Locally: this token unlinks from the name."
265
225
  />
266
226
  </Box>
267
- <Box marginTop={1}>
227
+ <Box>
268
228
  <Select<'delete' | 'back'>
269
229
  options={[
270
230
  { value: 'delete', role: 'section', label: 'Action' },
@@ -310,10 +270,10 @@ export function renderEnsMaintenancePhase({
310
270
  subtitle={`${phase.fullName} is cleared onchain and unlinked from this token.`}
311
271
  footer={footerHint('enter select · esc back')}
312
272
  >
313
- <Box flexDirection="column" marginTop={1}>
273
+ <Box flexDirection="column" marginBottom={1}>
314
274
  <Text color={theme.text}>The label is freed for reuse on the parent name.</Text>
315
275
  </Box>
316
- <Box marginTop={1}>
276
+ <Box>
317
277
  <Select<'back'>
318
278
  options={[
319
279
  { value: 'back', role: 'section', label: 'Navigation' },
@@ -113,19 +113,6 @@ export const EnsSetupReviewScreen: React.FC<EnsSetupReviewScreenProps> = ({
113
113
  }) => {
114
114
  type Action = 'begin' | 'back'
115
115
  const isSimple = setup.mode === 'simple'
116
- const [ownerIsSmartAccount, setOwnerIsSmartAccount] = React.useState(false)
117
- React.useEffect(() => {
118
- if (isSimple) return
119
- let cancelled = false
120
- const client = createErc8004PublicClient(registry)
121
- client.getBytecode({ address: getAddress(setup.ownerAddress) })
122
- .then(code => {
123
- if (cancelled) return
124
- if (code && code !== '0x') setOwnerIsSmartAccount(true)
125
- })
126
- .catch(() => {})
127
- return () => { cancelled = true }
128
- }, [isSimple, registry, setup.ownerAddress])
129
116
  const signerLabel = isSimple ? 'Connected wallet' : 'Owner wallet'
130
117
  const switchNotice = setupSwitchNotice(currentEnsName, currentMode, setup.fullName, setup.mode)
131
118
  const createLabel = setup.registryAction === 'create-subdomain'
@@ -160,11 +147,6 @@ export const EnsSetupReviewScreen: React.FC<EnsSetupReviewScreenProps> = ({
160
147
  <EnsSetupRow label="ENS network" value="Ethereum Mainnet" />
161
148
  <EnsSetupRow label="Signer wallet" value={`${shortAddress(setup.ownerAddress)} (${signerLabel.toLowerCase()})`} />
162
149
  <EnsSetupRow label="Registry action" value={createLabel} />
163
- {ownerIsSmartAccount ? (
164
- <Box marginTop={1}>
165
- <Text color={theme.accentError}>Owner wallet appears to be a smart account. Resolver delegation may require special handling depending on your wallet provider; if the operator wallet later cannot write ENS records, run "Fix Records" to retry.</Text>
166
- </Box>
167
- ) : null}
168
150
  </Box>
169
151
  <Box marginTop={1}>
170
152
  <Select<Action>
@@ -1,28 +1,16 @@
1
1
  import React from 'react'
2
- import { Box, Text } from 'ink'
3
2
  import type { Address } from 'viem'
4
3
  import { mainnet } from 'viem/chains'
5
- import { Surface } from '../../../../ui/Surface.js'
6
4
  import { useAppInput } from '../../../../app/input/AppInputProvider.js'
7
5
  import {
8
6
  createMainnetClient,
9
7
  } from '../../../ens/ensLookup.js'
10
- import {
11
- buildCommitment,
12
- encodeCommitTransaction,
13
- encodeRegisterTransaction,
14
- MIN_COMMIT_AGE_SECONDS,
15
- ONE_YEAR_SECONDS,
16
- } from '../../../ens/ensRegistration.js'
17
8
  import type { EnsSubdomainDeletePlan } from '../../../ens/ensAutomation.js'
18
9
  import {
19
10
  sendBrowserWalletTransaction,
20
11
  type BrowserWalletReady,
21
12
  } from '../../../wallet/browserWallet.js'
22
13
  import { WalletApprovalScreen } from '../../components/WalletApprovalScreen.js'
23
- import { footerHint } from './EnsEditShared.js'
24
- import type { RegisterCommitPhase } from './ensEditTypes.js'
25
- import { theme } from '../../../../ui/theme.js'
26
14
 
27
15
  export const EscCancel: React.FC<{ onCancel: () => void }> = ({ onCancel }) => {
28
16
  useAppInput((_input, key) => {
@@ -31,80 +19,6 @@ export const EscCancel: React.FC<{ onCancel: () => void }> = ({ onCancel }) => {
31
19
  return null
32
20
  }
33
21
 
34
- export const RegisterRootCommitRunner: React.FC<{
35
- phase: RegisterCommitPhase
36
- ownerAddress: Address
37
- walletSession: BrowserWalletReady | null
38
- onWalletReady: (session: BrowserWalletReady | null) => void
39
- onCommitted: () => void
40
- onError: (msg: string) => void
41
- }> = ({ phase, ownerAddress, walletSession, onWalletReady, onCommitted, onError }) => {
42
- const startedRef = React.useRef(false)
43
- React.useEffect(() => {
44
- if (startedRef.current) return
45
- startedRef.current = true
46
- const built = buildCommitment({ label: phase.label, owner: ownerAddress, durationSeconds: BigInt(ONE_YEAR_SECONDS), secret: phase.secret })
47
- const tx = encodeCommitTransaction(built.commitment)
48
- sendBrowserWalletTransaction({
49
- chainId: mainnet.id,
50
- expectedAccount: ownerAddress,
51
- to: tx.to,
52
- data: tx.data,
53
- purpose: 'register-root-commit',
54
- onReady: ready => onWalletReady(ready),
55
- })
56
- .then(async result => {
57
- onWalletReady(null)
58
- const client = createMainnetClient()
59
- await client.waitForTransactionReceipt({ hash: result.txHash })
60
- onCommitted()
61
- })
62
- .catch((err: unknown) => {
63
- onWalletReady(null)
64
- onError(err instanceof Error ? err.message : String(err))
65
- })
66
- }, [])
67
- return (
68
- <WalletApprovalScreen
69
- title="Commit ENS Name"
70
- subtitle={`Submitting the first of two transactions for ${phase.label}.eth on Ethereum mainnet. Wait for confirmation.`}
71
- walletSession={walletSession}
72
- label="waiting for connected wallet transaction..."
73
- onCancel={() => onError('Commit cancelled. Restart from the name input.')}
74
- />
75
- )
76
- }
77
-
78
- export const RegisterRootWaitScreen: React.FC<{
79
- phase: RegisterCommitPhase & { commitMinedAt: number }
80
- onReady: () => void
81
- onCancel: () => void
82
- }> = ({ phase, onReady, onCancel }) => {
83
- const [, setTick] = React.useState(0)
84
- React.useEffect(() => {
85
- const interval = setInterval(() => setTick(t => t + 1), 500)
86
- return () => clearInterval(interval)
87
- }, [])
88
- const elapsedSec = Math.floor((Date.now() - phase.commitMinedAt) / 1000)
89
- const remaining = Math.max(0, MIN_COMMIT_AGE_SECONDS + 1 - elapsedSec)
90
- React.useEffect(() => {
91
- if (remaining === 0) onReady()
92
- }, [remaining])
93
- return (
94
- <Surface
95
- title="Commit Confirmed, Waiting"
96
- subtitle={`Anti-frontrun delay before registering ${phase.label}.eth. ENS requires a 60-second wait between commit and register.`}
97
- footer={footerHint('esc cancel registration')}
98
- >
99
- <Box marginTop={1} flexDirection="column">
100
- <Text color={theme.text}>{remaining > 0 ? `${remaining} seconds remaining...` : 'Ready, advancing to the registration transaction...'}</Text>
101
- <Text color={theme.dim}>Keep this window open. The commit expires after 24 hours if you walk away.</Text>
102
- </Box>
103
- <EscCancel onCancel={onCancel} />
104
- </Surface>
105
- )
106
- }
107
-
108
22
  export const DeleteSubdomainTxRunner: React.FC<{
109
23
  plan: EnsSubdomainDeletePlan
110
24
  ownerAddress: Address
@@ -146,53 +60,3 @@ export const DeleteSubdomainTxRunner: React.FC<{
146
60
  />
147
61
  )
148
62
  }
149
-
150
- export const RegisterRootTxRunner: React.FC<{
151
- phase: RegisterCommitPhase
152
- ownerAddress: Address
153
- walletSession: BrowserWalletReady | null
154
- onWalletReady: (session: BrowserWalletReady | null) => void
155
- onRegistered: () => void
156
- onError: (msg: string) => void
157
- }> = ({ phase, ownerAddress, walletSession, onWalletReady, onRegistered, onError }) => {
158
- const startedRef = React.useRef(false)
159
- React.useEffect(() => {
160
- if (startedRef.current) return
161
- startedRef.current = true
162
- const tx = encodeRegisterTransaction({
163
- label: phase.label,
164
- owner: ownerAddress,
165
- durationSeconds: BigInt(ONE_YEAR_SECONDS),
166
- secret: phase.secret,
167
- rentPrice: phase.price,
168
- })
169
- sendBrowserWalletTransaction({
170
- chainId: mainnet.id,
171
- expectedAccount: ownerAddress,
172
- to: tx.to,
173
- data: tx.data,
174
- value: tx.value,
175
- purpose: 'register-root-tx',
176
- onReady: ready => onWalletReady(ready),
177
- })
178
- .then(async result => {
179
- onWalletReady(null)
180
- const client = createMainnetClient()
181
- await client.waitForTransactionReceipt({ hash: result.txHash })
182
- onRegistered()
183
- })
184
- .catch((err: unknown) => {
185
- onWalletReady(null)
186
- onError(err instanceof Error ? err.message : String(err))
187
- })
188
- }, [])
189
- return (
190
- <WalletApprovalScreen
191
- title="Register ENS Name"
192
- subtitle={`Paying 1 year of rent and registering ${phase.label}.eth on Ethereum mainnet.`}
193
- walletSession={walletSession}
194
- label="waiting for connected wallet transaction..."
195
- onCancel={() => onError('Registration cancelled. The commit will expire in 24 hours; restart from the name input to retry.')}
196
- />
197
- )
198
- }
@@ -123,13 +123,14 @@ export const AssignEnsCurrentSetup: React.FC<AssignEnsCurrentSetupProps> = ({
123
123
  type SubdomainEntryProps = {
124
124
  parent: string
125
125
  ownerAddress: Address
126
- suggestion: string
126
+ initialValue?: string
127
+ placeholder?: string
127
128
  error?: string
128
129
  onConfirm: (fullName: string) => void
129
130
  onBack: () => void
130
131
  }
131
132
 
132
- export const SubdomainEntry: React.FC<SubdomainEntryProps> = ({ parent, ownerAddress, suggestion, error, onConfirm, onBack }) => (
133
+ export const SubdomainEntry: React.FC<SubdomainEntryProps> = ({ parent, ownerAddress, initialValue, placeholder, error, onConfirm, onBack }) => (
133
134
  <Surface
134
135
  title={`Subdomain of ${parent}`}
135
136
  footer={footerHint('enter continues · esc back')}
@@ -139,8 +140,8 @@ export const SubdomainEntry: React.FC<SubdomainEntryProps> = ({ parent, ownerAdd
139
140
  {error ? <Text color={theme.accentError}>{error}</Text> : null}
140
141
  <TextInput
141
142
  key={`edit-ens-subdomain-${parent}`}
142
- initialValue={suggestion}
143
- placeholder="subdomain name"
143
+ initialValue={initialValue ?? ''}
144
+ placeholder={placeholder || 'subdomain name'}
144
145
  validate={value => {
145
146
  const v = sanitizeSubdomainPrefix(value)
146
147
  if (!v) return 'Subdomain name cannot be empty'