[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
167
application/state/usePluginAuthenticationChallenges.ts
Normal file
167
application/state/usePluginAuthenticationChallenges.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { AuthenticationChallenge } from "@netcatty/plugin-contract";
|
||||
import { isSafePluginAuthenticationUrl } from "../../domain/pluginConnection";
|
||||
import { pluginExtensionBridge } from "./pluginExtensionBridge";
|
||||
|
||||
type ChallengeEvent = NetcattyPluginAuthenticationChallengeEvent;
|
||||
export type PluginAuthenticationChallengeResponse = string | boolean | ReadonlyArray<string>;
|
||||
type ActiveChallengeEvent = Extract<ChallengeEvent, { challenge: AuthenticationChallenge }>;
|
||||
type ChallengeQueueRef = { current: ActiveChallengeEvent[] };
|
||||
type ChallengeQueueSetter = (next: ActiveChallengeEvent[]) => void;
|
||||
type ChallengeResponder = typeof pluginExtensionBridge.respondAuthenticationChallenge;
|
||||
|
||||
const MAX_PENDING_AUTHENTICATION_CHALLENGES = 32;
|
||||
|
||||
export const handlePluginAuthenticationChallengeEvent = (
|
||||
queueRef: ChallengeQueueRef,
|
||||
setQueue: ChallengeQueueSetter,
|
||||
event: ChallengeEvent,
|
||||
respond: ChallengeResponder,
|
||||
) => {
|
||||
const existing = queueRef.current;
|
||||
if ("cancelled" in event && event.cancelled === true) {
|
||||
const next = existing.filter((item) => item.challengeRequestId !== event.challengeRequestId);
|
||||
if (next.length !== existing.length) {
|
||||
queueRef.current = next;
|
||||
setQueue(next);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (existing.some((item) => item.challengeRequestId === event.challengeRequestId)) return;
|
||||
if (existing.length >= MAX_PENDING_AUTHENTICATION_CHALLENGES) {
|
||||
void respond({
|
||||
requestId: event.requestId,
|
||||
challengeRequestId: event.challengeRequestId,
|
||||
challengeId: event.challenge.id,
|
||||
cancelled: true,
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
const next = [...existing, event];
|
||||
queueRef.current = next;
|
||||
setQueue(next);
|
||||
};
|
||||
|
||||
export const pluginAuthenticationChallengeMessage = (challenge: AuthenticationChallenge): string | undefined => (
|
||||
"message" in challenge && typeof challenge.message === "string" ? challenge.message : undefined
|
||||
);
|
||||
|
||||
export const pluginAuthenticationResponseErrorMessage = (error: unknown): string => {
|
||||
const message = error instanceof Error
|
||||
? error.message
|
||||
: typeof error === "string" ? error : "";
|
||||
return message.trim().slice(0, 512);
|
||||
};
|
||||
|
||||
export function usePluginAuthenticationChallenges() {
|
||||
const [queue, setQueue] = useState<ActiveChallengeEvent[]>([]);
|
||||
const queueRef = useRef<ActiveChallengeEvent[]>([]);
|
||||
const [textValue, setTextValue] = useState("");
|
||||
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
|
||||
const [responseError, setResponseError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const current = queue[0];
|
||||
const challenge = current?.challenge;
|
||||
|
||||
useEffect(() => {
|
||||
return pluginExtensionBridge.onAuthenticationChallenge((event) => {
|
||||
handlePluginAuthenticationChallengeEvent(
|
||||
queueRef,
|
||||
setQueue,
|
||||
event,
|
||||
(response) => pluginExtensionBridge.respondAuthenticationChallenge(response),
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTextValue("");
|
||||
setSelectedChoices([]);
|
||||
setResponseError(null);
|
||||
setBusy(false);
|
||||
}, [current?.challengeRequestId]);
|
||||
|
||||
const complete = useCallback(async (
|
||||
response?: PluginAuthenticationChallengeResponse,
|
||||
cancelled = false,
|
||||
) => {
|
||||
if (!current || busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await pluginExtensionBridge.respondAuthenticationChallenge({
|
||||
requestId: current.requestId,
|
||||
challengeRequestId: current.challengeRequestId,
|
||||
challengeId: current.challenge.id,
|
||||
...(cancelled ? { cancelled: true } : { response }),
|
||||
});
|
||||
const next = queueRef.current.filter(
|
||||
(item) => item.challengeRequestId !== current.challengeRequestId,
|
||||
);
|
||||
queueRef.current = next;
|
||||
setQueue(next);
|
||||
setResponseError(null);
|
||||
} catch (error) {
|
||||
setResponseError(pluginAuthenticationResponseErrorMessage(error));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [busy, current]);
|
||||
|
||||
const externalUrl = useMemo(() => {
|
||||
if (!challenge) return null;
|
||||
const value = challenge.kind === "browser"
|
||||
? challenge.url
|
||||
: challenge.kind === "deviceCode"
|
||||
? challenge.verificationUri
|
||||
: null;
|
||||
return value && isSafePluginAuthenticationUrl(value) ? value : null;
|
||||
}, [challenge]);
|
||||
|
||||
const openExternal = useCallback(async () => {
|
||||
if (!externalUrl) return;
|
||||
await pluginExtensionBridge.openExternal(externalUrl);
|
||||
}, [externalUrl]);
|
||||
|
||||
const isText = challenge?.kind === "text" || challenge?.kind === "password" || challenge?.kind === "otp";
|
||||
const canSubmit = challenge?.kind === "choice"
|
||||
? selectedChoices.length > 0
|
||||
: isText
|
||||
? textValue.length > 0
|
||||
: challenge?.kind === "browser" || challenge?.kind === "deviceCode"
|
||||
? externalUrl !== null
|
||||
: Boolean(challenge);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (!challenge) return;
|
||||
if (isText) void complete(textValue);
|
||||
else if (challenge.kind === "choice") {
|
||||
void complete(challenge.multiple ? selectedChoices : selectedChoices[0]);
|
||||
} else {
|
||||
void complete(true);
|
||||
}
|
||||
}, [challenge, complete, isText, selectedChoices, textValue]);
|
||||
|
||||
const setChoiceSelected = useCallback((choiceId: string, checked: boolean) => {
|
||||
setSelectedChoices((existing) => challenge?.kind === "choice" && challenge.multiple
|
||||
? checked
|
||||
? [...new Set([...existing, choiceId])]
|
||||
: existing.filter((id) => id !== choiceId)
|
||||
: checked ? [choiceId] : []);
|
||||
}, [challenge]);
|
||||
|
||||
return {
|
||||
challenge,
|
||||
busy,
|
||||
textValue,
|
||||
setTextValue,
|
||||
selectedChoices,
|
||||
setChoiceSelected,
|
||||
externalUrl,
|
||||
responseError,
|
||||
openExternal,
|
||||
complete,
|
||||
isText,
|
||||
canSubmit,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user