witbitz-code 1.11.0__tar.gz → 1.12.0__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.
Files changed (119) hide show
  1. {witbitz_code-1.11.0/src/witbitz_code.egg-info → witbitz_code-1.12.0}/PKG-INFO +1 -1
  2. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/pyproject.toml +1 -1
  3. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/__init__.py +1 -1
  4. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/codekeys.py +55 -7
  5. {witbitz_code-1.11.0 → witbitz_code-1.12.0/src/witbitz_code.egg-info}/PKG-INFO +1 -1
  6. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_codekeys.py +35 -0
  7. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/LICENSE +0 -0
  8. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/README.md +0 -0
  9. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/setup.cfg +0 -0
  10. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/__main__.py +0 -0
  11. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/_js.py +0 -0
  12. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/account.py +0 -0
  13. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/app.py +0 -0
  14. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/asks.py +0 -0
  15. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/attachments.py +0 -0
  16. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/auto.py +0 -0
  17. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/auto_runner.py +0 -0
  18. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/browse.py +0 -0
  19. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/browsetree.py +0 -0
  20. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/cli.py +0 -0
  21. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/codetools.py +0 -0
  22. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/compress.py +0 -0
  23. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/computers.py +0 -0
  24. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/confidential.py +0 -0
  25. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/confidential_models.json +0 -0
  26. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/connector.py +0 -0
  27. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/devicelink.py +0 -0
  28. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/dirs.py +0 -0
  29. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/folders.py +0 -0
  30. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/gateway.py +0 -0
  31. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
  32. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/icons/witbitz-code.ico +0 -0
  33. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/images.py +0 -0
  34. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/keys.py +0 -0
  35. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/link.py +0 -0
  36. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/mkdir.py +0 -0
  37. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/models.py +0 -0
  38. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/net.py +0 -0
  39. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/offered_models.json +0 -0
  40. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  41. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +0 -0
  42. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
  43. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +0 -0
  44. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +0 -0
  45. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/opencode_plugins/witbitz-browser.js +0 -0
  46. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  47. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  48. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  49. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/outputs.py +0 -0
  50. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/pair.py +0 -0
  51. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/pairings.py +0 -0
  52. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/payers.py +0 -0
  53. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/plugins.py +0 -0
  54. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/policy.py +0 -0
  55. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/ports.py +0 -0
  56. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/qr.py +0 -0
  57. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/receipt.py +0 -0
  58. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/relay.py +0 -0
  59. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/remind.py +0 -0
  60. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/seccheck.py +0 -0
  61. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/seen.py +0 -0
  62. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/selfupdate.py +0 -0
  63. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/tinfoil_read.py +0 -0
  64. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/tools_catalog.json +0 -0
  65. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/tools_probe.py +0 -0
  66. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/toolsetup.py +0 -0
  67. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/ui.py +0 -0
  68. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/ui_static/app.css +0 -0
  69. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/ui_static/app.js +0 -0
  70. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/ui_static/index.html +0 -0
  71. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/uitools.py +0 -0
  72. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/uninstall.py +0 -0
  73. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/winagent.py +0 -0
  74. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/winbg.py +0 -0
  75. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/winconsole.py +0 -0
  76. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/wizard.py +0 -0
  77. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code/wslstart.py +0 -0
  78. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  79. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  80. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  81. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code.egg-info/requires.txt +0 -0
  82. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/src/witbitz_code.egg-info/top_level.txt +0 -0
  83. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_asks.py +0 -0
  84. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_attachments.py +0 -0
  85. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_auto.py +0 -0
  86. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_browse.py +0 -0
  87. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_browsetree.py +0 -0
  88. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_cli.py +0 -0
  89. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_codetools.py +0 -0
  90. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_confidential.py +0 -0
  91. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_connector_e2e.py +0 -0
  92. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_devicelink.py +0 -0
  93. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_dirs.py +0 -0
  94. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_files_and_compress.py +0 -0
  95. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_folders.py +0 -0
  96. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_images.py +0 -0
  97. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_outputs.py +0 -0
  98. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_pair_flows.py +0 -0
  99. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_plugins.py +0 -0
  100. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_policy.py +0 -0
  101. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_ports.py +0 -0
  102. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_ports_win.py +0 -0
  103. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_registry.py +0 -0
  104. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_relay_codec.py +0 -0
  105. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_remind.py +0 -0
  106. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_seccheck.py +0 -0
  107. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_selfupdate.py +0 -0
  108. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_setup.py +0 -0
  109. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_tools_probe.py +0 -0
  110. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_toolsetup.py +0 -0
  111. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_ui.py +0 -0
  112. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_ui_page.py +0 -0
  113. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_uitools.py +0 -0
  114. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_uninstall.py +0 -0
  115. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_winagent.py +0 -0
  116. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_winbg.py +0 -0
  117. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_winconsole.py +0 -0
  118. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_windows_files.py +0 -0
  119. {witbitz_code-1.11.0 → witbitz_code-1.12.0}/tests/test_wslstart.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.11.0
3
+ Version: 1.12.0
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "witbitz-code"
7
- version = "1.11.0"
7
+ version = "1.12.0"
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"
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.11.0"
7
+ __version__ = "1.12.0"
@@ -26,6 +26,7 @@ from cryptography.hazmat.primitives.asymmetric import ec, ed25519
26
26
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
27
27
  from cryptography.hazmat.primitives.kdf.hkdf import HKDF
28
28
 
29
+ from . import app as app_mod
29
30
  from . import pairings as pp
30
31
 
31
32
  API = "https://api.witbitz.chat/v1/spaces/code-keys"
@@ -194,7 +195,7 @@ def sign_in_left_s(token: str, now: float | None = None) -> float:
194
195
 
195
196
 
196
197
  async def mint(client: httpx.AsyncClient, *, id_token: str, account: str, computer_id: str, name: str,
197
- ident: dict | None = None) -> dict:
198
+ ident: dict | None = None, app: str | None = None) -> dict:
198
199
  """Ask the account for this computer's keys → the entry to save. Raises CodeKeyRefusal with a sentence."""
199
200
  if not id_token:
200
201
  raise CodeKeyRefusal("not_signed_in", REFUSALS["not_signed_in"])
@@ -202,19 +203,28 @@ async def mint(client: httpx.AsyncClient, *, id_token: str, account: str, comput
202
203
  raise CodeKeyRefusal("signin_stale", REFUSALS["signin_stale"])
203
204
  ident = ident or new_identity()
204
205
  try:
206
+ # `app`: the Spaces app this pairing belongs to — each app keeps its own computers and allowance (the owner,
207
+ # 2026-10-23: "they should be completely separate"); the server files the keys under that app's tenant.
205
208
  r = await client.post(api_base(), json={"idToken": id_token, "computerId": computer_id, "name": name,
206
- "sealPub": seal_pub(ident), "signPub": sign_pub(ident)}, timeout=30)
209
+ "sealPub": seal_pub(ident), "signPub": sign_pub(ident), "app": app or app_mod.name()}, timeout=30)
207
210
  except httpx.HTTPError as e:
208
211
  raise CodeKeyRefusal("unreachable", f"could not reach Witbitz to get this computer's keys ({str(e)[:80]})") from e
209
212
  if r.status_code != 200:
210
213
  raise _refusal(r, account)
211
214
  d = (r.json() or {}).get("data") or {}
212
- entry = {"account": account, "computerId": computer_id, "name": name, "keyId": d.get("keyId"), "tenantId": d.get("tenantId"),
213
- "userId": d.get("userId"), "kid": d.get("kid"), **ident, "caps": d.get("caps") or {}, "at": int(time.time() * 1000)}
215
+ entry = {"account": account, "computerId": computer_id, "name": name, **ident, "app": app or app_mod.name(), "at": int(time.time() * 1000)}
216
+ return _take(entry, d)
217
+
218
+
219
+ def _take(entry: dict, d: dict) -> dict:
220
+ """A minted pair (a pairing, or a move to the computer's own app) into `entry`: its ids, caps and keys opened with the
221
+ computer's own identity. Raises CodeKeyRefusal when they cannot be opened."""
222
+ entry.update({"keyId": d.get("keyId"), "tenantId": d.get("tenantId"), "userId": d.get("userId"), "kid": d.get("kid"), "caps": d.get("caps") or {}})
223
+ entry.pop("tinfoil", None)
214
224
  sealed = d.get("sealed") or {}
215
225
  for provider in PROVIDERS:
216
226
  if sealed.get(provider):
217
- secret = open_sealed(sealed[provider], ident, aad(entry["tenantId"], entry["userId"], entry["kid"], provider))
227
+ secret = open_sealed(sealed[provider], entry, aad(entry["tenantId"], entry["userId"], entry["kid"], provider))
218
228
  if secret is None:
219
229
  raise CodeKeyRefusal("unsealable", "the keys Witbitz sent could not be opened on this computer — pair again")
220
230
  entry[provider] = secret
@@ -223,6 +233,18 @@ async def mint(client: httpx.AsyncClient, *, id_token: str, account: str, comput
223
233
  return entry
224
234
 
225
235
 
236
+ def app_of(entry: dict) -> str:
237
+ """The Spaces app these keys are for: recorded at the mint since 2026-10-23; before that, the app of the pairing they
238
+ belong to (its `origin`), else this install's."""
239
+ if entry.get("app") in (app_mod.REGULAR, app_mod.VAULTED):
240
+ return entry["app"]
241
+ try:
242
+ p = next((x for x in pp.read_pairings()["pairings"] if x.get("computerId") == entry.get("computerId")), None)
243
+ except Exception: # noqa: BLE001 — a reading; the install's own app is the answer then
244
+ p = None
245
+ return app_mod.of_origin(p.get("origin")) if p and p.get("origin") else app_mod.name()
246
+
247
+
226
248
  def _server_time(r: httpx.Response) -> float | None:
227
249
  try:
228
250
  return parsedate_to_datetime(r.headers.get("date") or "").timestamp()
@@ -230,14 +252,25 @@ def _server_time(r: httpx.Response) -> float | None:
230
252
  return None
231
253
 
232
254
 
255
+ def _status_body(entry: dict) -> dict:
256
+ """The status call, naming the app these keys are for and signing a move there ('move:<app>'): a computer the server has
257
+ under the other app's tenant moves to its own (every computer paired before 2026-10-23 was under the hosted app's)."""
258
+ b = signed_body("status", entry)
259
+ a = app_of(entry)
260
+ b["app"] = a
261
+ b["moveSig"] = _b64u(_sign_key(entry).sign(signed_message("move:" + a, entry["keyId"], b["ts"]).encode()))
262
+ return b
263
+
264
+
233
265
  async def _signed_post(client: httpx.AsyncClient, path: str, op: str, entry: dict, timeout: float = 10) -> httpx.Response:
234
266
  """A signed call; refused while this computer's clock is off the server's, it is signed again to the server's time."""
235
- r = await client.post(api_base() + path, json=signed_body(op, entry), timeout=timeout)
267
+ body = (lambda: _status_body(entry)) if op == "status" else (lambda: signed_body(op, entry))
268
+ r = await client.post(api_base() + path, json=body(), timeout=timeout)
236
269
  if r.status_code == 401:
237
270
  server = _server_time(r)
238
271
  if server is not None and abs(server - _now()) > SKEW_FIX_S:
239
272
  _clock["skew"] = server - time.time()
240
- r = await client.post(api_base() + path, json=signed_body(op, entry), timeout=timeout)
273
+ r = await client.post(api_base() + path, json=body(), timeout=timeout)
241
274
  return r
242
275
 
243
276
 
@@ -259,6 +292,19 @@ async def status(client: httpx.AsyncClient, entry: dict) -> dict:
259
292
  if r.status_code != 200:
260
293
  return {"live": None, "why": _why(r)} # unknown — keep going as before, and say why
261
294
  d = (r.json() or {}).get("data") or {}
295
+ moved = d.get("moved")
296
+ if isinstance(moved, dict) and moved.get("keyId"):
297
+ # MOVED TO ITS OWN APP: the server retired the old pair, so the new one is taken and SAVED here, whoever asked
298
+ # (serve's check, the page, `pair --status`) — keys left unsaved would be keys this computer no longer has.
299
+ try:
300
+ _take(entry, moved)
301
+ entry["app"] = app_of(entry)
302
+ save(entry)
303
+ d["changed"] = False # saved already
304
+ d["movedTo"] = entry["app"]
305
+ except CodeKeyRefusal as e:
306
+ return {"live": None, "why": str(e)}
307
+ return d
262
308
  tf = (d.get("sealed") or {}).get("tinfoil")
263
309
  if tf:
264
310
  secret = open_sealed(tf, entry, aad(entry["tenantId"], entry["userId"], entry["kid"], "tinfoil"))
@@ -354,6 +400,8 @@ class LiveKeys:
354
400
  say(f'witbitz-code: coding for {e.get("account")} is paused by Witbitz')
355
401
  self.refuse[cid] = PAUSED_TEXT
356
402
  elif st.get("live") is True:
403
+ if st.get("movedTo"):
404
+ say(f'witbitz-code: "{e.get("name")}" → {e.get("account")} now codes under the {app_mod.label(st["movedTo"])}\'s own allowance')
357
405
  if self.refuse.pop(cid, None):
358
406
  say(f'witbitz-code: coding for {e.get("account")} is back on')
359
407
  if st.get("changed"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.11.0
3
+ Version: 1.12.0
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -349,3 +349,38 @@ def test_the_python_and_node_connectors_read_each_others_keys_and_payers_files()
349
349
  pub.verify(codekeys._unb64u(out["body"]["sig"]), codekeys.signed_message("status", e["keyId"], 1800000000).encode()) # raises if not
350
350
  assert codekeys.for_computer("cmp_js")["trustedrouter"] == "sk-js" and SessionPayers().payer("ses_J") == "cmp_js"
351
351
  assert codekeys.signed_body("status", codekeys.for_computer("cmp_js"))["sig"], "Python signs with a Node-made key"
352
+
353
+
354
+ @requires_node
355
+ def test_a_status_that_names_its_app_moves_the_keys_there_and_saves_them(monkeypatch):
356
+ """The owner, 2026-10-23: each app its own computers and allowance. The status names this pairing's app and signs the
357
+ move ('move:<app>'); the server answers with a fresh pair under that app, which is opened and SAVED at once (the old one
358
+ is retired server-side, so unsaved keys would be lost)."""
359
+ from cryptography.hazmat.primitives.asymmetric import ed25519 as ed
360
+ T2, U2, K2 = "t_" + "c" * 32, "u_" + "d" * 24, "k_" + "2" * 24
361
+ ident = codekeys.new_identity()
362
+ entry = {**ident, "account": "me@x", "computerId": "cmp_1", "name": "desk", "keyId": f"{U}.{K}", "tenantId": T, "userId": U,
363
+ "kid": K, "trustedrouter": "sk-tr-old", "app": "regular"}
364
+ codekeys.save(entry)
365
+ sealed = run_node("import { sealForEnclave } from '@REPO@/control-plane/src/lib/enclaveKeySeal.mjs';"
366
+ "OUT({ trustedrouter: await sealForEnclave('sk-tr-new', { tenantId: INPUT.t, userId: INPUT.u, kind: `code:${INPUT.k}:trustedrouter` }, INPUT.pub) })"
367
+ .replace("@REPO@", (__import__("conftest").REPO).as_uri()),
368
+ {"t": T2, "u": U2, "k": K2, "pub": codekeys.seal_pub(ident)})
369
+ seen = []
370
+
371
+ def handler(req):
372
+ b = json.loads(req.content)
373
+ seen.append(b)
374
+ pub = ed.Ed25519PublicKey.from_public_bytes(base64.urlsafe_b64decode(codekeys.sign_pub(ident) + "="))
375
+ pub.verify(base64.urlsafe_b64decode(b["moveSig"] + "=" * (-len(b["moveSig"]) % 4)), codekeys.signed_message("move:regular", b["keyId"], b["ts"]).encode())
376
+ return httpx.Response(200, json={"data": {"live": True, "moved": {"keyId": f"{U2}.{K2}", "tenantId": T2, "userId": U2, "kid": K2, "sealed": sealed, "caps": {"modelsUsd": 50}}}})
377
+
378
+ async def go():
379
+ async with client_for(handler) as c:
380
+ return await codekeys.status(c, entry)
381
+ st = run(go())
382
+ assert seen[0]["app"] == "regular"
383
+ assert st["live"] is True and st["movedTo"] == "regular"
384
+ saved = codekeys.for_computer("cmp_1")
385
+ assert (saved["keyId"], saved["tenantId"], saved["trustedrouter"]) == (f"{U2}.{K2}", T2, "sk-tr-new")
386
+ assert entry["trustedrouter"] == "sk-tr-new", "the running serve's entry follows too"
File without changes
File without changes
File without changes