ethagent 0.2.1 → 1.0.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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -32
  3. package/bin/ethagent.js +11 -2
  4. package/package.json +25 -7
  5. package/src/app/FirstRun.tsx +412 -0
  6. package/src/app/hooks/useCancelRequest.ts +22 -0
  7. package/src/app/hooks/useDoublePress.ts +46 -0
  8. package/src/app/hooks/useExitOnCtrlC.ts +36 -0
  9. package/src/app/input/AppInputProvider.tsx +116 -0
  10. package/src/app/input/appInputParser.ts +279 -0
  11. package/src/app/keybindings/KeybindingProvider.tsx +134 -0
  12. package/src/app/keybindings/resolver.ts +42 -0
  13. package/src/app/keybindings/types.ts +26 -0
  14. package/src/chat/ChatBottomPane.tsx +280 -0
  15. package/src/chat/ChatInput.tsx +722 -0
  16. package/src/chat/ChatScreen.tsx +1575 -0
  17. package/src/chat/ContextLimitView.tsx +95 -0
  18. package/src/chat/ContinuityEditReviewView.tsx +48 -0
  19. package/src/chat/ConversationStack.tsx +47 -0
  20. package/src/chat/CopyPicker.tsx +52 -0
  21. package/src/chat/MessageList.tsx +609 -0
  22. package/src/chat/PermissionPrompt.tsx +153 -0
  23. package/src/chat/PermissionsView.tsx +159 -0
  24. package/src/chat/PlanApprovalView.tsx +91 -0
  25. package/src/chat/ResumeView.tsx +267 -0
  26. package/src/chat/RewindView.tsx +386 -0
  27. package/src/chat/SessionStatus.tsx +51 -0
  28. package/src/chat/TranscriptView.tsx +202 -0
  29. package/src/chat/chatInputState.ts +247 -0
  30. package/src/chat/chatPaste.ts +49 -0
  31. package/src/chat/chatScreenUtils.ts +187 -0
  32. package/src/chat/chatSessionState.ts +142 -0
  33. package/src/chat/chatTurnOrchestrator.ts +701 -0
  34. package/src/chat/commands.ts +673 -0
  35. package/src/chat/textCursor.ts +202 -0
  36. package/src/chat/toolResultDisplay.ts +8 -0
  37. package/src/chat/transcriptViewport.ts +247 -0
  38. package/src/cli/ResetConfirmView.tsx +61 -0
  39. package/src/cli/main.tsx +177 -0
  40. package/src/cli/preview.tsx +19 -0
  41. package/src/cli/reset.ts +106 -0
  42. package/src/identity/continuity/editor.ts +149 -0
  43. package/src/identity/continuity/envelope.ts +345 -0
  44. package/src/identity/continuity/history.ts +153 -0
  45. package/src/identity/continuity/privateEdit.ts +334 -0
  46. package/src/identity/continuity/publicSkills.ts +173 -0
  47. package/src/identity/continuity/snapshots.ts +183 -0
  48. package/src/identity/continuity/storage.ts +507 -0
  49. package/src/identity/crypto/backupEnvelope.ts +486 -0
  50. package/src/identity/crypto/eth.ts +137 -0
  51. package/src/identity/hub/IdentityHub.tsx +868 -0
  52. package/src/identity/hub/identityHubEffects.ts +1146 -0
  53. package/src/identity/hub/identityHubModel.ts +291 -0
  54. package/src/identity/hub/identityHubReducer.ts +212 -0
  55. package/src/identity/hub/screens/BusyScreen.tsx +26 -0
  56. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +144 -0
  57. package/src/identity/hub/screens/CreateFlow.tsx +206 -0
  58. package/src/identity/hub/screens/DetailsScreen.tsx +64 -0
  59. package/src/identity/hub/screens/EditProfileFlow.tsx +145 -0
  60. package/src/identity/hub/screens/ErrorScreen.tsx +35 -0
  61. package/src/identity/hub/screens/IdentitySummary.tsx +70 -0
  62. package/src/identity/hub/screens/MenuScreen.tsx +117 -0
  63. package/src/identity/hub/screens/NetworkScreen.tsx +41 -0
  64. package/src/identity/hub/screens/RebackupStorageScreen.tsx +50 -0
  65. package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +85 -0
  66. package/src/identity/hub/screens/RestoreFlow.tsx +206 -0
  67. package/src/identity/hub/screens/StorageCredentialScreen.tsx +128 -0
  68. package/src/identity/hub/screens/WalletApprovalScreen.tsx +43 -0
  69. package/src/identity/profile/imagePicker.ts +180 -0
  70. package/src/identity/registry/erc8004.ts +1106 -0
  71. package/src/identity/registry/registryConfig.ts +69 -0
  72. package/src/identity/storage/ipfs.ts +212 -0
  73. package/src/identity/storage/pinataJwt.ts +53 -0
  74. package/src/identity/wallet/browserWallet.ts +393 -0
  75. package/src/identity/wallet/wallet-page/wallet.html +1082 -0
  76. package/src/mcp/approvals.ts +113 -0
  77. package/src/mcp/config.ts +235 -0
  78. package/src/mcp/manager.ts +541 -0
  79. package/src/mcp/names.ts +19 -0
  80. package/src/mcp/output.ts +96 -0
  81. package/src/models/ModelPicker.tsx +1446 -0
  82. package/src/models/catalog.ts +296 -0
  83. package/src/models/huggingface.ts +651 -0
  84. package/src/models/llamacpp.ts +810 -0
  85. package/src/models/llamacppPreflight.ts +150 -0
  86. package/src/models/modelDisplay.ts +105 -0
  87. package/src/models/modelPickerOptions.ts +421 -0
  88. package/src/models/modelRecommendation.ts +140 -0
  89. package/src/models/runtimeDetection.ts +81 -0
  90. package/src/models/uncensoredCatalog.ts +86 -0
  91. package/src/providers/anthropic.ts +259 -0
  92. package/src/providers/contracts.ts +62 -0
  93. package/src/providers/errors.ts +62 -0
  94. package/src/providers/gemini.ts +152 -0
  95. package/src/providers/openai-chat.ts +472 -0
  96. package/src/providers/registry.ts +42 -0
  97. package/src/providers/retry.ts +58 -0
  98. package/src/providers/sse.ts +93 -0
  99. package/src/runtime/compaction.ts +389 -0
  100. package/src/runtime/cwd.ts +43 -0
  101. package/src/runtime/sessionMode.ts +55 -0
  102. package/src/runtime/systemPrompt.ts +209 -0
  103. package/src/runtime/toolClaimGuards.ts +143 -0
  104. package/src/runtime/toolExecution.ts +304 -0
  105. package/src/runtime/toolIntent.ts +163 -0
  106. package/src/runtime/turn.ts +858 -0
  107. package/src/storage/atomicWrite.ts +68 -0
  108. package/src/storage/config.ts +189 -0
  109. package/src/storage/factoryReset.ts +130 -0
  110. package/src/storage/history.ts +58 -0
  111. package/src/storage/identity.ts +99 -0
  112. package/src/storage/permissions.ts +76 -0
  113. package/src/storage/rewind.ts +246 -0
  114. package/src/storage/secrets.ts +181 -0
  115. package/src/storage/sessionExport.ts +49 -0
  116. package/src/storage/sessions.ts +482 -0
  117. package/src/tools/bashSafety.ts +174 -0
  118. package/src/tools/bashTool.ts +140 -0
  119. package/src/tools/changeDirectoryTool.ts +213 -0
  120. package/src/tools/contracts.ts +179 -0
  121. package/src/tools/deleteFileTool.ts +111 -0
  122. package/src/tools/editTool.ts +160 -0
  123. package/src/tools/editUtils.ts +170 -0
  124. package/src/tools/listDirectoryTool.ts +55 -0
  125. package/src/tools/mcpResourceTools.ts +95 -0
  126. package/src/tools/permissionRules.ts +85 -0
  127. package/src/tools/privateContinuityEditTool.ts +178 -0
  128. package/src/tools/privateContinuityReadTool.ts +107 -0
  129. package/src/tools/readTool.ts +85 -0
  130. package/src/tools/registry.ts +67 -0
  131. package/src/tools/writeFileTool.ts +142 -0
  132. package/src/ui/BrandSplash.tsx +193 -0
  133. package/src/ui/ProgressBar.tsx +34 -0
  134. package/src/ui/Select.tsx +143 -0
  135. package/src/ui/Spinner.tsx +269 -0
  136. package/src/ui/Surface.tsx +47 -0
  137. package/src/ui/TextInput.tsx +97 -0
  138. package/src/ui/theme.ts +59 -0
  139. package/src/utils/clipboard.ts +216 -0
  140. package/src/utils/markdownSegments.ts +51 -0
  141. package/src/utils/messages.ts +35 -0
  142. package/src/utils/withRetry.ts +280 -0
  143. package/src/cli.tsx +0 -147
@@ -0,0 +1,69 @@
1
+ import type { EthagentConfig, SelectableNetwork } from '../../storage/config.js'
2
+ import {
3
+ chainIdForNetwork,
4
+ DEFAULT_ERC8004_CHAIN_ID,
5
+ DEFAULT_ETHEREUM_RPC_URL,
6
+ DEFAULT_ERC8004_IDENTITY_REGISTRY_ADDRESS,
7
+ MissingRegistryAddressError,
8
+ networkForChainId,
9
+ normalizeErc8004RegistryConfig,
10
+ supportedErc8004ChainForId,
11
+ type Erc8004RegistryConfig,
12
+ } from './erc8004.js'
13
+
14
+ export type RegistryResolution = {
15
+ config: Erc8004RegistryConfig | null
16
+ network: SelectableNetwork
17
+ chainId: number
18
+ needsRegistryAddress: boolean
19
+ defaultRpcUrl: string
20
+ }
21
+
22
+ export function resolveSelectedNetwork(config?: EthagentConfig): SelectableNetwork {
23
+ if (config?.selectedNetwork) return config.selectedNetwork
24
+ if (config?.erc8004?.chainId) {
25
+ const inferred = networkForChainId(config.erc8004.chainId)
26
+ if (inferred) return inferred
27
+ }
28
+ return 'mainnet'
29
+ }
30
+
31
+ export function registryConfigFromConfig(config?: EthagentConfig): RegistryResolution {
32
+ const network = resolveSelectedNetwork(config)
33
+ const chainId = chainIdForNetwork(network)
34
+ const chain = supportedErc8004ChainForId(chainId)
35
+ const overrideMatchesChain = config?.erc8004?.chainId === chainId
36
+ const defaultRpcUrl = chain?.rpcUrl ?? (chainId === DEFAULT_ERC8004_CHAIN_ID ? DEFAULT_ETHEREUM_RPC_URL : '')
37
+
38
+ const inputAddress = overrideMatchesChain ? config?.erc8004?.identityRegistryAddress : undefined
39
+ const inputRpc = overrideMatchesChain ? config?.erc8004?.rpcUrl : undefined
40
+ const inputFromBlock = overrideMatchesChain ? config?.erc8004?.fromBlock : undefined
41
+
42
+ try {
43
+ const resolved = normalizeErc8004RegistryConfig({
44
+ chainId,
45
+ rpcUrl: inputRpc ?? process.env.ETHAGENT_RPC_URL,
46
+ identityRegistryAddress: inputAddress ?? chain?.identityRegistryAddress
47
+ ?? (chainId === DEFAULT_ERC8004_CHAIN_ID ? DEFAULT_ERC8004_IDENTITY_REGISTRY_ADDRESS : undefined),
48
+ fromBlock: inputFromBlock,
49
+ })
50
+ return {
51
+ config: resolved,
52
+ network,
53
+ chainId,
54
+ needsRegistryAddress: false,
55
+ defaultRpcUrl,
56
+ }
57
+ } catch (err) {
58
+ if (err instanceof MissingRegistryAddressError) {
59
+ return {
60
+ config: null,
61
+ network,
62
+ chainId,
63
+ needsRegistryAddress: true,
64
+ defaultRpcUrl,
65
+ }
66
+ }
67
+ throw err
68
+ }
69
+ }
@@ -0,0 +1,212 @@
1
+ export const PINATA_UPLOAD_API_URL = 'https://uploads.pinata.cloud/v3/files'
2
+ export const PINATA_AUTH_TEST_URL = 'https://api.pinata.cloud/data/testAuthentication'
3
+ export const DEFAULT_PINATA_GATEWAY_URL = 'https://gateway.pinata.cloud'
4
+ export const DEFAULT_IPFS_API_URL = process.env.ETHAGENT_IPFS_API_URL?.trim() || PINATA_UPLOAD_API_URL
5
+
6
+ export type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>
7
+
8
+ export type IpfsClient = {
9
+ apiUrl: string
10
+ add: (content: string | Uint8Array) => Promise<IpfsAddResult>
11
+ cat: (cid: string) => Promise<Uint8Array>
12
+ }
13
+
14
+ export type IpfsAddResult = {
15
+ cid: string
16
+ pinVerified: boolean
17
+ provider: 'pinata' | 'ipfs'
18
+ }
19
+
20
+ type IpfsOptions = {
21
+ pinataJwt?: string
22
+ }
23
+
24
+ export function createIpfsClient(apiUrl = DEFAULT_IPFS_API_URL, fetchImpl: FetchLike = fetch, options: IpfsOptions = {}): IpfsClient {
25
+ const base = normalizeApiUrl(apiUrl)
26
+ return {
27
+ apiUrl: base,
28
+ add: content => addToIpfs(base, content, fetchImpl, options),
29
+ cat: cid => catFromIpfs(base, cid, fetchImpl),
30
+ }
31
+ }
32
+
33
+ export function needsPinataJwt(apiUrl = DEFAULT_IPFS_API_URL, options: IpfsOptions = {}): boolean {
34
+ return isPinataUploadUrl(apiUrl) && !pinataJwt(options)
35
+ }
36
+
37
+ export function extractPinataJwt(input: string): string {
38
+ const trimmed = input.trim()
39
+ const matches = trimmed.match(/\b[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g) ?? []
40
+ const jwt = matches.find(isWellFormedJwt)
41
+ if (jwt) return jwt
42
+ if (/api\s*key|api\s*secret|secret\s*key/i.test(trimmed)) {
43
+ throw new Error('Use the JWT, not the API key or secret.')
44
+ }
45
+ throw new Error('Paste the JWT from Pinata.')
46
+ }
47
+
48
+ export async function validatePinataJwt(
49
+ input: string,
50
+ fetchImpl: FetchLike = fetch,
51
+ ): Promise<string> {
52
+ const jwt = extractPinataJwt(input)
53
+ let response: Response
54
+ try {
55
+ response = await fetchImpl(PINATA_AUTH_TEST_URL, {
56
+ method: 'GET',
57
+ headers: {
58
+ accept: 'application/json',
59
+ Authorization: `Bearer ${jwt}`,
60
+ },
61
+ })
62
+ } catch {
63
+ throw new Error('Could not validate Pinata JWT. Check your connection, then try again.')
64
+ }
65
+ if (response.status === 401 || response.status === 403) {
66
+ throw new Error('Pinata rejected this JWT. Paste a valid Pinata JWT.')
67
+ }
68
+ if (!response.ok) {
69
+ throw new Error(`Pinata credential validation failed: ${response.status} ${response.statusText}`)
70
+ }
71
+ return jwt
72
+ }
73
+
74
+ export async function addToIpfs(
75
+ apiUrl: string,
76
+ content: string | Uint8Array,
77
+ fetchImpl: FetchLike = fetch,
78
+ options: IpfsOptions = {},
79
+ ): Promise<IpfsAddResult> {
80
+ if (isPinataUploadUrl(apiUrl)) return addToPinata(apiUrl, content, fetchImpl, options)
81
+ return addFileToIpfs(apiUrl, content, 'ethagent-identity-backup.json', 'application/json', fetchImpl, options)
82
+ }
83
+
84
+ export async function addFileToIpfs(
85
+ apiUrl: string,
86
+ content: string | Uint8Array,
87
+ filename: string,
88
+ contentType: string,
89
+ fetchImpl: FetchLike = fetch,
90
+ options: IpfsOptions = {},
91
+ ): Promise<IpfsAddResult> {
92
+ if (isPinataUploadUrl(apiUrl)) return addFileToPinata(apiUrl, content, filename, contentType, fetchImpl, options)
93
+ const body = new FormData()
94
+ const blobPart: BlobPart = typeof content === 'string'
95
+ ? content
96
+ : new Uint8Array(content).buffer as ArrayBuffer
97
+ const blob = new Blob([blobPart], { type: contentType })
98
+ body.append('file', blob, filename)
99
+ const response = await fetchImpl(`${normalizeApiUrl(apiUrl)}/api/v0/add?pin=true`, {
100
+ method: 'POST',
101
+ body,
102
+ })
103
+ if (!response.ok) throw new Error(`IPFS add failed: ${response.status} ${response.statusText}`)
104
+ const data = await response.json() as { Hash?: string; Cid?: string; Name?: string }
105
+ const cid = data.Hash ?? data.Cid
106
+ if (!cid) throw new Error('IPFS add response did not include a CID')
107
+ return { cid, pinVerified: true, provider: 'ipfs' }
108
+ }
109
+
110
+ export async function catFromIpfs(
111
+ apiUrl: string,
112
+ cid: string,
113
+ fetchImpl: FetchLike = fetch,
114
+ ): Promise<Uint8Array> {
115
+ if (isPinataUploadUrl(apiUrl)) return catFromPinata(cid, fetchImpl)
116
+ const arg = encodeURIComponent(cid.trim())
117
+ const response = await fetchImpl(`${normalizeApiUrl(apiUrl)}/api/v0/cat?arg=${arg}`, {
118
+ method: 'POST',
119
+ })
120
+ if (!response.ok) throw new Error(`IPFS cat failed: ${response.status} ${response.statusText}`)
121
+ return new Uint8Array(await response.arrayBuffer())
122
+ }
123
+
124
+ function normalizeApiUrl(apiUrl: string): string {
125
+ const trimmed = apiUrl.trim() || DEFAULT_IPFS_API_URL
126
+ return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
127
+ }
128
+
129
+ async function addToPinata(
130
+ apiUrl: string,
131
+ content: string | Uint8Array,
132
+ fetchImpl: FetchLike,
133
+ options: IpfsOptions,
134
+ ): Promise<IpfsAddResult> {
135
+ return addFileToPinata(apiUrl, content, 'ethagent-agent-state.json', 'application/json', fetchImpl, options)
136
+ }
137
+
138
+ async function addFileToPinata(
139
+ apiUrl: string,
140
+ content: string | Uint8Array,
141
+ filename: string,
142
+ contentType: string,
143
+ fetchImpl: FetchLike,
144
+ options: IpfsOptions,
145
+ ): Promise<IpfsAddResult> {
146
+ const jwt = pinataJwt(options)
147
+ if (!jwt) throw new Error('IPFS storage credential is missing')
148
+ const body = new FormData()
149
+ const blobPart: BlobPart = typeof content === 'string'
150
+ ? content
151
+ : new Uint8Array(content).buffer as ArrayBuffer
152
+ const blob = new Blob([blobPart], { type: contentType })
153
+ body.append('network', 'public')
154
+ body.append('file', blob, filename)
155
+ const response = await fetchImpl(normalizeApiUrl(apiUrl), {
156
+ method: 'POST',
157
+ headers: {
158
+ Authorization: `Bearer ${jwt}`,
159
+ },
160
+ body,
161
+ })
162
+ if (!response.ok) throw new Error(`IPFS upload failed: ${response.status} ${response.statusText}`)
163
+ const data = await response.json() as { data?: { cid?: string }; IpfsHash?: string; Hash?: string; Cid?: string }
164
+ const cid = data.data?.cid ?? data.IpfsHash ?? data.Hash ?? data.Cid
165
+ if (!cid) throw new Error('IPFS upload response did not include a CID')
166
+ return { cid, pinVerified: true, provider: 'pinata' }
167
+ }
168
+
169
+ function pinataJwt(options: IpfsOptions): string | undefined {
170
+ return options.pinataJwt?.trim() || process.env.PINATA_JWT?.trim() || undefined
171
+ }
172
+
173
+ async function catFromPinata(cid: string, fetchImpl: FetchLike): Promise<Uint8Array> {
174
+ const gateway = normalizeApiUrl(process.env.PINATA_GATEWAY_URL?.trim() || DEFAULT_PINATA_GATEWAY_URL)
175
+ const path = cid.trim().split('/').map(part => encodeURIComponent(part)).join('/')
176
+ const response = await fetchImpl(`${gateway}/ipfs/${path}`)
177
+ if (!response.ok) throw new Error(`IPFS fetch failed: ${response.status} ${response.statusText}`)
178
+ return new Uint8Array(await response.arrayBuffer())
179
+ }
180
+
181
+ export function isPinataUploadUrl(apiUrl: string): boolean {
182
+ try {
183
+ const url = new URL(normalizeApiUrl(apiUrl))
184
+ return url.hostname === 'uploads.pinata.cloud'
185
+ || url.hostname === 'api.pinata.cloud'
186
+ } catch {
187
+ return false
188
+ }
189
+ }
190
+
191
+ function isWellFormedJwt(input: string): boolean {
192
+ const parts = input.split('.')
193
+ if (parts.length !== 3 || parts.some(part => part.length === 0)) return false
194
+ const [header, payload] = parts
195
+ return isJsonObjectBase64Url(header!) && isJsonObjectBase64Url(payload!)
196
+ }
197
+
198
+ function isJsonObjectBase64Url(value: string): boolean {
199
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) return false
200
+ try {
201
+ const json = Buffer.from(base64UrlToBase64(value), 'base64').toString('utf8')
202
+ const parsed = JSON.parse(json) as unknown
203
+ return Boolean(parsed && typeof parsed === 'object' && !Array.isArray(parsed))
204
+ } catch {
205
+ return false
206
+ }
207
+ }
208
+
209
+ function base64UrlToBase64(value: string): string {
210
+ const normalized = value.replaceAll('-', '+').replaceAll('_', '/')
211
+ return normalized.padEnd(normalized.length + ((4 - normalized.length % 4) % 4), '=')
212
+ }
@@ -0,0 +1,53 @@
1
+ import { getSecret, hasSecret, rmSecret, setSecret, type KeyBackend } from '../../storage/secrets.js'
2
+ import { extractPinataJwt, validatePinataJwt, type FetchLike } from './ipfs.js'
3
+
4
+ const ACCOUNT = 'pinata:jwt'
5
+
6
+ let cached: string | null | undefined
7
+
8
+ type SavePinataJwtOptions = {
9
+ fetchImpl?: FetchLike
10
+ validate?: boolean
11
+ }
12
+
13
+ export async function getPinataJwt(): Promise<string | null> {
14
+ return getSecret(ACCOUNT)
15
+ }
16
+
17
+ export async function hasPinataJwt(): Promise<boolean> {
18
+ return hasSecret(ACCOUNT)
19
+ }
20
+
21
+ export async function savePinataJwt(input: string, options: SavePinataJwtOptions = {}): Promise<{ jwt: string; backend: KeyBackend }> {
22
+ const jwt = extractPinataJwt(input)
23
+ if (options.validate !== false) await validatePinataJwt(jwt, options.fetchImpl)
24
+ const backend = await setSecret(ACCOUNT, jwt)
25
+ cached = jwt
26
+ return { jwt, backend }
27
+ }
28
+
29
+ export async function clearPinataJwt(): Promise<void> {
30
+ await rmSecret(ACCOUNT)
31
+ cached = null
32
+ }
33
+
34
+ export async function resolvePinataJwt(): Promise<string | undefined> {
35
+ if (cached !== undefined) return cached ?? envJwt()
36
+ cached = await getSecret(ACCOUNT)
37
+ return cached ?? envJwt()
38
+ }
39
+
40
+ export async function resolveValidatedPinataJwt(fetchImpl: FetchLike = fetch): Promise<string | undefined> {
41
+ const jwt = await resolvePinataJwt()
42
+ if (!jwt) return undefined
43
+ return validatePinataJwt(jwt, fetchImpl)
44
+ }
45
+
46
+ export function invalidatePinataJwtCache(): void {
47
+ cached = undefined
48
+ }
49
+
50
+ function envJwt(): string | undefined {
51
+ const v = process.env.PINATA_JWT?.trim()
52
+ return v ? v : undefined
53
+ }