Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1037c81
feat(discovery): remember receivers independently of IP
Betafer Aug 30, 2026
a49ca1a
feat(sender): add safe per-receiver codec selection
Betafer Aug 20, 2026
dab7569
feat(display): render 1080p profiles at native 1x scale
polmarinm Aug 15, 2026
b3375ce
fix(display): complete 1080p profile integration
swellweb Oct 3, 2026
2c7d1dc
fix(sender): stream virtual display directly when headless
Betafer Aug 19, 2026
0e3594f
fix(sender): preserve audio in headless direct capture
Betafer Aug 19, 2026
5ddee03
feat(sender): guide users through macOS permissions
swellweb Oct 3, 2026
adbf8bf
release: prepare TargetBridge 3.6.0 candidate
swellweb Oct 3, 2026
5f38362
ci: validate pull requests targeting dev-3.6.0
swellweb Oct 3, 2026
f1253eb
fix(input): keep mouse relay off the logging hot path
swellweb Oct 1, 2026
3c061d0
fix(display): prefer HiDPI backing mode for render matching
swellweb Oct 1, 2026
f21f4a4
fix(network): declare local network and Bonjour access
swellweb Oct 1, 2026
060fb80
release: stamp TargetBridge 3.6.0 build
swellweb Oct 5, 2026
252fb88
fix(release): share build identifier across app bundles
swellweb Oct 6, 2026
82acad8
fix(sender): require persistent signing identity for updates
Betafer Sep 9, 2026
24b5d73
fix(sender): pass update identity as a literal requirement
Betafer Sep 9, 2026
246f9f7
build(sender): retain unconfigured disposable CI builds
Betafer Sep 12, 2026
39cb619
build(sender): package standalone executable for strict signing
Betafer Sep 12, 2026
10f8ccd
fix(release): preserve macOS permission identity
swellweb Oct 6, 2026
602c57e
fix(release): validate signing certificate conditions
swellweb Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/actions/import-macos-signing-certificate/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Import macOS signing certificate
description: Imports the release signing certificate and exports its persistent identity.
inputs:
certificate-base64:
required: true
certificate-password:
required: true
runs:
using: composite
steps:
- shell: bash
env:
CERTIFICATE_BASE64: ${{ inputs.certificate-base64 }}
CERTIFICATE_PASSWORD: ${{ inputs.certificate-password }}
run: |
set -euo pipefail
certificate_path="$RUNNER_TEMP/targetbridge-signing.p12"
keychain_path="$RUNNER_TEMP/targetbridge-signing.keychain-db"
keychain_password="$(uuidgen)"
printf '%s' "$CERTIFICATE_BASE64" | base64 -D > "$certificate_path"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -k "$keychain_path" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security list-keychain -d user -s "$keychain_path"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path"
identity="$(security find-identity -v -p codesigning "$keychain_path" | sed -nE 's/.* ([A-F0-9]{40}) .*/\1/p' | head -n1)"
test -n "$identity"
echo "TARGETBRIDGE_CODESIGN_IDENTITY=$identity" >> "$GITHUB_ENV"
echo "Imported TargetBridge signing identity ${identity:0:8}..."
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI Build Verification

on:
push:
branches: [ main, 3.1-maint, maint-3.5, 3.2-dev, dev-3.3.0, dev-3.4.0 ]
branches: [ main, 3.1-maint, maint-3.5, 3.2-dev, dev-3.3.0, dev-3.4.0, dev-3.6.0 ]
pull_request:
branches: [ main, 3.1-maint, maint-3.5, 3.2-dev, dev-3.3.0, dev-3.4.0 ]
branches: [ main, 3.1-maint, maint-3.5, 3.2-dev, dev-3.3.0, dev-3.4.0, dev-3.6.0 ]

jobs:
build-sender:
Expand Down
30 changes: 28 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ permissions:
jobs:
build-sender:
name: Build Sender (${{ matrix.os }} - ${{ matrix.arch }})
env:
TARGETBRIDGE_SIGNING_CONFIGURED: ${{ secrets.TARGETBRIDGE_CODESIGN_P12_BASE64 != '' }}
strategy:
matrix:
include:
Expand All @@ -29,10 +31,21 @@ jobs:
run: |
brew install xcodegen

- name: Import release signing certificate
if: ${{ env.TARGETBRIDGE_SIGNING_CONFIGURED == 'true' }}
uses: ./.github/actions/import-macos-signing-certificate
with:
certificate-base64: ${{ secrets.TARGETBRIDGE_CODESIGN_P12_BASE64 }}
certificate-password: ${{ secrets.TARGETBRIDGE_CODESIGN_P12_PASSWORD }}

- name: Build Sender App
run: |
cd TargetBridge-Sender
./scripts/build_targetbridge_sender_app.sh
TB_BUILD="$(grep -m1 'CURRENT_PROJECT_VERSION:' project.yml | sed -E 's/.*\"([^\"]+)\".*/\1/')"
test -n "$TB_BUILD"
REQUIRE_PERSISTENT_SIGNING=0
[[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && REQUIRE_PERSISTENT_SIGNING=1
TB_SENDER_BUILD="$TB_BUILD" TARGETBRIDGE_REQUIRE_PERSISTENT_SIGNING="$REQUIRE_PERSISTENT_SIGNING" ./scripts/build_targetbridge_sender_app.sh
file ../build/TargetBridge.app/Contents/MacOS/TargetBridge | grep -q "${{ matrix.arch }}"

- name: Zip App Bundle
Expand All @@ -55,6 +68,8 @@ jobs:
build-receiver:
name: Build Receiver (macOS ${{ matrix.arch }})
runs-on: ${{ matrix.os }}
env:
TARGETBRIDGE_SIGNING_CONFIGURED: ${{ secrets.TARGETBRIDGE_CODESIGN_P12_BASE64 != '' }}
strategy:
matrix:
include:
Expand All @@ -70,10 +85,21 @@ jobs:
run: |
brew install ffmpeg sdl2 sdl3 dylibbundler

- name: Import release signing certificate
if: ${{ env.TARGETBRIDGE_SIGNING_CONFIGURED == 'true' }}
uses: ./.github/actions/import-macos-signing-certificate
with:
certificate-base64: ${{ secrets.TARGETBRIDGE_CODESIGN_P12_BASE64 }}
certificate-password: ${{ secrets.TARGETBRIDGE_CODESIGN_P12_PASSWORD }}

- name: Build Receiver App
run: |
cd TargetBridge-Receiver
./scripts/build_tbreceiver_c_app.sh
TB_BUILD="$(grep -m1 'CURRENT_PROJECT_VERSION:' ../TargetBridge-Sender/project.yml | sed -E 's/.*\"([^\"]+)\".*/\1/')"
test -n "$TB_BUILD"
REQUIRE_PERSISTENT_SIGNING=0
[[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && REQUIRE_PERSISTENT_SIGNING=1
APP_BUILD="$TB_BUILD" TARGETBRIDGE_REQUIRE_PERSISTENT_SIGNING="$REQUIRE_PERSISTENT_SIGNING" ./scripts/build_tbreceiver_c_app.sh

- name: Zip App Bundle
run: |
Expand Down
32 changes: 20 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,23 @@ and improves the project. A special thank-you for this release goes to:
- [@preggocl](https://github.com/preggocl) for the physical Receiver display profile and the software decode fallback.
- [@Betafer](https://github.com/Betafer) for input-queue safety, cursor work, idle audio/display handling, watchdog protection, monitor shielding, and build improvements.

## TargetBridge 3.5.2
## TargetBridge 3.6.0

TargetBridge 3.5.2 is a focused maintenance update to the Input Dockstation.
When the Receiver is the input master, Shift and Command now remain active for
multi-file selection even when the keyboard is connected to the Receiver and
the mouse or trackpad is used locally on the Sender. This fixes Finder range
selection and non-contiguous selection without changing ordinary local input.
TargetBridge 3.6.0 improves everyday connection setup and makes the display
pipeline more adaptable without changing the stable default workflows:

It also includes the video queue, capture-health and permission-status fixes
prepared for this maintenance line. For the wider 3.5.1 maintenance work,
including Receiver display profiles, idle resource handling, and compatibility
with older Macs:
- remembers a discovered Receiver by its stable identity instead of losing the
selection when its Thunderbolt or network IP changes
- offers per-Receiver H.264, HEVC, or automatic hardware codec selection with
a safe fallback when a selected codec is unavailable
- adds `Full HD` and `Full HD 60` 1:1 stream profiles for non-Retina 1080p
receivers
- uses direct virtual-display capture in a genuinely headless mirror session,
while keeping system-audio relay active
- adds a built-in permission assistant for Screen Recording, Accessibility,
and Input Monitoring

The release also retains the 3.5 maintenance work:

- preserve keyboard and mouse release events when the Receiver input queue is under pressure
- preserve native macOS cursor shapes by default, including the screenshot crosshair; the custom overlay is reserved for the explicit large-cursor option
Expand All @@ -61,8 +66,8 @@ with older Macs:
- keep local notification banners behind the active monitor surface across display/fullscreen transitions
- package SDL runtimes safely and support explicit build/version overrides

See the [full 3.5.2 release notes](docs/releases/3.5.2.md), the
[3.5.1 release notes](docs/releases/3.5.1.md), and the
See the [full 3.6.0 release notes](docs/releases/3.6.0.md), the
[3.5.2 release notes](docs/releases/3.5.2.md), and the
[testing guide](docs/Testing.md), including the new monitor-shield lifecycle test.

It also keeps the established multi-Mac workspace features:
Expand Down Expand Up @@ -91,6 +96,8 @@ It also keeps the established multi-Mac workspace features:
- Overview hub: [docs/Features.md](docs/Features.md)
- Mirror mode and Extended Desktop: [docs/Features.md#display-modes](docs/Features.md#display-modes)
- Guided configuration check: [docs/Features.md#guided-configuration-check](docs/Features.md#guided-configuration-check)
- Stream profiles and per-Receiver codecs: [docs/Features.md#stream-profiles-and-codecs](docs/Features.md#stream-profiles-and-codecs)
- Permission assistant: [docs/Features.md#permission-assistant](docs/Features.md#permission-assistant)
- Multi-receiver layouts: [docs/Features.md#multi-receiver-workflows](docs/Features.md#multi-receiver-workflows)
- Network Link (experimental): [docs/Features.md#network-link-experimental](docs/Features.md#network-link-experimental)
- Audio Relay: [docs/Features.md#audio-relay](docs/Features.md#audio-relay)
Expand Down Expand Up @@ -151,6 +158,7 @@ If you build from source, app outputs go into `build/` folder.
- `Input Dockstation` may also require `Accessibility` and `Input Monitoring`, depending on the active role.
- Receiver may require `Accessibility` or `Input Monitoring` when it participates in input relay.
- In practice, `Input Dockstation` is a two-sided feature: one Mac captures input, the other injects it, so permissions may be needed on both Sender and Receiver.
- On the Sender, use the `Permissions` toolbar button to review live status and request the native macOS prompts. macOS still requires confirmation from the person using the Mac; TargetBridge cannot grant or overwrite these permissions automatically. Official updates keep a stable app identity so future releases can retain consent already granted to the app installed in `Applications`. See [Signing and permission continuity](docs/Sender-Signing.md).
- See [docs/Addons.md#input-dockstation](docs/Addons.md#input-dockstation) for the permission matrix.

## Quick start
Expand Down
2 changes: 1 addition & 1 deletion TargetBridge-Receiver/TBReceiverC/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

CC ?= clang
PKG ?= pkgconf
APP_VERSION ?= 3.5.2
APP_VERSION ?= 3.6.0
APP_BUILD ?= dev

MACOSX_DEPLOYMENT_TARGET ?= 11.0
Expand Down
2 changes: 1 addition & 1 deletion TargetBridge-Receiver/TBReceiverC/src/display.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
#include <string.h>

#ifndef TB_RECEIVER_VERSION
#define TB_RECEIVER_VERSION "3.5.2"
#define TB_RECEIVER_VERSION "3.6.0"
#endif

#ifndef TB_RECEIVER_BUILD
Expand Down
45 changes: 45 additions & 0 deletions TargetBridge-Receiver/TBReceiverC/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ struct app {
char net_ip_text[64];
char ethernet_ip_text[64];
char wifi_ip_text[64];
char receiver_id[64];
char display_host[128]; /* short hostname (or hostname+IP), cached at startup */
char status_text[128];
char sender_text[128];
Expand Down Expand Up @@ -574,6 +575,41 @@ static void bonjour_deinit(struct app *a) {
}
}

static int tb_receiver_load_or_create_id(char *dest, size_t size, CFStringRef app_id) {
if (!dest || size == 0) return -1;
dest[0] = '\0';
if (size < 37 || !app_id) return -1;

CFStringRef key = CFSTR("receiverID");
CFPropertyListRef stored = CFPreferencesCopyAppValue(key, app_id);
CFUUIDRef uuid = NULL;
if (stored && CFGetTypeID(stored) == CFStringGetTypeID() &&
CFStringGetLength((CFStringRef)stored) == 36) {
uuid = CFUUIDCreateFromString(kCFAllocatorDefault, (CFStringRef)stored);
}
if (stored) CFRelease(stored);

const Boolean needs_persistence = uuid == NULL;
if (!uuid) uuid = CFUUIDCreate(kCFAllocatorDefault);
if (!uuid) return -1;
CFStringRef value = CFUUIDCreateString(kCFAllocatorDefault, uuid);
CFRelease(uuid);
if (!value) return -1;

const Boolean copied = CFStringGetCString(value, dest, (CFIndex)size, kCFStringEncodingUTF8);
Boolean persisted = true;
if (copied && dest[0] != '\0' && needs_persistence) {
CFPreferencesSetAppValue(key, value, app_id);
persisted = CFPreferencesAppSynchronize(app_id);
}
CFRelease(value);
if (!copied || !persisted) {
dest[0] = '\0';
return -1;
}
return dest[0] != '\0' ? 0 : -1;
}

static void on_bonjour_register(DNSServiceRef sdRef,
DNSServiceFlags flags,
DNSServiceErrorType errorCode,
Expand All @@ -599,6 +635,9 @@ static void bonjour_update(struct app *a, uint16_t port) {
TXTRecordRef txt;
TXTRecordCreate(&txt, 0, NULL);
TXTRecordSetValue(&txt, "name", (uint8_t)strlen(a->bonjour_name), a->bonjour_name);
if (a->receiver_id[0] != '\0') {
TXTRecordSetValue(&txt, "receiverID", (uint8_t)strlen(a->receiver_id), a->receiver_id);
}
TXTRecordSetValue(&txt, "ip", (uint8_t)strlen(a->ip_text), a->ip_text);
if (a->tb_ip_text[0] != '\0') {
TXTRecordSetValue(&txt, "tbIP", (uint8_t)strlen(a->tb_ip_text), a->tb_ip_text);
Expand Down Expand Up @@ -1968,6 +2007,12 @@ int main(int argc, char **argv) {
memset(&a, 0, sizeof(a));
a.server_fd = -1;
a.client_fd = -1;
if (tb_receiver_load_or_create_id(a.receiver_id, sizeof(a.receiver_id),
CFSTR("com.targetbridge.receiver")) == 0) {
fprintf(stderr, "[identity] receiver ID ready\n");
} else {
fprintf(stderr, "[identity] unable to persist receiver ID; using Bonjour name fallback\n");
}
{
char host[96] = {0};
if (gethostname(host, sizeof(host)) != 0 || host[0] == '\0') {
Expand Down
22 changes: 16 additions & 6 deletions TargetBridge-Receiver/scripts/build_tbreceiver_c_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,25 @@ BUILD_DIR="${REPO_ROOT}/build"
APP_DIR="${BUILD_DIR}/TargetBridge Receiver.app"
BIN_NAME="TargetBridgeReceiver"
APP_NAME="TargetBridge Receiver"
APP_VERSION="${APP_VERSION:-3.5.2}"
APP_VERSION="${APP_VERSION:-3.6.0}"
STAMP="${APP_BUILD:-$(date +%Y%m%d%H%M%S)}"
ARCH="$(uname -m)"
ICONSET_DIR="$(mktemp -d)"
ICON_FILE="${ROOT}/TargetBridgeAssets/Assets.xcassets/AppIcon.appiconset/icon_1024.png"
ICNS_PATH="${APP_DIR}/Contents/Resources/TargetBridgeReceiver.icns"
SIGNING_CONFIG="${TARGETBRIDGE_SIGNING_CONFIG:-$HOME/Library/Application Support/TargetBridge/Build/receiver-signing-identity.txt}"

# Development builds remain possible without a certificate. A configured or
# required release build must never silently fall back to a new ad-hoc identity.
if [[ -n "${TARGETBRIDGE_CODESIGN_IDENTITY:-}" ||
-n "${TARGETBRIDGE_SIGNING_CONFIG:-}" || -e "$SIGNING_CONFIG" ||
"${TARGETBRIDGE_REQUIRE_PERSISTENT_SIGNING:-0}" == 1 ]]; then
"$SCRIPT_DIR/sign_targetbridge_receiver_app.sh" --check-identity
else
echo "WARNING: unconfigured disposable build; this ad-hoc app is not an identity-preserving update." >&2
export TARGETBRIDGE_CODESIGN_IDENTITY=-
export TARGETBRIDGE_ALLOW_ADHOC=1
fi

cd "$ROOT/TBReceiverC"
make clean
Expand Down Expand Up @@ -108,11 +121,8 @@ cat > "$APP_DIR/Contents/Info.plist" <<EOF
EOF

printf 'APPL????' > "$APP_DIR/Contents/PkgInfo"
# Sign each bundled dylib first, then the app
find "$APP_DIR/Contents/Frameworks" -name "*.dylib" | while read dylib; do
codesign --force --sign - "$dylib" >/dev/null 2>&1 || true
done
codesign --force --deep --sign - "$APP_DIR" >/dev/null 2>&1 || true
# Sign the complete bundle only after all runtime dependencies are present.
"$SCRIPT_DIR/sign_targetbridge_receiver_app.sh" "$APP_DIR"

# A released bundle must never depend on the Homebrew installation used to
# compile it. Check the executable and every bundled dynamic library after
Expand Down
42 changes: 42 additions & 0 deletions TargetBridge-Receiver/scripts/sign_targetbridge_receiver_app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/zsh
set -euo pipefail

fail() { print -u2 -- "Receiver signing: $*"; exit 1; }
APP="${1:-}"
[[ -n "$APP" && $# -eq 1 ]] || fail "Usage: $0 'TargetBridge Receiver.app' | --check-identity"

CONFIG="${TARGETBRIDGE_SIGNING_CONFIG:-$HOME/Library/Application Support/TargetBridge/Build/receiver-signing-identity.txt}"
IDENTITY="${TARGETBRIDGE_CODESIGN_IDENTITY:-}"
if [[ -z "$IDENTITY" && -f "$CONFIG" ]]; then
IDENTITY=$(<"$CONFIG")
fi
if [[ -z "$IDENTITY" ]]; then
fail "No persistent signing identity. Configure TARGETBRIDGE_CODESIGN_IDENTITY or $CONFIG. Refusing an implicit ad-hoc build."
fi
if [[ "$IDENTITY" == - ]]; then
[[ "${TARGETBRIDGE_ALLOW_ADHOC:-0}" == 1 ]] || fail "Ad-hoc signing requires TARGETBRIDGE_ALLOW_ADHOC=1; updates may lose privacy permissions."
print -u2 -- "WARNING: disposable ad-hoc development build; do not install over a persistently signed Receiver."
else
[[ "$IDENTITY" =~ '^[[:xdigit:]]{40}$' ]] || fail "Use the exact 40-character certificate fingerprint, not a display name"
IDENTITIES=$(/usr/bin/security find-identity -v -p codesigning)
[[ "${(U)IDENTITIES}" == *" ${(U)IDENTITY} "* ]] || fail "Configured certificate is not a valid code-signing identity in this user's Keychain."
fi
[[ "$APP" != --check-identity ]] || { print -- 'Signing identity preflight passed'; exit 0; }

PLIST="$APP/Contents/Info.plist"
[[ -f "$PLIST" && ! -L "$APP" ]] || fail "Expected a real application bundle"
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$PLIST")
[[ "$BUNDLE_ID" == com.targetbridge.receiver ]] || fail "Wrong bundle identifier: $BUNDLE_ID"

# Frameworks must carry the same identity before sealing the containing app.
while IFS= read -r dylib; do
/usr/bin/codesign --force --sign "$IDENTITY" "$dylib"
done < <(/usr/bin/find "$APP/Contents/Frameworks" -type f -name '*.dylib' -print)
/usr/bin/codesign --force --sign "$IDENTITY" "$APP"
/usr/bin/codesign --verify --deep --strict --all-architectures "$APP"
DETAILS=$(/usr/bin/codesign -dv "$APP" 2>&1)
if [[ "$IDENTITY" != - && "$DETAILS" == *"Signature=adhoc"* ]]; then
fail "A persistent signature was requested but the result is ad-hoc"
fi
/usr/bin/codesign -d -r- "$APP" 2>&1
print -- "Receiver signature verified: $APP"
Loading
Loading