From 62f5c02536d3e8caf3a8f1cf3decbe06323efdc2 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Sat, 27 Jun 2026 17:19:41 -0600 Subject: [PATCH] feat(link): derive journal id + mark from CA public key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add solstone/think/link/mark.py — a pure, no-write derivation core that turns a journal CA's P-256 public-key SPKI into a stable journal id (jid, a 128-bit UUIDv8 via HKDF-SHA256) and a journal mark (two distinct icons with color + 45-degree rotation, two words) via argon2id over the jid bytes. Both are deterministic functions of the canonicalized key, so any peer that sees the public key re-derives the identical jid and mark. The recipe is a frozen cross-surface/cross-language contract; the suite pins four independently-computed reference vectors (covering all four rotation states and both pick_distinct branches) and asserts the jid, argon2 digest, and icon/color/word/rotation selections byte-for-byte, plus the pick_distinct off-by-one boundary, canonicalization + loud ValueError on wrong-curve/non-key/PEM/compressed input, asset length-guards, and the locked argon2 parameters. Adds argon2-cffi to the thin base dependencies (the think/link/ access surface) in lockstep with scripts/check_extras_consistency.py THIN_BASE. Derivation core only — no init/LinkState wiring, no UI, no persisted format change (later lodes). Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 2 + scripts/check_extras_consistency.py | 1 + solstone/think/link/mark.py | 248 ++++++++++++++++++ tests/link/test_mark.py | 383 ++++++++++++++++++++++++++++ uv.lock | 45 ++++ 5 files changed, 679 insertions(+) create mode 100644 solstone/think/link/mark.py create mode 100644 tests/link/test_mark.py diff --git a/pyproject.toml b/pyproject.toml index 27163b3ec..6fa6ca59d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,8 @@ dependencies = [ # handshake-time verify callback that stdlib ssl can't express. "cryptography>=42", "pyOpenSSL>=24.0", + # argon2id for the journal-mark work factor (think/link/mark.py). + "argon2-cffi", "websockets>=13.0", "psutil", "userpath>=1.9.2,<2", diff --git a/scripts/check_extras_consistency.py b/scripts/check_extras_consistency.py index c70d74093..cf46a61c4 100755 --- a/scripts/check_extras_consistency.py +++ b/scripts/check_extras_consistency.py @@ -40,6 +40,7 @@ THIN_BASE = { "timefhuman", "cryptography>=42", "pyOpenSSL>=24.0", + "argon2-cffi", "websockets>=13.0", "psutil", "userpath>=1.9.2,<2", diff --git a/solstone/think/link/mark.py b/solstone/think/link/mark.py new file mode 100644 index 000000000..11f477223 --- /dev/null +++ b/solstone/think/link/mark.py @@ -0,0 +1,248 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Journal jid and mark derivation for P-256 public-key SPKI bytes. + +This module implements the frozen journal identity contract. The jid and mark +are deterministic functions of a canonicalized P-256 public key, not of the +incoming byte encoding, and the module performs no writes or runtime caching. +""" + +from __future__ import annotations + +import json +import uuid +from dataclasses import dataclass +from importlib import resources +from types import MappingProxyType +from typing import Mapping + +import argon2.low_level +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.kdf.hkdf import HKDF + +_ICON_COUNT = 60 +_COLOR_COUNT = 16 +_WORD_COUNT = 7776 + +_JID_HKDF_SALT = b"solstone/journal/v1" +_JID_HKDF_INFO = b"solstone/jid/uuidv8/v1" +_MARK_ARGON2_SALT = b"solstone-journal-mark-v1" + + +@dataclass(frozen=True) +class MarkIcon: + """One icon selected by the journal-mark derivation contract.""" + + name: str + svg: str + color_name: str + color_hex: str + rot: int + + +@dataclass(frozen=True) +class Mark: + """Full journal mark selected from the frozen asset sets.""" + + icon1: MarkIcon + icon2: MarkIcon + words: tuple[str, str] + + def to_render_spec(self) -> dict[str, object]: + """Return the pinned renderer-facing dict shape for this mark.""" + return { + "icon1": { + "name": self.icon1.name, + "svg": self.icon1.svg, + "color": { + "name": self.icon1.color_name, + "hex": self.icon1.color_hex, + }, + "rot": self.icon1.rot, + }, + "icon2": { + "name": self.icon2.name, + "svg": self.icon2.svg, + "color": { + "name": self.icon2.color_name, + "hex": self.icon2.color_hex, + }, + "rot": self.icon2.rot, + }, + "words": [self.words[0], self.words[1]], + } + + +def _load_json_asset(filename: str) -> object: + asset = resources.files("solstone.think.link") / "mark_assets" / filename + return json.loads(asset.read_text(encoding="utf-8")) + + +def _load_glyphs() -> Mapping[str, str]: + data = _load_json_asset("glyphs.json") + if not isinstance(data, dict): + raise ValueError("mark asset glyphs.json must be a JSON object") + + glyphs: dict[str, str] = {} + for name, svg in data.items(): + if not isinstance(name, str) or not isinstance(svg, str): + raise ValueError("mark asset glyphs.json must map strings to strings") + glyphs[name] = svg + + if len(glyphs) != _ICON_COUNT: + raise ValueError( + f"mark asset glyphs.json must contain {_ICON_COUNT} icons; " + f"found {len(glyphs)}" + ) + return MappingProxyType(glyphs) + + +def _load_colors() -> tuple[tuple[str, str], ...]: + data = _load_json_asset("colors.json") + if not isinstance(data, list): + raise ValueError("mark asset colors.json must be a JSON list") + + colors: list[tuple[str, str]] = [] + for item in data: + if ( + not isinstance(item, list) + or len(item) != 2 + or not isinstance(item[0], str) + or not isinstance(item[1], str) + ): + raise ValueError("mark asset colors.json must contain [name, hex] pairs") + colors.append((item[0], item[1])) + + if len(colors) != _COLOR_COUNT: + raise ValueError( + f"mark asset colors.json must contain {_COLOR_COUNT} colors; " + f"found {len(colors)}" + ) + return tuple(colors) + + +def _load_words() -> tuple[str, ...]: + data = _load_json_asset("words.json") + if not isinstance(data, list): + raise ValueError("mark asset words.json must be a JSON list") + + words: list[str] = [] + for word in data: + if not isinstance(word, str): + raise ValueError("mark asset words.json must contain only strings") + words.append(word) + + if len(words) != _WORD_COUNT: + raise ValueError( + f"mark asset words.json must contain {_WORD_COUNT} words; " + f"found {len(words)}" + ) + return tuple(words) + + +_GLYPHS = _load_glyphs() +_ICON_NAMES = tuple(_GLYPHS) +_COLORS = _load_colors() +_WORDS = _load_words() + + +def pick(w: int, n: int) -> int: + """Select an index from an unsigned word by modulo reduction.""" + return w % n + + +def pick_distinct(w: int, n: int, not_i: int) -> int: + """Select an index from ``n`` values while excluding ``not_i``.""" + i = w % (n - 1) + if i >= not_i: + i += 1 + return i + + +def jid_from_spki(spki_der: bytes) -> uuid.UUID: + """Derive the journal UUIDv8 from a P-256 public-key SPKI. + + The input must be DER-encoded SubjectPublicKeyInfo for an EC P-256 public + key. The key is loaded and re-serialized before HKDF derivation so the jid + is a function of the key, not the exact incoming byte encoding. + """ + key = serialization.load_der_public_key(spki_der) + if not isinstance(key, ec.EllipticCurvePublicKey): + raise ValueError("journal jid requires an EC P-256 public-key SPKI") + if not isinstance(key.curve, ec.SECP256R1): + raise ValueError("journal jid requires an EC P-256 public-key SPKI") + + ikm = key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + raw16 = HKDF( + algorithm=hashes.SHA256(), + length=16, + salt=_JID_HKDF_SALT, + info=_JID_HKDF_INFO, + ).derive(ikm) + + b = bytearray(raw16) + b[6] = (b[6] & 0x0F) | 0x80 + b[8] = (b[8] & 0x3F) | 0x80 + return uuid.UUID(bytes=bytes(b)) + + +def mark_from_jid(jid: uuid.UUID) -> Mark: + """Derive the journal mark from a journal UUIDv8.""" + digest = argon2.low_level.hash_secret_raw( + secret=jid.bytes, + salt=_MARK_ARGON2_SALT, + time_cost=3, + memory_cost=65536, + parallelism=1, + hash_len=32, + type=argon2.low_level.Type.ID, + version=0x13, + ) + u = [int.from_bytes(digest[i * 4 : i * 4 + 4], "big") for i in range(7)] + + icon1_i = pick(u[0], _ICON_COUNT) + icon2_i = pick_distinct(u[1], _ICON_COUNT, icon1_i) + color1_i = pick(u[2], _COLOR_COUNT) + color2_i = pick_distinct(u[3], _COLOR_COUNT, color1_i) + word1_i = pick(u[4], _WORD_COUNT) + word2_i = pick_distinct(u[5], _WORD_COUNT, word1_i) + rot1 = u[6] & 1 + rot2 = (u[6] >> 1) & 1 + + icon1_name = _ICON_NAMES[icon1_i] + icon2_name = _ICON_NAMES[icon2_i] + color1_name, color1_hex = _COLORS[color1_i] + color2_name, color2_hex = _COLORS[color2_i] + + return Mark( + icon1=MarkIcon( + name=icon1_name, + svg=_GLYPHS[icon1_name], + color_name=color1_name, + color_hex=color1_hex, + rot=45 if rot1 else 0, + ), + icon2=MarkIcon( + name=icon2_name, + svg=_GLYPHS[icon2_name], + color_name=color2_name, + color_hex=color2_hex, + rot=45 if rot2 else 0, + ), + words=(_WORDS[word1_i], _WORDS[word2_i]), + ) + + +def mark_from_spki(spki_der: bytes) -> Mark: + """Derive the journal mark from a P-256 public-key SPKI. + + The input must be DER-encoded SubjectPublicKeyInfo for an EC P-256 public + key. The key is canonicalized through cryptography before jid derivation, + so the mark is a function of the key, not the exact incoming byte encoding. + """ + return mark_from_jid(jid_from_spki(spki_der)) diff --git a/tests/link/test_mark.py b/tests/link/test_mark.py new file mode 100644 index 000000000..1aed0b115 --- /dev/null +++ b/tests/link/test_mark.py @@ -0,0 +1,383 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Tests for journal jid and mark derivation.""" + +from __future__ import annotations + +import json +import uuid +from dataclasses import dataclass +from importlib import resources +from typing import Any + +import argon2.low_level +import pytest +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.hazmat.primitives.kdf.hkdf import HKDF + +from solstone.think.link import mark as mark_module +from solstone.think.link.mark import ( + Mark, + jid_from_spki, + mark_from_jid, + mark_from_spki, + pick_distinct, +) + + +@dataclass(frozen=True) +class ReferenceVector: + name: str + spki_hex: str + jid: str + digest: str + icon1: str + color1: str + color1_hex: str + icon2: str + color2: str + color2_hex: str + word1: str + word2: str + rot1: int + rot2: int + raw16: str | None = None + + +VECTORS = ( + ReferenceVector( + name="primary", + spki_hex="3059301306072a8648ce3d020106082a8648ce3d03010703420004471c3e758c4904285bba7e53118ed0f524adeb0757d25bd2f8e7b0d76dfa714cdd520f7aca8a8b917acc37f51de8f0c9bbe3ad858382e702dc25a12d09f7a858", + raw16="f30ed159ef466e9c113fe49f0fe7d201", + jid="f30ed159-ef46-8e9c-913f-e49f0fe7d201", + digest="4e436f135100f0ecc94146f99ac603b5e31161ed90774e499770618925543fc2", + icon1="piano", + color1="blue", + color1_hex="#3b82f6", + icon2="key", + color2="purple", + color2_hex="#a855f7", + word1="liquefy", + word2="smock", + rot1=45, + rot2=0, + ), + ReferenceVector( + name="rot-0-0", + spki_hex="3059301306072a8648ce3d020106082a8648ce3d030107034200047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d1", + jid="62bde3af-1ef4-8292-84db-1e5ac2c07e8b", + digest="15b2e261a06544e78334d07e0e6505d9e657a22193b46221b8e7e9ec6b897f50", + icon1="turtle", + color1="pink", + color1_hex="#ec4899", + icon2="pizza", + color2="cyan", + color2_hex="#06b6d4", + word1="distrust", + word2="chokehold", + rot1=0, + rot2=0, + ), + ReferenceVector( + name="rot-0-1", + spki_hex="3059301306072a8648ce3d020106082a8648ce3d030107034200048e533b6fa0bf7b4625bb30667c01fb607ef9f8b8a80fef5b300628703187b2a373eb1dbde03318366d069f83a6f5900053c73633cb041b21c55e1a86c1f400b4", + jid="75e46c0d-1c50-892b-98ff-4d174c135add", + digest="92703a37b827760c681472dd01e1bc86b9003a1f8e0174215837caba1b5ee2a3", + icon1="dice-5", + color1="magenta", + color1_hex="#d946ef", + icon2="snail", + color2="sky", + color2_hex="#38bdf8", + word1="delay", + word2="safari", + rot1=0, + rot2=45, + ), + ReferenceVector( + name="rot-1-1", + spki_hex="3059301306072a8648ce3d020106082a8648ce3d03010703420004ea68d7b6fedf0b71878938d51d71f8729e0acb8c2c6df8b3d79e8a4b90949ee02a2744c972c9fce787014a964a8ea0c84d714feaa4de823fe85a224a4dd048fa", + jid="bb8f23b4-fd5e-8ca9-98c7-c1dfa927a840", + digest="00d3c5bc17e50c956badedd1a3c02788ac2990261b885c4dabdd7cbf9506a05c", + icon1="truck", + color1="orange", + color1_hex="#f97316", + icon2="snail", + color2="teal", + color2_hex="#14b8a6", + word1="duvet", + word2="capital", + rot1=45, + rot2=45, + ), +) + + +def _spki_der(vector: ReferenceVector) -> bytes: + return bytes.fromhex(vector.spki_hex) + + +def _public_spki(public_key: object, encoding: serialization.Encoding) -> bytes: + if not hasattr(public_key, "public_bytes"): + raise TypeError("public key object must support public_bytes") + return public_key.public_bytes( # type: ignore[attr-defined] + encoding, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + +def _p256_spki() -> bytes: + return _public_spki( + ec.generate_private_key(ec.SECP256R1()).public_key(), + serialization.Encoding.DER, + ) + + +def _raw16_from_spki(spki_der: bytes) -> bytes: + key = serialization.load_der_public_key(spki_der) + ikm = key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + return HKDF( + algorithm=hashes.SHA256(), + length=16, + salt=b"solstone/journal/v1", + info=b"solstone/jid/uuidv8/v1", + ).derive(ikm) + + +def _argon2_digest_hex(jid: uuid.UUID) -> str: + return argon2.low_level.hash_secret_raw( + secret=jid.bytes, + salt=b"solstone-journal-mark-v1", + time_cost=3, + memory_cost=65536, + parallelism=1, + hash_len=32, + type=argon2.low_level.Type.ID, + version=0x13, + ).hex() + + +def _assert_mark_matches(mark: Mark, vector: ReferenceVector) -> None: + assert mark.icon1.name == vector.icon1 + assert mark.icon1.color_name == vector.color1 + assert mark.icon1.color_hex == vector.color1_hex + assert mark.icon1.rot == vector.rot1 + assert mark.icon2.name == vector.icon2 + assert mark.icon2.color_name == vector.color2 + assert mark.icon2.color_hex == vector.color2_hex + assert mark.icon2.rot == vector.rot2 + assert mark.words == (vector.word1, vector.word2) + + +def _load_asset(filename: str) -> Any: + asset = resources.files("solstone.think.link") / "mark_assets" / filename + return json.loads(asset.read_text(encoding="utf-8")) + + +@pytest.mark.parametrize("vector", VECTORS, ids=[vector.name for vector in VECTORS]) +def test_jid_from_spki_matches_reference_vectors(vector: ReferenceVector) -> None: + jid = jid_from_spki(_spki_der(vector)) + + assert jid == uuid.UUID(vector.jid) + assert jid.version == 8 + assert (jid.bytes[8] >> 6) == 0b10 + + if vector.raw16 is not None: + assert _raw16_from_spki(_spki_der(vector)).hex() == vector.raw16 + assert jid.bytes.hex() == "f30ed159ef468e9c913fe49f0fe7d201" + + +@pytest.mark.parametrize("vector", VECTORS, ids=[vector.name for vector in VECTORS]) +def test_mark_from_spki_matches_reference_vectors(vector: ReferenceVector) -> None: + jid = uuid.UUID(vector.jid) + + assert _argon2_digest_hex(jid) == vector.digest + + mark_from_key = mark_from_spki(_spki_der(vector)) + mark_from_identity = mark_from_jid(jid) + + _assert_mark_matches(mark_from_key, vector) + _assert_mark_matches(mark_from_identity, vector) + assert mark_from_key == mark_from_identity + + +def test_primary_render_spec_matches_pinned_shape() -> None: + vector = VECTORS[0] + glyphs = _load_asset("glyphs.json") + mark = mark_from_spki(_spki_der(vector)) + + assert mark.to_render_spec() == { + "icon1": { + "name": "piano", + "svg": glyphs["piano"], + "color": {"name": "blue", "hex": "#3b82f6"}, + "rot": 45, + }, + "icon2": { + "name": "key", + "svg": glyphs["key"], + "color": {"name": "purple", "hex": "#a855f7"}, + "rot": 0, + }, + "words": ["liquefy", "smock"], + } + + +@pytest.mark.parametrize("n", [60, 16, 7776]) +def test_pick_distinct_boundary_branches(n: int) -> None: + not_i = n // 2 + cases = ( + (not_i - 1, not_i - 1), + (not_i + 1, not_i + 2), + (not_i, not_i + 1), + ) + + for w, expected in cases: + result = pick_distinct(w, n, not_i) + assert result == expected + assert result != not_i + assert result < n + + +@pytest.mark.parametrize("vector", VECTORS, ids=[vector.name for vector in VECTORS]) +def test_reference_vectors_have_distinct_mark_parts(vector: ReferenceVector) -> None: + mark = mark_from_spki(_spki_der(vector)) + + assert mark.icon2.name != mark.icon1.name + assert mark.icon2.color_name != mark.icon1.color_name + assert mark.words[1] != mark.words[0] + + +def test_fresh_p256_keys_have_distinct_mark_parts() -> None: + for _ in range(3): + mark = mark_from_spki(_p256_spki()) + + assert mark.icon2.name != mark.icon1.name + assert mark.icon2.color_name != mark.icon1.color_name + assert mark.words[1] != mark.words[0] + + +def test_wrong_inputs_raise_value_error() -> None: + p256_public = ec.generate_private_key(ec.SECP256R1()).public_key() + invalid_inputs = ( + _public_spki( + rsa.generate_private_key(public_exponent=65537, key_size=2048).public_key(), + serialization.Encoding.DER, + ), + _public_spki( + ec.generate_private_key(ec.SECP384R1()).public_key(), + serialization.Encoding.DER, + ), + p256_public.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.CompressedPoint, + ), + _public_spki(p256_public, serialization.Encoding.PEM), + b"not a key", + ) + + for invalid in invalid_inputs: + with pytest.raises(ValueError): + jid_from_spki(invalid) + with pytest.raises(ValueError): + mark_from_spki(invalid) + + +def test_p256_spki_canonicalization_is_stable() -> None: + original_spki = _p256_spki() + key = serialization.load_der_public_key(original_spki) + reserialized_spki = key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + assert jid_from_spki(original_spki) == jid_from_spki(reserialized_spki) + assert mark_from_spki(original_spki) == mark_from_spki(reserialized_spki) + + +def test_same_key_is_deterministic() -> None: + spki_der = _p256_spki() + + assert jid_from_spki(spki_der) == jid_from_spki(spki_der) + assert jid_from_spki(spki_der) == jid_from_spki(spki_der) + assert mark_from_spki(spki_der) == mark_from_spki(spki_der) + assert mark_from_spki(spki_der) == mark_from_spki(spki_der) + + +@pytest.mark.parametrize("vector", VECTORS, ids=[vector.name for vector in VECTORS]) +def test_uuid_round_trips_wire_shape(vector: ReferenceVector) -> None: + jid = jid_from_spki(_spki_der(vector)) + + assert uuid.UUID(bytes=jid.bytes) == jid + assert uuid.UUID(str(jid)).bytes == jid.bytes + + +def test_mark_assets_load_from_package_path_with_expected_counts() -> None: + glyphs = _load_asset("glyphs.json") + colors = _load_asset("colors.json") + words = _load_asset("words.json") + + assert len(glyphs) == 60 + assert len(colors) == 16 + assert len(words) == 7776 + + +def test_asset_loaders_raise_on_wrong_counts(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + mark_module, + "_load_json_asset", + lambda _filename: {"only": ""}, + ) + with pytest.raises(ValueError): + mark_module._load_glyphs() + + monkeypatch.setattr( + mark_module, + "_load_json_asset", + lambda _filename: [["crimson", "#ef4444"]], + ) + with pytest.raises(ValueError): + mark_module._load_colors() + + monkeypatch.setattr( + mark_module, + "_load_json_asset", + lambda _filename: ["abacus"], + ) + with pytest.raises(ValueError): + mark_module._load_words() + + +def test_mark_from_jid_uses_locked_argon2_parameters( + monkeypatch: pytest.MonkeyPatch, +) -> None: + jid = uuid.UUID(VECTORS[0].jid) + captured: dict[str, object] = {} + + def fake_hash_secret_raw(**kwargs: object) -> bytes: + captured.update(kwargs) + return bytes(32) + + monkeypatch.setattr( + mark_module.argon2.low_level, + "hash_secret_raw", + fake_hash_secret_raw, + ) + + mark_from_jid(jid) + + assert captured == { + "secret": jid.bytes, + "salt": b"solstone-journal-mark-v1", + "time_cost": 3, + "memory_cost": 65536, + "parallelism": 1, + "hash_len": 32, + "type": argon2.low_level.Type.ID, + "version": 0x13, + } diff --git a/uv.lock b/uv.lock index f44e2d3e8..fe5c54485 100644 --- a/uv.lock +++ b/uv.lock @@ -212,6 +212,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + [[package]] name = "asgiref" version = "3.11.1" @@ -4225,6 +4268,7 @@ name = "solstone" version = "0.6.15" source = { editable = "." } dependencies = [ + { name = "argon2-cffi" }, { name = "cryptography" }, { name = "psutil" }, { name = "pyopenssl" }, @@ -4336,6 +4380,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "anthropic", marker = "extra == 'journal-host'" }, + { name = "argon2-cffi" }, { name = "av", marker = "extra == 'journal-host'" }, { name = "blessed", marker = "extra == 'journal-host'", specifier = ">=1.20.0" }, { name = "cryptography", specifier = ">=42" }, -- 2.51.2