From 75779bdacbb2d87f7a191e606bff20b427958cb0 Mon Sep 17 00:00:00 2001 From: Patrick St-Louis Date: Mon, 26 Jan 2026 09:06:12 -0500 Subject: [PATCH 1/6] update acapy to 1.5.0rc1 Signed-off-by: Patrick St-Louis --- plugins/docker/Dockerfile | 4 ++-- plugins/traction_innkeeper/poetry.lock | 10 +++++----- plugins/traction_innkeeper/pyproject.toml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/docker/Dockerfile b/plugins/docker/Dockerfile index 62ed7c132..effbf8843 100644 --- a/plugins/docker/Dockerfile +++ b/plugins/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/openwallet-foundation/acapy-agent:py3.13-1.5.0rc0 AS base +FROM ghcr.io/openwallet-foundation/acapy-agent:py3.13-1.5.0rc1 AS base # Install and Configure Poetry USER root @@ -24,7 +24,7 @@ RUN poetry install --only main RUN ln -s $(poetry env info -p)/lib/python3.6/site-packages site-packages -FROM ghcr.io/openwallet-foundation/acapy-agent:py3.13-1.5.0rc0 +FROM ghcr.io/openwallet-foundation/acapy-agent:py3.13-1.5.0rc1 COPY --from=base --chown=aries:aries /home/aries/.venv /home/aries/.venv ENV PATH="/home/aries/.venv/bin:$PATH" diff --git a/plugins/traction_innkeeper/poetry.lock b/plugins/traction_innkeeper/poetry.lock index 6e462a2aa..e6e13d2a2 100644 --- a/plugins/traction_innkeeper/poetry.lock +++ b/plugins/traction_innkeeper/poetry.lock @@ -2,14 +2,14 @@ [[package]] name = "acapy-agent" -version = "1.5.0rc0" +version = "1.5.0rc1" description = "(ACA-Py) A Cloud Agent Python is a foundation for building decentralized identity applications and services running in non-mobile environments. " optional = false python-versions = "<4.0,>=3.13" groups = ["main"] files = [ - {file = "acapy_agent-1.5.0rc0-py3-none-any.whl", hash = "sha256:a6b8fcd822f77153876042fb4e771fb118bd9a2208efd5d4fd5c96f6e49583a0"}, - {file = "acapy_agent-1.5.0rc0.tar.gz", hash = "sha256:d8bffeb6ccae40879b59636478712a69e8ca1e7e6d5e8bd5f6102865442b7765"}, + {file = "acapy_agent-1.5.0rc1-py3-none-any.whl", hash = "sha256:cabd1d48e9a06d6533734f2ad2332cda29242606117281fd8e428220c7b48a01"}, + {file = "acapy_agent-1.5.0rc1.tar.gz", hash = "sha256:12134bf7300da497064a2cf0fe508c5d6fb40fcc728c875cae142a68d8a44f8c"}, ] [package.dependencies] @@ -49,7 +49,7 @@ requests = ">=2.32.3,<2.33.0" rlp = ">=4.1.0,<5.0.0" sd-jwt = ">=0.10.3,<0.11.0" unflatten = ">=0.2,<0.3" -uuid_utils = ">=0.10,<0.13" +uuid_utils = ">=0.10,<0.14" [package.extras] bbs = ["ursa-bbs-signatures (>=1.0.1,<1.1.0)"] @@ -3482,4 +3482,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "024aac753f331a472168a6728498439ac628acfbd558f57aba9fd64ae1a58551" +content-hash = "e435fe4ac0b8d979651bf42dd980ccf4763b2e83ded0d7ed8e275d580b8f4d20" diff --git a/plugins/traction_innkeeper/pyproject.toml b/plugins/traction_innkeeper/pyproject.toml index 1160c33f7..83fb358cb 100644 --- a/plugins/traction_innkeeper/pyproject.toml +++ b/plugins/traction_innkeeper/pyproject.toml @@ -14,7 +14,7 @@ packages = [{include = "traction_innkeeper"}] [tool.poetry.dependencies] python = "^3.13" -acapy-agent = { version = "1.5.0rc0" } +acapy-agent = { version = "1.5.0rc1" } python-dateutil = "^2.9.0" bcrypt = "^4.2.1" mergedeep = "^1.3.4" From 2a350998d8fa1a20c35c037bb9ac54394de54843 Mon Sep 17 00:00:00 2001 From: Patrick St-Louis Date: Tue, 27 Jan 2026 18:29:25 -0500 Subject: [PATCH 2/6] update revocation event sub Signed-off-by: Patrick St-Louis --- .../schema_storage/schema_storage_service.py | 31 ++--- .../v1_0/tenant/__init__.py | 10 ++ .../v1_0/tenant/issuer_revocation_service.py | 113 ++++++++++++++++++ .../components/connections/Connections.vue | 4 +- .../frontend/src/helpers/tableFormatters.ts | 14 ++- .../frontend/src/store/connectionStore.ts | 7 +- .../frontend/src/store/governanceStore.ts | 42 +------ .../frontend/src/store/issuerStore.ts | 104 ++++++---------- 8 files changed, 194 insertions(+), 131 deletions(-) create mode 100644 plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/issuer_revocation_service.py diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/schema_storage/schema_storage_service.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/schema_storage/schema_storage_service.py index 7ef28a2cf..b2103149c 100644 --- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/schema_storage/schema_storage_service.py +++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/schema_storage/schema_storage_service.py @@ -19,15 +19,10 @@ EVENT_LISTENER_PATTERN as INDY_SCHEMA_EVENT_PATTERN, ) -# Try to import ANONCREDS_SCHEMA_FINISHED_EVENT, but handle the case where it doesn't exist -# (e.g., if using an older version of acapy that doesn't have this event yet) -try: - from acapy_agent.anoncreds.events import ( - SCHEMA_FINISHED_EVENT as ANONCREDS_SCHEMA_FINISHED_EVENT, - ) -except (ImportError, AttributeError): - # If the event doesn't exist, we'll only subscribe to Indy events - ANONCREDS_SCHEMA_FINISHED_EVENT = None +# Import the AnonCreds schema finished event +from acapy_agent.anoncreds.events import ( + SCHEMA_FINISHED_EVENT as ANONCREDS_SCHEMA_FINISHED_EVENT, +) LOGGER = logging.getLogger(__name__) @@ -274,13 +269,12 @@ async def sync_created(self, profile: Profile): def subscribe(bus: EventBus): # Subscribe to Indy schema events bus.subscribe(INDY_SCHEMA_EVENT_PATTERN, schemas_event_handler) - # Subscribe to AnonCreds schema events if available - # Explicitly compile as literal pattern to ensure it's a Pattern object, not a string - if ANONCREDS_SCHEMA_FINISHED_EVENT: - bus.subscribe( - re.compile(re.escape(ANONCREDS_SCHEMA_FINISHED_EVENT)), - schemas_event_handler, - ) + # Subscribe to AnonCreds schema finished events + # Use exact match pattern - escape special chars and anchor to start/end + bus.subscribe( + re.compile(f"^{re.escape(ANONCREDS_SCHEMA_FINISHED_EVENT)}$"), + schemas_event_handler, + ) def _normalize_schema_event_payload(event: Event) -> dict: @@ -292,10 +286,7 @@ def _normalize_schema_event_payload(event: Event) -> dict: payload = event.payload # Check event topic to determine if it's AnonCreds or Indy - if ( - ANONCREDS_SCHEMA_FINISHED_EVENT - and event.topic == ANONCREDS_SCHEMA_FINISHED_EVENT - ): + if event.topic == ANONCREDS_SCHEMA_FINISHED_EVENT: # AnonCreds event: SchemaFinishedPayload NamedTuple if hasattr(payload, "schema_id"): return { diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/__init__.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/__init__.py index f75b104c1..69231269e 100644 --- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/__init__.py +++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/__init__.py @@ -6,6 +6,10 @@ from acapy_agent.core.protocol_registry import ProtocolRegistry from .holder_revocation_service import subscribe, HolderRevocationService +from .issuer_revocation_service import ( + subscribe as subscribe_issuer_revocation, + IssuerRevocationService, +) LOGGER = logging.getLogger(__name__) @@ -74,6 +78,12 @@ async def setup(context: InjectionContext): subscribe(bus) + # Subscribe to issuer revocation events + issuer_srv = IssuerRevocationService() + context.injector.bind_instance(IssuerRevocationService, issuer_srv) + + subscribe_issuer_revocation(bus) + setup_multitenant_logging() LOGGER.info("< plugin setup.") diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/issuer_revocation_service.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/issuer_revocation_service.py new file mode 100644 index 000000000..a182c22a8 --- /dev/null +++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/issuer_revocation_service.py @@ -0,0 +1,113 @@ +import logging +import re + +from acapy_agent.core.event_bus import EventBus, Event +from acapy_agent.core.profile import Profile +from acapy_agent.protocols.issue_credential.v2_0.models.cred_ex_record import ( + V20CredExRecord, +) +from acapy_agent.revocation.models.issuer_cred_rev_record import IssuerCredRevRecord +from acapy_agent.storage.error import StorageNotFoundError + +LOGGER = logging.getLogger(__name__) + +# Subscribe to the event that's actually emitted by IssuerCredRevRecord +# Format: acapy::record::issuer_cred_rev::revoked +ISSUER_CRED_REV_REVOKED_EVENT_PATTERN = re.compile( + r"^acapy::record::issuer_cred_rev::revoked$" +) + + +class IssuerRevocationService: + def __init__(self): + self._logger = logging.getLogger(__name__) + + async def update_credential_exchange_state( + self, profile: Profile, rev_rec: IssuerCredRevRecord + ) -> bool: + """Update credential exchange record state to credential-revoked. + + Args: + profile: The profile to use + rev_rec: The IssuerCredRevRecord that was revoked + + Returns: + bool: True if the state was updated, False otherwise + """ + self._logger.info( + f"> update_credential_exchange_state(cred_ex_id={rev_rec.cred_ex_id})" + ) + + if not rev_rec.cred_ex_id: + self._logger.warning( + "IssuerCredRevRecord has no cred_ex_id, cannot update credential exchange state" + ) + return False + + updated = False + try: + async with profile.transaction() as txn: + cred_ex_record = await V20CredExRecord.retrieve_by_id( + txn, rev_rec.cred_ex_id, for_update=True + ) + cred_ex_record.state = V20CredExRecord.STATE_CREDENTIAL_REVOKED + await cred_ex_record.save(txn, reason="revoke credential") + await txn.commit() + updated = True + self._logger.info( + f"Updated credential exchange {rev_rec.cred_ex_id} state to credential-revoked" + ) + except StorageNotFoundError: + self._logger.warning( + f"Credential exchange record not found for cred_ex_id: {rev_rec.cred_ex_id}" + ) + except Exception as err: + self._logger.error( + f"Error updating credential exchange state for cred_ex_id {rev_rec.cred_ex_id}", + exc_info=err, + ) + + self._logger.info( + f"< update_credential_exchange_state(cred_ex_id={rev_rec.cred_ex_id}): updated={updated}" + ) + return updated + + +def subscribe(bus: EventBus): + """Subscribe to issuer credential revocation events.""" + bus.subscribe(ISSUER_CRED_REV_REVOKED_EVENT_PATTERN, issuer_cred_rev_revoked_handler) + + +async def issuer_cred_rev_revoked_handler(profile: Profile, event: Event): + """Handle issuer credential revocation event. + + This handler is triggered when an IssuerCredRevRecord state changes to 'revoked'. + It updates the corresponding credential exchange record state to 'credential-revoked'. + """ + LOGGER.info( + f"> issuer_cred_rev_revoked_handler: topic={event.topic}, payload_type={type(event.payload)}" + ) + + try: + # The event payload is the serialized IssuerCredRevRecord (dict) + # Deserialize it to get the IssuerCredRevRecord object + LOGGER.debug(f"Event payload: {event.payload}") + rev_rec = IssuerCredRevRecord.deserialize(event.payload) + LOGGER.info( + f"Deserialized IssuerCredRevRecord: cred_ex_id={rev_rec.cred_ex_id}, state={rev_rec.state}" + ) + + # Only process if cred_ex_id is present + if rev_rec.cred_ex_id: + srv = profile.inject(IssuerRevocationService) + await srv.update_credential_exchange_state(profile, rev_rec) + else: + LOGGER.warning( + "IssuerCredRevRecord has no cred_ex_id, skipping credential exchange update" + ) + except Exception as err: + LOGGER.error( + "Error handling issuer credential revocation event", exc_info=err + ) + + LOGGER.info("< issuer_cred_rev_revoked_handler") diff --git a/services/tenant-ui/frontend/src/components/connections/Connections.vue b/services/tenant-ui/frontend/src/components/connections/Connections.vue index b43bba544..949bd883a 100644 --- a/services/tenant-ui/frontend/src/components/connections/Connections.vue +++ b/services/tenant-ui/frontend/src/components/connections/Connections.vue @@ -42,13 +42,13 @@