forked from trent_larson/crowd-funder-for-time-pwa
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.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
89
test-deeplinks.sh
Normal file
89
test-deeplinks.sh
Normal file
@@ -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 "======================================"
|
||||
Reference in New Issue
Block a user