Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -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.0 AS base

# Install and Configure Poetry
USER root
Expand All @@ -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.0
COPY --from=base --chown=aries:aries /home/aries/.venv /home/aries/.venv
ENV PATH="/home/aries/.venv/bin:$PATH"

Expand Down
12 changes: 6 additions & 6 deletions plugins/traction_innkeeper/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugins/traction_innkeeper/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.0" }
Comment on lines 16 to +17
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title is focused on a UI revocation badge fix, but this change also upgrades acapy-agent from an RC to 1.5.0 (plus corresponding Docker image/lockfile changes) and adjusts schema event subscription behavior. If these are required for the badge fix, it would help to mention that in the PR description; otherwise consider splitting the dependency/runtime upgrade into a separate PR to keep scope and rollback surface smaller.

Copilot uses AI. Check for mistakes.
python-dateutil = "^2.9.0"
bcrypt = "^4.2.1"
mergedeep = "^1.3.4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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,
)
Comment on lines 269 to +277
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subscribe() now unconditionally adds a second subscription for ANONCREDS_SCHEMA_FINISHED_EVENT with a newly constructed anchored regex. There are existing unit tests for subscribe(), but they only assert the Indy subscription and would not catch regressions in the AnonCreds subscription (e.g., wrong pattern/handler). Consider extending test_subscribe to assert that the event bus is also subscribed for the AnonCreds schema-finished topic with the expected handler.

Copilot uses AI. Check for mistakes.


def _normalize_schema_event_payload(event: Event) -> dict:
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@
<template #body="{ data }">
<MessageConnection
:connection-id="data.connection_id"
:connection-name="data.alias"
:connection-name="data.alias || data.their_label || ''"
/>
<Button
title="Delete Connection"
icon="pi pi-trash"
class="p-button-rounded p-button-icon-only p-button-text"
:disabled="deleteDisabled(data.alias)"
:disabled="deleteDisabled(data.alias || data.their_label || '')"
@click="deleteConnection($event, data.connection_id)"
/>
<EditConnection :connection-id="data.connection_id" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,7 @@
<script setup lang="ts">
// Types
import { FormattedIssuedCredentialRecord } from '@/helpers/tableFormatters';
import {
RevokeRequest,
V20CredExRecordDetail,
} from '@/types/acapyApi/acapyInterface';
import { RevokeRequest } from '@/types/acapyApi/acapyInterface';

// Vue/State
import { reactive, ref } from 'vue';
Expand Down Expand Up @@ -109,52 +106,22 @@ const handleSubmit = async (isFormValid: boolean) => {
}

try {
// Use cred_ex_id if available (preferred), otherwise use rev_reg_id and cred_rev_id
const payload: RevokeRequest = {
comment: formFields.comment,
connection_id: props.credExchRecord.connection_id,
rev_reg_id: props.credExchRecord.rev_reg_id,
cred_rev_id: props.credExchRecord.cred_rev_id,
...(props.credExchRecord.credential_exchange_id
? { cred_ex_id: props.credExchRecord.credential_exchange_id }
: {
rev_reg_id: props.credExchRecord.rev_reg_id,
cred_rev_id: props.credExchRecord.cred_rev_id,
}),
publish: true,
notify: true,
};

// Determine if this is an AnonCreds credential by checking if anoncreds/indy fields exist
// FormattedIssuedCredentialRecord extends V20CredExRecordDetail which has indy/anoncreds fields
// If only anoncreds exists, it's an AnonCreds credential; if only indy exists, it's Indy
// If both or neither exist, pass undefined to let the store determine based on wallet type
const credRecord = props.credExchRecord as V20CredExRecordDetail;
const hasAnoncreds = !!credRecord.anoncreds;
const hasIndy = !!credRecord.indy;
const isAnonCredsCredential =
hasAnoncreds && !hasIndy
? true
: hasIndy && !hasAnoncreds
? false
: undefined;

console.log('Credential format detection:', {
hasAnoncreds,
hasIndy,
isAnonCredsCredential,
});

// Warn if credential format can't be detected - revocation might fail silently
if (isAnonCredsCredential === undefined) {
toast.warning(
'Credential format could not be detected. Please verify the revocation succeeded by checking the credential list.'
);
}

await issuerStore.revokeCredential(payload, isAnonCredsCredential);

// If format couldn't be detected, show a warning message instead of success
if (isAnonCredsCredential === undefined) {
toast.warning(
'Revocation request sent. Please verify the credential was actually revoked in the credential list.'
);
emit('closed');
return;
}
// The store now determines the endpoint based on wallet type
await issuerStore.revokeCredential(payload);

emit('success');
// close up on success
Expand Down
16 changes: 14 additions & 2 deletions services/tenant-ui/frontend/src/helpers/tableFormatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface FormattedIssuedCredentialRecord extends V20CredExRecordDetail {
created: string;
cred_rev_id?: string;
rev_reg_id?: string;
credential_exchange_id?: string;
state: V20CredExRecord['state'];
}

Expand Down Expand Up @@ -185,12 +186,23 @@ export const formatIssuedCredentials = (
}
}

const connectionId = ce.cred_ex_record?.connection_id ?? '';
// findConnectionName is a function that takes connectionId and returns the connection name
// It should use alias first, then their_label as fallback
const connectionName =
connectionId && findConnectionName
? findConnectionName(connectionId) || ''
: '';

return {
// Preserve the full V20CredExRecordDetail structure for format detection
...ce,
// Explicitly set all formatted values to ensure they override any spread properties
state: ce.cred_ex_record?.state,
cred_rev_id: credRevId,
rev_reg_id: revRegId,
connection_id: ce.cred_ex_record?.connection_id,
connection: findConnectionName(ce.cred_ex_record?.connection_id ?? ''),
connection_id: connectionId,
connection: connectionName,
credential_definition_id: credentialDefinitionDisplay,
credential_exchange_id: ce.cred_ex_record?.cred_ex_id,
created: formatDateLong(ce.cred_ex_record?.created_at),
Expand Down
7 changes: 5 additions & 2 deletions services/tenant-ui/frontend/src/store/connectionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ export const useConnectionStore = defineStore('connection', () => {

const findConnectionName = computed(() => (connectionId: string) => {
if (loading.value) return undefined;
// Find the connection alias for an ID
// Find the connection alias for an ID, fallback to their_label
const connection = connections.value?.find((c: any) => {
return c.connection_id === connectionId;
});
return connection && connection.alias ? connection.alias : '';
if (connection) {
return connection.alias || connection.their_label || '';
}
return '';
});

// actions
Expand Down
42 changes: 5 additions & 37 deletions services/tenant-ui/frontend/src/store/governanceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,46 +589,14 @@ export const useGovernanceStore = defineStore('governance', () => {
.then((res) => {
result = res.data;
console.log(result);
// Schema finished event will be handled by the backend event handler
// which will automatically add it to storage
})
.then(async () => {
// Extract schema_id from response
const schemaId = result?.schema_state?.schema_id;

if (!schemaId) {
console.warn('No schema_id found in response, cannot verify storage');
return;
}

// Wait a bit for event handler to complete (if it runs)
await new Promise((resolve) => setTimeout(resolve, 1500));

// Refresh the stored schema list
// Wait a moment for the backend event handler to process and store the schema
await new Promise((resolve) => setTimeout(resolve, 500));
// Refresh the schema list to show the newly created schema
await listStoredSchemas();
Comment on lines +596 to 599
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createAnoncredsSchema now relies on a fixed 500ms delay followed by a single listStoredSchemas() refresh. Since schema storage is handled asynchronously by a backend event handler, this can still race (event processing >500ms) and the newly created schema may not appear until a later manual refresh. Consider polling listStoredSchemas() until the new schema is present (with a timeout), or having the backend return/emit a definitive completion signal to avoid timing-based UI flakiness.

Suggested change
// Wait a moment for the backend event handler to process and store the schema
await new Promise((resolve) => setTimeout(resolve, 500));
// Refresh the schema list to show the newly created schema
await listStoredSchemas();
// Poll for a short period to give the backend event handler time
// to process and store the schema before refreshing the list.
const maxAttempts = 10;
const delayMs = 500;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
// Refresh the schema list to show the newly created schema
await listStoredSchemas();
// If more attempts remain, wait before the next poll
if (attempt < maxAttempts - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}

Copilot uses AI. Check for mistakes.

// Check if schema is in storage after refresh
const storedSchema = storedSchemas.value.find(
(s: any) => s.schema_id === schemaId
);

if (!storedSchema) {
// Schema not in storage, manually add it via storage endpoint
console.log(
`Schema ${schemaId} not found in storage after event handler, manually adding...`
);
try {
await addSchemaFromLedgerToStorage({ schema_id: schemaId });
// Refresh again after manual add
await listStoredSchemas();
} catch (err) {
console.error(
`Failed to manually add schema ${schemaId} to storage:`,
err
);
// Don't throw - schema was created successfully, just storage failed
}
} else {
console.log(`Schema ${schemaId} found in storage`);
}
})
.catch((err) => {
console.log(err);
Expand Down
Loading
Loading