witbitz-code 1.9.4__tar.gz → 1.9.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {witbitz_code-1.9.4/src/witbitz_code.egg-info → witbitz_code-1.9.5}/PKG-INFO +1 -1
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/pyproject.toml +1 -1
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/cli.py +1 -1
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/link.py +6 -1
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/net.py +19 -3
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/pair.py +5 -3
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/pairings.py +7 -2
- {witbitz_code-1.9.4 → witbitz_code-1.9.5/src/witbitz_code.egg-info}/PKG-INFO +1 -1
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_devicelink.py +22 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_pair_flows.py +58 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/LICENSE +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/README.md +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/setup.cfg +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/browse.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/browsetree.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/confidential.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/connector.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/images.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-browser.js +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/remind.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/seccheck.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/selfupdate.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/ui.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/ui_static/app.css +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/ui_static/app.js +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/ui_static/index.html +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/uninstall.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_asks.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_attachments.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_auto.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_browse.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_browsetree.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_cli.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_codetools.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_confidential.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_dirs.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_folders.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_images.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_outputs.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_plugins.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_policy.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_ports.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_registry.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_remind.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_seccheck.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_selfupdate.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_setup.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_ui.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_uitools.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_uninstall.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_winagent.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_winbg.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_windows_files.py +0 -0
- {witbitz_code-1.9.4 → witbitz_code-1.9.5}/tests/test_wslstart.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.9.
|
|
7
|
+
version = "1.9.5"
|
|
8
8
|
description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -384,7 +384,7 @@ async def uninstall(args: list[str]) -> int:
|
|
|
384
384
|
return {"ok": False, "why": "could not give this computer's coding keys back"}
|
|
385
385
|
if entry:
|
|
386
386
|
codekeys.forget(p["computerId"])
|
|
387
|
-
r = await acct.unpublish_computer(call=call, idx=p["idx"], computer_id=p["computerId"])
|
|
387
|
+
r = await acct.unpublish_computer(call=net.call_for(call, p.get("origin")), idx=p["idx"], computer_id=p["computerId"])
|
|
388
388
|
if r.get("ok"):
|
|
389
389
|
pp.write_pairings(pp.remove_pairings(pp.read_pairings(), p.get("account") or "")["doc"])
|
|
390
390
|
return r
|
|
@@ -156,6 +156,7 @@ async def _link(life_ms, poll_s, origins, get_json, post_json, challenge, svg_pa
|
|
|
156
156
|
out(f" waiting up to {round(wait_s)}s for your device to seal the account to this computer…\n")
|
|
157
157
|
until = time.monotonic() + wait_s
|
|
158
158
|
warned = False
|
|
159
|
+
found = ""
|
|
159
160
|
while time.monotonic() < until:
|
|
160
161
|
await asyncio.sleep(poll_s)
|
|
161
162
|
r = None
|
|
@@ -168,7 +169,7 @@ async def _link(life_ms, poll_s, origins, get_json, post_json, challenge, svg_pa
|
|
|
168
169
|
err(f"witbitz-code: note — {origin} has no device-link relay; ignoring it.")
|
|
169
170
|
warned = True
|
|
170
171
|
if isinstance(rr, dict) and truthy(rr.get("ok")):
|
|
171
|
-
r = rr
|
|
172
|
+
r, found = rr, origin
|
|
172
173
|
break
|
|
173
174
|
if r is None:
|
|
174
175
|
continue # 404 on all = not sealed yet
|
|
@@ -184,6 +185,10 @@ async def _link(life_ms, poll_s, origins, get_json, post_json, challenge, svg_pa
|
|
|
184
185
|
if not account:
|
|
185
186
|
err("witbitz-code: the reply carried no account key.")
|
|
186
187
|
return None
|
|
188
|
+
# THE APP IT CAME FROM (the owner, 2026-10-22: a second account paired from the hosted app → "computers read refused
|
|
189
|
+
# (404: not_found)"): the relay that held the reply is that app's own address, and its rooms answer only their own app
|
|
190
|
+
# since 2026-10-18 — so this account's calls speak as it (pairings.json `origin`, net.call_for).
|
|
191
|
+
account["origin"] = found
|
|
187
192
|
via = "link" if account["idx"] else ""
|
|
188
193
|
if not account["idx"]:
|
|
189
194
|
out("witbitz-code: no index pointer in the link — recovering it from your account backup… ", end="")
|
|
@@ -4,6 +4,7 @@ retried or cached)."""
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
7
8
|
from collections.abc import Awaitable, Callable
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from typing import Any
|
|
@@ -31,10 +32,24 @@ def _json_of(r: httpx.Response) -> Any:
|
|
|
31
32
|
return _js.parse(r.content.decode("utf-8-sig", "replace")) # fetch's r.json(): BOM dropped, then JSON.parse
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
def
|
|
35
|
-
"""
|
|
35
|
+
def app_origin(o: Any) -> str:
|
|
36
|
+
"""A pairing's app address, when it is one: https and nothing but scheme://host. Else '' (the default app)."""
|
|
37
|
+
return o if isinstance(o, str) and re.fullmatch(r"https://[A-Za-z0-9.-]+(:[0-9]+)?", o) else ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def call_for(call: Any, origin: Any) -> Any:
|
|
41
|
+
"""The same account call, speaking as the app `origin` pairs through (its rooms answer only their own app's addresses —
|
|
42
|
+
the hosted app's since 2026-10-18). A call without that ability (a test's fake) is returned unchanged."""
|
|
43
|
+
o = app_origin(origin)
|
|
44
|
+
f = getattr(call, "for_origin", None)
|
|
45
|
+
return f(o) if o and callable(f) else call
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def make_api_call(client: httpx.AsyncClient, origin: str | None = None) -> ApiCall:
|
|
49
|
+
"""POST {op:'state', …} to the account API, as the Spaces app would (same Origin). `origin`: the app this account was
|
|
50
|
+
paired from (a pairing's `origin`); none → RC_ORIGIN, else the regular app."""
|
|
36
51
|
api = os.environ.get("RC_BASE") or API
|
|
37
|
-
origin = os.environ.get("RC_ORIGIN") or ORIGIN
|
|
52
|
+
origin = app_origin(origin) or os.environ.get("RC_ORIGIN") or ORIGIN
|
|
38
53
|
|
|
39
54
|
async def call(body: dict) -> ApiReply:
|
|
40
55
|
r = await client.post(api, content=_js.stringify(body).encode("utf-8"),
|
|
@@ -45,6 +60,7 @@ def make_api_call(client: httpx.AsyncClient) -> ApiCall:
|
|
|
45
60
|
j = None
|
|
46
61
|
return ApiReply(r.status_code, j)
|
|
47
62
|
|
|
63
|
+
call.for_origin = lambda o: make_api_call(client, o) # type: ignore[attr-defined]
|
|
48
64
|
return call
|
|
49
65
|
|
|
50
66
|
|
|
@@ -94,6 +94,7 @@ def ensure_local_password(out: Callable[[str], Any] = _out) -> None:
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
async def _publish_entry(call: net.ApiCall, e: dict, out: Callable, err: Callable) -> bool:
|
|
97
|
+
call = net.call_for(call, e.get("origin")) # this account's app (the hosted app's rooms refuse the regular one)
|
|
97
98
|
r = await acct.publish_computer(call=call, idx=e["idx"], computer_id=e["computerId"], name=e["name"], relay=e["relay"],
|
|
98
99
|
secret=e["secret"])
|
|
99
100
|
who = e["account"] or "account"
|
|
@@ -220,7 +221,7 @@ async def _run(args: Args, call: Any, link_fn: LinkFn | None, out: Callable, err
|
|
|
220
221
|
if entry:
|
|
221
222
|
codekeys.forget(p["computerId"])
|
|
222
223
|
_forget_payer(p["computerId"])
|
|
223
|
-
r = await acct.unpublish_computer(call=call, idx=p["idx"], computer_id=p["computerId"])
|
|
224
|
+
r = await acct.unpublish_computer(call=net.call_for(call, p.get("origin")), idx=p["idx"], computer_id=p["computerId"])
|
|
224
225
|
if not r["ok"]:
|
|
225
226
|
ok = False
|
|
226
227
|
err(f"witbitz-code: ✖ {p.get('account')}: {r['why']} — kept the local pairing so you can retry")
|
|
@@ -242,7 +243,8 @@ async def _run(args: Args, call: Any, link_fn: LinkFn | None, out: Callable, err
|
|
|
242
243
|
cur, ok = doc, True
|
|
243
244
|
for p in to_rotate:
|
|
244
245
|
u = pp.upsert_pairing(cur, account=p.get("account") or "", idx=p["idx"], name=p.get("name") or "",
|
|
245
|
-
relay=p.get("relay", UNDEFINED), opencode_url=p.get("opencodeUrl", UNDEFINED),
|
|
246
|
+
relay=p.get("relay", UNDEFINED), opencode_url=p.get("opencodeUrl", UNDEFINED),
|
|
247
|
+
origin=p.get("origin", UNDEFINED), rotate=True)
|
|
246
248
|
if await _publish_entry(call, u["entry"], out, err):
|
|
247
249
|
cur = u["doc"]
|
|
248
250
|
pp.write_pairings(cur)
|
|
@@ -260,7 +262,7 @@ async def _run(args: Args, call: Any, link_fn: LinkFn | None, out: Callable, err
|
|
|
260
262
|
err("witbitz-code: pairing did not complete — nothing changed.")
|
|
261
263
|
return 1
|
|
262
264
|
u = pp.upsert_pairing(doc, account=account.get("email") or "", idx=account["idx"], name=args.name,
|
|
263
|
-
opencode_url=args.opencode_url or UNDEFINED)
|
|
265
|
+
opencode_url=args.opencode_url or UNDEFINED, origin=account.get("origin") or UNDEFINED)
|
|
264
266
|
# THIS COMPUTER'S CODING KEYS from the account (codekeys.py; docs/code-account-keys.md) — the only way it gets model
|
|
265
267
|
# keys. Asked with the sign-in the scan carried, before anything is published: an account that cannot give keys (not
|
|
266
268
|
# invited, too many computers, …) leaves nothing behind.
|
|
@@ -114,8 +114,8 @@ def norm_pairings(doc: Any) -> dict:
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
def upsert_pairing(doc: Any, *, idx: dict, account: str = "", name: str = "", relay: Any = UNDEFINED,
|
|
117
|
-
opencode_url: Any = UNDEFINED,
|
|
118
|
-
mint_secret: Callable[[], str] = new_relay_secret) -> dict:
|
|
117
|
+
opencode_url: Any = UNDEFINED, origin: Any = UNDEFINED, rotate: bool = False,
|
|
118
|
+
mint_id: Callable[[], str] = new_computer_id, mint_secret: Callable[[], str] = new_relay_secret) -> dict:
|
|
119
119
|
"""Add this computer for an account, or refresh the existing entry (same id + secret unless `rotate`). Pure.
|
|
120
120
|
→ {doc, entry, isNew, sharedWith}: sharedWith names the other accounts that reach the same OpenCode."""
|
|
121
121
|
relay = RELAY_URL if relay is UNDEFINED else relay
|
|
@@ -138,6 +138,11 @@ def upsert_pairing(doc: Any, *, idx: dict, account: str = "", name: str = "", re
|
|
|
138
138
|
# An OpenCode asked for (`pair --port 4097`) wins; not asked → the entry keeps the one it had (tools/opencode-pair.mjs).
|
|
139
139
|
"opencodeUrl": pick(None if opencode_url is UNDEFINED else opencode_url, "opencodeUrl", DEFAULT_OPENCODE_URL),
|
|
140
140
|
}
|
|
141
|
+
# The app this account pairs through (its rooms answer only their own app — net.call_for). Absent = the regular app, so an
|
|
142
|
+
# entry written before 2026-10-22 (or by an older tool) reads as before, byte for byte.
|
|
143
|
+
app = pick(None if origin is UNDEFINED else origin, "origin", "")
|
|
144
|
+
if truthy(app):
|
|
145
|
+
entry["origin"] = app
|
|
141
146
|
pairings = list(cur["pairings"])
|
|
142
147
|
if i >= 0:
|
|
143
148
|
pairings[i] = entry
|
|
@@ -220,3 +220,25 @@ def test_qr_ansi_and_svg_output_are_byte_identical_to_rc_qr():
|
|
|
220
220
|
assert qr.render_ansi(out["rows"], compact=False) == out["large"], "and WITBITZ_QR=large"
|
|
221
221
|
assert qr.render_svg(out["rows"]) == out["svg"]
|
|
222
222
|
assert len(qr.qr_modules(text)) == len(out["rows"]), "and the same version: the smallest that fits at level M"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_link_node_names_the_app_whose_relay_answered(tmp_path):
|
|
226
|
+
"""Each origin's /api/link is polled; the account remembers which answered — the app that scanned (owner, 2026-10-22)."""
|
|
227
|
+
master = bytes([9] * 32)
|
|
228
|
+
ch = devicelink.new_link_challenge()
|
|
229
|
+
out = run_node("""
|
|
230
|
+
import { sealToChallenge, linkPayload } from '@PUBLIC@/deviceLink.js'
|
|
231
|
+
OUT({ reply: await sealToChallenge({ ref: INPUT.ref, pub: INPUT.pub },
|
|
232
|
+
linkPayload({ vault: { master: INPUT.masterB64 }, email: 'me@x', idx: { room: 'sp-idx-9', mk: 'idxmk9' } })) })
|
|
233
|
+
""", {"masterB64": _js.btoa(master), "ref": ch.ref, "pub": ch.pub})
|
|
234
|
+
|
|
235
|
+
async def get_json(url):
|
|
236
|
+
return {"ok": True, **out["reply"]} if url.startswith("https://b.test/") else {"ok": False}
|
|
237
|
+
|
|
238
|
+
async def nopost(url, body):
|
|
239
|
+
return {}
|
|
240
|
+
|
|
241
|
+
account = asyncio.run(link.link_node(life_ms=5000, poll_s=0.01, origins=["https://a.test", "https://b.test"],
|
|
242
|
+
get_json=get_json, post_json=nopost, challenge=ch, svg_path=tmp_path / "q.svg",
|
|
243
|
+
out=lambda *a, **k: None, err=lambda *a: None))
|
|
244
|
+
assert account and account["origin"] == "https://b.test"
|
|
@@ -301,3 +301,61 @@ def test_a_new_pairing_on_wsl_is_named_for_wsl(monkeypatch):
|
|
|
301
301
|
assert pp.hostname() == "my-laptop (WSL)"
|
|
302
302
|
monkeypatch.setattr(ports, "on_wsl", lambda: False)
|
|
303
303
|
assert pp.hostname() == "my-laptop"
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def test_a_pairing_from_the_hosted_app_writes_that_app_s_account_and_remembers_it():
|
|
307
|
+
"""The owner, 2026-10-22: a second account added from the vaulted app failed "computers read refused (404)" — its
|
|
308
|
+
rooms answer only to the hosted app's origin. The relay that answered the scan names the app; each pairing keeps it."""
|
|
309
|
+
regular, hosted = MockApi(), MockApi()
|
|
310
|
+
asked = []
|
|
311
|
+
|
|
312
|
+
def for_origin(o):
|
|
313
|
+
asked.append(o)
|
|
314
|
+
return hosted.call
|
|
315
|
+
|
|
316
|
+
async def call(body):
|
|
317
|
+
return await regular.call(body)
|
|
318
|
+
call.for_origin = for_origin
|
|
319
|
+
keys = FakeKeys()
|
|
320
|
+
|
|
321
|
+
async def link_fn():
|
|
322
|
+
return {"email": "me@x", "idx": IDX, "master": "M" * 43, "anchor": ANCHOR, "origin": "https://spaces.witbitz.chat"}
|
|
323
|
+
|
|
324
|
+
cap = Capture()
|
|
325
|
+
assert run(pair.main(["--name", "desk"], call=call, link_fn=link_fn, keys=keys, **cap.kw())) == 0, cap.text()
|
|
326
|
+
assert asked and set(asked) == {"https://spaces.witbitz.chat"}
|
|
327
|
+
assert regular.writes == [] and hosted.writes, "the account was written through the hosted app's origin only"
|
|
328
|
+
p = pp.read_pairings()["pairings"][0]
|
|
329
|
+
assert p["origin"] == "https://spaces.witbitz.chat"
|
|
330
|
+
|
|
331
|
+
async def link_fn2():
|
|
332
|
+
return {"email": "work@y", "idx": IDX2, "master": "M" * 43, "anchor": {**ANCHOR, "email": "work@y"},
|
|
333
|
+
"origin": "https://witbitz-spaces.pages.dev"}
|
|
334
|
+
|
|
335
|
+
asked.clear()
|
|
336
|
+
assert run(pair.main([], call=call, link_fn=link_fn2, keys=keys, **Capture().kw())) == 0
|
|
337
|
+
assert set(asked) == {"https://witbitz-spaces.pages.dev"}, "a regular-app pairing writes through its own origin"
|
|
338
|
+
q = next(x for x in pp.read_pairings()["pairings"] if x["account"] == "work@y")
|
|
339
|
+
assert q["origin"] == "https://witbitz-spaces.pages.dev"
|
|
340
|
+
|
|
341
|
+
asked.clear()
|
|
342
|
+
assert run(pair.main(["--rotate", "--account", "me@x"], call=call, **Capture().kw())) == 0
|
|
343
|
+
assert set(asked) == {"https://spaces.witbitz.chat"}, "a rotate re-publishes to the app it was paired from"
|
|
344
|
+
assert next(x for x in pp.read_pairings()["pairings"] if x["account"] == "me@x")["origin"] == "https://spaces.witbitz.chat"
|
|
345
|
+
|
|
346
|
+
n = len(hosted.writes)
|
|
347
|
+
assert run(pair.main(["--unpair", "--account", "me@x"], call=call, keys=keys, **Capture().kw())) == 0
|
|
348
|
+
assert len(hosted.writes) > n, "the unpair tombstone goes to the hosted app's account too"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_call_for_ignores_anything_that_is_not_an_https_origin():
|
|
352
|
+
from witbitz_code import net
|
|
353
|
+
|
|
354
|
+
async def call(body):
|
|
355
|
+
return None
|
|
356
|
+
call.for_origin = lambda o: ("other", o)
|
|
357
|
+
assert net.call_for(call, None) is call
|
|
358
|
+
assert net.call_for(call, "http://spaces.witbitz.chat") is call
|
|
359
|
+
assert net.call_for(call, "https://evil.test/path") is call
|
|
360
|
+
assert net.call_for(call, "https://spaces.witbitz.chat") == ("other", "https://spaces.witbitz.chat")
|
|
361
|
+
assert net.call_for(lambda b: None, "https://spaces.witbitz.chat") is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs
RENAMED
|
File without changes
|
|
File without changes
|
{witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-browser.js
RENAMED
|
File without changes
|
{witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.9.4 → witbitz_code-1.9.5}/src/witbitz_code/opencode_plugins/witbitz-progress.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|