mirror of
https://github.com/lbr77/SideImpactor.git
synced 2026-05-06 11:14:01 -04:00
refactor: migrate frontend to React + Tailwind, add Docker + tests
Replace the vanilla-TS innerHTML frontend with a type-checked React component tree (React 19 + Tailwind v4 + Vite). Frontend: - 14 components: Header, Stepper, LoginPage, LoginModal, SignPage, DropZone, DevicePicker, ProgressCard, SavedAccountsList, TrustModal, TwoFactorModal, Button, Field, Chip, Modal - lib/ extracts: storage (10 localStorage keys preserved), pair-record, account-session, log-parser, ids, use-log hook - flows/ encapsulate async pair/login/sign/install with dependency injection - Accounts page as main view with Add Account modal - Fullscreen progress overlay during sign/install - Account selector + device picker on Sign page - Security notice in login modal (server trust warning) - All addLog calls mirrored to console.log for devtools debugging Build: - bun run dev: submodule init + install + wasm dist + vite + wrangler - bun run setup: one-shot project bootstrap - Docker: multi-stage bun build → nginx on :3000 - build:wasm:dist copies pre-built src→dist (no Rust/Emscripten needed) - jszip/node-forge/fflate pre-bundled for CJS→ESM conversion Tests: - 163 vitest tests (happy-dom): all lib, components, App integration, WASM dist artifact checks, libcurl Apple connectivity, anisette init error handling Cleanup: - Delete yarn.lock (bun.lock canonical), expand .gitignore - Remove README.zh.md, rewrite README.md + AGENTS.md - Update libcurl.js submodule to f65d440 (CI build artifacts)
This commit is contained in:
15
frontend/src/flows/install.ts
Normal file
15
frontend/src/flows/install.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { installIpaViaInstProxy, sanitizeIpaFileName, type DirectUsbMuxClient } from 'webmuxd';
|
||||
|
||||
export interface InstallRequest {
|
||||
client: DirectUsbMuxClient;
|
||||
signedFile: File;
|
||||
log: (msg: string) => void;
|
||||
}
|
||||
|
||||
export async function installFlow(req: InstallRequest): Promise<void> {
|
||||
req.log('install: uploading and installing...');
|
||||
const bytes = new Uint8Array(await req.signedFile.arrayBuffer());
|
||||
const safeName = sanitizeIpaFileName(req.signedFile.name);
|
||||
await installIpaViaInstProxy(req.client, bytes, safeName, req.log);
|
||||
req.log('install: complete');
|
||||
}
|
||||
77
frontend/src/flows/login.ts
Normal file
77
frontend/src/flows/login.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { AnisetteData } from '../anisette-service';
|
||||
import type { AppleDeveloperContext } from '../apple-signing';
|
||||
import { shortToken } from '../lib/ids';
|
||||
|
||||
type AnisetteService = typeof import('../anisette-service');
|
||||
type AppleSigningModule = typeof import('../apple-signing');
|
||||
|
||||
let anisetteServicePromise: Promise<AnisetteService> | null = null;
|
||||
let appleSigningModulePromise: Promise<AppleSigningModule> | null = null;
|
||||
|
||||
export async function loadAnisetteService(): Promise<AnisetteService> {
|
||||
if (!anisetteServicePromise) {
|
||||
anisetteServicePromise = import('../anisette-service');
|
||||
}
|
||||
return await anisetteServicePromise;
|
||||
}
|
||||
|
||||
export async function loadAppleSigningModule(): Promise<AppleSigningModule> {
|
||||
if (!appleSigningModulePromise) {
|
||||
appleSigningModulePromise = import('../apple-signing');
|
||||
}
|
||||
return await appleSigningModulePromise;
|
||||
}
|
||||
|
||||
export interface EnsureAnisetteResult {
|
||||
anisetteData: AnisetteData;
|
||||
provisioned: boolean;
|
||||
}
|
||||
|
||||
export async function ensureAnisetteData(
|
||||
existing: AnisetteData | null,
|
||||
log: (msg: string) => void,
|
||||
): Promise<EnsureAnisetteResult> {
|
||||
if (existing) {
|
||||
return { anisetteData: existing, provisioned: true };
|
||||
}
|
||||
|
||||
const anisetteService = await loadAnisetteService();
|
||||
const anisette = await anisetteService.initAnisette();
|
||||
if (anisette.isProvisioned) {
|
||||
log('login: anisette already provisioned');
|
||||
} else {
|
||||
log('login: preparing anisette environment...');
|
||||
await anisetteService.provisionAnisette();
|
||||
log('login: anisette provisioned');
|
||||
}
|
||||
|
||||
const anisetteData = await anisetteService.getAnisetteData();
|
||||
log(`login: anisette ready (${shortToken(anisetteData.machineID)})`);
|
||||
return { anisetteData, provisioned: true };
|
||||
}
|
||||
|
||||
export async function checkAnisetteProvisioned(): Promise<boolean> {
|
||||
const anisetteService = await loadAnisetteService();
|
||||
const anisette = await anisetteService.initAnisette();
|
||||
return anisette.isProvisioned;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
appleId: string;
|
||||
password: string;
|
||||
anisetteData: AnisetteData;
|
||||
log: (msg: string) => void;
|
||||
onTwoFactorRequired: (submit: (code: string) => void) => void;
|
||||
}
|
||||
|
||||
export async function loginAccount(req: LoginRequest): Promise<AppleDeveloperContext> {
|
||||
const appleSigning = await loadAppleSigningModule();
|
||||
req.log('login: authenticating Apple account...');
|
||||
const context = await appleSigning.loginAppleDeveloperAccount({
|
||||
anisetteData: req.anisetteData,
|
||||
credentials: { appleId: req.appleId, password: req.password },
|
||||
onLog: req.log,
|
||||
onTwoFactorRequired: req.onTwoFactorRequired,
|
||||
});
|
||||
return await appleSigning.refreshAppleDeveloperContext(context, req.log);
|
||||
}
|
||||
99
frontend/src/flows/pair.ts
Normal file
99
frontend/src/flows/pair.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { DirectUsbMuxClient, LOCKDOWN_PORT, WebUsbTransport, createOpenSslWasmTlsFactory } from 'webmuxd';
|
||||
import {
|
||||
createPairRecord,
|
||||
getOrCreateHostId,
|
||||
getOrCreateSystemBuid,
|
||||
loadPairRecordForUdid,
|
||||
savePairRecordForUdid,
|
||||
} from '../lib/pair-record';
|
||||
import { HOST_ID_STORAGE_KEY, SYSTEM_BUID_STORAGE_KEY, saveText } from '../lib/storage';
|
||||
|
||||
export interface PairedDeviceInfo {
|
||||
udid: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
export interface PairContext {
|
||||
log: (message: string) => void;
|
||||
clientRef: { current: DirectUsbMuxClient | null };
|
||||
onStateChange: () => void;
|
||||
onTrustPending: () => void;
|
||||
}
|
||||
|
||||
export function isPairingDialogPendingError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.includes('PairingDialogResponsePending');
|
||||
}
|
||||
|
||||
export async function ensureClientSelected(ctx: PairContext): Promise<DirectUsbMuxClient> {
|
||||
if (ctx.clientRef.current) return ctx.clientRef.current;
|
||||
|
||||
const transport = await WebUsbTransport.requestAppleDevice();
|
||||
const client = new DirectUsbMuxClient(transport, {
|
||||
log: ctx.log,
|
||||
onStateChange: ctx.onStateChange,
|
||||
lockdownLabel: 'webmuxd.frontend',
|
||||
tlsFactory: createOpenSslWasmTlsFactory(),
|
||||
pairRecordFactory: {
|
||||
createPairRecord: async (request) => {
|
||||
return await createPairRecord(request.devicePublicKey, request.hostId, request.systemBuid);
|
||||
},
|
||||
},
|
||||
});
|
||||
ctx.clientRef.current = client;
|
||||
|
||||
ctx.log('device selected from browser popup');
|
||||
ctx.onStateChange();
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function pairDeviceFlow(ctx: PairContext): Promise<PairedDeviceInfo> {
|
||||
const client = await ensureClientSelected(ctx);
|
||||
|
||||
if (!client.isHandshakeComplete) {
|
||||
ctx.log('pair: opening mux handshake...');
|
||||
await client.openAndHandshake();
|
||||
}
|
||||
if (!client.isLockdownConnected) {
|
||||
ctx.log('pair: connecting lockdownd...');
|
||||
await client.connectLockdown(LOCKDOWN_PORT);
|
||||
}
|
||||
|
||||
const udid = await client.getOrFetchDeviceUdid();
|
||||
const name = await client.getOrFetchDeviceName();
|
||||
|
||||
let hostId = getOrCreateHostId();
|
||||
let systemBuid = getOrCreateSystemBuid();
|
||||
|
||||
const storedPair = loadPairRecordForUdid(udid);
|
||||
if (storedPair && !client.isPaired) {
|
||||
client.loadPairRecord(storedPair);
|
||||
hostId = storedPair.hostId;
|
||||
systemBuid = storedPair.systemBuid;
|
||||
saveText(HOST_ID_STORAGE_KEY, hostId);
|
||||
saveText(SYSTEM_BUID_STORAGE_KEY, systemBuid);
|
||||
ctx.log(`pair: loaded local pair record for ${udid}`);
|
||||
}
|
||||
|
||||
if (!client.isPaired) {
|
||||
ctx.log('pair: creating pair record...');
|
||||
try {
|
||||
const pairResult = await client.pairDevice(hostId, systemBuid);
|
||||
savePairRecordForUdid(udid, pairResult);
|
||||
ctx.log('pair: success');
|
||||
} catch (error) {
|
||||
if (isPairingDialogPendingError(error)) {
|
||||
ctx.onTrustPending();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!client.isSessionStarted) {
|
||||
const session = await client.startSession(hostId, systemBuid);
|
||||
ctx.log(`pair: session ready, ssl=${String(session.enableSessionSsl)}`);
|
||||
}
|
||||
|
||||
ctx.log(`pair: udid=${udid}${name ? ` (${name})` : ''}`);
|
||||
return { udid, name };
|
||||
}
|
||||
43
frontend/src/flows/sign.ts
Normal file
43
frontend/src/flows/sign.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { AnisetteData } from '../anisette-service';
|
||||
import type { AppleDeveloperContext } from '../apple-signing';
|
||||
import { loadAppleSigningModule } from './login';
|
||||
|
||||
export interface SignIpaRequest {
|
||||
ipaFile: File;
|
||||
context: AppleDeveloperContext;
|
||||
anisetteData: AnisetteData;
|
||||
deviceUdid: string;
|
||||
deviceName?: string;
|
||||
log: (msg: string) => void;
|
||||
}
|
||||
|
||||
export interface SignIpaResult {
|
||||
signedFile: File;
|
||||
context: AppleDeveloperContext;
|
||||
}
|
||||
|
||||
export async function signIpaFlow(req: SignIpaRequest): Promise<SignIpaResult> {
|
||||
const appleSigning = await loadAppleSigningModule();
|
||||
|
||||
const contextWithAnisette: AppleDeveloperContext = {
|
||||
...req.context,
|
||||
session: {
|
||||
...req.context.session,
|
||||
anisetteData: req.anisetteData,
|
||||
},
|
||||
};
|
||||
|
||||
const refreshed = await appleSigning.refreshAppleDeveloperContext(contextWithAnisette, req.log);
|
||||
|
||||
req.log('sign: preparing ipa...');
|
||||
const result = await appleSigning.signIpaWithAppleContext({
|
||||
ipaFile: req.ipaFile,
|
||||
context: refreshed,
|
||||
deviceUdid: req.deviceUdid,
|
||||
deviceName: req.deviceName,
|
||||
onLog: req.log,
|
||||
});
|
||||
req.log(`sign: done -> ${result.signedFile.name}`);
|
||||
|
||||
return { signedFile: result.signedFile, context: refreshed };
|
||||
}
|
||||
Reference in New Issue
Block a user