Skip to content
Open
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
48 changes: 47 additions & 1 deletion docs/vpn/management/dynamic-access-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,50 @@ S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-buckets-s3.ht
or [GitHub secret
gists](https://docs.github.com/en/get-started/writing-on-github/editing-and-sharing-content-with-gists/creating-gists).
Evaluate the specific needs of your deployment and choose a platform that aligns
with your requirements for accessibility and security.
with your requirements for accessibility and security.

## Share the Key with Your Users

Hosting the configuration is only half of a dynamic access key. What your users
add to the Outline client is the _location_ of that configuration, written with
the `ssconf://` scheme: take the HTTPS URL the configuration is served from and
replace `https://` with `ssconf://`.

```none
https://keys.example.com/a1b2c3d4e5f6.yml <- where you host the config
ssconf://keys.example.com/a1b2c3d4e5f6.yml <- the access key you share
```

The client replaces the scheme back with `https://` before fetching, so the
host, port, path and query string are all preserved as they are. A plain
`https://` URL is also accepted as a dynamic access key; the `ssconf://` scheme
exists so that the operating system opens the link with the Outline client
instead of a browser.

You can append a fragment to name the entry in the client's server list:

```none
ssconf://keys.example.com/a1b2c3d4e5f6.yml#My%20Server
```

The client shows this entry as "My Server"; the fragment is not part of the
Comment thread
angelodlfrtr marked this conversation as resolved.
fetched URL.

:::caution
A dynamic access key URL is a credential. Anyone who has it can fetch your
configuration and read the secret inside it. Serve it over HTTPS, give each user
their own long, randomly generated path rather than a guessable one such as
`/alice.yml`, and avoid platforms that publish or index what they host, such as
public GitHub repositories or public gists. See [Choose a Hosting
Platform](#choose-a-hosting-platform) for more factors to weigh when picking
where to host.
:::

:::note
Deleting the hosted configuration does not revoke access: the credentials it
contained remain valid until you remove the key from the server, connected
clients stay connected, and clients that reconnect automatically (at device
boot or app launch) reuse the last configuration they fetched successfully. A
connected client also keeps using the configuration it fetched at connect time;
edits to the hosted file take effect the next time it reconnects.
:::
70 changes: 47 additions & 23 deletions scripts/verify_translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
Known acceptable differences are documented and excluded:
- Code block counts may differ where English MD was updated after translation
export (content added post-export is not in translation HTML sources).
- Link counts may differ where the English MD has footnotes or escaped brackets
that the HTML export did not preserve.
- Specific English links may be absent from translations (footnotes the HTML
export did not preserve, or links added post-export); the remaining links
are still compared URL-by-URL.
- Internal links may lack .md extensions (Docusaurus resolves both forms).
- Some locales have link order swapped by the translator.
"""
Expand Down Expand Up @@ -54,6 +55,8 @@
"sdk/reference/smart-dialer-config": (10, 9),
# 9 code blocks added to MD after translation export (expanded reference)
"vpn/reference/access-key-config": (17, 8),
# 2 code blocks added to MD after translation export (ssconf:// key section)
"vpn/management/dynamic-access-keys": (7, 5),
}

# Known code block content differences: counts match but the content of
Expand All @@ -66,21 +69,27 @@
"vpn/advanced/prefixing": {0},
}

# Known link count differences: English MD has links (footnotes, escaped
# brackets) that the HTML export did not preserve.
# Format: doc_path -> (english_count, translated_count)
KNOWN_LINK_COUNT_DIFFS = {
# Links that only exist in the English version: added to the MD after
# translation export, or footnotes/escaped brackets the HTML export did not
# preserve. The listed English link indices (0-based, in document order) are
# removed before comparison, after which every remaining link must still
# match the translation URL-by-URL.
# Format: doc_path -> set of English link indices
KNOWN_ENGLISH_ONLY_LINKS: dict[str, set[int]] = {
# 2 footnote refs ([^1] in "Alternative[^1]:") not captured by HTML export
"download-links": (16, 14),
"download-links": {13, 15},
# 11 links added to MD after translation export (expanded reference)
"vpn/reference/access-key-config": (39, 28),
"vpn/reference/access-key-config": {0, 1, 20, 21, 22, 33, 34, 35, 36, 37, 38},
# 1 link to advanced-config added to MD after translation export
"vpn/advanced/caddy": (9, 8),
"vpn/advanced/caddy": {3},
# 1 anchor link added to MD after translation export (ssconf:// key section)
"vpn/management/dynamic-access-keys": {10},
}

# Known link order swaps: some translators reordered text, causing link
# URLs to appear in a different order. Format: doc_path -> set of link indices
# (0-based) where order may differ.
# URLs to appear in a different order. The URLs at the swapped positions must
# still match as a set. Indices are 0-based positions after English-only
# links have been removed. Format: doc_path -> set of link indices.
KNOWN_LINK_SWAPS = {
# Wikipedia Base64 link and Google encode/decode toolbox link swapped
"vpn/management/dynamic-access-keys": {3, 4},
Expand All @@ -96,7 +105,10 @@

# Known admonition count differences: English has admonitions added after
# translation export. Format: doc_path -> (english_count, translated_count)
KNOWN_ADMONITION_DIFFS: dict[str, tuple[int, int]] = {}
KNOWN_ADMONITION_DIFFS: dict[str, tuple[int, int]] = {
# 2 admonitions added to MD after translation export (ssconf:// key section)
"vpn/management/dynamic-access-keys": (2, 0),
}

# Threshold for content parity warnings. If the translation's non-code
# content is less than this fraction of the English content length, flag it.
Expand Down Expand Up @@ -446,25 +458,37 @@ def verify_locale(locale: str, english_paths: set[str]) -> list[Issue]:
f" (first diff at line {diff_line})"
))

# Link URLs (normalized to strip .md extensions)
# Link URLs (normalized to strip .md extensions). English-only links
# (added after translation export) are removed first so the remaining
# links can still be compared URL-by-URL.
en_links = [normalize_link_url(u) for u in extract_link_urls(en_text)]
tr_links = [normalize_link_url(u) for u in extract_link_urls(tr_text)]
en_only = KNOWN_ENGLISH_ONLY_LINKS.get(doc_path, set())
en_links = [u for i, u in enumerate(en_links) if i not in en_only]
if len(en_links) != len(tr_links):
known = KNOWN_LINK_COUNT_DIFFS.get(doc_path)
if known and known == (len(en_links), len(tr_links)):
pass # Known acceptable difference
else:
issues.append(Issue(
locale, doc_path, "LINKS",
f"Count mismatch: English has {len(en_links)}, "
f"translation has {len(tr_links)}"
))
issues.append(Issue(
locale, doc_path, "LINKS",
f"Count mismatch: English has {len(en_links)}"
f"{f' (excluding {len(en_only)} English-only)' if en_only else ''}, "
f"translation has {len(tr_links)}"
))
else:
swap_indices = KNOWN_LINK_SWAPS.get(doc_path, set())
if swap_indices:
# Swapped positions must still hold the same URLs, just
# possibly in a different order.
en_swapped = sorted(en_links[i] for i in swap_indices)
tr_swapped = sorted(tr_links[i] for i in swap_indices)
if en_swapped != tr_swapped:
issues.append(Issue(
locale, doc_path, "LINKS",
f"Swapped links {sorted(swap_indices)} don't match: "
f"English={en_swapped}, translation={tr_swapped}"
))
for idx, (en_url, tr_url) in enumerate(zip(en_links, tr_links)):
if en_url != tr_url:
if idx in swap_indices:
continue # Known link order swap
continue # Known link order swap (validated above)
issues.append(Issue(
locale, doc_path, "LINKS",
f"Link {idx + 1} differs: "
Expand Down
Loading