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
359 changes: 354 additions & 5 deletions src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ import {
} from "@/model/entities/types/IdentityTypes"
import log from "@/utilities/logger"
import { IncentiveManager } from "./IncentiveManager"
import {
verifyTLSNotaryPresentation,
parseHttpResponse,
extractUser,
type TLSNotaryPresentation,
type TLSNIdentityContext,
} from "@/libs/tlsnotary"

export default class GCRIdentityRoutines {
// SECTION XM Identity Routines
Expand Down Expand Up @@ -261,9 +268,9 @@ export default class GCRIdentityRoutines {
context === "telegram"
? "Telegram attestation validation failed"
: "Sha256 proof mismatch: Expected " +
data.proofHash +
" but got " +
Hashing.sha256(data.proof),
data.proofHash +
" but got " +
Hashing.sha256(data.proof),
}
}

Expand Down Expand Up @@ -575,8 +582,9 @@ export default class GCRIdentityRoutines {
if (!validNetworks.includes(payload.network)) {
return {
success: false,
message: `Invalid network: ${payload.network
}. Must be one of: ${validNetworks.join(", ")}`,
message: `Invalid network: ${
payload.network
}. Must be one of: ${validNetworks.join(", ")}`,
}
}
if (!validRegistryTypes.includes(payload.registryType)) {
Expand Down Expand Up @@ -869,6 +877,20 @@ export default class GCRIdentityRoutines {
simulate,
)
break
case "tlsnadd":
result = await this.applyTLSNIdentityAdd(
identityEdit,
gcrMainRepository,
simulate,
)
break
case "tlsnremove":
result = await this.applyTLSNIdentityRemove(
identityEdit,
gcrMainRepository,
simulate,
)
break
default:
result = {
success: false,
Expand Down Expand Up @@ -1122,4 +1144,331 @@ export default class GCRIdentityRoutines {

return { success: true, message: "Nomis identity removed" }
}

// SECTION TLSNotary Identity Routines

/**
* Expected API endpoints for TLSN verification per context
*/
private static TLSN_EXPECTED_ENDPOINTS: Record<
string,
{ server: string; pathPrefix: string }
> = {
github: { server: "api.github.com", pathPrefix: "/user" },
discord: { server: "discord.com", pathPrefix: "/api/users/@me" },
telegram: {
server: "telegram-backend",
pathPrefix: "/api/telegram/user",
},
}

/**
* Add an identity via TLSNotary proof verification.
*
* This method performs cryptographic verification of the TLSNotary proof,
* extracts the proven data, and compares it with the claimed values.
* Only stores the identity if the proof is valid and claims match.
*
* Security: Data is extracted directly from the cryptographic proof,
* never trusting client-provided claims without verification.
Comment on lines +1165 to +1173
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Doc comment claims cryptographic verification and "never trusting client-provided claims" — both are currently false.

The JSDoc states the method "performs cryptographic verification" and "Data is extracted directly from the cryptographic proof, never trusting client-provided claims without verification." In reality, since verifyTLSNotaryPresentation returns no recv data, the code always falls to the else branch (line 1325) which explicitly trusts client claims. This doc will mislead any reviewer or auditor.

Update the doc to accurately reflect the current trust model.

🤖 Prompt for AI Agents
In `@src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts` around lines
1165 - 1173, The JSDoc for the "Add an identity via TLSNotary proof
verification" routine is inaccurate: the code path currently falls back to
trusting client-provided claims because verifyTLSNotaryPresentation returns no
recv data, so cryptographic verification is not performed. Update the comment on
the method in GCRIdentityRoutines.ts to state the current behavior: that
verifyTLSNotaryPresentation does not yield cryptographically-extracted recv data
and the implementation therefore accepts client-supplied claims in the fallback
branch (where the code uses the client-provided values), and remove or soften
any absolute statements like "never trusting client-provided claims" and
"performs cryptographic verification."

*/
static async applyTLSNIdentityAdd(
editOperation: any,
gcrMainRepository: Repository<GCRMain>,
simulate: boolean,
): Promise<GCRResult> {
// Extract context from editOperation.data (top level)
const { context } = editOperation.data
// Extract nested data fields (proof, username, userId are inside data.data)
const {
proof: proofString,
username,
userId,
} = editOperation.data.data || {}
// referralCode is at the editOperation level
const referralCode = editOperation.referralCode

// Parse the proof JSON string back to object
let proof: any
try {
proof =
typeof proofString === "string"
? JSON.parse(proofString)
: proofString
} catch (e) {
return {
success: false,
message: "Invalid proof: failed to parse proof JSON string",
}
}

// 1. Validate context is supported
const expected = this.TLSN_EXPECTED_ENDPOINTS[context]
if (!expected) {
return {
success: false,
message: `Unsupported TLSN context: ${context}`,
}
}
Comment on lines +1180 to +1212
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing validation on destructured fields from editOperation.data.data.

Line 1187 uses editOperation.data.data || {}, so if data.data is undefined, all destructured fields (proofString, username, userId) become undefined. The method continues to line 1206 where context (from editOperation.data) is checked, but username and userId are never validated before being used (e.g., logged at line 1232, stored at line 1353).

Add early validation for required fields:

Proposed fix
         const {
             proof: proofString,
             username,
             userId,
         } = editOperation.data.data || {}
         // referralCode is at the editOperation level
         const referralCode = editOperation.referralCode
 
+        if (!username || userId === undefined || userId === null) {
+            return {
+                success: false,
+                message: "Invalid payload: missing username or userId",
+            }
+        }
+
         // Parse the proof JSON string back to object
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Extract context from editOperation.data (top level)
const { context } = editOperation.data
// Extract nested data fields (proof, username, userId are inside data.data)
const {
proof: proofString,
username,
userId,
} = editOperation.data.data || {}
// referralCode is at the editOperation level
const referralCode = editOperation.referralCode
// Parse the proof JSON string back to object
let proof: any
try {
proof =
typeof proofString === "string"
? JSON.parse(proofString)
: proofString
} catch (e) {
return {
success: false,
message: "Invalid proof: failed to parse proof JSON string",
}
}
// 1. Validate context is supported
const expected = this.TLSN_EXPECTED_ENDPOINTS[context]
if (!expected) {
return {
success: false,
message: `Unsupported TLSN context: ${context}`,
}
}
// Extract context from editOperation.data (top level)
const { context } = editOperation.data
// Extract nested data fields (proof, username, userId are inside data.data)
const {
proof: proofString,
username,
userId,
} = editOperation.data.data || {}
// referralCode is at the editOperation level
const referralCode = editOperation.referralCode
if (!username || userId === undefined || userId === null) {
return {
success: false,
message: "Invalid payload: missing username or userId",
}
}
// Parse the proof JSON string back to object
let proof: any
try {
proof =
typeof proofString === "string"
? JSON.parse(proofString)
: proofString
} catch (e) {
return {
success: false,
message: "Invalid proof: failed to parse proof JSON string",
}
}
// 1. Validate context is supported
const expected = this.TLSN_EXPECTED_ENDPOINTS[context]
if (!expected) {
return {
success: false,
message: `Unsupported TLSN context: ${context}`,
}
}
🤖 Prompt for AI Agents
In `@src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts` around lines
1180 - 1212, The destructured fields proofString, username, and userId (from
editOperation.data.data) are not validated and can be undefined; add explicit
checks in the GCRIdentityRoutines method after parsing proof (and/or immediately
after destructuring) to ensure username and userId are present and non-empty
(and that proof was successfully parsed), returning a failure object with a
clear message (e.g., "Missing required field: username" or "Missing required
field: userId") if they are missing; keep existing context validation
(this.TLSN_EXPECTED_ENDPOINTS[context]) but perform these new validations before
any use of username/userId (e.g., before logging or storing at later lines) to
prevent downstream errors.


// 2. Validate proof structure
if (!proof || typeof proof !== "object") {
return {
success: false,
message:
"Invalid proof: expected TLSNotary presentation object",
}
}

if (!proof.data || !proof.version) {
return {
success: false,
message: "Invalid proof structure: missing data or version",
}
}

// 3. Verify proof using WASM
log.info(
`[TLSN Identity] Verifying proof for ${context} identity: ${username}`,
)
Comment on lines +1231 to +1233
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

1. Pii in tlsn logs 📘 Rule violation ⛨ Security

The new TLSN identity flow logs username and userId, which can be considered personal user
information and must not appear in application logs. These logs are also plain interpolated strings,
making them harder to audit/parse consistently.
Agent Prompt
## Issue description
TLSN identity-related log messages include personal user information (`username`, `userId`) and are emitted as unstructured interpolated strings.

## Issue Context
Compliance requires logs to avoid sensitive/personal user data and be useful for auditing. Identity operations are security-sensitive, so logging must be carefully redacted and ideally structured.

## Fix Focus Areas
- src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts[1231-1331]
- src/libs/tlsnotary/verifier.ts[326-328]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

const verified = await verifyTLSNotaryPresentation(
proof as TLSNotaryPresentation,
)

if (!verified.success) {
log.warn(
`[TLSN Identity] Proof verification failed: ${verified.error}`,
)
return {
success: false,
message: `Proof verification failed: ${verified.error}`,
}
Comment on lines +1238 to +1245
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

2. Internal errors returned to clients 📘 Rule violation ⛨ Security

Several TLSN verification failures return verified.error and other internal details (e.g.,
expected/actual server names) in the message, which can leak implementation details to end-users.
Detailed failure context should be kept in internal logs, while client-facing messages remain
generic.
Agent Prompt
## Issue description
TLSN-related APIs return internal verification details (`verified.error`, server mismatch specifics) in response messages, risking information leakage.

## Issue Context
Client-facing errors should be generic per secure error-handling requirements; detailed context should be logged internally for debugging.

## Fix Focus Areas
- src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts[1238-1260]
- src/libs/tlsnotary/verifier.ts[317-324]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}

// 4. Check server name matches expected (skip if WASM verification disabled)
// When WASM is disabled, serverName is not extracted from proof
// We trust the frontend's cryptographic verification in this mode
if (verified.verifyingKey !== "structure-validation-only") {
if (verified.serverName !== expected.server) {
log.warn(
`[TLSN Identity] Server mismatch: expected ${expected.server}, got ${verified.serverName}`,
)
return {
success: false,
message: `Server mismatch: expected ${expected.server}, got ${verified.serverName}`,
}
}
} else {
log.info(
`[TLSN Identity] Skipping serverName check (structure-validation-only mode)`,
)
}

// 5. Parse HTTP response and extract user data (if WASM provided recv data)
let extractedUser: { username: string; userId: string } | null = null

// 5. Parse HTTP response and extract user data
// if (!verified.recv) {
// return {
// success: false,
// message: "No response data in proof",
// }
// }

if (verified.recv) {
const httpResponse = parseHttpResponse(verified.recv)
if (!httpResponse) {
return {
success: false,
message: "Failed to parse HTTP response from proof",
}
}

// 6. Extract user data based on context
extractedUser = extractUser(
context as TLSNIdentityContext,
httpResponse.body,
)

if (!extractedUser) {
return {
success: false,
message: `Failed to extract user data from ${context} response`,
}
}

// 7. CRITICAL SECURITY CHECK: Compare claimed vs extracted values
if (extractedUser.username !== username) {
log.warn(
`[TLSN Identity] Username mismatch: claimed "${username}", proof contains "${extractedUser.username}"`,
)
return {
success: false,
message: `Username mismatch: claimed "${username}", proof contains "${extractedUser.username}"`,
}
}

if (extractedUser.userId !== String(userId)) {
log.warn(
`[TLSN Identity] UserId mismatch: claimed "${userId}", proof contains "${extractedUser.userId}"`,
)
return {
success: false,
message: `UserId mismatch: claimed "${userId}", proof contains "${extractedUser.userId}"`,
}
}

log.info(
// `[TLSN Identity] Proof verified successfully for ${context}: ${username} (${userId})`,
`[TLSN Identity] Proof verified with WASM for ${context}: ${username} (${userId})`,
)
} else {
// WASM verification disabled - trust claimed data with warning
// NOTE: This is less secure but allows operation until WASM works in Node.js
log.warn(
`[TLSN Identity] WASM disabled - trusting claimed data for ${context}: ${username} (${userId})`,
)
extractedUser = { username, userId: String(userId) }
}
Comment on lines +1326 to +1332
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

3. Tlsn add trusts client claims 📘 Rule violation ⛨ Security

When operating in structure-validation-only mode (or when verified.recv is absent), the code
proceeds by trusting client-provided username/userId rather than deriving them from a verified
proof. This is insufficient validation for an externally supplied identity claim and can enable
identity spoofing.
Agent Prompt
## Issue description
TLSN identity assignment can succeed without server-side cryptographic verification by trusting client claims when running in structure-only verification mode.

## Issue Context
Identity binding is security-sensitive. Validating only proof structure is insufficient to authenticate the claimed `username`/`userId`.

## Fix Focus Areas
- src/libs/tlsnotary/verifier.ts[1-167]
- src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts[1230-1332]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1267 to +1332
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

The recv-based extraction path is dead code; the else branch always executes.

verifyTLSNotaryPresentation never returns recv in its result (see verifier.ts line 156-160 — only success, time, verifyingKey are returned). So the if (verified.recv) block (lines 1278-1324) — including the critical security checks — never executes. The else branch (lines 1325-1332) always runs, unconditionally trusting client claims.

The commented-out block (lines 1271-1276) reinforces that this was recognized but deferred. At minimum, add a clear // TODO at the top of the method noting that all identity claims are currently unverified, and remove or gate the dead code to avoid giving a false impression of security.

🤖 Prompt for AI Agents
In `@src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts` around lines
1267 - 1332, The code block that depends on verified.recv is dead because
verifyTLSNotaryPresentation never returns recv; update the TLSN verification
flow in GCRIdentityRoutines.verify... to avoid implying proof verification: add
a top-of-function TODO comment stating "identity claims are currently unverified
— WASM recv path disabled" and either remove/disable the entire recv-based
branch (the if (verified.recv) block that calls parseHttpResponse, extractUser,
and performs username/userId checks) or gate it explicitly behind a clearly
named feature flag (e.g., ENABLE_WASM_RECV) so it cannot run accidentally; also
update the log messages (the else branch and any success logs) to make it
explicit they are trusting client claims, and keep references to
verifyTLSNotaryPresentation, verified.recv, parseHttpResponse, extractUser, and
TLSNIdentityContext to locate the code.


// 8. Get/create GCR and check for duplicates
const accountGCR = await ensureGCRForUser(editOperation.account)

accountGCR.identities.web2 = accountGCR.identities.web2 || {}
accountGCR.identities.web2[context] =
accountGCR.identities.web2[context] || []

// Check if identity already exists (by userId to prevent duplicate registrations)
const exists = accountGCR.identities.web2[context].some(
(id: Web2GCRData["data"]) => id.userId === String(userId),
)

if (exists) {
return { success: false, message: "Identity already exists" }
}

// 9. Prepare data for storage
const proofHash = Hashing.sha256(JSON.stringify(proof))
const data = {
userId: String(userId),
username: username,
proof: proof, // Store full TLSNotary proof for re-verification
proofHash: proofHash,
proofType: "tlsn", // Mark as TLSNotary-verified
timestamp: Date.now(),
}

accountGCR.identities.web2[context].push(data)

// 10. Save and award incentives
if (!simulate) {
await gcrMainRepository.save(accountGCR)

if (context === "github") {
const isFirst = await this.isFirstConnection(
"github",
{ userId: String(userId) },
gcrMainRepository,
editOperation.account,
)

if (isFirst) {
await IncentiveManager.githubLinked(
editOperation.account,
String(userId),
referralCode,
)
}
} else if (context === "discord") {
const isFirst = await this.isFirstConnection(
"discord",
{ userId: String(userId) },
gcrMainRepository,
editOperation.account,
)

if (isFirst) {
await IncentiveManager.discordLinked(
editOperation.account,
referralCode,
)
}
} else if (context === "telegram") {
const isFirst = await this.isFirstConnection(
"telegram",
{ userId: String(userId) },
gcrMainRepository,
editOperation.account,
)

if (isFirst) {
await IncentiveManager.telegramLinked(
editOperation.account,
String(userId),
referralCode,
)
}
Comment on lines +1396 to +1410
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the telegramLinked function definition to understand its signature
rg -n 'telegramLinked\s*\(' --type=ts -A5 | head -100

Repository: kynesyslabs/node

Length of output: 1948


🏁 Script executed:

# Look at the specific lines mentioned in the review
sed -n '322,327p' src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts
sed -n '1396,1410p' src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts

Repository: kynesyslabs/node

Length of output: 916


🏁 Script executed:

# Find the file containing IncentiveManager to locate telegramLinked
fd -t f -e ts -e js | xargs rg -l 'telegramLinked' | grep -i incentive

Repository: kynesyslabs/node

Length of output: 116


🏁 Script executed:

# Get the full implementation of telegramLinked method
sed -n '93,150p' src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts

Repository: kynesyslabs/node

Length of output: 1699


🏁 Script executed:

# Also search for references to attestation or group_membership in IncentiveManager
rg -n 'attestation|group_membership' src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts -A2

Repository: kynesyslabs/node

Length of output: 278


🏁 Script executed:

# Find awardTelegramPoints method and check if it uses attestation
rg -n 'awardTelegramPoints' --type=ts -A20

Repository: kynesyslabs/node

Length of output: 3610


🏁 Script executed:

# Also check if group_membership is referenced anywhere
rg -n 'group_membership' --type=ts -B2 -A2

Repository: kynesyslabs/node

Length of output: 1463


🏁 Script executed:

# Check what happens when isGroupMember is false
sed -n '880,920p' src/features/incentive/PointSystem.ts

Repository: kynesyslabs/node

Length of output: 1604


🏁 Script executed:

# Check what variables are available in the TLSN path context around line 1396-1410
sed -n '1370,1420p' src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts

Repository: kynesyslabs/node

Length of output: 1755


🏁 Script executed:

# Also check if there's a proof/attestation variable available in the TLSN context
sed -n '1360,1430p' src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts | head -100

Repository: kynesyslabs/node

Length of output: 2361


Pass data.proof (TelegramSignedAttestation) as the fourth argument to IncentiveManager.telegramLinked.

The TLSN path omits the attestation parameter that the Web2 path (line 326) passes as data.proof. The awardTelegramPoints method checks attestation?.payload?.group_membership === true to determine eligibility for incentive points. Without it, the check evaluates to false and users receive 0 points with the message "you must join the required group to earn points"—even though points should be awarded on successful linking for group members. Pass data.proof as the fourth argument to match the Web2 path behavior.

🤖 Prompt for AI Agents
In `@src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts` around lines
1396 - 1410, The TLSN branch calling IncentiveManager.telegramLinked is missing
the attestation argument; update the call in GCRIdentityRoutines (where
isFirstConnection is used) to pass data.proof as the fourth parameter so the
call becomes IncentiveManager.telegramLinked(editOperation.account,
String(userId), referralCode, data.proof), matching the Web2 path and allowing
awardTelegramPoints to inspect attestation.payload.group_membership.

}
}

return { success: true, message: "TLSN identity added successfully" }
}

/**
* Remove an identity that was added via TLSNotary.
*
* Removes the identity from the web2 identities storage.
*/
static async applyTLSNIdentityRemove(
editOperation: any,
gcrMainRepository: Repository<GCRMain>,
simulate: boolean,
): Promise<GCRResult> {
const { context, username } = editOperation.data

if (!context || !username) {
return {
success: false,
message: "Invalid payload: missing context or username",
}
}

const accountGCR = await ensureGCRForUser(editOperation.account)

accountGCR.identities.web2 = accountGCR.identities.web2 || {}
accountGCR.identities.web2[context] =
accountGCR.identities.web2[context] || []

// Find the identity to remove
const identity = accountGCR.identities.web2[context].find(
(id: Web2GCRData["data"]) => id.username === username,
)

if (!identity) {
return { success: false, message: "Identity not found" }
}
Comment on lines +1422 to +1449
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

applyTLSNIdentityRemove uses ensureGCRForUser instead of findOneBy — may create empty GCR records on remove.

Other remove methods (e.g., applyXmIdentityRemove at line 142, applyNomisIdentityRemove at line 1100) use gcrMainRepository.findOneBy(...) and return "Account not found" when the account doesn't exist. This method uses ensureGCRForUser, which creates a new GCR record if one doesn't exist, then proceeds to find no matching identity. This is wasteful and inconsistent.

Proposed fix
     static async applyTLSNIdentityRemove(
         editOperation: any,
         gcrMainRepository: Repository<GCRMain>,
         simulate: boolean,
     ): Promise<GCRResult> {
         const { context, username } = editOperation.data
 
         if (!context || !username) {
             return {
                 success: false,
                 message: "Invalid payload: missing context or username",
             }
         }
 
-        const accountGCR = await ensureGCRForUser(editOperation.account)
+        const accountGCR = await gcrMainRepository.findOneBy({
+            pubkey: editOperation.account,
+        })
+
+        if (!accountGCR) {
+            return { success: false, message: "Account not found" }
+        }
 
         accountGCR.identities.web2 = accountGCR.identities.web2 || {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static async applyTLSNIdentityRemove(
editOperation: any,
gcrMainRepository: Repository<GCRMain>,
simulate: boolean,
): Promise<GCRResult> {
const { context, username } = editOperation.data
if (!context || !username) {
return {
success: false,
message: "Invalid payload: missing context or username",
}
}
const accountGCR = await ensureGCRForUser(editOperation.account)
accountGCR.identities.web2 = accountGCR.identities.web2 || {}
accountGCR.identities.web2[context] =
accountGCR.identities.web2[context] || []
// Find the identity to remove
const identity = accountGCR.identities.web2[context].find(
(id: Web2GCRData["data"]) => id.username === username,
)
if (!identity) {
return { success: false, message: "Identity not found" }
}
static async applyTLSNIdentityRemove(
editOperation: any,
gcrMainRepository: Repository<GCRMain>,
simulate: boolean,
): Promise<GCRResult> {
const { context, username } = editOperation.data
if (!context || !username) {
return {
success: false,
message: "Invalid payload: missing context or username",
}
}
const accountGCR = await gcrMainRepository.findOneBy({
pubkey: editOperation.account,
})
if (!accountGCR) {
return { success: false, message: "Account not found" }
}
accountGCR.identities.web2 = accountGCR.identities.web2 || {}
accountGCR.identities.web2[context] =
accountGCR.identities.web2[context] || []
// Find the identity to remove
const identity = accountGCR.identities.web2[context].find(
(id: Web2GCRData["data"]) => id.username === username,
)
if (!identity) {
return { success: false, message: "Identity not found" }
}
🤖 Prompt for AI Agents
In `@src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts` around lines
1422 - 1449, The applyTLSNIdentityRemove function currently calls
ensureGCRForUser which can create a new GCR record; change it to load the record
with gcrMainRepository.findOneBy({ account: editOperation.account }) (same
pattern as applyXmIdentityRemove / applyNomisIdentityRemove), and if no record
is found return { success: false, message: "Account not found" } instead of
proceeding; keep the rest of the logic that accesses accountGCR.identities.web2
the same so you only avoid creating empty GCRs when removing an identity.


// Filter out the identity
accountGCR.identities.web2[context] = accountGCR.identities.web2[
context
].filter((id: Web2GCRData["data"]) => id.username !== username)

Comment on lines +1427 to +1455
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

5. Tlsn remove deletes non-tlsn 🐞 Bug ✓ Correctness

applyTLSNIdentityRemove deletes identities purely by context+username and does not check that the
record was added via TLSN (proofType). This allows a tlsn_remove operation to remove legacy web2
identities and trigger incentive rollback unexpectedly.
Agent Prompt
### Issue description
`applyTLSNIdentityRemove` removes any web2 identity matching `username` without checking `proofType === "tlsn"`, and without validating `context` is one of the TLSN-supported platforms. This can delete non-TLSN identities.

### Issue Context
TLSN identities are stored in the same `accountGCR.identities.web2[context]` arrays used for regular web2 identities.

### Fix Focus Areas
- src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts[1422-1470]
- src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts[197-415]

### Suggested changes
1. Validate `context` in remove (e.g., only `github|discord|telegram`).
2. When finding/filtering, require the record to be TLSN-added, e.g. `(id as any).proofType === "tlsn"`.
3. Prefer removing by `userId` (or both `userId` and `username`) to avoid removing the wrong record when usernames collide or change.
4. Only trigger TLSN incentive rollback when the removed record is confirmed TLSN provenance.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

if (!simulate) {
await gcrMainRepository.save(accountGCR)

// Trigger incentive rollback if applicable
if (context === "github" && identity.userId) {
await IncentiveManager.githubUnlinked(
editOperation.account,
identity.userId,
)
} else if (context === "discord") {
await IncentiveManager.discordUnlinked(editOperation.account)
} else if (context === "telegram") {
await IncentiveManager.telegramUnlinked(editOperation.account)
}
}

return { success: true, message: "TLSN identity removed successfully" }
Comment on lines +1456 to +1472
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

4. Tlsn remove lacks audit log 📘 Rule violation ✧ Quality

The TLSN identity removal operation performs a write (gcrMainRepository.save) without emitting an
audit log capturing who performed the action, what changed, and the outcome. This makes it harder to
reconstruct identity changes during incident response or compliance reviews.
Agent Prompt
## Issue description
TLSN identity removal writes state without an accompanying audit log entry describing the action and outcome.

## Issue Context
Identity add/remove operations are critical security events and should be traceable without exposing sensitive user data.

## Fix Focus Areas
- src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts[1422-1473]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
}
Loading