Browse Source

docs: improve endorserServer.ts documentation and types

Changes:
- Add comprehensive JSDoc headers with examples
- Improve function documentation with param/return types
- Add module-level documentation explaining purpose
- Clean up testRecursivelyOnStrings implementation
- Add type annotations to cache functions
- Simplify serverMessageForUser implementation

This improves code maintainability by adding clear documentation
and improving type safety throughout the endorser server module.
side_step
Matthew Raymer 2 weeks ago
parent
commit
89d970da1d
  1. 182
      src/libs/endorserServer.ts
  2. 89
      test-deeplinks.sh

182
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 { Axios, AxiosRequestConfig } from "axios";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { sha256 } from "ethereum-cryptography/sha256"; import { sha256 } from "ethereum-cryptography/sha256";
@ -31,21 +49,48 @@ import {
CreateAndSubmitClaimResult, CreateAndSubmitClaimResult,
} from "../interfaces"; } from "../interfaces";
/**
* Standard context for schema.org data
* @constant {string}
*/
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; 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"; 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"; 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/"; 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="; 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="; 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 ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> = export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
{ {
claim: { "@type": "" }, claim: { "@type": "" },
@ -54,41 +99,99 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCr
issuedAt: "", issuedAt: "",
issuer: "", issuer: "",
}; };
// This is used to check for hidden info. // This is used to check for hidden info.
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN"; 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:"); 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; 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 * Recursively tests strings within an object/array against a test function
* * @param {Function} func - Test function to apply to strings
* Similar logic is found in endorser-mobile. * @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]") { if (Object.prototype.toString.call(input) === "[object String]") {
return func(input); return func(input);
} else if (input instanceof Object) { }
// Recursively test objects and arrays
else if (input instanceof Object) {
if (!Array.isArray(input)) { if (!Array.isArray(input)) {
// it's an object // Handle plain objects
for (const key in input) { for (const key in input) {
if (testRecursivelyOnStrings(func, input[key])) { if (testRecursivelyOnStrings(func, input[key])) {
return true; return true;
} }
} }
} else { } else {
// it's an array // Handle arrays
for (const value of input) { for (const value of input) {
if (testRecursivelyOnStrings(func, value)) { if (testRecursivelyOnStrings(func, value)) {
return true; return true;
@ -97,6 +200,7 @@ function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) {
} }
return false; return false;
} else { } else {
// Non-string, non-object values can't contain strings
return false; return false;
} }
} }
@ -363,15 +467,23 @@ export async function getHeaders(
return headers; return headers;
} }
/**
* Cache for storing plan data
* @constant {LRUCache}
*/
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500, max: 500,
}); });
/** /**
* @param handleId nullable, in which case "undefined" will be returned * Retrieves plan data from cache or server
* @param requesterDid optional, in which case no private info will be returned * @param {string} handleId - Plan handle ID
* @param axios * @param {Axios} axios - Axios instance
* @param apiServer * @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( export async function getPlanFromCache(
handleId: string | undefined, handleId: string | undefined,
@ -414,25 +526,25 @@ export async function getPlanFromCache(
return cred; return cred;
} }
/**
* Updates plan data in cache
* @param {string} handleId - Plan handle ID
* @param {PlanSummaryRecord} planSummary - Plan data to cache
*/
export async function setPlanInCache( export async function setPlanInCache(
handleId: string, handleId: string,
planSummary: PlanSummaryRecord, planSummary: PlanSummaryRecord,
) { ): Promise<void> {
planCache.set(handleId, planSummary); planCache.set(handleId, planSummary);
} }
/** /**
* * Extracts user-friendly message from server error
* @param error that is thrown from an Endorser server call by Axios * @param {any} error - Error thrown from Endorser server call
* @returns user-friendly message, or undefined if none found * @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): string | undefined {
export function serverMessageForUser(error: any) { return error?.response?.data?.error?.message;
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
);
} }
/** /**

89
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 "======================================"
Loading…
Cancel
Save