Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button, ButtonGroup, GridItem, SimpleGrid, Stack, Text } from "@chakra-
import { KeyRoundIcon, XIcon } from "lucide-react";

import { useEffect } from "react";
import { useFetchGrants } from "../shared";
import { safeParse, useFetchGrants } from "../shared";
import type { idOSCredentialWithShares } from "../types";

type CredentialCardProps = {
Expand All @@ -18,7 +18,7 @@ export const CredentialCard = ({
onManageGrants,
onDelete,
}: CredentialCardProps) => {
const publicFields = JSON.parse(credential.public_notes);
const publicFields = safeParse(credential.public_notes);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the safeParse definition
find . -name "index.ts" | grep -i shared | head -20

Repository: idos-network/idos-sdk-js

Length of output: 143


🏁 Script executed:

# Search for safeParse definition and all usages
rg -n "safeParse" --type=ts -B2 -A5

Repository: idos-network/idos-sdk-js

Length of output: 11905


🏁 Script executed:

# Look specifically in the credentials shared folder
find . -path "*/credentials/shared/index.ts"

Repository: idos-network/idos-sdk-js

Length of output: 143


safeParse does not guard against JSON.parse("null")Object.entries will crash on null values.

If credential.public_notes contains the string "null", JSON.parse("null") succeeds and returns JavaScript null. This bypasses the catch block, and then Object.entries(null) on line 24 throws TypeError: Cannot convert undefined or null to object.

The same issue affects delete-credential.tsx (line 154, property access) and credential-details.tsx (line 69, property access).

Fix by adding a post-parse guard in apps/idos-data-dashboard/src/routes/dashboard/credentials/shared/index.ts:

Proposed fix
 export const safeParse = <T = Record<string, unknown>>(json?: string | null): T => {
   try {
-    return JSON.parse(json ?? "{}") as T;
+    const parsed = JSON.parse(json ?? "{}");
+    if (parsed == null || typeof parsed !== "object") return {} as T;
+    return parsed as T;
   } catch (_e) {
     return {} as T;
   }
 };
🤖 Prompt for AI Agents
In
`@apps/idos-data-dashboard/src/routes/dashboard/credentials/components/credential-card.tsx`
at line 21, safeParse currently returns JavaScript null when given JSON like
"null", which later causes Object.entries or property access to throw; update
safeParse in
apps/idos-data-dashboard/src/routes/dashboard/credentials/shared/index.ts so
that after JSON.parse it checks if the result is null or not an object and
returns a safe default (e.g., {} or an empty string as appropriate) instead of
null. Ensure callers like credential-card.tsx (uses
safeParse(credential.public_notes)), delete-credential.tsx (property access from
parsed public_notes), and credential-details.tsx handle the normalized return
and remove any direct assumptions that the parse can yield non-objects. Also add
unit/inline test or a comment noting that safeParse normalizes "null" to {} to
prevent Object.entries/type errors.

const shares = useFetchGrants({ credentialId: credential.id });

const meta = Object.entries(publicFields)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { base64Decode, utf8Decode } from "@idos-network/utils/codecs";
import { useQuery } from "@tanstack/react-query";
import { DownloadIcon } from "lucide-react";
import { useIdOS } from "@/idOS.provider";
import { safeParse } from "../shared";

const useFetchCredentialDetails = ({ credentialId }: { credentialId: string }) => {
const idOSClient = useIdOS();
Expand Down Expand Up @@ -81,7 +82,7 @@ export const CredentialDetails = ({ isOpen, credentialId, onClose }: CredentialD
})()
: "No content to display";

const meta = credential.data?.public_notes ? JSON.parse(credential.data.public_notes) : {};
const meta = safeParse<{ type?: string; issuer?: string }>(credential.data?.public_notes);

const downloadFileName = credential.data?.public_notes
? `${meta.type || "credential"}_${meta.issuer || "unknown"}.json`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { useRef } from "react";
import { useIdOS } from "@/idOS.provider";

import { timelockToMs } from "../../utils/time";
import { useFetchGrants, useRevokeGrants } from "../shared";
import { safeParse, useFetchGrants, useRevokeGrants } from "../shared";

type DeleteCredentialProps = {
isOpen: boolean;
Expand Down Expand Up @@ -159,7 +159,7 @@ export const DeleteCredential = ({ isOpen, credential, onClose }: DeleteCredenti
const [currentToRevoke] = state;
const { ag_grantee_wallet_identifier } = currentToRevoke ?? {};

const meta = JSON.parse(credential.public_notes);
const meta = safeParse<{ type?: string; issuer?: string }>(credential.public_notes);

return (
<AlertDialog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ import { useIdOS } from "@/idOS.provider";

import type { idOSCredentialWithShares } from "../types";

/**
* Safely parses JSON string with fallback to empty object
* @param json - JSON string to parse (can be null or undefined)
* @returns Parsed object or empty object if parsing fails
* @template T - Expected return type (defaults to Record<string, unknown>)
*/
export const safeParse = <T = Record<string, unknown>>(json?: string | null): T => {
try {
return JSON.parse(json ?? "{}") as T;
} catch (_e) {
return {} as T;
}
};

export const useFetchGrants = ({ credentialId }: { credentialId: string }) => {
const idOSClient = useIdOS();
const queryClient = useQueryClient();
Expand Down
Loading