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
@@ -0,0 +1,115 @@
1
+ export const OPENAI_OAUTH_ISSUER = 'https://auth.openai.com'
2
+ export const OPENAI_OAUTH_TOKEN_URL = `${OPENAI_OAUTH_ISSUER}/oauth/token`
3
+ export const OPENAI_OAUTH_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
4
+ export const OPENAI_OAUTH_CALLBACK_PORT = 1455
5
+ export const OPENAI_OAUTH_SCOPE =
6
+ 'openid profile email offline_access api.connectors.read api.connectors.invoke'
7
+ export const OPENAI_OAUTH_ORIGINATOR = 'codex_cli_rs'
8
+ export const OPENAI_API_KEY_TOKEN_NAME = 'openai-api-key'
9
+ export const OPENAI_ID_TOKEN_SUBJECT_TYPE =
10
+ 'urn:ietf:params:oauth:token-type:id_token'
11
+ export const OPENAI_TOKEN_EXCHANGE_GRANT =
12
+ 'urn:ietf:params:oauth:grant-type:token-exchange'
13
+
14
+ export function asTrimmedString(value: unknown): string | undefined {
15
+ if (typeof value !== 'string') return undefined
16
+ const trimmed = value.trim()
17
+ return trimmed ? trimmed : undefined
18
+ }
19
+
20
+ export function decodeJwtPayload(
21
+ token: string,
22
+ ): Record<string, unknown> | undefined {
23
+ const parts = token.split('.')
24
+ if (parts.length < 2) return undefined
25
+ const segment = parts[1]
26
+ if (!segment) return undefined
27
+
28
+ try {
29
+ const normalized = segment.replace(/-/g, '+').replace(/_/g, '/')
30
+ const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4)
31
+ const json = Buffer.from(padded, 'base64').toString('utf8')
32
+ const parsed = JSON.parse(json) as unknown
33
+ return parsed && typeof parsed === 'object'
34
+ ? (parsed as Record<string, unknown>)
35
+ : undefined
36
+ } catch {
37
+ return undefined
38
+ }
39
+ }
40
+
41
+ export function parseChatgptAccountId(
42
+ token: string | undefined,
43
+ ): string | undefined {
44
+ if (!token) return undefined
45
+
46
+ const payload = decodeJwtPayload(token)
47
+ const nestedAuthRaw = payload?.['https://api.openai.com/auth']
48
+ const nestedAuth =
49
+ nestedAuthRaw && typeof nestedAuthRaw === 'object'
50
+ ? (nestedAuthRaw as Record<string, unknown>)
51
+ : undefined
52
+
53
+ return (
54
+ asTrimmedString(
55
+ nestedAuth?.chatgpt_account_id ??
56
+ payload?.['https://api.openai.com/auth.chatgpt_account_id'] ??
57
+ payload?.chatgpt_account_id,
58
+ ) ?? undefined
59
+ )
60
+ }
61
+
62
+ export function escapeHtml(value: string): string {
63
+ return value.replace(/[&<>"']/g, char => {
64
+ switch (char) {
65
+ case '&':
66
+ return '&amp;'
67
+ case '<':
68
+ return '&lt;'
69
+ case '>':
70
+ return '&gt;'
71
+ case '"':
72
+ return '&quot;'
73
+ case '\'':
74
+ return '&#39;'
75
+ default:
76
+ return char
77
+ }
78
+ })
79
+ }
80
+
81
+ export async function exchangeIdTokenForApiKey(idToken: string): Promise<string> {
82
+ const body = new URLSearchParams({
83
+ grant_type: OPENAI_TOKEN_EXCHANGE_GRANT,
84
+ client_id: OPENAI_OAUTH_CLIENT_ID,
85
+ requested_token: OPENAI_API_KEY_TOKEN_NAME,
86
+ subject_token: idToken,
87
+ subject_token_type: OPENAI_ID_TOKEN_SUBJECT_TYPE,
88
+ })
89
+
90
+ const response = await fetch(OPENAI_OAUTH_TOKEN_URL, {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
93
+ body,
94
+ signal: AbortSignal.timeout(15_000),
95
+ })
96
+
97
+ if (!response.ok) {
98
+ const bodyText = await response.text().catch(() => '')
99
+ throw new Error(
100
+ bodyText.trim()
101
+ ? `OpenAI API key exchange failed (${response.status}): ${bodyText.trim()}`
102
+ : `OpenAI API key exchange failed with status ${response.status}.`,
103
+ )
104
+ }
105
+
106
+ const payload = (await response.json()) as { access_token?: string }
107
+ const apiKey = asTrimmedString(payload.access_token)
108
+ if (!apiKey) {
109
+ throw new Error(
110
+ 'OpenAI API key exchange completed, but no key was returned.',
111
+ )
112
+ }
113
+
114
+ return apiKey
115
+ }
@@ -5,6 +5,7 @@ import type { MessageRow } from './MessageList.js'
5
5
  import type { ModelPickerSelection } from '../models/ModelPicker.js'
6
6
  import { sessionMessagesToRows } from './chatScreenUtils.js'
7
7
  import { formatModelDisplayName } from '../models/modelDisplay.js'
8
+ import { providerDisplayName } from '../models/modelPickerOptions.js'
8
9
 
9
10
  export type ModelSelectionResolution =
10
11
  | { kind: 'noop' }
@@ -69,7 +70,7 @@ export function resolveModelSelection(
69
70
  return {
70
71
  kind: 'switch',
71
72
  config: nextConfig,
72
- notice: `${selection.keyJustSet ? `${selection.provider} key saved.` : `${selection.provider} ready.`} Now using ${nextConfig.provider} · ${formatModelDisplayName(nextConfig.provider, nextConfig.model, { maxLength: 64 })}.`,
73
+ notice: `${selection.keyJustSet ? `${providerDisplayName(selection.provider)} key saved.` : `${providerDisplayName(selection.provider)} ready.`} Now using ${providerDisplayName(nextConfig.provider)} · ${formatModelDisplayName(nextConfig.provider, nextConfig.model, { maxLength: 64 })}.`,
73
74
  tone: 'dim',
74
75
  }
75
76
  }
@@ -19,6 +19,7 @@ import { setCwd } from '../runtime/cwd.js'
19
19
  import type { SessionMode } from '../runtime/sessionMode.js'
20
20
  import type { ContextUsage } from '../runtime/compaction.js'
21
21
  import { formatModelDisplayName } from '../models/modelDisplay.js'
22
+ import { providerDisplayName } from '../models/modelPickerOptions.js'
22
23
  import type { McpManager } from '../mcp/manager.js'
23
24
 
24
25
  export type IdentityRequestAction =
@@ -210,7 +211,7 @@ const COMMANDS: CommandSpec[] = [
210
211
  }
211
212
  await saveConfig(next)
212
213
  ctx.onReplaceConfig(next)
213
- return { kind: 'note', text: `Now using ${next.provider} · ${formatModelDisplayName(next.provider, name, { maxLength: 64 })}.` }
214
+ return { kind: 'note', text: `Now using ${providerDisplayName(next.provider)} · ${formatModelDisplayName(next.provider, name, { maxLength: 64 })}.` }
214
215
  },
215
216
  },
216
217
  {
@@ -1,20 +1,17 @@
1
1
  export const AGENT_RECORD_KEYS = {
2
- token: 'org.ethagent.token',
3
- profile: 'org.ethagent.profile',
2
+ token: 'org.ethagent.token',
4
3
  } as const
5
4
 
6
5
  type AgentRecordKey = typeof AGENT_RECORD_KEYS[keyof typeof AGENT_RECORD_KEYS]
7
6
 
8
7
  export const AGENT_RECORD_KEY_LIST: readonly AgentRecordKey[] = [
9
8
  AGENT_RECORD_KEYS.token,
10
- AGENT_RECORD_KEYS.profile,
11
9
  ] as const
12
10
 
13
11
  export const AGENT_RECORD_READ_KEY_LIST: readonly string[] = AGENT_RECORD_KEY_LIST
14
12
 
15
13
  export type AgentEnsRecords = {
16
14
  token?: string
17
- profile?: string
18
15
  }
19
16
 
20
17
  export type AgentEnsRecordState = AgentEnsRecords
@@ -28,19 +25,16 @@ export type AgentRecordDiff = {
28
25
  }
29
26
 
30
27
  const FIELD_FOR_KEY: Record<AgentRecordKey, keyof AgentEnsRecords> = {
31
- [AGENT_RECORD_KEYS.token]: 'token',
32
- [AGENT_RECORD_KEYS.profile]: 'profile',
28
+ [AGENT_RECORD_KEYS.token]: 'token',
33
29
  }
34
30
 
35
31
  const LABEL_FOR_FIELD: Record<keyof AgentEnsRecordState, string> = {
36
- token: 'Agent token',
37
- profile: 'Agent profile',
32
+ token: 'Agent token',
38
33
  }
39
34
 
40
35
  export function recordsFromTextMap(text: Record<string, string>): AgentEnsRecordState {
41
36
  return {
42
- token: text[AGENT_RECORD_KEYS.token] ?? '',
43
- profile: text[AGENT_RECORD_KEYS.profile] ?? '',
37
+ token: text[AGENT_RECORD_KEYS.token] ?? '',
44
38
  }
45
39
  }
46
40
 
@@ -71,11 +65,7 @@ export function recordLabel(field: keyof AgentEnsRecordState): string {
71
65
  return LABEL_FOR_FIELD[field]
72
66
  }
73
67
 
74
- export function formatRecordValue(field: keyof AgentEnsRecordState, value: string): string {
75
- if (field === 'profile' && value.startsWith('ipfs://')) {
76
- const cid = value.slice('ipfs://'.length)
77
- return cid.length > 18 ? `ipfs://${cid.slice(0, 10)}...${cid.slice(-6)}` : value
78
- }
68
+ export function formatRecordValue(_field: keyof AgentEnsRecordState, value: string): string {
79
69
  return value
80
70
  }
81
71
 
@@ -83,14 +73,10 @@ export function buildAgentEnsRecords(args: {
83
73
  chainId: number
84
74
  identityRegistryAddress: string
85
75
  agentId: string | undefined
86
- agentCardCid: string | undefined
87
76
  }): AgentEnsRecords {
88
77
  const records: AgentEnsRecords = {}
89
78
  if (args.agentId) {
90
79
  records.token = `eip155:${args.chainId}:${args.identityRegistryAddress.toLowerCase()}:${args.agentId}`
91
80
  }
92
- if (args.agentCardCid) {
93
- records.profile = `ipfs://${args.agentCardCid}`
94
- }
95
81
  return records
96
82
  }
@@ -230,7 +230,6 @@ export async function preflightEnsSetup(args: EnsSetupPreflightArgs): Promise<En
230
230
  chainId: args.registry.chainId,
231
231
  identityRegistryAddress: args.registry.identityRegistryAddress,
232
232
  agentId: String(args.agentId),
233
- agentCardCid: args.agentCardCid,
234
233
  })
235
234
  if (currentRecords.token && nextRecords.token && currentRecords.token !== nextRecords.token) {
236
235
  return manual(args, {
@@ -82,7 +82,6 @@ export type EnsSetupPreflightArgs = {
82
82
  allowSameOwnerOperator?: boolean
83
83
  registry: Erc8004RegistryConfig
84
84
  agentId: string | bigint | undefined
85
- agentCardCid?: string
86
85
  ensClient?: EnsAutomationReadClient
87
86
  tokenPublicClient?: TokenOwnerReadClient
88
87
  }
@@ -2,9 +2,6 @@ import React from 'react'
2
2
  import { hasPendingPublish } from './model/continuity.js'
3
3
  import type { ProfileUpdates } from './identityHubReducer.js'
4
4
  import { clearPinataJwt, savePinataJwt } from '../storage/pinataJwt.js'
5
- import {
6
- runFixRecordsSubmit,
7
- } from './effects/restoreAdmin.js'
8
5
  import {
9
6
  runRebackupStorageSubmit,
10
7
  } from './effects/rebackup/runRebackup.js'
@@ -203,11 +200,13 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
203
200
  <IdentityHubEnsFlow
204
201
  step={step}
205
202
  walletSession={walletSession}
203
+ reconciliation={reconciliation}
206
204
  onSetStep={setStep}
207
205
  onBack={back}
208
206
  onWalletReady={setWalletSession}
209
207
  onTriggerRebackup={triggerRebackup}
210
208
  onTriggerPublicProfileSave={triggerPublicProfileSave}
209
+ onWithdrawTokenForEns={currentStep => custodyFlow.beginWithdrawToken(currentStep, currentStep, 'ens')}
211
210
  />
212
211
  )
213
212
  }
@@ -240,14 +239,6 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
240
239
  onManageOperatorWallets={() => {
241
240
  setStep({ kind: 'manage-ens-operators', identity: step.identity, registry: step.registry, returnTo: step })
242
241
  }}
243
- onFixRecords={async plan => {
244
- try {
245
- await runFixRecordsSubmit({ identity: step.identity, registry: step.registry, plan, callbacks })
246
- setStep({ ...step })
247
- } catch (err: unknown) {
248
- handleStepError(err, step)
249
- }
250
- }}
251
242
  onPrepareTransfer={openTokenTransferFlow}
252
243
  onBack={back}
253
244
  />
@@ -25,11 +25,12 @@ interface IdentitySummaryProps {
25
25
  config?: EthagentConfig
26
26
  workingStatus?: ContinuityWorkingTreeStatus | null
27
27
  hideLocalChanges?: boolean
28
+ hideHeader?: boolean
28
29
  tokenLinked?: boolean
29
30
  onchainOwner?: string
30
31
  }
31
32
 
32
- export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, config, workingStatus, hideLocalChanges = false, tokenLinked = true, onchainOwner }) => {
33
+ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, config, workingStatus, hideLocalChanges = false, hideHeader = false, tokenLinked = true, onchainOwner }) => {
33
34
  if (!identity) {
34
35
  return (
35
36
  <Text color={theme.dim}>No agent yet. Create or load one.</Text>
@@ -60,8 +61,12 @@ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, conf
60
61
 
61
62
  return (
62
63
  <Box flexDirection="column">
63
- <Text color={theme.accentPeriwinkle} bold>{stateName || 'Active Agent'}</Text>
64
- <Text color={identity.agentId ? theme.text : theme.dim} bold={Boolean(identity.agentId)}>{tokenLine}</Text>
64
+ {hideHeader ? null : (
65
+ <>
66
+ <Text color={theme.accentPeriwinkle} bold>{stateName || 'Active Agent'}</Text>
67
+ <Text color={identity.agentId ? theme.text : theme.dim} bold={Boolean(identity.agentId)}>{tokenLine}</Text>
68
+ </>
69
+ )}
65
70
  <Text>
66
71
  <Text color={theme.dim}>{'ENS'.padEnd(12)}</Text>
67
72
  {ensStatus.kind === 'linked'
@@ -85,7 +85,7 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
85
85
  ? menuFlagsFromReconciliation(reconciliation, perspective)
86
86
  : (perspective === 'operator'
87
87
  ? menuFlagsFromReconciliation({
88
- token: 'unknown', custody: 'unknown', agentUri: 'unknown', ensRecords: 'unknown',
88
+ token: 'unknown', custody: 'unknown', agentUri: 'unknown',
89
89
  vault: 'unknown', workingTree: 'unknown', rpc: 'reachable', driftCount: 0, lastCheckedAt: '',
90
90
  }, perspective)
91
91
  : null)
@@ -223,7 +223,6 @@ function renderReconciliationBanner(r: AgentReconciliation, identity: EthagentId
223
223
  if (r.custody === 'mid-flow-uri-pending') lines.push('Advanced setup pending. Open Custody Mode to finish.')
224
224
  if (r.agentUri === 'local-newer') lines.push('Local state newer than chain. Save Snapshot Now to publish.')
225
225
  if (r.agentUri === 'chain-newer') lines.push('Onchain agentURI is newer than local. Refetch Latest.')
226
- if (r.ensRecords === 'drift') lines.push('ENS records out of sync. Open Custody Mode to Fix Records.')
227
226
  if (r.vault === 'missing') lines.push('Recorded vault address has no contract at it. Open Custody Mode to redeploy.')
228
227
  if (r.workingTree === 'dirty') lines.push('Local edits pending. Save Snapshot Now to publish.')
229
228
  return (
@@ -30,7 +30,7 @@ export function menuFlagsFromReconciliation(r: AgentReconciliation, perspective:
30
30
  prepareTransferReason = 'Token is in the vault. Withdraw it first in Custody Mode.'
31
31
  }
32
32
 
33
- const custodyAsterisk = r.custody === 'mid-flow-uri-pending' || r.ensRecords === 'drift' || r.vault === 'missing'
33
+ const custodyAsterisk = r.custody === 'mid-flow-uri-pending' || r.vault === 'missing'
34
34
  let custodyHint: string | undefined
35
35
  if (isOperator) {
36
36
  custodyHint = undefined
@@ -38,8 +38,6 @@ export function menuFlagsFromReconciliation(r: AgentReconciliation, perspective:
38
38
  custodyHint = 'Advanced setup pending. Open to finish.'
39
39
  } else if (r.vault === 'missing') {
40
40
  custodyHint = 'Vault contract not found. Open to redeploy.'
41
- } else if (r.ensRecords === 'drift') {
42
- custodyHint = 'ENS records out of sync. Open to fix.'
43
41
  }
44
42
 
45
43
  const custodyModeReason = isOperator
@@ -1,15 +1,15 @@
1
- import type { Address, Hex, PublicClient } from 'viem'
2
- import {
3
- DEFAULT_ETHEREUM_RPC_URL,
4
- RegisterAgentPreflightError,
5
- createErc8004PublicClient,
6
- supportedErc8004ChainForId,
7
- } from '../../../registry/erc8004.js'
8
- import { encodeSetEthagentTextRecords } from '../../../ens/ensLookup.js'
9
- import { encodeEnsRecordsTransaction, encodeEnsRegistryTransaction, type EnsSetupPlan } from '../../../ens/ensAutomation.js'
10
- import type { AgentEnsRecordState, AgentEnsRecords } from '../../../ens/agentRecords.js'
11
- import { changedRecords } from '../../../ens/agentRecords.js'
12
- import { sendBrowserWalletTransaction, type BrowserWalletSession, type WalletPurpose } from '../../../wallet/browserWallet.js'
1
+ import type { Address, Hex, PublicClient } from 'viem'
2
+ import {
3
+ DEFAULT_ETHEREUM_RPC_URL,
4
+ RegisterAgentPreflightError,
5
+ createErc8004PublicClient,
6
+ supportedErc8004ChainForId,
7
+ } from '../../../registry/erc8004.js'
8
+ import { encodeSetEthagentTextRecords } from '../../../ens/ensLookup.js'
9
+ import { encodeEnsRecordsTransaction, encodeEnsRegistryTransaction, type EnsSetupPlan } from '../../../ens/ensAutomation.js'
10
+ import type { AgentEnsRecordState, AgentEnsRecords } from '../../../ens/agentRecords.js'
11
+ import { changedRecords } from '../../../ens/agentRecords.js'
12
+ import { sendBrowserWalletTransaction, type BrowserWalletSession, type WalletPurpose } from '../../../wallet/browserWallet.js'
13
13
  import type { EffectCallbacks } from '../types.js'
14
14
  function chainLabel(chainId: number): string {
15
15
  return supportedErc8004ChainForId(chainId)?.name ?? `chain ${chainId}`
@@ -84,9 +84,9 @@ export function ensRecordWritesForUpdate(args: {
84
84
  clearRecords?: boolean
85
85
  }): Record<string, string> {
86
86
  if (args.clearRecords) {
87
- return changedRecords(args.currentRecords ?? {}, { token: '', profile: '' })
87
+ return changedRecords(args.currentRecords ?? {}, { token: '' })
88
88
  }
89
- return changedRecords(args.currentRecords ?? { token: '', profile: '' }, args.records)
89
+ return changedRecords(args.currentRecords ?? { token: '' }, args.records)
90
90
  }
91
91
 
92
92
  export async function runEnsSetupRegistryTransaction(args: {
@@ -187,7 +187,7 @@ export async function runEnsSetupRecordsTransaction(args: {
187
187
  return { txHash: result.txHash }
188
188
  }
189
189
 
190
- export function createMainnetEnsPublicClient(): PublicClient {
190
+ export function createMainnetEnsPublicClient(): PublicClient {
191
191
  return createErc8004PublicClient({
192
192
  chainId: 1,
193
193
  rpcUrl: DEFAULT_ETHEREUM_RPC_URL,
@@ -52,7 +52,6 @@ export {
52
52
  runStorageSubmit,
53
53
  } from './create.js'
54
54
  export {
55
- runFixRecordsSubmit,
56
55
  runRestoreRegistrySubmit,
57
56
  } from './restoreAdmin.js'
58
57
  export {
@@ -71,11 +71,19 @@ export function applyEnsValidationState(
71
71
  throw new Error('Advanced custody requires owner wallet and operator wallet addresses')
72
72
  }
73
73
  const ownerAddress = getAddress(ownerAddressValue)
74
- const existingOperators = mergeApprovedOperatorWallets(baseState.approvedOperatorWallets, profile.approvedOperatorWallets ?? [], { walletAddress: ownerAddress })
75
- const approvedOperatorWallets = upsertApprovedOperatorWallet(existingOperators, getAddress(operatorWallet), { walletAddress: ownerAddress })
76
74
  setOwnerAddressField(state, getAddress(ownerAddress))
77
- state.approvedOperatorWallets = approvedOperatorWallets
78
- state.activeOperatorAddress = getAddress(operatorWallet)
75
+ const operatorTouched =
76
+ profile.approvedOperatorWallets !== undefined
77
+ || profile.activeOperatorAddress !== undefined
78
+ if (operatorTouched) {
79
+ const existingOperators = mergeApprovedOperatorWallets(baseState.approvedOperatorWallets, profile.approvedOperatorWallets ?? [], { walletAddress: ownerAddress })
80
+ const approvedOperatorWallets = upsertApprovedOperatorWallet(existingOperators, getAddress(operatorWallet), { walletAddress: ownerAddress })
81
+ state.approvedOperatorWallets = approvedOperatorWallets
82
+ state.activeOperatorAddress = getAddress(operatorWallet)
83
+ } else {
84
+ state.approvedOperatorWallets = normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
85
+ state.activeOperatorAddress = getAddress(operatorWallet)
86
+ }
79
87
  return
80
88
  }
81
89
  clearOwnerAddressField(state)