more refactor

This commit is contained in:
LiBr
2026-04-10 20:34:14 +08:00
parent 8d31144741
commit 72123cc922
47 changed files with 868 additions and 365 deletions

View File

@@ -11,10 +11,11 @@
},
"dependencies": {
"@lbr77/anisette-js": "0.1.3",
"@lbr77/zsign-wasm-resigner-wrapper": "^0.1.5",
"@lbr77/zsign-wasm-resigner-wrapper": "workspace:*",
"altsign.js": "^0.1.2",
"fflate": "^0.8.2",
"jszip": "^3.10.1",
"libcurl.js": "workspace:*",
"node-forge": "^1.3.3",
"webmuxd": "workspace:*"
},

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,9 +1,11 @@
import type { HttpClient } from "@lbr77/anisette-js"
import { initLibcurl, libcurl } from "./anisette-libcurl-init"
import { initLibcurl } from "./anisette-libcurl-init"
import { requireLibcurl } from "./wasm/libcurl"
export class LibcurlHttpClient implements HttpClient {
async get(url: string, headers: Record<string, string>): Promise<Uint8Array> {
await initLibcurl()
const libcurl = requireLibcurl()
const response = await libcurl.fetch(url, {
method: "GET",
@@ -21,6 +23,7 @@ export class LibcurlHttpClient implements HttpClient {
async post(url: string, body: string, headers: Record<string, string>): Promise<Uint8Array> {
await initLibcurl()
const libcurl = requireLibcurl()
const response = await libcurl.fetch(url, {
method: "POST",

View File

@@ -1,5 +1,4 @@
// @ts-ignore
import { libcurl } from "../public/anisette/libcurl_full.mjs"
import { loadLibcurl, libcurl } from "./wasm/libcurl"
let initialized = false
let initPromise: Promise<void> | null = null
@@ -13,10 +12,11 @@ export async function initLibcurl(): Promise<void> {
}
initPromise = (async () => {
const loadedLibcurl = await loadLibcurl()
const wsProto = location.protocol === "https:" ? "wss:" : "ws:"
const wsUrl = `${wsProto}//${location.host}/wisp/`
libcurl.set_websocket(wsUrl)
await libcurl.load_wasm()
loadedLibcurl.set_websocket(wsUrl)
await loadedLibcurl.load_wasm()
initialized = true
})()

View File

@@ -1,13 +1,14 @@
import { strFromU8, unzipSync } from "fflate"
import {
type AnisetteData,
type AppleAPI,
type AppID,
type Certificate,
type Device,
type Team,
import type {
AnisetteData,
AppleAPI,
AppID,
Certificate,
Device,
Team,
} from "altsign.js"
import { initLibcurl, libcurl } from "./anisette-libcurl-init"
import { initLibcurl } from "./anisette-libcurl-init"
import { requireLibcurl } from "./wasm/libcurl"
const SIGNING_IDENTITY_STORAGE_KEY = "webmuxd:signing-identities"
const PRIMARY_APP_INFO_PLIST_RE = /^Payload\/[^/]+\.app\/Info\.plist$/
@@ -129,6 +130,7 @@ async function getAppleApi(): Promise<AppleAPI> {
}
const { AppleAPI, Fetch } = await loadAltsignModule()
const appleFetch = new Fetch(initLibcurl, async (url, options) => {
const libcurl = requireLibcurl()
const response = await libcurl.fetch(url, {
method: options.method,
headers: options.headers,

View File

@@ -1,9 +0,0 @@
export function setupCounter(element: HTMLButtonElement) {
let counter = 0
const setCounter = (count: number) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}

View File

@@ -1,121 +1,35 @@
import "./style.css"
import * as webmuxdModule from "webmuxd"
import {
initAnisette,
getAnisetteData,
provisionAnisette,
type AnisetteData,
} from "./anisette-service"
import {
loginAppleDeveloperAccount,
refreshAppleDeveloperContext,
signIpaWithAppleContext,
type AppleDeveloperContext,
} from "./apple-signing"
DirectUsbMuxClient,
LOCKDOWN_PORT,
WebUsbTransport,
createHostId,
createOpenSslWasmTlsFactory,
createSystemBuid,
decodeStoredPairRecord,
encodeStoredPairRecord,
generatePairRecordWithOpenSslWasm,
installIpaViaInstProxy,
sanitizeIpaFileName,
type PairRecord,
type StoredPairRecordPayload,
} from "webmuxd"
import type { AnisetteData } from "./anisette-service"
import type { AppleDeveloperContext } from "./apple-signing"
interface WebUsbTransportInstance {
readonly isOpen: boolean
open(): Promise<void>
close(): Promise<void>
send(data: ArrayBuffer): Promise<void>
setDataHandler(handler: ((data: ArrayBuffer) => void) | null): void
setDisconnectHandler(handler: ((reason?: unknown) => void) | null): void
}
type WasmPairRecordPayload = Pick<
PairRecord,
| "hostId"
| "systemBuid"
| "hostCertificatePem"
| "hostPrivateKeyPem"
| "rootCertificatePem"
| "rootPrivateKeyPem"
| "deviceCertificatePem"
>
interface WebUsbTransportCtor {
supported(): boolean
requestAppleDevice(): Promise<WebUsbTransportInstance>
}
interface PairRecord {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
devicePublicKey: Uint8Array
escrowBag?: Uint8Array
}
interface StoredPairRecordPayload {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
devicePublicKey: string
escrowBag: string | null
}
interface StartSessionResult {
sessionId: string
enableSessionSsl: boolean
}
interface DirectUsbMuxClient {
readonly isHandshakeComplete: boolean
readonly isLockdownConnected: boolean
readonly isSessionStarted: boolean
readonly isSessionSslEnabled: boolean
readonly isTlsActive: boolean
readonly isPaired: boolean
loadPairRecord(record: PairRecord | null): void
openAndHandshake(): Promise<void>
connectLockdown(port?: number): Promise<void>
getOrFetchDeviceUdid(): Promise<string>
getOrFetchDeviceName(): Promise<string | null>
pairDevice(hostId: string, systemBuid: string): Promise<PairRecord>
startSession(hostId: string, systemBuid: string): Promise<StartSessionResult>
close(): Promise<void>
}
interface DirectUsbMuxClientCtor {
new (
transport: WebUsbTransportInstance,
options?: {
log?: (message: string) => void
onStateChange?: () => void
lockdownLabel?: string
tlsFactory?: {
ensureReady?: () => Promise<void>
createConnection(request: {
serverName: string
caCertificatePem: string
certificatePem: string
privateKeyPem: string
}): {
is_handshaking(): boolean
write_plaintext(data: Uint8Array): void
feed_tls(data: Uint8Array): void
take_tls_out(): Uint8Array
take_plain_out(): Uint8Array
free(): void
}
}
pairRecordFactory?: {
createPairRecord(request: {
devicePublicKey: Uint8Array
hostId: string
systemBuid: string
}): Promise<PairRecord>
}
},
): DirectUsbMuxClient
}
interface WasmPairRecordPayload {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
}
type AnisetteServiceModule = typeof import("./anisette-service")
type AppleSigningModule = typeof import("./apple-signing")
interface PairedDeviceInfo {
udid: string
@@ -162,8 +76,6 @@ interface StoredAccountSessionPayload {
type AppPage = "login" | "sign"
const LOCKDOWN_PORT = 62078
const HOST_ID_STORAGE_KEY = "webmuxd:host-id"
const SYSTEM_BUID_STORAGE_KEY = "webmuxd:system-buid"
const PAIR_RECORDS_STORAGE_KEY = "webmuxd:pair-records-by-udid"
@@ -177,67 +89,24 @@ const SELECTED_DEVICE_UDID_STORAGE_KEY = "webmuxd:selected-device-udid"
const LOGIN_PAGE_HASH = "#/login"
const SIGN_PAGE_HASH = "#/sign"
const webmuxdModuleValue = webmuxdModule as unknown as Record<string, unknown>
let anisetteServicePromise: Promise<AnisetteServiceModule> | null = null
let appleSigningModulePromise: Promise<AppleSigningModule> | null = null
const WebUsbTransport = resolveWebmuxdExport<WebUsbTransportCtor>(
webmuxdModuleValue,
"WebUsbTransport",
)
const WebmuxdDirectUsbMuxClient = resolveWebmuxdExport<DirectUsbMuxClientCtor>(
webmuxdModuleValue,
"DirectUsbMuxClient",
)
const webmuxdInstallIpaViaInstProxy = resolveWebmuxdExport<
(
client: DirectUsbMuxClient,
ipaData: Uint8Array,
fileName: string,
onLog?: (message: string) => void,
) => Promise<void>
>(webmuxdModuleValue, "installIpaViaInstProxy")
const webmuxdSanitizeIpaFileName = resolveWebmuxdExport<(fileName: string) => string>(
webmuxdModuleValue,
"sanitizeIpaFileName",
)
const webmuxdCreateHostId = resolveWebmuxdExport<() => string>(
webmuxdModuleValue,
"createHostId",
)
const webmuxdCreateSystemBuid = resolveWebmuxdExport<() => string>(
webmuxdModuleValue,
"createSystemBuid",
)
const webmuxdEncodeStoredPairRecord = resolveWebmuxdExport<
(record: PairRecord) => StoredPairRecordPayload
>(webmuxdModuleValue, "encodeStoredPairRecord")
const webmuxdDecodeStoredPairRecord = resolveWebmuxdExport<
(payload: StoredPairRecordPayload) => PairRecord | null
>(webmuxdModuleValue, "decodeStoredPairRecord")
const webmuxdCreateOpenSslWasmTlsFactory = resolveWebmuxdExport<
() => {
ensureReady?(): Promise<void>
createConnection(request: {
serverName: string
caCertificatePem: string
certificatePem: string
privateKeyPem: string
}): {
is_handshaking(): boolean
write_plaintext(data: Uint8Array): void
feed_tls(data: Uint8Array): void
take_tls_out(): Uint8Array
take_plain_out(): Uint8Array
free(): void
}
const loadAnisetteService = async (): Promise<AnisetteServiceModule> => {
if (!anisetteServicePromise) {
anisetteServicePromise = import("./anisette-service")
}
>(webmuxdModuleValue, "createOpenSslWasmTlsFactory")
const webmuxdGeneratePairRecordWithOpenSslWasm = resolveWebmuxdExport<
(request: {
devicePublicKey: Uint8Array
hostId: string
systemBuid: string
}) => Promise<string>
>(webmuxdModuleValue, "generatePairRecordWithOpenSslWasm")
return await anisetteServicePromise
}
const loadAppleSigningModule = async (): Promise<AppleSigningModule> => {
if (!appleSigningModulePromise) {
appleSigningModulePromise = import("./apple-signing")
}
return await appleSigningModulePromise
}
const app = document.querySelector<HTMLDivElement>("#app")
if (!app) {
@@ -586,11 +455,11 @@ const ensureClientSelected = async (): Promise<DirectUsbMuxClient> => {
}
const transport = await WebUsbTransport.requestAppleDevice()
directClient = new WebmuxdDirectUsbMuxClient(transport, {
directClient = new DirectUsbMuxClient(transport, {
log: addLog,
onStateChange: refreshUi,
lockdownLabel: "webmuxd.frontend",
tlsFactory: webmuxdCreateOpenSslWasmTlsFactory(),
tlsFactory: createOpenSslWasmTlsFactory(),
pairRecordFactory: {
createPairRecord: async (request) => {
return await createPairRecord(request.devicePublicKey, request.hostId, request.systemBuid)
@@ -681,19 +550,20 @@ const ensureAnisetteData = async (): Promise<AnisetteData> => {
return anisetteData
}
const anisette = await initAnisette()
const anisetteService = await loadAnisetteService()
const anisette = await anisetteService.initAnisette()
const alreadyProvisioned = anisette.isProvisioned
anisetteProvisioned = alreadyProvisioned
if (alreadyProvisioned) {
addLog("login: anisette already provisioned")
} else {
addLog("login: preparing anisette environment...")
await provisionAnisette()
await anisetteService.provisionAnisette()
anisetteProvisioned = true
addLog("login: anisette provisioned")
}
anisetteData = await getAnisetteData()
anisetteData = await anisetteService.getAnisetteData()
addLog(`login: anisette ready (${shortToken(anisetteData.machineID)})`)
refreshUi()
return anisetteData
@@ -701,7 +571,8 @@ const ensureAnisetteData = async (): Promise<AnisetteData> => {
const syncAnisetteProvisionedStatus = async (): Promise<void> => {
try {
const anisette = await initAnisette()
const anisetteService = await loadAnisetteService()
const anisette = await anisetteService.initAnisette()
anisetteProvisioned = anisette.isProvisioned
refreshUi()
} catch (error) {
@@ -727,7 +598,8 @@ const loginAndSignFlow = async (): Promise<void> => {
const anisette = await ensureAnisetteData()
addLog("login: authenticating Apple account...")
const context = await loginAppleDeveloperAccount({
const appleSigning = await loadAppleSigningModule()
const context = await appleSigning.loginAppleDeveloperAccount({
anisetteData: anisette,
credentials: { appleId, password },
onLog: addLog,
@@ -736,7 +608,7 @@ const loginAndSignFlow = async (): Promise<void> => {
},
})
loginContext = await refreshAppleDeveloperContext(context, addLog)
loginContext = await appleSigning.refreshAppleDeveloperContext(context, addLog)
accountContextMap.set(accountKey(loginContext.appleId, loginContext.team.identifier), loginContext)
persistAccountSummary(loginContext)
if (rememberSessionInput.checked) {
@@ -781,7 +653,8 @@ const signSelectedIpa = async (): Promise<File> => {
},
}
const refreshed = await refreshAppleDeveloperContext(loginContext, addLog)
const appleSigning = await loadAppleSigningModule()
const refreshed = await appleSigning.refreshAppleDeveloperContext(loginContext, addLog)
loginContext = refreshed
accountContextMap.set(accountKey(refreshed.appleId, refreshed.team.identifier), refreshed)
persistAccountSummary(refreshed)
@@ -790,7 +663,7 @@ const signSelectedIpa = async (): Promise<File> => {
}
addLog("sign: preparing ipa...")
const result = await signIpaWithAppleContext({
const result = await appleSigning.signIpaWithAppleContext({
ipaFile: selectedIpaFile,
context: refreshed,
deviceUdid: targetUdid,
@@ -868,8 +741,8 @@ const installFlow = async (): Promise<void> => {
addLog("install: uploading and installing...")
const bytes = new Uint8Array(await upload.arrayBuffer())
const safeName = webmuxdSanitizeIpaFileName(upload.name)
await webmuxdInstallIpaViaInstProxy(client, bytes, safeName, addLog)
const safeName = sanitizeIpaFileName(upload.name)
await installIpaViaInstProxy(client, bytes, safeName, addLog)
addLog("install: complete")
setInstallProgress(100, "complete")
} catch (error) {
@@ -1405,7 +1278,7 @@ function getOrCreateHostId(): string {
if (existing && existing.trim().length > 0) {
return existing
}
const created = webmuxdCreateHostId()
const created = createHostId()
saveText(HOST_ID_STORAGE_KEY, created)
return created
}
@@ -1415,7 +1288,7 @@ function getOrCreateSystemBuid(): string {
if (existing && existing.trim().length > 0) {
return existing
}
const created = webmuxdCreateSystemBuid()
const created = createSystemBuid()
saveText(SYSTEM_BUID_STORAGE_KEY, created)
return created
}
@@ -1446,7 +1319,7 @@ function savePairRecordForUdid(udid: string, record: PairRecord): void {
return
}
const map = readPairRecordMap()
map[normalizedUdid] = webmuxdEncodeStoredPairRecord(record)
map[normalizedUdid] = encodeStoredPairRecord(record)
writePairRecordMap(map)
}
@@ -1457,7 +1330,7 @@ function loadLegacyPairRecord(): PairRecord | null {
}
try {
const parsed = JSON.parse(text) as StoredPairRecordPayload
return webmuxdDecodeStoredPairRecord(parsed)
return decodeStoredPairRecord(parsed)
} catch {
return null
}
@@ -1473,7 +1346,7 @@ function loadPairRecordForUdid(udid: string): PairRecord | null {
const fromMap = map[normalizedUdid]
if (fromMap) {
try {
return webmuxdDecodeStoredPairRecord(fromMap)
return decodeStoredPairRecord(fromMap)
} catch {
return null
}
@@ -1492,7 +1365,7 @@ async function createPairRecord(
hostId: string,
systemBuid: string,
): Promise<PairRecord> {
const payloadText = await webmuxdGeneratePairRecordWithOpenSslWasm({
const payloadText = await generatePairRecordWithOpenSslWasm({
devicePublicKey: devicePublicKeyBytes,
hostId,
systemBuid,
@@ -1763,20 +1636,3 @@ function formatError(error: unknown): string {
}
return String(error)
}
function resolveWebmuxdExport<T>(moduleValue: Record<string, unknown>, key: string): T {
const direct = moduleValue[key]
if (direct !== undefined) {
return direct as T
}
const defaultValue = moduleValue.default
if (defaultValue && typeof defaultValue === "object") {
const fromDefault = (defaultValue as Record<string, unknown>)[key]
if (fromDefault !== undefined) {
return fromDefault as T
}
}
throw new Error(`webmuxd export ${key} is unavailable`)
}

View File

@@ -1,2 +0,0 @@
declare module "node-forge"

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

6
frontend/src/wasm/libcurl-entry.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export const libcurl: {
fetch(input: string | URL, init?: Record<string, unknown>): Promise<Response>
load_wasm(url?: string): Promise<void>
set_websocket(url: string): void
readonly ready?: boolean
}

View File

@@ -0,0 +1 @@
export { libcurl } from "libcurl.js/bundled"

View File

@@ -0,0 +1,53 @@
export interface LibcurlApi {
fetch(input: string | URL, init?: Record<string, unknown>): Promise<Response>
load_wasm(url?: string): Promise<void>
set_websocket(url: string): void
readonly ready?: boolean
}
let libcurlModulePromise: Promise<LibcurlApi> | null = null
export let libcurl: LibcurlApi
const toLibcurl = (moduleValue: unknown): LibcurlApi => {
if (!moduleValue || typeof moduleValue !== "object") {
throw new Error("libcurl module did not return an object")
}
const candidate = moduleValue as Record<string, unknown>
if (!candidate.libcurl || typeof candidate.libcurl !== "object") {
throw new Error("libcurl module is missing the libcurl export")
}
const loadedLibcurl = candidate.libcurl as Partial<LibcurlApi>
if (typeof loadedLibcurl.fetch !== "function") {
throw new Error("libcurl export is missing fetch")
}
if (typeof loadedLibcurl.load_wasm !== "function") {
throw new Error("libcurl export is missing load_wasm")
}
if (typeof loadedLibcurl.set_websocket !== "function") {
throw new Error("libcurl export is missing set_websocket")
}
return loadedLibcurl as LibcurlApi
}
export const loadLibcurl = async (): Promise<LibcurlApi> => {
if (!libcurlModulePromise) {
libcurlModulePromise = import("./libcurl-entry.js").then((moduleValue) => {
const loadedLibcurl = toLibcurl(moduleValue)
libcurl = loadedLibcurl
return loadedLibcurl
})
}
return await libcurlModulePromise
}
export const requireLibcurl = (): LibcurlApi => {
if (!libcurl) {
throw new Error("libcurl is not ready. Call initLibcurl() first.")
}
return libcurl
}

View File

@@ -0,0 +1,75 @@
let opensslWasmModule = null
let opensslWasmModulePromise = null
let opensslWasmInitPromise = null
const loadOpenSslWasmModule = async () => {
if (!opensslWasmModulePromise) {
opensslWasmModulePromise = import("../../../wasm/openssl/dist/index.mjs").then(
(moduleValue) => {
if (!moduleValue || typeof moduleValue !== "object") {
throw new Error("OpenSSL wasm module did not return an object")
}
const candidate = moduleValue
if (typeof candidate.default !== "function") {
throw new Error("OpenSSL wasm module is missing its default initializer")
}
if (typeof candidate.OpensslClient !== "function") {
throw new Error("OpenSSL wasm module is missing OpensslClient")
}
if (typeof candidate.libimobiledevice_generate_pair_record !== "function") {
throw new Error("OpenSSL wasm module is missing pair record generation")
}
opensslWasmModule = candidate
return candidate
},
)
}
return await opensslWasmModulePromise
}
const requireOpenSslWasmModule = () => {
if (!opensslWasmModule) {
throw new Error("OpenSSL wasm is not ready. Call ensureOpenSslWasmReady() first.")
}
return opensslWasmModule
}
export const ensureOpenSslWasmReady = async () => {
if (!opensslWasmInitPromise) {
opensslWasmInitPromise = loadOpenSslWasmModule().then(async (moduleValue) => {
await moduleValue.default()
})
}
await opensslWasmInitPromise
}
export const createOpenSslWasmConnection = (request) => {
const moduleValue = requireOpenSslWasmModule()
return new moduleValue.OpensslClient(
request.serverName,
request.caCertificatePem,
request.certificatePem,
request.privateKeyPem,
)
}
export const createOpenSslWasmTlsFactory = () => {
return {
ensureReady: ensureOpenSslWasmReady,
createConnection: createOpenSslWasmConnection,
}
}
export const generatePairRecordWithOpenSslWasm = async (request) => {
await ensureOpenSslWasmReady()
const moduleValue = requireOpenSslWasmModule()
return moduleValue.libimobiledevice_generate_pair_record(
new Uint8Array(request.devicePublicKey),
request.hostId,
request.systemBuid,
)
}

120
frontend/src/webmuxd-browser.d.ts vendored Normal file
View File

@@ -0,0 +1,120 @@
declare module "webmuxd" {
export interface WebUsbTransportInstance {
readonly isOpen: boolean
open(): Promise<void>
close(): Promise<void>
send(data: ArrayBuffer): Promise<void>
setDataHandler(handler: ((data: ArrayBuffer) => void) | null): void
setDisconnectHandler(handler: ((reason?: unknown) => void) | null): void
}
export interface PairRecord {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
devicePublicKey: Uint8Array
escrowBag?: Uint8Array
}
export interface StoredPairRecordPayload {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
devicePublicKey: string
escrowBag: string | null
}
export interface TlsConnection {
is_handshaking(): boolean
write_plaintext(data: Uint8Array): void
feed_tls(data: Uint8Array): void
take_tls_out(): Uint8Array
take_plain_out(): Uint8Array
free(): void
}
export interface TlsConnectionFactory {
ensureReady?(): Promise<void>
createConnection(request: {
serverName: string
caCertificatePem: string
certificatePem: string
privateKeyPem: string
}): TlsConnection
}
export class WebUsbTransport implements WebUsbTransportInstance {
constructor(device: unknown, options?: { logger?: unknown; transferSize?: number })
readonly isOpen: boolean
static supported(): boolean
static requestAppleDevice(logger?: unknown): Promise<WebUsbTransport>
open(): Promise<void>
close(): Promise<void>
send(data: ArrayBuffer): Promise<void>
setDataHandler(handler: ((data: ArrayBuffer) => void) | null): void
setDisconnectHandler(handler: ((reason?: unknown) => void) | null): void
}
export class DirectUsbMuxClient {
constructor(
transport: WebUsbTransportInstance,
options?: {
log?: (message: string) => void
onStateChange?: () => void
lockdownLabel?: string
tlsFactory?: TlsConnectionFactory
pairRecordFactory?: {
createPairRecord(request: {
devicePublicKey: Uint8Array
hostId: string
systemBuid: string
}): Promise<PairRecord>
}
},
)
readonly isHandshakeComplete: boolean
readonly isLockdownConnected: boolean
readonly isSessionStarted: boolean
readonly isSessionSslEnabled: boolean
readonly isTlsActive: boolean
readonly isPaired: boolean
loadPairRecord(record: PairRecord | null): void
openAndHandshake(): Promise<void>
connectLockdown(port?: number): Promise<void>
getOrFetchDeviceUdid(): Promise<string>
getOrFetchDeviceName(): Promise<string | null>
pairDevice(hostId: string, systemBuid: string): Promise<PairRecord>
startSession(hostId: string, systemBuid: string): Promise<{
sessionId: string
enableSessionSsl: boolean
}>
close(): Promise<void>
}
export const LOCKDOWN_PORT: number
export function installIpaViaInstProxy(
client: DirectUsbMuxClient,
ipaData: Uint8Array,
fileName: string,
onLog?: (message: string) => void,
): Promise<void>
export function sanitizeIpaFileName(fileName: string): string
export function createHostId(): string
export function createSystemBuid(): string
export function encodeStoredPairRecord(record: PairRecord): StoredPairRecordPayload
export function decodeStoredPairRecord(parsed: StoredPairRecordPayload): PairRecord | null
export function createOpenSslWasmTlsFactory(): TlsConnectionFactory
export function generatePairRecordWithOpenSslWasm(request: {
devicePublicKey: Uint8Array
hostId: string
systemBuid: string
}): Promise<string>
}

View File

@@ -0,0 +1,15 @@
export { WebUsbTransport } from "../../dependencies/webmuxd/src/core/webusb-transport.ts"
export {
DirectUsbMuxClient,
LOCKDOWN_PORT,
installIpaViaInstProxy,
sanitizeIpaFileName,
createHostId,
createSystemBuid,
encodeStoredPairRecord,
decodeStoredPairRecord,
} from "../../dependencies/webmuxd/src/core/imobiledevice-client.ts"
export {
createOpenSslWasmTlsFactory,
generatePairRecordWithOpenSslWasm,
} from "./wasm/openssl-webmuxd.js"

View File

@@ -1,4 +0,0 @@
declare module "webmuxd" {
const webmuxd: Record<string, unknown>
export default webmuxd
}

View File

@@ -18,16 +18,22 @@ export default defineConfig({
},
resolve: {
alias: {
webmuxd: resolve(repoRootDir, "dependencies/webmuxd/lib/webmuxd.js"),
webmuxd: resolve(frontendDir, "src/webmuxd-browser.js"),
},
preserveSymlinks: true,
},
optimizeDeps: {
include: ["@lbr77/anisette-js/browser"],
exclude: [
"altsign.js",
"@lbr77/anisette-js",
"@lbr77/anisette-js/browser",
"@lbr77/zsign-wasm-resigner-wrapper",
"libcurl.js",
"libcurl.js/bundled",
],
},
build: {
commonjsOptions: {
include: [/node_modules/, /\/lib\/webmuxd\.js/, /\/lib\/core\/.*\.js/],
include: [/node_modules/],
},
},
})