install.sh reads INKWELL_SERVER, INKWELL_CHANNEL and INKWELL_TOKEN (#5372)
The installer's environment variables kept the ThoughtSync-era TS_ prefix after the rename to Inkwell. They are now INKWELL_*, as is the INKWELL_SERVER_DEFAULT line the server fills in when it serves the script. The old names are not read any more; the operator approved the clean cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
# fallback that chased the `v*` release its manifest named. That came out once
|
||||
# `main` had published to `stable` for real (`b6673c6`); the two channels are the
|
||||
# same shape now and nothing here should special-case one of them again.
|
||||
# Pick one with `--channel dev` or `TS_CHANNEL=dev`. Through a pipe the options go
|
||||
# Pick one with `--channel dev` or `INKWELL_CHANNEL=dev`. Through a pipe the options go
|
||||
# after a `--`: curl -fsSL <url> | sh -s -- --channel dev
|
||||
#
|
||||
# Served from `dev` rather than `main`: `main` exists but trails day-to-day work by
|
||||
@@ -62,12 +62,12 @@ Inkwell desktop installer.
|
||||
--channel dev from the forge: rolling build from the latest green push to `dev`
|
||||
-h, --help this text
|
||||
|
||||
The options can also come from TS_SERVER and TS_CHANNEL. Through a pipe, pass
|
||||
The options can also come from INKWELL_SERVER and INKWELL_CHANNEL. Through a pipe, pass
|
||||
them after `--`: curl -fsSL <url> | sh -s -- --channel dev
|
||||
|
||||
From a server, the download needs a device token: make one in the web app under
|
||||
Account → Linked devices and paste it when asked. A token typed at the prompt is
|
||||
revoked again once the download is done. TS_TOKEN supplies one without a prompt,
|
||||
revoked again once the download is done. INKWELL_TOKEN supplies one without a prompt,
|
||||
and is left alone, since whoever set it is managing it.
|
||||
USAGE
|
||||
}
|
||||
@@ -75,12 +75,12 @@ USAGE
|
||||
# The address of the server that served this script. Written by that server
|
||||
# (src/inkwell/installer.py rewrites exactly this line) and empty in the copy the
|
||||
# forge serves, which is what keeps the forge path the default there.
|
||||
TS_SERVER_DEFAULT=""
|
||||
INKWELL_SERVER_DEFAULT=""
|
||||
|
||||
# --- channel ----------------------------------------------------------------
|
||||
channel="${TS_CHANNEL:-stable}"
|
||||
channel_asked="${TS_CHANNEL:-}"
|
||||
server="${TS_SERVER:-$TS_SERVER_DEFAULT}"
|
||||
channel="${INKWELL_CHANNEL:-stable}"
|
||||
channel_asked="${INKWELL_CHANNEL:-}"
|
||||
server="${INKWELL_SERVER:-$INKWELL_SERVER_DEFAULT}"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--channel)
|
||||
@@ -200,11 +200,11 @@ say "Installing ${version:-unknown} from $server"
|
||||
|
||||
# The token, from the environment or from the person at the keyboard. Read from the
|
||||
# TERMINAL, not stdin: through `curl | sh`, stdin is this script.
|
||||
token="${TS_TOKEN:-}"
|
||||
token="${INKWELL_TOKEN:-}"
|
||||
prompted=""
|
||||
if [ -z "$token" ]; then
|
||||
{ [ -r /dev/tty ] && [ -w /dev/tty ]; } ||
|
||||
die "the download needs a device token and there's no terminal to ask on; set TS_TOKEN."
|
||||
die "the download needs a device token and there's no terminal to ask on; set INKWELL_TOKEN."
|
||||
printf 'A device token is needed to download from %s.\n' "$server" > /dev/tty
|
||||
printf 'Make one in the web app under Account → Linked devices (it is revoked once the download is done).\n' > /dev/tty
|
||||
printf 'Token: ' > /dev/tty
|
||||
|
||||
@@ -12,7 +12,7 @@ editing a URL by hand, which is the friction `client_dist.py` exists to remove.
|
||||
## Which way it fails
|
||||
|
||||
A server that can put arbitrary text into a script a person pipes to `sh` is the
|
||||
whole attack. So the substitution is ONE variable, `TS_SERVER_DEFAULT`, given a value
|
||||
whole attack. So the substitution is ONE variable, `INKWELL_SERVER_DEFAULT`, given a value
|
||||
that has already passed [SAFE_ADDRESS] — no quote, no space, no `$`, nothing a shell
|
||||
reads as anything but characters — and the script checks the shape again before it
|
||||
uses it. If the address cannot pass, the route refuses rather than serving a script
|
||||
@@ -45,7 +45,7 @@ SOURCE_SCRIPT = Path(__file__).resolve().parents[2] / "desktop" / "packaging" /
|
||||
|
||||
# The one line the server rewrites. The script carries it exactly like this, and
|
||||
# `render` refuses a script that does not, rather than serving one with no address in.
|
||||
PLACEHOLDER = 'TS_SERVER_DEFAULT=""'
|
||||
PLACEHOLDER = 'INKWELL_SERVER_DEFAULT=""'
|
||||
|
||||
# What a server address may look like before it goes into a shell script: a scheme,
|
||||
# a host name or IPv4 address, an optional port and an optional path. Deliberately
|
||||
@@ -81,7 +81,7 @@ def render(script: str, address: str) -> str | None:
|
||||
"""`script` with `address` as its default server, or None if it cannot be done safely."""
|
||||
if not SAFE_ADDRESS.fullmatch(address) or script.count(PLACEHOLDER) != 1:
|
||||
return None
|
||||
return script.replace(PLACEHOLDER, f"TS_SERVER_DEFAULT='{address}'")
|
||||
return script.replace(PLACEHOLDER, f"INKWELL_SERVER_DEFAULT='{address}'")
|
||||
|
||||
|
||||
@bp.get("/install.sh")
|
||||
|
||||
@@ -7,7 +7,7 @@ from inkwell.installer import PLACEHOLDER, SOURCE_SCRIPT, render, server_address
|
||||
# DB-free, like test_client_dist: the route reads `public_url` from the live cache
|
||||
# rather than from the database, which is what makes it testable here.
|
||||
|
||||
SCRIPT = f"#!/bin/sh\nset -eu\n{PLACEHOLDER}\nserver=\"${{TS_SERVER:-$TS_SERVER_DEFAULT}}\"\n"
|
||||
SCRIPT = f"#!/bin/sh\nset -eu\n{PLACEHOLDER}\nserver=\"${{INKWELL_SERVER:-$INKWELL_SERVER_DEFAULT}}\"\n"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -31,10 +31,10 @@ def test_the_published_script_carries_the_line_the_server_rewrites_exactly_once(
|
||||
|
||||
def test_render_writes_the_address_into_the_one_variable():
|
||||
body = render(SCRIPT, "https://notes.example.com")
|
||||
assert "TS_SERVER_DEFAULT='https://notes.example.com'" in body
|
||||
assert "INKWELL_SERVER_DEFAULT='https://notes.example.com'" in body
|
||||
assert PLACEHOLDER not in body
|
||||
# Nothing else moved.
|
||||
assert body.replace("TS_SERVER_DEFAULT='https://notes.example.com'", PLACEHOLDER) == SCRIPT
|
||||
assert body.replace("INKWELL_SERVER_DEFAULT='https://notes.example.com'", PLACEHOLDER) == SCRIPT
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -97,14 +97,14 @@ async def test_the_route_serves_the_script_pointed_at_this_server(app):
|
||||
assert resp.status_code == 200
|
||||
assert resp.mimetype == "text/plain"
|
||||
body = await resp.get_data(as_text=True)
|
||||
assert "TS_SERVER_DEFAULT='http://notes.example.com'" in body
|
||||
assert "INKWELL_SERVER_DEFAULT='http://notes.example.com'" in body
|
||||
assert body.startswith("#!/bin/sh")
|
||||
|
||||
|
||||
async def test_the_route_uses_the_public_address_when_one_is_set(app, monkeypatch):
|
||||
monkeypatch.setitem(settings._live, "public_url", "https://notes.example.com")
|
||||
resp = await app.test_client().get("/install.sh", headers={"Host": "inkwell:5000"})
|
||||
assert "TS_SERVER_DEFAULT='https://notes.example.com'" in await resp.get_data(as_text=True)
|
||||
assert "INKWELL_SERVER_DEFAULT='https://notes.example.com'" in await resp.get_data(as_text=True)
|
||||
|
||||
|
||||
async def test_the_route_refuses_rather_than_serve_an_unsafe_address(app, monkeypatch):
|
||||
@@ -113,7 +113,7 @@ async def test_the_route_refuses_rather_than_serve_an_unsafe_address(app, monkey
|
||||
monkeypatch.setitem(settings._live, "public_url", "https://notes.example.com/'$(id)'")
|
||||
resp = await app.test_client().get("/install.sh", headers={"Host": "notes.example.com"})
|
||||
assert resp.status_code == 500
|
||||
assert "TS_SERVER_DEFAULT" not in await resp.get_data(as_text=True)
|
||||
assert "INKWELL_SERVER_DEFAULT" not in await resp.get_data(as_text=True)
|
||||
|
||||
|
||||
async def test_a_server_with_no_copy_of_the_script_404s(app, monkeypatch, tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user