From 66b820f2a62e83ce5ac0ea4488a8d11915894372 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 11:39:11 +0700 Subject: [PATCH 01/28] fix(eslint): change no-unused-vars rule to error level --- .eslintrc.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 23c23c32b0..5f8f463e0e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -44,7 +44,10 @@ "@typescript-eslint/no-duplicate-enum-values": "error", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-non-null-assertion": "warn", - "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-unused-vars": [ + "error", + { "argsIgnorePattern": "^_" } + ], "react/no-unescaped-entities": 0, "react/react-in-jsx-scope": "off", "no-console": "error", From 96ffe4ca91f7cf08507f50bd20345fa82188db5c Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 11:48:54 +0700 Subject: [PATCH 02/28] fix(tests): correct usage of optional chaining in connectionService tests --- src/core/agent/services/connectionService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/agent/services/connectionService.test.ts b/src/core/agent/services/connectionService.test.ts index dba97ce864..871ef5d769 100644 --- a/src/core/agent/services/connectionService.test.ts +++ b/src/core/agent/services/connectionService.test.ts @@ -1911,7 +1911,7 @@ describe("Connection service of agent", () => { }); expect(result.historyItems).toHaveLength(2); expect( - result.historyItems?.some( + result.historyItems.some( (item) => item.type === ConnectionHistoryType.IPEX_AGREE_COMPLETE ) ).toBe(true); From c4d120b145a627cd7f508446ab08a5e72e34929c Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 11:51:08 +0700 Subject: [PATCH 03/28] fix(core): ensure contact exists before accessing properties in connection handling --- src/core/agent/services/connectionService.ts | 26 +++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/core/agent/services/connectionService.ts b/src/core/agent/services/connectionService.ts index 1babc6666c..a0189b4278 100644 --- a/src/core/agent/services/connectionService.ts +++ b/src/core/agent/services/connectionService.ts @@ -258,18 +258,22 @@ class ConnectionService extends AgentService { } const contact = contactRecordMap.get(connectionPair.contactId); - if (contact?.alias && contact?.oobi) { - connections.push({ - id: connectionPair.contactId, - alias: contact.alias, - createdAt: connectionPair.createdAt, - oobi: contact.oobi, - groupId: contact.groupId, - creationStatus: connectionPair.creationStatus, - pendingDeletion: connectionPair.pendingDeletion, - identifier: connectionPair.identifier, // Include identifier from connection pair - }); + if (!contact) { + throw new Error( + `Contact missing from map for contactId: ${connectionPair.contactId}` + ); } + + connections.push({ + id: connectionPair.contactId, + alias: contact.alias, + createdAt: connectionPair.createdAt, + oobi: contact.oobi, + groupId: contact.groupId, + creationStatus: connectionPair.creationStatus, + pendingDeletion: connectionPair.pendingDeletion, + identifier: connectionPair.identifier, // Include identifier from connection pair + }); } return connections.map((connection) => From a674fd5b5a21f8bff2d7d4221148b90c90b58e3f Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 12:00:13 +0700 Subject: [PATCH 04/28] fix(core): improve error handling and structure in connection details retrieval --- src/core/agent/services/connectionService.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/core/agent/services/connectionService.ts b/src/core/agent/services/connectionService.ts index a0189b4278..1b961e716f 100644 --- a/src/core/agent/services/connectionService.ts +++ b/src/core/agent/services/connectionService.ts @@ -316,17 +316,24 @@ class ConnectionService extends AgentService { status = ConnectionStatus.FAILED; } - return { + const baseDetails = { id: record.id, label: record.alias, createdAtUTC: record.createdAt.toISOString(), status, oobi: record.oobi, contactId: record.id, - ...(record.groupId - ? { groupId: record.groupId } - : { identifier: record.identifier || "" }), }; + + if (record.groupId !== undefined) { + return { ...baseDetails, groupId: record.groupId }; + } + + if (!record.identifier) { + throw new Error(`Regular contact must have identifier: ${record.id}`); + } + + return { ...baseDetails, identifier: record.identifier }; } async getConnectionById( From 29c0392193545a70b68cfbaaa3f60a0711bc3553 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 12:02:33 +0700 Subject: [PATCH 05/28] fix(core): add agent initialization check in identifier creation --- src/core/agent/services/identifierService.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/agent/services/identifierService.ts b/src/core/agent/services/identifierService.ts index 0830f3d507..5662f69db6 100644 --- a/src/core/agent/services/identifierService.ts +++ b/src/core/agent/services/identifierService.ts @@ -230,6 +230,10 @@ class IdentifierService extends AgentService { metadata: Omit, backgroundTask = false ): Promise { + if (!this.props.signifyClient.agent) { + throw new Error("Agent not initialized"); + } + const { toad, witnesses } = await this.getAvailableWitnesses(); if (!UI_THEMES.includes(metadata.theme)) { @@ -303,9 +307,6 @@ class IdentifierService extends AgentService { .identifiers() .get(identifier)) as HabState; - if (!this.props.signifyClient.agent) { - throw new Error("Agent not initialized"); - } const addRoleOperation = await this.props.signifyClient .identifiers() .addEndRole(identifier, "agent", this.props.signifyClient.agent.pre); From d0d14bfc08b93fddd81af329eb65022781c69d46 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 12:04:19 +0700 Subject: [PATCH 06/28] refactor(core): simplify cloudIdentifiers type definition in syncKeriaIdentifiers method --- src/core/agent/services/identifierService.ts | 24 +++++++------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/core/agent/services/identifierService.ts b/src/core/agent/services/identifierService.ts index 5662f69db6..ed46c210fb 100644 --- a/src/core/agent/services/identifierService.ts +++ b/src/core/agent/services/identifierService.ts @@ -659,16 +659,7 @@ class IdentifierService extends AgentService { } async syncKeriaIdentifiers(): Promise { - const cloudIdentifiers: Array<{ - prefix: string; - name: string; - group?: { - mhab: { - name: string; - prefix: string; - }; - }; - }> = []; + const cloudIdentifiers: HabState[] = []; let returned = -1; let iteration = 0; @@ -684,8 +675,10 @@ class IdentifierService extends AgentService { const localIdentifiers = await this.identifierStorage.getAllIdentifiers(); - const unSyncedDataWithGroup = []; - const unSyncedDataWithoutGroup = []; + const unSyncedDataWithGroup: (HabState & { + group: NonNullable; + })[] = []; + const unSyncedDataWithoutGroup: HabState[] = []; for (const identifier of cloudIdentifiers) { if (localIdentifiers.find((item) => item.id === identifier.prefix)) { continue; @@ -694,7 +687,9 @@ class IdentifierService extends AgentService { if (identifier.group === undefined) { unSyncedDataWithoutGroup.push(identifier); } else { - unSyncedDataWithGroup.push(identifier); + unSyncedDataWithGroup.push( + identifier as HabState & { group: NonNullable } + ); } } @@ -763,9 +758,6 @@ class IdentifierService extends AgentService { .identifiers() .get(identifier.prefix)) as HabState; - if (!identifier.group) { - throw new Error("Group identifier missing group data"); - } const nameToParse = identifier.name.startsWith( IdentifierService.DELETED_IDENTIFIER_THEME ) From e5871cc3d7b007b492baacfc830841c48fbef0b3 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 12:43:53 +0700 Subject: [PATCH 07/28] fix(core): enforce presence of e.acdc in CREDENTIAL_REVOKED messages and improve typing for grant exn structure --- src/core/agent/services/ipexCommunicationService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index 2cd91c6811..7d54ec43d4 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -558,8 +558,11 @@ class IpexCommunicationService extends AgentService { let key; switch (historyType) { case ConnectionHistoryType.CREDENTIAL_REVOKED: + if (!message.exn.e.acdc) { + throw new Error("CREDENTIAL_REVOKED message must have e.acdc"); + } prefix = KeriaContactKeyPrefix.HISTORY_REVOKE; - key = message.exn.e.acdc?.d; + key = message.exn.e.acdc.d; break; case ConnectionHistoryType.CREDENTIAL_ISSUANCE: case ConnectionHistoryType.CREDENTIAL_REQUEST_PRESENT: @@ -726,6 +729,8 @@ class IpexCommunicationService extends AgentService { throw new Error(IpexCommunicationService.NO_CURRENT_IPEX_MSG_TO_JOIN); } + // Extract grant exn - type is complex nested structure from Signify + // @TODO: Improve typing when Signify-TS provides better types for nested exn structures const grantExn = multiSigExn.exn.e.exn as unknown as { i: string; p: string; From 8a21efebb6f0fe69f3dd88fb70ff22e67e36badc Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 12:51:52 +0700 Subject: [PATCH 08/28] fix(core): add initialization check for signify client manager in IpexCommunicationService methods --- .../services/ipexCommunicationService.ts | 23 +++++++++++-------- .../ipexCommunicationService.types.ts | 1 + 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index 7d54ec43d4..de5252ce64 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -772,8 +772,12 @@ class IpexCommunicationService extends AgentService { notificationSaid: string, acdcDetail: CredentialMetadataRecordProps, discloseePrefix: string, - offerExnToJoin?: { ked: unknown } + offerExnToJoin?: unknown ): Promise { + if (!this.props.signifyClient.manager) { + throw new Error("Signify client manager not initialized"); + } + let exn: Serder; let sigsMes: string[]; let mend: string; @@ -795,9 +799,6 @@ class IpexCommunicationService extends AgentService { if (offerExnToJoin) { const [, ked] = Saider.saidify(offerExnToJoin); const offer = new Serder(ked); - if (!this.props.signifyClient.manager) { - throw new Error("Signify client manager not initialized"); - } const keeper = this.props.signifyClient.manager.get(gHab); const sigs = await keeper.sign(b(new Serder(offerExnToJoin).raw)); @@ -888,6 +889,10 @@ class IpexCommunicationService extends AgentService { }, grantToJoin?: GrantToJoinMultisigExnPayload ): Promise { + if (!this.props.signifyClient.manager) { + throw new Error("Signify client manager not initialized"); + } + let exn: Serder; let sigsMes: string[]; let mend: string; @@ -907,9 +912,6 @@ class IpexCommunicationService extends AgentService { const { grantExn, atc } = grantToJoin; const [, ked] = Saider.saidify(grantExn); const grant = new Serder(ked); - if (!this.props.signifyClient.manager) { - throw new Error("Signify client manager not initialized"); - } const keeper = this.props.signifyClient.manager.get(gHab); const sigs = await keeper.sign(b(new Serder(grantExn).raw)); const mstateNew = gHab["state"]; @@ -1046,6 +1048,10 @@ class IpexCommunicationService extends AgentService { schemaSaids: string[], admitExnToJoin?: { ked: unknown } ): Promise { + if (!this.props.signifyClient.manager) { + throw new Error("Signify client manager not initialized"); + } + let exn: Serder; let sigsMes: string[]; let mend: string; @@ -1080,9 +1086,6 @@ class IpexCommunicationService extends AgentService { if (admitExnToJoin) { const [, ked] = Saider.saidify(admitExnToJoin); const admit = new Serder(ked); - if (!this.props.signifyClient.manager) { - throw new Error("Signify client manager not initialized"); - } const keeper = this.props.signifyClient.manager.get(gHab); const sigs = await keeper.sign(b(new Serder(admitExnToJoin).raw)); diff --git a/src/core/agent/services/ipexCommunicationService.types.ts b/src/core/agent/services/ipexCommunicationService.types.ts index 225433b13d..cb65a8c2b4 100644 --- a/src/core/agent/services/ipexCommunicationService.types.ts +++ b/src/core/agent/services/ipexCommunicationService.types.ts @@ -9,6 +9,7 @@ interface CredentialsMatchingApply { }; credentials: { connectionId: string; + // @TODO: Use narrower ACDC type from Patrick's types for KERIA/Signify when available acdc: Record; }[]; attributes: JSONObject; From 78187a4a68109cbc88688461a26d20448856bfbd Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 13:01:04 +0700 Subject: [PATCH 09/28] fix(core): improve error handling by throwing an error when contact is not found in KeriaNotificationService --- src/core/agent/services/keriaNotificationService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/agent/services/keriaNotificationService.ts b/src/core/agent/services/keriaNotificationService.ts index e532205173..a3315a5ce1 100644 --- a/src/core/agent/services/keriaNotificationService.ts +++ b/src/core/agent/services/keriaNotificationService.ts @@ -1270,7 +1270,9 @@ class KeriaNotificationService extends AgentService { connectionPairRecord.contactId ); if (!contact) { - continue; + throw new Error( + `Contact not found for connection pair: ${connectionPairRecord.contactId}` + ); } await this.props.signifyClient From 192c29f232a44acc09872709c64ef33963d5a127 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 13:07:55 +0700 Subject: [PATCH 10/28] fix(core): standardize error messages for signify client manager initialization across services --- src/core/agent/agent.types.ts | 4 ++++ src/core/agent/services/ipexCommunicationService.ts | 7 ++++--- src/core/agent/services/multiSigService.ts | 10 +++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/core/agent/agent.types.ts b/src/core/agent/agent.types.ts index 9dfd7cb397..1ece58cf67 100644 --- a/src/core/agent/agent.types.ts +++ b/src/core/agent/agent.types.ts @@ -264,6 +264,10 @@ export const OOBI_AGENT_ONLY_RE = export const DOOBI_RE = /^\/oobi\/(?[^/]+)$/i; export const WOOBI_RE = /^\/\.well-known\/keri\/oobi\/(?[^/]+)$/; +// Common error messages +export const SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED = + "Signify client manager not initialized"; + export { ConnectionStatus, MiscRecordId, diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index de5252ce64..db8e01bb1c 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -9,6 +9,7 @@ import { Siger, } from "signify-ts"; import type { ExnMessage, AgentServicesProps } from "../agent.types"; +import { SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED } from "../agent.types"; import type { KeriaNotification } from "./keriaNotificationService.types"; import type { IpexGrantMultiSigExn } from "./multiSig.types"; import { ExchangeRoute } from "./keriaNotificationService.types"; @@ -775,7 +776,7 @@ class IpexCommunicationService extends AgentService { offerExnToJoin?: unknown ): Promise { if (!this.props.signifyClient.manager) { - throw new Error("Signify client manager not initialized"); + throw new Error(SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED); } let exn: Serder; @@ -890,7 +891,7 @@ class IpexCommunicationService extends AgentService { grantToJoin?: GrantToJoinMultisigExnPayload ): Promise { if (!this.props.signifyClient.manager) { - throw new Error("Signify client manager not initialized"); + throw new Error(SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED); } let exn: Serder; @@ -1049,7 +1050,7 @@ class IpexCommunicationService extends AgentService { admitExnToJoin?: { ked: unknown } ): Promise { if (!this.props.signifyClient.manager) { - throw new Error("Signify client manager not initialized"); + throw new Error(SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED); } let exn: Serder; diff --git a/src/core/agent/services/multiSigService.ts b/src/core/agent/services/multiSigService.ts index d095851953..b8cd0d6b66 100644 --- a/src/core/agent/services/multiSigService.ts +++ b/src/core/agent/services/multiSigService.ts @@ -15,6 +15,7 @@ import { AgentServicesProps, MiscRecordId, CreationStatus, + SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED, } from "../agent.types"; import { NotificationRoute } from "./keriaNotificationService.types"; import type { @@ -77,6 +78,8 @@ class MultiSigService extends AgentService { "Cannot retry creating group identifier if retry data is missing from the DB"; static readonly MULTI_SIG_INCEPTION_EXCHANGE_MESSAGE_NOT_FOUND = "Cannot find inception exchange message for multisigId"; + static readonly GROUP_DATA_MISSING_FOR_INITIATOR = + "Group data missing for initiator"; protected readonly identifierStorage: IdentifierStorage; protected readonly operationPendingStorage: OperationPendingStorage; @@ -370,10 +373,11 @@ class MultiSigService extends AgentService { mHab: HabState, groupConnections: ConnectionShortDetails[] ): Promise { - const { witnesses } = await this.identifiers.getAvailableWitnesses(); if (!this.props.signifyClient.manager) { - throw new Error("Signify client manager not initialized"); + throw new Error(SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED); } + + const { witnesses } = await this.identifiers.getAvailableWitnesses(); const keeper = this.props.signifyClient.manager.get(mHab); for (const witness of witnesses) { @@ -798,7 +802,7 @@ class MultiSigService extends AgentService { .queued as QueuedGroupCreation[]) { if (queued.initiator) { if (!queued.data.group) { - throw new Error("Group data missing for initiator"); + throw new Error(MultiSigService.GROUP_DATA_MISSING_FOR_INITIATOR); } const threshold = queued.threshold; From 67420e7fef8cb52394ba30c4f344ed5e2f5b6676 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 17 Oct 2025 14:34:40 +0700 Subject: [PATCH 11/28] fix(core): update type handling for record tags in IonicStorage to improve type safety --- src/core/storage/ionicStorage/ionicStorage.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/storage/ionicStorage/ionicStorage.ts b/src/core/storage/ionicStorage/ionicStorage.ts index 4463a88820..e991d9b093 100644 --- a/src/core/storage/ionicStorage/ionicStorage.ts +++ b/src/core/storage/ionicStorage/ionicStorage.ts @@ -5,6 +5,7 @@ import { StorageService, BaseRecordConstructor, StorageMessage, + Tags, } from "../storage.types"; import { deserializeRecord } from "../utils"; import { BasicRecord } from "../../agent/records"; @@ -114,7 +115,7 @@ class IonicStorage implements StorageService { } private checkRecordIsValidWithQuery( - record: Record, + record: unknown, query: Query ): boolean { for (const [queryKey, queryVal] of Object.entries(query)) { @@ -141,7 +142,7 @@ class IonicStorage implements StorageService { } else { if (Array.isArray(queryVal) && queryVal.length > 0) { // compare them item by item - const tags = record.tags as Record; + const tags = (record as { tags: Tags }).tags; const check = queryVal.every( (element) => Array.isArray(tags?.[queryKey]) && @@ -151,7 +152,7 @@ class IonicStorage implements StorageService { return false; } } else { - const tags = record.tags as Record; + const tags = (record as { tags: Tags }).tags; if (tags[queryKey] !== queryVal) { // If you query for an unknown tag `m` that is not a part of the record with `{ m: undefined }` all items will match this query. This behaves differently in SQLite storage - this risk is accepted since we will only query for known tags and Ionic Storage is only used in development. return false; From 0e42e01d7916bb6dfb5b9354307499db875d07c9 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Wed, 29 Oct 2025 15:29:09 +0700 Subject: [PATCH 12/28] fix(tests): remove optional chaining for historyItems in connectionService tests --- src/core/agent/services/connectionService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/agent/services/connectionService.test.ts b/src/core/agent/services/connectionService.test.ts index 871ef5d769..9a1aeda52b 100644 --- a/src/core/agent/services/connectionService.test.ts +++ b/src/core/agent/services/connectionService.test.ts @@ -1991,7 +1991,7 @@ describe("Connection service of agent", () => { }); expect(result.historyItems).toHaveLength(1); expect( - result.historyItems?.some( + result.historyItems.some( (item) => item.type === ConnectionHistoryType.IPEX_AGREE_COMPLETE ) ).toBe(false); From e0450434e17997b990507aa034efefa90592a9fd Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 31 Oct 2025 15:33:50 +0700 Subject: [PATCH 13/28] fix(core): update type definition for item tags in migration utilities to enhance type safety --- src/core/storage/sqliteStorage/migrations/migrationUtils.ts | 4 +++- .../migrations/v1.2.0.1-connections-per-account.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/storage/sqliteStorage/migrations/migrationUtils.ts b/src/core/storage/sqliteStorage/migrations/migrationUtils.ts index 4977914952..a684c1a0be 100644 --- a/src/core/storage/sqliteStorage/migrations/migrationUtils.ts +++ b/src/core/storage/sqliteStorage/migrations/migrationUtils.ts @@ -1,3 +1,5 @@ +import { Tags } from "../../storage.types"; + /** * Utility functions for migrations */ @@ -9,7 +11,7 @@ */ export function createInsertItemTagsStatements(itemRecord: { id: string; - tags?: Record; + tags?: Tags; }): { statement: string; values?: unknown[] }[] { const statements: { statement: string; values?: unknown[] }[] = []; const statement = diff --git a/src/core/storage/sqliteStorage/migrations/v1.2.0.1-connections-per-account.ts b/src/core/storage/sqliteStorage/migrations/v1.2.0.1-connections-per-account.ts index 55ebb7537d..0c3918aab6 100644 --- a/src/core/storage/sqliteStorage/migrations/v1.2.0.1-connections-per-account.ts +++ b/src/core/storage/sqliteStorage/migrations/v1.2.0.1-connections-per-account.ts @@ -1,3 +1,4 @@ +import { Tags } from "../../storage.types"; import { MigrationType, TsMigration } from "./migrations.types"; import { createInsertItemTagsStatements, @@ -67,7 +68,7 @@ export const DATA_V1201: TsMigration = { identifier: string; creationStatus: string; pendingDeletion: boolean; - tags: Record; + tags: Tags; type: string; }> = []; From c94d9f69bc0b263926357623193d6931baa023df Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 31 Oct 2025 16:17:08 +0700 Subject: [PATCH 14/28] feat(core): introduce ACDC type and integrate it into credential handling across services --- .../agent/services/credentialService.types.ts | 16 ++++++++++++- .../ipexCommunicationService.types.ts | 5 ++-- .../ChooseCredential.test.tsx | 6 +++-- .../ChooseCredential/ChooseCredential.tsx | 12 ++++------ .../CredentialRequest/CredentialRequest.tsx | 24 ++++++++++++------- .../CredentialRequest.types.ts | 18 +------------- 6 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/core/agent/services/credentialService.types.ts b/src/core/agent/services/credentialService.types.ts index e3e874f304..5fc207647d 100644 --- a/src/core/agent/services/credentialService.types.ts +++ b/src/core/agent/services/credentialService.types.ts @@ -8,6 +8,20 @@ enum CredentialStatus { type CredentialShortDetails = Omit; +interface ACDC { + v: string; + d: string; + i: string; + ri: string; + s: string; + a: { + d: string; + i: string; + dt: string; + [key: string]: unknown; + }; +} + interface ACDCDetails extends Omit { i: string; @@ -45,4 +59,4 @@ interface KeriaCredential { } export { CredentialStatus }; -export type { CredentialShortDetails, ACDCDetails, KeriaCredential }; +export type { CredentialShortDetails, ACDCDetails, KeriaCredential, ACDC }; diff --git a/src/core/agent/services/ipexCommunicationService.types.ts b/src/core/agent/services/ipexCommunicationService.types.ts index cb65a8c2b4..a0c4c90571 100644 --- a/src/core/agent/services/ipexCommunicationService.types.ts +++ b/src/core/agent/services/ipexCommunicationService.types.ts @@ -2,6 +2,8 @@ import { Operation } from "signify-ts"; import { LinkedRequest } from "../records/notificationRecord.types"; import { JSONObject } from "../agent.types"; import { MultisigThresholds } from "./identifier.types"; +import { ACDC } from "./credentialService.types"; + interface CredentialsMatchingApply { schema: { name: string; @@ -9,8 +11,7 @@ interface CredentialsMatchingApply { }; credentials: { connectionId: string; - // @TODO: Use narrower ACDC type from Patrick's types for KERIA/Signify when available - acdc: Record; + acdc: ACDC; }[]; attributes: JSONObject; identifier: string; diff --git a/src/ui/pages/NotificationDetails/components/CredentialRequest/ChooseCredential/ChooseCredential.test.tsx b/src/ui/pages/NotificationDetails/components/CredentialRequest/ChooseCredential/ChooseCredential.test.tsx index 1b5c727ea2..a93c8a386f 100644 --- a/src/ui/pages/NotificationDetails/components/CredentialRequest/ChooseCredential/ChooseCredential.test.tsx +++ b/src/ui/pages/NotificationDetails/components/CredentialRequest/ChooseCredential/ChooseCredential.test.tsx @@ -4,7 +4,10 @@ import { IonReactMemoryRouter } from "@ionic/react-router"; import { act, fireEvent, render, waitFor } from "@testing-library/react"; import { createMemoryHistory } from "history"; import { Provider } from "react-redux"; -import { CredentialStatus } from "../../../../../../core/agent/services/credentialService.types"; +import { + ACDC, + CredentialStatus, +} from "../../../../../../core/agent/services/credentialService.types"; import { KeriaNotification } from "../../../../../../core/agent/services/keriaNotificationService.types"; import { KeyStoreKeys, SecureStorage } from "../../../../../../core/storage"; import EN_TRANSLATIONS from "../../../../../../locales/en/en.json"; @@ -21,7 +24,6 @@ import { } from "../../../../../utils/formatters"; import { makeTestStore } from "../../../../../utils/makeTestStore"; import { passcodeFiller } from "../../../../../utils/passcodeFiller"; -import { ACDC } from "../CredentialRequest.types"; import { ChooseCredential } from "./ChooseCredential"; const deleteNotificationMock = jest.fn((id: string) => Promise.resolve(id)); diff --git a/src/ui/pages/NotificationDetails/components/CredentialRequest/ChooseCredential/ChooseCredential.tsx b/src/ui/pages/NotificationDetails/components/CredentialRequest/ChooseCredential/ChooseCredential.tsx index 86963242f2..bfbe15ec81 100644 --- a/src/ui/pages/NotificationDetails/components/CredentialRequest/ChooseCredential/ChooseCredential.tsx +++ b/src/ui/pages/NotificationDetails/components/CredentialRequest/ChooseCredential/ChooseCredential.tsx @@ -33,7 +33,6 @@ import { formatShortDate, formatTimeToSec, } from "../../../../../utils/formatters"; -import type { ACDC } from "../CredentialRequest.types"; import { ChooseCredentialProps, RequestCredential, @@ -65,18 +64,17 @@ const ChooseCredential = ({ const mappedCredentials = credentialRequest.credentials.map( (cred): CardItem => { - const acdc = cred.acdc as unknown as ACDC; const connection = connections?.find((c) => c.id === cred.connectionId)?.label || i18n.t("tabs.connections.unknown").toString(); return { - id: acdc.d, + id: cred.acdc.d, title: connection, - subtitle: `${formatShortDate(String(acdc.a.dt))} - ${formatTimeToSec( - String(acdc.a.dt) - )}`, - data: { connectionId: cred.connectionId, acdc }, + subtitle: `${formatShortDate( + String(cred.acdc.a.dt) + )} - ${formatTimeToSec(String(cred.acdc.a.dt))}`, + data: { connectionId: cred.connectionId, acdc: cred.acdc }, }; } ); diff --git a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx index d7dfa7e5c5..2c6f4b7550 100644 --- a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx +++ b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx @@ -1,7 +1,6 @@ import { IonSpinner } from "@ionic/react"; import { useCallback, useMemo, useState } from "react"; import { Agent } from "../../../../../core/agent/agent"; -import type { CredentialMetadataRecordProps } from "../../../../../core/agent/records/credentialMetadataRecord.types"; import { CredentialStatus } from "../../../../../core/agent/services/credentialService.types"; import { IdentifierType } from "../../../../../core/agent/services/identifier.types"; import { CredentialsMatchingApply } from "../../../../../core/agent/services/ipexCommunicationService.types"; @@ -23,11 +22,7 @@ import { showError } from "../../../../utils/error"; import { NotificationDetailsProps } from "../../NotificationDetails.types"; import { ChooseCredential } from "./ChooseCredential"; import "./CredentialRequest.scss"; -import { - ACDC, - LinkedGroup, - RequestCredential, -} from "./CredentialRequest.types"; +import { LinkedGroup, RequestCredential } from "./CredentialRequest.types"; import { CredentialRequestInformation } from "./CredentialRequestInformation"; const CredentialRequest = ({ @@ -136,7 +131,7 @@ const CredentialRequest = ({ const mappedCredentials = credentialRequest.credentials.map( (cred): RequestCredential => ({ connectionId: cred.connectionId, - acdc: cred.acdc as unknown as ACDC, + acdc: cred.acdc, }) ); @@ -154,9 +149,20 @@ const CredentialRequest = ({ try { setLoading(true); + // Get full credential metadata from cache using credential ID + const credentialMetadata = credsCache.find( + (cred) => cred.id === credential.acdc.d + ); + + if (!credentialMetadata) { + throw new Error( + `Credential metadata not found for ID: ${credential.acdc.d}` + ); + } + await Agent.agent.ipexCommunications.offerAcdcFromApply( notificationDetails.id, - credential.acdc as unknown as CredentialMetadataRecordProps + credentialMetadata ); if (!linkedGroup) { @@ -177,7 +183,7 @@ const CredentialRequest = ({ setLoading(false); } }, - [notificationDetails.id, linkedGroup, dispatch, handleBack] + [notificationDetails.id, linkedGroup, dispatch, handleBack, credsCache] ); const changeToStageTwo = () => { diff --git a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.types.ts b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.types.ts index 4bc31b95c8..1c11841daf 100644 --- a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.types.ts +++ b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.types.ts @@ -4,6 +4,7 @@ import { } from "../../../../../core/agent/services/ipexCommunicationService.types"; import { KeriaNotification } from "../../../../../core/agent/services/keriaNotificationService.types"; import { BackReason } from "../../../../components/CredentialDetailModule/CredentialDetailModule.types"; +import { ACDC } from "../../../../../core/agent/services/credentialService.types"; interface MemberInfo { aid: string; @@ -40,22 +41,6 @@ interface ChooseCredentialProps { notificationDetails: KeriaNotification; } -interface ACDC { - v: string; - d: string; - i: string; - ri: string; - s: string; - a: Attendee; -} - -interface Attendee { - d: string; - i: string; - dt: Date; - attendeeName: string; -} - interface RequestCredential { connectionId: string; acdc: ACDC; @@ -86,7 +71,6 @@ interface MembersModalProps { } export type { - ACDC, ChooseCredentialProps, CredentialRequestProps, JoinedMemberProps, From 61a8b35986836b19129fbd90f4da455a943e0c2b Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 31 Oct 2025 17:18:23 +0700 Subject: [PATCH 15/28] fix(core): update ExnMessage types to use unknown for exn structure and add type guard in KeriaNotificationService --- src/core/agent/agent.types.ts | 10 ++-------- .../services/keriaNotificationService.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/core/agent/agent.types.ts b/src/core/agent/agent.types.ts index 1ece58cf67..2d22b146d7 100644 --- a/src/core/agent/agent.types.ts +++ b/src/core/agent/agent.types.ts @@ -120,10 +120,7 @@ interface ExnMessageA { }; d?: string; r?: string; - exn?: { - r: string; - p: string; - }; + exn?: unknown; } // Define types for the 'e' property in ExnMessage @@ -152,10 +149,7 @@ interface ExnMessageE { icp: { i: string; }; - exn: { - r: string; - p: string; - }; + exn: unknown; [key: string]: unknown; } diff --git a/src/core/agent/services/keriaNotificationService.ts b/src/core/agent/services/keriaNotificationService.ts index a3315a5ce1..60cac040c4 100644 --- a/src/core/agent/services/keriaNotificationService.ts +++ b/src/core/agent/services/keriaNotificationService.ts @@ -48,6 +48,20 @@ import { ConnectionHistoryType, ExnMessage } from "./connectionService.types"; import { NotificationAttempts } from "../records/notificationRecord.types"; import { StorageMessage } from "../../storage/storage.types"; import { IdentifierService } from "./identifierService"; + +// Type guard for exchange data with route information +function isExnWithRoute( + exn: unknown +): exn is { r: string; p: string; [key: string]: unknown } { + return ( + typeof exn === "object" && + exn !== null && + "r" in exn && + "p" in exn && + typeof (exn as { r: unknown }).r === "string" && + typeof (exn as { p: unknown }).p === "string" + ); +} import { ConnectionService } from "./connectionService"; import { LATEST_CONTACT_VERSION } from "../../storage/sqliteStorage/cloudMigrations"; @@ -717,6 +731,12 @@ class KeriaNotificationService extends AgentService { ): Promise { const exnData = exchange.exn.e?.exn; + if (!isExnWithRoute(exnData)) { + throw new Error( + `Invalid exn data structure in processMultiSigExnNotification: ${notif.i}` + ); + } + switch (exnData.r) { case ExchangeRoute.IpexAdmit: { const grantNotificationRecords = From d91008e25d33a94cda209af657860e288d0da60b Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 31 Oct 2025 18:29:57 +0700 Subject: [PATCH 16/28] fix(core): enhance type safety and validation for ExnMessage structures in IpexCommunicationService and KeriaNotificationService --- .../agent/ipexCommunicationFixtures.ts | 4 +- .../agent/keriaNotificationFixtures.ts | 4 +- src/core/agent/agent.types.ts | 10 +++- .../services/ipexCommunicationService.ts | 50 +++++++++++------ .../services/keriaNotificationService.ts | 5 ++ src/core/agent/services/multiSig.types.ts | 53 +++++++++++++++++-- 6 files changed, 105 insertions(+), 21 deletions(-) diff --git a/src/core/__fixtures__/agent/ipexCommunicationFixtures.ts b/src/core/__fixtures__/agent/ipexCommunicationFixtures.ts index 7cbc851854..5bfc20e0cb 100644 --- a/src/core/__fixtures__/agent/ipexCommunicationFixtures.ts +++ b/src/core/__fixtures__/agent/ipexCommunicationFixtures.ts @@ -431,7 +431,9 @@ const multisigExnGrant = { d: "EE8_Xc0ZUh_sUJLtmBpVSEr-RFS2mRUIpFyL-pmvtPvx", }, }, - pathed: {}, + pathed: { + exn: "d", + }, }; const credentialRecord = { diff --git a/src/core/__fixtures__/agent/keriaNotificationFixtures.ts b/src/core/__fixtures__/agent/keriaNotificationFixtures.ts index 7aa6d68a80..3be9db1eeb 100644 --- a/src/core/__fixtures__/agent/keriaNotificationFixtures.ts +++ b/src/core/__fixtures__/agent/keriaNotificationFixtures.ts @@ -269,7 +269,9 @@ const multisigExnGrant = { d: "EE8_Xc0ZUh_sUJLtmBpVSEr-RFS2mRUIpFyL-pmvtPvx", }, }, - pathed: {}, + pathed: { + exn: "d", + }, }; const notificationMultisigRpyProp = { diff --git a/src/core/agent/agent.types.ts b/src/core/agent/agent.types.ts index 2d22b146d7..ccefb96587 100644 --- a/src/core/agent/agent.types.ts +++ b/src/core/agent/agent.types.ts @@ -125,7 +125,7 @@ interface ExnMessageA { // Define types for the 'e' property in ExnMessage interface ExnMessageE { - acdc: { + acdc?: { d: string; i: string; s: string; @@ -175,6 +175,13 @@ type ExnMessage = { }; }; +// Type guard to check if ExnMessageE has acdc +function exnHasAcdc( + e: ExnMessageE +): e is ExnMessageE & { acdc: NonNullable } { + return e.acdc !== undefined && e.acdc !== null; +} + type ConnectionNoteProps = Pick; interface ConnectionDetailsExtras { @@ -269,6 +276,7 @@ export { CreationStatus, isRegularConnectionDetails, isMultisigConnectionDetails, + exnHasAcdc, }; export type { diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index db8e01bb1c..cd97e14cff 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -9,9 +9,11 @@ import { Siger, } from "signify-ts"; import type { ExnMessage, AgentServicesProps } from "../agent.types"; -import { SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED } from "../agent.types"; +import { + exnHasAcdc, + SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED, +} from "../agent.types"; import type { KeriaNotification } from "./keriaNotificationService.types"; -import type { IpexGrantMultiSigExn } from "./multiSig.types"; import { ExchangeRoute } from "./keriaNotificationService.types"; import { CredentialStorage, @@ -37,7 +39,11 @@ import { } from "./ipexCommunicationService.types"; import { OperationPendingRecordType } from "../records/operationPendingRecord.type"; import { MultiSigService } from "./multiSigService"; -import { GrantToJoinMultisigExnPayload, MultiSigRoute } from "./multiSig.types"; +import { + GrantToJoinMultisigExnPayload, + MultiSigRoute, + isIpexGrantMultiSigExn, +} from "./multiSig.types"; import { AcdcStateChangedEvent, EventTypes } from "../event.types"; import { ConnectionService } from "./connectionService"; import { IdentifierType } from "./identifier.types"; @@ -532,7 +538,11 @@ class IpexCommunicationService extends AgentService { } let schemaSaid; - if (message.exn.r === ExchangeRoute.IpexGrant && message.exn.e.acdc) { + // Type narrowing: IpexGrant route requires acdc + if ( + message.exn.r === ExchangeRoute.IpexGrant && + exnHasAcdc(message.exn.e) + ) { schemaSaid = message.exn.e.acdc.s; } else if (message.exn.r === ExchangeRoute.IpexApply) { schemaSaid = message.exn.a.s; @@ -543,6 +553,12 @@ class IpexCommunicationService extends AgentService { const previousExchange = await this.props.signifyClient .exchanges() .get(message.exn.p); + // Type narrowing: IpexAgree and IpexAdmit routes require acdc in previous exchange + if (!exnHasAcdc(previousExchange.exn.e)) { + throw new Error( + `${message.exn.r} message's previous exchange must have e.acdc` + ); + } schemaSaid = previousExchange.exn.e.acdc.s; } @@ -559,10 +575,12 @@ class IpexCommunicationService extends AgentService { let key; switch (historyType) { case ConnectionHistoryType.CREDENTIAL_REVOKED: - if (!message.exn.e.acdc) { + // Type narrowing: CREDENTIAL_REVOKED messages must have acdc + if (!exnHasAcdc(message.exn.e)) { throw new Error("CREDENTIAL_REVOKED message must have e.acdc"); } prefix = KeriaContactKeyPrefix.HISTORY_REVOKE; + // TypeScript now knows message.exn.e.acdc exists key = message.exn.e.acdc.d; break; case ConnectionHistoryType.CREDENTIAL_ISSUANCE: @@ -730,13 +748,15 @@ class IpexCommunicationService extends AgentService { throw new Error(IpexCommunicationService.NO_CURRENT_IPEX_MSG_TO_JOIN); } - // Extract grant exn - type is complex nested structure from Signify - // @TODO: Improve typing when Signify-TS provides better types for nested exn structures - const grantExn = multiSigExn.exn.e.exn as unknown as { - i: string; - p: string; - e: { acdc: { i: string; d: string; s: string } }; - }; + // Type narrowing: Validate multiSigExn structure first + if (!isIpexGrantMultiSigExn(multiSigExn)) { + throw new Error( + "Invalid multisig exchange structure for joinMultisigGrant: missing required fields" + ); + } + + // After type narrowing, TypeScript knows the structure is correct + const grantExn = multiSigExn.exn.e.exn; // Create the credential structure expected by submitMultisigGrant const credentialData = { @@ -751,8 +771,8 @@ class IpexCommunicationService extends AgentService { grantExn.p, credentialData, { - grantExn: multiSigExn.exn.e.exn as unknown as IpexGrantMultiSigExn, - atc: multiSigExn.pathed.exn as string, + grantExn: multiSigExn.exn.e.exn, + atc: multiSigExn.pathed.exn, } ); @@ -1047,7 +1067,7 @@ class IpexCommunicationService extends AgentService { multisigId: string, grantExn: ExnMessage, schemaSaids: string[], - admitExnToJoin?: { ked: unknown } + admitExnToJoin?: Record ): Promise { if (!this.props.signifyClient.manager) { throw new Error(SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED); diff --git a/src/core/agent/services/keriaNotificationService.ts b/src/core/agent/services/keriaNotificationService.ts index 60cac040c4..fdb8eb4c21 100644 --- a/src/core/agent/services/keriaNotificationService.ts +++ b/src/core/agent/services/keriaNotificationService.ts @@ -5,6 +5,7 @@ import { ConnectionStatus, MiscRecordId, CreationStatus, + exnHasAcdc, } from "../agent.types"; import { KeriaNotificationMarker, @@ -524,6 +525,10 @@ class KeriaNotificationService extends AgentService { exchange: ExnMessage ): Promise { // Only consider issuances for now + // Type narrowing: IpexGrant route must have e.acdc + if (!exnHasAcdc(exchange.exn.e)) { + throw new Error(`IpexGrant notification must have e.acdc: ${notif.i}`); + } const ourIdentifier = await this.identifierStorage .getIdentifierMetadata(exchange.exn.e.acdc.a.i) diff --git a/src/core/agent/services/multiSig.types.ts b/src/core/agent/services/multiSig.types.ts index be445b2fd9..1cd4d142c0 100644 --- a/src/core/agent/services/multiSig.types.ts +++ b/src/core/agent/services/multiSig.types.ts @@ -95,7 +95,12 @@ interface IpexGrantMultiSigExn { m: string; }; e: { - acdc: unknown; // @TODO - foconnor: We can type these. + acdc: { + d: string; + i: string; + s: string; + [key: string]: unknown; + }; iss: unknown; anc: unknown; d: string; @@ -104,6 +109,45 @@ interface IpexGrantMultiSigExn { d: string; }; }; + pathed: { + exn: string; + }; +} + +// Type guard for IpexGrantMultiSigExn +function isIpexGrantMultiSigExn(obj: unknown): obj is IpexGrantMultiSigExn { + if (typeof obj !== "object" || obj === null) return false; + + const candidate = obj as { + exn?: unknown; + pathed?: unknown; + }; + + // Check pathed.exn is string + if ( + typeof candidate.pathed !== "object" || + candidate.pathed === null || + typeof (candidate.pathed as { exn?: unknown }).exn !== "string" + ) { + return false; + } + + // Check exn structure + if (typeof candidate.exn !== "object" || candidate.exn === null) { + return false; + } + + const exn = candidate.exn as { + e?: unknown; + }; + + // Check exn.e.exn exists + if (typeof exn.e !== "object" || exn.e === null) { + return false; + } + + const e = exn.e as { exn?: unknown }; + return typeof e.exn === "object" && e.exn !== null; } interface IpexAdmitMultiSigRequest { @@ -128,8 +172,11 @@ interface IpexAdmitMultiSigRequest { }; } +// Extract the inner exn type from IpexGrantMultiSigExn +type InnerGrantExn = IpexGrantMultiSigExn["exn"]["e"]["exn"]; + interface GrantToJoinMultisigExnPayload { - grantExn: IpexGrantMultiSigExn; + grantExn: InnerGrantExn; atc: string; } @@ -144,7 +191,7 @@ interface GroupInformation { members: GroupMemberInfo[]; } -export { MultiSigRoute }; +export { MultiSigRoute, isIpexGrantMultiSigExn }; export type { RotationMultiSigExnMessage, From 82524258d4d0a047f164eaaf54927eeceda78c9d Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 31 Oct 2025 18:47:58 +0700 Subject: [PATCH 17/28] fix(core): improve type exports and enhance group data handling in MultiSigService --- src/core/agent/services/identifier.types.ts | 6 ++---- src/core/agent/services/multiSigService.ts | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/agent/services/identifier.types.ts b/src/core/agent/services/identifier.types.ts index 3e8a745f53..5b491639ab 100644 --- a/src/core/agent/services/identifier.types.ts +++ b/src/core/agent/services/identifier.types.ts @@ -110,6 +110,8 @@ interface RemoteSignRequest { payload: JSONObject; } +export { IdentifierType }; + export type { IdentifierShortDetails, IdentifierDetails, @@ -117,10 +119,6 @@ export type { CreateIdentifierInputs, CreateIdentifierResult, MultisigThresholds, -}; - -export { IdentifierType }; -export type { GroupMetadata, QueuedIdentifierCreation, QueuedGroupProps, diff --git a/src/core/agent/services/multiSigService.ts b/src/core/agent/services/multiSigService.ts index b8cd0d6b66..a323f7d85a 100644 --- a/src/core/agent/services/multiSigService.ts +++ b/src/core/agent/services/multiSigService.ts @@ -805,9 +805,10 @@ class MultiSigService extends AgentService { throw new Error(MultiSigService.GROUP_DATA_MISSING_FOR_INITIATOR); } + const groupData = queued.data.group; const threshold = queued.threshold; await this.createGroup( - queued.data.group.mhab.prefix, + groupData.mhab.prefix, queued.groupConnections as MultisigConnectionDetails[], threshold, true From 2462d86fb7d3589e3ab692bf29d033f47c17ded2 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Fri, 31 Oct 2025 22:13:23 +0700 Subject: [PATCH 18/28] fix(core): enhance type safety for queued group creation and improve data handling in MultiSigService --- .../__fixtures__/agent/multiSigFixtures.ts | 4 +-- src/core/agent/services/identifier.types.ts | 32 +++++++++++++----- src/core/agent/services/multiSigService.ts | 33 ++++++++++++++----- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/core/__fixtures__/agent/multiSigFixtures.ts b/src/core/__fixtures__/agent/multiSigFixtures.ts index 3a3b05b5d1..6bba8f552c 100644 --- a/src/core/__fixtures__/agent/multiSigFixtures.ts +++ b/src/core/__fixtures__/agent/multiSigFixtures.ts @@ -1,4 +1,4 @@ -import { CreateIdentifierBody, Tier } from "signify-ts"; +import { CreateIdentifierBody, HabState, Tier } from "signify-ts"; import { ConnectionStatus, CreationStatus } from "../../agent/agent.types"; import { IdentifierMetadataRecord, @@ -862,7 +862,7 @@ const linkedContacts = [ const queuedIdentifier: QueuedGroupCreation & { initiator: true } = { name: "1.2.0.2:0:Identifier 2", - data: inceptionDataFix, + data: inceptionDataFix as CreateIdentifierBody & { group: HabState }, initiator: true, groupConnections: linkedContacts, threshold: { diff --git a/src/core/agent/services/identifier.types.ts b/src/core/agent/services/identifier.types.ts index 5b491639ab..5c4b4a752e 100644 --- a/src/core/agent/services/identifier.types.ts +++ b/src/core/agent/services/identifier.types.ts @@ -1,4 +1,4 @@ -import { CreateIdentifierBody } from "signify-ts"; +import { CreateIdentifierBody, HabState } from "signify-ts"; import { MultisigConnectionDetails, CreationStatus, @@ -61,29 +61,45 @@ enum IdentifierType { Group = "group", } -type QueuedIdentifierCreation = { - name: string; - data: CreateIdentifierBody; -}; - interface MultisigThresholds { signingThreshold: number; rotationThreshold: number; } -type QueuedGroupProps = +// Discriminated union with proper type safety for group data +type QueuedGroupCreation = | { initiator: true; + name: string; + data: CreateIdentifierBody & { group: HabState }; groupConnections: MultisigConnectionDetails[]; threshold: number | MultisigThresholds; } | { initiator: false; + name: string; + data: CreateIdentifierBody; notificationId: string; notificationSaid: string; }; -type QueuedGroupCreation = QueuedIdentifierCreation & QueuedGroupProps; +// Legacy type for backward compatibility if needed elsewhere +type QueuedIdentifierCreation = { + name: string; + data: CreateIdentifierBody; +}; + +type QueuedGroupProps = + | { + initiator: true; + groupConnections: MultisigConnectionDetails[]; + threshold: number | MultisigThresholds; + } + | { + initiator: false; + notificationId: string; + notificationSaid: string; + }; interface GroupParticipants { ourIdentifier: IdentifierMetadataRecord; diff --git a/src/core/agent/services/multiSigService.ts b/src/core/agent/services/multiSigService.ts index a323f7d85a..c0d0d7021d 100644 --- a/src/core/agent/services/multiSigService.ts +++ b/src/core/agent/services/multiSigService.ts @@ -310,7 +310,28 @@ class MultiSigService extends AgentService { rstates: states, }); - queued.push({ name: groupName, data: inceptionData, ...queuedProps }); + // Build properly typed queued item based on discriminated union + if (queuedProps.initiator) { + // Ensure group data exists for initiator + if (!inceptionData.group) { + throw new Error(MultiSigService.GROUP_DATA_MISSING_FOR_INITIATOR); + } + queued.push({ + initiator: true, + name: groupName, + data: inceptionData as CreateIdentifierBody & { group: HabState }, + groupConnections: queuedProps.groupConnections, + threshold: queuedProps.threshold, + }); + } else { + queued.push({ + initiator: false, + name: groupName, + data: inceptionData, + notificationId: queuedProps.notificationId, + notificationSaid: queuedProps.notificationSaid, + }); + } await this.basicStorage.createOrUpdateBasicRecord( new BasicRecord({ @@ -801,15 +822,11 @@ class MultiSigService extends AgentService { for (const queued of pendingGroupsRecord.content .queued as QueuedGroupCreation[]) { if (queued.initiator) { - if (!queued.data.group) { - throw new Error(MultiSigService.GROUP_DATA_MISSING_FOR_INITIATOR); - } - - const groupData = queued.data.group; + // TypeScript guarantees queued.data.group exists when initiator is true const threshold = queued.threshold; await this.createGroup( - groupData.mhab.prefix, - queued.groupConnections as MultisigConnectionDetails[], + queued.data.group.mhab.prefix, + queued.groupConnections, threshold, true ); From 2979559bbac2aecfedae7de5beffd38aaeb7d9ab Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Sat, 1 Nov 2025 01:04:11 +0700 Subject: [PATCH 19/28] fix(core): improve type safety for queued group handling in MultiSigService by refining type assertions --- src/core/agent/services/multiSigService.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/core/agent/services/multiSigService.ts b/src/core/agent/services/multiSigService.ts index c0d0d7021d..ff6d04f38b 100644 --- a/src/core/agent/services/multiSigService.ts +++ b/src/core/agent/services/multiSigService.ts @@ -822,18 +822,27 @@ class MultiSigService extends AgentService { for (const queued of pendingGroupsRecord.content .queued as QueuedGroupCreation[]) { if (queued.initiator) { - // TypeScript guarantees queued.data.group exists when initiator is true - const threshold = queued.threshold; + // TypeScript narrows to initiator variant + const initiatorQueued = queued as Extract< + QueuedGroupCreation, + { initiator: true } + >; + const threshold = initiatorQueued.threshold; await this.createGroup( - queued.data.group.mhab.prefix, - queued.groupConnections, + initiatorQueued.data.group.mhab.prefix, + initiatorQueued.groupConnections, threshold, true ); } else { + // TypeScript narrows to join variant + const joinQueued = queued as Extract< + QueuedGroupCreation, + { initiator: false } + >; await this.joinGroup( - queued.notificationId, - queued.notificationSaid, + joinQueued.notificationId, + joinQueued.notificationSaid, true ); } From 0c23980023b20ac855b806205a5d57424c22c468 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Mon, 3 Nov 2025 23:03:17 +0700 Subject: [PATCH 20/28] fix(core): enhance type safety for identifier metadata handling in SQLite migration by refining type definitions --- .../migrations/v1.2.0.1-connections-per-account.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/storage/sqliteStorage/migrations/v1.2.0.1-connections-per-account.ts b/src/core/storage/sqliteStorage/migrations/v1.2.0.1-connections-per-account.ts index 0c3918aab6..f62673c6e3 100644 --- a/src/core/storage/sqliteStorage/migrations/v1.2.0.1-connections-per-account.ts +++ b/src/core/storage/sqliteStorage/migrations/v1.2.0.1-connections-per-account.ts @@ -1,4 +1,5 @@ import { Tags } from "../../storage.types"; +import { IdentifierMetadataRecordProps } from "../../../agent/records/identifierMetadataRecord"; import { MigrationType, TsMigration } from "./migrations.types"; import { createInsertItemTagsStatements, @@ -16,9 +17,12 @@ export const DATA_V1201: TsMigration = { let identifiers = identifierResult.values; identifiers = identifiers - ?.map((identifier: { value: string }) => JSON.parse(identifier.value)) + ?.map( + (row: { value: string }): IdentifierMetadataRecordProps => + JSON.parse(row.value) as IdentifierMetadataRecordProps + ) .filter( - (identifier: { isDeleted?: boolean; pendingDeletion?: boolean }) => + (identifier: IdentifierMetadataRecordProps) => !identifier.isDeleted && !identifier.pendingDeletion ); From 6b4d8507f91c0af6b0363f3595bd2194a1e5b322 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Mon, 3 Nov 2025 23:13:34 +0700 Subject: [PATCH 21/28] fix(core): refine type definitions for ACDC handling in IpexCommunicationService and CredentialRequest component --- .../agent/services/credentialService.types.ts | 1 + .../services/ipexCommunicationService.test.ts | 129 +++++++++--------- .../services/ipexCommunicationService.ts | 4 +- .../CredentialRequest/CredentialRequest.tsx | 15 +- 4 files changed, 67 insertions(+), 82 deletions(-) diff --git a/src/core/agent/services/credentialService.types.ts b/src/core/agent/services/credentialService.types.ts index 5fc207647d..870f83ed95 100644 --- a/src/core/agent/services/credentialService.types.ts +++ b/src/core/agent/services/credentialService.types.ts @@ -20,6 +20,7 @@ interface ACDC { dt: string; [key: string]: unknown; }; + [key: string]: unknown; } interface ACDCDetails diff --git a/src/core/agent/services/ipexCommunicationService.test.ts b/src/core/agent/services/ipexCommunicationService.test.ts index 0b4c43b806..792af1a94f 100644 --- a/src/core/agent/services/ipexCommunicationService.test.ts +++ b/src/core/agent/services/ipexCommunicationService.test.ts @@ -1332,21 +1332,20 @@ describe("Offer ACDC individual actions", () => { }); markNotificationMock.mockResolvedValueOnce({ status: "done" }); - const credentialProps: CredentialMetadataRecordProps = { - id: "credential-id", - issuanceDate: "2024-01-01T00:00:00.000Z", - credentialType: "Test Credential", - status: CredentialStatus.CONFIRMED, - connectionId: "connection-id", - schema: "schema-said", - identifierId: "identifier-id", - identifierType: IdentifierType.Individual, - createdAt: new Date("2024-01-01T00:00:00.000Z"), - isArchived: false, - pendingDeletion: false, + const acdcData = { + v: "ACDC10JSON000197_", + d: "credential-id", + i: "issuer-aid", + ri: "registry-aid", + s: "schema-said", + a: { + d: "attribute-said", + i: "holder-aid", + dt: "2024-01-01T00:00:00.000Z", + }, }; - await ipexCommunicationService.offerAcdcFromApply(id, credentialProps); + await ipexCommunicationService.offerAcdcFromApply(id, acdcData); expect(operationPendingStorage.save).toBeCalledWith({ id: "opName", @@ -1355,7 +1354,7 @@ describe("Offer ACDC individual actions", () => { expect(ipexOfferMock).toBeCalledWith({ senderName: "abc123", recipient: "i", - acdc: new Serder(credentialProps), + acdc: new Serder(acdcData), applySaid: "d", }); expect(ipexSubmitOfferMock).toBeCalledWith( @@ -1388,22 +1387,21 @@ describe("Offer ACDC individual actions", () => { eventEmitter.emit = jest.fn(); notificationStorage.findById.mockResolvedValueOnce(null); - const credentialProps: CredentialMetadataRecordProps = { - id: "credential-id", - issuanceDate: "2024-01-01T00:00:00.000Z", - credentialType: "Test Credential", - status: CredentialStatus.CONFIRMED, - connectionId: "connection-id", - schema: "schema-said", - identifierId: "identifier-id", - identifierType: IdentifierType.Individual, - createdAt: new Date("2024-01-01T00:00:00.000Z"), - isArchived: false, - pendingDeletion: false, + const acdcData = { + v: "ACDC10JSON000197_", + d: "credential-id", + i: "issuer-aid", + ri: "registry-aid", + s: "schema-said", + a: { + d: "attribute-said", + i: "holder-aid", + dt: "2024-01-01T00:00:00.000Z", + }, }; await expect( - ipexCommunicationService.offerAcdcFromApply(id, credentialProps) + ipexCommunicationService.offerAcdcFromApply(id, acdcData) ).rejects.toThrowError( `${IpexCommunicationService.NOTIFICATION_NOT_FOUND} ${id}` ); @@ -1462,26 +1460,25 @@ describe("Offer ACDC group actions", () => { recordType: OperationPendingRecordType.ExchangeOfferCredential, }); - const credentialProps: CredentialMetadataRecordProps = { - id: "credential-id", - issuanceDate: "2024-01-01T00:00:00.000Z", - credentialType: "Test Credential", - status: CredentialStatus.CONFIRMED, - connectionId: "connection-id", - schema: "schema-said", - identifierId: "identifier-id", - identifierType: IdentifierType.Group, - createdAt: new Date("2024-01-01T00:00:00.000Z"), - isArchived: false, - pendingDeletion: false, + const acdcData = { + v: "ACDC10JSON000197_", + d: "credential-id", + i: "issuer-aid", + ri: "registry-aid", + s: "schema-said", + a: { + d: "attribute-said", + i: "holder-aid", + dt: "2024-01-01T00:00:00.000Z", + }, }; - await ipexCommunicationService.offerAcdcFromApply(id, credentialProps); + await ipexCommunicationService.offerAcdcFromApply(id, acdcData); expect(ipexOfferMock).toBeCalledWith({ senderName: "EC1cyV3zLnGs4B9AYgoGNjXESyQZrBWygz3jLlRD30bR", recipient: "ECS7jn05fIP_JK1Ub4E6hPviRKEdC55QhxZToxDIHo_E", - acdc: new Serder(credentialProps), + acdc: new Serder(acdcData), applySaid: "EIDUavcmyHBseNZAdAHR3SF8QMfX1kSJ3Ct0OqS0-HCW", message: "", datetime: expect.any(String), @@ -1525,22 +1522,21 @@ describe("Offer ACDC group actions", () => { }; notificationStorage.findById.mockResolvedValue(applyNoteRecord); - const credentialProps: CredentialMetadataRecordProps = { - id: "credential-id", - issuanceDate: "2024-01-01T00:00:00.000Z", - credentialType: "Test Credential", - status: CredentialStatus.CONFIRMED, - connectionId: "connection-id", - schema: "schema-said", - identifierId: "identifier-id", - identifierType: IdentifierType.Individual, - createdAt: new Date("2024-01-01T00:00:00.000Z"), - isArchived: false, - pendingDeletion: false, + const acdcData = { + v: "ACDC10JSON000197_", + d: "credential-id", + i: "issuer-aid", + ri: "registry-aid", + s: "schema-said", + a: { + d: "attribute-said", + i: "holder-aid", + dt: "2024-01-01T00:00:00.000Z", + }, }; await expect( - ipexCommunicationService.offerAcdcFromApply("id", credentialProps) + ipexCommunicationService.offerAcdcFromApply("id", acdcData) ).rejects.toThrowError(IpexCommunicationService.IPEX_ALREADY_REPLIED); expect(ipexOfferMock).not.toBeCalled(); @@ -2539,22 +2535,21 @@ describe("IPEX communication service of agent", () => { receivingPre: "EGR7Jm38EcsXRIidKDZBYDm_xox6eapfU1tqxdAUzkFA", }; - const credentialProps: CredentialMetadataRecordProps = { - id: "credential-id", - issuanceDate: "2024-01-01T00:00:00.000Z", - credentialType: "Test Credential", - status: CredentialStatus.CONFIRMED, - connectionId: "connection-id", - schema: "schema-said", - identifierId: "identifier-id", - identifierType: IdentifierType.Individual, - createdAt: new Date("2024-01-01T00:00:00.000Z"), - isArchived: false, - pendingDeletion: false, + const acdcData = { + v: "ACDC10JSON000197_", + d: "credential-id", + i: "issuer-aid", + ri: "registry-aid", + s: "schema-said", + a: { + d: "attribute-said", + i: "holder-aid", + dt: "2024-01-01T00:00:00.000Z", + }, }; await expect( - ipexCommunicationService.offerAcdcFromApply(noti.id, credentialProps) + ipexCommunicationService.offerAcdcFromApply(noti.id, acdcData) ).rejects.toThrowError(Agent.KERIA_CONNECTION_BROKEN); await expect( ipexCommunicationService.grantAcdcFromAgree(noti.a.d) diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index cd97e14cff..fe2d399e4a 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -220,7 +220,7 @@ class IpexCommunicationService extends AgentService { @OnlineOnly async offerAcdcFromApply( notificationId: string, - acdc: CredentialMetadataRecordProps + acdc: Record ): Promise { const applyNoteRecord = await this.notificationStorage.findById( notificationId @@ -791,7 +791,7 @@ class IpexCommunicationService extends AgentService { private async submitMultisigOffer( multisigId: string, notificationSaid: string, - acdcDetail: CredentialMetadataRecordProps, + acdcDetail: Record, discloseePrefix: string, offerExnToJoin?: unknown ): Promise { diff --git a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx index 2c6f4b7550..9f82dcce1d 100644 --- a/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx +++ b/src/ui/pages/NotificationDetails/components/CredentialRequest/CredentialRequest.tsx @@ -149,20 +149,9 @@ const CredentialRequest = ({ try { setLoading(true); - // Get full credential metadata from cache using credential ID - const credentialMetadata = credsCache.find( - (cred) => cred.id === credential.acdc.d - ); - - if (!credentialMetadata) { - throw new Error( - `Credential metadata not found for ID: ${credential.acdc.d}` - ); - } - await Agent.agent.ipexCommunications.offerAcdcFromApply( notificationDetails.id, - credentialMetadata + credential.acdc ); if (!linkedGroup) { @@ -183,7 +172,7 @@ const CredentialRequest = ({ setLoading(false); } }, - [notificationDetails.id, linkedGroup, dispatch, handleBack, credsCache] + [notificationDetails.id, linkedGroup, dispatch, handleBack] ); const changeToStageTwo = () => { From 9202fbfd1755c5f1b0ac9bfa8cab9f2221e51189 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Mon, 3 Nov 2025 23:34:05 +0700 Subject: [PATCH 22/28] fix(core): refine type definition for offerExnToJoin --- src/core/agent/services/ipexCommunicationService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index fe2d399e4a..4d068441ad 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -793,7 +793,7 @@ class IpexCommunicationService extends AgentService { notificationSaid: string, acdcDetail: Record, discloseePrefix: string, - offerExnToJoin?: unknown + offerExnToJoin?: Record ): Promise { if (!this.props.signifyClient.manager) { throw new Error(SIGNIFY_CLIENT_MANAGER_NOT_INITIALIZED); From 5ef88435c0bd542b345b4f48a967c514c789eb38 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Mon, 3 Nov 2025 23:42:38 +0700 Subject: [PATCH 23/28] fix(core): enhance type safety for queued group creation --- src/core/__fixtures__/agent/multiSigFixtures.ts | 2 +- src/core/agent/services/identifier.types.ts | 11 +++-------- src/core/agent/services/multiSigService.ts | 11 ++++++----- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/core/__fixtures__/agent/multiSigFixtures.ts b/src/core/__fixtures__/agent/multiSigFixtures.ts index 6bba8f552c..65baca2ea3 100644 --- a/src/core/__fixtures__/agent/multiSigFixtures.ts +++ b/src/core/__fixtures__/agent/multiSigFixtures.ts @@ -873,7 +873,7 @@ const queuedIdentifier: QueuedGroupCreation & { initiator: true } = { const queuedJoin: QueuedGroupCreation & { initiator: false } = { name: "0:testUser", - data: inceptionDataFix, + data: inceptionDataFix as CreateIdentifierBody & { group: HabState }, initiator: false, notificationId: "notification-id", notificationSaid: "notification-said", diff --git a/src/core/agent/services/identifier.types.ts b/src/core/agent/services/identifier.types.ts index 5c4b4a752e..89a950369e 100644 --- a/src/core/agent/services/identifier.types.ts +++ b/src/core/agent/services/identifier.types.ts @@ -67,6 +67,7 @@ interface MultisigThresholds { } // Discriminated union with proper type safety for group data +// Both initiator and joiner have group data when creating/joining a multisig type QueuedGroupCreation = | { initiator: true; @@ -78,17 +79,12 @@ type QueuedGroupCreation = | { initiator: false; name: string; - data: CreateIdentifierBody; + data: CreateIdentifierBody & { group: HabState }; notificationId: string; notificationSaid: string; }; -// Legacy type for backward compatibility if needed elsewhere -type QueuedIdentifierCreation = { - name: string; - data: CreateIdentifierBody; -}; - +// Helper type used in multiSigService for generating inception data type QueuedGroupProps = | { initiator: true; @@ -136,7 +132,6 @@ export type { CreateIdentifierResult, MultisigThresholds, GroupMetadata, - QueuedIdentifierCreation, QueuedGroupProps, QueuedGroupCreation, GroupParticipants, diff --git a/src/core/agent/services/multiSigService.ts b/src/core/agent/services/multiSigService.ts index ff6d04f38b..1a0c2b9d7c 100644 --- a/src/core/agent/services/multiSigService.ts +++ b/src/core/agent/services/multiSigService.ts @@ -311,11 +311,12 @@ class MultiSigService extends AgentService { }); // Build properly typed queued item based on discriminated union + // Both initiators and joiners will have group data when creating a multisig + if (!inceptionData.group) { + throw new Error(MultiSigService.GROUP_DATA_MISSING_FOR_INITIATOR); + } + if (queuedProps.initiator) { - // Ensure group data exists for initiator - if (!inceptionData.group) { - throw new Error(MultiSigService.GROUP_DATA_MISSING_FOR_INITIATOR); - } queued.push({ initiator: true, name: groupName, @@ -327,7 +328,7 @@ class MultiSigService extends AgentService { queued.push({ initiator: false, name: groupName, - data: inceptionData, + data: inceptionData as CreateIdentifierBody & { group: HabState }, notificationId: queuedProps.notificationId, notificationSaid: queuedProps.notificationSaid, }); From b13d94c3e8a48ff28c22a388d56ac7a085ae7ccd Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Tue, 4 Nov 2025 00:04:48 +0700 Subject: [PATCH 24/28] fix(core): improve type safety for ACDC --- src/core/agent/services/ipexCommunicationService.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index 4d068441ad..7dcf5f4476 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -27,6 +27,7 @@ import { import type { CredentialMetadataRecordProps } from "../records/credentialMetadataRecord.types"; import { AgentService } from "./agentService"; import { getCredentialShortDetails, OnlineOnly } from "./utils"; +import type { ACDC } from "./credentialService.types"; import { CredentialStatus, ACDCDetails, @@ -218,10 +219,7 @@ class IpexCommunicationService extends AgentService { } @OnlineOnly - async offerAcdcFromApply( - notificationId: string, - acdc: Record - ): Promise { + async offerAcdcFromApply(notificationId: string, acdc: ACDC): Promise { const applyNoteRecord = await this.notificationStorage.findById( notificationId ); @@ -791,7 +789,7 @@ class IpexCommunicationService extends AgentService { private async submitMultisigOffer( multisigId: string, notificationSaid: string, - acdcDetail: Record, + acdcDetail: ACDC, discloseePrefix: string, offerExnToJoin?: Record ): Promise { From b2d50ab39e06c4d44d101d36f9dddbf2f332666d Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Tue, 4 Nov 2025 00:23:02 +0700 Subject: [PATCH 25/28] chore(core): revert logic --- src/core/agent/services/connectionService.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/core/agent/services/connectionService.ts b/src/core/agent/services/connectionService.ts index 1b961e716f..25b9161a37 100644 --- a/src/core/agent/services/connectionService.ts +++ b/src/core/agent/services/connectionService.ts @@ -329,11 +329,7 @@ class ConnectionService extends AgentService { return { ...baseDetails, groupId: record.groupId }; } - if (!record.identifier) { - throw new Error(`Regular contact must have identifier: ${record.id}`); - } - - return { ...baseDetails, identifier: record.identifier }; + return { ...baseDetails, identifier: record.identifier || "" }; } async getConnectionById( From cd1f9f3ade0aba596908c971ea8c40e4d46c1274 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Tue, 4 Nov 2025 00:31:45 +0700 Subject: [PATCH 26/28] fix(core): improve type safety for multisig creation --- src/core/agent/services/multiSigService.ts | 33 +++++++++------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/core/agent/services/multiSigService.ts b/src/core/agent/services/multiSigService.ts index 1a0c2b9d7c..ad236e7c25 100644 --- a/src/core/agent/services/multiSigService.ts +++ b/src/core/agent/services/multiSigService.ts @@ -310,17 +310,20 @@ class MultiSigService extends AgentService { rstates: states, }); - // Build properly typed queued item based on discriminated union - // Both initiators and joiners will have group data when creating a multisig + // Type guard: Ensure group data exists for multisig creation if (!inceptionData.group) { throw new Error(MultiSigService.GROUP_DATA_MISSING_FOR_INITIATOR); } + // After this check, TypeScript knows inceptionData has group property + const inceptionDataWithGroup = inceptionData as CreateIdentifierBody & { + group: HabState; + }; if (queuedProps.initiator) { queued.push({ initiator: true, name: groupName, - data: inceptionData as CreateIdentifierBody & { group: HabState }, + data: inceptionDataWithGroup, groupConnections: queuedProps.groupConnections, threshold: queuedProps.threshold, }); @@ -328,7 +331,7 @@ class MultiSigService extends AgentService { queued.push({ initiator: false, name: groupName, - data: inceptionData as CreateIdentifierBody & { group: HabState }, + data: inceptionDataWithGroup, notificationId: queuedProps.notificationId, notificationSaid: queuedProps.notificationSaid, }); @@ -823,27 +826,19 @@ class MultiSigService extends AgentService { for (const queued of pendingGroupsRecord.content .queued as QueuedGroupCreation[]) { if (queued.initiator) { - // TypeScript narrows to initiator variant - const initiatorQueued = queued as Extract< - QueuedGroupCreation, - { initiator: true } - >; - const threshold = initiatorQueued.threshold; + // TypeScript automatically narrows to initiator variant + const threshold = queued.threshold; await this.createGroup( - initiatorQueued.data.group.mhab.prefix, - initiatorQueued.groupConnections, + queued.data.group.mhab.prefix, + queued.groupConnections, threshold, true ); } else { - // TypeScript narrows to join variant - const joinQueued = queued as Extract< - QueuedGroupCreation, - { initiator: false } - >; + // TypeScript automatically narrows to join variant await this.joinGroup( - joinQueued.notificationId, - joinQueued.notificationSaid, + queued.notificationId, + queued.notificationSaid, true ); } From 12e33c2599d26facb8a10b5ecca61975e5b52d60 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Tue, 4 Nov 2025 00:32:04 +0700 Subject: [PATCH 27/28] fix(core): rename type guard for multisig exchange messages --- src/core/agent/services/ipexCommunicationService.ts | 4 ++-- src/core/agent/services/multiSig.types.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index 7dcf5f4476..7550400d0b 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -43,7 +43,7 @@ import { MultiSigService } from "./multiSigService"; import { GrantToJoinMultisigExnPayload, MultiSigRoute, - isIpexGrantMultiSigExn, + isMultiSigExn, } from "./multiSig.types"; import { AcdcStateChangedEvent, EventTypes } from "../event.types"; import { ConnectionService } from "./connectionService"; @@ -747,7 +747,7 @@ class IpexCommunicationService extends AgentService { } // Type narrowing: Validate multiSigExn structure first - if (!isIpexGrantMultiSigExn(multiSigExn)) { + if (!isMultiSigExn(multiSigExn)) { throw new Error( "Invalid multisig exchange structure for joinMultisigGrant: missing required fields" ); diff --git a/src/core/agent/services/multiSig.types.ts b/src/core/agent/services/multiSig.types.ts index 1cd4d142c0..19fa1c08a0 100644 --- a/src/core/agent/services/multiSig.types.ts +++ b/src/core/agent/services/multiSig.types.ts @@ -114,8 +114,8 @@ interface IpexGrantMultiSigExn { }; } -// Type guard for IpexGrantMultiSigExn -function isIpexGrantMultiSigExn(obj: unknown): obj is IpexGrantMultiSigExn { +// Type guard for multisig exchange messages +function isMultiSigExn(obj: unknown): obj is IpexGrantMultiSigExn { if (typeof obj !== "object" || obj === null) return false; const candidate = obj as { @@ -191,7 +191,7 @@ interface GroupInformation { members: GroupMemberInfo[]; } -export { MultiSigRoute, isIpexGrantMultiSigExn }; +export { MultiSigRoute, isMultiSigExn }; export type { RotationMultiSigExnMessage, From 2e8005e361b93af0ea43e1d85ab4bcef62d36a90 Mon Sep 17 00:00:00 2001 From: Sotatek-DucPhung Date: Wed, 5 Nov 2025 16:20:49 +0700 Subject: [PATCH 28/28] fix(core): add type guard for group inception data in multisig service --- src/core/agent/services/identifier.types.ts | 11 ++++++++--- src/core/agent/services/multiSigService.ts | 14 ++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/core/agent/services/identifier.types.ts b/src/core/agent/services/identifier.types.ts index 89a950369e..7a5e9818ba 100644 --- a/src/core/agent/services/identifier.types.ts +++ b/src/core/agent/services/identifier.types.ts @@ -66,8 +66,6 @@ interface MultisigThresholds { rotationThreshold: number; } -// Discriminated union with proper type safety for group data -// Both initiator and joiner have group data when creating/joining a multisig type QueuedGroupCreation = | { initiator: true; @@ -84,6 +82,13 @@ type QueuedGroupCreation = notificationSaid: string; }; +// Type guard to check if CreateIdentifierBody has group data +function isGroupInceptionData( + data: CreateIdentifierBody +): data is CreateIdentifierBody & { group: HabState } { + return data.group !== undefined; +} + // Helper type used in multiSigService for generating inception data type QueuedGroupProps = | { @@ -122,7 +127,7 @@ interface RemoteSignRequest { payload: JSONObject; } -export { IdentifierType }; +export { IdentifierType, isGroupInceptionData }; export type { IdentifierShortDetails, diff --git a/src/core/agent/services/multiSigService.ts b/src/core/agent/services/multiSigService.ts index ad236e7c25..a3ac6dd09d 100644 --- a/src/core/agent/services/multiSigService.ts +++ b/src/core/agent/services/multiSigService.ts @@ -36,6 +36,7 @@ import { MultiSigIcpRequestDetails, QueuedGroupCreation, QueuedGroupProps, + isGroupInceptionData, } from "./identifier.types"; import type { MultisigThresholds } from "./identifier.types"; import { @@ -310,20 +311,15 @@ class MultiSigService extends AgentService { rstates: states, }); - // Type guard: Ensure group data exists for multisig creation - if (!inceptionData.group) { + if (!isGroupInceptionData(inceptionData)) { throw new Error(MultiSigService.GROUP_DATA_MISSING_FOR_INITIATOR); } - // After this check, TypeScript knows inceptionData has group property - const inceptionDataWithGroup = inceptionData as CreateIdentifierBody & { - group: HabState; - }; if (queuedProps.initiator) { queued.push({ initiator: true, name: groupName, - data: inceptionDataWithGroup, + data: inceptionData, groupConnections: queuedProps.groupConnections, threshold: queuedProps.threshold, }); @@ -331,7 +327,7 @@ class MultiSigService extends AgentService { queued.push({ initiator: false, name: groupName, - data: inceptionDataWithGroup, + data: inceptionData, notificationId: queuedProps.notificationId, notificationSaid: queuedProps.notificationSaid, }); @@ -826,7 +822,6 @@ class MultiSigService extends AgentService { for (const queued of pendingGroupsRecord.content .queued as QueuedGroupCreation[]) { if (queued.initiator) { - // TypeScript automatically narrows to initiator variant const threshold = queued.threshold; await this.createGroup( queued.data.group.mhab.prefix, @@ -835,7 +830,6 @@ class MultiSigService extends AgentService { true ); } else { - // TypeScript automatically narrows to join variant await this.joinGroup( queued.notificationId, queued.notificationSaid,