diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index f1080df..5be85ce 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -128,6 +128,7 @@ export default withMermaid({ items: [ { text: 'Management UI', link: '/guide/management-ui' }, { text: 'API keys & peering', link: '/guide/peering' }, + { text: 'Quick links', link: '/guide/quick-links' }, { text: 'Running without Docker', link: '/guide/running-without-docker' }, { text: 'Troubleshooting', link: '/guide/troubleshooting' }, ], diff --git a/website/guide/management-ui.md b/website/guide/management-ui.md index 73fbcaa..57b99df 100644 --- a/website/guide/management-ui.md +++ b/website/guide/management-ui.md @@ -22,6 +22,8 @@ operate jack day to day: [`downloads`](/reference/configuration#downloads) block: where finished files land, how many transfers run at once, whether jack drops its copy after an import, and the retry and import-watcher tuning. +- **Settings → Quick linking** — how peers reach this instance, plus the + buttons that generate and import [quick links](/guide/quick-links). - **API keys** — issue and revoke the keys you hand to peers. - **Logs** — live-tail the backend's logs. @@ -50,6 +52,21 @@ One **Save** covers the form and only lights up once something changed; you save — **every other key here is read at startup, so restart jack** to apply it. +## Settings → Quick linking + +This section holds the [`jack.external`](/reference/configuration#jack-external) +profile — the instance name, external URL, and any proxy headers a peer needs to +reach you — and the two buttons that use it: + +- **Generate quick link** issues a fresh peer API key and encodes it, together + with the profile, into a single shareable string. It's enabled only once the + profile is saved, and the link is revealed exactly once. +- **Add via quick link** decodes a link a friend sent you and opens the *Add + peer* form prefilled, for you to review before saving. + +Treat a generated link as a credential — see +[Quick links](/guide/quick-links) for the full workflow and how to revoke one. + ## Access control The UI supports two auth modes, depending on where the management key comes diff --git a/website/guide/peering.md b/website/guide/peering.md index 3ff2a49..c09f21c 100644 --- a/website/guide/peering.md +++ b/website/guide/peering.md @@ -8,9 +8,10 @@ jack authenticates each external surface by **key type**, so a credential only works where it's meant to (`/ping` is the only unauthenticated route): - **Peer API keys.** Issued per peer from the management UI's *API keys* section - (or the management API) — named, revocable, and optionally expiring. Scoped to - the **peer API** (`/handshake`, `/peer/*`): a peer key **cannot** query your - Torznab indexer or act as your download client. + (or the management API, or implicitly when you generate a + [quick link](/guide/quick-links)) — named, revocable, and optionally expiring. + Scoped to the **peer API** (`/handshake`, `/peer/*`): a peer key **cannot** + query your Torznab indexer or act as your download client. - **Managed keys.** jack mints these automatically and registers them in your Radarr/Sonarr when it auto-registers as their indexer + download client. Scoped to the ***arr surface** (`/torznab`, the qBittorrent API); you never @@ -46,6 +47,12 @@ address your own Radarr/Sonarr use to reach jack. After that, each side's Radarr/Sonarr can find and pull media the other has. +::: tip Skip the copy-paste +[Quick links](/guide/quick-links) package the URL, a freshly issued key, and any +proxy headers into one string your friend pastes into their management UI. Same +peering, fewer things to get wrong. +::: + ::: tip One key per peer Issue a **separate key per peer**. Each is scoped to the peer API — it can't reach your indexer or download client — and can be revoked individually without diff --git a/website/guide/quick-links.md b/website/guide/quick-links.md new file mode 100644 index 0000000..f082ee2 --- /dev/null +++ b/website/guide/quick-links.md @@ -0,0 +1,129 @@ +--- +description: Share a jack instance with a friend by generating a single quick link that carries the peer URL, a freshly issued API key, and any proxy headers needed to reach you. +--- + +# Quick links + +A quick link is one string carrying everything a friend needs to add you as a +peer: your peer URL, an API key issued for them, and any headers your proxy +requires. + +``` +jack-link:v1:eyJ2IjoxLCJ0eXBlIjoicGVlciIsIm5hbWUiOiJSb3oncyBKYWNrIiwidXJsIjoi... +``` + +They paste it into their management UI, review the decoded fields, and save the +peer. + +::: danger A quick link is a credential +The link **contains a working API key in plain text**. Anyone who gets it can +search and download from your libraries until you revoke the key. Send it over +a channel you trust and never paste one into a public issue, gist, or forum. +::: + +## Set up quick linking + +Generating links requires a `jack.external` profile, editable under +***Settings -> Quick linking*** in the [management UI](/guide/management-ui): + +- **Instance name**: the peer name suggested to whoever imports your link. They + can change it before saving. +- **External URL**: the URL a peer uses to reach you, e.g. + `https://jack.example.com`. HTTP and HTTPS only, with no `user:password@` + credentials embedded. +- **External headers**: optional headers a peer must send to get through your + proxy, such as Cloudflare Access service tokens. Each value is a + [`ConfigSecret`](/reference/configuration#configsecret), so prefer an `env` or + `file` reference over a literal. + +Every link you generate reuses the saved profile. Full schema: +[`jack.external`](/reference/configuration#jack-external). + +::: tip Use HTTPS +The API key in a quick link, and in every request the peer later makes, travels +in a header. An `http://` external URL exposes it to anything on the path, and +jack warns at startup about peers configured over plain HTTP. +::: + +::: warning Internal and external URLs are not the same +[`jack.internalUrl`](/reference/configuration#jack-internalurl) is the address +your own Radarr/Sonarr use to reach jack. Putting it in the external URL hands +peers an address that only resolves on your side. +::: + +## Generate a link + +***Settings -> Quick linking -> Generate quick link*** asks for: + +- **Suggested peer name**: prefilled from your instance name, this is how you + show up in your friend's peer list. +- **Key name** and an optional **description**: how you identify this credential + later when you need to revoke it. + +On submit, jack resolves the external profile, **issues a new peer API key**, +and returns the encoded link. + +The plaintext link appears **once**, in that dialog. Closing it discards the key +from the UI, the same one-time reveal as any other +[peer API key](/guide/peering). Copy it before closing; if you lose it, generate +a fresh link. + +::: tip One link per friend +Every generated link carries its own key, so revoking one friend's access leaves +everyone else connected. Never reuse one link for two people. +::: + +## Import a link + +***Settings -> Quick linking -> Add via quick link*** takes the pasted link and +opens the normal ***Add peer*** form with the name, URL, key, and headers +filled in. +Nothing is saved until you submit, so you can rename the peer or check where +the URL points first. + +A link that fails any of the [format checks](#link-format) is rejected outright, +instead of half-filling the form. + +## Revoking access + +Revoke the key under ***Settings -> API keys*** and the link stops working +immediately. Find it by the name you gave it at generation time. + +Removing the `jack.external` block (***Remove configuration*** in the +***Quick linking*** section) only stops you from generating *new* links. Links already out +there keep working until their keys are revoked. + +## Link format + +The prefix `jack-link:v1:` followed by a base64url-encoded JSON object: + +```json +{ + "v": 1, + "type": "peer", + "name": "Roz's Jack", + "url": "https://jack.example.com", + "apiKey": "", + "headers": { "CF-Access-Client-Id": "..." } +} +``` + +There is no encryption. Anyone holding the link can read the key out of it. + +Decoding is strict: + +- `v` and `type` must match the values above. +- `url` must be `http`/`https`, with no embedded credentials. +- `headers` are validated against the same rules the config uses: + - reserved names are rejected: `X-Api-Key`, `Host`, `Content-Length`, + `Connection`, and `Transfer-Encoding`. + - names must be valid HTTP header tokens. + - no name may repeat. + - values must be non-empty. + - values may not contain line breaks. + +To generate a link outside the UI, call +[`POST /quick-links`](/reference/management-api/postQuickLinks) on the +management API. A missing external profile, or one +with a secret reference that cannot be resolved, fails the request before any +key is issued, so a broken profile leaves no orphaned credential. diff --git a/website/reference/configuration.md b/website/reference/configuration.md index 8d1c2c1..ab0a7e5 100644 --- a/website/reference/configuration.md +++ b/website/reference/configuration.md @@ -21,6 +21,9 @@ it alone.) | --- | --- | | [`jack.internalUrl`](#jack-internalurl) | URL your *arr apps use to reach jack | | [`jack.tmdbApiKey`](#jack-tmdbapikey) | TMDB metadata for the catalog | +| [`jack.external.instanceName`](#jack-external-instancename) | Peer name suggested in your quick links | +| [`jack.external.url`](#jack-external-url) | URL peers use to reach you | +| [`jack.external.headers`](#jack-external-headers) | Headers peers must send to reach you | | [`downloads.completedPath`](#downloads-completedpath) | Where finished downloads go | | [`downloads.maxConcurrentDownloads`](#downloads-maxconcurrentdownloads) | Simultaneous transfer cap | | [`downloads.maxDownloadAttempts`](#downloads-maxdownloadattempts) | Retries before a download fails | @@ -67,6 +70,53 @@ they use whatever you hand them (see [API keys & peering](/guide/peering)). TMDB v3 API key, used by the management UI's catalog to enrich peer libraries with artwork and metadata. +### `jack.external` + +**Type:** `object` + +How *another* jack reaches this instance — the profile jack encodes into the +[quick links](/guide/quick-links) you generate. Optional: without it, peering +still works, you just hand out the URL and key by hand. + +Edit it from the management UI (**Settings → Quick linking**), which writes this +block through its own endpoints so the rest of `jack` is left untouched. + +### `jack.external.instanceName` + +**Type:** `string`\ +**Format:** 1–100 characters + +The peer name suggested to whoever imports one of your quick links. They can +change it before saving the peer. The management UI won't generate a link until +this is set; calling the API directly, the suggested name comes from the request +body instead. + +### `jack.external.url` + +**Type:** `string` · **Required**\ +**Format:** `http`/`https` URL, no embedded `user:password@` credentials + +The URL a peer should use to reach this instance — what ends up in the `url` +field of every quick link you generate. Distinct from +[`jack.internalUrl`](#jack-internalurl), which is for your own *arr apps. Use +`https://`: the peer's API key travels in a request header. + +### `jack.external.headers` + +**Type:** `object`\ +**Content:** header name → [`ConfigSecret`](#configsecret) value\ +**Default:** `{}` + +Extra headers a peer must send to get through whatever sits in front of you — +Cloudflare Access service tokens and the like. They're copied into the quick +link so your friend doesn't have to configure them by hand, landing in their +config as [`peers[].headers`](#peers-headers). + +Values are resolved when the profile is saved and when a link is generated, +while `env` and `file` references remain references in your config file. Reserved headers +(`X-Api-Key`, `Host`, `Content-Length`, `Connection`, `Transfer-Encoding`), +duplicate names, and values with line breaks are rejected; at most 100 headers. + ## `downloads` Needed to **consume** (download) from peers — jack only registers itself as a @@ -341,7 +391,15 @@ reports the problem and refuses to load that config. { "jack": { "internalUrl": "http://jack:5225", - "tmdbApiKey": { "env": "TMDB_API_KEY" } + "tmdbApiKey": { "env": "TMDB_API_KEY" }, + "external": { + "instanceName": "Roz's Jack", + "url": "https://jack.example.com", + "headers": { + "CF-Access-Client-Id": { "env": "MY_CF_CLIENT_ID" }, + "CF-Access-Client-Secret": { "env": "MY_CF_CLIENT_SECRET" } + } + } }, "downloads": { "completedPath": "/data/torrents/completed",