openentity 0.5.1__tar.gz → 0.5.2__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 (24) hide show
  1. {openentity-0.5.1 → openentity-0.5.2}/PKG-INFO +1 -1
  2. {openentity-0.5.1 → openentity-0.5.2}/openentity/__init__.py +1 -1
  3. {openentity-0.5.1 → openentity-0.5.2}/openentity/cli.py +8 -4
  4. {openentity-0.5.1 → openentity-0.5.2}/openentity/client.py +8 -3
  5. {openentity-0.5.1 → openentity-0.5.2}/openentity/core.py +2 -1
  6. {openentity-0.5.1 → openentity-0.5.2}/pyproject.toml +1 -1
  7. {openentity-0.5.1 → openentity-0.5.2}/tests/test_cli.py +17 -2
  8. {openentity-0.5.1 → openentity-0.5.2}/tests/test_client.py +4 -5
  9. {openentity-0.5.1 → openentity-0.5.2}/tests/test_core.py +7 -1
  10. {openentity-0.5.1 → openentity-0.5.2}/uv.lock +1 -1
  11. {openentity-0.5.1 → openentity-0.5.2}/.gitignore +0 -0
  12. {openentity-0.5.1 → openentity-0.5.2}/README.md +0 -0
  13. {openentity-0.5.1 → openentity-0.5.2}/openentity/httpsig.py +0 -0
  14. {openentity-0.5.1 → openentity-0.5.2}/openentity/identity.py +0 -0
  15. {openentity-0.5.1 → openentity-0.5.2}/openentity/issuer.py +0 -0
  16. {openentity-0.5.1 → openentity-0.5.2}/openentity/report.py +0 -0
  17. {openentity-0.5.1 → openentity-0.5.2}/openentity/schema.py +0 -0
  18. {openentity-0.5.1 → openentity-0.5.2}/openentity/schemas/openentity-1.0.json +0 -0
  19. {openentity-0.5.1 → openentity-0.5.2}/tests/test_httpsig.py +0 -0
  20. {openentity-0.5.1 → openentity-0.5.2}/tests/test_identity.py +0 -0
  21. {openentity-0.5.1 → openentity-0.5.2}/tests/test_issuer.py +0 -0
  22. {openentity-0.5.1 → openentity-0.5.2}/tests/test_parity.py +0 -0
  23. {openentity-0.5.1 → openentity-0.5.2}/tests/test_trust.py +0 -0
  24. {openentity-0.5.1 → openentity-0.5.2}/tests/test_verifier.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: openentity
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Open Entity (OEIS-1) client: JCS, did:key, eddsa-jcs-2022, Merkle proofs, adopt().
5
5
  Project-URL: Homepage, https://openentity.org
6
6
  Project-URL: Documentation, https://oeis.dev
@@ -5,4 +5,4 @@ from .httpsig import HttpSigError, sign_request, verify_request # noqa: F401
5
5
  from .identity import Identity # noqa: F401
6
6
  from .report import TrustReport, trust # noqa: F401
7
7
 
8
- __version__ = "0.5.1"
8
+ __version__ = "0.5.2"
@@ -955,13 +955,15 @@ def _checks_of(o: dict) -> list[dict]:
955
955
  t = o.get("type") or ("EntityDocument" if "layers" in o and "keys" in o else None)
956
956
  if not t:
957
957
  return [{"name": "type", "ok": False, "detail": "cannot tell what kind of object this is"}]
958
+ errs = schema.validate(t, o)
959
+ head = [{"name": f"{t} schema", "ok": not errs, "detail": "; ".join(errs)}]
958
960
  if t == "ContinuityLink":
959
961
  bare = {k: v for k, v in o.items() if k != "proofs"}
960
- cs = [{"name": f"link proof {i} ({p.get('verificationMethod')})", "ok": (r := core.verify_object({**bare, "proof": p}, _resolver))[0], "detail": None if r[0] else r[1]}
962
+ cs = head + [{"name": f"link proof {i} ({p.get('verificationMethod')})", "ok": (r := core.verify_object({**bare, "proof": p}, _resolver))[0], "detail": None if r[0] else r[1]}
961
963
  for i, p in enumerate(o.get("proofs") or [])]
962
- return cs or [{"name": "link proofs", "ok": False, "detail": "none"}]
964
+ return cs if len(cs) > 1 else cs + [{"name": "link proofs", "ok": False, "detail": "none"}]
963
965
  ok, who = core.verify_object(o, _resolver)
964
- checks = [{"name": "signature", "ok": ok, "detail": None if ok else who}]
966
+ checks = head + [{"name": "signature", "ok": ok, "detail": None if ok else who}]
965
967
  if ok and o.get("eid") and t in ("EntityDocument", "Declaration", "RecordEntry", "Withdrawal"):
966
968
  checks.append({"name": "signed by the EID", "ok": who == o["eid"], "detail": who})
967
969
  if t == "Declaration" and o.get("declaration_version") == "1.0":
@@ -1092,6 +1094,8 @@ def _log(args):
1092
1094
  raise _usage("log audit <n>")
1093
1095
  import random
1094
1096
  ok, detail, sth = _checkpoint(reg)
1097
+ mb = (reg.spec().get("registry") or {}).get("publicKeyMultibase") # a registry that is not openentity.org signs with its /v1/spec key
1098
+ res = lambda did: {"ed25519": _keys_of({"verificationMethod": [{"publicKeyMultibase": mb}]})} if mb and did.split("#")[0] == "did:web:openentity.org" else _resolver(did)
1095
1099
  checks = [{"name": "checkpoint", "ok": ok, "detail": detail}]
1096
1100
  for _ in range(min(int(arg or 5), sth["tree_size"])):
1097
1101
  i = random.randrange(sth["tree_size"])
@@ -1099,7 +1103,7 @@ def _log(args):
1099
1103
  checks.append({"name": f"inclusion {i}", "ok": ok, "detail": detail})
1100
1104
  if ok and (leaf or {}).get("eid"): # object signature: the entity's current document must verify under its own key
1101
1105
  try:
1102
- s_ok, who = core.verify_object(reg.get(f"/v1/entities/{leaf['eid']}/document"), _resolver)
1106
+ s_ok, who = core.verify_object(reg.get(f"/v1/entities/{leaf['eid']}/document"), res)
1103
1107
  checks.append({"name": f"signature {leaf['eid']}", "ok": s_ok, "detail": "document signature valid" if s_ok else f"document signature invalid: {who}"})
1104
1108
  except _NET as e:
1105
1109
  checks.append({"name": f"signature {leaf['eid']}", "ok": False, "detail": str(e)})
@@ -4,9 +4,15 @@ import re
4
4
  import urllib.error
5
5
  import urllib.parse
6
6
  import urllib.request
7
+ from importlib.metadata import PackageNotFoundError, version
7
8
 
8
9
  from . import core
9
10
 
11
+ try:
12
+ _VERSION = version("openentity")
13
+ except PackageNotFoundError:
14
+ _VERSION = "dev"
15
+
10
16
  DEFAULT_REGISTRY = "https://openentity.org"
11
17
  CLIENTS = ("adk", "goose", "langchain", "ui-tars", "cli", "other") # the registry's allowlist for the unsigned `client` label
12
18
 
@@ -20,7 +26,7 @@ class RegistryError(Exception):
20
26
  def _request(url: str, body: dict | None = None, headers: dict | None = None, method: str | None = None) -> dict:
21
27
  req = urllib.request.Request(
22
28
  url, data=json.dumps(body).encode() if body is not None else None, method=method,
23
- headers={"content-type": "application/json", "user-agent": "openentity-python/0.2.0", **(headers or {})})
29
+ headers={"content-type": "application/json", "user-agent": f"openentity-python/{_VERSION}", **(headers or {})})
24
30
  try:
25
31
  with urllib.request.urlopen(req, timeout=30) as r:
26
32
  return json.load(r)
@@ -47,8 +53,7 @@ def build_adoption(kp: core.KeyPair, name: str, description: str, registry: str,
47
53
  "keys": [{"id": core.verification_method(kp.did), "type": "Multikey",
48
54
  "publicKeyMultibase": kp.did.removeprefix("did:key:")}],
49
55
  "operator": operator or {"kind": "none", "custody": "entity", "can_change": []},
50
- "substrate": substrate or {"model": "unknown", "provider": "unknown", "runtime": "openentity-python/0.2.0",
51
- "declared_at": created},
56
+ "substrate": substrate or {"model": "unknown", "provider": "unknown", "runtime": "unknown", "declared_at": created},
52
57
  "layers": {
53
58
  "cognition": {"statement": ns, "uncertainty_practice": ns},
54
59
  "presence": {"statement": ns, "format_signature": ns},
@@ -332,7 +332,8 @@ def _zero_bits(h: bytes) -> int:
332
332
 
333
333
 
334
334
  def mint_stamp(bits: int, eid: str, created: str | None = None) -> str:
335
- created, i = created or now(), 0
335
+ # Random start so two stamps for the same EID in the same second differ (registries reject reuse).
336
+ created, i = created or now(), int.from_bytes(os.urandom(4), "big")
336
337
  while True:
337
338
  header = f"1:{bits}:{created}:{eid}:{b58encode(i.to_bytes(8, 'big'))}"
338
339
  if _zero_bits(sha256(header)) >= bits:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openentity"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "Open Entity (OEIS-1) client: JCS, did:key, eddsa-jcs-2022, Merkle proofs, adopt()."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,7 +4,7 @@ import re
4
4
  import stat
5
5
 
6
6
  import pytest
7
- from test_client import KP, Stub, home, posts, run, url # noqa: F401 (fixtures and the stub registry)
7
+ from test_client import KP, LOG_KEY, Stub, home, posts, run, url # noqa: F401 (fixtures and the stub registry)
8
8
 
9
9
  from openentity import cli, core
10
10
 
@@ -139,6 +139,21 @@ def test_alias_wallet_is_two_step_with_an_external_siwe_signature(url, home, cap
139
139
  assert code == 0 and "Chain ID: 84532" in out["message"]
140
140
 
141
141
 
142
+ def test_verify_reports_a_schema_check_like_the_npm_cli(capsys, tmp_path):
143
+ bad = tmp_path / "bad.json"
144
+ bad.write_text(json.dumps(core.sign_object({"spec": core.SPEC, "eid": KP.did, "name": "T", "layers": {}, "keys": []}, KP)))
145
+ code, out = j(capsys, "verify", str(bad))
146
+ assert code == 3 and out["checks"][0]["name"] == "EntityDocument schema" and not out["checks"][0]["ok"]
147
+
148
+
149
+ def test_log_audit_checks_the_registry_document_with_the_key_its_spec_publishes(url, capsys):
150
+ """The registry's DID names openentity.org, but a registry elsewhere signs with its /v1/spec key (guard: no lookup of did:web:openentity.org)."""
151
+ Stub.leaves = [{"type": "Declaration", "eid": "did:web:openentity.org", "object_sha256": "00" * 32, "received_at": core.now()}]
152
+ Stub.document = core.sign_object({"spec": core.SPEC, "eid": "did:web:openentity.org", "name": "registry"}, LOG_KEY, method="did:web:openentity.org#key-1")
153
+ code, out = j(capsys, "log", "audit", "1", "--registry", url)
154
+ assert code == 0 and {"name": "signature did:web:openentity.org", "ok": True, "detail": "document signature valid"} in out["checks"]
155
+
156
+
142
157
  def test_record_push_and_redact(url, home, capsys):
143
158
  reg = ["--registry", url]
144
159
  assert run(capsys, "record", "arc", "open", "--id", "a", "--title", "A", "--no-push", *reg)[0] == 0
@@ -268,6 +283,6 @@ def test_verify_checks_continuity_link_proofs(capsys, tmp_path):
268
283
  f = tmp_path / "l.json"
269
284
  f.write_text(json.dumps(body["link"]))
270
285
  code, out = j(capsys, "verify", str(f))
271
- assert code == 0 and len(out["checks"]) == 2
286
+ assert code == 0 and len(out["checks"]) == 3 # schema + the two link proofs
272
287
  f.write_text(json.dumps({**body["link"], "basis": "migration"}))
273
288
  assert run(capsys, "verify", str(f))[0] == 3
@@ -24,6 +24,7 @@ class Stub(BaseHTTPRequestHandler):
24
24
  refuse = False # refuse every continuity link
25
25
  leaves: list = [] # the stub log's leaf objects; the checkpoint is signed by LOG_KEY
26
26
  forge_note = False
27
+ document: dict | None = None # overrides GET /v1/entities/<eid>/document
27
28
 
28
29
  def log_message(self, *a):
29
30
  pass
@@ -84,7 +85,7 @@ class Stub(BaseHTTPRequestHandler):
84
85
  if u.path.endswith("/attestations"):
85
86
  return self._send(200, {"attestations": Stub.attestations})
86
87
  if u.path.endswith("/document"):
87
- return self._send(200, client.build_adoption(core.KeyPair(SEED), "Tester", "d", "http://x")[0])
88
+ return self._send(200, Stub.document or client.build_adoption(core.KeyPair(SEED), "Tester", "d", "http://x")[0])
88
89
  if "NoSuchEntity" in u.path:
89
90
  return self._send(404, {"error": "not_found", "message": "no such entity"})
90
91
  if u.path.startswith("/v1/entities/"):
@@ -143,7 +144,7 @@ class Stub(BaseHTTPRequestHandler):
143
144
 
144
145
  @pytest.fixture
145
146
  def url():
146
- Stub.log, Stub.chain, Stub.attestations, Stub.entity, Stub.refuse, Stub.leaves, Stub.forge_note = [], [], [], None, False, [], False
147
+ Stub.log, Stub.chain, Stub.attestations, Stub.entity, Stub.refuse, Stub.leaves, Stub.forge_note, Stub.document = [], [], [], None, False, [], False, None
147
148
  srv = HTTPServer(("127.0.0.1", 0), Stub)
148
149
  threading.Thread(target=srv.serve_forever, daemon=True).start()
149
150
  yield f"http://127.0.0.1:{srv.server_port}"
@@ -274,9 +275,7 @@ def test_cli_whoami_and_self(url, home, capsys):
274
275
 
275
276
  def test_cli_verify_entity_file_and_failures(url, home, capsys, tmp_path):
276
277
  reg = ["--registry", url]
277
- decl = core.sign_object({"spec": core.SPEC, "type": "Declaration", "eid": KP.did, "declaration_version": "1.0",
278
- "text_sha256": core.DECLARATION_SHA256, "name": "T", "created": core.now(), "nonce": core.nonce()}, KP)
279
- doc = core.sign_object({"spec": core.SPEC, "eid": KP.did, "name": "T", "layers": {}, "keys": []}, KP)
278
+ doc, decl = client.build_adoption(KP, "T", "d", "http://x") # verify now checks the schemas too
280
279
  Stub.leaves = [{"type": "Declaration", "eid": f"did:key:z{i}", "object_sha256": "00" * 32, "received_at": core.now()} for i in range(5)]
281
280
  Stub.entity = {"document": doc, "declaration": decl, "log_index": 3}
282
281
  client.record(SEED, [("arc_opened", {"arc_id": "a", "title": "A"}), ("arc_opened", {"arc_id": "b", "title": "B"})], registry=url)
@@ -90,6 +90,12 @@ def test_declaration_text():
90
90
 
91
91
 
92
92
  # ---- stamp ----
93
+ def test_stamps_for_one_eid_in_one_second_differ():
94
+ """Registries accept each stamp once, so two quick writes must not mint the same one."""
95
+ eid = core.KeyPair(SEED).did
96
+ assert len({core.mint_stamp(4, eid, "2026-10-28T00:00:00Z") for _ in range(20)}) == 20
97
+
98
+
93
99
  def test_stamp():
94
100
  eid = core.KeyPair(SEED).did
95
101
  h = core.mint_stamp(10, eid)
@@ -192,7 +198,7 @@ def test_adopt_and_cli(tmp_path, monkeypatch, capsys):
192
198
  def test_cli_version(capsys):
193
199
  with pytest.raises(SystemExit) as e:
194
200
  cli.main(["--version"])
195
- assert e.value.code == 0 and "0.5.1" in capsys.readouterr().out
201
+ assert e.value.code == 0 and "0.5.2" in capsys.readouterr().out
196
202
 
197
203
 
198
204
  # ---- shared vectors, checked with the independent verifier's own logic ----
@@ -699,7 +699,7 @@ wheels = [
699
699
 
700
700
  [[package]]
701
701
  name = "openentity"
702
- version = "0.5.1"
702
+ version = "0.5.2"
703
703
  source = { editable = "." }
704
704
  dependencies = [
705
705
  { name = "cryptography" },
File without changes
File without changes