forked from jsnbuchanan/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 { 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
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