diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 37c7ac297..ad0489f20 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -81,6 +81,9 @@ jobs: - name: Run Linter run: yarn lint + - name: Test Safari compatibility + run: yarn build:safari && yarn test:safari + - name: Build App env: BRANCH: ${{ (inputs.branch != 'stable' && 'nightly') || '' }} diff --git a/.gitignore b/.gitignore index 173641977..47795b68a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,11 +11,15 @@ node_modules dist dist-ssr dist-hosted +safari-build +test-results +safari-project/**/7TV for Safari Extension/Resources/ *.local # Editor directories and files .idea .DS_Store +**/xcuserdata/ *.suo *.ntvs* *.njsproj @@ -28,4 +32,3 @@ msg_types.json crx/* script/*.json - diff --git a/CHANGELOG-nightly.md b/CHANGELOG-nightly.md index 72ddf7500..326dbd62e 100644 --- a/CHANGELOG-nightly.md +++ b/CHANGELOG-nightly.md @@ -1,3 +1,7 @@ +## Next + +- Added a native Safari Web Extension build for Twitch, Kick and YouTube + ## 3.1.25.1000 - Fixed Kick Emote Menu position diff --git a/README.md b/README.md index 363c47b05..23d36b9a7 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,22 @@ ## Development +### Safari + +The native Safari Web Extension wrapper uses the same Twitch, Kick and YouTube +implementations as the other browser builds. On macOS with Xcode installed: + +```sh +yarn build:safari +yarn test:safari +./script/build-safari-local.sh +``` + +See [SAFARI.md](SAFARI.md) for signing and installation instructions, +[SAFARI-TESTING.md](SAFARI-TESTING.md) for the validation matrix, and +[SECURITY-SAFARI.md](SECURITY-SAFARI.md) for the security model. A locally +signed development build is not a notarized public binary. + ### Building - make deps diff --git a/SAFARI-COMPATIBILITY.md b/SAFARI-COMPATIBILITY.md new file mode 100644 index 000000000..191fdd620 --- /dev/null +++ b/SAFARI-COMPATIBILITY.md @@ -0,0 +1,71 @@ +# Safari compatibility inventory + +The Safari package now injects the unmodified upstream site implementation on +all three officially supported origins using static manifest entries. This +keeps the Safari delta small and makes upstream rebases reviewable. + +## Included upstream surfaces + +The entries below describe the upstream modules packaged by the Safari build; +they are not all separate claims of completed live Safari validation. + +### Twitch + +- 7TV, FFZ and BTTV chat emotes +- chat input and autocomplete +- emote menu +- settings +- cosmetics, paints, badges and avatars +- VOD chat +- mod logs and moderation UI +- custom commands +- player controls and stream statistics +- sidebar previews and hidden-element settings +- automatic channel-point claims + +### Kick + +- 7TV chat emotes +- chat input and autocomplete +- emote menu, including native Kick sets +- settings +- cosmetics supported by upstream + +### YouTube + +- 7TV, FFZ and BTTV emotes in live chat +- chat autocomplete + +## Intentional Safari differences + +- Extension-management compatibility scanning remains unavailable. Safari does + not expose Chromium's `management` permission, so 7TV cannot enumerate or + disable other extensions. +- Platform access is static. Safari enables Twitch, Kick and YouTube in the + signed manifest instead of asking for optional hosts at runtime. +- Extension self-update checks are advisory only. A local build is updated + by rebuilding and replacing the signed app. +- Kick authentication is not counted as a Safari gap: its module is disabled in + the upstream source itself. +- Commands that require third-party credentials, such as AudD `/song`, still + require their upstream configuration and are not enabled implicitly. + +## Validation status + +| Surface | Current evidence | +| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| Twitch core chat and 7TV emotes | Manually validated in Safari, including repeated page reloads | +| Kick core chat and 7TV emotes | Manually validated in Safari during development | +| YouTube live chat | Packaged and covered by manifest regression tests; live Safari validation is still pending | +| Settings, autocomplete, emote menu, cosmetics and moderation features | Upstream modules are packaged; exhaustive Safari feature-by-feature validation is still pending | + +Automated tests cover the Safari manifest, registration behavior, message +boundaries, worker loading, HTML sanitization, native entitlements and build +artifacts. They do not replace live tests against each supported website. + +## Update rule + +For each upstream update, compare `origin/master`, resolve the small Safari +build/permission delta, rebuild, run `verify:safari`, then run the live latency +test once on Twitch and once on Kick. Do not copy features into a separate +implementation when the upstream site module can be loaded unchanged. diff --git a/SAFARI-TESTING.md b/SAFARI-TESTING.md new file mode 100644 index 000000000..101c24eb0 --- /dev/null +++ b/SAFARI-TESTING.md @@ -0,0 +1,126 @@ +# Safari test protocol + +This protocol separates deterministic gates from tests that require a real +Safari installation. Test artifacts are written under `test-results/` and are +ignored by Git. + +## 1. Deterministic regression gate + +```sh +npx --no-install yarn@1.22.22 verify:safari +``` + +This compiles the Safari variant, runs the source/package/native regression +tests, validates the installed app when present, and audits dependencies. + +## 2. Real Safari smoke test + +First, check prerequisites without opening an automation session: + +```sh +npx --no-install yarn@1.22.22 test:safari:live:check +``` + +Safari WebDriver is disabled by default. Enable it explicitly in Safari under +Develop > Developer Settings > Allow remote automation. Then run: + +```sh +npx --no-install yarn@1.22.22 test:safari:live -- \ + --url https://www.twitch.tv/illojuan \ + --reloads 2 \ + --timeout 45 +``` + +The test only permits HTTPS Twitch or Kick URLs. It waits for the extension marker, +the single injected 7TV root, and the 7TV menu button. It opens the 7TV emote +menu and requires at least one fully loaded image from a `*.7tv.app` host. It +repeats those assertions after every reload and saves screenshots plus JSON. +It also records navigation-to-injection, menu readiness, first real 7TV image, +and p50/p95 timings for observed 7TV resources. It does not log in, send a chat +message, or modify site data. + +The JSON also includes `pipelineMarks` for worker startup, channel lookup, the +7TV/FFZ/BTTV set arrivals, and the moment all provider requests settle. This is +enough to distinguish an API/set delay from an image-CDN delay before changing +the cache or request ordering. + +Stress the reload lifecycle with: + +```sh +npx --no-install yarn@1.22.22 test:safari:stress +``` + +## 3. A/B resource benchmark + +Use the same channel, video quality, window size, brightness, power source and +duration for both runs. Close unrelated Safari windows. A 30-minute run is a +useful first measurement; a two-hour run is better for memory growth. + +With 7TV disabled in Safari: + +```sh +npx --no-install yarn@1.22.22 benchmark:safari -- sample \ + --label disabled \ + --duration 1800 \ + --interval 5 \ + --video-quality 1080p60 \ + --brightness 50 \ + --output test-results/safari-benchmark/disabled.json +``` + +Repeat with 7TV enabled and otherwise identical conditions: + +```sh +npx --no-install yarn@1.22.22 benchmark:safari -- sample \ + --label enabled \ + --duration 1800 \ + --interval 5 \ + --video-quality 1080p60 \ + --brightness 50 \ + --output test-results/safari-benchmark/enabled.json +``` + +Compare the runs: + +```sh +npx --no-install yarn@1.22.22 benchmark:safari -- compare \ + --disabled test-results/safari-benchmark/disabled.json \ + --enabled test-results/safari-benchmark/enabled.json +``` + +The comparison is informational until evidence-based limits are supplied. A +gate can be introduced later with `--max-cpu-mean-delta`, +`--max-rss-mean-delta`, and `--max-rss-slope-delta`. Do not invent thresholds +before collecting a baseline on the target Mac. + +The sampler counts the Safari app and WebKit content/network processes that can +be attributed to Safari's data container. Shared GPU services cannot be +reliably assigned without privileged instrumentation, but their impact remains +visible in the whole-machine battery result. This test does not attribute every +sample to one JavaScript function. Use Instruments Time Profiler, +Allocations/Leaks and Energy Log after a regression is detected. + +## 4. Clean install and release artifact + +Verify any built or downloaded app without installing it: + +```sh +./script/verify-safari-release.sh "/absolute/path/7TV for Safari.app" development +``` + +For a public artifact, use `distribution`; that additionally requires +Gatekeeper acceptance and a stapled notarization ticket. Run the distributed +ZIP or DMG on a separate Mac or clean macOS user, not from Xcode's build +directory. + +On a clean profile, record these scenarios: + +1. Fresh install and first Twitch load. +2. Upgrade over a configured older build; settings must remain. +3. Browser restart and Mac restart. +4. Sleep/wake and network loss/recovery. +5. Rollback to the prior compatible build. +6. Uninstall; no active plug-in registration may remain. + +Use `tests/safari/compatibility-matrix.json` to record which Safari/macOS +combinations passed. Public release requires every entry under `required`. diff --git a/SAFARI.md b/SAFARI.md new file mode 100644 index 000000000..dbd20beaa --- /dev/null +++ b/SAFARI.md @@ -0,0 +1,50 @@ +# 7TV for Safari + +This project packages the official 7TV extension as a Safari Web Extension. +The Safari build keeps the upstream site implementations and statically +supports the same three sites: + +- site access: Twitch, Kick and YouTube +- extension permission: local extension storage +- no Chrome extension-management permission +- no runtime registration; all three content-script matches are declared once +- no remote worker override +- no automatic download of build tools + +The wrapper app and extension use the App Sandbox and hardened runtime. The +wrapper has no file-selection or outgoing-network entitlement. Extension pages +use a restrictive content security policy. + +Build the locally signed macOS application with: + +```sh +./script/build-safari-local.sh +``` + +The script checks the reviewed lockfile, audits runtime dependencies, compiles +the Safari variant, signs it with an existing Apple Development identity, +verifies the nested signature, and checks the final permissions. It does not +install or replace the application automatically, and it does not register its +temporary Xcode build with Launch Services. + +The result is a locally signed development build. A downloadable public release +would additionally require a distribution certificate, notarization, an update +design, and a separate release review. Local development signing does not +require publishing through the Mac App Store. + +See `SECURITY-SAFARI.md` for the security model and remaining trust boundaries. +See `SAFARI-TESTING.md` for the real-Safari, stress, release-package and A/B +resource test protocol. + +Run the repeatable Safari regression gate with: + +```sh +npx --no-install yarn@1.22.22 verify:safari +``` + +It verifies the compiled manifest, Safari compatibility regressions, worker and +HTML boundaries, and native entitlements. When `SEVENTV_INSTALLED_APP` is set, +it also verifies the installed signature, registration, and byte equality of +critical resources. A release still needs one real-Safari smoke test: +reload Twitch twice and verify that a known 7TV emote changes from text to an +image after each reload. diff --git a/SECURITY-SAFARI.md b/SECURITY-SAFARI.md new file mode 100644 index 000000000..37b8fb72d --- /dev/null +++ b/SECURITY-SAFARI.md @@ -0,0 +1,52 @@ +# Safari security notes + +## Scope + +This variant packages the official 7TV extension for Safari. It runs on Twitch, +Kick and YouTube and retains 7TV's upstream emote and chat implementation for +each site. + +## Enforced controls + +- The manifest grants only extension storage and static access to Twitch, Kick + and YouTube. +- Safari does not dynamically register content scripts, preventing duplicate + registrations after service-worker restarts. +- Messages reaching the extension background are accepted only from HTTPS + Twitch pages and validated before use. +- Safari rejects all page-originated permission requests. +- Closed-tab messaging errors are consumed and cannot break initialization. +- The worker URL is always an extension URL; Twitch page storage cannot replace + it with an arbitrary URL. +- Login popup messages require the exact 7TV origin and popup window. +- Changelog HTML is sanitized before rendering. +- Extension pages disallow remote scripts, objects, base rewriting, and framing. +- The native wrapper does not log or echo extension messages. +- Both native targets are sandboxed and use hardened runtime. +- Dependency installation scripts are not run during the reviewed install, and + the runtime dependency audit has no known advisories. + +The upstream development toolchain is substantially older and its full audit +contains known advisories in build and lint packages. Modernizing that toolchain +is intentionally kept separate from the Safari compatibility change so it can +be reviewed and tested independently. Do not expose the Vite development server +to an untrusted network. + +## Remaining trust boundaries + +7TV must run page-world code inside Twitch to integrate with Twitch's chat UI. +That means Twitch page code and 7TV page-world code share an execution origin. +This is inherited from the upstream extension architecture, not a Safari-only +permission. For passive emote viewing, no 7TV login token is required. + +A local app can be signed with an Apple Development certificate. Its nested +signature is verifiable locally, but that is not equivalent to an App Store or +notarized public distribution. Public binaries require a separate +release-signing and notarization process. + +## Updating + +Treat every upstream update as new code. Review the upstream revision, retain +the Safari permission assertions, install only from the lockfile, rerun the +dependency audit, rebuild, and verify the final nested signature before +replacing the installed app. diff --git a/index.html b/index.html index b7ccb124d..78591646f 100644 --- a/index.html +++ b/index.html @@ -4,25 +4,6 @@ - - - - - - -