Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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') || '' }}
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,4 +32,3 @@ msg_types.json

crx/*
script/*.json

4 changes: 4 additions & 0 deletions CHANGELOG-nightly.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions SAFARI-COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -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.
126 changes: 126 additions & 0 deletions SAFARI-TESTING.md
Original file line number Diff line number Diff line change
@@ -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`.
50 changes: 50 additions & 0 deletions SAFARI.md
Original file line number Diff line number Diff line change
@@ -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.
52 changes: 52 additions & 0 deletions SECURITY-SAFARI.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 0 additions & 19 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,6 @@
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,400&display=swap"
rel="stylesheet"
/>
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@700&display=swap" rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@100;600;900&amp;display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=BBH+Bartle&family=Pixelify+Sans:wght@400..700&family=Syne:wght@400..800&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Pixelify+Sans,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,400&display=swap"
rel="stylesheet"
/>
</head>
<body data-seventv-app>
<noscript>
Expand Down
Loading
Loading