ethagent 3.3.4 → 4.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.
- package/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +35 -0
- package/LICENSE +1 -1
- package/README.md +64 -104
- package/commands/ethagent.md +40 -0
- package/package.json +16 -16
- package/src/app/keybindings/KeybindingProvider.tsx +1 -6
- package/src/app/keybindings/types.ts +1 -6
- package/src/cli/ResetConfirmView.tsx +54 -53
- package/src/cli/demo.ts +86 -0
- package/src/cli/hookIo.ts +45 -0
- package/src/cli/main.tsx +94 -123
- package/src/cli/memoryGuard.ts +49 -0
- package/src/cli/reset.ts +28 -70
- package/src/cli/sessionStart.ts +33 -0
- package/src/cli/status.ts +46 -0
- package/src/cli/sync.ts +167 -0
- package/src/cli/syncAdapters/claude-code.ts +86 -0
- package/src/cli/syncAdapters/codex.ts +66 -0
- package/src/cli/syncAdapters/index.ts +45 -0
- package/src/cli/syncAdapters/managedBlock.ts +175 -0
- package/src/cli/syncAdapters/shared.ts +63 -0
- package/src/identity/continuity/envelopeParse.ts +20 -1
- package/src/identity/continuity/publicSkills.ts +3 -1
- package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
- package/src/identity/continuity/skills/scaffold.ts +5 -2
- package/src/identity/continuity/snapshots.ts +12 -5
- package/src/identity/continuity/storage/defaults.ts +20 -19
- package/src/identity/continuity/storage/status.ts +1 -1
- package/src/identity/ens/ensLookup/constants.ts +1 -1
- package/src/identity/manager/IdentityManager.tsx +33 -0
- package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
- package/src/identity/{hub → manager}/Routes.tsx +48 -34
- package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
- package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
- package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
- package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
- package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
- package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
- package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
- package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
- package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
- package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
- package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
- package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
- package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
- package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
- package/src/identity/{hub → manager}/create/effects.ts +19 -10
- package/src/identity/manager/create/importScan.ts +122 -0
- package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
- package/src/identity/{hub → manager}/custody/actions.ts +1 -15
- package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
- package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
- package/src/identity/{hub → manager}/custody/types.ts +1 -2
- package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
- package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
- package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
- package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
- package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
- package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
- package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
- package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
- package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
- package/src/identity/{hub → manager}/profile/effects.ts +1 -3
- package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
- package/src/identity/{hub → manager}/profile/state.ts +1 -1
- package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
- package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
- package/src/identity/{hub → manager}/restore/apply.ts +1 -1
- package/src/identity/{hub → manager}/restore/auth.ts +1 -1
- package/src/identity/{hub → manager}/restore/discover.ts +1 -1
- package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
- package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
- package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
- package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
- package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
- package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
- package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
- package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
- package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
- package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
- package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
- package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
- package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
- package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
- package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
- package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
- package/src/identity/manager/shared/effects/types.ts +30 -0
- package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
- package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
- package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
- package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
- package/src/identity/{hub → manager}/shared/utils.ts +6 -10
- package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
- package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
- package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
- package/src/identity/{hub → manager}/types.ts +5 -6
- package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
- package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
- package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
- package/src/identity/registry/erc8004/discovery.ts +3 -17
- package/src/identity/registry/erc8004/utils.ts +1 -1
- package/src/identity/storage/ipfs.ts +21 -1
- package/src/identity/wallet/browserWallet/html.ts +10 -2
- package/src/identity/wallet/browserWallet/http.ts +18 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
- package/src/identity/wallet/browserWallet/requests.ts +10 -28
- package/src/identity/wallet/browserWallet/session.ts +26 -33
- package/src/identity/wallet/browserWallet/validation.ts +14 -0
- package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
- package/src/identity/wallet/page/boot.ts +43 -0
- package/src/identity/wallet/page/config.ts +59 -0
- package/src/identity/wallet/page/constants.ts +12 -0
- package/src/identity/wallet/page/copy.ts +47 -68
- package/src/identity/wallet/page/css.ts +638 -0
- package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
- package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
- package/src/identity/wallet/page/markup.ts +44 -34
- package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
- package/src/identity/wallet/page/resize.ts +95 -0
- package/src/identity/wallet/page/state.ts +135 -8
- package/src/identity/wallet/page/timeline.ts +161 -0
- package/src/identity/wallet/page/view.ts +22 -302
- package/src/storage/config.ts +30 -80
- package/src/storage/reset.ts +31 -0
- package/src/storage/secrets.ts +1 -16
- package/src/ui/Select.tsx +27 -5
- package/src/ui/Spinner.tsx +16 -15
- package/src/ui/Surface.tsx +21 -17
- package/src/ui/TextArea.tsx +173 -0
- package/src/ui/TextInput.tsx +31 -133
- package/src/ui/theme.ts +22 -13
- package/src/utils/clipboard.ts +0 -140
- package/src/app/FirstRun.tsx +0 -577
- package/src/app/FirstRunTimeline.tsx +0 -51
- package/src/app/firstRunConfig.ts +0 -26
- package/src/app/hooks/useCancelRequest.ts +0 -22
- package/src/app/hooks/useDoublePress.ts +0 -46
- package/src/app/hooks/useExitOnCtrlC.ts +0 -36
- package/src/auth/openaiOAuth/credentials.ts +0 -47
- package/src/auth/openaiOAuth/crypto.ts +0 -23
- package/src/auth/openaiOAuth/index.ts +0 -238
- package/src/auth/openaiOAuth/landingPage.ts +0 -116
- package/src/auth/openaiOAuth/listener.ts +0 -151
- package/src/auth/openaiOAuth/refresh.ts +0 -70
- package/src/auth/openaiOAuth/shared.ts +0 -115
- package/src/chat/ChatBottomPane.tsx +0 -296
- package/src/chat/ChatScreen.tsx +0 -1685
- package/src/chat/ConversationStack.tsx +0 -56
- package/src/chat/MessageList.tsx +0 -638
- package/src/chat/SessionStatus.tsx +0 -53
- package/src/chat/chatEnvironment.ts +0 -16
- package/src/chat/chatScreenUtils.ts +0 -194
- package/src/chat/chatSessionState.ts +0 -146
- package/src/chat/chatTurnContext.ts +0 -50
- package/src/chat/chatTurnOrchestrator.ts +0 -603
- package/src/chat/chatTurnRows.ts +0 -64
- package/src/chat/commands.ts +0 -494
- package/src/chat/continuityEditReview.ts +0 -42
- package/src/chat/display/DiffView.tsx +0 -193
- package/src/chat/display/SyntaxText.tsx +0 -192
- package/src/chat/display/toolCallDisplay.ts +0 -103
- package/src/chat/display/toolResultDisplay.ts +0 -19
- package/src/chat/input/ChatInput.tsx +0 -625
- package/src/chat/input/chatInputHelpers.ts +0 -62
- package/src/chat/input/chatInputState.ts +0 -247
- package/src/chat/input/chatPaste.ts +0 -49
- package/src/chat/input/imageRefs.ts +0 -30
- package/src/chat/input/inputRendering.tsx +0 -93
- package/src/chat/input/textCursor.ts +0 -212
- package/src/chat/messageMarkdown.ts +0 -220
- package/src/chat/messageRows.ts +0 -43
- package/src/chat/planImplementation.ts +0 -62
- package/src/chat/slashCommandHandlers.ts +0 -122
- package/src/chat/slashCommandViews.ts +0 -120
- package/src/chat/transcript/TranscriptView.tsx +0 -184
- package/src/chat/transcript/transcriptViewport.ts +0 -295
- package/src/chat/views/ContextLimitView.tsx +0 -95
- package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
- package/src/chat/views/CopyPicker.tsx +0 -50
- package/src/chat/views/PermissionPrompt.tsx +0 -156
- package/src/chat/views/PermissionsView.tsx +0 -165
- package/src/chat/views/PlanApprovalView.tsx +0 -91
- package/src/chat/views/ResumeView.tsx +0 -273
- package/src/chat/views/RewindView.tsx +0 -412
- package/src/cli/preview.tsx +0 -14
- package/src/cli/updateNotice.ts +0 -54
- package/src/identity/continuity/privateEdit/apply.ts +0 -170
- package/src/identity/continuity/privateEdit/diff.ts +0 -6
- package/src/identity/continuity/privateEdit/files.ts +0 -23
- package/src/identity/continuity/privateEdit/types.ts +0 -28
- package/src/identity/continuity/privateEdit.ts +0 -46
- package/src/identity/hub/IdentityHub.tsx +0 -14
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
- package/src/identity/hub/ens/effects.ts +0 -218
- package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
- package/src/identity/hub/shared/effects/types.ts +0 -53
- package/src/identity/hub/shared/reconciliation/index.ts +0 -14
- package/src/identity/wallet/page/grainient.ts +0 -278
- package/src/identity/wallet/page/html.ts +0 -28
- package/src/identity/wallet/page/styles/base.ts +0 -260
- package/src/identity/wallet/page/styles/components.ts +0 -262
- package/src/identity/wallet/page/styles/index.ts +0 -5
- package/src/identity/wallet/page/styles/responsive.ts +0 -247
- package/src/identity/wallet/page.tsx +0 -38
- package/src/mcp/approvals.ts +0 -113
- package/src/mcp/config.ts +0 -235
- package/src/mcp/manager.ts +0 -482
- package/src/mcp/managerHelpers.ts +0 -70
- package/src/mcp/names.ts +0 -19
- package/src/mcp/output.ts +0 -96
- package/src/models/ModelPicker.tsx +0 -1009
- package/src/models/catalog.ts +0 -327
- package/src/models/huggingface.ts +0 -712
- package/src/models/huggingfaceStorage.ts +0 -136
- package/src/models/llamacpp.ts +0 -848
- package/src/models/llamacppCommands.ts +0 -44
- package/src/models/llamacppConfig.ts +0 -34
- package/src/models/llamacppDiscovery.ts +0 -176
- package/src/models/llamacppOutput.ts +0 -65
- package/src/models/llamacppPreflight.ts +0 -158
- package/src/models/modelDisplay.ts +0 -180
- package/src/models/modelPickerCatalogFlow.ts +0 -56
- package/src/models/modelPickerCredentials.ts +0 -166
- package/src/models/modelPickerData.ts +0 -41
- package/src/models/modelPickerDisplay.tsx +0 -132
- package/src/models/modelPickerHfFlow.ts +0 -192
- package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
- package/src/models/modelPickerOptions.ts +0 -457
- package/src/models/modelPickerTypes.ts +0 -69
- package/src/models/modelPickerUninstallFlow.ts +0 -48
- package/src/models/modelPickerViewHelpers.ts +0 -174
- package/src/models/modelRecommendation.ts +0 -139
- package/src/models/providerDisplay.ts +0 -16
- package/src/models/runtimeDetection.ts +0 -81
- package/src/models/uncensoredCatalog.ts +0 -86
- package/src/providers/anthropic.ts +0 -290
- package/src/providers/contracts.ts +0 -71
- package/src/providers/errors.ts +0 -80
- package/src/providers/gemini.ts +0 -391
- package/src/providers/openai-chat.ts +0 -474
- package/src/providers/openai-responses-format.ts +0 -177
- package/src/providers/openai-responses.ts +0 -306
- package/src/providers/openaiChatWire.ts +0 -124
- package/src/providers/registry.ts +0 -120
- package/src/providers/retry.ts +0 -58
- package/src/providers/sse.ts +0 -93
- package/src/runtime/compaction.ts +0 -395
- package/src/runtime/cwd.ts +0 -43
- package/src/runtime/providerTurn.ts +0 -38
- package/src/runtime/sessionMode.ts +0 -55
- package/src/runtime/systemPrompt.ts +0 -213
- package/src/runtime/textToolParser.ts +0 -161
- package/src/runtime/toolClaimGuards.ts +0 -143
- package/src/runtime/toolExecution.ts +0 -304
- package/src/runtime/toolIntent.ts +0 -143
- package/src/runtime/turn.ts +0 -369
- package/src/runtime/turnNudges.ts +0 -223
- package/src/runtime/turnTypes.ts +0 -86
- package/src/storage/factoryReset.ts +0 -127
- package/src/storage/history.ts +0 -58
- package/src/storage/permissions.ts +0 -76
- package/src/storage/rewind.ts +0 -266
- package/src/storage/sessionExport.ts +0 -49
- package/src/storage/sessions.ts +0 -495
- package/src/tools/bashSafety.ts +0 -186
- package/src/tools/bashTool.ts +0 -140
- package/src/tools/changeDirectoryTool.ts +0 -213
- package/src/tools/contracts.ts +0 -192
- package/src/tools/deleteFileTool.ts +0 -116
- package/src/tools/editTool.ts +0 -165
- package/src/tools/editUtils.ts +0 -170
- package/src/tools/fileDiff.ts +0 -261
- package/src/tools/listDirectoryTool.ts +0 -55
- package/src/tools/listSkillFilesTool.ts +0 -77
- package/src/tools/listSkillsTool.ts +0 -68
- package/src/tools/mcpResourceTools.ts +0 -95
- package/src/tools/permissionRules.ts +0 -85
- package/src/tools/privateContinuityEditTool.ts +0 -187
- package/src/tools/privateContinuityReadTool.ts +0 -106
- package/src/tools/readSkillTool.ts +0 -107
- package/src/tools/readTool.ts +0 -85
- package/src/tools/registry.ts +0 -103
- package/src/tools/writeFileTool.ts +0 -167
- package/src/ui/BrandSplash.tsx +0 -133
- package/src/ui/terminalTitle.ts +0 -30
- package/src/utils/images.ts +0 -140
- package/src/utils/markdownSegments.ts +0 -51
- package/src/utils/messages.ts +0 -37
- package/src/utils/withRetry.ts +0 -324
- /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
- /package/src/identity/{hub → manager}/custody/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
- /package/src/identity/{hub → manager}/ens/state.ts +0 -0
- /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
- /package/src/identity/{hub → manager}/ens/types.ts +0 -0
- /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
- /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
- /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
- /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
- /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
- /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
- /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
- /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
- /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'react'
|
|
3
|
-
|
|
4
|
-
export const DOUBLE_PRESS_TIMEOUT_MS = 1800
|
|
5
|
-
|
|
6
|
-
export function useDoublePress(
|
|
7
|
-
setPending: (pending: boolean) => void,
|
|
8
|
-
onDoublePress: () => void,
|
|
9
|
-
): () => void {
|
|
10
|
-
const lastPressRef = useRef<number>(0)
|
|
11
|
-
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
|
12
|
-
|
|
13
|
-
const clearPendingTimeout = useCallback(() => {
|
|
14
|
-
if (timeoutRef.current) {
|
|
15
|
-
clearTimeout(timeoutRef.current)
|
|
16
|
-
timeoutRef.current = undefined
|
|
17
|
-
}
|
|
18
|
-
}, [])
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
return () => {
|
|
22
|
-
clearPendingTimeout()
|
|
23
|
-
}
|
|
24
|
-
}, [clearPendingTimeout])
|
|
25
|
-
|
|
26
|
-
return useCallback(() => {
|
|
27
|
-
const now = Date.now()
|
|
28
|
-
const elapsed = now - lastPressRef.current
|
|
29
|
-
const isDouble = elapsed <= DOUBLE_PRESS_TIMEOUT_MS && timeoutRef.current !== undefined
|
|
30
|
-
|
|
31
|
-
if (isDouble) {
|
|
32
|
-
clearPendingTimeout()
|
|
33
|
-
setPending(false)
|
|
34
|
-
onDoublePress()
|
|
35
|
-
} else {
|
|
36
|
-
setPending(true)
|
|
37
|
-
clearPendingTimeout()
|
|
38
|
-
timeoutRef.current = setTimeout(() => {
|
|
39
|
-
setPending(false)
|
|
40
|
-
timeoutRef.current = undefined
|
|
41
|
-
}, DOUBLE_PRESS_TIMEOUT_MS)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
lastPressRef.current = now
|
|
45
|
-
}, [setPending, onDoublePress, clearPendingTimeout])
|
|
46
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from 'react'
|
|
2
|
-
import { useApp } from 'ink'
|
|
3
|
-
import { useKeybinding } from '../keybindings/KeybindingProvider.js'
|
|
4
|
-
import { useDoublePress } from './useDoublePress.js'
|
|
5
|
-
|
|
6
|
-
export type ExitState = {
|
|
7
|
-
pending: boolean
|
|
8
|
-
keyName: 'ctrl+c' | null
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
type Options = {
|
|
12
|
-
isActive?: boolean
|
|
13
|
-
onInterrupt?: () => boolean
|
|
14
|
-
onExit?: () => void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function useExitOnCtrlC({ isActive = true, onInterrupt, onExit }: Options = {}): ExitState {
|
|
18
|
-
const { exit } = useApp()
|
|
19
|
-
const [state, setState] = useState<ExitState>({ pending: false, keyName: null })
|
|
20
|
-
|
|
21
|
-
const exitFn = useMemo(() => onExit ?? exit, [onExit, exit])
|
|
22
|
-
|
|
23
|
-
const ctrlCDouble = useDoublePress(
|
|
24
|
-
pending => setState({ pending, keyName: 'ctrl+c' }),
|
|
25
|
-
exitFn,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
const handleInterrupt = useCallback(() => {
|
|
29
|
-
if (onInterrupt?.()) return
|
|
30
|
-
ctrlCDouble()
|
|
31
|
-
}, [onInterrupt, ctrlCDouble])
|
|
32
|
-
|
|
33
|
-
useKeybinding('app:interrupt', handleInterrupt, { context: 'Global', isActive })
|
|
34
|
-
|
|
35
|
-
return state
|
|
36
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { getSecret, rmSecret, setSecret } from '../../storage/secrets.js'
|
|
2
|
-
|
|
3
|
-
const ACCOUNT = 'openai-oauth'
|
|
4
|
-
|
|
5
|
-
export type OpenAIOAuthCredentials = {
|
|
6
|
-
accessToken: string
|
|
7
|
-
refreshToken: string
|
|
8
|
-
idToken?: string
|
|
9
|
-
accountId?: string
|
|
10
|
-
expiresAt: number
|
|
11
|
-
lastRefreshAt: number
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function getOpenAIOAuthCredentials(): Promise<OpenAIOAuthCredentials | null> {
|
|
15
|
-
const raw = await getSecret(ACCOUNT)
|
|
16
|
-
if (!raw) return null
|
|
17
|
-
try {
|
|
18
|
-
const parsed = JSON.parse(raw) as Partial<OpenAIOAuthCredentials>
|
|
19
|
-
if (typeof parsed.accessToken !== 'string' || !parsed.accessToken) return null
|
|
20
|
-
if (typeof parsed.refreshToken !== 'string' || !parsed.refreshToken) return null
|
|
21
|
-
if (typeof parsed.expiresAt !== 'number' || !Number.isFinite(parsed.expiresAt)) return null
|
|
22
|
-
if (typeof parsed.lastRefreshAt !== 'number' || !Number.isFinite(parsed.lastRefreshAt)) return null
|
|
23
|
-
return {
|
|
24
|
-
accessToken: parsed.accessToken,
|
|
25
|
-
refreshToken: parsed.refreshToken,
|
|
26
|
-
idToken: typeof parsed.idToken === 'string' ? parsed.idToken : undefined,
|
|
27
|
-
accountId: typeof parsed.accountId === 'string' ? parsed.accountId : undefined,
|
|
28
|
-
expiresAt: parsed.expiresAt,
|
|
29
|
-
lastRefreshAt: parsed.lastRefreshAt,
|
|
30
|
-
}
|
|
31
|
-
} catch {
|
|
32
|
-
return null
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function setOpenAIOAuthCredentials(creds: OpenAIOAuthCredentials): Promise<void> {
|
|
37
|
-
await setSecret(ACCOUNT, JSON.stringify(creds))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function rmOpenAIOAuthCredentials(): Promise<void> {
|
|
41
|
-
await rmSecret(ACCOUNT)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function hasOpenAIOAuthCredentials(): Promise<boolean> {
|
|
45
|
-
const creds = await getOpenAIOAuthCredentials()
|
|
46
|
-
return creds !== null
|
|
47
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { randomBytes, webcrypto } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
function base64UrlEncode(buffer: Buffer): string {
|
|
4
|
-
return buffer
|
|
5
|
-
.toString('base64')
|
|
6
|
-
.replace(/\+/g, '-')
|
|
7
|
-
.replace(/\//g, '_')
|
|
8
|
-
.replace(/=/g, '')
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function generateCodeVerifier(): string {
|
|
12
|
-
return base64UrlEncode(randomBytes(32))
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function generateCodeChallenge(verifier: string): Promise<string> {
|
|
16
|
-
const encoded = new TextEncoder().encode(verifier)
|
|
17
|
-
const digest = await webcrypto.subtle.digest('SHA-256', encoded)
|
|
18
|
-
return base64UrlEncode(Buffer.from(digest))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function generateState(): string {
|
|
22
|
-
return base64UrlEncode(randomBytes(32))
|
|
23
|
-
}
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { AuthCodeListener } from './listener.js'
|
|
2
|
-
import { generateCodeChallenge, generateCodeVerifier, generateState } from './crypto.js'
|
|
3
|
-
import {
|
|
4
|
-
OPENAI_OAUTH_CALLBACK_PORT,
|
|
5
|
-
OPENAI_OAUTH_CLIENT_ID,
|
|
6
|
-
OPENAI_OAUTH_ISSUER,
|
|
7
|
-
OPENAI_OAUTH_ORIGINATOR,
|
|
8
|
-
OPENAI_OAUTH_SCOPE,
|
|
9
|
-
OPENAI_OAUTH_TOKEN_URL,
|
|
10
|
-
asTrimmedString,
|
|
11
|
-
exchangeIdTokenForApiKey,
|
|
12
|
-
parseChatgptAccountId,
|
|
13
|
-
} from './shared.js'
|
|
14
|
-
import { setOpenAIOAuthCredentials } from './credentials.js'
|
|
15
|
-
import { renderOAuthLandingPage } from './landingPage.js'
|
|
16
|
-
|
|
17
|
-
type OpenAIOAuthTokenResponse = {
|
|
18
|
-
id_token?: string
|
|
19
|
-
access_token?: string
|
|
20
|
-
refresh_token?: string
|
|
21
|
-
expires_in?: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type OpenAIOAuthResult =
|
|
25
|
-
| { kind: 'apikey'; apiKey: string; accountId?: string }
|
|
26
|
-
| { kind: 'oauth-only'; accountId?: string; reason?: string }
|
|
27
|
-
|
|
28
|
-
export type OpenAIOAuthOnReady = (authUrl: string) => void | Promise<void>
|
|
29
|
-
|
|
30
|
-
function buildAuthorizeUrl(args: { port: number; codeChallenge: string; state: string }): string {
|
|
31
|
-
const redirectUri = `http://localhost:${args.port}/auth/callback`
|
|
32
|
-
const params: Array<[string, string]> = [
|
|
33
|
-
['response_type', 'code'],
|
|
34
|
-
['client_id', OPENAI_OAUTH_CLIENT_ID],
|
|
35
|
-
['redirect_uri', redirectUri],
|
|
36
|
-
['scope', OPENAI_OAUTH_SCOPE],
|
|
37
|
-
['code_challenge', args.codeChallenge],
|
|
38
|
-
['code_challenge_method', 'S256'],
|
|
39
|
-
['id_token_add_organizations', 'true'],
|
|
40
|
-
['codex_cli_simplified_flow', 'true'],
|
|
41
|
-
['state', args.state],
|
|
42
|
-
['originator', OPENAI_OAUTH_ORIGINATOR],
|
|
43
|
-
]
|
|
44
|
-
const qs = params
|
|
45
|
-
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
46
|
-
.join('&')
|
|
47
|
-
return `${OPENAI_OAUTH_ISSUER}/oauth/authorize?${qs}`
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function renderSuccessPage(): string {
|
|
51
|
-
return renderOAuthLandingPage({
|
|
52
|
-
tone: 'success',
|
|
53
|
-
pageTitle: 'OpenAI Sign-in Complete',
|
|
54
|
-
headline: 'OpenAI sign-in complete',
|
|
55
|
-
message: 'You can return to your terminal.',
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function renderErrorPage(reason: string): string {
|
|
60
|
-
return renderOAuthLandingPage({
|
|
61
|
-
tone: 'error',
|
|
62
|
-
pageTitle: 'OpenAI Sign-in Failed',
|
|
63
|
-
headline: 'OpenAI sign-in failed',
|
|
64
|
-
message: reason,
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function renderCancelledPage(): string {
|
|
69
|
-
return renderOAuthLandingPage({
|
|
70
|
-
tone: 'cancelled',
|
|
71
|
-
pageTitle: 'OpenAI Sign-in Cancelled',
|
|
72
|
-
headline: 'OpenAI sign-in cancelled',
|
|
73
|
-
message: 'You can return to your terminal.',
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function exchangeAuthorizationCode(args: {
|
|
78
|
-
authorizationCode: string
|
|
79
|
-
codeVerifier: string
|
|
80
|
-
port: number
|
|
81
|
-
signal: AbortSignal
|
|
82
|
-
}): Promise<OpenAIOAuthResult> {
|
|
83
|
-
const redirectUri = `http://localhost:${args.port}/auth/callback`
|
|
84
|
-
const body = new URLSearchParams({
|
|
85
|
-
grant_type: 'authorization_code',
|
|
86
|
-
code: args.authorizationCode,
|
|
87
|
-
redirect_uri: redirectUri,
|
|
88
|
-
client_id: OPENAI_OAUTH_CLIENT_ID,
|
|
89
|
-
code_verifier: args.codeVerifier,
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
const response = await fetch(OPENAI_OAUTH_TOKEN_URL, {
|
|
93
|
-
method: 'POST',
|
|
94
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
95
|
-
body,
|
|
96
|
-
signal: AbortSignal.any([args.signal, AbortSignal.timeout(15_000)]),
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
if (!response.ok) {
|
|
100
|
-
const errorText = await response.text().catch(() => '')
|
|
101
|
-
throw new Error(
|
|
102
|
-
errorText.trim()
|
|
103
|
-
? `OpenAI OAuth token exchange failed (${response.status}): ${errorText.trim()}`
|
|
104
|
-
: `OpenAI OAuth token exchange failed with status ${response.status}.`,
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const payload = (await response.json()) as OpenAIOAuthTokenResponse
|
|
109
|
-
const accessToken = asTrimmedString(payload.access_token)
|
|
110
|
-
const refreshToken = asTrimmedString(payload.refresh_token)
|
|
111
|
-
const idToken = asTrimmedString(payload.id_token)
|
|
112
|
-
if (!accessToken || !refreshToken) {
|
|
113
|
-
throw new Error('OpenAI OAuth completed, but the response was missing access_token or refresh_token.')
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const accountId = parseChatgptAccountId(idToken) ?? parseChatgptAccountId(accessToken)
|
|
117
|
-
const expiresIn = typeof payload.expires_in === 'number' && payload.expires_in > 0
|
|
118
|
-
? payload.expires_in
|
|
119
|
-
: 3600
|
|
120
|
-
const now = Date.now()
|
|
121
|
-
await setOpenAIOAuthCredentials({
|
|
122
|
-
accessToken,
|
|
123
|
-
refreshToken,
|
|
124
|
-
idToken,
|
|
125
|
-
accountId,
|
|
126
|
-
expiresAt: now + expiresIn * 1000,
|
|
127
|
-
lastRefreshAt: now,
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
if (!idToken) {
|
|
131
|
-
return { kind: 'oauth-only', accountId, reason: 'no id_token returned' }
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const apiKey = await exchangeIdTokenForApiKey(idToken)
|
|
136
|
-
if (typeof apiKey !== 'string' || apiKey.length === 0) {
|
|
137
|
-
return { kind: 'oauth-only', accountId, reason: 'API key exchange returned an empty token' }
|
|
138
|
-
}
|
|
139
|
-
return { kind: 'apikey', apiKey, accountId }
|
|
140
|
-
} catch (err) {
|
|
141
|
-
const reason = err instanceof Error ? err.message : String(err)
|
|
142
|
-
return { kind: 'oauth-only', accountId, reason }
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export class OpenAIOAuthService {
|
|
147
|
-
private listener: AuthCodeListener | null = null
|
|
148
|
-
private exchangeAbort: AbortController | null = null
|
|
149
|
-
|
|
150
|
-
async start(onReady: OpenAIOAuthOnReady): Promise<OpenAIOAuthResult> {
|
|
151
|
-
const codeVerifier = generateCodeVerifier()
|
|
152
|
-
const listener = new AuthCodeListener('/auth/callback')
|
|
153
|
-
this.listener = listener
|
|
154
|
-
|
|
155
|
-
let port: number
|
|
156
|
-
try {
|
|
157
|
-
port = await listener.start(OPENAI_OAUTH_CALLBACK_PORT)
|
|
158
|
-
} catch (err) {
|
|
159
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
160
|
-
if (message.includes('EADDRINUSE') || message.includes(String(OPENAI_OAUTH_CALLBACK_PORT))) {
|
|
161
|
-
listener.close()
|
|
162
|
-
this.listener = null
|
|
163
|
-
throw new Error(
|
|
164
|
-
`OpenAI sign-in needs localhost:${OPENAI_OAUTH_CALLBACK_PORT} for its callback. Close any app already using that port and try again.`,
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
listener.close()
|
|
168
|
-
this.listener = null
|
|
169
|
-
throw err
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const state = generateState()
|
|
173
|
-
const codeChallenge = await generateCodeChallenge(codeVerifier)
|
|
174
|
-
const authUrl = buildAuthorizeUrl({ port, codeChallenge, state })
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const authorizationCode = await listener.waitForAuthorization(state, async () => {
|
|
178
|
-
await onReady(authUrl)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
const abort = new AbortController()
|
|
182
|
-
this.exchangeAbort = abort
|
|
183
|
-
let result: OpenAIOAuthResult
|
|
184
|
-
try {
|
|
185
|
-
result = await exchangeAuthorizationCode({
|
|
186
|
-
authorizationCode,
|
|
187
|
-
codeVerifier,
|
|
188
|
-
port,
|
|
189
|
-
signal: abort.signal,
|
|
190
|
-
})
|
|
191
|
-
} finally {
|
|
192
|
-
if (this.exchangeAbort === abort) this.exchangeAbort = null
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (this.listener !== listener) {
|
|
196
|
-
throw new Error('OpenAI sign-in was cancelled.')
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
listener.handleSuccessRedirect([], res => {
|
|
200
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
201
|
-
res.end(renderSuccessPage())
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
return result
|
|
205
|
-
} catch (error) {
|
|
206
|
-
const resolved = this.listener === listener
|
|
207
|
-
? error
|
|
208
|
-
: new Error('OpenAI sign-in was cancelled.')
|
|
209
|
-
|
|
210
|
-
if (listener.hasPendingResponse()) {
|
|
211
|
-
const isCancellation = resolved instanceof Error && resolved.message === 'OpenAI sign-in was cancelled.'
|
|
212
|
-
listener.handleErrorRedirect(res => {
|
|
213
|
-
res.writeHead(isCancellation ? 200 : 400, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
214
|
-
res.end(isCancellation ? renderCancelledPage() : renderErrorPage(
|
|
215
|
-
resolved instanceof Error ? resolved.message : String(resolved),
|
|
216
|
-
))
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
throw resolved
|
|
220
|
-
} finally {
|
|
221
|
-
this.cleanup()
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
cleanup(): void {
|
|
226
|
-
const cancellationError = new Error('OpenAI sign-in was cancelled.')
|
|
227
|
-
this.exchangeAbort?.abort(cancellationError)
|
|
228
|
-
this.exchangeAbort = null
|
|
229
|
-
if (this.listener?.hasPendingResponse()) {
|
|
230
|
-
this.listener.handleErrorRedirect(res => {
|
|
231
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
232
|
-
res.end(renderCancelledPage())
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
this.listener?.cancelPendingAuthorization(cancellationError)
|
|
236
|
-
this.listener = null
|
|
237
|
-
}
|
|
238
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { transformSync } from 'esbuild'
|
|
3
|
-
import { WALLET_CSS } from '../../identity/wallet/page/styles/index.js'
|
|
4
|
-
import { glyphs } from '../../identity/wallet/page/html.js'
|
|
5
|
-
import { walletPageSourceFile } from '../../identity/wallet/browserWallet/walletPageSource.js'
|
|
6
|
-
import { escapeHtml } from './shared.js'
|
|
7
|
-
|
|
8
|
-
export type LandingTone = 'success' | 'error' | 'cancelled'
|
|
9
|
-
|
|
10
|
-
const GRAINIENT_SOURCE_FILE = walletPageSourceFile('page/grainient.ts')
|
|
11
|
-
|
|
12
|
-
const COMPILED_GRAINIENT = compileGrainientModule()
|
|
13
|
-
|
|
14
|
-
const GRAINIENT_OPTIONS = {
|
|
15
|
-
color1: '#000422',
|
|
16
|
-
color2: '#d8dcfa',
|
|
17
|
-
color3: '#000422',
|
|
18
|
-
timeSpeed: 0.25,
|
|
19
|
-
colorBalance: 0,
|
|
20
|
-
warpStrength: 1,
|
|
21
|
-
warpFrequency: 5,
|
|
22
|
-
warpSpeed: 2,
|
|
23
|
-
warpAmplitude: 10,
|
|
24
|
-
blendAngle: 0,
|
|
25
|
-
blendSoftness: 0.05,
|
|
26
|
-
rotationAmount: 500,
|
|
27
|
-
noiseScale: 2,
|
|
28
|
-
grainAmount: 0.1,
|
|
29
|
-
grainScale: 2,
|
|
30
|
-
grainAnimated: false,
|
|
31
|
-
contrast: 1.5,
|
|
32
|
-
gamma: 1,
|
|
33
|
-
saturation: 1,
|
|
34
|
-
centerX: 0,
|
|
35
|
-
centerY: 0,
|
|
36
|
-
zoom: 0.9,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const CHECK_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>'
|
|
40
|
-
|
|
41
|
-
const LANDING_EXTRA_CSS = `
|
|
42
|
-
main[data-flow="signin"] .body > .status { margin-top: 9px; }
|
|
43
|
-
main[data-tone="error"] .status .marker {
|
|
44
|
-
color: var(--c-danger);
|
|
45
|
-
border-color: color-mix(in srgb, var(--c-danger) 45%, transparent);
|
|
46
|
-
}
|
|
47
|
-
`
|
|
48
|
-
|
|
49
|
-
function markerHtml(tone: LandingTone): string {
|
|
50
|
-
if (tone === 'success') return CHECK_SVG
|
|
51
|
-
if (tone === 'error') return '<span aria-hidden="true">!</span>'
|
|
52
|
-
return '<span aria-hidden="true">–</span>'
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function renderOAuthLandingPage(args: {
|
|
56
|
-
tone: LandingTone
|
|
57
|
-
pageTitle: string
|
|
58
|
-
headline: string
|
|
59
|
-
message: string
|
|
60
|
-
}): string {
|
|
61
|
-
const title = escapeHtml(args.pageTitle)
|
|
62
|
-
const headline = escapeHtml(args.headline)
|
|
63
|
-
const message = escapeHtml(args.message)
|
|
64
|
-
const splash = escapeHtml(glyphs.eyes)
|
|
65
|
-
const optionsJson = JSON.stringify(GRAINIENT_OPTIONS).replaceAll('<', '\\u003c')
|
|
66
|
-
const safeScript = COMPILED_GRAINIENT.replaceAll('</', '<\\/')
|
|
67
|
-
const marker = markerHtml(args.tone)
|
|
68
|
-
|
|
69
|
-
return `<!doctype html>
|
|
70
|
-
<html lang="en">
|
|
71
|
-
<head>
|
|
72
|
-
<meta charset="utf-8" />
|
|
73
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
74
|
-
<title>${title}</title>
|
|
75
|
-
<style>${WALLET_CSS}
|
|
76
|
-
${LANDING_EXTRA_CSS}</style>
|
|
77
|
-
</head>
|
|
78
|
-
<body>
|
|
79
|
-
<canvas id="grainient" class="grainient-canvas" aria-hidden="true"></canvas>
|
|
80
|
-
<main id="card" data-flow="signin" data-tone="${args.tone}">
|
|
81
|
-
<div class="chrome">
|
|
82
|
-
<span class="chrome-spacer"></span>
|
|
83
|
-
<span class="chrome-title">ethagent</span>
|
|
84
|
-
<span class="chrome-actions"></span>
|
|
85
|
-
</div>
|
|
86
|
-
<div class="body">
|
|
87
|
-
<div class="splash-wrap"><pre class="splash">${splash}</pre></div>
|
|
88
|
-
<h2 class="flow-title">${headline}</h2>
|
|
89
|
-
<div class="status">
|
|
90
|
-
<p class="status-line">
|
|
91
|
-
<span class="marker">${marker}</span>
|
|
92
|
-
<span>${message}</span>
|
|
93
|
-
</p>
|
|
94
|
-
<p class="status-hint">You may close this tab now.</p>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
</main>
|
|
98
|
-
<script>
|
|
99
|
-
${safeScript}
|
|
100
|
-
;(function () {
|
|
101
|
-
var canvas = document.getElementById('grainient');
|
|
102
|
-
if (canvas) startGrainient(canvas, ${optionsJson});
|
|
103
|
-
})();
|
|
104
|
-
</script>
|
|
105
|
-
</body>
|
|
106
|
-
</html>`
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function compileGrainientModule(): string {
|
|
110
|
-
const source = readFileSync(GRAINIENT_SOURCE_FILE, 'utf8')
|
|
111
|
-
const stripped = source
|
|
112
|
-
.split(/\r?\n/)
|
|
113
|
-
.map(line => line.replace(/^export\s+(?=(function|const|let|interface|type|class)\b)/, ''))
|
|
114
|
-
.join('\n')
|
|
115
|
-
return transformSync(stripped, { loader: 'ts', target: 'es2020' }).code
|
|
116
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
|
|
2
|
-
import type { AddressInfo } from 'node:net'
|
|
3
|
-
|
|
4
|
-
export class AuthCodeListener {
|
|
5
|
-
private localServer: Server
|
|
6
|
-
private port = 0
|
|
7
|
-
private promiseResolver: ((authorizationCode: string) => void) | null = null
|
|
8
|
-
private promiseRejecter: ((error: Error) => void) | null = null
|
|
9
|
-
private expectedState: string | null = null
|
|
10
|
-
private pendingResponse: ServerResponse | null = null
|
|
11
|
-
private readonly callbackPath: string
|
|
12
|
-
|
|
13
|
-
constructor(callbackPath = '/auth/callback') {
|
|
14
|
-
this.localServer = createServer()
|
|
15
|
-
this.callbackPath = callbackPath
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async start(port?: number): Promise<number> {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
const onError = (err: Error): void => {
|
|
21
|
-
reject(new Error(`Failed to start OAuth callback server: ${err.message}`))
|
|
22
|
-
}
|
|
23
|
-
this.localServer.once('error', onError)
|
|
24
|
-
this.localServer.listen(port ?? 0, 'localhost', () => {
|
|
25
|
-
this.localServer.removeListener('error', onError)
|
|
26
|
-
const address = this.localServer.address() as AddressInfo
|
|
27
|
-
this.port = address.port
|
|
28
|
-
resolve(this.port)
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
getPort(): number {
|
|
34
|
-
return this.port
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
hasPendingResponse(): boolean {
|
|
38
|
-
return this.pendingResponse !== null
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async waitForAuthorization(
|
|
42
|
-
state: string,
|
|
43
|
-
onReady: () => Promise<void> | void,
|
|
44
|
-
): Promise<string> {
|
|
45
|
-
return new Promise<string>((resolve, reject) => {
|
|
46
|
-
this.promiseResolver = resolve
|
|
47
|
-
this.promiseRejecter = reject
|
|
48
|
-
this.expectedState = state
|
|
49
|
-
this.localServer.on('request', this.handleRedirect.bind(this))
|
|
50
|
-
this.localServer.on('error', err => this.cancelPendingAuthorization(err))
|
|
51
|
-
void Promise.resolve(onReady()).catch(err => this.cancelPendingAuthorization(err as Error))
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
handleSuccessRedirect(
|
|
56
|
-
_scopes: string[],
|
|
57
|
-
customHandler?: (res: ServerResponse) => void,
|
|
58
|
-
): void {
|
|
59
|
-
if (!this.pendingResponse) return
|
|
60
|
-
const response = this.pendingResponse
|
|
61
|
-
try {
|
|
62
|
-
if (customHandler) customHandler(response)
|
|
63
|
-
else {
|
|
64
|
-
response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
65
|
-
response.end('OpenAI sign-in complete. You can close this tab.')
|
|
66
|
-
}
|
|
67
|
-
if (!response.writableEnded && !response.destroyed) response.end()
|
|
68
|
-
} finally {
|
|
69
|
-
this.pendingResponse = null
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
handleErrorRedirect(customHandler?: (res: ServerResponse) => void): void {
|
|
74
|
-
if (!this.pendingResponse) return
|
|
75
|
-
const response = this.pendingResponse
|
|
76
|
-
try {
|
|
77
|
-
if (customHandler) customHandler(response)
|
|
78
|
-
else {
|
|
79
|
-
response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
80
|
-
response.end('OpenAI sign-in failed. You can close this tab.')
|
|
81
|
-
}
|
|
82
|
-
if (!response.writableEnded && !response.destroyed) response.end()
|
|
83
|
-
} finally {
|
|
84
|
-
this.pendingResponse = null
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
cancelPendingAuthorization(error: Error = new Error('OAuth authorization was cancelled.')): void {
|
|
89
|
-
this.reject(error)
|
|
90
|
-
this.close()
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private handleRedirect(req: IncomingMessage, res: ServerResponse): void {
|
|
94
|
-
const parsedUrl = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`)
|
|
95
|
-
if (parsedUrl.pathname !== this.callbackPath) {
|
|
96
|
-
res.writeHead(404)
|
|
97
|
-
res.end()
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
const authCode = parsedUrl.searchParams.get('code') ?? undefined
|
|
101
|
-
const state = parsedUrl.searchParams.get('state') ?? undefined
|
|
102
|
-
this.validateAndRespond(authCode, state, res)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private validateAndRespond(
|
|
106
|
-
authCode: string | undefined,
|
|
107
|
-
state: string | undefined,
|
|
108
|
-
res: ServerResponse,
|
|
109
|
-
): void {
|
|
110
|
-
if (!authCode) {
|
|
111
|
-
res.writeHead(400)
|
|
112
|
-
res.end('Authorization code not found')
|
|
113
|
-
this.reject(new Error('No authorization code received'))
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
if (state !== this.expectedState) {
|
|
117
|
-
res.writeHead(400)
|
|
118
|
-
res.end('Invalid state parameter')
|
|
119
|
-
this.reject(new Error('Invalid state parameter'))
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
this.pendingResponse = res
|
|
123
|
-
this.resolve(authCode)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private resolve(authorizationCode: string): void {
|
|
127
|
-
if (this.promiseResolver) {
|
|
128
|
-
this.promiseResolver(authorizationCode)
|
|
129
|
-
this.promiseResolver = null
|
|
130
|
-
this.promiseRejecter = null
|
|
131
|
-
this.expectedState = null
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private reject(error: Error): void {
|
|
136
|
-
if (this.promiseRejecter) {
|
|
137
|
-
this.promiseRejecter(error)
|
|
138
|
-
this.promiseResolver = null
|
|
139
|
-
this.promiseRejecter = null
|
|
140
|
-
this.expectedState = null
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
close(): void {
|
|
145
|
-
if (this.pendingResponse) this.handleErrorRedirect()
|
|
146
|
-
this.localServer.removeAllListeners()
|
|
147
|
-
this.localServer.close()
|
|
148
|
-
this.expectedState = null
|
|
149
|
-
this.port = 0
|
|
150
|
-
}
|
|
151
|
-
}
|