App layer, four sequential slices (a shared .xcodeproj means adding files regenerates it, so these could not run in parallel): - token UX end to end: pairing prompts for a token when a host 401s, POST /auth validates it, and 204-without-Set-Cookie is correctly read as "this server has auth disabled" rather than "authenticated". A host paired before the token was turned on recovers by re-pairing in place. Remove-host now exists and finally gives PushRegistrar.handleHostRemoved a caller. - project git panel + worktree lifecycle (T-iOS-32) + claude --resume history — the parity gap with Android and the web front end. - terminal search (T-iOS-33) and voice PTT (T-iOS-31) with an epoch guard so a session switch between dictation and confirm cannot inject into the wrong session. - theme + Dynamic Type (T-iOS-34) and web ?join= interop (T-iOS-35). RootView no longer hard-locks .preferredColorScheme(.dark). Also unpins SwiftTerm to 1.15.0 by dropping the local hasActiveSelection that collided with the upstream one, verified green from a fresh derivedDataPath. Includes the two HIGH fixes the security review found: - iOS resolved the WS token host-independently, so a token-gated host sitting next to an open one could never open a terminal and no on-screen remedy could fix it. Now one transport per host; cross-host leakage is structurally impossible since both read paths return only that host's own value. - Android reported the host's own git-credential 401 (git-ops.ts:108, "Push authentication required on the host.") as "your access token is wrong", because a blanket 401 mapping ran ahead of the per-route one. Git-write routes are now ROUTE_DEFINED and keep the server's message. And the doc sync: README/ios README no longer claim the client is unmerged on feat/ios-client, the Clients section finally lists Android, and the plan checkboxes reflect what is actually built. iOS 534 app tests + 452 package tests; Android 687 tests.
233 lines
11 KiB
YAML
233 lines
11 KiB
YAML
# XcodeGen project spec — regenerate with: cd ios && xcodegen generate
|
|
# The generated WebTerm.xcodeproj is NOT committed (see ios/.gitignore).
|
|
#
|
|
# TOOLCHAIN DEVIATION (recorded per plan §7 T-iOS-1): the plan names
|
|
# "default MainActor isolation" (SWIFT_DEFAULT_ACTOR_ISOLATION), which is
|
|
# Swift 6.2+/Xcode 26 only. On Swift 6.1 / Xcode 16.3 the closest supported
|
|
# equivalent is Swift 6 language mode + strict concurrency, with explicit
|
|
# @MainActor annotations where isolation is required.
|
|
name: WebTerm
|
|
|
|
options:
|
|
bundleIdPrefix: com.yaojia
|
|
deploymentTarget:
|
|
iOS: "17.0"
|
|
createIntermediateGroups: true
|
|
|
|
settings:
|
|
base:
|
|
SWIFT_VERSION: "6.0" # Swift 6 language mode
|
|
SWIFT_STRICT_CONCURRENCY: complete
|
|
IPHONEOS_DEPLOYMENT_TARGET: "17.0"
|
|
TARGETED_DEVICE_FAMILY: "1,2" # iPhone + iPad (T-iPad-1; adaptive layout)
|
|
CODE_SIGN_STYLE: Automatic
|
|
# Free personal Apple team (O="Yaojia Wang"). Required for ANY device build:
|
|
# without it xcodebuild stops at 'Signing for "WebTerm" requires a
|
|
# development team'. Not a secret — a Team ID is public in every signed ipa.
|
|
DEVELOPMENT_TEAM: C738Z66SRW
|
|
# Default for the opt-in push-entitlements switch (A1). Empty ⇒ the target's
|
|
# CODE_SIGN_ENTITLEMENTS = "${WEBTERM_PUSH_ENTITLEMENTS}" resolves to nothing
|
|
# and no entitlements file is signed in. Declaring the default HERE (rather
|
|
# than relying on the setting being undefined) keeps the switch strictly
|
|
# generate-time: a project-level build setting outranks the build-time
|
|
# process environment, so an exported shell var can never silently attach
|
|
# entitlements to a build generated without it.
|
|
WEBTERM_PUSH_ENTITLEMENTS: ""
|
|
|
|
packages:
|
|
WireProtocol:
|
|
path: Packages/WireProtocol
|
|
SessionCore:
|
|
path: Packages/SessionCore
|
|
HostRegistry:
|
|
path: Packages/HostRegistry
|
|
APIClient:
|
|
path: Packages/APIClient
|
|
ClientTLS:
|
|
path: Packages/ClientTLS # C-iOS · device client-cert (mTLS) leaf package
|
|
TestSupport:
|
|
path: Packages/TestSupport # test doubles — WebTermTests only, never the app target
|
|
# SwiftTerm is the ONLY third-party dependency, attached to the App target
|
|
# ONLY (plan §2) — packages must never import it.
|
|
#
|
|
# PINNED, not floating (`from:`). The generated .xcodeproj — and with it
|
|
# Package.resolved, which lives inside the bundle — is gitignored, so every
|
|
# agent/CI run re-resolves from scratch, and a floating `from:` would silently
|
|
# drift to whatever is newest. An exact pin makes every agent/CI run resolve
|
|
# the SAME source (the earlier drift 1.13.0 → 1.15.0 is what broke the App
|
|
# target); raising it is a deliberate, verified edit.
|
|
#
|
|
# History (T-iOS-33/31 root fix): v1.14.0 added `public var hasActiveSelection`
|
|
# to iOSTerminalView, which collided with a same-named property the App
|
|
# declared on its `TerminalView` subclass — and `override` cannot fix it,
|
|
# since upstream's is `public`, not `open`. The fix was to DROP the local
|
|
# property and use upstream's (identical semantics: `selection?.active ??
|
|
# false`, vs. the local `canPerformAction(copy)` which SwiftTerm answers from
|
|
# the same flag). Done in TerminalScreen.swift, so the pin now rides at
|
|
# 1.15.0, which also brings `searchMatchSummary` (a "2/14" match counter the
|
|
# find bar can adopt later — deliberately not consumed yet, so the pin stays
|
|
# revertible).
|
|
SwiftTerm:
|
|
url: https://github.com/migueldeicaza/SwiftTerm
|
|
exactVersion: 1.15.0
|
|
|
|
targets:
|
|
WebTerm:
|
|
type: application
|
|
platform: iOS
|
|
sources:
|
|
# The .entitlements file lives next to the sources but must never be
|
|
# bundled as a resource — it is signing input, not app content.
|
|
- path: App/WebTerm
|
|
excludes:
|
|
- WebTerm.entitlements
|
|
dependencies:
|
|
- package: WireProtocol
|
|
- package: SessionCore
|
|
- package: HostRegistry
|
|
- package: APIClient
|
|
- package: ClientTLS
|
|
- package: SwiftTerm
|
|
settings:
|
|
base:
|
|
PRODUCT_BUNDLE_IDENTIFIER: com.yaojia.webterm
|
|
# XcodeGen's iOS-application preset injects Apple's DEPRECATED
|
|
# pre-Xcode-12 development identity string at target level (which
|
|
# outranks anything set in the project-level settings above), so it has
|
|
# to be overridden right here. "Apple Development" is the modern unified
|
|
# identity Xcode 12+ issues — it is what `security find-identity`
|
|
# actually holds, so the old string could only ever resolve by alias.
|
|
CODE_SIGN_IDENTITY: "Apple Development"
|
|
# OPT-IN push entitlements (frozen switch name WEBTERM_PUSH_ENTITLEMENTS).
|
|
# XcodeGen substitutes ${VAR} from the environment at `xcodegen generate`
|
|
# time when the var is SET, and leaves the literal placeholder when it is
|
|
# UNSET — the placeholder then resolves against the project-level
|
|
# WEBTERM_PUSH_ENTITLEMENTS: "" default, i.e. no entitlements.
|
|
# default (free team): xcodegen generate
|
|
# push on (paid team): WEBTERM_PUSH_ENTITLEMENTS=App/WebTerm/WebTerm.entitlements xcodegen generate
|
|
CODE_SIGN_ENTITLEMENTS: ${WEBTERM_PUSH_ENTITLEMENTS}
|
|
# Target-level on purpose (T-iOS-19 finding): XcodeGen's target platform
|
|
# default overrides a project-level value. iPad adaptation (T-iPad-1)
|
|
# opens this to iPhone + iPad; the adaptive layout (LayoutPolicy /
|
|
# NavigationSplitView, T-iPad-2) makes the iPad layout first-class.
|
|
TARGETED_DEVICE_FAMILY: "1,2"
|
|
info:
|
|
path: App/WebTerm/Resources/Info.plist
|
|
properties:
|
|
CFBundleDisplayName: WebTerm
|
|
UILaunchScreen: {}
|
|
# Deep-link scheme (P1 T-iOS-22): webterminal://open?host=<id>&join=<uuid>
|
|
# Registered here by the orchestrator (project.yml coordination point);
|
|
# all parsing/validation lives in DeepLinkRouter (untrusted input).
|
|
CFBundleURLTypes:
|
|
- CFBundleURLName: com.yaojia.webterm.deeplink
|
|
CFBundleURLSchemes: [webterminal]
|
|
# iPhone: portrait + both landscapes (no upside-down — matches P0).
|
|
UISupportedInterfaceOrientations:
|
|
- UIInterfaceOrientationPortrait
|
|
- UIInterfaceOrientationLandscapeLeft
|
|
- UIInterfaceOrientationLandscapeRight
|
|
# iPad (T-iPad-1): all four orientations — iPad users hold it any way,
|
|
# and Split View / Stage Manager assume full orientation freedom.
|
|
UISupportedInterfaceOrientations~ipad:
|
|
- UIInterfaceOrientationPortrait
|
|
- UIInterfaceOrientationPortraitUpsideDown
|
|
- UIInterfaceOrientationLandscapeLeft
|
|
- UIInterfaceOrientationLandscapeRight
|
|
# ── Security-critical keys, transcribed verbatim from PLAN_IOS_CLIENT §5.2 ──
|
|
# NO NSAllowsArbitraryLoads anywhere (release OR debug): the five CIDR
|
|
# exceptions below cover LAN + hotspot + Tailscale CGNAT + simulator
|
|
# loopback, so no arbitrary-loads escape hatch is needed in P0.
|
|
# T-iOS-19 re-verifies all five CIDR blocks in the release ipa.
|
|
NSAppTransportSecurity:
|
|
NSAllowsLocalNetworking: true # only covers .local / dotless hostnames, not bare IPs
|
|
NSExceptionDomains: # bare IPs use CIDR exceptions (iOS 17+ semantics)
|
|
"192.168.0.0/16":
|
|
NSExceptionAllowsInsecureHTTPLoads: true
|
|
"10.0.0.0/8":
|
|
NSExceptionAllowsInsecureHTTPLoads: true
|
|
"172.16.0.0/12": # RFC1918 third block (iPhone hotspot 172.20.10.x / corp LAN)
|
|
NSExceptionAllowsInsecureHTTPLoads: true
|
|
"100.64.0.0/10": # Tailscale CGNAT
|
|
NSExceptionAllowsInsecureHTTPLoads: true
|
|
"127.0.0.0/8": # simulator dev-loop (CIDR, not a single-IP key — DevForums 6205)
|
|
NSExceptionAllowsInsecureHTTPLoads: true
|
|
NSLocalNetworkUsageDescription: "连接你自己电脑上的 web-terminal 服务器"
|
|
NSCameraUsageDescription: "扫描 web 终端的配对二维码"
|
|
# Voice push-to-talk (T-iOS-31, P2). Both keys are mandatory before the
|
|
# feature ships: missing either one = TCC crash the moment PTT is pressed.
|
|
# Added here by A1 because project.yml has a single owner.
|
|
NSMicrophoneUsageDescription: "按住说话,把你的口述录成终端输入"
|
|
NSSpeechRecognitionUsageDescription: "把你的口述转成文字,你确认后才送进终端"
|
|
# Silent APNs wake so a hook notification can be processed without the
|
|
# app in the foreground (T-iOS-21). A background MODE is not an Apple
|
|
# capability, so it is unconditional and safe on the free team — unlike
|
|
# the aps-environment ENTITLEMENT, which stays opt-in (see above).
|
|
UIBackgroundModes:
|
|
- remote-notification
|
|
|
|
# Unit-test bundle for App-target logic (ViewModels & pure UI components,
|
|
# W3 T-iOS-11…14). Added by the orchestrator as a coordination point
|
|
# (project.yml is T-iOS-1's Owns; same precedent as the W1 manifest wiring).
|
|
# Coverage GATE still counts only the 4 packages (plan §9) — these tests
|
|
# exist for correctness, not for the gate.
|
|
WebTermTests:
|
|
type: bundle.unit-test
|
|
platform: iOS
|
|
sources:
|
|
- App/WebTermTests
|
|
dependencies:
|
|
- target: WebTerm
|
|
- package: WireProtocol
|
|
- package: SessionCore
|
|
- package: HostRegistry
|
|
- package: APIClient
|
|
- package: ClientTLS
|
|
- package: TestSupport
|
|
settings:
|
|
base:
|
|
GENERATE_INFOPLIST_FILE: true
|
|
TARGETED_DEVICE_FAMILY: "1,2"
|
|
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/WebTerm.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WebTerm"
|
|
BUNDLE_LOADER: "$(TEST_HOST)"
|
|
|
|
# XCUITest — exactly ONE scripted happy path per plan §9 (T-iOS-18 finding):
|
|
# pair via manual entry against a live loopback server → attach → type →
|
|
# approve a held gate. Kept deliberately minimal (fragile-surface rule).
|
|
WebTermUITests:
|
|
type: bundle.ui-testing
|
|
platform: iOS
|
|
sources:
|
|
- App/WebTermUITests
|
|
dependencies:
|
|
- target: WebTerm
|
|
settings:
|
|
base:
|
|
GENERATE_INFOPLIST_FILE: true
|
|
TARGETED_DEVICE_FAMILY: "1,2"
|
|
TEST_TARGET_NAME: WebTerm
|
|
|
|
schemes:
|
|
WebTerm:
|
|
build:
|
|
targets:
|
|
WebTerm: all
|
|
WebTermTests: [test]
|
|
run:
|
|
config: Debug
|
|
test:
|
|
config: Debug
|
|
targets:
|
|
- WebTermTests
|
|
# Separate scheme so the slow UI pass never gates the fast unit loop; CI
|
|
# runs it as its own leg (see .github/workflows/ios.yml).
|
|
WebTermUITests:
|
|
build:
|
|
targets:
|
|
WebTerm: all
|
|
WebTermUITests: [test]
|
|
test:
|
|
config: Debug
|
|
targets:
|
|
- WebTermUITests
|