Merge branch 'master' into claimview-fullfills-offer

This commit is contained in:
2025-09-08 04:36:48 -04:00
278 changed files with 25504 additions and 4850 deletions

View File

@@ -60,6 +60,7 @@ import { PlanSummaryRecord } from "../interfaces/records";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { APP_SERVER } from "@/constants/app";
import { SOMEONE_UNNAMED } from "@/constants/entities";
/**
* Standard context for schema.org data
@@ -309,7 +310,7 @@ export function didInfoForContact(
showDidForVisible: boolean = false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): { known: boolean; displayName: string; profileImageUrl?: string } {
if (!did) return { displayName: "Someone Unnamed/Unknown", known: false };
if (!did) return { displayName: SOMEONE_UNNAMED, known: false };
if (did === activeDid) {
return { displayName: "You", known: true };
} else if (contact) {
@@ -485,6 +486,15 @@ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500,
});
/**
* Tracks in-flight requests to prevent duplicate API calls for the same plan
* @constant {Map}
*/
const inFlightRequests = new Map<
string,
Promise<PlanSummaryRecord | undefined>
>();
/**
* Retrieves plan data from cache or server
* @param {string} handleId - Plan handle ID
@@ -504,40 +514,140 @@ export async function getPlanFromCache(
if (!handleId) {
return undefined;
}
let cred = planCache.get(handleId);
if (!cred) {
const url =
apiServer +
"/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId);
const headers = await getHeaders(requesterDid);
try {
const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) {
cred = resp.data.data[0];
planCache.set(handleId, cred);
} else {
// Use debug level for development to reduce console noise
const isDevelopment = process.env.VITE_PLATFORM === "development";
const log = isDevelopment ? logger.debug : logger.log;
log(
"[EndorserServer] Plan cache is empty for handle",
handleId,
" Got data:",
JSON.stringify(resp.data),
);
}
} catch (error) {
logger.error(
"[EndorserServer] Failed to load plan with handle",
handleId,
" Got error:",
JSON.stringify(error),
);
}
// Check cache first (existing behavior)
const cred = planCache.get(handleId);
if (cred) {
return cred;
}
// Check if request is already in flight (NEW: request deduplication)
if (inFlightRequests.has(handleId)) {
logger.debug(
"[Plan Loading] 🔄 Request already in flight, reusing promise:",
{
handleId,
requesterDid,
timestamp: new Date().toISOString(),
},
);
return inFlightRequests.get(handleId);
}
// Create new request promise (NEW: request coordination)
const requestPromise = performPlanRequest(
handleId,
axios,
apiServer,
requesterDid,
);
inFlightRequests.set(handleId, requestPromise);
try {
const result = await requestPromise;
return result;
} finally {
// Clean up in-flight request tracking (NEW: cleanup)
inFlightRequests.delete(handleId);
}
}
/**
* Performs the actual plan request to the server
* @param {string} handleId - Plan handle ID
* @param {Axios} axios - Axios instance
* @param {string} apiServer - API server URL
* @param {string} [requesterDid] - Optional requester DID for private info
* @returns {Promise<PlanSummaryRecord|undefined>} Plan data or undefined if not found
*
* @throws {Error} If server request fails
*/
async function performPlanRequest(
handleId: string,
axios: Axios,
apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> {
const url =
apiServer + "/api/v2/report/plans?handleId=" + encodeURIComponent(handleId);
const headers = await getHeaders(requesterDid);
// Enhanced diagnostic logging for plan loading
const requestId = `plan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[Plan Loading] 🔍 Loading plan from server:", {
requestId,
handleId,
apiServer,
endpoint: url,
requesterDid,
timestamp: new Date().toISOString(),
});
try {
const resp = await axios.get(url, { headers });
logger.debug("[Plan Loading] ✅ Plan loaded successfully:", {
requestId,
handleId,
status: resp.status,
hasData: !!resp.data?.data,
dataLength: resp.data?.data?.length || 0,
timestamp: new Date().toISOString(),
});
if (resp.status === 200 && resp.data?.data?.length > 0) {
const cred = resp.data.data[0];
planCache.set(handleId, cred);
logger.debug("[Plan Loading] 💾 Plan cached:", {
requestId,
handleId,
planName: cred?.name,
planIssuer: cred?.issuerDid,
});
return cred;
} else {
// Use debug level for development to reduce console noise
const isDevelopment = process.env.VITE_PLATFORM === "development";
const log = isDevelopment ? logger.debug : logger.log;
log(
"[Plan Loading] ⚠️ Plan cache is empty for handle",
handleId,
" Got data:",
JSON.stringify(resp.data),
);
return undefined;
}
} catch (error) {
// Enhanced error logging for plan loading failures
const axiosError = error as {
response?: {
data?: unknown;
status?: number;
statusText?: string;
};
message?: string;
};
logger.error("[Plan Loading] ❌ Failed to load plan:", {
requestId,
handleId,
apiServer,
endpoint: url,
requesterDid,
errorStatus: axiosError.response?.status,
errorStatusText: axiosError.response?.statusText,
errorData: axiosError.response?.data,
errorMessage: axiosError.message || String(error),
timestamp: new Date().toISOString(),
});
throw error;
}
return cred;
}
/**
@@ -1018,19 +1128,82 @@ export async function createAndSubmitClaim(
const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload);
// Enhanced diagnostic logging for claim submission
const requestId = `claim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.info("[Claim Submission] 🚀 Starting claim submission:", {
requestId,
apiServer,
requesterDid: issuerDid,
endpoint: `${apiServer}/api/v2/claim`,
timestamp: new Date().toISOString(),
jwtLength: vcJwt.length,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = `${apiServer}/api/v2/claim`;
logger.debug("[Claim Submission] 📡 Making API request:", {
requestId,
url,
payloadSize: payload.length,
headers: { "Content-Type": "application/json" },
});
const response = await axios.post(url, payload, {
headers: {
"Content-Type": "application/json",
},
});
logger.info("[Claim Submission] ✅ Claim submitted successfully:", {
requestId,
status: response.status,
handleId: response.data?.handleId,
responseSize: JSON.stringify(response.data).length,
timestamp: new Date().toISOString(),
});
return { success: true, handleId: response.data?.handleId };
} catch (error: unknown) {
logger.error("Error submitting claim:", error);
// Enhanced error logging with comprehensive context
const requestId = `claim_error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
statusText?: string;
headers?: Record<string, string>;
};
config?: {
url?: string;
method?: string;
headers?: Record<string, string>;
};
message?: string;
};
logger.error("[Claim Submission] ❌ Claim submission failed:", {
requestId,
apiServer,
requesterDid: issuerDid,
endpoint: `${apiServer}/api/v2/claim`,
errorCode: axiosError.response?.data?.error?.code,
errorMessage: axiosError.response?.data?.error?.message,
httpStatus: axiosError.response?.status,
httpStatusText: axiosError.response?.statusText,
responseHeaders: axiosError.response?.headers,
requestConfig: {
url: axiosError.config?.url,
method: axiosError.config?.method,
headers: axiosError.config?.headers,
},
originalError: axiosError.message || String(error),
timestamp: new Date().toISOString(),
});
const errorMessage: string =
serverMessageForUser(error) ||
(error && typeof error === "object" && "message" in error
@@ -1515,14 +1688,56 @@ export async function fetchEndorserRateLimits(
) {
const url = `${apiServer}/api/report/rateLimits`;
const headers = await getHeaders(issuerDid);
// Enhanced diagnostic logging for user registration tracking
logger.debug("[User Registration] Checking user status on server:", {
did: issuerDid,
server: apiServer,
endpoint: url,
timestamp: new Date().toISOString(),
});
try {
const response = await axios.get(url, { headers } as AxiosRequestConfig);
// Log successful registration check
logger.debug("[User Registration] User registration check successful:", {
did: issuerDid,
server: apiServer,
status: response.status,
isRegistered: true,
timestamp: new Date().toISOString(),
});
return response;
} catch (error) {
// Enhanced error logging with user registration context
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
const errorCode = axiosError.response?.data?.error?.code;
const errorMessage = axiosError.response?.data?.error?.message;
const httpStatus = axiosError.response?.status;
logger.warn("[User Registration] User not registered on server:", {
did: issuerDid,
server: apiServer,
errorCode: errorCode,
errorMessage: errorMessage,
httpStatus: httpStatus,
needsRegistration: true,
timestamp: new Date().toISOString(),
});
// Log the original error for debugging
logger.error(
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
errorStringForLog(error),
);
throw error;
}
}
@@ -1535,8 +1750,53 @@ export async function fetchEndorserRateLimits(
* @param {string} issuerDid - The DID for which to check rate limits.
* @returns {Promise<AxiosResponse>} The Axios response object.
*/
export async function fetchImageRateLimits(axios: Axios, issuerDid: string) {
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
export async function fetchImageRateLimits(
axios: Axios,
issuerDid: string,
imageServer?: string,
) {
const server = imageServer || DEFAULT_IMAGE_API_SERVER;
const url = server + "/image-limits";
const headers = await getHeaders(issuerDid);
return await axios.get(url, { headers } as AxiosRequestConfig);
// Enhanced diagnostic logging for image server calls
logger.debug("[Image Server] Checking image rate limits:", {
did: issuerDid,
server: server,
endpoint: url,
timestamp: new Date().toISOString(),
});
try {
const response = await axios.get(url, { headers } as AxiosRequestConfig);
// Log successful image server call
logger.debug("[Image Server] Image rate limits check successful:", {
did: issuerDid,
server: server,
status: response.status,
timestamp: new Date().toISOString(),
});
return response;
} catch (error) {
// Enhanced error logging for image server failures
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
logger.warn("[Image Server] Image rate limits check failed:", {
did: issuerDid,
server: server,
errorCode: axiosError.response?.data?.error?.code,
errorMessage: axiosError.response?.data?.error?.message,
httpStatus: axiosError.response?.status,
timestamp: new Date().toISOString(),
});
throw error;
}
}

View File

@@ -20,6 +20,7 @@ import {
faCameraRotate,
faCaretDown,
faChair,
faChartLine,
faCheck,
faChevronDown,
faChevronLeft,
@@ -28,6 +29,7 @@ import {
faCircle,
faCircleCheck,
faCircleInfo,
faCirclePlus,
faCircleQuestion,
faCircleRight,
faCircleUser,
@@ -49,6 +51,7 @@ import {
faFloppyDisk,
faFolderOpen,
faForward,
faGear,
faGift,
faGlobe,
faHammer,
@@ -58,6 +61,7 @@ import {
faHouseChimney,
faImage,
faImagePortrait,
faInfo,
faLeftRight,
faLightbulb,
faLink,
@@ -72,8 +76,8 @@ import {
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQuestion,
faQrcode,
faQuestion,
faRightFromBracket,
faRotate,
faShareNodes,
@@ -106,6 +110,7 @@ library.add(
faCameraRotate,
faCaretDown,
faChair,
faChartLine,
faCheck,
faChevronDown,
faChevronLeft,
@@ -114,6 +119,7 @@ library.add(
faCircle,
faCircleCheck,
faCircleInfo,
faCirclePlus,
faCircleQuestion,
faCircleRight,
faCircleUser,
@@ -135,6 +141,7 @@ library.add(
faFloppyDisk,
faFolderOpen,
faForward,
faGear,
faGift,
faGlobe,
faHammer,
@@ -144,6 +151,7 @@ library.add(
faHouseChimney,
faImage,
faImagePortrait,
faInfo,
faLeftRight,
faLightbulb,
faLink,

View File

@@ -33,6 +33,7 @@ import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { IIdentifier } from "@veramo/core";
import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
import { UNNAMED_PERSON } from "@/constants/entities";
// Consolidate this with src/utils/PlatformServiceMixin.mapQueryResultToValues
function mapQueryResultToValues(
@@ -227,7 +228,7 @@ export const nameForContact = (
): string => {
return (
(contact?.name as string) ||
(capitalize ? "This" : "this") + " unnamed user"
(capitalize ? "This" : "this") + " " + UNNAMED_PERSON
);
};
@@ -648,57 +649,64 @@ export const retrieveAllAccountsMetadata = async (): Promise<
return result;
};
export const DUPLICATE_ACCOUNT_ERROR = "Cannot import duplicate account.";
/**
* Saves a new identity to both SQL and Dexie databases
* Saves a new identity to SQL database
*/
export async function saveNewIdentity(
identity: IIdentifier,
mnemonic: string,
derivationPath: string,
): Promise<void> {
try {
// add to the new sql db
const platformService = await getPlatformService();
// add to the new sql db
const platformService = await getPlatformService();
const secrets = await platformService.dbQuery(
`SELECT secretBase64 FROM secret`,
);
if (!secrets?.values?.length || !secrets.values[0]?.length) {
throw new Error(
"No initial encryption supported. We recommend you clear your data and start over.",
);
}
// Check if account already exists before attempting to save
const existingAccount = await platformService.dbQuery(
"SELECT did FROM accounts WHERE did = ?",
[identity.did],
);
const secretBase64 = secrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64);
const identityStr = JSON.stringify(identity);
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
VALUES (?, ?, ?, ?, ?, ?)`;
const params = [
new Date().toISOString(),
derivationPath,
identity.did,
encryptedIdentityBase64,
encryptedMnemonicBase64,
identity.keys[0].publicKeyHex,
];
await platformService.dbExec(sql, params);
await platformService.updateDefaultSettings({ activeDid: identity.did });
await platformService.insertDidSpecificSettings(identity.did);
} catch (error) {
logger.error("Failed to update default settings:", error);
if (existingAccount?.values?.length) {
throw new Error(
"Failed to set default settings. Please try again or restart the app.",
`Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`,
);
}
const secrets = await platformService.dbQuery(
`SELECT secretBase64 FROM secret`,
);
if (!secrets?.values?.length || !secrets.values[0]?.length) {
throw new Error(
"No initial encryption supported. We recommend you clear your data and start over.",
);
}
const secretBase64 = secrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64);
const identityStr = JSON.stringify(identity);
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
VALUES (?, ?, ?, ?, ?, ?)`;
const params = [
new Date().toISOString(),
derivationPath,
identity.did,
encryptedIdentityBase64,
encryptedMnemonicBase64,
identity.keys[0].publicKeyHex,
];
await platformService.dbExec(sql, params);
await platformService.updateDefaultSettings({ activeDid: identity.did });
await platformService.insertNewDidIntoSettings(identity.did);
}
/**
@@ -989,7 +997,7 @@ export async function importFromMnemonic(
try {
// First, ensure the DID-specific settings record exists
await platformService.insertDidSpecificSettings(newId.did);
await platformService.insertNewDidIntoSettings(newId.did);
// Then update with Test User #0 specific settings
await platformService.updateDidSpecificSettings(newId.did, {
@@ -1008,13 +1016,16 @@ export async function importFromMnemonic(
const firstName = settings[0];
const isRegistered = settings[1];
logger.info("[importFromMnemonic] Test User #0 settings verification", {
did: newId.did,
firstName,
isRegistered,
expectedFirstName: "User Zero",
expectedIsRegistered: true,
});
logger.debug(
"[importFromMnemonic] Test User #0 settings verification",
{
did: newId.did,
firstName,
isRegistered,
expectedFirstName: "User Zero",
expectedIsRegistered: true,
},
);
// If settings weren't saved correctly, try individual updates
if (firstName !== "User Zero" || isRegistered !== 1) {
@@ -1040,7 +1051,7 @@ export async function importFromMnemonic(
if (retryResult?.values?.length) {
const retrySettings = retryResult.values[0];
logger.info(
logger.debug(
"[importFromMnemonic] Test User #0 settings after retry",
{
firstName: retrySettings[0],
@@ -1063,3 +1074,58 @@ export async function importFromMnemonic(
}
}
}
/**
* Checks if an account with the given DID already exists in the database
*
* @param did - The DID to check for duplicates
* @returns Promise<boolean> - True if account already exists, false otherwise
* @throws Error if database query fails
*/
export async function checkForDuplicateAccount(did: string): Promise<boolean>;
/**
* Checks if an account with the given DID already exists in the database
*
* @param mnemonic - The mnemonic phrase to derive DID from
* @param derivationPath - The derivation path to use
* @returns Promise<boolean> - True if account already exists, false otherwise
* @throws Error if database query fails
*/
export async function checkForDuplicateAccount(
mnemonic: string,
derivationPath: string,
): Promise<boolean>;
/**
* Implementation of checkForDuplicateAccount with overloaded signatures
*/
export async function checkForDuplicateAccount(
didOrMnemonic: string,
derivationPath?: string,
): Promise<boolean> {
let didToCheck: string;
if (derivationPath) {
// Derive the DID from mnemonic and derivation path
const [address, privateHex, publicHex] = deriveAddress(
didOrMnemonic.trim().toLowerCase(),
derivationPath,
);
const newId = newIdentifier(address, privateHex, publicHex, derivationPath);
didToCheck = newId.did;
} else {
// Use the provided DID directly
didToCheck = didOrMnemonic;
}
// Check if an account with this DID already exists
const platformService = await getPlatformService();
const existingAccount = await platformService.dbQuery(
"SELECT did FROM accounts WHERE did = ?",
[didToCheck],
);
return (existingAccount?.values?.length ?? 0) > 0;
}