forked from trent_larson/crowd-funder-for-time-pwa
- Fix Vue template syntax in App.vue by using proper event handler format - Update Vite config to properly handle ESM imports and crypto modules - Add manual chunks for better code splitting - Improve environment variable handling in vite-env.d.ts - Fix TypeScript linting errors in App.vue
1409 lines
41 KiB
TypeScript
1409 lines
41 KiB
TypeScript
/**
|
|
* @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'
|
|
import { LRUCache } from 'lru-cache'
|
|
import * as R from 'ramda'
|
|
|
|
import {
|
|
DEFAULT_IMAGE_API_SERVER,
|
|
NotificationIface,
|
|
APP_SERVER
|
|
} from '../constants/app'
|
|
import { Contact } from '../db/tables/contacts'
|
|
import { accessToken, deriveAddress, nextDerivationPath } from '../libs/crypto'
|
|
import { logConsoleAndDb, NonsensitiveDexie } from '../db/index'
|
|
|
|
import {
|
|
retrieveAccountMetadata,
|
|
retrieveFullyDecryptedAccount,
|
|
getPasskeyExpirationSeconds
|
|
} from '../libs/util'
|
|
import { createEndorserJwtForKey, KeyMeta } from '../libs/crypto/vc'
|
|
|
|
import {
|
|
GiveVerifiableCredential,
|
|
OfferVerifiableCredential,
|
|
RegisterVerifiableCredential,
|
|
GenericVerifiableCredential,
|
|
GenericCredWrapper,
|
|
PlanSummaryRecord,
|
|
UserInfo,
|
|
CreateAndSubmitClaimResult
|
|
} from '../interfaces'
|
|
import { logger } from '../utils/logger'
|
|
|
|
/**
|
|
* Standard context for schema.org data
|
|
* @constant {string}
|
|
*/
|
|
export const SCHEMA_ORG_CONTEXT = 'https://schema.org'
|
|
|
|
/**
|
|
* Service identifier for RegisterAction claims
|
|
* @constant {string}
|
|
*/
|
|
export const SERVICE_ID = 'endorser.ch'
|
|
|
|
/**
|
|
* Header line format for contacts exported via Endorser Mobile
|
|
* @constant {string}
|
|
*/
|
|
export const CONTACT_CSV_HEADER = 'name,did,pubKeyBase64,seesMe,registered'
|
|
|
|
/**
|
|
* URL path suffix for contact confirmation before import
|
|
* @constant {string}
|
|
*/
|
|
export const CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI = '/contact-import/'
|
|
|
|
/**
|
|
* 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='
|
|
|
|
/**
|
|
* 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='
|
|
|
|
/**
|
|
* 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': '' },
|
|
handleId: '',
|
|
id: '',
|
|
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'
|
|
|
|
/**
|
|
* 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:')
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
/**
|
|
* 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: unknown) => boolean,
|
|
input: unknown
|
|
): boolean {
|
|
// Test direct string values
|
|
if (Object.prototype.toString.call(input) === '[object String]') {
|
|
return func(input)
|
|
}
|
|
// Recursively test objects and arrays
|
|
else if (input instanceof Object) {
|
|
if (!Array.isArray(input)) {
|
|
// Handle plain objects
|
|
for (const key in input) {
|
|
if (testRecursivelyOnStrings(func, input[key])) {
|
|
return true
|
|
}
|
|
}
|
|
} else {
|
|
// Handle arrays
|
|
for (const value of input) {
|
|
if (testRecursivelyOnStrings(func, value)) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
} else {
|
|
// Non-string, non-object values can't contain strings
|
|
return false
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function containsHiddenDid(obj: any) {
|
|
return testRecursivelyOnStrings(isHiddenDid, obj)
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export const containsNonHiddenDid = (obj: any) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return testRecursivelyOnStrings((s: any) => isDid(s) && !isHiddenDid(s), obj)
|
|
}
|
|
|
|
export function stripEndorserPrefix(claimId: string) {
|
|
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
|
|
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length)
|
|
} else {
|
|
return claimId
|
|
}
|
|
}
|
|
|
|
// similar logic is found in endorser-mobile
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function removeSchemaContext(obj: any) {
|
|
return obj['@context'] === SCHEMA_ORG_CONTEXT
|
|
? R.omit(['@context'], obj)
|
|
: obj
|
|
}
|
|
|
|
// similar logic is found in endorser-mobile
|
|
export function addLastClaimOrHandleAsIdIfMissing(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
obj: any,
|
|
lastClaimId?: string,
|
|
handleId?: string
|
|
) {
|
|
if (!obj.identifier && lastClaimId) {
|
|
const result = R.clone(obj)
|
|
result.lastClaimId = lastClaimId
|
|
return result
|
|
} else if (!obj.identifier && handleId) {
|
|
const result = R.clone(obj)
|
|
result.identifier = handleId
|
|
return result
|
|
} else {
|
|
return obj
|
|
}
|
|
}
|
|
|
|
// return clone of object without any nested *VisibleToDids keys
|
|
// similar code is also contained in endorser-mobile
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function removeVisibleToDids(input: any): any {
|
|
if (input instanceof Object) {
|
|
if (!Array.isArray(input)) {
|
|
// it's an object
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const result: Record<string, any> = {}
|
|
for (const key in input) {
|
|
if (!key.endsWith('VisibleToDids')) {
|
|
result[key] = removeVisibleToDids(R.clone(input[key]))
|
|
}
|
|
}
|
|
return result
|
|
} else {
|
|
// it's an array
|
|
return R.map(removeVisibleToDids, input)
|
|
}
|
|
} else {
|
|
return input
|
|
}
|
|
}
|
|
|
|
export function contactForDid(
|
|
did: string | undefined,
|
|
contacts: Contact[]
|
|
): Contact | undefined {
|
|
return isEmptyOrHiddenDid(did)
|
|
? undefined
|
|
: R.find((c) => c.did === did, contacts)
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Similar logic is found in endorser-mobile.
|
|
*
|
|
* @param did
|
|
* @param activeDid
|
|
* @param contact
|
|
* @param allMyDids
|
|
* @return { known: boolean, displayName: string, profileImageUrl?: string }
|
|
* where 'known' is true if they are in the contacts
|
|
*/
|
|
export function didInfoForContact(
|
|
did: string | undefined,
|
|
activeDid: string | undefined,
|
|
contact?: Contact,
|
|
allMyDids: string[] = [],
|
|
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 === activeDid) {
|
|
return { displayName: 'You', known: true }
|
|
} else if (contact) {
|
|
return {
|
|
displayName: contact.name || 'Contact With No Name',
|
|
known: true,
|
|
profileImageUrl: contact.profileImageUrl
|
|
}
|
|
} else {
|
|
const myId = R.find(R.equals(did), allMyDids)
|
|
return myId
|
|
? { displayName: 'You (Alt ID)', known: true }
|
|
: isHiddenDid(did)
|
|
? { displayName: 'Someone Outside Your View', known: false }
|
|
: {
|
|
displayName: showDidForVisible
|
|
? did
|
|
: 'Someone Visible But Not In Your Contact List',
|
|
known: false
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns full contact info object (never undefined), where did is searched in contacts and allMyDids
|
|
*/
|
|
export function didInfoObject(
|
|
did: string | undefined,
|
|
activeDid: string | undefined,
|
|
allMyDids: string[],
|
|
contacts: Contact[]
|
|
): { known: boolean; displayName: string; profileImageUrl?: string } {
|
|
const contact = contactForDid(did, contacts)
|
|
return didInfoForContact(did, activeDid, contact, allMyDids)
|
|
}
|
|
|
|
/**
|
|
always returns text, maybe something like "unnamed" or "unknown"
|
|
|
|
Now that we're using more informational didInfoForContact under the covers, we might want to consolidate.
|
|
**/
|
|
export function didInfo(
|
|
did: string | undefined,
|
|
activeDid: string | undefined,
|
|
allMyDids: string[],
|
|
contacts: Contact[]
|
|
): string {
|
|
const contact = contactForDid(did, contacts)
|
|
return didInfoForContact(did, activeDid, contact, allMyDids).displayName
|
|
}
|
|
|
|
/**
|
|
* return text description without any references to "you" as user
|
|
*/
|
|
export function didInfoForCertificate(
|
|
did: string | undefined,
|
|
contacts: Contact[]
|
|
): string {
|
|
return didInfoForContact(
|
|
did,
|
|
undefined,
|
|
contactForDid(did, contacts),
|
|
[],
|
|
true
|
|
).displayName
|
|
}
|
|
|
|
let passkeyAccessToken: string = ''
|
|
let passkeyTokenExpirationEpochSeconds: number = 0
|
|
|
|
export function clearPasskeyToken() {
|
|
passkeyAccessToken = ''
|
|
passkeyTokenExpirationEpochSeconds = 0
|
|
}
|
|
|
|
export function tokenExpiryTimeDescription() {
|
|
if (
|
|
!passkeyAccessToken ||
|
|
passkeyTokenExpirationEpochSeconds < new Date().getTime() / 1000
|
|
) {
|
|
return 'Token has expired'
|
|
} else {
|
|
return (
|
|
'Token expires at ' +
|
|
new Date(passkeyTokenExpirationEpochSeconds * 1000).toLocaleString()
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the headers for a request, potentially including Authorization
|
|
*/
|
|
export async function getHeaders(
|
|
did?: string,
|
|
$notify?: (notification: NotificationIface, timeout?: number) => void,
|
|
failureMessage?: string
|
|
) {
|
|
const headers: { 'Content-Type': string; Authorization?: string } = {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
if (did) {
|
|
try {
|
|
let token
|
|
const account = await retrieveAccountMetadata(did)
|
|
if (account?.passkeyCredIdHex) {
|
|
if (
|
|
passkeyAccessToken &&
|
|
passkeyTokenExpirationEpochSeconds > Date.now() / 1000
|
|
) {
|
|
// there's an active current passkey token
|
|
token = passkeyAccessToken
|
|
} else {
|
|
// there's no current passkey token or it's expired
|
|
token = await accessToken(did)
|
|
|
|
passkeyAccessToken = token
|
|
const passkeyExpirationSeconds = await getPasskeyExpirationSeconds()
|
|
passkeyTokenExpirationEpochSeconds =
|
|
Date.now() / 1000 + passkeyExpirationSeconds
|
|
}
|
|
} else {
|
|
token = await accessToken(did)
|
|
}
|
|
headers['Authorization'] = 'Bearer ' + token
|
|
} catch (error) {
|
|
// This rarely happens: we've seen it when they have account info but the
|
|
// encryption secret got lost. But in most cases we want users to at
|
|
// least see their feed -- and anything else that returns results for
|
|
// anonymous users.
|
|
|
|
// We'll continue with an anonymous request... still want to show feed and other things, but ideally let them know.
|
|
logConsoleAndDb(
|
|
'Something failed in getHeaders call (will proceed anonymously' +
|
|
($notify ? ' and notify user' : '') +
|
|
'): ' +
|
|
// IntelliJ type system complains about getCircularReplacer() with: Argument of type '(obj: any, key: string, value: any) => any' is not assignable to parameter of type '(this: any, key: string, value: any) => any'.
|
|
//JSON.stringify(error, getCircularReplacer()), // JSON.stringify(error) on a Dexie error throws another error about: Converting circular structure to JSON
|
|
error,
|
|
true
|
|
)
|
|
if ($notify) {
|
|
// remember: only want to do this if they supplied a DID, expecting personal results
|
|
const notifyMessage =
|
|
failureMessage ||
|
|
'Showing anonymous data. See the Help page for help with personal data.'
|
|
$notify(
|
|
{
|
|
group: 'alert',
|
|
type: 'danger',
|
|
title: 'Personal Data Error',
|
|
text: notifyMessage
|
|
},
|
|
3000
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
// it's usually OK to request without auth; we assume we're only here when allowed
|
|
}
|
|
return headers
|
|
}
|
|
|
|
/**
|
|
* Cache for storing plan data
|
|
* @constant {LRUCache}
|
|
*/
|
|
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
|
|
max: 500
|
|
})
|
|
|
|
/**
|
|
* 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,
|
|
axios: Axios,
|
|
apiServer: string,
|
|
requesterDid?: string
|
|
): Promise<PlanSummaryRecord | undefined> {
|
|
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 {
|
|
logger.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)
|
|
)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
export function serverMessageForUser(error: unknown): string | undefined {
|
|
return error?.response?.data?.error?.message
|
|
}
|
|
|
|
/**
|
|
* Helpful for server errors, to get all the info -- including stuff skipped by toString & JSON.stringify
|
|
* It works with AxiosError, eg handling an error.response intelligently.
|
|
*
|
|
* @param error
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function errorStringForLog(error: unknown) {
|
|
let stringifiedError = '' + error
|
|
try {
|
|
stringifiedError = JSON.stringify(error)
|
|
} catch (e) {
|
|
// can happen with Dexie, eg:
|
|
// TypeError: Converting circular structure to JSON
|
|
// --> starting at object with constructor 'DexieError2'
|
|
// | property '_promise' -> object with constructor 'DexiePromise'
|
|
// --- property '_value' closes the circle
|
|
}
|
|
let fullError = '' + error + ' - JSON: ' + stringifiedError
|
|
const errorResponseText = JSON.stringify(error.response)
|
|
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
|
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
|
// add error.response stuff
|
|
if (R.equals(error?.config, error?.response?.config)) {
|
|
// but exclude "config" because it's already in there
|
|
const newErrorResponseText = JSON.stringify(
|
|
R.omit(['config'] as never[], error.response)
|
|
)
|
|
fullError += ' - .response w/o same config JSON: ' + newErrorResponseText
|
|
} else {
|
|
fullError += ' - .response JSON: ' + errorResponseText
|
|
}
|
|
}
|
|
return fullError
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns { data: Array<OfferSummaryRecord>, hitLimit: boolean true if maximum was hit and there may be more }
|
|
*/
|
|
export async function getNewOffersToUser(
|
|
axios: Axios,
|
|
apiServer: string,
|
|
activeDid: string,
|
|
afterOfferJwtId?: string,
|
|
beforeOfferJwtId?: string
|
|
) {
|
|
let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`
|
|
if (afterOfferJwtId) {
|
|
url += '&afterId=' + afterOfferJwtId
|
|
}
|
|
if (beforeOfferJwtId) {
|
|
url += '&beforeId=' + beforeOfferJwtId
|
|
}
|
|
const headers = await getHeaders(activeDid)
|
|
const response = await axios.get(url, { headers })
|
|
return response.data
|
|
}
|
|
/**
|
|
*
|
|
* @returns { data: Array<OfferToPlanSummaryRecord>, hitLimit: boolean true if maximum was hit and there may be more }
|
|
*/
|
|
export async function getNewOffersToUserProjects(
|
|
axios: Axios,
|
|
apiServer: string,
|
|
activeDid: string,
|
|
afterOfferJwtId?: string,
|
|
beforeOfferJwtId?: string
|
|
) {
|
|
let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`
|
|
if (afterOfferJwtId) {
|
|
url += '?afterId=' + afterOfferJwtId
|
|
}
|
|
if (beforeOfferJwtId) {
|
|
url += afterOfferJwtId ? '&' : '?'
|
|
url += 'beforeId=' + beforeOfferJwtId
|
|
}
|
|
const headers = await getHeaders(activeDid)
|
|
const response = await axios.get(url, { headers })
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Construct GiveAction VC for submission to server
|
|
*
|
|
* @param lastClaimId supplied when editing a previous claim
|
|
*/
|
|
export function hydrateGive(
|
|
vcClaimOrig?: GiveVerifiableCredential,
|
|
fromDid?: string,
|
|
toDid?: string,
|
|
description?: string,
|
|
amount?: number,
|
|
unitCode?: string,
|
|
fulfillsProjectHandleId?: string,
|
|
fulfillsOfferHandleId?: string,
|
|
isTrade: boolean = false,
|
|
imageUrl?: string,
|
|
providerPlanHandleId?: string,
|
|
lastClaimId?: string
|
|
): GiveVerifiableCredential {
|
|
// Remember: replace values or erase if it's null
|
|
|
|
const vcClaim: GiveVerifiableCredential = vcClaimOrig
|
|
? R.clone(vcClaimOrig)
|
|
: {
|
|
'@context': SCHEMA_ORG_CONTEXT,
|
|
'@type': 'GiveAction'
|
|
}
|
|
|
|
if (lastClaimId) {
|
|
// this is an edit
|
|
vcClaim.lastClaimId = lastClaimId
|
|
delete vcClaim.identifier
|
|
}
|
|
|
|
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined
|
|
vcClaim.recipient = toDid ? { identifier: toDid } : undefined
|
|
vcClaim.description = description || undefined
|
|
vcClaim.object =
|
|
amount && !isNaN(amount)
|
|
? { amountOfThisGood: amount, unitCode: unitCode || 'HUR' }
|
|
: undefined
|
|
|
|
// ensure fulfills is an array
|
|
if (!Array.isArray(vcClaim.fulfills)) {
|
|
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : []
|
|
}
|
|
// ... and replace or add each element, ending with Trade or Donate
|
|
// I realize the following doesn't change any elements that are not PlanAction or Offer or Trade/Action.
|
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
|
(elem) => elem['@type'] !== 'PlanAction'
|
|
)
|
|
if (fulfillsProjectHandleId) {
|
|
vcClaim.fulfills.push({
|
|
'@type': 'PlanAction',
|
|
identifier: fulfillsProjectHandleId
|
|
})
|
|
}
|
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
|
(elem) => elem['@type'] !== 'Offer'
|
|
)
|
|
if (fulfillsOfferHandleId) {
|
|
vcClaim.fulfills.push({
|
|
'@type': 'Offer',
|
|
identifier: fulfillsOfferHandleId
|
|
})
|
|
}
|
|
// do Trade/Donate last because current endorser.ch only looks at the first for plans & offers
|
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
|
(elem) =>
|
|
elem['@type'] !== 'DonateAction' && elem['@type'] !== 'TradeAction'
|
|
)
|
|
vcClaim.fulfills.push({ '@type': isTrade ? 'TradeAction' : 'DonateAction' })
|
|
|
|
vcClaim.image = imageUrl || undefined
|
|
|
|
vcClaim.provider = providerPlanHandleId
|
|
? { '@type': 'PlanAction', identifier: providerPlanHandleId }
|
|
: undefined
|
|
|
|
return vcClaim
|
|
}
|
|
|
|
/**
|
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
|
*
|
|
* @param fromDid may be null
|
|
* @param toDid
|
|
* @param description may be null
|
|
* @param amount may be null
|
|
*/
|
|
export async function createAndSubmitGive(
|
|
axios: Axios,
|
|
apiServer: string,
|
|
issuerDid: string,
|
|
fromDid?: string,
|
|
toDid?: string,
|
|
description?: string,
|
|
amount?: number,
|
|
unitCode?: string,
|
|
fulfillsProjectHandleId?: string,
|
|
fulfillsOfferHandleId?: string,
|
|
isTrade: boolean = false,
|
|
imageUrl?: string,
|
|
providerPlanHandleId?: string
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
const vcClaim = hydrateGive(
|
|
undefined,
|
|
fromDid,
|
|
toDid,
|
|
description,
|
|
amount,
|
|
unitCode,
|
|
fulfillsProjectHandleId,
|
|
fulfillsOfferHandleId,
|
|
isTrade,
|
|
imageUrl,
|
|
providerPlanHandleId,
|
|
undefined
|
|
)
|
|
return createAndSubmitClaim(
|
|
vcClaim as GenericVerifiableCredential,
|
|
issuerDid,
|
|
apiServer,
|
|
axios
|
|
)
|
|
}
|
|
|
|
/**
|
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
|
*
|
|
* @param fromDid may be null
|
|
* @param toDid may be null if project is provided
|
|
* @param description may be null
|
|
* @param amount may be null
|
|
*/
|
|
export async function editAndSubmitGive(
|
|
axios: Axios,
|
|
apiServer: string,
|
|
fullClaim: GenericCredWrapper<GiveVerifiableCredential>,
|
|
issuerDid: string,
|
|
fromDid?: string,
|
|
toDid?: string,
|
|
description?: string,
|
|
amount?: number,
|
|
unitCode?: string,
|
|
fulfillsProjectHandleId?: string,
|
|
fulfillsOfferHandleId?: string,
|
|
isTrade: boolean = false,
|
|
imageUrl?: string,
|
|
providerPlanHandleId?: string
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
const vcClaim = hydrateGive(
|
|
fullClaim.claim,
|
|
fromDid,
|
|
toDid,
|
|
description,
|
|
amount,
|
|
unitCode,
|
|
fulfillsProjectHandleId,
|
|
fulfillsOfferHandleId,
|
|
isTrade,
|
|
imageUrl,
|
|
providerPlanHandleId,
|
|
fullClaim.id
|
|
)
|
|
return createAndSubmitClaim(
|
|
vcClaim as GenericVerifiableCredential,
|
|
issuerDid,
|
|
apiServer,
|
|
axios
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Construct Offer VC for submission to server
|
|
*
|
|
* @param lastClaimId supplied when editing a previous claim
|
|
*/
|
|
export function hydrateOffer(
|
|
vcClaimOrig?: OfferVerifiableCredential,
|
|
fromDid?: string,
|
|
toDid?: string,
|
|
itemDescription?: string,
|
|
amount?: number,
|
|
unitCode?: string,
|
|
conditionDescription?: string,
|
|
fulfillsProjectHandleId?: string,
|
|
validThrough?: string,
|
|
lastClaimId?: string
|
|
): OfferVerifiableCredential {
|
|
// Remember: replace values or erase if it's null
|
|
|
|
const vcClaim: OfferVerifiableCredential = vcClaimOrig
|
|
? R.clone(vcClaimOrig)
|
|
: {
|
|
'@context': SCHEMA_ORG_CONTEXT,
|
|
'@type': 'Offer'
|
|
}
|
|
|
|
if (lastClaimId) {
|
|
// this is an edit
|
|
vcClaim.lastClaimId = lastClaimId
|
|
delete vcClaim.identifier
|
|
}
|
|
|
|
vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined
|
|
vcClaim.recipient = toDid ? { identifier: toDid } : undefined
|
|
vcClaim.description = conditionDescription || undefined
|
|
|
|
vcClaim.includesObject =
|
|
amount && !isNaN(amount)
|
|
? { amountOfThisGood: amount, unitCode: unitCode || 'HUR' }
|
|
: undefined
|
|
|
|
if (itemDescription || fulfillsProjectHandleId) {
|
|
vcClaim.itemOffered = vcClaim.itemOffered || {}
|
|
vcClaim.itemOffered.description = itemDescription || undefined
|
|
if (fulfillsProjectHandleId) {
|
|
vcClaim.itemOffered.isPartOf = {
|
|
'@type': 'PlanAction',
|
|
identifier: fulfillsProjectHandleId
|
|
}
|
|
}
|
|
}
|
|
vcClaim.validThrough = validThrough || undefined
|
|
|
|
return vcClaim
|
|
}
|
|
|
|
/**
|
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
|
*
|
|
* @param identity
|
|
* @param description may be null
|
|
* @param amount may be null
|
|
* @param validThrough ISO 8601 date string YYYY-MM-DD (may be null)
|
|
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
|
*/
|
|
export async function createAndSubmitOffer(
|
|
axios: Axios,
|
|
apiServer: string,
|
|
issuerDid: string,
|
|
itemDescription: string,
|
|
amount?: number,
|
|
unitCode?: string,
|
|
conditionDescription?: string,
|
|
validThrough?: string,
|
|
recipientDid?: string,
|
|
fulfillsProjectHandleId?: string
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
const vcClaim = hydrateOffer(
|
|
undefined,
|
|
issuerDid,
|
|
recipientDid,
|
|
itemDescription,
|
|
amount,
|
|
unitCode,
|
|
conditionDescription,
|
|
fulfillsProjectHandleId,
|
|
validThrough,
|
|
undefined
|
|
)
|
|
return createAndSubmitClaim(
|
|
vcClaim as OfferVerifiableCredential,
|
|
issuerDid,
|
|
apiServer,
|
|
axios
|
|
)
|
|
}
|
|
|
|
export async function editAndSubmitOffer(
|
|
axios: Axios,
|
|
apiServer: string,
|
|
fullClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
|
issuerDid: string,
|
|
itemDescription: string,
|
|
amount?: number,
|
|
unitCode?: string,
|
|
conditionDescription?: string,
|
|
validThrough?: string,
|
|
recipientDid?: string,
|
|
fulfillsProjectHandleId?: string
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
const vcClaim = hydrateOffer(
|
|
fullClaim.claim,
|
|
issuerDid,
|
|
recipientDid,
|
|
itemDescription,
|
|
amount,
|
|
unitCode,
|
|
conditionDescription,
|
|
fulfillsProjectHandleId,
|
|
validThrough,
|
|
fullClaim.id
|
|
)
|
|
return createAndSubmitClaim(
|
|
vcClaim as OfferVerifiableCredential,
|
|
issuerDid,
|
|
apiServer,
|
|
axios
|
|
)
|
|
}
|
|
|
|
// similar logic is found in endorser-mobile
|
|
export const createAndSubmitConfirmation = async (
|
|
issuerDid: string,
|
|
claim: GenericVerifiableCredential,
|
|
lastClaimId: string, // used to set the lastClaimId
|
|
handleId: string | undefined,
|
|
apiServer: string,
|
|
axios: Axios
|
|
) => {
|
|
const goodClaim = removeSchemaContext(
|
|
removeVisibleToDids(
|
|
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId)
|
|
)
|
|
)
|
|
const confirmationClaim: GenericVerifiableCredential = {
|
|
'@context': SCHEMA_ORG_CONTEXT,
|
|
'@type': 'AgreeAction',
|
|
object: goodClaim
|
|
}
|
|
return createAndSubmitClaim(confirmationClaim, issuerDid, apiServer, axios)
|
|
}
|
|
|
|
export async function createAndSubmitClaim(
|
|
vcClaim: GenericVerifiableCredential,
|
|
issuerDid: string,
|
|
apiServer: string,
|
|
axios: Axios
|
|
): Promise<CreateAndSubmitClaimResult> {
|
|
try {
|
|
const vcPayload = {
|
|
vc: {
|
|
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
|
type: ['VerifiableCredential'],
|
|
credentialSubject: vcClaim
|
|
}
|
|
}
|
|
|
|
const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload)
|
|
|
|
// Make the xhr request payload
|
|
const payload = JSON.stringify({ jwtEncoded: vcJwt })
|
|
const url = `${apiServer}/api/v2/claim`
|
|
|
|
const response = await axios.post(url, payload, {
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
|
|
return { type: 'success', response }
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
logger.error('Error submitting claim:', error)
|
|
const errorMessage: string =
|
|
serverMessageForUser(error) ||
|
|
error.message ||
|
|
'Got some error submitting the claim. Check your permissions, network, and error logs.'
|
|
|
|
return {
|
|
type: 'error',
|
|
error: {
|
|
error: errorMessage
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function generateEndorserJwtUrlForAccount(
|
|
account: KeyMeta,
|
|
isRegistered: boolean,
|
|
givenName: string,
|
|
profileImageUrl: string,
|
|
isContact: boolean
|
|
) {
|
|
const publicKeyHex = account.publicKeyHex
|
|
const publicEncKey = Buffer.from(publicKeyHex, 'hex').toString('base64')
|
|
|
|
const contactInfo = {
|
|
iat: Date.now(),
|
|
iss: account.did,
|
|
own: {
|
|
did: account.did,
|
|
name: givenName ?? '',
|
|
publicEncKey,
|
|
registered: !!isRegistered
|
|
} as UserInfo
|
|
}
|
|
if (profileImageUrl) {
|
|
contactInfo.own.profileImageUrl = profileImageUrl
|
|
}
|
|
|
|
// Add the next key -- not recommended for the QR code for such a high resolution
|
|
if (isContact && account?.mnemonic && account?.derivationPath) {
|
|
const newDerivPath = nextDerivationPath(account.derivationPath as string)
|
|
const nextPublicHex = deriveAddress(
|
|
account.mnemonic as string,
|
|
newDerivPath
|
|
)[2]
|
|
const nextPublicEncKey = Buffer.from(nextPublicHex, 'hex')
|
|
const nextPublicEncKeyHash = sha256(nextPublicEncKey)
|
|
const nextPublicEncKeyHashBase64 =
|
|
Buffer.from(nextPublicEncKeyHash).toString('base64')
|
|
contactInfo.own.nextPublicEncKeyHash = nextPublicEncKeyHashBase64
|
|
}
|
|
|
|
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo)
|
|
|
|
const viewPrefix = APP_SERVER + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI
|
|
return viewPrefix + vcJwt
|
|
}
|
|
|
|
export async function createEndorserJwtForDid(
|
|
issuerDid: string,
|
|
payload: object,
|
|
expiresIn?: number
|
|
) {
|
|
const account = await retrieveFullyDecryptedAccount(issuerDid)
|
|
return createEndorserJwtForKey(account as KeyMeta, payload, expiresIn)
|
|
}
|
|
|
|
/**
|
|
* An AcceptAction is when someone accepts some contract or pledge.
|
|
*
|
|
* @param claim has properties '@context' & '@type'
|
|
* @return true if the claim is a schema.org AcceptAction
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export const isAccept = (claim: Record<string, any>) => {
|
|
return (
|
|
claim &&
|
|
claim['@context'] === SCHEMA_ORG_CONTEXT &&
|
|
claim['@type'] === 'AcceptAction'
|
|
)
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export const isOffer = (claim: Record<string, any>) => {
|
|
return (
|
|
claim &&
|
|
claim['@context'] === SCHEMA_ORG_CONTEXT &&
|
|
claim['@type'] === 'Offer'
|
|
)
|
|
}
|
|
|
|
export function currencyShortWordForCode(unitCode: string, single: boolean) {
|
|
return unitCode === 'HUR' ? (single ? 'hour' : 'hours') : unitCode
|
|
}
|
|
|
|
export function displayAmount(code: string, amt: number) {
|
|
return '' + amt + ' ' + currencyShortWordForCode(code, amt === 1)
|
|
}
|
|
|
|
// insert a space before any capital letters except the initial letter
|
|
// (and capitalize initial letter, just in case)
|
|
export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
|
|
return !text
|
|
? ''
|
|
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, ' $1')
|
|
}
|
|
|
|
/**
|
|
return readable summary of claim, or something generic
|
|
|
|
similar code is also contained in endorser-mobile
|
|
**/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const claimSummary = (
|
|
claim: GenericCredWrapper<GenericVerifiableCredential>
|
|
) => {
|
|
if (!claim) {
|
|
// to differentiate from "something" above
|
|
return 'something'
|
|
}
|
|
let specificClaim:
|
|
| GenericVerifiableCredential
|
|
| GenericCredWrapper<GenericVerifiableCredential> = claim
|
|
if (claim.claim) {
|
|
// probably a Verified Credential
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
specificClaim = claim.claim
|
|
}
|
|
if (Array.isArray(specificClaim)) {
|
|
if (specificClaim.length === 1) {
|
|
specificClaim = specificClaim[0]
|
|
} else {
|
|
return 'multiple claims'
|
|
}
|
|
}
|
|
const type = specificClaim['@type']
|
|
if (!type) {
|
|
return 'a claim'
|
|
} else {
|
|
let typeExpl = capitalizeAndInsertSpacesBeforeCaps(type)
|
|
if (typeExpl === 'Person') {
|
|
typeExpl += ' claim'
|
|
}
|
|
return 'a ' + typeExpl
|
|
}
|
|
}
|
|
|
|
/**
|
|
return readable description of claim if possible, as a past-tense action
|
|
|
|
identifiers is a list of objects with a 'did' field, each representing the user
|
|
contacts is a list of objects with a 'did' field for others and a 'name' field for their name
|
|
|
|
similar code is also contained in endorser-mobile
|
|
**/
|
|
export const claimSpecialDescription = (
|
|
record: GenericCredWrapper<GenericVerifiableCredential>,
|
|
activeDid: string,
|
|
identifiers: Array<string>,
|
|
contacts: Array<Contact>
|
|
) => {
|
|
let claim = record.claim
|
|
if (claim.claim) {
|
|
// it's probably a Verified Credential
|
|
claim = claim.claim
|
|
}
|
|
|
|
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts)
|
|
const type = claim['@type'] || 'UnknownType'
|
|
|
|
if (type === 'AgreeAction') {
|
|
return issuer + ' agreed with ' + claimSummary(claim.object)
|
|
} else if (isAccept(claim)) {
|
|
return issuer + ' accepted ' + claimSummary(claim.object)
|
|
} else if (type === 'GiveAction') {
|
|
// agent.did is for legacy data, before March 2023
|
|
const giver = claim.agent?.identifier || claim.agent?.did
|
|
const giverInfo = didInfo(giver, activeDid, identifiers, contacts)
|
|
let gaveAmount = claim.object?.amountOfThisGood
|
|
? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
|
: ''
|
|
if (claim.description) {
|
|
if (gaveAmount) {
|
|
gaveAmount = gaveAmount + ', and also: '
|
|
}
|
|
gaveAmount = gaveAmount + claim.description
|
|
}
|
|
if (!gaveAmount) {
|
|
gaveAmount = 'something not described'
|
|
}
|
|
// recipient.did is for legacy data, before March 2023
|
|
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did
|
|
const gaveRecipientInfo = gaveRecipientId
|
|
? ' to ' + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
|
|
: ''
|
|
return giverInfo + ' gave' + gaveRecipientInfo + ': ' + gaveAmount
|
|
} else if (type === 'JoinAction') {
|
|
// agent.did is for legacy data, before March 2023
|
|
const agent = claim.agent?.identifier || claim.agent?.did
|
|
const contactInfo = didInfo(agent, activeDid, identifiers, contacts)
|
|
|
|
let eventOrganizer =
|
|
claim.event && claim.event.organizer && claim.event.organizer.name
|
|
eventOrganizer = eventOrganizer || ''
|
|
let eventName = claim.event && claim.event.name
|
|
eventName = eventName ? ' ' + eventName : ''
|
|
let fullEvent = eventOrganizer + eventName
|
|
fullEvent = fullEvent ? ' attended the ' + fullEvent : ''
|
|
|
|
let eventDate = claim.event && claim.event.startTime
|
|
eventDate = eventDate ? ' at ' + eventDate : ''
|
|
return contactInfo + fullEvent + eventDate
|
|
} else if (isOffer(claim)) {
|
|
const offerer = claim.offeredBy?.identifier
|
|
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts)
|
|
let offering = ''
|
|
if (claim.includesObject) {
|
|
offering +=
|
|
' ' +
|
|
displayAmount(
|
|
claim.includesObject.unitCode,
|
|
claim.includesObject.amountOfThisGood
|
|
)
|
|
}
|
|
if (claim.itemOffered?.description) {
|
|
offering += ', saying: ' + claim.itemOffered?.description
|
|
}
|
|
// recipient.did is for legacy data, before March 2023
|
|
const offerRecipientId = claim.recipient?.identifier || claim.recipient?.did
|
|
const offerRecipientInfo = offerRecipientId
|
|
? ' to ' + didInfo(offerRecipientId, activeDid, identifiers, contacts)
|
|
: ''
|
|
return contactInfo + ' offered' + offering + offerRecipientInfo
|
|
} else if (type === 'PlanAction') {
|
|
const claimer = claim.agent?.identifier || record.issuer
|
|
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts)
|
|
return claimerInfo + ' announced a project: ' + claim.name
|
|
} else if (type === 'Tenure') {
|
|
// party.did is for legacy data, before March 2023
|
|
const claimer = claim.party?.identifier || claim.party?.did
|
|
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts)
|
|
const polygon = claim.spatialUnit?.geo?.polygon || ''
|
|
return (
|
|
contactInfo +
|
|
' possesses [' +
|
|
polygon.substring(0, polygon.indexOf(' ')) +
|
|
'...]'
|
|
)
|
|
} else {
|
|
return (
|
|
issuer +
|
|
' declared ' +
|
|
claimSummary(claim as GenericCredWrapper<GenericVerifiableCredential>)
|
|
)
|
|
}
|
|
}
|
|
|
|
export const BVC_MEETUPS_PROJECT_CLAIM_ID =
|
|
import.meta.env.VITE_BVC_MEETUPS_PROJECT_CLAIM_ID ||
|
|
'https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H' // production value, which seems like the safest value if forgotten
|
|
|
|
export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
|
|
return {
|
|
'@context': SCHEMA_ORG_CONTEXT,
|
|
'@type': 'JoinAction',
|
|
agent: {
|
|
identifier: did
|
|
},
|
|
event: {
|
|
organizer: {
|
|
name: 'Bountiful Voluntaryist Community'
|
|
},
|
|
name: 'Saturday Morning Meeting',
|
|
startTime: startTime
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function createEndorserJwtVcFromClaim(
|
|
issuerDid: string,
|
|
claim: object
|
|
) {
|
|
// Make a payload for the claim
|
|
const vcPayload = {
|
|
vc: {
|
|
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
|
type: ['VerifiableCredential'],
|
|
credentialSubject: claim
|
|
}
|
|
}
|
|
return createEndorserJwtForDid(issuerDid, vcPayload)
|
|
}
|
|
|
|
export async function createInviteJwt(
|
|
activeDid: string,
|
|
contact?: Contact,
|
|
inviteId?: string,
|
|
expiresIn?: number
|
|
): Promise<string> {
|
|
const vcClaim: RegisterVerifiableCredential = {
|
|
'@context': SCHEMA_ORG_CONTEXT,
|
|
'@type': 'RegisterAction',
|
|
agent: { identifier: activeDid },
|
|
object: SERVICE_ID
|
|
}
|
|
if (contact) {
|
|
vcClaim.participant = { identifier: contact.did }
|
|
}
|
|
if (inviteId) {
|
|
vcClaim.identifier = inviteId
|
|
}
|
|
// Make a payload for the claim
|
|
const vcPayload = {
|
|
vc: {
|
|
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
|
type: ['VerifiableCredential'],
|
|
credentialSubject: vcClaim
|
|
}
|
|
}
|
|
// Create a signature using private key of identity
|
|
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn)
|
|
return vcJwt
|
|
}
|
|
|
|
export async function register(
|
|
activeDid: string,
|
|
apiServer: string,
|
|
axios: Axios,
|
|
contact: Contact
|
|
): Promise<{ success?: boolean; error?: string }> {
|
|
const vcJwt = await createInviteJwt(activeDid, contact)
|
|
|
|
const url = apiServer + '/api/v2/claim'
|
|
const resp = await axios.post(url, { jwtEncoded: vcJwt })
|
|
if (resp.data?.success?.handleId) {
|
|
return { success: true }
|
|
} else if (resp.data?.success?.embeddedRecordError) {
|
|
let message =
|
|
'There was some problem with the registration and so it may not be complete.'
|
|
if (typeof resp.data.success.embeddedRecordError == 'string') {
|
|
message += ' ' + resp.data.success.embeddedRecordError
|
|
}
|
|
return { error: message }
|
|
} else {
|
|
logger.error(resp)
|
|
return { error: 'Got a server error when registering.' }
|
|
}
|
|
}
|
|
|
|
export async function setVisibilityUtil(
|
|
activeDid: string,
|
|
apiServer: string,
|
|
axios: Axios,
|
|
db: NonsensitiveDexie,
|
|
contact: Contact,
|
|
visibility: boolean
|
|
) {
|
|
if (!activeDid) {
|
|
return { error: 'Cannot set visibility without an identifier.' }
|
|
}
|
|
const url =
|
|
apiServer + '/api/report/' + (visibility ? 'canSeeMe' : 'cannotSeeMe')
|
|
const headers = await getHeaders(activeDid)
|
|
const payload = JSON.stringify({ did: contact.did })
|
|
|
|
try {
|
|
const resp = await axios.post(url, payload, { headers })
|
|
if (resp.status === 200) {
|
|
const success = resp.data.success
|
|
if (success) {
|
|
db.contacts.update(contact.did, { seesMe: visibility })
|
|
}
|
|
return { success }
|
|
} else {
|
|
logger.error(
|
|
'Got some bad server response when setting visibility: ',
|
|
resp.status,
|
|
resp
|
|
)
|
|
const message =
|
|
resp.data.error?.message || 'Got some error setting visibility.'
|
|
return { error: message }
|
|
}
|
|
} catch (err) {
|
|
logger.error('Got some error when setting visibility:', err)
|
|
return { error: 'Check connectivity and try again.' }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches rate limits from the Endorser server.
|
|
*
|
|
* @param apiServer endorser server URL string
|
|
* @param axios Axios instance
|
|
* @param {string} issuerDid - The DID for which to check rate limits.
|
|
* @returns {Promise<AxiosResponse>} The Axios response object.
|
|
*/
|
|
export async function fetchEndorserRateLimits(
|
|
apiServer: string,
|
|
axios: Axios,
|
|
issuerDid: string
|
|
) {
|
|
const url = `${apiServer}/api/report/rateLimits`
|
|
const headers = await getHeaders(issuerDid)
|
|
return await axios.get(url, { headers } as AxiosRequestConfig)
|
|
}
|
|
|
|
/**
|
|
* Fetches rate limits from the image server.
|
|
*
|
|
* @param apiServer image server URL string
|
|
* @param axios Axios instance
|
|
* @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'
|
|
const headers = await getHeaders(issuerDid)
|
|
return await axios.get(url, { headers } as AxiosRequestConfig)
|
|
}
|