(wip): migrate ttp to mtp
(feat): migrate tauth to mtp (qol): update todo (qol): add direnv
This commit is contained in:
parent
8e831fd993
commit
ee5955fc75
8 changed files with 97 additions and 38 deletions
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from "react";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { onResume } from "tauri-plugin-app-events-api";
|
||||
import { MTPClient } from "mtp";
|
||||
import { ConnectionState, MTPClient } from "mtp";
|
||||
import type { z } from "zod";
|
||||
|
||||
import {
|
||||
|
|
@ -31,12 +31,6 @@ import {
|
|||
RETRY_INTERVAL,
|
||||
} from "./values";
|
||||
|
||||
const READY_STATE = {
|
||||
CLOSED: 0,
|
||||
CONNECTING: 1,
|
||||
OPEN: 2,
|
||||
} as const;
|
||||
|
||||
const PUSH_TYPES = [
|
||||
"message_live",
|
||||
"message_state",
|
||||
|
|
@ -153,6 +147,7 @@ function mapDataKeys(value: unknown, mapKey: (key: string) => string): unknown {
|
|||
);
|
||||
}
|
||||
|
||||
// Zod schema validation
|
||||
function validateResponse<T extends keyof Schemas & string>(
|
||||
type: T,
|
||||
message: { id?: number; type: string; data: unknown },
|
||||
|
|
@ -189,8 +184,9 @@ export function Provider(props: {
|
|||
}) {
|
||||
const { load } = useStorage();
|
||||
|
||||
const [readyState, setReadyState] = useState<number>(READY_STATE.CLOSED);
|
||||
const [connected, setConnected] = useState<boolean>(false);
|
||||
const [readyState, setReadyState] = useState<number>(
|
||||
ConnectionState.Disconnected,
|
||||
);
|
||||
const [identified, setIdentified] = useState<boolean>(false);
|
||||
const [identifying, setIdentifying] = useState<boolean>(false);
|
||||
|
||||
|
|
@ -208,11 +204,15 @@ export function Provider(props: {
|
|||
null,
|
||||
);
|
||||
|
||||
const connected = readyState === ConnectionState.Connected;
|
||||
|
||||
// MTP url
|
||||
const [mtpUrl, setMtpUrl] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
load("mtp_url").then(setMtpUrl);
|
||||
}, [load]);
|
||||
|
||||
// Validation override functions
|
||||
const send: BoundSendFn = useMemo(
|
||||
() => async (type, data, options) => {
|
||||
const client = clientRef.current;
|
||||
|
|
@ -259,6 +259,7 @@ export function Provider(props: {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// No Iota check
|
||||
useEffect(() => {
|
||||
if (!connected) return;
|
||||
|
||||
|
|
@ -272,6 +273,7 @@ export function Provider(props: {
|
|||
});
|
||||
}, [connected, subscribe]);
|
||||
|
||||
// Custom Pings
|
||||
useEffect(() => {
|
||||
if (!connected || !identified) {
|
||||
return;
|
||||
|
|
@ -297,11 +299,9 @@ export function Provider(props: {
|
|||
};
|
||||
}, [connected, identified, send]);
|
||||
|
||||
// Reconnect stuff
|
||||
useEffect(() => {
|
||||
if (!mtpUrl) {
|
||||
return;
|
||||
}
|
||||
const url = mtpUrl;
|
||||
if (!mtpUrl) return;
|
||||
|
||||
let attempts = 0;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
|
@ -358,17 +358,28 @@ export function Provider(props: {
|
|||
}
|
||||
|
||||
try {
|
||||
setReadyState(READY_STATE.CONNECTING);
|
||||
setConnected(false);
|
||||
setIdentified(false);
|
||||
setIdentifying(false);
|
||||
|
||||
await MTPClient.init();
|
||||
const client = await MTPClient.create({
|
||||
url,
|
||||
url: mtpUrl ?? "",
|
||||
descriptor: "client",
|
||||
pings: true,
|
||||
logger: (event) => {
|
||||
log(2, "mtp", event.type === "state" ? "cyan" : "blue", event.type === "state" ? event.data : event.type, event);
|
||||
if (event.type === "state") {
|
||||
setReadyState(
|
||||
clientRef.current?.state ?? ConnectionState.Disconnected,
|
||||
);
|
||||
}
|
||||
|
||||
log(
|
||||
2,
|
||||
"mtp",
|
||||
event.type === "state" ? "cyan" : "blue",
|
||||
event.type === "state" ? event.data : event.type,
|
||||
event,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -378,6 +389,7 @@ export function Provider(props: {
|
|||
}
|
||||
|
||||
clientRef.current = client;
|
||||
setReadyState(client.state);
|
||||
await client.connect();
|
||||
|
||||
if (disposed) {
|
||||
|
|
@ -401,8 +413,7 @@ export function Provider(props: {
|
|||
|
||||
clearReconnectTimer();
|
||||
scheduleReconnectReset();
|
||||
setReadyState(READY_STATE.OPEN);
|
||||
setConnected(true);
|
||||
setReadyState(client.state);
|
||||
setIdentifying(true);
|
||||
setError("");
|
||||
setErrorDescription("");
|
||||
|
|
@ -425,8 +436,7 @@ export function Provider(props: {
|
|||
clientRef.current?.disconnect();
|
||||
clientRef.current = null;
|
||||
clearReconnectResetTimer();
|
||||
setReadyState(READY_STATE.CLOSED);
|
||||
setConnected(false);
|
||||
setReadyState(ConnectionState.Disconnected);
|
||||
setIdentified(false);
|
||||
setIdentifying(false);
|
||||
log(
|
||||
|
|
@ -475,16 +485,16 @@ export function Provider(props: {
|
|||
|
||||
clientRef.current?.disconnect();
|
||||
clientRef.current = null;
|
||||
setReadyState(READY_STATE.CLOSED);
|
||||
setConnected(false);
|
||||
setReadyState(ConnectionState.Disconnected);
|
||||
setIdentified(false);
|
||||
setIdentifying(false);
|
||||
};
|
||||
}, [mtpUrl, props.blockConnection]);
|
||||
|
||||
// Loading bar
|
||||
const progress = useMemo(() => {
|
||||
if (!mtpUrl) return 10;
|
||||
if (readyState === READY_STATE.CONNECTING) return 30;
|
||||
if (readyState === ConnectionState.Connecting) return 30;
|
||||
if (!connected) return 45;
|
||||
if (identifying) return 75;
|
||||
if (!identified) return 90;
|
||||
|
|
@ -493,7 +503,7 @@ export function Provider(props: {
|
|||
|
||||
const loadingTitle = useMemo(() => {
|
||||
if (!mtpUrl) return "Looking up configuration";
|
||||
if (readyState === READY_STATE.CONNECTING || !connected)
|
||||
if (readyState === ConnectionState.Connecting || !connected)
|
||||
return "Connecting to Tensamin";
|
||||
if (identifying || !identified) return "Identifying secure session";
|
||||
return "Loading";
|
||||
|
|
@ -501,13 +511,14 @@ export function Provider(props: {
|
|||
|
||||
const loadingDescription = useMemo(() => {
|
||||
if (!mtpUrl) return "Loading connection details";
|
||||
if (readyState === READY_STATE.CONNECTING || !connected) {
|
||||
if (readyState === ConnectionState.Connecting || !connected) {
|
||||
return "Establishing transport channel";
|
||||
}
|
||||
if (identifying || !identified) return "Waiting for authenticated session";
|
||||
return undefined;
|
||||
}, [connected, identified, identifying, readyState, mtpUrl]);
|
||||
|
||||
// Return
|
||||
if (error !== "" && errorDescription !== "") {
|
||||
return <ErrorScreen error={error} description={errorDescription} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@tensamin/ui";
|
||||
import { useLocation } from "@tanstack/react-router";
|
||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||
import { useDeeplinks } from "@tensamin/tauri/deeplinkHandler";
|
||||
import { useMTP } from "@tensamin/mtp";
|
||||
import { log, toast } from "@tensamin/shared/log";
|
||||
|
|
@ -25,28 +25,40 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
const { send } = useMTP();
|
||||
const { getSharedSecret } = useCrypto();
|
||||
const { searchStr } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [identifier, setIdentifier] = useState<string | null>(null);
|
||||
const [redirect, setRedirect] = useState<string | null>(null);
|
||||
const [challenge, setChallenge] = useState<string | null>(null);
|
||||
const [appPublicKey, setAppPublicKey] = useState<string | null>(null);
|
||||
const [allowChildern, setAllowChildern] = useState(false);
|
||||
|
||||
const { deeplinks } = useDeeplinks();
|
||||
|
||||
const authorizeApp = async () => {
|
||||
if (!identifier || !redirect || !challenge) return;
|
||||
if (!identifier || !redirect || !challenge || !appPublicKey) return;
|
||||
|
||||
try {
|
||||
// Get Data
|
||||
const user = await get(await load("user_id"));
|
||||
const {
|
||||
data: { content: appPublicKey },
|
||||
data: { content: appPublicKeyHash },
|
||||
} = await send("load_txt_record", {
|
||||
path: "tauth." + identifier,
|
||||
});
|
||||
|
||||
const verifiedAppPublicKeyHash = await sha256Hex(appPublicKey);
|
||||
if (normalizeHash(appPublicKeyHash) !== verifiedAppPublicKeyHash) {
|
||||
log(1, "tauth", "red", "App public key hash mismatch", undefined, {
|
||||
appPublicKey,
|
||||
appPublicKeyHash,
|
||||
verifiedAppPublicKeyHash,
|
||||
});
|
||||
throw new Error("App public key hash mismatch");
|
||||
}
|
||||
|
||||
// Get Shared Secret
|
||||
const sharedSecret = await getSharedSecret(
|
||||
await load("private_key"),
|
||||
|
|
@ -75,7 +87,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
finalUrl.searchParams.set("challenge", solvedChallenge);
|
||||
finalUrl.searchParams.set("originalChallenge", challenge);
|
||||
|
||||
const session = Date.now();
|
||||
const session = new Date().getTime();
|
||||
finalUrl.searchParams.set("sessionId", String(session));
|
||||
|
||||
// Save Session
|
||||
|
|
@ -92,11 +104,14 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
setIdentifier(null);
|
||||
setRedirect(null);
|
||||
setChallenge(null);
|
||||
setAppPublicKey(null);
|
||||
|
||||
toast("success", "App authorized successfully");
|
||||
return;
|
||||
} else {
|
||||
window.location.href = finalUrl.toString();
|
||||
navigate({
|
||||
to: finalUrl.toString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -108,9 +123,27 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
setIdentifier(null);
|
||||
setRedirect(null);
|
||||
setChallenge(null);
|
||||
setAppPublicKey(null);
|
||||
}
|
||||
};
|
||||
|
||||
async function sha256Hex(value: string): Promise<string> {
|
||||
const hash = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(value),
|
||||
);
|
||||
return [...new Uint8Array(hash)]
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function normalizeHash(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^sha256[:=]/, "");
|
||||
}
|
||||
|
||||
function hexToBase64(hex: string): string {
|
||||
const bytes = hex.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) ?? [];
|
||||
const binaryString = String.fromCharCode(...bytes);
|
||||
|
|
@ -122,6 +155,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
const identifier = params.get("identifier");
|
||||
const redirect = params.get("redirect");
|
||||
const challenge = params.get("challenge");
|
||||
const appPublicKey = params.get("public_key");
|
||||
if (!identifier || !redirect) {
|
||||
setAllowChildern(true);
|
||||
return;
|
||||
|
|
@ -135,10 +169,18 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!appPublicKey) {
|
||||
toast("error", "Missing app public key");
|
||||
log(1, "tauth", "red", "Missing app public key");
|
||||
setAllowChildern(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setAllowChildern(true);
|
||||
setIdentifier(identifier);
|
||||
setRedirect(redirect);
|
||||
setChallenge(hexToBase64(challenge || ""));
|
||||
setAppPublicKey(appPublicKey);
|
||||
setDialogOpen(true);
|
||||
});
|
||||
}, [searchStr, load]);
|
||||
|
|
@ -149,11 +191,13 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
const url = new URL(link);
|
||||
const identifier = url.searchParams.get("identifier");
|
||||
const redirect = url.searchParams.get("redirect");
|
||||
const appPublicKey = url.searchParams.get("public_key");
|
||||
|
||||
if (identifier && redirect) {
|
||||
if (identifier && redirect && appPublicKey) {
|
||||
setIdentifier(identifier);
|
||||
setRedirect(redirect);
|
||||
setChallenge(hexToBase64(url.searchParams.get("challenge") || ""));
|
||||
setAppPublicKey(appPublicKey);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -170,6 +214,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
setIdentifier(null);
|
||||
setRedirect(null);
|
||||
setChallenge(null);
|
||||
setAppPublicKey(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -205,6 +250,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
|||
setIdentifier(null);
|
||||
setRedirect(null);
|
||||
setChallenge(null);
|
||||
setAppPublicKey(null);
|
||||
}}
|
||||
>
|
||||
Deny
|
||||
|
|
|
|||
Loading…
Reference in a new issue