Materializing wrote a second plaintext file per secret, which is the problem the store
was meant to solve: two files drift, and the copy is what ends up committed or grepped.
- tangled_pr / tangled_pr_edit / tangled_repo now read tangled.cookie from the store.
engine/.tangled-session is deleted; --cookie-file remains as a legacy escape hatch.
- materialize() is replaced by run-time injection that leaves nothing at rest:
exec-env <group> -- cmd group as env vars (use this instead of a systemd
EnvironmentFile — same effect, no plaintext on disk)
with-file <key> -- cmd {} 0600 file in a private tmpdir, removed when cmd exits,
for consumers that insist on a path (ssh -i, a TLS key)
Co-Authored-By: Claude <noreply@anthropic.com>
67 lines
3.4 KiB
Python
Executable File
67 lines
3.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Create a Tangled repo via a reused session cookie (sibling of tangled_pr.py).
|
|
|
|
Tangled repo creation is an htmx POST to /repo/new (the web "New repository" form), authenticated by the
|
|
bot's appview session cookie. There is no separate git-create; you create the repo here, then push to
|
|
git@tangled.org:<owner>/<repo>. Cookie: engine/.tangled-session (refresh with scripts/get-tangled-cookie.py).
|
|
|
|
python3 engine/tangled_repo.py --name lichen.page.backup --description "..."
|
|
# then: git remote add backup git@tangled.org:notplants-bot.bsky.social/lichen.page.backup
|
|
# git push --force backup 'refs/remotes/<src>/*:refs/heads/*' && git push --force backup --tags
|
|
"""
|
|
import argparse, os, sys, urllib.parse, urllib.request
|
|
|
|
BASE = "https://tangled.org"
|
|
|
|
def load_cookie(path=None):
|
|
"""The cookie lives in the encrypted store (tangled.cookie) — see engine/README.md (Secrets).
|
|
`path` is a legacy escape hatch: a TANGLED_COOKIE=... file, used only if explicitly passed."""
|
|
if path:
|
|
for line in open(path):
|
|
if line.strip().startswith("TANGLED_COOKIE="):
|
|
return line.strip()[len("TANGLED_COOKIE="):]
|
|
sys.exit(f"{path} has no TANGLED_COOKIE= line")
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import secrets as _store
|
|
c = _store.get("tangled.cookie")
|
|
if not c:
|
|
sys.exit("no tangled.cookie in the secret store — add it with: sops /secrets/store.yaml")
|
|
return c
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="create a Tangled repo via a reused session cookie")
|
|
ap.add_argument("--name", required=True, help="repo name, e.g. lichen.page.backup")
|
|
ap.add_argument("--description", default="")
|
|
ap.add_argument("--branch", default="main", help="default branch (form default: main)")
|
|
ap.add_argument("--domain", default="knot1.tangled.sh", help="knot to host on (radio value)")
|
|
ap.add_argument("--cookie-file", default=None, help="legacy: read the cookie from this file instead of the store")
|
|
a = ap.parse_args()
|
|
|
|
cookie = load_cookie(a.cookie_file)
|
|
# The form only processes the create when it sees HX-Request (otherwise it re-renders the page — a 200
|
|
# that creates nothing). Success = HTTP 200 with an HX-Location header pointing at the owner/repo.
|
|
form = {"name": a.name, "description": a.description, "branch": a.branch, "domain": a.domain}
|
|
data = urllib.parse.urlencode(form).encode()
|
|
req = urllib.request.Request(f"{BASE}/repo/new", data=data, method="POST", headers={
|
|
"Cookie": cookie,
|
|
"HX-Request": "true",
|
|
"HX-Current-URL": f"{BASE}/repo/new",
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
})
|
|
try:
|
|
resp = urllib.request.urlopen(req, timeout=30)
|
|
body = resp.read().decode("utf-8", "replace")
|
|
loc = resp.headers.get("HX-Location") or resp.headers.get("HX-Redirect") or ""
|
|
except urllib.error.HTTPError as e:
|
|
sys.exit(f"create failed: HTTP {e.code}\n{e.read().decode('utf-8','replace')[:500]}")
|
|
|
|
if resp.status == 200 and loc:
|
|
print(f"OK: repo created -> {loc} (push to git@tangled.org:<owner>/{a.name})")
|
|
elif "already exists" in body.lower():
|
|
sys.exit(f"repo {a.name!r} already exists")
|
|
else:
|
|
sys.exit(f"unexpected response (status {resp.status}, no HX-Location). Body head:\n{body[:500]}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|