[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: ChiChou\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\nBefore you submit the issue, please check the FAQ section in Wiki: https://github.com/ChiChou/bagbak/wiki#faq\nThen delete this section.\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. Ubuntu]\n - nodejs: [e.g. v18.16.0]\n - frida on device version\n - iOS and jailbreak version\n - The app you are trying to work on [e.g. com.example.app, better with AppStore link]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/workflows/npm-publish.yml",
    "content": "name: Node.js Package\n\non:\n  push:\n    tags:\n      - v*\n\njobs:\n  publish-npm:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      id-token: write\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 24\n          registry-url: 'https://registry.npmjs.org'\n\n      - name: Ensure npm version for Trusted Publishing\n        run: npm install -g npm@latest\n\n      - name: Install agent dependencies\n        run: npm ci\n        working-directory: agent\n\n      - name: Build agent\n        run: npm run build\n        working-directory: agent\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Build\n        run: npm run build\n\n      - name: Pack tarball\n        run: npm pack\n\n      - name: Publish to npm\n        run: npm publish --provenance --access public\n        env:\n          NODE_AUTH_TOKEN: dummy\n\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ github.ref_name }}\n          name: Release ${{ github.ref_name }}\n          files: '*.tgz'\n"
  },
  {
    "path": ".gitignore",
    "content": "dist/\n.vscode/\n.ccls-cache/\ndump/\noutput/\n\n*.app/\n*.ipa\n\n.DS_Store\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n"
  },
  {
    "path": ".npmignore",
    "content": "!agent/dist/springboard.js\n!agent/dist/app.js\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 CodeColorist\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# bagbak\n\n[![version](https://img.shields.io/npm/v/bagbak)](https://www.npmjs.com/package/bagbak)\n[![downloads](https://img.shields.io/npm/dm/bagbak)](https://www.npmjs.com/package/bagbak)\n[![issues](https://img.shields.io/github/issues/chichou/bagbak)](https://github.com/chichou/bagbak/issues)\n[![sponsers](https://img.shields.io/github/sponsors/chichou)](https://github.com/sponsors/chichou)\n[![license](https://img.shields.io/github/license/chichou/bagbak)](LICENSE)\n\nYet another frida based App decryptor. Requires jailbroken iOS device and [frida.re](https://www.frida.re/)\n\nTested on iOS 15 (Dopamine) and iOS 16 (palera1n).\n\n*The name of this project doesn't have any meaning. I was just listening to that song while typing.*\n\n## Prerequisites\n\n**Note:** bagbak@5 requires frida@17. If your frida-server is v16, use `npm install -g bagbak@4` instead.\n\n### On device\n\n* [frida.re](https://www.frida.re/docs/ios/)\n\n### On desktop\n\n- [node.js](https://nodejs.org/). \n- `npm install -g bagbak`\n\n## Usage\n\nbagbak [bundle id or name]\n\n```\nOptions:\n  -l, --list                list apps\n  -j, --json                output as json (only works with --list)\n  -U, --usb                 connect to USB device (default)\n  -R, --remote              connect to remote frida-server\n  -D, --device <uuid>       connect to device with the given ID\n  -H, --host <host>         connect to remote frida-server on HOST\n  -d, --debug               enable debug output\n  -o, --output <output>     ipa filename or directory to dump to\n  --remove-keys <keys>      Info.plist keys to remove (comma-separated)\n  -h, --help                display help for command\n```\n\nDump modes (second argument):\n\n* `all` (default) — full IPA with all binaries decrypted\n* `main` (alias: `app`) — decrypt main app binary only\n* `extensions` (aliases: `ext`, `exts`) — decrypt extension binaries only\n* `binaries` (aliases: `bin`, `executables`) — decrypt all binaries, output as zip\n\nEnvironments variables:\n\n* `DEBUG=1` enable debug output for troubleshooting\n\nExample:\n\n* `bagbak -l` to list all apps\n* `bagbak com.google.chrome.ios` to dump app to `com.google.chrome.ios-[version].ipa`\n* `bagbak com.google.chrome.ios main` to dump only the main binary\n* `bagbak --remove-keys UISupportedDevices com.google.chrome.ios` to remove device restrictions from Info.plist\n"
  },
  {
    "path": "README.zh-CN.md",
    "content": "# bagbak\n\n[![version](https://img.shields.io/npm/v/bagbak)](https://www.npmjs.com/package/bagbak)\n[![downloads](https://img.shields.io/npm/dm/bagbak)](https://www.npmjs.com/package/bagbak)\n[![issues](https://img.shields.io/github/issues/chichou/bagbak)](https://github.com/chichou/bagbak/issues)\n[![sponsers](https://img.shields.io/github/sponsors/chichou)](https://github.com/sponsors/chichou)\n[![license](https://img.shields.io/github/license/chichou/bagbak)](LICENSE)\n\n又一个基于 Frida 的 App 解密工具。需要越狱的 iOS 设备和 [frida.re](https://www.frida.re/)\n\n已在 iOS 15 (Dopamine) 和 iOS 16 (palera1n) 上测试通过。\n\n*这个项目的名字没有任何含义，我写代码的时候正在听那首歌。*\n\n## 环境要求\n\n**注意：** bagbak@5 需要 frida@17。如果你的 frida-server 是 v16，请使用 `npm install -g bagbak@4`。\n\n### 设备端\n\n* [frida.re](https://www.frida.re/docs/ios/)\n\n### 电脑端\n\n- [node.js](https://nodejs.org/)\n- `npm install -g bagbak`\n\n## 使用方法\n\nbagbak [bundle id 或应用名称]\n\n```\nOptions:\n  -l, --list                列出所有应用\n  -j, --json                以 JSON 格式输出 (仅配合 --list 使用)\n  -U, --usb                 连接 USB 设备 (默认)\n  -R, --remote              连接远程 frida-server\n  -D, --device <uuid>       连接到指定 ID 的设备\n  -H, --host <host>         连接到指定主机的远程 frida-server\n  -d, --debug               启用调试输出\n  -o, --output <output>     ipa 文件名或输出目录\n  --remove-keys <keys>      需要移除的 Info.plist 键 (逗号分隔)\n  -h, --help                显示帮助信息\n```\n\n解密模式 (第二个参数):\n\n* `all` (默认) — 完整 IPA，解密所有二进制文件\n* `main` (别名: `app`) — 仅解密主应用二进制文件\n* `extensions` (别名: `ext`, `exts`) — 仅解密扩展二进制文件\n* `binaries` (别名: `bin`, `executables`) — 解密所有二进制文件，输出为 zip\n\n环境变量:\n\n* `DEBUG=1` 启用调试输出\n\n示例:\n\n* `bagbak -l` 列出所有应用\n* `bagbak com.google.chrome.ios` 解密应用并输出到 `com.google.chrome.ios-[version].ipa`\n* `bagbak com.google.chrome.ios main` 仅解密主应用二进制文件\n* `bagbak --remove-keys UISupportedDevices com.google.chrome.ios` 移除 Info.plist 中的设备限制\n\n<p align=\"center\">想看更多中文技术分享？欢迎关注我的公众号</p>\n<p align=\"center\"><image src=\"images/weixin.jpg\" width=\"240\" /></p>"
  },
  {
    "path": "agent/.gitignore",
    "content": "/node_modules/\n/dist/springboard.js\n/dist/app.js\n"
  },
  {
    "path": "agent/.npmignore",
    "content": "!dist/springboard.js\n!dist/app.js\n"
  },
  {
    "path": "agent/package.json",
    "content": "{\n  \"name\": \"bagbak-agent\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Frida agent written in TypeScript\",\n  \"private\": true,\n  \"main\": \"src/index.ts\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"prepare\": \"npm run build\",\n    \"build\": \"frida-compile src/springboard.ts -o dist/springboard.js -c && frida-compile src/app.ts -o dist/app.js -c\",\n    \"watch\": \"frida-compile src/springboard.ts -o dist/springboard.js -w & frida-compile src/app.ts -o dist/app.js -w\"\n  },\n  \"devDependencies\": {\n    \"@types/frida-gum\": \"^19.0.0\",\n    \"@types/node\": \"^18.14.0\",\n    \"frida-compile\": \"^19.0.5\"\n  },\n  \"dependencies\": {\n    \"frida-objc-bridge\": \"^8.0.5\",\n    \"frida-remote-stream\": \"^6.0.3\"\n  }\n}\n"
  },
  {
    "path": "agent/src/app.ts",
    "content": "import ObjC from \"frida-objc-bridge\";\nimport type { MachOTasks } from \"./shared.js\";\nimport { getApi } from \"./shared.js\";\n\nconst HIGH_WATER_MARK = 1024 * 1024;\nconst O_RDWR = 0x0002;\n\nconst EXTENSION_SYMBOLS = [\n  \"libxpc.dylib`xpc_main\",\n  \"Foundation`NSExtensionMain\",\n  \"ExtensionFoundation`EXExtensionMain\",\n];\n\nconst APP_SYMBOLS = [\"UIKitCore`UIApplicationMain\", \"AppKit`NSApplicationMain\"];\n\nfunction replaceWithRunLoop(symbols: string[]) {\n  const CFRunLoopRun =\n    Process.getModuleByName(\"CoreFoundation\").getExportByName(\"CFRunLoopRun\");\n\n  for (const symbol of symbols) {\n    const [dylib, func] = symbol.split(\"`\");\n    try {\n      const p = Process.getModuleByName(dylib).getExportByName(func);\n      console.log(\"replace function\", symbol, p);\n      Interceptor.replace(p, CFRunLoopRun);\n    } catch (e: unknown) {\n      console.log(\"skip\", symbol, (e as Error).message);\n    }\n  }\n\n  ObjC.schedule(ObjC.mainQueue, () => {\n    console.log(\"mainQueue scheduled\");\n  });\n}\n\nrpc.exports = {\n  hookAppMain() {\n    replaceWithRunLoop(APP_SYMBOLS);\n  },\n  hookExtensionMain() {\n    replaceWithRunLoop(EXTENSION_SYMBOLS);\n  },\n\n  dump(\n    remoteRoot: string,\n    tempRoot: string,\n    binaries: MachOTasks,\n    isExtension: boolean = false,\n  ) {\n    const { open, close, pwrite, exit } = getApi();\n\n    for (const [relative, info] of Object.entries(binaries)) {\n      console.log(\"decrypt\", relative);\n\n      const { offset, size } = info.encrypt;\n      const absoluteOriginal = remoteRoot + \"/\" + relative;\n      const absoluteTemp = tempRoot + \"/\" + relative;\n\n      const mod = Module.load(absoluteOriginal);\n      const fatOffset = Process.findRangeByAddress(mod.base)!.file!.offset;\n\n      console.log(\"module =>\", mod.name, mod.base, mod.size);\n      console.log(\"encrypted =>\", offset, size, \"fatOffset =>\", fatOffset);\n\n      const fd = open(Memory.allocUtf8String(absoluteTemp), O_RDWR) as number;\n      if (fd < 0) {\n        console.error(\"Failed to open\", absoluteTemp);\n        continue;\n      }\n\n      let p = mod.base.add(offset);\n      let fileOffset = offset + fatOffset;\n      let remaining = size;\n\n      while (remaining > 0) {\n        const chunk = Math.min(HIGH_WATER_MARK, remaining);\n        pwrite(fd, p, chunk, fileOffset);\n        p = p.add(chunk);\n        fileOffset += chunk;\n        remaining -= chunk;\n      }\n\n      // patch LC_ENCRYPTION_INFO_64: zero out cryptoff, cryptsize, cryptid\n      const zeros = Memory.alloc(12);\n      pwrite(fd, zeros, 12, info.offset + 8 + fatOffset);\n\n      close(fd);\n\n      send({ event: \"patch\", name: relative });\n      recv(\"ack\", () => {}).wait();\n    }\n\n    if (isExtension) {\n      ObjC.schedule(ObjC.mainQueue, () => {\n        exit(0);\n      });\n    }\n\n    return true;\n  },\n};\n"
  },
  {
    "path": "agent/src/shared.ts",
    "content": "import ObjC from \"frida-objc-bridge\";\n\nexport interface EncryptInfo {\n  offset: number;\n  size: number;\n  id: number;\n}\n\nexport interface MachOInfo {\n  type: number;\n  encrypt: EncryptInfo;\n  offset: number; // offset of LC_ENCRYPTION_INFO_64 command\n}\n\nexport interface ExtensionInfo {\n  id: string;\n  path: string;\n  exec: string;\n  abs: string;\n}\n\nexport type MachOTasks = Record<string, MachOInfo>;\n\ntype NativeAPI = {\n  open: NativeFunction<number, [NativePointerValue, number]>;\n  close: NativeFunction<number, [number]>;\n  read: NativeFunction<number, [number, NativePointerValue, number]>;\n  pwrite: NativeFunction<number, [number, NativePointerValue, number, number]>;\n  exit: NativeFunction<void, [number]>;\n};\n\nlet api: NativeAPI | null = null;\n\nexport function nsError<T>(fn: (pError: NativePointer) => T): T {\n  const pError = Memory.alloc(Process.pointerSize);\n  pError.writePointer(NULL);\n  const result = fn(pError);\n  const err = pError.readPointer();\n  if (!err.isNull()) throw new Error(new ObjC.Object(err).toString());\n  return result;\n}\n\nexport function getApi(): NativeAPI {\n  if (api) return api;\n\n  api = {\n    open: new NativeFunction(Module.getGlobalExportByName(\"open\"), \"int\", [\n      \"pointer\",\n      \"int\",\n    ]),\n    close: new NativeFunction(Module.getGlobalExportByName(\"close\"), \"int\", [\n      \"int\",\n    ]),\n    read: new NativeFunction(Module.getGlobalExportByName(\"read\"), \"long\", [\n      \"int\",\n      \"pointer\",\n      \"long\",\n    ]),\n    pwrite: new NativeFunction(Module.getGlobalExportByName(\"pwrite\"), \"long\", [\n      \"int\",\n      \"pointer\",\n      \"long\",\n      \"long\",\n    ]),\n    exit: new NativeFunction(Module.getGlobalExportByName(\"exit\"), \"void\", [\n      \"int\",\n    ]),\n  };\n\n  return api;\n}\n"
  },
  {
    "path": "agent/src/springboard.ts",
    "content": "import ObjC from \"frida-objc-bridge\";\nimport Controller, { type Packet } from \"frida-remote-stream\";\nimport type { ExtensionInfo, MachOInfo, MachOTasks } from \"./shared.js\";\nimport { getApi, nsError } from \"./shared.js\";\n\nconst MH_MAGIC_64 = 0xfeedfacf;\nconst LC_ENCRYPTION_INFO_64 = 0x2c;\nconst HEADER_SIZE_64 = 32;\nconst O_RDONLY = 0;\nconst STREAM_CHUNK = 2 * 1024 * 1024;\n\nconst EXCLUDE_DIRS = new Set([\"SC_Info\", \"_CodeSignature\"]);\nconst EXCLUDE_FILES = new Set([\n  \"iTunesMetadata.plist\",\n  \"embedded.mobileprovision\",\n]);\n\nfunction fileMgr() {\n  return ObjC.classes.NSFileManager.defaultManager();\n}\n\nfunction parseMachO(path: string): MachOInfo | null {\n  const { open, close, read } = getApi();\n  const fd = open(Memory.allocUtf8String(path), O_RDONLY) as number;\n  if (fd < 0) return null;\n\n  try {\n    const hdr = Memory.alloc(HEADER_SIZE_64);\n    if ((read(fd, hdr, HEADER_SIZE_64) as number) < HEADER_SIZE_64) return null;\n    if (hdr.readU32() !== MH_MAGIC_64) return null;\n\n    const type = hdr.add(12).readU32();\n    const ncmds = hdr.add(16).readU32();\n    const sizeOfCmds = hdr.add(20).readU32();\n\n    const cmds = Memory.alloc(sizeOfCmds);\n    if ((read(fd, cmds, sizeOfCmds) as number) < sizeOfCmds) return null;\n\n    const result: MachOInfo = {\n      type,\n      encrypt: { offset: 0, size: 0, id: 0 },\n      offset: 0,\n    };\n\n    for (let off = 0, i = 0; i < ncmds && off + 8 <= sizeOfCmds; i++) {\n      const cmd = cmds.add(off).readU32();\n      const cmdsize = cmds.add(off + 4).readU32();\n      if (cmd === LC_ENCRYPTION_INFO_64) {\n        result.encrypt = {\n          offset: cmds.add(off + 8).readU32(),\n          size: cmds.add(off + 12).readU32(),\n          id: cmds.add(off + 16).readU32(),\n        };\n        result.offset = off + HEADER_SIZE_64;\n      }\n      off += cmdsize;\n    }\n\n    return result;\n  } finally {\n    close(fd);\n  }\n}\n\nfunction scanDir(root: string, dir: string, tasks: MachOTasks): void {\n  const items = fileMgr().contentsOfDirectoryAtPath_error_(dir, NULL);\n  if (!items) return;\n\n  for (let i = 0; i < items.count(); i++) {\n    const name: string = items.objectAtIndex_(i).toString();\n    const full = dir + \"/\" + name;\n\n    const pIsDir = Memory.alloc(Process.pointerSize);\n    pIsDir.writeU8(0);\n    if (!fileMgr().fileExistsAtPath_isDirectory_(full, pIsDir)) continue;\n\n    if (pIsDir.readU8()) {\n      if (!EXCLUDE_DIRS.has(name)) scanDir(root, full, tasks);\n    } else {\n      const info = parseMachO(full);\n      if (info && info.encrypt.id !== 0) {\n        tasks[full.substring(root.length + 1)] = info;\n      }\n    }\n  }\n}\n\nfunction removeExcludedDirs(dir: string): void {\n  const items = fileMgr().contentsOfDirectoryAtPath_error_(dir, NULL);\n  if (!items) return;\n\n  for (let i = 0; i < items.count(); i++) {\n    const name: string = items.objectAtIndex_(i).toString();\n    const full = dir + \"/\" + name;\n\n    const pIsDir = Memory.alloc(Process.pointerSize);\n    pIsDir.writeU8(0);\n    if (!fileMgr().fileExistsAtPath_isDirectory_(full, pIsDir)) continue;\n    if (!pIsDir.readU8()) continue;\n\n    if (EXCLUDE_DIRS.has(name)) {\n      fileMgr().removeItemAtPath_error_(full, NULL);\n    } else {\n      removeExcludedDirs(full);\n    }\n  }\n}\n\nfunction zipDirectory(sourceDir: string, destPath: string): string {\n  const coordinator =\n    ObjC.classes.NSFileCoordinator.alloc().initWithFilePresenter_(null);\n  const folderURL = ObjC.classes.NSURL.fileURLWithPath_(sourceDir);\n\n  const block = new ObjC.Block({\n    retType: \"void\",\n    argTypes: [\"object\"],\n    implementation(newURL: ObjC.Object) {\n      nsError((e) =>\n        fileMgr().copyItemAtPath_toPath_error_(\n          newURL.path().toString(),\n          destPath,\n          e,\n        ),\n      );\n    },\n  });\n\n  const NSFileCoordinatorReadingForUploading = 1 << 3;\n\n  nsError((e) =>\n    coordinator.coordinateReadingItemAtURL_options_error_byAccessor_(\n      folderURL,\n      NSFileCoordinatorReadingForUploading,\n      e,\n      block,\n    ),\n  );\n\n  return destPath;\n}\n\nrpc.exports = {\n  prepare(bundlePath: string, bundleId: string, removeKeys: string[] = []) {\n    const tmp = Process.getTmpDir().replace(/\\/$/, \"\");\n    const bundleName = bundlePath.split(\"/\").pop()!;\n    const base = tmp + \"/.bagbak-\" + bundleName;\n    const payloadDir = base + \"/Payload\";\n    const root = payloadDir + \"/\" + bundleName;\n\n    fileMgr().removeItemAtPath_error_(base, NULL);\n\n    nsError((e) =>\n      fileMgr().createDirectoryAtPath_withIntermediateDirectories_attributes_error_(\n        payloadDir,\n        true,\n        NULL,\n        e,\n      ),\n    );\n\n    nsError((e) => fileMgr().copyItemAtPath_toPath_error_(bundlePath, root, e));\n\n    removeExcludedDirs(root);\n    for (const f of EXCLUDE_FILES) {\n      fileMgr().removeItemAtPath_error_(root + \"/\" + f, NULL);\n    }\n\n    const infoPlist = root + \"/Info.plist\";\n    const infoPlistURL = ObjC.classes.NSURL.fileURLWithPath_(infoPlist);\n    const dict =\n      ObjC.classes.NSMutableDictionary.alloc().initWithContentsOfURL_(\n        infoPlistURL,\n      );\n\n    if (dict && removeKeys.length > 0) {\n      for (const key of removeKeys) {\n        dict.removeObjectForKey_(key);\n      }\n      dict.writeToURL_atomically_(infoPlistURL, true);\n    }\n\n    const tasks: MachOTasks = {};\n    scanDir(root, root, tasks);\n\n    ObjC.classes.NSBundle.bundleWithPath_(\n      \"/System/Library/Frameworks/CoreServices.framework/\",\n    ).load();\n\n    const app =\n      ObjC.classes.LSApplicationProxy.applicationProxyForIdentifier_(bundleId);\n    if (!app) throw new Error(`app ${bundleId} not found`);\n\n    const extensions: ExtensionInfo[] = [];\n    const plugins = app.plugInKitPlugins();\n    for (let i = 0; i < plugins.count(); i++) {\n      const plugin = plugins.objectAtIndex_(i);\n      const plist = plugin.infoPlist();\n      const exec: string = plist.objectForKey_(\"CFBundleExecutable\").toString();\n      const path: string = plist.objectForKey_(\"Path\").toString();\n      extensions.push({\n        id: plugin.bundleIdentifier().toString(),\n        path,\n        exec,\n        abs: path + \"/\" + exec,\n      });\n    }\n\n    const mainBinary: string = app.bundleExecutable().toString();\n\n    return { base, root, tasks, extensions, mainBinary };\n  },\n\n  zip(base: string) {\n    return zipDirectory(base + \"/Payload\", base + \"/app.ipa\");\n  },\n\n  zipFiles(root: string, files: string[]) {\n    const base = root.replace(/\\/Payload\\/[^/]+$/, \"\");\n    const staging = base + \"/binaries\";\n    const zipDest = base + \"/binaries.zip\";\n\n    fileMgr().removeItemAtPath_error_(staging, NULL);\n    fileMgr().removeItemAtPath_error_(zipDest, NULL);\n\n    for (const relative of files) {\n      const src = root + \"/\" + relative;\n      const dst = staging + \"/\" + relative;\n      const dstDir = dst.substring(0, dst.lastIndexOf(\"/\"));\n\n      nsError((e) =>\n        fileMgr().createDirectoryAtPath_withIntermediateDirectories_attributes_error_(\n          dstDir,\n          true,\n          NULL,\n          e,\n        ),\n      );\n\n      nsError((e) => fileMgr().copyItemAtPath_toPath_error_(src, dst, e));\n    }\n\n    return zipDirectory(staging, zipDest);\n  },\n\n  stream(filePath: string) {\n    const { open, close, read } = getApi();\n\n    const attrs = fileMgr().attributesOfItemAtPath_error_(filePath, NULL);\n    const fileSize: number = attrs\n      .objectForKey_(\"NSFileSize\")\n      .unsignedLongLongValue();\n    console.log(\"stream:\", filePath, \"size:\", fileSize);\n\n    const fd = open(Memory.allocUtf8String(filePath), O_RDONLY) as number;\n    if (fd < 0) throw new Error(\"Failed to open \" + filePath);\n\n    const controller = new Controller();\n\n    // agent → host: send stanza requests\n    controller.events.on(\"send\", (packet: Packet) => {\n      const buf = packet.data as Buffer | null;\n      send(\n        packet.stanza,\n        buf\n          ? (buf.buffer.slice(\n              buf.byteOffset,\n              buf.byteOffset + buf.byteLength,\n            ) as ArrayBuffer)\n          : null,\n      );\n    });\n\n    // host → agent: receive stanza responses\n    function listen() {\n      recv(\"stream\", (message: any, data: ArrayBuffer | null) => {\n        controller.receive({ stanza: message, data: data as any });\n        listen();\n      });\n    }\n    listen();\n\n    const sink = controller.open(\"ipa\", { size: fileSize });\n\n    return new Promise<boolean>((resolve, reject) => {\n      sink.on(\"error\", reject);\n      sink.on(\"finish\", () => {\n        close(fd);\n        resolve(true);\n      });\n\n      const buf = Memory.alloc(STREAM_CHUNK);\n      let remaining = fileSize;\n\n      function writeNext() {\n        while (remaining > 0) {\n          const n = Math.min(STREAM_CHUNK, remaining);\n          const bytesRead = read(fd, buf, n) as number;\n          if (bytesRead <= 0) {\n            sink.end();\n            return;\n          }\n\n          const chunk = Buffer.from(buf.readByteArray(bytesRead)!);\n          remaining -= bytesRead;\n\n          if (remaining <= 0) {\n            sink.end(chunk);\n            return;\n          }\n\n          if (!sink.write(chunk)) {\n            sink.once(\"drain\", writeNext);\n            return;\n          }\n        }\n        sink.end();\n      }\n\n      writeNext();\n    });\n  },\n\n  cleanup(base: string) {\n    fileMgr().removeItemAtPath_error_(base, NULL);\n  },\n};\n"
  },
  {
    "path": "agent/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2022\"],\n    \"module\": \"Node16\",\n    \"strict\": true,\n    \"noEmit\": true,\n  },\n  \"include\": [\"src/**/*.ts\"],\n}\n"
  },
  {
    "path": "bin/bagbak.ts",
    "content": "#!/usr/bin/env node\n\nimport chalk from \"chalk\";\n\nimport { Command } from \"commander\";\nimport {\n  DeviceManager,\n  getDevice,\n  getRemoteDevice,\n  getUsbDevice,\n  Scope,\n} from \"frida\";\nimport type { Device } from \"frida\";\n\nimport { BagBak, type DumpMode } from \"../index.ts\";\nimport { enableDebug, version } from \"../lib/utils.ts\";\n\nconst VALID_MODES = [\"all\", \"main\", \"extensions\", \"binaries\"] as const;\n\nconst MODE_ALIASES: Record<string, DumpMode> = {\n  app: \"main\",\n  ext: \"extensions\",\n  exts: \"extensions\",\n  executables: \"binaries\",\n  bin: \"binaries\",\n};\n\ninterface Options {\n  device?: string;\n  usb?: boolean;\n  remote?: boolean;\n  host?: string;\n  debug?: boolean;\n  list?: boolean;\n  json?: boolean;\n  output?: string;\n  removeKeys?: string;\n}\n\nfunction getDeviceFromOptions(opts: Options): Promise<Device> {\n  let count = 0;\n\n  if (opts.device) count++;\n  if (opts.usb) count++;\n  if (opts.remote) count++;\n  if (opts.host) count++;\n\n  if (count === 0 || opts.usb) return getUsbDevice();\n  if (count > 1)\n    throw new Error(\n      \"Only one of --device, --usb, --remote, --host can be specified\",\n    );\n\n  if (opts.device) return getDevice(opts.device);\n  if (opts.remote) return getRemoteDevice();\n  if (opts.host) {\n    const manager = new DeviceManager();\n    return manager.addRemoteDevice(opts.host);\n  }\n\n  return getUsbDevice();\n}\n\nasync function main() {\n  const program = new Command();\n\n  program\n    .name(\"bagbak\")\n    .option(\"-l, --list\", \"list apps\")\n    .option(\"-j, --json\", \"output as json (only works with --list)\")\n\n    .option(\"-U, --usb\", \"connect to USB device (default)\")\n    .option(\"-R, --remote\", \"connect to remote frida-server\")\n    .option(\"-D, --device <uuid>\", \"connect to device with the given ID\")\n    .option(\"-H, --host <host>\", \"connect to remote frida-server on HOST\")\n\n    .option(\"-d, --debug\", \"enable debug output\")\n    .option(\"-o, --output <output>\", \"ipa filename or directory\")\n    .option(\"--remove-keys <keys>\", \"Info.plist keys to remove (comma-separated)\")\n    .argument(\"[target]\", \"bundle id or name\")\n    .argument(\"[mode]\", \"dump mode: all, main (app), extensions (ext, exts), binaries (bin, executables)\", \"all\")\n    .version(version());\n\n  program.parse(process.argv);\n\n  const opts = program.opts<Options>();\n\n  if (opts.debug) enableDebug(true);\n\n  const device = await getDeviceFromOptions(opts);\n  const info = await device.querySystemParameters();\n\n  if (\n    info.access !== \"full\" ||\n    info.os.id !== \"ios\" ||\n    info.platform !== \"darwin\" ||\n    info.arch !== \"arm64\"\n  ) {\n    console.error(\"This tool requires a jailbroken 64bit iOS device\");\n    process.exit(1);\n  }\n\n  if (opts.list) {\n    const apps = await device.enumerateApplications({ scope: Scope.Metadata });\n\n    if (opts.json) {\n      console.log(JSON.stringify(apps, null, 2));\n      return;\n    }\n\n    const verWidth = Math.max(\n      ...apps.map((app) => (app.parameters?.version as string)?.length || 0),\n    );\n    const idWidth = Math.max(...apps.map((app) => app.identifier.length));\n\n    console.log(\n      chalk.gray(\"Version\".padStart(verWidth)),\n      chalk.gray(\"Identifier\".padEnd(idWidth)),\n      chalk.gray(\"Name\"),\n    );\n\n    console.log(chalk.gray(\"\\u2500\".repeat(10 + verWidth + idWidth)));\n\n    for (const app of apps) {\n      console.log(\n        chalk.yellowBright(\n          ((app.parameters?.version as string) || \"\").padStart(verWidth),\n        ),\n        chalk.greenBright(app.identifier.padEnd(idWidth)),\n        app.name,\n      );\n    }\n\n    return;\n  }\n\n  if (program.args.length >= 1) {\n    const target = program.args[0];\n    const rawMode = program.args[1] || \"all\";\n    const mode = (MODE_ALIASES[rawMode] || rawMode) as DumpMode;\n\n    if (!VALID_MODES.includes(mode)) {\n      console.error(\n        chalk.red(`Invalid mode \"${rawMode}\". Must be one of: ${VALID_MODES.join(\", \")} (aliases: ${Object.keys(MODE_ALIASES).join(\", \")})`),\n      );\n      process.exit(1);\n    }\n\n    const apps = await device.enumerateApplications({ scope: Scope.Metadata });\n    const app = apps.find(\n      (app) => app.name === target || app.identifier === target,\n    );\n    if (!app) throw new Error(`Unable to find app ${target}`);\n\n    const job = new BagBak(device, app);\n\n    job\n      .on(\"status\", (msg: string) => {\n        console.log(chalk.greenBright(\"[info]\"), msg);\n      })\n      .on(\"patch\", (name: string) => {\n        console.log(chalk.redBright(\"[decrypt]\"), name);\n      })\n      .on(\"streaming\", (totalSize: number) => {\n        console.log(\n          chalk.greenBright(\"[info]\"),\n          `Streaming ${(totalSize / 1024 / 1024).toFixed(1)} MB from device...`,\n        );\n      })\n      .on(\"progress\", (transferred: number, totalSize: number) => {\n        const percent = Math.min(transferred / totalSize, 1);\n        const barWidth = 30;\n        const filled = Math.round(barWidth * percent);\n        const bar =\n          chalk.greenBright(\"\\u2588\".repeat(filled)) +\n          chalk.gray(\"\\u2591\".repeat(barWidth - filled));\n        const mb = (transferred / 1024 / 1024).toFixed(1);\n        const totalMb = (totalSize / 1024 / 1024).toFixed(1);\n        const pct = (percent * 100).toFixed(0).padStart(3);\n        process.stdout.write(\n          `\\r  ${bar} ${pct}% ${mb}/${totalMb} MB`,\n        );\n        if (transferred >= totalSize) {\n          process.stdout.write(\"\\n\");\n        }\n      });\n\n    const removeKeys = opts.removeKeys\n      ? opts.removeKeys.split(\",\").map((k) => k.trim())\n      : [];\n\n    const saved = await job.pack(opts.output, mode, removeKeys);\n    console.log(`Saved to ${chalk.yellow(saved)}`);\n    return;\n  }\n\n  program.help();\n}\n\nmain();\n"
  },
  {
    "path": "index.ts",
    "content": "import { EventEmitter } from \"events\";\nimport { createWriteStream, type PathLike } from \"fs\";\nimport { resolve } from \"path\";\nimport { Transform } from \"stream\";\nimport { pipeline } from \"stream/promises\";\n\nimport chalk from \"chalk\";\nimport type { Application, Device, Script } from \"frida\";\nimport Controller from \"frida-remote-stream\";\n\nimport { debug, directoryExists, readFromPackage, sleep } from \"./lib/utils.ts\";\n\nconst MH_EXECUTE = 0x2;\nconst MAX_DECRYPT_RETRIES = 3;\n\nexport type DumpMode = \"all\" | \"main\" | \"extensions\" | \"binaries\";\n\ninterface Extension {\n  id: string;\n  path: string;\n  abs: string;\n}\n\ninterface BinaryInfo {\n  type: number;\n  [key: string]: unknown;\n}\n\ninterface PrepareResult {\n  base: string;\n  root: string;\n  tasks: Record<string, BinaryInfo>;\n  extensions: Extension[];\n  mainBinary: string;\n}\n\nexport class BagBak extends EventEmitter {\n  #device: Device;\n  #app: Application;\n\n  constructor(device: Device, app: Application) {\n    super();\n    this.#app = app;\n    this.#device = device;\n  }\n\n  get bundle() {\n    return this.#app.identifier;\n  }\n\n  get remote() {\n    return this.#app.parameters.path as string;\n  }\n\n  async #attach() {\n    const session = await this.#device.attach(\"SpringBoard\");\n    const code = await readFromPackage(\"agent\", \"dist\", \"springboard.js\");\n    const script = await session.createScript(code.toString());\n    script.logHandler = (level, text) =>\n      console.log(\"[springboard]\", level, text);\n    await script.load();\n    return { session, script };\n  }\n\n  async #decrypt(\n    pid: number,\n    remoteRoot: string,\n    root: string,\n    binaries: Record<string, BinaryInfo>,\n    isExtension: boolean,\n  ) {\n    const session = await this.#device.attach(pid);\n    const code = await readFromPackage(\"agent\", \"dist\", \"app.js\");\n    const script = await session.createScript(code.toString());\n\n    script.logHandler = (level, text) => debug(\"[app]\", level, text);\n    script.message.connect((message) => {\n      if (message.type === \"send\" && message.payload?.event === \"patch\") {\n        this.emit(\"patch\", message.payload.name);\n        script.post({ type: \"ack\" });\n      }\n    });\n\n    await script.load();\n\n    if (isExtension) {\n      await script.exports.hookExtensionMain();\n    } else {\n      await script.exports.hookAppMain();\n    }\n\n    await this.#device.resume(pid);\n    const result = await script.exports.dump(\n      remoteRoot,\n      root,\n      binaries,\n      isExtension,\n    );\n    debug(\"dump result =>\", result);\n\n    await script.unload();\n    await session.detach();\n  }\n\n  async #decryptWithRetry(\n    label: string,\n    spawnTarget: string | string[],\n    remoteRoot: string,\n    root: string,\n    binaries: Record<string, BinaryInfo>,\n    isExtension: boolean,\n  ): Promise<void> {\n    let lastError: unknown;\n    for (let attempt = 1; attempt <= MAX_DECRYPT_RETRIES; attempt++) {\n      const pid = await this.#device.spawn(spawnTarget, {\n        env: { DISABLE_TWEAKS: \"1\" },\n      });\n      debug(\"spawned\", label, \"pid =>\", pid);\n      try {\n        await this.#decrypt(pid, remoteRoot, root, binaries, isExtension);\n        return;\n      } catch (e) {\n        lastError = e;\n        if (attempt < MAX_DECRYPT_RETRIES) {\n          this.emit(\n            \"status\",\n            `Retry ${attempt}/${MAX_DECRYPT_RETRIES} for ${label}...`,\n          );\n          await sleep(1000);\n        }\n      } finally {\n        await this.#device.kill(pid).catch(() => {});\n      }\n    }\n    throw lastError;\n  }\n\n  async #pull(coordScript: Script, zipPath: string, destPath: string) {\n    const controller = new Controller();\n\n    const done = new Promise<void>((resolve, reject) => {\n      controller.events.on(\"stream\", (source: any) => {\n        const totalSize: number = source.details.size;\n        this.emit(\"streaming\", totalSize);\n\n        let transferred = 0;\n        const progress = new Transform({\n          transform(chunk, _encoding, callback) {\n            transferred += chunk.length;\n            this.push(chunk);\n            callback();\n          },\n        });\n\n        const interval = setInterval(() => {\n          this.emit(\"progress\", transferred, totalSize);\n        }, 200);\n\n        pipeline(source, progress, createWriteStream(destPath)).then(\n          () => {\n            clearInterval(interval);\n            this.emit(\"progress\", totalSize, totalSize);\n            resolve();\n          },\n          (err) => {\n            clearInterval(interval);\n            reject(err);\n          },\n        );\n      });\n    });\n\n    controller.events.on(\"send\", (packet: any) => {\n      coordScript.post({ type: \"stream\", ...packet.stanza }, packet.data);\n    });\n\n    const handler = (message: any, data: any) => {\n      if (\n        message.type === \"send\" &&\n        typeof message.payload?.name === \"string\"\n      ) {\n        controller.receive({ stanza: message.payload, data });\n      }\n    };\n    coordScript.message.connect(handler);\n\n    try {\n      await coordScript.exports.stream(zipPath);\n      await done;\n    } finally {\n      coordScript.message.disconnect(handler);\n    }\n  }\n\n  async pack(suggested?: PathLike, mode: DumpMode = \"all\", removeKeys: string[] = []): Promise<string> {\n    const { session: coordSession, script: coordScript } = await this.#attach();\n\n    try {\n      this.emit(\"status\", \"Preparing app bundle...\");\n      const { base, root, tasks, extensions, mainBinary } =\n        (await coordScript.exports.prepare(\n          this.remote,\n          this.bundle,\n          removeKeys,\n        )) as PrepareResult;\n\n      const taskCount = Object.keys(tasks).length;\n      debug(\"root\", root);\n      debug(\"tasks\", taskCount, \"encrypted binaries\");\n      debug(\"extensions\", extensions.length);\n\n      if (taskCount === 0) {\n        this.emit(\"status\", \"No encrypted binaries found\");\n      }\n\n      const groupByExtensions = new Map<string, Record<string, BinaryInfo>>(\n        extensions.map((ext) => [ext.id, {}]),\n      );\n      const binariesForMain: Record<string, BinaryInfo> = {};\n\n      for (const [relative, info] of Object.entries(tasks)) {\n        const absolute = this.remote + \"/\" + relative;\n        const ext = extensions.find((ext) => absolute.startsWith(ext.path));\n\n        if (ext) {\n          debug(\"scope for\", chalk.green(relative), \"is\", chalk.gray(ext.id));\n          groupByExtensions.get(ext.id)![relative] = info;\n        } else if (\n          info.type === MH_EXECUTE &&\n          absolute !== this.remote + \"/\" + mainBinary\n        ) {\n          console.error(chalk.red(\"Executable\"), chalk.yellowBright(relative));\n          console.error(\n            chalk.red(\n              \"is not within any extension. Likely requires higher MinimumOSVersion.\",\n            ),\n          );\n          console.error(chalk.red(\"This binary will be left encrypted.\"));\n        } else {\n          debug(\"scope for\", relative, \"is\", chalk.green(\"main app\"));\n          binariesForMain[relative] = info;\n        }\n      }\n\n      const decryptMain = mode !== \"extensions\";\n      const decryptExtensions = mode !== \"main\";\n\n      if (decryptMain && Object.keys(binariesForMain).length) {\n        this.emit(\"status\", \"Decrypting main app...\");\n        await this.#decryptWithRetry(\n          \"main app\",\n          this.bundle,\n          this.remote,\n          root,\n          binariesForMain,\n          false,\n        );\n      }\n\n      if (decryptExtensions) {\n        for (const [extId, binaries] of groupByExtensions.entries()) {\n          if (Object.keys(binaries).length === 0) continue;\n\n          const ext = extensions.find((e) => e.id === extId)!;\n          this.emit(\"status\", `Decrypting extension ${extId}...`);\n          await this.#decryptWithRetry(\n            extId,\n            [ext.abs],\n            this.remote,\n            root,\n            binaries,\n            true,\n          );\n        }\n      }\n\n      const ver = (this.#app.parameters.version as string) || \"Unknown\";\n      let remotePath: string;\n      let defaultFilename: string;\n      let ext: string;\n\n      if (mode === \"all\") {\n        this.emit(\"status\", \"Packaging IPA...\");\n        remotePath = (await coordScript.exports.zip(base)) as string;\n        ext = \".ipa\";\n        defaultFilename = `${this.bundle}-${ver}.ipa`;\n      } else {\n        const files: string[] = [];\n        if (mode === \"main\" || mode === \"binaries\") {\n          files.push(...Object.keys(binariesForMain));\n        }\n        if (mode === \"extensions\" || mode === \"binaries\") {\n          for (const [extId, binaries] of groupByExtensions.entries()) {\n            files.push(...Object.keys(binaries));\n          }\n        }\n\n        const plists = new Set<string>();\n        for (const f of files) {\n          const dir = f.lastIndexOf(\"/\");\n          plists.add(dir === -1 ? \"Info.plist\" : f.substring(0, dir) + \"/Info.plist\");\n        }\n        files.push(...plists);\n\n        this.emit(\"status\", `Compressing ${files.length} files...`);\n        remotePath = (await coordScript.exports.zipFiles(\n          root,\n          files,\n        )) as string;\n        ext = \".zip\";\n        defaultFilename = `${this.bundle}-${ver}-${mode}.zip`;\n      }\n\n      debug(\"remote path:\", remotePath);\n\n      const suggestedStr = suggested?.toString();\n      const dest = suggestedStr\n        ? (await directoryExists(suggestedStr))\n          ? suggestedStr + \"/\" + defaultFilename\n          : suggestedStr\n        : defaultFilename;\n\n      if (ext && !dest.endsWith(ext))\n        throw new Error(`Invalid filename ${dest}, expected ${ext} extension`);\n\n      this.emit(\"status\", \"Downloading...\");\n      await this.#pull(coordScript, remotePath, resolve(process.cwd(), dest));\n\n      await coordScript.exports.cleanup(base);\n\n      return dest;\n    } finally {\n      await coordScript.unload().catch(() => {});\n      await coordSession.detach().catch(() => {});\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils.ts",
    "content": "import { readFile, stat } from \"fs/promises\";\nimport { type PathLike } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nimport pkg from \"../package.json\" with { type: \"json\" };\n\nexport const sleep = (ms: number): Promise<void> =>\n  new Promise((resolve) => setTimeout(resolve, ms));\n\nexport const directoryExists = (path: PathLike): Promise<boolean> =>\n  stat(path)\n    .then((info) => info.isDirectory())\n    .catch(() => false);\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst packageRoot = join(\n  __dirname,\n  process.env.TSDOWN_BUILD ? join(\"..\", \"..\") : \"..\",\n);\n\nexport function readFromPackage(...components: string[]): Promise<string> {\n  return readFile(join(packageRoot, ...components), \"utf8\");\n}\n\nexport function version(): string {\n  return pkg.version;\n}\n\nlet __debug = \"DEBUG\" in process.env;\n\nexport function debug(...args: unknown[]) {\n  if (__debug) console.log(...args);\n}\n\nexport function enableDebug(value?: boolean): boolean {\n  if (value !== undefined) __debug = value;\n  return __debug;\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"bagbak\",\n  \"version\": \"5.1.2\",\n  \"description\": \"Dump iOS app from a jailbroken device, based on frida.re\",\n  \"main\": \"dist/index.mjs\",\n  \"types\": \"dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"import\": \"./dist/index.mjs\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"prepublishOnly\": \"npm run build\",\n    \"test\": \"echo 'Not supported'\"\n  },\n  \"author\": \"CodeColorist\",\n  \"license\": \"MIT\",\n  \"bin\": {\n    \"bagbak\": \"dist/bin/bagbak.mjs\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"provenance\": true\n  },\n  \"files\": [\n    \"/dist\",\n    \"/agent/dist/app.js\",\n    \"/agent/dist/springboard.js\"\n  ],\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"type\": \"module\",\n  \"devDependencies\": {\n    \"@types/frida-gum\": \"^19.0.2\",\n    \"@types/node\": \"^25.5.0\",\n    \"tsdown\": \"^0.21.4\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"dependencies\": {\n    \"chalk\": \"^5.6.2\",\n    \"commander\": \"^14.0.3\",\n    \"frida\": \"^17.8.2\",\n    \"frida-remote-stream\": \"^6.0.3\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/ChiChou/bagbak.git\"\n  },\n  \"keywords\": [\n    \"ios\",\n    \"reverse-engineering\",\n    \"frida\",\n    \"jailbreak\"\n  ],\n  \"bugs\": {\n    \"url\": \"https://github.com/ChiChou/bagbak/issues\"\n  },\n  \"homepage\": \"https://github.com/ChiChou/bagbak#readme\"\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"nodenext\",\n    \"noEmit\": true,\n    \"rewriteRelativeImportExtensions\": true,\n  },\n}\n"
  },
  {
    "path": "tsdown.config.ts",
    "content": "import { defineConfig } from \"tsdown\";\n\nexport default defineConfig({\n  entry: [\"index.ts\", \"bin/bagbak.ts\"],\n  format: \"esm\",\n  dts: true,\n  outDir: \"dist\",\n  unbundle: true,\n  define: {\n    \"process.env.TSDOWN_BUILD\": \"'1'\",\n  },\n});\n"
  }
]