From ce32f637bcfecd28a805c9e1dc798185bc590178 Mon Sep 17 00:00:00 2001 From: Roz Date: Wed, 19 Aug 2026 21:43:41 +0200 Subject: [PATCH 1/3] docs: document peer quick links --- website/.vitepress/config.mts | 1 + website/guide/management-ui.md | 17 ++++ website/guide/peering.md | 13 ++- website/guide/quick-links.md | 123 +++++++++++++++++++++++++++++ website/reference/configuration.md | 60 +++++++++++++- 5 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 website/guide/quick-links.md 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..df63d27 --- /dev/null +++ b/website/guide/quick-links.md @@ -0,0 +1,123 @@ +--- +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 + +Peering by hand means copying three things into your friend's config: your peer +URL, an API key you issued them, and — if you sit behind an authenticating proxy +— whatever headers that proxy demands. A **quick link** bundles all of it into +one string you paste into a chat window: + +``` +jack-link:v1:eyJ2IjoxLCJ0eXBlIjoicGVlciIsIm5hbWUiOiJSb3rigJlzIEphY2siLCJ1cmwiOi… +``` + +The friend pastes it into their management UI, reviews the decoded fields, and +saves the peer. No key typed by hand, no URL typo, no forgotten header. + +::: 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 needs one thing configured first: how another jack reaches +*this* instance. That lives in the `jack.external` block, editable from +**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 should use to reach you, e.g. + `https://jack.example.com`. This is *not* [`jack.internalUrl`](/reference/configuration#jack-internalurl), + which is the address your own Radarr/Sonarr use. HTTP and HTTPS only, and no + `user:password@` credentials embedded in the URL. +- **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. + +Save it once; the profile is reused for every link you generate. See +[`jack.external`](/reference/configuration#jack-external) for the full schema. + +::: tip Use HTTPS +The API key in a quick link — and in every request the peer later makes — is +carried 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. +::: + +## Generate a link + +**Settings → Quick linking → Generate quick link** asks for: + +- **Suggested peer name** — how you'll show up in your friend's peer list, + prefilled from your instance name. +- **Key name** and an optional **description** — how *you'll* identify this + credential later, so you know which link to revoke. + +On submit, jack resolves your external profile, **issues a brand-new peer API +key**, and returns the encoded link. + +The plaintext link is shown **once**, in that dialog. Close it and the key is +gone from the UI — the same one-time reveal as any other +[peer API key](/guide/peering). Copy it before you close, and generate a fresh +link if you lose it. + +::: tip One link per friend +Every generated link carries its own key, so revoking one friend's access — +**Settings → API keys**, find the key by the name you gave it, revoke — leaves +everyone else connected. Never reuse one link for two people. +::: + +## Import a link + +On the receiving side: **Settings → Quick linking → Add via quick link**, paste, +and hit **Review peer**. jack decodes the link and opens the normal *Add peer* +form with the name, URL, key, and headers filled in. Nothing is saved until you +review the fields and submit, so you can rename the peer or check where the URL +actually points first. + +A link that fails to decode is rejected outright — bad prefix, corrupt payload, +a non-HTTP URL, or a header the format doesn't allow. jack won't half-import it. + +## Revoking access + +A quick link is only as live as the key inside it. Revoke that key in +**Settings → API keys** and the link stops working immediately, for whoever +holds it. That's the whole reason each link gets its own key: the name and +description you set at generation time are how you find the right one later. + +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. + +## What's inside a link + +A quick link is 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": "…" } +} +``` + +It is **encoded, not encrypted** — anyone can decode it, which is exactly why +it's treated as a secret. Decoding is strict: the version and type must match, +the URL must be `http`/`https` without embedded credentials, and header names +are validated against the same rules the config uses (no `X-Api-Key`, no `Host`, +no duplicates, no line breaks). + +Programmatically, `POST /quick-links` on the +[management API](/reference/management-api) does the same thing as the button — +resolve the profile, mint a key, return the link once. If the external profile +is missing or one of its secret references can't be resolved, the request fails +**before** a key is issued, so a broken profile never leaves an orphaned +credential behind. diff --git a/website/reference/configuration.md b/website/reference/configuration.md index 8d1c2c1..63d6993 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 **only when a link is generated**, so `env` and `file` +references stay as 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", From dd17554815fb313bd7d34884a18b1921966dc200 Mon Sep 17 00:00:00 2001 From: Roz Date: Wed, 19 Aug 2026 22:28:29 +0200 Subject: [PATCH 2/3] docs: tighten quick links page prose --- website/guide/quick-links.md | 138 ++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 66 deletions(-) diff --git a/website/guide/quick-links.md b/website/guide/quick-links.md index df63d27..f082ee2 100644 --- a/website/guide/quick-links.md +++ b/website/guide/quick-links.md @@ -4,17 +4,16 @@ description: Share a jack instance with a friend by generating a single quick li # Quick links -Peering by hand means copying three things into your friend's config: your peer -URL, an API key you issued them, and — if you sit behind an authenticating proxy -— whatever headers that proxy demands. A **quick link** bundles all of it into -one string you paste into a chat window: +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:eyJ2IjoxLCJ0eXBlIjoicGVlciIsIm5hbWUiOiJSb3rigJlzIEphY2siLCJ1cmwiOi… +jack-link:v1:eyJ2IjoxLCJ0eXBlIjoicGVlciIsIm5hbWUiOiJSb3oncyBKYWNrIiwidXJsIjoi... ``` -The friend pastes it into their management UI, reviews the decoded fields, and -saves the peer. No key typed by hand, no URL typo, no forgotten header. +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 @@ -24,100 +23,107 @@ a channel you trust and never paste one into a public issue, gist, or forum. ## Set up quick linking -Generating links needs one thing configured first: how another jack reaches -*this* instance. That lives in the `jack.external` block, editable from -**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 should use to reach you, e.g. - `https://jack.example.com`. This is *not* [`jack.internalUrl`](/reference/configuration#jack-internalurl), - which is the address your own Radarr/Sonarr use. HTTP and HTTPS only, and no - `user:password@` credentials embedded in the URL. -- **External headers** — optional headers a peer must send to get through your +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. -Save it once; the profile is reused for every link you generate. See -[`jack.external`](/reference/configuration#jack-external) for the full schema. +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 — is -carried 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. +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: +***Settings -> Quick linking -> Generate quick link*** asks for: -- **Suggested peer name** — how you'll show up in your friend's peer list, - prefilled from your instance name. -- **Key name** and an optional **description** — how *you'll* identify this - credential later, so you know which link to revoke. +- **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 your external profile, **issues a brand-new peer API -key**, and returns the encoded link. +On submit, jack resolves the external profile, **issues a new peer API key**, +and returns the encoded link. -The plaintext link is shown **once**, in that dialog. Close it and the key is -gone from the UI — the same one-time reveal as any other -[peer API key](/guide/peering). Copy it before you close, and generate a fresh -link if you lose it. +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 — -**Settings → API keys**, find the key by the name you gave it, revoke — leaves +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 -On the receiving side: **Settings → Quick linking → Add via quick link**, paste, -and hit **Review peer**. jack decodes the link and opens the normal *Add peer* -form with the name, URL, key, and headers filled in. Nothing is saved until you -review the fields and submit, so you can rename the peer or check where the URL -actually points first. +***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 to decode is rejected outright — bad prefix, corrupt payload, -a non-HTTP URL, or a header the format doesn't allow. jack won't half-import it. +A link that fails any of the [format checks](#link-format) is rejected outright, +instead of half-filling the form. ## Revoking access -A quick link is only as live as the key inside it. Revoke that key in -**Settings → API keys** and the link stops working immediately, for whoever -holds it. That's the whole reason each link gets its own key: the name and -description you set at generation time are how you find the right one later. +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 +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. -## What's inside a link +## Link format -A quick link is the prefix `jack-link:v1:` followed by a base64url-encoded JSON -object: +The prefix `jack-link:v1:` followed by a base64url-encoded JSON object: ```json { "v": 1, "type": "peer", - "name": "Roz’s Jack", + "name": "Roz's Jack", "url": "https://jack.example.com", "apiKey": "", - "headers": { "CF-Access-Client-Id": "…" } + "headers": { "CF-Access-Client-Id": "..." } } ``` -It is **encoded, not encrypted** — anyone can decode it, which is exactly why -it's treated as a secret. Decoding is strict: the version and type must match, -the URL must be `http`/`https` without embedded credentials, and header names -are validated against the same rules the config uses (no `X-Api-Key`, no `Host`, -no duplicates, no line breaks). - -Programmatically, `POST /quick-links` on the -[management API](/reference/management-api) does the same thing as the button — -resolve the profile, mint a key, return the link once. If the external profile -is missing or one of its secret references can't be resolved, the request fails -**before** a key is issued, so a broken profile never leaves an orphaned -credential behind. +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. From a2b7fe99b7ac1fd8740dd97fe52ccdd064aa2a39 Mon Sep 17 00:00:00 2001 From: Roz <3948961+roziscoding@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:48:32 +0200 Subject: [PATCH 3/3] chore: clarify secret resolution Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- website/reference/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/reference/configuration.md b/website/reference/configuration.md index 63d6993..ab0a7e5 100644 --- a/website/reference/configuration.md +++ b/website/reference/configuration.md @@ -112,8 +112,8 @@ 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 **only when a link is generated**, so `env` and `file` -references stay as references in your config file. Reserved 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.