diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index a9efc034..f64b5680 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1,3 +1,21 @@ +/** + * @fileoverview Endorser Server Interface and Utilities + * @author Matthew Raymer + * + * This module provides the interface and utilities for interacting with the Endorser server. + * It handles authentication, data validation, and server communication for claims, contacts, + * and other core functionality. + * + * Key Features: + * - Deep link URL path constants + * - DID validation and handling + * - Contact management utilities + * - Server authentication + * - Plan caching + * + * @module endorserServer + */ + import { Axios, AxiosRequestConfig } from "axios"; import { Buffer } from "buffer"; import { sha256 } from "ethereum-cryptography/sha256"; @@ -31,21 +49,48 @@ import { CreateAndSubmitClaimResult, } from "../interfaces"; +/** + * Standard context for schema.org data + * @constant {string} + */ export const SCHEMA_ORG_CONTEXT = "https://schema.org"; -// the object in RegisterAction claims + +/** + * Service identifier for RegisterAction claims + * @constant {string} + */ export const SERVICE_ID = "endorser.ch"; -// the header line for contacts exported via Endorser Mobile + +/** + * Header line format for contacts exported via Endorser Mobile + * @constant {string} + */ export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered"; -// the suffix for the contact URL in this app where they are confirmed before import + +/** + * URL path suffix for contact confirmation before import + * @constant {string} + */ export const CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact-import/"; -// the suffix for the contact URL in this app where a single one gets imported automatically + +/** + * URL path suffix for the contact URL in this app where a single one gets imported automatically + * @constant {string} + */ export const CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI = "/contacts?contactJwt="; -// the suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server + +/** + * URL path suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server + * @constant {string} + */ export const CONTACT_URL_PATH_ENDORSER_CH_OLD = "/contact?jwt="; -// unused now that we match on the URL path; just note that it was used for a while to create URLs that showed at endorser.ch -//export const CONTACT_URL_PREFIX_ENDORSER_CH_OLD = "https://endorser.ch"; -// the prefix for handle IDs, the permanent ID for claims on Endorser + +/** + * The prefix for handle IDs, the permanent ID for claims on Endorser + * @constant {string} + */ export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/"; + export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> = { claim: { "@type": "" }, @@ -54,41 +99,99 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCr issuedAt: "", issuer: "", }; + // This is used to check for hidden info. // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 const HIDDEN_DID = "did:none:HIDDEN"; -export function isDid(did: string) { +/** + * Validates if a string is a valid DID + * @param {string} did - The DID string to validate + * @returns {boolean} True if string is a valid DID format + */ +export function isDid(did: string): boolean { return did.startsWith("did:"); } -export function isHiddenDid(did: string) { +/** + * Checks if a DID is the special hidden DID value + * @param {string} did - The DID to check + * @returns {boolean} True if DID is hidden + */ +export function isHiddenDid(did: string): boolean { return did === HIDDEN_DID; } -export function isEmptyOrHiddenDid(did?: string) { - return !did || did === HIDDEN_DID; // catching empty string as well +/** + * Checks if a DID is empty or hidden + * @param {string} [did] - The DID to check + * @returns {boolean} True if DID is empty or hidden + */ +export function isEmptyOrHiddenDid(did?: string): boolean { + return !did || did === HIDDEN_DID; } /** - * @return true for any string within this primitive/object/array where func(input) === true - * - * Similar logic is found in endorser-mobile. + * Recursively tests strings within an object/array against a test function + * @param {Function} func - Test function to apply to strings + * @param {any} input - Object/array to recursively test + * @returns {boolean} True if any string passes the test function + * + * @example + * testRecursivelyOnStrings(isDid, { user: { id: "did:example:123" } }) + * // Returns: true */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) { +/** + * Recursively tests strings within a nested object/array structure against a test function + * + * This function traverses through objects and arrays to find all string values and applies + * a test function to each string found. It handles: + * - Direct string values + * - Strings in objects (at any depth) + * - Strings in arrays (at any depth) + * - Mixed nested structures (objects containing arrays containing objects, etc) + * + * @param {Function} func - Test function that takes a string and returns boolean + * @param {any} input - Value to recursively search (can be string, object, array, or other) + * @returns {boolean} True if any string in the structure passes the test function + * + * @example + * // Test if any string is a DID + * const obj = { + * user: { + * id: "did:example:123", + * details: ["name", "did:example:456"] + * } + * }; + * testRecursivelyOnStrings(isDid, obj); // Returns: true + * + * @example + * // Test for hidden DIDs + * const obj = { + * visible: "did:example:123", + * hidden: ["did:none:HIDDEN"] + * }; + * testRecursivelyOnStrings(isHiddenDid, obj); // Returns: true + */ +function testRecursivelyOnStrings( + func: (arg0: any) => boolean, + input: any +): boolean { + // Test direct string values if (Object.prototype.toString.call(input) === "[object String]") { return func(input); - } else if (input instanceof Object) { + } + // Recursively test objects and arrays + else if (input instanceof Object) { if (!Array.isArray(input)) { - // it's an object + // Handle plain objects for (const key in input) { if (testRecursivelyOnStrings(func, input[key])) { return true; } } } else { - // it's an array + // Handle arrays for (const value of input) { if (testRecursivelyOnStrings(func, value)) { return true; @@ -97,6 +200,7 @@ function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) { } return false; } else { + // Non-string, non-object values can't contain strings return false; } } @@ -363,15 +467,23 @@ export async function getHeaders( return headers; } +/** + * Cache for storing plan data + * @constant {LRUCache} + */ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({ max: 500, }); /** - * @param handleId nullable, in which case "undefined" will be returned - * @param requesterDid optional, in which case no private info will be returned - * @param axios - * @param apiServer + * Retrieves plan data from cache or 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 */ export async function getPlanFromCache( handleId: string | undefined, @@ -414,25 +526,25 @@ export async function getPlanFromCache( return cred; } +/** + * Updates plan data in cache + * @param {string} handleId - Plan handle ID + * @param {PlanSummaryRecord} planSummary - Plan data to cache + */ export async function setPlanInCache( handleId: string, planSummary: PlanSummaryRecord, -) { +): Promise<void> { planCache.set(handleId, planSummary); } /** - * - * @param error that is thrown from an Endorser server call by Axios - * @returns user-friendly message, or undefined if none found + * Extracts user-friendly message from server error + * @param {any} error - Error thrown from Endorser server call + * @returns {string|undefined} User-friendly message or undefined if none found */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function serverMessageForUser(error: any) { - return ( - // this is how most user messages are returned - error?.response?.data?.error?.message - // some are returned as "error" with a string, but those are more for devs and are less helpful to the user - ); +export function serverMessageForUser(error: any): string | undefined { + return error?.response?.data?.error?.message; } /** diff --git a/test-deeplinks.sh b/test-deeplinks.sh new file mode 100644 index 00000000..a10186bb --- /dev/null +++ b/test-deeplinks.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# Configurable pause duration (in seconds) +PAUSE_DURATION=2 +MANUAL_CONTINUE=true + +# Function to test deep link +test_link() { + echo "----------------------------------------" + echo "Testing: $1" + echo "----------------------------------------" + adb shell am start -W -a android.intent.action.VIEW -d "$1" app.timesafari.app + + if [ "$MANUAL_CONTINUE" = true ]; then + read -p "Press Enter to continue to next test..." + else + sleep $PAUSE_DURATION + fi +} + +# Allow command line override of pause settings +while getopts "t:a" opt; do + case $opt in + t) PAUSE_DURATION=$OPTARG ;; + a) MANUAL_CONTINUE=false ;; + esac +done + +echo "Starting TimeSafari Deep Link Tests" +echo "======================================" +echo "Pause duration: $PAUSE_DURATION seconds" +echo "Manual continue: $MANUAL_CONTINUE" + +# Test claim routes +echo "\nTesting Claim Routes:" +test_link "timesafari://claim/01JMAAFZRNSRTQ0EBSD70A8E1H" +test_link "timesafari://claim/01JMAAFZRNSRTQ0EBSD70A8E1H?view=details" +test_link "timesafari://claim-cert/01JMAAFZRNSRTQ0EBSD70A8E1H" + +# Test contact routes +echo "\nTesting Contact Routes:" +test_link "timesafari://contact-import/eyJhbGciOiJFUzI1NksifQ" +test_link "timesafari://contact-edit/did:example:123" + +# Test project routes +echo "\nTesting Project Routes:" +test_link "timesafari://project/456?view=details" + +# Test invite routes +echo "\nTesting Invite Routes:" +test_link "timesafari://invite-one-accept/eyJhbGciOiJFUzI1NksifQ" + +# Test gift routes +echo "\nTesting Gift Routes:" +test_link "timesafari://confirm-gift/789" + +# Test offer routes +echo "\nTesting Offer Routes:" +test_link "timesafari://offer-details/101" + +# Test complex query parameters +echo "\nTesting Complex Query Parameters:" +test_link "timesafari://contact-import/jwt?contacts=%5B%7B%22name%22%3A%22Test%22%7D%5D" + +# New test cases +echo "\nTesting DID Routes:" +test_link "timesafari://did/did:example:123" +test_link "timesafari://did/did:example:456?view=details" + +echo "\nTesting Additional Claim Routes:" +test_link "timesafari://claim/123?view=certificate" +test_link "timesafari://claim/123?view=raw" +test_link "timesafari://claim-add-raw/123?claimJwtId=jwt123" + +echo "\nTesting Additional Contact Routes:" +test_link "timesafari://contact-import/jwt?contacts=%5B%7B%22did%22%3A%22did%3Aexample%3A123%22%7D%5D" +test_link "timesafari://contact-edit/did:example:123?action=edit" + +echo "\nTesting Error Cases:" +test_link "timesafari://invalid-route/123" +test_link "timesafari://claim/123?view=invalid" +test_link "timesafari://did/invalid-did" + +# Test contact import one route +echo "\nTesting Contact Import One Route:" +test_link "timesafari://contacts?contactJwt=eyJhbGciOiJFUzI1NksifQ" + +echo "\nDeep link testing complete" +echo "======================================" \ No newline at end of file