Extracted from the officer-suite monorepo into its own public repo. The app is
open source and free; the rest of the suite is not, so it could not stay in a
tree that ships an account layer.
What moved:
- apps/offscale/* -> repo root (App shell, app.json, assets)
- packages/officer-net/ -> modules/officer-net/, a `file:` dependency
- packages/core VPN closure -> src/vpn/ (screen, hook, native bridge, invite)
- packages/core shared primitives -> src/ui/ (theme, cards, tabs, backdrop, splash)
- scripts/build-{ios,android}.sh -> scripts/, cut down to this one app
What deliberately did NOT move: the platform account layer. VpnScreen used to
render core's AppSettingsScreen, which drags in ServersSection -> servers.ts,
useAuth, api.ts, shared-session, install-guard, api-keys. None of that exists
here. src/settings/SettingsScreen.tsx is a fresh replacement rendering exactly
what VpnScreen passed it — the app's own sections plus About — with no Servers
tab, no Appearance control (the app is pinned to dark) and no sign-out.
The one platform reference left in the closure was a dead `import { request }
from './api'` in officer-net.ts, unused since enrollVpn() was removed. Gone. The
app now contacts nothing but the control server you point it at, its peers, and
the enrolment sidecar named inside an invite link you tapped.
Dependencies are pinned to the exact versions the monorepo has installed, not to
ranges: this repo starts from versions already proven to build, and an unpinned
install is a supply-chain decision nobody made. react-query, react-dom,
react-native-screens, expo-secure-store, track-player, expo-audio and
expo-file-system were all declared by the old app and imported by none of this
code, so they are not here.
NOTICE carries Tailscale's BSD-3 text, names the built artifacts that contain
their compiled code, and treats the trademark question separately — including
that src/ui/TailscaleMark.tsx reproduces their logo in the attribution strip.
55 lines
3.1 KiB
Markdown
55 lines
3.1 KiB
Markdown
# officer-net Expo config plugin
|
|
|
|
Wires the OS-level capabilities the VPN needs into the app at prebuild. Enabled from `app.json`:
|
|
|
|
```js
|
|
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.
|