Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- dependencies
groups:
elgato-dev:
patterns:
- "@elgato/*"
- "@rollup/*"
- "rollup"
- "typescript"
- "@types/*"
- "@tsconfig/*"
77 changes: 77 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Build and release

on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build and validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Require release tags to point at main
if: github.ref_type == 'tag'
run: |
git fetch --no-tags origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "Refusing build/release: tag ${GITHUB_REF_NAME} (${GITHUB_SHA}) is not on main."
exit 1
fi

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm

- name: Install dependencies
run: npm ci

- name: Build and validate
run: npm run validate

- name: Pack plugin
if: github.event_name != 'pull_request'
run: npm run pack

- name: Upload plugin artifact
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: com.ssheppdev.litra.streamDeckPlugin
path: release/com.ssheppdev.litra.streamDeckPlugin
if-no-files-found: error

release:
name: Publish GitHub release
needs: build
if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: com.ssheppdev.litra.streamDeckPlugin
path: release

- name: Create or update release
uses: softprops/action-gh-release@v2
with:
files: release/com.ssheppdev.litra.streamDeckPlugin
generate_release_notes: true
fail_on_unmatched_files: true
19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.DS_Store
node_modules/

# Reference clones (read-only upstream)
ref/litra-rs/
ref/litra/

# Stream Deck build / runtime output
*.sdPlugin/bin
*.sdPlugin/logs
*.sdPlugin/node_modules
release/
*.streamDeckPlugin

# Local tooling / noisy capture byproducts
.venv-frida/
ref/hid-captures/*.jsonl
ref/hid-captures/*.jsonl.err
ref/hid-captures/lghub-logstream.txt
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Plugin",
"type": "node",
"request": "attach",
"processId": "${command:PickProcess}",
"outFiles": [
"${workspaceFolder}/bin/**/*.js"
],
"resolveSourceMapLocations": [
"${workspaceFolder}/**"
]
}
]
}
17 changes: 17 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
/* JSON schemas */
"json.schemas": [
{
"fileMatch": [
"**/manifest.json"
],
"url": "https://schemas.elgato.com/streamdeck/plugins/manifest.json"
},
{
"fileMatch": [
"**/layouts/*.json"
],
"url": "https://schemas.elgato.com/streamdeck/plugins/layout.json"
}
]
}
18 changes: 15 additions & 3 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@ MIT License

Copyright (c) 2026 Seth Sheppard

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
110 changes: 109 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,112 @@
# Litra for Stream Deck

Initial import lands via squash-merge from `initial-commit`.
Control [Logitech Litra](https://www.logitech.com/) lights from an [Elgato Stream Deck](https://www.elgato.com/streamdeck) — including dials on Stream Deck+.

**Not an official Logitech or Elgato product.**

End users only need Stream Deck software and a USB-connected Litra. There is nothing else to install (no Node, no `litra` CLI, no Homebrew). The plugin ships with its USB HID stack baked in.

Under the hood (for developers): it uses the [`litra`](https://github.com/timrogers/litra) Node library **in-process** over USB HID — no CLI subprocess.

## Features

| Action | Key | Dial (Stream Deck+) |
|--------|-----|---------------------|
| **Power** | On / off / toggle | — |
| **Brightness** | Jump to a preset % | Rotate to adjust; **press toggles power** |
| **Temperature** | Jump to a preset Kelvin | Rotate to adjust; **press toggles power** |

- Works with **all connected Litra devices** at once (no per-light picker yet)
- Beam LX front brightness uses a corrected lumen read (stock JS only used the low byte)
- macOS and Windows (USB; Bluetooth control is not supported by the underlying stack)

## Supported hardware

| Light | Front power / brightness / temperature |
|-------|----------------------------------------|
| Litra Glow | Yes |
| Litra Beam | Yes |
| Litra Beam LX | Yes (front key light) |

**Not included (yet):** Beam LX rear RGB / G HUB effect presets. Research notes are in [`ref/hid-captures/NOTES.md`](ref/hid-captures/NOTES.md).

## Requirements

**To use the plugin (install from Releases)**

- [Stream Deck](https://www.elgato.com/streamdeck) software **7.1+**
- A Litra connected over **USB**
- macOS 12+ or Windows 10+

You do **not** need Node.js, npm, Homebrew, or the `litra` CLI.

Quit **Logitech G HUB** if HID access fights the plugin (especially on macOS).

**To build from source** (contributors only)

- Node.js **20**
- npm

## Install (prebuilt)

1. Download the latest `.streamDeckPlugin` from [Releases](https://github.com/SSheppDev/streamdeck-litra/releases).
2. Double-click the file (or open it with Stream Deck).
3. Drag **Power**, **Brightness**, or **Temperature** onto your layout.

That’s the full end-user install — the package already includes `litra` / `node-hid`.

If you linked a local/dev copy earlier, unlink it first:

```bash
streamdeck unlink com.ssheppdev.litra
```

## Build from source

```bash
git clone https://github.com/SSheppDev/streamdeck-litra.git
cd streamdeck-litra
npm install
npm run build
```

### Link for development

```bash
npm run link # streamdeck link ./com.ssheppdev.litra.sdPlugin
npm run watch # rebuild + restart on change
```

### Package a distributable

```bash
npm run pack
```

Writes `release/com.ssheppdev.litra.streamDeckPlugin`.

Validate without packing:

```bash
npm run validate
```

## Usage tips

- **Brightness / temperature keys** show the configured preset as the title.
- **Dials** use Stream Deck’s built-in `$A1` layout; rotate adjusts, press toggles front power.
- With **no lights connected**, actions show Stream Deck’s alert indicator.
- Multiple lights are always controlled together in v1.

## Credits

- [timrogers/litra](https://github.com/timrogers/litra) — HID protocol / Node library
- [Elgato Stream Deck SDK](https://docs.elgato.com/streamdeck)

## Security

See [SECURITY.md](SECURITY.md) for how to report vulnerabilities and this project’s secrets policy.

## License

[MIT](LICENSE)
60 changes: 60 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Security policy

## Reporting a vulnerability

Please **do not** open a public GitHub issue for security problems.

Use [GitHub private vulnerability reporting](https://github.com/SSheppDev/streamdeck-litra/security/advisories/new) on this repository, or email **sethcsheppard@outlook.com** with:

- A short description of the issue
- Steps to reproduce (or a PoC)
- Affected versions / commit if known
- Your assessment of impact

You should hear back within a few days. There is no bug bounty for this project.

## Scope

**In scope**

- The Stream Deck plugin source and packaged `.streamDeckPlugin` artifacts from this repo
- Accidental exposure of credentials or secrets in this repository
- Dependency vulnerabilities that affect build or runtime of this plugin

**Out of scope**

- Logitech / Elgato / third-party products and firmware
- Issues that only appear when Logitech G HUB or other software holds exclusive HID access
- Social engineering, physical access, or compromised developer machines

## Secrets policy

This repository must not contain secrets. Treat the following as secrets and **never** commit them:

- API keys, access tokens, PATs, OAuth client secrets
- Passwords, private keys, certificates, `.pem` / `.p12` material
- `.env` files, credential JSON, cloud/provider config with keys
- Session cookies or webhook signing secrets

### Rules

1. **No secrets in git** — not in source, docs, examples, commits, or release assets.
2. **No secrets in the plugin package** — Stream Deck plugins run on the user’s machine; still do not embed provider credentials. Persist any future auth material only via Stream Deck global settings (encrypted on-device), never in `manifest.json` or bundled files.
3. **Push protection** — GitHub secret scanning with push protection is enabled. Do not bypass alerts; rotate any credential that was nearly or actually committed.
4. **History is forever** — deleting a file in a later commit does not remove it from history. If a secret lands in git, **rotate it immediately**, then scrub history if needed.
5. **Local machine data stays local** — do not commit personal inventories, host paths with sensitive context, or unrelated tooling snapshots.
6. **Dependencies** — keep lockfiles committed; address Dependabot / `npm audit` findings in a timely way for high/critical issues.

### If a secret is exposed

1. Revoke / rotate the credential at the provider (do this first).
2. Remove it from the working tree and stop using the leaked value.
3. Open a private report (or notify maintainers) with what leaked and when.
4. Optionally rewrite history (`git filter-repo` / BFG) and force-push if the secret remains reachable in old commits — assume anyone who cloned already has it until rotation is done.

## Supported versions

| Version | Supported |
|---------|-----------|
| Latest release (`v1.x`) | Yes |
| Older tags / forks | Best effort only |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading