Files
offscale-mobile/modules/officer-net/plugin
pastilhasandClaude Opus 5 79fbfee5fb fix: build standalone — five things the monorepo was quietly providing
The extraction typechecked from day one and had never been built. It took five
attempts, each failing further along, and every cause was the same shape: something the
shared node_modules or the monorepo's layout supplied without anyone declaring it.

1. The config plugin had the monorepo's geometry written into it —
   ../../../packages/officer-net/ios/libs, correct from apps/<name>/ios and meaningless
   from a repo root. It reported a path belonging to neither repo. Now derived from the
   plugin's own location with path.relative, so it is right wherever the module sits.

2. That fix appeared to do nothing, because officer-net was a  dependency and bun
   COPIES those. Prebuild ran a week-old copy while the source was edited — a clean edit
   and a stale build, the same shadow-copy trap that cost a night in the monorepo.
   Making it a workspace turns node_modules/officer-net into a symlink.

3. @babel/plugin-transform-react-jsx was never declared by anything. It was hoisted into
   the monorepo by some other package, so babel-preset-expo with jsxImportSource found
   it by luck.

4. Same for react-native-reanimated and react-native-worklets: nativewind depends on
   react-native-css-interop, which lists reanimated as a PEER, whose babel plugin
   requires react-native-worklets/plugin. Three levels of nobody-declared-it.

5. And the self-inflicted one. Adding  for (2) silently flipped bun to its
   isolated linker — only direct dependencies at top level, everything else symlinked
   into node_modules/.bun, which Metro cannot resolve. It surfaced as 'Unable to resolve
   module react-native-css-interop/jsx-runtime', naming a package that was installed and
   in the lockfile and simply not where the resolver looks. bunfig.toml now pins both
   properties that are needed at once: workspaces for the symlink, hoisted for the
   layout.

Result: OffScale.app, 82 MB, with OffTailTunnel.appex at 20 MB carrying 129 Tailcore
symbols — the Go core is in the extension, which is the half that matters.

Not yet run on a device. It builds; that is a different claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 23:04:09 +01:00
..

officer-net Expo config plugin

Wires the OS-level capabilities the VPN needs into the app at prebuild. Enabled from app.json:

plugins: [
  ["officer-net", {
    appGroup: "group.pastilhas.officer.offscale",
    neBundleId: "dev.officer.offscale.net",
    enableNetworkExtension: true,
  }],
]

Props (all optional; defaults shown):

prop default what it controls
appGroup group.pastilhas.officer.mobile iOS App Group shared by the app and the NE
neBundleId dev.officer.mobile.net the Network Extension target's bundle id
specialUseSubtype VPN mesh tunnel justification Android FOREGROUND_SERVICE_SPECIAL_USE purpose
enableNetworkExtension false add the iOS entitlement and synthesise the NE target

enableNetworkExtension is off by default because the entitlement fails device code-signing until the Apple provisioning below exists. With it off the app signs, installs and renders; the engine is inert. The repo's own app.json turns it on.

What the plugin does

  • iOS — adds com.apple.developer.networking.networkextension (packet-tunnel-provider) to the main app, and writes OfficerNetExtensionBundleId into Info.plist. That key is load-bearing, not a breadcrumb: the Swift module reads it to fill providerBundleIdentifier, which is how iOS knows which extension to launch. A hardcoded value there means the start command is accepted, nothing matches, and the tunnel silently never starts.
  • iOS, under enableNetworkExtension — synthesises the Network Extension Xcode target (withNetworkExtensionTarget.js): a new target compiling this package's ios/PacketTunnelProvider.swift, its Info.plist (NSExtensionPointIdentifier = com.apple.networkextension.packet-tunnel), its own entitlements, NetworkExtension.framework, the vendored TailCore.xcframework, and the app's Embed App Extensions phase. Doing this by hand after every expo prebuild --clean is what this replaces.
  • Android — adds FOREGROUND_SERVICE and FOREGROUND_SERVICE_SPECIAL_USE, and registers OfficerVpnService with BIND_VPN_SERVICE, foregroundServiceType="specialUse" and the android.net.VpnService intent-filter.

Apple provisioning prerequisites (one-time, in the Apple Developer portal)

  • A separate App ID for the extension (neBundleId) with the Network Extensions capability.
  • The Network Extensions capability on the main app ID.
  • Provisioning profiles for both, with those capabilities.

xcodebuild -allowProvisioningUpdates cannot create these while Xcode has no account signed in: it falls back to the wildcard team profile and fails with "Provisioning profile 'iOS Team Provisioning Profile: *' doesn't include the Network Extensions capability". Sign in under Xcode → Settings → Accounts first. Android needs none of this.